diff --git a/.context/retros/2026-03-22-1.json b/.context/retros/2026-03-22-1.json new file mode 100644 index 0000000..b71bad6 --- /dev/null +++ b/.context/retros/2026-03-22-1.json @@ -0,0 +1,60 @@ +{ + "date": "2026-03-22", + "window": "7d", + "metrics": { + "commits": 23, + "contributors": 1, + "prs_merged": 0, + "insertions": 17007, + "deletions": 504, + "net_loc": 16503, + "test_loc": 415, + "test_ratio": 0.024, + "active_days": 3, + "sessions": 9, + "deep_sessions": 1, + "avg_session_minutes": 16, + "loc_per_session_hour": 5500, + "feat_pct": 0.39, + "fix_pct": 0.43, + "docs_pct": 0.13, + "peak_hour": 20, + "ai_assisted_commits": 19 + }, + "authors": { + "shenxianpeng": { + "commits": 23, + "insertions": 17007, + "deletions": 504, + "test_ratio": 0.024, + "top_area": "website/" + } + }, + "version_range": ["initial", "initial"], + "streak_days": 3, + "tweetable": "Week of Mar 15: 23 commits, 17k LOC, 3 active days, 9 sessions | Streak: 3d | 83% AI-assisted", + "backlog": { + "total_open": 9, + "p0_p1": 0, + "p2": 1, + "p3": 1, + "completed_this_period": 0, + "added_this_period": 2 + }, + "test_health": { + "total_test_files": 10, + "tests_added_this_period": 0, + "regression_test_commits": 0, + "test_files_changed": 0 + }, + "skill_usage": { + "office-hours": 6, + "plan-eng-review": 3, + "plan-ceo-review": 2, + "qa": 2, + "ship": 1, + "document-release": 1, + "design-review": 1, + "retro": 1 + } +} diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 5894d73..40e8d6f 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -1,8 +1,11 @@ # CAST — Deploy official website to GitHub Pages # # Triggers on push to main (website/ or docs/ changes). -# Builds HTML from Markdown sources, then serves website/ at -# https://castops.github.io/cast/ +# Builds HTML from Markdown sources, then pushes website/ to the +# gh-pages branch. PR preview subdirectories (pr-*/) are preserved. +# +# Pages source must be set to: Deploy from branch → gh-pages → / (root) +# Repo Settings → Pages → Source → Deploy from a branch name: Deploy Website @@ -15,21 +18,16 @@ on: workflow_dispatch: permissions: - contents: read - pages: write - id-token: write + contents: write concurrency: - group: pages + group: pages-main cancel-in-progress: false jobs: deploy: name: Deploy to GitHub Pages runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} steps: - uses: actions/checkout@v4 @@ -44,14 +42,11 @@ jobs: - name: Build docs HTML from Markdown run: python scripts/build_docs.py - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + - name: Deploy to gh-pages branch + uses: JamesIves/github-pages-deploy-action@v4 with: - path: website/ - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + folder: website + branch: gh-pages + clean: true + clean-exclude: | + pr-* diff --git a/.github/workflows/devsecops.yml b/.github/workflows/devsecops.yml index f7f3089..51e0f0f 100644 --- a/.github/workflows/devsecops.yml +++ b/.github/workflows/devsecops.yml @@ -80,7 +80,10 @@ jobs: - name: pip-audit run: | pip install pip-audit - pip-audit + pip install -e ".[dev]" + # CVE-2026-4539: ReDoS in pygments AdlLexer — no fix released yet + # (https://github.com/advisories/GHSA-5239-wwwm-4pmq); re-enable once patched + pip-audit --ignore-vuln CVE-2026-4539 # ── 4. Container Security ────────────────────────────────────────────────── container: diff --git a/.github/workflows/pr-preview-cleanup.yml b/.github/workflows/pr-preview-cleanup.yml new file mode 100644 index 0000000..11d8f7a --- /dev/null +++ b/.github/workflows/pr-preview-cleanup.yml @@ -0,0 +1,38 @@ +# CAST — PR Preview Cleanup +# +# Removes the pr-{number}/ subdirectory from the gh-pages branch +# when a pull request is closed (merged or dismissed). + +name: PR Preview Cleanup + +on: + pull_request: + types: [closed] + +permissions: + contents: write + +jobs: + cleanup: + name: Remove PR preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 1 + + - name: Remove preview directory for PR #${{ github.event.number }} + env: + PR_NUM: ${{ github.event.number }} + run: | + if [ -d "pr-${PR_NUM}" ]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git rm -rf "pr-${PR_NUM}" + git commit -m "chore: remove preview for PR #${PR_NUM}" + git push + echo "Removed pr-${PR_NUM}/" + else + echo "No preview directory found for PR #${PR_NUM} — nothing to clean up." + fi diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..691b9ab --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,83 @@ +# CAST — PR Preview Deployment +# +# Deploys a preview of the website to the gh-pages branch under +# pr-{number}/ on every push to a pull request. +# Posts a comment with the preview URL (updates existing comment on re-push). +# Cleaned up automatically when the PR is closed. + +name: PR Preview + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'website/**' + - 'docs/**' + +permissions: + contents: write + pull-requests: write + +concurrency: + group: pr-preview-${{ github.event.number }} + cancel-in-progress: true + +jobs: + preview: + name: Deploy PR preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Markdown library + run: pip install markdown + + - name: Build docs HTML from Markdown + run: python scripts/build_docs.py + + - name: Deploy to gh-pages/pr-${{ github.event.number }}/ + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: website + branch: gh-pages + target-folder: pr-${{ github.event.number }} + clean: true + + - name: Post preview URL comment + uses: actions/github-script@v7 + with: + script: | + const pr = context.issue.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + const base = `https://${owner}.github.io/${repo}/pr-${pr}`; + const sha = context.payload.pull_request.head.sha.slice(0, 7); + + const body = [ + `## 🔍 Preview ready`, + ``, + `| Page | URL |`, + `|------|-----|`, + `| EN homepage | [${base}/](${base}/) |`, + `| ZH homepage | [${base}/zh/](${base}/zh/) |`, + `| Docs (EN) | [${base}/docs/getting-started.html](${base}/docs/getting-started.html) |`, + `| Docs (ZH) | [${base}/zh/docs/getting-started.html](${base}/zh/docs/getting-started.html) |`, + ``, + `> Built from \`${sha}\`. Updates on every push. Removed when PR is closed.`, + ].join('\n'); + + const comments = await github.rest.issues.listComments({ owner, repo, issue_number: pr }); + const existing = comments.data.find( + c => c.user.login === 'github-actions[bot]' && c.body.includes('Preview ready') + ); + + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number: pr, body }); + } diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f28b7..e07fec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,37 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +## [0.1.1] — 2026-03-24 + +### Changed + +- **Complete website redesign (EN + ZH)** — redesigned `website/index.html` and + `website/zh/index.html` to the Industrial Editorial design system defined in `DESIGN.md`: + - Display font: Fraunces 900 (headlines/hero) with Instrument Sans body and IBM Plex Sans + Condensed labels + - Color palette: warm near-black `#0D0C0B` background, electric chartreuse `#CBFF2E` + accent, warm off-white `#EDE9E3` text + - 55/45 left-anchored hero grid with diagonal structural line + - Operator-console section labels (uppercase, IBM Plex Sans Condensed, chartreuse hairline) + - Replaced GitHub-ish blue/gray palette with Industrial Editorial tokens +- **Website navigation and link fixes:** + - Fixed root-relative hrefs (`/`, `/index.html`) → relative paths (`./`) for correct + GitHub Pages project-site routing + - Fixed broken `../docs/` links → rendered HTML docs at `docs/getting-started.html` + and `docs/plugin-guide.html` (built by `scripts/build_docs.py` at deploy time) + - Unified GitHub repo references to `castops/cast-cli` in both language variants + - Added `target="_blank" rel="noopener noreferrer"` to all external links in both files +- **Website SEO and meta improvements:** + - Added `og:title`, `og:description`, `og:type` Open Graph tags to EN page + - Fixed stale `og:title` mismatch with `` in EN page + - Added `<meta name="theme-color" content="#0D0C0B">` to both pages + - Added `<link rel="canonical">` to both pages + - Added `hreflang` alternate link tags (`en` / `zh-CN`) to both pages + - Fixed `preconnect` hint to add missing `crossorigin` attribute on `fonts.googleapis.com` + - Fixed EN nav `color-mix()` background → `rgba()` with `-webkit-backdrop-filter` for + Safari/WebView/enterprise browser compatibility + - Added mobile responsive nav collapse at 900px breakpoint to ZH page + ### Added - **Node.js and Go GitHub Actions templates** — production-ready pipelines for both stacks: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..74ec2a0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,15 @@ +# CLAUDE.md — CastOps Project Instructions + +## Design System +Always read `DESIGN.md` before making any visual or UI decisions. +All font choices, colors, spacing, border-radius, and aesthetic direction are defined there. +Do not deviate without explicit user approval. + +Key rules: +- **Display font:** Fraunces (serif) — never substitute with Inter, Roboto, or other grotesks without approval +- **Accent color:** `#CBFF2E` (electric chartreuse) — use sparingly; this is a high-value signal color, not a background fill +- **Background:** `#0D0C0B` (warm near-black) — not cool gray, not pure black +- **Aesthetic:** Industrial Editorial — asymmetric, left-anchored, operator-console energy +- **Never:** purple gradients, 3-column icon grids, centered hero with floating CTA, glassmorphism, decorative blobs + +In QA mode, flag any code that doesn't match `DESIGN.md` — including font substitutions, off-palette colors, or layout patterns that conflict with the defined aesthetic. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..912761c --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,113 @@ +# Design System — CastOps + +## Product Context +- **What this is:** An open-source platform engineering suite for the AI-native era, comprising Cast CLI (DevSecOps governance) and Cast Slice (GPU FinOps engine for Kubernetes) +- **Who it's for:** Platform engineers, DevOps/MLOps engineers, and AI infrastructure teams — technical, opinionated people who run clusters and care deeply about operational efficiency +- **Space/industry:** DevOps / platform tooling; peers include Pulumi, Railway, Fly.io, Grafana, Warp +- **Project type:** Marketing/documentation website +- **License:** Apache 2.0 (open source) + +## Aesthetic Direction +- **Direction:** Industrial Editorial +- **Decoration level:** Intentional — signal lines, grid fragments, annotated diagrams, structural rule marks +- **Mood:** An operations room designed by someone who also reads *Monocle*. Not glassy, not neon. Machined surfaces, operator confidence, the energy of a flight operations center that happens to be beautifully typeset. Every design choice signals: "these people run clusters and have taste." +- **Anti-patterns (never use):** Purple/violet gradients, 3-column icon feature grids, centered-everything hero layouts, generic stock developer illustrations, glassmorphism cards, decorative blobs, gradient buttons as primary CTA, fake dashboards full of meaningless charts + +## Typography + +- **Display/Hero:** [Fraunces](https://fonts.google.com/specimen/Fraunces) — high-contrast editorial serif; in a zero-serif DevOps category, this creates instant visual differentiation and signals authority before a word is read. Use at 72–100px, -0.03em letter-spacing, weight 900 +- **Body:** [Instrument Sans](https://fonts.google.com/specimen/Instrument+Sans) — technical clarity with more character than default SaaS grotesks; 16–18px, 1.65 line-height +- **UI/Labels:** Instrument Sans (same as body), 13–14px, weight 600, uppercase with 0.06em tracking for labels +- **Data/Tables:** [IBM Plex Sans Condensed](https://fonts.google.com/specimen/IBM+Plex+Sans+Condensed) — compact, operational, excellent for dense infrastructure content; must use `font-variant-numeric: tabular-nums` for all numeric values +- **Code/Terminal:** [Berkeley Mono](https://berkeleygraphics.com/typefaces/berkeley-mono/) — the best monospace typeface of the last decade; has weight and personality. Requires a commercial license. Use [JetBrains Mono](https://fonts.google.com/specimen/JetBrains+Mono) as a free fallback +- **Loading:** Google Fonts CDN for Fraunces, Instrument Sans, IBM Plex Sans Condensed, JetBrains Mono. Berkeley Mono self-hosted after licensing. +- **Scale:** + | Level | Size | Usage | + |-------|------|-------| + | display-2xl | 88–100px | Hero headline (Fraunces) | + | display-xl | 64–88px | Section openers (Fraunces) | + | display-lg | 48–64px | Major subheads (Fraunces) | + | display-md | 32–48px | Card titles (Fraunces) | + | body-lg | 18px | Marketing prose (Instrument Sans) | + | body-md | 16px | Docs body (Instrument Sans) | + | body-sm | 14px | Secondary descriptions (Instrument Sans) | + | label | 11–12px | Uppercase labels (IBM Plex Sans Condensed, weight 700, 0.1em tracking) | + | code | 12.5–14px | Terminal / config blocks (Berkeley Mono / JetBrains Mono) | + +## Color + +- **Approach:** Restrained — the accent is rare and high-value; most of the site lives in graphite and ash tones +- **Background:** `#0D0C0B` — near-black with warmth stripped of blue; feels like matte metal, not a generic dark-mode SaaS clone +- **Surface:** `#171614` — for cards, panels, sidebars, terminal backgrounds +- **Surface Elevated:** `#1F1D1B` — for dropdowns, tooltips, modal backgrounds +- **Border/Hairline:** `#2A2723` — just enough to see the edge without a bright line +- **Primary Text:** `#EDE9E3` — warm off-white; pure white on this background is aggressive and cheap +- **Secondary Text:** `#B5AFA8` — for descriptions, supporting copy +- **Muted Text:** `#7A736C` — for metadata, timestamps, labels, placeholder text +- **Primary Accent:** `#CBFF2E` — electric chartreuse; the color of a GPU under load on a thermal camera. Nothing in DevOps tooling uses this color. Reserve for: CTAs, active states, key metrics, hover highlights, the single brand moment per section +- **Secondary Accent:** `#E87C3E` — construction/engineering orange; for warnings, cost anomalies, "this matters right now" states +- **Semantic:** + | Role | Hex | Usage | + |------|-----|-------| + | Success | `#7DFA9B` | Passing checks, healthy nodes, cost savings | + | Warning | `#FFBF47` | Idle nodes, policy warnings, approaching limits | + | Danger | `#FF6B57` | Failed checks, blocked deployments, critical alerts | + | Info | `#63C7FF` | Informational banners, doc notes | +- **Dark mode:** This IS the dark mode. The base palette is dark. +- **Light mode:** Invert to warm off-white base (`#F5F3EF`), surface white (`#FFFFFF`), adjust accent to `#6B8B00` (darker chartreuse for contrast on light). Keep the same type system. + +## Spacing + +- **Base unit:** 8px +- **Density:** Comfortable-dense — platform engineers can handle information density; sparse layouts signal "made for non-technical stakeholders" +- **Scale:** + | Token | Value | Usage | + |-------|-------|-------| + | 2xs | 2px | Micro gaps, hairlines | + | xs | 4px | Icon-to-label, tight pairs | + | sm | 8px | Component internal padding | + | md | 16px | Standard padding, card internal | + | lg | 24px | Section-internal gaps | + | xl | 32px | Between subsections | + | 2xl | 48px | Between major sections | + | 3xl | 64px | Hero padding, section separation | + | 4xl | 96px | Page-level section padding | + +## Layout + +- **Approach:** Creative-editorial — left-anchored, asymmetric composition; the hero must not look like a document +- **Grid:** 12-column; max content width 1160px; 40px gutters on desktop, 20px on mobile +- **Hero composition:** Left 55% carries the headline (high-left anchor, not centered) and CTA row; right 45% carries a live product tableau (terminal + pipeline visualization). A diagonal structural line or clipped panel edge cuts through for motion. +- **Section rhythm:** Alternate dense content blocks with more open ones. Mix editorial prose blocks with operator-console data modules. +- **Max content width:** 1160px +- **Border radius:** + | Token | Value | Usage | + |-------|-------|-------| + | sm | 4px | Buttons, tags, code tokens | + | md | 8px | Cards, terminal windows, inputs | + | lg | 12px | Modals, dialogs | + | full | 9999px | Pills, avatars | + +## Motion + +- **Approach:** Intentional — only transitions that aid comprehension; no decorative scroll animations +- **Easing:** enter: `ease-out` / exit: `ease-in` / move: `ease-in-out` +- **Duration:** + | Token | Range | Usage | + |-------|-------|-------| + | micro | 50–100ms | Hover states, focus rings | + | short | 150–250ms | Button feedback, menu open | + | medium | 250–400ms | Panel transitions, accordion | + | long | 400–700ms | Page transitions, terminal line animation | +- **Permitted motion:** Terminal line input animation, number counter rollup on stats, status indicator pulse for active nodes +- **Forbidden motion:** Scroll-triggered entrance animations on marketing copy, parallax backgrounds, looping decorative animations + +## Decisions Log + +| Date | Decision | Rationale | +|------|----------|-----------| +| 2026-03-24 | Initial design system created | Created by /design-consultation. Research covered Railway, Fly.io, Pulumi, Warp, and Grafana. Outside voices: Codex (GPT-5.4) and Claude subagent both independently converged on industrial/editorial direction and chartreuse accent. | +| 2026-03-24 | Fraunces as display font (Risk #1) | Every competitor uses grotesque sans-serifs. In a zero-serif category, editorial serif = instant visual differentiation and psychological authority before a word is read. | +| 2026-03-24 | Accent color #CBFF2E — electric chartreuse (Risk #2) | GPU thermal imaging color. Not blue, not purple, not teal. Nothing in DevOps tooling looks like this. Must be used sparingly (rare = valuable). | +| 2026-03-24 | Information-dense layout (Risk #3) | Target users are platform engineers who handle data density daily. Sparse layouts pre-screen for the wrong audience. | +| 2026-03-24 | Background warm near-black #0D0C0B instead of cool graphite | Cool gray (#0B0F10) reads as generic dark-mode SaaS. Warm near-black feels like matte metal — more physical, more material. | diff --git a/TODOS.md b/TODOS.md index d7fbbb0..629dd89 100644 --- a/TODOS.md +++ b/TODOS.md @@ -2,14 +2,6 @@ ## Design -### Create DESIGN.md -**What:** Run `/design-consultation` to document the dashboard's design system. -**Why:** `dashboard/template.html` uses GitHub-dark CSS tokens (`#0d1117`, `#58a6ff`, etc.) but this palette is implicit and undocumented. Without a reference, future contributors may introduce inconsistent colors. -**Pros:** Prevents design drift; gives future contributors a reference for new UI additions (e.g., multi-repo dashboard, dark/light toggle). -**Cons:** Minor time investment (~15 min). -**Context:** Design tokens live in the `:root` block of `dashboard/template.html`. The implicit system already has semantic naming (--critical-bg, --pass-bg, --high-bg) — DESIGN.md would formalize and explain these choices. -**Depends on:** None. - ## Accessibility ### Verify color contrast ratios meet WCAG AA @@ -114,3 +106,13 @@ **Context:** 存储方案候选:项目根目录 `.cast-score.json`(与项目绑定)vs `~/.cast/{repo-slug}.json`(全局)。需要在 Phase 2 设计时决定,避免后期迁移。 **Priority:** P2 — Phase 2 后续迭代。 **Depends on:** cast score (Phase 2) 基础实施完成。 + +## Completed + +### Create DESIGN.md +**What:** Run `/design-consultation` to document the project's design system (Industrial Editorial aesthetic). +**Completed:** v0.1.1 (2026-03-24) — `DESIGN.md` created with full token set: Fraunces/Instrument Sans typography, `#0D0C0B`/`#CBFF2E` palette, spacing scale, and layout rules. + +### Redesign website (EN + ZH) per DESIGN.md +**What:** Apply the Industrial Editorial design system to `website/index.html` and `website/zh/index.html`. +**Completed:** v0.1.1 (2026-03-24) — Both pages fully redesigned. Fixed navigation links, SEO meta tags, cross-browser compat, and mobile responsive nav. diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fad7e57 --- /dev/null +++ b/uv.lock @@ -0,0 +1,344 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "castops" +source = { editable = "." } +dependencies = [ + { name = "rich" }, + { name = "typer", version = "0.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typer", version = "0.24.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, + { name = "rich", specifier = ">=13" }, + { name = "typer", specifier = ">=0.12" }, +] +provides-extras = ["dev"] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typer" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "rich", marker = "python_full_version < '3.10'" }, + { name = "shellingham", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/93d16574e66dfe4c2284ffdaca4b0320ade32858cb2cc586c8dd79f127c5/typer-0.23.2.tar.gz", hash = "sha256:a99706a08e54f1aef8bb6a8611503808188a4092808e86addff1828a208af0de", size = 120162, upload-time = "2026-02-16T18:52:40.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2c/dee705c427875402200fe779eb8a3c00ccb349471172c41178336e9599cc/typer-0.23.2-py3-none-any.whl", hash = "sha256:e9c8dc380f82450b3c851a9b9d5a0edf95d1d6456ae70c517d8b06a50c7a9978", size = 56834, upload-time = "2026-02-16T18:52:39.308Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "shellingham", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/website/index.html b/website/index.html index b8f60c9..aeb4ace 100644 --- a/website/index.html +++ b/website/index.html @@ -3,777 +3,1117 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>CAST — Production-Grade DevSecOps Pipeline - - + CAST — DevSecOps pipeline governance, one command + + + + + + + + + - - - -
-
+ +
+
+
- - Open source · Apache 2.0 + + Open source · Apache 2.0
-

+

One engineer's standards.
- Every team's pipeline. + Every team's pipeline.

- Enforce your DevSecOps standards across every repository — secrets scanning, - SAST, SCA, container security, and a policy-as-code gate. GitHub Actions and - GitLab CI. One command to start. + DevSecOps governance that enforces secrets detection, SAST, SCA, container security, and code quality — in every CI run, without hunting for yet another tool.

- -
-
- - - - bash -
-
-
$ pip install castops
-
Collecting castops… ✓
-
 
-
$ cast init
-
🔍 Detected: Python · GitHub Actions
-
 
-
Writing .github/workflows/devsecops.yml
-
 
-
✓ Secrets Detection Gitleaks
-
✓ SAST Semgrep
-
✓ SCA pip-audit
-
✓ Container Security Trivy
-
✓ Code Quality Ruff
-
✓ Security Gate conftest + OPA
-
 
-
🛡️ Pipeline installed. Push to activate.
+
+
+
- -
+ +
+
-
-
6
+
+
6
Security layers
-
-
3
+
+
3
Languages
-
-
2
+
+
2
CI platforms
-
-
1 command
-
to install
+
+
1
+
Command
-
-
0
+
+
0
External accounts
+
- -
-
- -

Every layer of the security stack.
Out of the box.

-

Each CAST pipeline runs 5 parallel jobs, followed by a policy-as-code gate that controls whether a pull request can merge.

- -
-
-
🔑
-
Secrets Detection
-
Powered by Gitleaks
-
Scans your entire git history for leaked credentials, API keys, and tokens — not just the latest commit.
-
-
-
🔍
-
SAST
-
Powered by Semgrep
-
Finds security bugs and anti-patterns in your source code using 1,000+ open-source rules, with SARIF upload to GitHub Security.
-
-
-
📦
-
SCA
-
pip-audit · npm audit · govulncheck
-
Detects known CVEs in your dependencies. Stack-appropriate tool is selected automatically.
-
-
-
🐳
-
Container Security
-
Powered by Trivy
-
Scans your Docker image for OS and library CVEs. Skipped automatically if no Dockerfile is present.
-
-
-
✏️
-
Code Quality
-
Ruff · ESLint · staticcheck
-
Enforces code style and quality standards. Informational — does not block merges by default.
-
-
-
🚦
-
Security Gate
-
Powered by conftest + OPA Rego
-
Evaluates all SARIF findings against versioned policies. Blocks merges on critical findings. Policies live in your repo.
-
-
+ +
+
+
+ +

+ Every layer of the security stack.
Out of the box. +

-
- - -
-
- -

From zero to production pipeline
in under five minutes.

- -
-
-
1
-
Install the CLI
-
One pip install. Works with Python 3.9+. No external accounts or API tokens required.
-
pip install castops
-
-
-
2
-
Run cast init
-
CAST detects your stack and CI platform, then writes the workflow file to the right place.
-
cast init
-
-
-
3
-
Push and ship
-
Every push and pull request now triggers the full security pipeline. Findings block unsafe merges.
-
git push
-
+
+ +
+ Layer 1 +
Secrets Detection
+
via Gitleaks
+
+ +
+ Layer 2 +
SAST
+
via Semgrep
+
+ +
+ Layer 3 +
SCA — Dependency Audit
+
via pip-audit · npm audit · govulncheck
+
+ +
+ Layer 4 +
Container Security
+
via Trivy
+
+ +
+ Layer 5 +
Code Quality
+
via Ruff · ESLint · staticcheck
+ +
+ Gate +
Security Gate
+
via conftest + OPA Rego
+
+ +
+
+
+ + +
+
+
+ +

Three commands to governed pipelines.

-
- - -
-
- -

Every stack. Both platforms.

-

All templates include the full 6-layer security stack and are tested on every CAST release.

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
StackSCA ToolQuality Tool - GitHub Actions - - GitLab CI -
-
🐍 Python
-
pyproject.toml · requirements.txt · setup.py
-
pip-auditRuff
-
⬡ Node.js
-
package.json
-
npm auditESLint
-
🐹 Go
-
go.mod
-
govulncheckstaticcheck
+
+ +
+
01
+
Install
+
Add CAST to your environment. No accounts, no SaaS sign-ups, no phone-home telemetry.
+
$ pip install castops
+
+ +
+
02
+
Init
+
CAST detects your stack and scaffolds a pipeline config tuned to your language and CI platform.
+
$ cast init
+
+ +
+
03
+
Push
+
All six security layers run on every push. Failing the gate blocks the merge. No configuration drift.
+
$ git push
+
-
- - -
-
-
- -

Bring your own tools.

-

- CAST's gate speaks SARIF — the universal security findings format. - Any tool that outputs SARIF slots into the pipeline automatically. - No forks. No rewrites. -

-

- Replace pip-audit with Snyk. Add Bandit alongside Semgrep. - Bring your internal compliance scanner. The gate evaluates everything. +

+
+ + +
+
+
+ +

Works where you already work.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageSCA ToolQuality ToolGitHub ActionsGitLab CI
Pythonpip-auditRuff
Node.jsnpm auditESLint
Gogovulncheckstaticcheck
+
+
+ + +
+
+
+
+ +

Your tools. Your rules.

+

+ CAST is a governance layer, not a walled garden. Drop in any tool you already trust — Snyk, Checkov, custom OPA policies — by adding a YAML plugin definition. No patching the core.

- +
    +
  • Drop-in YAML plugin definitions
  • +
  • Any CLI tool becomes a security layer
  • +
  • Custom OPA Rego policies for your gate
  • +
  • Works alongside existing CI steps
  • +
+
+
+ + plugins/custom-snyk.yml +
+
# Custom Snyk plugin for CAST
+name: custom-snyk
+layer: sca
+enabled: true
 
-      
-
custom-snyk.yml — drop alongside CAST workflow
-
jobs:
-  snyk:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v4
-      - run: snyk test --sarif-file-output=snyk.sarif
-        env:
-          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
-      - uses: actions/upload-artifact@v4
-        with:
-          # CAST gate picks up any cast-sarif-* artifact
-          name: cast-sarif-snyk
-          path: snyk.sarif
+run: + command: snyk test --severity-threshold=high + fail_on: nonzero_exit + +report: + format: sarif + output: snyk-results.sarif + +gate: + policy: policy/snyk.rego + block_merge: true
-
- - -
-

Ship secure code, starting today.

-

One command. No external accounts. No configuration required.

-
+
- - +
+ + + + - + \ No newline at end of file diff --git a/website/zh/index.html b/website/zh/index.html index 748001a..89d987a 100644 --- a/website/zh/index.html +++ b/website/zh/index.html @@ -4,310 +4,683 @@ CAST — DevSecOps 标准,规模化覆盖每个团队 - + - + - @@ -316,20 +689,21 @@ -
-
-
- - 开源 · Apache 2.0 -
-

- 一个工程师的标准,
- 覆盖每个团队的流水线。 -

-

- 让你的 DevSecOps 规范自动执行于所有仓库——秘密扫描、SAST、SCA、容器安全、 - 策略即代码门禁,支持 Python、Node.js 和 Go,兼容 GitHub Actions 与 GitLab CI。 -

-
- 快速开始 → - - - 查看源码 - +
+
+
+
+ + 开源 · Apache 2.0 +
+

+ 一个工程师的标准,覆盖每个团队的流水线。 +

+

+ 让你的 DevSecOps 规范自动执行于所有仓库——秘密扫描、SAST、SCA、容器安全、策略即代码门禁,支持 Python、Node.js 和 Go,兼容 GitHub Actions 与 GitLab CI。 +

+
-
-
-
- - - - bash -
-
-
$ pip install castops
-
正在安装 castops… ✓
-
 
-
$ cast init
-
🔍 检测到:Python · GitHub Actions
-
 
-
写入 .github/workflows/devsecops.yml
-
 
-
✓ 秘密检测 Gitleaks
-
✓ SAST Semgrep
-
✓ SCA pip-audit
-
✓ 容器安全 Trivy
-
✓ 代码质量 Ruff
-
✓ 安全门禁 conftest + OPA
-
 
-
🛡️ 流水线已就绪。推送代码即可激活。
+
+
+ + + + bash — ~/project +
+
+
$ pip install castops
+
正在安装 castops… 
+
 
+
$ cast init
+
🔍 检测到:Python · GitHub Actions
+
 
+
写入 .github/workflows/devsecops.yml
+
 
+
✓ 秘密检测     Gitleaks
+
✓ SAST 静态分析  Semgrep
+
✓ SCA pip-audit
+
✓ 容器安全     Trivy
+
✓ 代码质量     Ruff
+
✓ 安全门禁     conftest + OPA
+
 
+
🛡  流水线已就绪。推送代码即可激活。
+
@@ -405,7 +781,7 @@

CI 平台

-
1 条命令
+
1条命令
完成安装
@@ -419,45 +795,45 @@

-

完整的安全技术栈。
开箱即用。

+

完整的安全技术栈。开箱即用。

每套 CAST 模板并行运行 5 个安全 Job,最后由策略即代码门禁决定 PR 能否合并。

🔑
秘密检测
-
基于 Gitleaks
-
扫描完整的 git 历史记录,检测泄露的凭证、API 密钥和 Token,而不仅仅是最新提交。
+
Gitleaks 驱动
+
扫描完整 git 历史中泄露的凭据、API 密钥和令牌——不仅限于最新提交。
🔍
SAST 静态分析
-
基于 Semgrep
-
使用 1000+ 开源规则在源码中发现安全漏洞和不安全模式,SARIF 结果自动上传至 GitHub Security。
+
Semgrep 驱动
+
使用 1,000+ 开源规则检测源码中的安全漏洞和反模式,并将 SARIF 上传至 GitHub Security。
📦
SCA 依赖审计
pip-audit · npm audit · govulncheck
-
检测依赖库中的已知 CVE。根据技术栈自动选择合适的工具。
+
检测依赖项中的已知 CVE,自动选择适合当前技术栈的工具。
🐳
容器安全
-
基于 Trivy
-
扫描 Docker 镜像中的操作系统和软件库 CVE。未检测到 Dockerfile 时自动跳过。
+
Trivy 驱动
+
扫描 Docker 镜像的操作系统和依赖库 CVE。若无 Dockerfile 则自动跳过。
✏️
代码质量
Ruff · ESLint · staticcheck
-
强制执行代码风格和质量标准。默认为信息性检查,不阻塞合并。
+
强制执行代码风格与质量标准。默认为信息模式,不阻断合并。
🚦
安全门禁
-
基于 conftest + OPA Rego
-
对所有 SARIF 结果执行版本化策略评估。发现 CRITICAL 漏洞时阻止合并。策略文件随仓库版本化。
+
conftest + OPA Rego 驱动
+
根据版本化策略评估所有 SARIF 发现。发现严重漏洞时阻断合并。策略文件存放于你的仓库。
@@ -467,25 +843,25 @@

完整的安全技术栈。
开箱即用。

-

从零到生产级流水线,
五分钟以内。

+

从零到生产级流水线,五分钟以内。

1
安装 CLI
-
一条 pip 命令。支持 Python 3.9+。无需外部账号或 API Token。
+
一条 pip install 命令。支持 Python 3.9+。无需任何外部账号或 API 令牌。
pip install castops
2
执行 cast init
-
CAST 自动识别你的技术栈和 CI 平台,将工作流文件写入正确路径。
+
CAST 自动检测你的技术栈和 CI 平台,将工作流文件写入正确位置。
cast init
3
推送代码
-
此后每次 push 和 PR 都会触发完整的安全流水线,存在风险时阻止不安全的合并。
+
每次推送和 Pull Request 都将触发完整安全流水线。发现高危漏洞时阻断不安全的合并。
git push
@@ -497,7 +873,7 @@

从零到生产级流水线,
五分钟以内。

每种技术栈。两个平台。

-

所有模板均包含完整的 6 层安全技术栈,并在每次 CAST 发布时经过测试验证。

+

所有模板均包含完整的 6 层安全技术栈,并在每次 CAST 发版时进行测试。

@@ -553,16 +929,13 @@

每种技术栈。两个平台。

接入你自己的工具。

-

- CAST 的门禁使用 SARIF 作为统一接口。 - 任何能输出 SARIF 文件的工具都可以自动接入流水线。 - 无需 fork,无需改动现有配置。 +

+ CAST 的门禁使用 SARIF——通用安全发现格式。任何输出 SARIF 的工具都能自动接入流水线。无需 fork,无需重写。

-

- 用 Snyk 替换 pip-audit,在 Semgrep 旁边加入 Bandit, - 接入内部合规扫描器 —— 门禁会自动评估所有结果。 +

+ 用 Snyk 替换 pip-audit。在 Semgrep 旁边加上 Bandit。接入你们内部的合规扫描器。门禁会统一评估所有结果。

- @@ -588,31 +961,37 @@

接入你自己的工具。

-

从今天开始,交付安全的代码。

-

一条命令。无需外部账号。零配置。

-
- 快速开始 → - - - 在 GitHub 上 Star - +
+
+

从今天开始,交付安全的代码。

+

一条命令。无需外部账号。零配置。

+
+
- + \ No newline at end of file