@erclx/canon 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/claude/.claude-plugin/plugin.json +19 -0
- package/claude/skills/bash-cli-script/REQUIREMENT.md +42 -0
- package/claude/skills/bash-cli-script/SKILL.md +48 -0
- package/claude/skills/bash-cli-script/references/template.md +43 -0
- package/claude/skills/bash-script/REQUIREMENT.md +36 -0
- package/claude/skills/bash-script/SKILL.md +100 -0
- package/claude/skills/bash-script/references/patterns.md +349 -0
- package/claude/skills/canon-cli/REQUIREMENT.md +41 -0
- package/claude/skills/canon-cli/SKILL.md +103 -0
- package/claude/skills/canon-feedback-file/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-file/SKILL.md +80 -0
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +63 -0
- package/claude/skills/canon-operator/REQUIREMENT.md +61 -0
- package/claude/skills/canon-operator/SKILL.md +108 -0
- package/claude/skills/canon-rollout/REQUIREMENT.md +59 -0
- package/claude/skills/canon-rollout/SKILL.md +147 -0
- package/claude/skills/canon-screencast/REQUIREMENT.md +39 -0
- package/claude/skills/canon-screencast/SKILL.md +167 -0
- package/claude/skills/canon-slides-draft/REQUIREMENT.md +39 -0
- package/claude/skills/canon-slides-draft/SKILL.md +62 -0
- package/claude/skills/ci-workflow/REQUIREMENT.md +40 -0
- package/claude/skills/ci-workflow/SKILL.md +65 -0
- package/claude/skills/ci-workflow/references/workflows.md +98 -0
- package/claude/skills/claude-address-review/REQUIREMENT.md +57 -0
- package/claude/skills/claude-address-review/SKILL.md +212 -0
- package/claude/skills/claude-address-review/references/rebase-conflicts.md +39 -0
- package/claude/skills/claude-autoship/REQUIREMENT.md +50 -0
- package/claude/skills/claude-autoship/SKILL.md +207 -0
- package/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
- package/claude/skills/claude-design-extract/SKILL.md +102 -0
- package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
- package/claude/skills/claude-diagram/SKILL.md +177 -0
- package/claude/skills/claude-docs/REQUIREMENT.md +60 -0
- package/claude/skills/claude-docs/SKILL.md +287 -0
- package/claude/skills/claude-docs/references/anchor-sweep.md +58 -0
- package/claude/skills/claude-docs/references/wireframe-sweep.md +45 -0
- package/claude/skills/claude-feature/REQUIREMENT.md +36 -0
- package/claude/skills/claude-feature/SKILL.md +115 -0
- package/claude/skills/claude-groundwork/REQUIREMENT.md +48 -0
- package/claude/skills/claude-groundwork/SKILL.md +142 -0
- package/claude/skills/claude-intake/REQUIREMENT.md +49 -0
- package/claude/skills/claude-intake/SKILL.md +114 -0
- package/claude/skills/claude-intake-answer/REQUIREMENT.md +48 -0
- package/claude/skills/claude-intake-answer/SKILL.md +90 -0
- package/claude/skills/claude-markdown-propose/REQUIREMENT.md +48 -0
- package/claude/skills/claude-markdown-propose/SKILL.md +118 -0
- package/claude/skills/claude-markdown-propose/references/format.md +107 -0
- package/claude/skills/claude-memory-capture/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-capture/SKILL.md +101 -0
- package/claude/skills/claude-memory-review/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-review/SKILL.md +210 -0
- package/claude/skills/claude-memory-review/references/receipt-format.md +48 -0
- package/claude/skills/claude-orchestrate/REQUIREMENT.md +121 -0
- package/claude/skills/claude-orchestrate/SKILL.md +241 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +132 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +32 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +72 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +87 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +30 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +21 -0
- package/claude/skills/claude-orchestrate/scripts/poll.sh +373 -0
- package/claude/skills/claude-orchestrate/scripts/watch.sh +181 -0
- package/claude/skills/claude-pr-review/REQUIREMENT.md +47 -0
- package/claude/skills/claude-pr-review/SKILL.md +295 -0
- package/claude/skills/claude-review/REQUIREMENT.md +39 -0
- package/claude/skills/claude-review/SKILL.md +142 -0
- package/claude/skills/claude-seed-sync/REQUIREMENT.md +45 -0
- package/claude/skills/claude-seed-sync/SKILL.md +156 -0
- package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
- package/claude/skills/claude-standards-audit/SKILL.md +99 -0
- package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
- package/claude/skills/claude-tasks/SKILL.md +161 -0
- package/claude/skills/claude-teach/REQUIREMENT.md +56 -0
- package/claude/skills/claude-teach/SKILL.md +196 -0
- package/claude/skills/claude-teach/references/lesson-craft.md +59 -0
- package/claude/skills/claude-teach/references/pedagogy.md +67 -0
- package/claude/skills/claude-teach/references/promotion.md +54 -0
- package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ui-test/SKILL.md +77 -0
- package/claude/skills/claude-ux-audit/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ux-audit/SKILL.md +79 -0
- package/claude/skills/claude-ux-measure/REQUIREMENT.md +48 -0
- package/claude/skills/claude-ux-measure/SKILL.md +122 -0
- package/claude/skills/claude-worker/REQUIREMENT.md +54 -0
- package/claude/skills/claude-worker/SKILL.md +96 -0
- package/claude/skills/claude-worktree/REQUIREMENT.md +58 -0
- package/claude/skills/claude-worktree/SKILL.md +136 -0
- package/claude/skills/create-rule/REQUIREMENT.md +45 -0
- package/claude/skills/create-rule/SKILL.md +68 -0
- package/claude/skills/create-skill/REQUIREMENT.md +38 -0
- package/claude/skills/create-skill/SKILL.md +32 -0
- package/claude/skills/create-snippet/REQUIREMENT.md +39 -0
- package/claude/skills/create-snippet/SKILL.md +30 -0
- package/claude/skills/create-standard/REQUIREMENT.md +35 -0
- package/claude/skills/create-standard/SKILL.md +29 -0
- package/claude/skills/decision-escalate/REQUIREMENT.md +45 -0
- package/claude/skills/decision-escalate/SKILL.md +79 -0
- package/claude/skills/docs-sync/REQUIREMENT.md +41 -0
- package/claude/skills/docs-sync/SKILL.md +95 -0
- package/claude/skills/git-branch/REQUIREMENT.md +38 -0
- package/claude/skills/git-branch/SKILL.md +60 -0
- package/claude/skills/git-commit/REQUIREMENT.md +36 -0
- package/claude/skills/git-commit/SKILL.md +49 -0
- package/claude/skills/git-followup/REQUIREMENT.md +43 -0
- package/claude/skills/git-followup/SKILL.md +48 -0
- package/claude/skills/git-issue/REQUIREMENT.md +38 -0
- package/claude/skills/git-issue/SKILL.md +65 -0
- package/claude/skills/git-pr/REQUIREMENT.md +50 -0
- package/claude/skills/git-pr/SKILL.md +164 -0
- package/claude/skills/git-pr/references/labels.md +95 -0
- package/claude/skills/git-ship/REQUIREMENT.md +42 -0
- package/claude/skills/git-ship/SKILL.md +55 -0
- package/claude/skills/git-split/REQUIREMENT.md +39 -0
- package/claude/skills/git-split/SKILL.md +162 -0
- package/claude/skills/git-stage/REQUIREMENT.md +39 -0
- package/claude/skills/git-stage/SKILL.md +73 -0
- package/claude/skills/git-worktree/REQUIREMENT.md +38 -0
- package/claude/skills/git-worktree/SKILL.md +130 -0
- package/claude/skills/migration-claude-md/REQUIREMENT.md +40 -0
- package/claude/skills/migration-claude-md/SKILL.md +76 -0
- package/claude/skills/migration-context/REQUIREMENT.md +36 -0
- package/claude/skills/migration-context/SKILL.md +95 -0
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +55 -0
- package/claude/skills/migration-standards-drop/SKILL.md +113 -0
- package/claude/skills/migration-superseded/REQUIREMENT.md +44 -0
- package/claude/skills/migration-superseded/SKILL.md +115 -0
- package/claude/skills/project-commands/REQUIREMENT.md +42 -0
- package/claude/skills/project-commands/SKILL.md +85 -0
- package/claude/skills/restate-plainly/REQUIREMENT.md +41 -0
- package/claude/skills/restate-plainly/SKILL.md +39 -0
- package/claude/skills/session-map/REQUIREMENT.md +57 -0
- package/claude/skills/session-map/SKILL.md +70 -0
- package/claude/skills/session-resume/REQUIREMENT.md +49 -0
- package/claude/skills/session-resume/SKILL.md +51 -0
- package/claude/skills/setup-gov/REQUIREMENT.md +37 -0
- package/claude/skills/setup-gov/SKILL.md +77 -0
- package/claude/skills/setup-indexes/REQUIREMENT.md +45 -0
- package/claude/skills/setup-indexes/SKILL.md +153 -0
- package/claude/skills/setup-init/REQUIREMENT.md +45 -0
- package/claude/skills/setup-init/SKILL.md +127 -0
- package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
- package/claude/skills/setup-plugins/SKILL.md +81 -0
- package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
- package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
- package/claude/skills/setup-verify/SKILL.md +51 -0
- package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
- package/claude/skills/systematic-debugging/SKILL.md +70 -0
- package/claude/skills/write-human/REQUIREMENT.md +46 -0
- package/claude/skills/write-human/SKILL.md +68 -0
- package/claude/skills/write-human/references/density.md +38 -0
- package/claude/skills/write-human/references/machine-tells.md +107 -0
- package/claude/skills/write-human/references/source-material.md +37 -0
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
- package/claude/skills/youtube-transcripts/SKILL.md +34 -0
- package/docs/agents/audits.md +98 -0
- package/docs/agents/capture.md +37 -0
- package/docs/agents/census.md +23 -0
- package/docs/agents/commands.md +146 -0
- package/docs/agents/comments.md +34 -0
- package/docs/agents/context-audit-checks.md +120 -0
- package/docs/agents/context-audit.md +83 -0
- package/docs/agents/counts.md +76 -0
- package/docs/agents/demo.md +86 -0
- package/docs/agents/docs.md +17 -0
- package/docs/agents/gate.md +84 -0
- package/docs/agents/index.md +47 -0
- package/docs/agents/indexes.md +35 -0
- package/docs/agents/install-and-sync.md +385 -0
- package/docs/agents/intake.md +81 -0
- package/docs/agents/key-changes.md +103 -0
- package/docs/agents/label-coverage.md +73 -0
- package/docs/agents/markdown-audit.md +197 -0
- package/docs/agents/output-shape.md +70 -0
- package/docs/agents/overview.md +26 -0
- package/docs/agents/records.md +170 -0
- package/docs/agents/restated.md +81 -0
- package/docs/agents/review-classification.md +77 -0
- package/docs/agents/routing.md +61 -0
- package/docs/agents/rule-citations.md +98 -0
- package/docs/agents/sandbox.md +71 -0
- package/docs/agents/scripting.md +149 -0
- package/docs/agents/sessions.md +120 -0
- package/docs/agents/skills-audit.md +94 -0
- package/docs/agents/skills-reach.md +64 -0
- package/docs/agents/standards-audit.md +38 -0
- package/docs/agents/state-scoped-risk.md +105 -0
- package/docs/agents/superseded.md +85 -0
- package/docs/agents/targets.md +83 -0
- package/docs/agents/tasks.md +200 -0
- package/docs/agents/teach.md +158 -0
- package/docs/agents/test-order.md +56 -0
- package/docs/agents/worktrees.md +62 -0
- package/docs/ai-workflow.md +317 -0
- package/docs/index.md +23 -0
- package/docs/operating-model.md +223 -0
- package/docs/target-projects.md +258 -0
- package/docs/visual-design-workflow.md +151 -0
- package/docs/zshrc-aliases.md +65 -0
- package/governance/rules/ci/700-ci-workflow.md +44 -0
- package/governance/rules/claude/500-prose.md +15 -0
- package/governance/rules/claude/501-markdown.md +14 -0
- package/governance/rules/claude/510-context.md +28 -0
- package/governance/rules/claude/511-indexes.md +14 -0
- package/governance/rules/claude/520-wireframes.md +12 -0
- package/governance/rules/claude/530-requirements.md +11 -0
- package/governance/rules/claude/540-architecture.md +11 -0
- package/governance/rules/claude/550-design.md +11 -0
- package/governance/rules/claude/555-tasks.md +12 -0
- package/governance/rules/claude/556-groundwork.md +11 -0
- package/governance/rules/claude/557-intake.md +11 -0
- package/governance/rules/claude/558-plan.md +22 -0
- package/governance/rules/claude/559-memory.md +11 -0
- package/governance/rules/claude/560-diagrams.md +18 -0
- package/governance/rules/claude/561-teach.md +13 -0
- package/governance/rules/claude/562-session.md +15 -0
- package/governance/rules/claude/570-skill.md +24 -0
- package/governance/rules/claude/575-hooks.md +17 -0
- package/governance/rules/claude/576-settings.md +14 -0
- package/governance/rules/claude/580-readme.md +11 -0
- package/governance/rules/claude/590-rule-authoring.md +12 -0
- package/governance/rules/claude/591-standard-authoring.md +12 -0
- package/governance/rules/claude/592-claude-md.md +19 -0
- package/governance/rules/core/000-constitution.md +30 -0
- package/governance/rules/core/005-behavior.md +27 -0
- package/governance/rules/core/010-testing.md +35 -0
- package/governance/rules/core/015-output.md +20 -0
- package/governance/rules/core/020-concurrency.md +22 -0
- package/governance/rules/core/025-indexes.md +9 -0
- package/governance/rules/core/030-error-handling.md +31 -0
- package/governance/rules/core/035-tasks.md +13 -0
- package/governance/rules/core/040-performance.md +20 -0
- package/governance/rules/core/045-memory.md +12 -0
- package/governance/rules/core/050-logging.md +20 -0
- package/governance/rules/core/055-scratch.md +9 -0
- package/governance/rules/core/060-naming.md +19 -0
- package/governance/rules/core/065-spelling.md +19 -0
- package/governance/rules/core/070-planning.md +18 -0
- package/governance/rules/core/075-dependencies.md +25 -0
- package/governance/rules/core/080-config-comments.md +22 -0
- package/governance/rules/core/085-worktrees.md +17 -0
- package/governance/rules/core/087-git.md +11 -0
- package/governance/rules/core/090-code-comments.md +39 -0
- package/governance/rules/framework/200-react.md +51 -0
- package/governance/rules/framework/210-astro.md +41 -0
- package/governance/rules/framework/220-fastapi.md +43 -0
- package/governance/rules/framework/230-nextjs.md +48 -0
- package/governance/rules/framework/250-tailwind.md +32 -0
- package/governance/rules/framework/260-shadcn.md +34 -0
- package/governance/rules/lang/100-typescript.md +40 -0
- package/governance/rules/lang/110-python.md +42 -0
- package/governance/rules/lang/120-bash.md +19 -0
- package/governance/rules/lib/300-testing-ts.md +39 -0
- package/governance/rules/lib/305-e2e-reliability.md +34 -0
- package/governance/rules/lib/306-test-scope.md +25 -0
- package/governance/rules/lib/310-zod.md +25 -0
- package/governance/rules/lib/320-tanstack-query.md +32 -0
- package/governance/rules/lib/330-testing-py.md +44 -0
- package/governance/rules/lib/340-pydantic.md +38 -0
- package/governance/rules/lib/350-security-web.md +32 -0
- package/governance/rules/lib/360-security-server.md +39 -0
- package/governance/rules/lib/370-database.md +35 -0
- package/governance/rules/snippets/505-at-references.md +9 -0
- package/governance/rules/ui/400-ui.md +36 -0
- package/governance/rules/ui/410-a11y.md +48 -0
- package/governance/rules/ui/420-forms.md +36 -0
- package/governance/rules/ui/430-ux-completeness.md +65 -0
- package/governance/rules/ui/440-surface-capture.md +34 -0
- package/governance/rules/ui/450-link-behavior.md +19 -0
- package/governance/stacks/astro.toml +2 -0
- package/governance/stacks/base.toml +8 -0
- package/governance/stacks/node-server.toml +2 -0
- package/governance/stacks/node.toml +2 -0
- package/governance/stacks/python-fastapi.toml +2 -0
- package/governance/stacks/python.toml +2 -0
- package/governance/stacks/react.toml +2 -0
- package/package.json +69 -0
- package/scripts/config.sh +11 -0
- package/scripts/core/bootstrap.sh +81 -0
- package/scripts/core/check-color-source.sh +41 -0
- package/scripts/core/check-ignore-parity.sh +162 -0
- package/scripts/core/check-plugin-boundary.sh +45 -0
- package/scripts/core/check-seed-independence.sh +59 -0
- package/scripts/core/check-skill-paths.sh +24 -0
- package/scripts/core/clean.sh +36 -0
- package/scripts/core/install-check.sh +101 -0
- package/scripts/core/list-seed-roots.sh +18 -0
- package/scripts/core/regen-claude-copies.sh +10 -0
- package/scripts/core/regen-hero.sh +217 -0
- package/scripts/core/regen-indexes.sh +10 -0
- package/scripts/core/regen-tooling-paths.sh +61 -0
- package/scripts/core/repair-bare-flag.sh +19 -0
- package/scripts/core/snapshot.sh +134 -0
- package/scripts/core/update.sh +35 -0
- package/scripts/docs/list.sh +165 -0
- package/scripts/lib/frontmatter.sh +30 -0
- package/scripts/lib/gov.sh +14 -0
- package/scripts/lib/sandbox-fixtures.sh +191 -0
- package/scripts/lib/sandbox-git.sh +125 -0
- package/scripts/lib/sandbox-path.sh +206 -0
- package/scripts/lib/tooling.sh +35 -0
- package/scripts/lib/ui.sh +266 -0
- package/scripts/lib/worktree.sh +20 -0
- package/scripts/manage-sandbox.sh +466 -0
- package/scripts/snippets/create.sh +156 -0
- package/scripts/standards/list.sh +115 -0
- package/scripts/tooling/create.sh +109 -0
- package/scripts/tooling/verify.sh +179 -0
- package/snippets/align.md +12 -0
- package/snippets/claude/decision-memo.md +39 -0
- package/snippets/claude/feature-recap.md +19 -0
- package/snippets/claude/figma-steps.md +24 -0
- package/snippets/compact-summary.md +5 -0
- package/snippets/decision-help.md +6 -0
- package/snippets/meta-prompt.md +14 -0
- package/snippets/research-prompt.md +7 -0
- package/snippets/session-notes.md +11 -0
- package/snippets/snippets.toml +5 -0
- package/snippets/step-by-step.md +10 -0
- package/snippets/web-research.md +21 -0
- package/src/audits/baseline.ts +201 -0
- package/src/audits/catalog.ts +876 -0
- package/src/audits/run.ts +204 -0
- package/src/autoship/classify.ts +75 -0
- package/src/autoship/paths.ts +51 -0
- package/src/binary.ts +16 -0
- package/src/browser/engine.ts +40 -0
- package/src/census/count.ts +113 -0
- package/src/claude/cases/all.ts +24 -0
- package/src/claude/cases/authoring.ts +53 -0
- package/src/claude/cases/claude-workflow.ts +158 -0
- package/src/claude/cases/git.ts +44 -0
- package/src/claude/cases/misc.ts +27 -0
- package/src/claude/cases/setup.ts +94 -0
- package/src/claude/gitignore.ts +51 -0
- package/src/claude/routing.ts +283 -0
- package/src/claude/seeds-list.ts +47 -0
- package/src/claude/seeds.ts +150 -0
- package/src/claude/settings.ts +151 -0
- package/src/claude/skills-audit.ts +228 -0
- package/src/claude/skills-drift.ts +156 -0
- package/src/claude/skills-list.ts +99 -0
- package/src/claude/skills-rank.ts +320 -0
- package/src/claude/skills-reach.ts +227 -0
- package/src/cli-run.ts +43 -0
- package/src/cli.ts +200 -0
- package/src/commands/audits.ts +350 -0
- package/src/commands/autoship.ts +129 -0
- package/src/commands/capture.ts +133 -0
- package/src/commands/census.ts +105 -0
- package/src/commands/claude.ts +1286 -0
- package/src/commands/comments.ts +240 -0
- package/src/commands/context.ts +857 -0
- package/src/commands/demo.ts +389 -0
- package/src/commands/deps.ts +173 -0
- package/src/commands/design.ts +36 -0
- package/src/commands/docs.ts +60 -0
- package/src/commands/feedback-format.ts +23 -0
- package/src/commands/feedback.ts +112 -0
- package/src/commands/gate.ts +189 -0
- package/src/commands/gov.ts +1265 -0
- package/src/commands/indexes.ts +184 -0
- package/src/commands/init.ts +113 -0
- package/src/commands/intake.ts +406 -0
- package/src/commands/inventory.ts +256 -0
- package/src/commands/labels.ts +361 -0
- package/src/commands/markdown.ts +544 -0
- package/src/commands/migrate.ts +175 -0
- package/src/commands/pass-through.ts +39 -0
- package/src/commands/pr.ts +411 -0
- package/src/commands/records.ts +728 -0
- package/src/commands/sandbox.ts +468 -0
- package/src/commands/secrets.ts +132 -0
- package/src/commands/serve.ts +159 -0
- package/src/commands/sessions.ts +408 -0
- package/src/commands/slides.ts +126 -0
- package/src/commands/snippets.ts +84 -0
- package/src/commands/standards.ts +247 -0
- package/src/commands/sync.ts +428 -0
- package/src/commands/targets.ts +319 -0
- package/src/commands/tasks.ts +743 -0
- package/src/commands/teach.ts +786 -0
- package/src/commands/tooling.ts +573 -0
- package/src/commands/transcripts.ts +44 -0
- package/src/commands/upgrade.ts +231 -0
- package/src/commands/wiki.ts +100 -0
- package/src/commands/worktrees.ts +191 -0
- package/src/comments/scan.ts +338 -0
- package/src/comments/trend.ts +207 -0
- package/src/comments/vocabulary.ts +85 -0
- package/src/context/architecture.ts +364 -0
- package/src/context/audit.ts +790 -0
- package/src/context/citations.ts +196 -0
- package/src/context/folders.ts +186 -0
- package/src/context/gate.ts +57 -0
- package/src/context/index-drift.ts +64 -0
- package/src/context/narration.ts +99 -0
- package/src/copy.ts +30 -0
- package/src/counts/catalogs.ts +96 -0
- package/src/counts/numbers.ts +79 -0
- package/src/counts/scan.ts +314 -0
- package/src/demo/beats.ts +135 -0
- package/src/demo/compile.ts +326 -0
- package/src/demo/container.ts +63 -0
- package/src/demo/cursors.ts +55 -0
- package/src/demo/drive.ts +357 -0
- package/src/demo/pointer.ts +178 -0
- package/src/demo/theme.ts +112 -0
- package/src/deps/audit.ts +153 -0
- package/src/design/parse.ts +116 -0
- package/src/design/render.ts +249 -0
- package/src/docs/read.ts +77 -0
- package/src/exec.ts +16 -0
- package/src/exempt-marker.ts +43 -0
- package/src/frontmatter.ts +13 -0
- package/src/gate/measures.ts +682 -0
- package/src/gate/sequencer.ts +386 -0
- package/src/gate/stages.ts +412 -0
- package/src/git-env.ts +36 -0
- package/src/git-files.ts +105 -0
- package/src/git-ignore.ts +46 -0
- package/src/github-format.ts +13 -0
- package/src/github.ts +24 -0
- package/src/gov/adapter.ts +103 -0
- package/src/gov/citations.ts +514 -0
- package/src/gov/consumed.ts +129 -0
- package/src/gov/install.ts +132 -0
- package/src/gov/list.ts +106 -0
- package/src/gov/payload.ts +39 -0
- package/src/gov/restated.ts +814 -0
- package/src/gov/stacks.ts +205 -0
- package/src/gov/superseded.ts +415 -0
- package/src/gov/test-order.ts +407 -0
- package/src/indexes/frontmatter.ts +46 -0
- package/src/indexes/regen.ts +84 -0
- package/src/indexes/render.ts +201 -0
- package/src/indexes/walk.ts +83 -0
- package/src/init/flags.ts +60 -0
- package/src/init/plan.ts +114 -0
- package/src/init/run.ts +46 -0
- package/src/init/steps.ts +77 -0
- package/src/intake/folder.ts +320 -0
- package/src/intake/items.ts +174 -0
- package/src/inventory/config.ts +117 -0
- package/src/inventory/group.ts +76 -0
- package/src/inventory/subjects.ts +114 -0
- package/src/inventory/walk.ts +129 -0
- package/src/labels/audit.ts +82 -0
- package/src/labels/coverage.ts +79 -0
- package/src/labels/map.ts +101 -0
- package/src/labels/phase.ts +95 -0
- package/src/markdown/bans.ts +94 -0
- package/src/markdown/files.ts +102 -0
- package/src/markdown/gate.ts +28 -0
- package/src/markdown/scan.ts +295 -0
- package/src/markdown/structure.ts +730 -0
- package/src/migrate/apply.ts +115 -0
- package/src/migrate/plan.ts +103 -0
- package/src/migrate/rename.ts +183 -0
- package/src/pr/bijection.ts +145 -0
- package/src/pr/paths.ts +335 -0
- package/src/process/harness.ts +167 -0
- package/src/project-root.ts +19 -0
- package/src/records/backup.ts +455 -0
- package/src/records/migrate.ts +78 -0
- package/src/records/size.ts +260 -0
- package/src/records/validate.ts +1162 -0
- package/src/sandbox/census.ts +228 -0
- package/src/sandbox/coverage.ts +115 -0
- package/src/sandbox/expect.ts +629 -0
- package/src/sandbox/tree.ts +47 -0
- package/src/secrets/marker.ts +30 -0
- package/src/secrets/patterns.ts +142 -0
- package/src/secrets/scan.ts +123 -0
- package/src/secrets/shipped.ts +111 -0
- package/src/seed-marker.ts +74 -0
- package/src/serve/static.ts +322 -0
- package/src/sessions/claim.ts +85 -0
- package/src/sessions/live.ts +79 -0
- package/src/sessions/registry.ts +137 -0
- package/src/sessions/resolve.ts +333 -0
- package/src/slides/layouts.ts +391 -0
- package/src/slides/open.ts +18 -0
- package/src/slides/parse.ts +84 -0
- package/src/slides/render.ts +88 -0
- package/src/slides/styles.ts +44 -0
- package/src/snippets/categories.ts +66 -0
- package/src/snippets/list.ts +32 -0
- package/src/snippets/presets.ts +50 -0
- package/src/standards/audit.ts +132 -0
- package/src/standards/read.ts +100 -0
- package/src/sync/check.ts +692 -0
- package/src/sync/engine.ts +576 -0
- package/src/sync/git.ts +225 -0
- package/src/sync/history.ts +123 -0
- package/src/sync/layout.ts +140 -0
- package/src/sync/reverse.ts +268 -0
- package/src/sync/seeds-report.ts +126 -0
- package/src/sync/stamp.ts +358 -0
- package/src/sync/target.ts +70 -0
- package/src/sync/workflow.ts +200 -0
- package/src/target.ts +43 -0
- package/src/targets/pulls.ts +250 -0
- package/src/targets/registry.ts +203 -0
- package/src/targets/resolve.ts +145 -0
- package/src/targets/sweep.ts +246 -0
- package/src/tasks/archive.ts +506 -0
- package/src/tasks/record.ts +311 -0
- package/src/tasks/trunk.ts +89 -0
- package/src/tasks/validate.ts +1001 -0
- package/src/teach/lesson.ts +180 -0
- package/src/teach/workspace.ts +842 -0
- package/src/tooling/gitignore.ts +122 -0
- package/src/tooling/inject.ts +193 -0
- package/src/tooling/list.ts +39 -0
- package/src/tooling/manifest.ts +176 -0
- package/src/tooling/package.ts +166 -0
- package/src/tooling/read.ts +65 -0
- package/src/tooling/scan.ts +147 -0
- package/src/tooling/stamp.ts +44 -0
- package/src/transcripts/fetch.ts +156 -0
- package/src/transcripts/metadata.ts +54 -0
- package/src/transcripts/vtt.ts +114 -0
- package/src/ui.ts +266 -0
- package/src/version/compare.ts +53 -0
- package/src/version/installed.ts +40 -0
- package/src/version/manager.ts +67 -0
- package/src/version/skew.ts +192 -0
- package/src/wiki/init.ts +85 -0
- package/src/worktree.ts +143 -0
- package/src/worktrees/reclaim.ts +306 -0
- package/standards/architecture.md +72 -0
- package/standards/branch.md +59 -0
- package/standards/commit.md +72 -0
- package/standards/context.md +151 -0
- package/standards/design.md +93 -0
- package/standards/diagrams.md +152 -0
- package/standards/glossary.md +75 -0
- package/standards/groundwork.md +211 -0
- package/standards/index.md +36 -0
- package/standards/intake.md +192 -0
- package/standards/issue.md +94 -0
- package/standards/markdown.md +137 -0
- package/standards/memory.md +144 -0
- package/standards/plan.md +172 -0
- package/standards/pr.md +139 -0
- package/standards/publish.md +51 -0
- package/standards/readme.md +208 -0
- package/standards/requirements.md +70 -0
- package/standards/rule.md +118 -0
- package/standards/session.md +109 -0
- package/standards/skill.md +300 -0
- package/standards/slug.md +39 -0
- package/standards/snippets.md +76 -0
- package/standards/standard.md +170 -0
- package/standards/tasks.md +254 -0
- package/standards/teach.md +153 -0
- package/standards/versioning.md +71 -0
- package/standards/wireframes.md +113 -0
- package/tooling/astro/configs/astro.config.mjs +31 -0
- package/tooling/astro/configs/eslint.config.js +79 -0
- package/tooling/astro/configs/playwright.config.ts +26 -0
- package/tooling/astro/configs/tsconfig.json +12 -0
- package/tooling/astro/configs/vitest.config.ts +22 -0
- package/tooling/astro/manifest.toml +34 -0
- package/tooling/astro/reference.md +60 -0
- package/tooling/base/configs/.editorconfig +5 -0
- package/tooling/base/configs/.github/pull_request_template.md +18 -0
- package/tooling/base/configs/.github/workflows/verify.yml +35 -0
- package/tooling/base/configs/.husky/commit-msg +1 -0
- package/tooling/base/configs/.husky/post-merge +61 -0
- package/tooling/base/configs/.husky/post-rewrite +21 -0
- package/tooling/base/configs/.husky/pre-commit +1 -0
- package/tooling/base/configs/.husky/pre-push +1 -0
- package/tooling/base/configs/.prettierrc +12 -0
- package/tooling/base/configs/.shellcheckrc +1 -0
- package/tooling/base/configs/.vscode/extensions.json +9 -0
- package/tooling/base/configs/.vscode/settings.json +3 -0
- package/tooling/base/configs/commitlint.config.js +11 -0
- package/tooling/base/configs/scripts/verify.sh +64 -0
- package/tooling/base/manifest.toml +30 -0
- package/tooling/base/reference.md +98 -0
- package/tooling/base/seeds/.claude/context/ci.md +33 -0
- package/tooling/base/seeds/.claude/context/development.md +37 -0
- package/tooling/base/seeds/.claude/context/index.md +11 -0
- package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
- package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/base/seeds/.lintstagedrc +8 -0
- package/tooling/base/seeds/.prettierignore +0 -0
- package/tooling/base/seeds/cspell.json +20 -0
- package/tooling/claude/manifest.toml +14 -0
- package/tooling/claude/reference.md +79 -0
- package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
- package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
- package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
- package/tooling/claude/seeds/.claude/diagrams/index.md +8 -0
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +50 -0
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +68 -0
- package/tooling/claude/seeds/.claude/hooks/path-form.sh +57 -0
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +52 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +86 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +71 -0
- package/tooling/claude/seeds/.claude/memory/index.md +8 -0
- package/tooling/claude/seeds/.claude/settings.json +47 -0
- package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
- package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
- package/tooling/claude/seeds/CLAUDE.md +29 -0
- package/tooling/claude/user/settings.template.json +10 -0
- package/tooling/claude/user/statusline-command.sh +53 -0
- package/tooling/python/configs/.coveragerc +14 -0
- package/tooling/python/configs/.python-version +1 -0
- package/tooling/python/configs/mypy.ini +6 -0
- package/tooling/python/configs/pytest.ini +4 -0
- package/tooling/python/configs/ruff.toml +15 -0
- package/tooling/python/configs/scripts/verify.sh +77 -0
- package/tooling/python/manifest.toml +16 -0
- package/tooling/python/reference.md +66 -0
- package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/python/seeds/tests/test_smoke.py +2 -0
- package/tooling/vite-react/configs/playwright.config.ts +26 -0
- package/tooling/vite-react/configs/tsconfig.json +35 -0
- package/tooling/vite-react/configs/vite.config.ts +24 -0
- package/tooling/vite-react/configs/vitest.config.ts +26 -0
- package/tooling/vite-react/manifest.toml +23 -0
- package/tooling/vite-react/reference.md +55 -0
- package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
- package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
- package/tooling/web/configs/.github/workflows/verify.yml +134 -0
- package/tooling/web/configs/.vscode/extensions.json +13 -0
- package/tooling/web/configs/.vscode/settings.json +10 -0
- package/tooling/web/configs/e2e/home.spec.ts +6 -0
- package/tooling/web/configs/e2e/screenshot.ts +53 -0
- package/tooling/web/configs/eslint.config.js +82 -0
- package/tooling/web/configs/scripts/screenshot.sh +28 -0
- package/tooling/web/configs/scripts/verify.sh +80 -0
- package/tooling/web/configs/scripts/worktree-port.sh +74 -0
- package/tooling/web/configs/src/test/setup.ts +8 -0
- package/tooling/web/manifest.toml +58 -0
- package/tooling/web/reference.md +115 -0
- package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { existsSync, realpathSync, statSync } from 'node:fs'
|
|
2
|
+
import { join, resolve, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The loopback interface, never a wildcard bind. A preview serves whatever
|
|
6
|
+
* directory it is pointed at, and the folders this exists for are the
|
|
7
|
+
* gitignored record trees, so reaching the network is the one thing it must
|
|
8
|
+
* not do.
|
|
9
|
+
*/
|
|
10
|
+
export const SERVE_HOST = '127.0.0.1'
|
|
11
|
+
|
|
12
|
+
/** Tried first, then the next ports in order, so a second preview still opens. */
|
|
13
|
+
export const DEFAULT_PORT = 8787
|
|
14
|
+
|
|
15
|
+
/** How far past the requested port to look before refusing. */
|
|
16
|
+
const PORT_ATTEMPTS = 20
|
|
17
|
+
|
|
18
|
+
const DEFAULT_ENTRY = 'index.html'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extensions a browser has to be told about. Anything absent is served as an
|
|
22
|
+
* octet stream, which downloads rather than renders, and that is the safe
|
|
23
|
+
* direction for a type this map does not claim to know.
|
|
24
|
+
*/
|
|
25
|
+
const CONTENT_TYPES: Readonly<Record<string, string>> = {
|
|
26
|
+
css: 'text/css; charset=utf-8',
|
|
27
|
+
gif: 'image/gif',
|
|
28
|
+
htm: 'text/html; charset=utf-8',
|
|
29
|
+
html: 'text/html; charset=utf-8',
|
|
30
|
+
ico: 'image/x-icon',
|
|
31
|
+
jpeg: 'image/jpeg',
|
|
32
|
+
jpg: 'image/jpeg',
|
|
33
|
+
js: 'text/javascript; charset=utf-8',
|
|
34
|
+
json: 'application/json; charset=utf-8',
|
|
35
|
+
/**
|
|
36
|
+
* Plain text rather than `text/markdown`, which a browser offers to save
|
|
37
|
+
* instead of showing. A reader following a link to a source page wants to
|
|
38
|
+
* read it, and the rendered sibling is a separate file.
|
|
39
|
+
*/
|
|
40
|
+
md: 'text/plain; charset=utf-8',
|
|
41
|
+
mjs: 'text/javascript; charset=utf-8',
|
|
42
|
+
pdf: 'application/pdf',
|
|
43
|
+
png: 'image/png',
|
|
44
|
+
svg: 'image/svg+xml',
|
|
45
|
+
txt: 'text/plain; charset=utf-8',
|
|
46
|
+
webp: 'image/webp',
|
|
47
|
+
woff: 'font/woff',
|
|
48
|
+
woff2: 'font/woff2',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ServeRefusal =
|
|
52
|
+
| 'no-root'
|
|
53
|
+
| 'not-a-directory'
|
|
54
|
+
| 'no-port'
|
|
55
|
+
| 'no-entry'
|
|
56
|
+
| 'bind-failed'
|
|
57
|
+
|
|
58
|
+
export interface ServeRefused {
|
|
59
|
+
readonly ok: false
|
|
60
|
+
readonly reason: ServeRefusal
|
|
61
|
+
readonly detail: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ServeStarted {
|
|
65
|
+
readonly ok: true
|
|
66
|
+
/** Absolute, so a report names the directory rather than the caller's cwd. */
|
|
67
|
+
readonly root: string
|
|
68
|
+
readonly host: string
|
|
69
|
+
readonly port: number
|
|
70
|
+
/** The entry page relative to the root, as the URL spells it. */
|
|
71
|
+
readonly entry: string
|
|
72
|
+
/** What a reader clicks. Complete, including the entry page. */
|
|
73
|
+
readonly url: string
|
|
74
|
+
/** Whether the entry page exists. A missing one is reported, never fatal. */
|
|
75
|
+
readonly entryExists: boolean
|
|
76
|
+
readonly stop: () => Promise<void>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ServeOutcome = ServeStarted | ServeRefused
|
|
80
|
+
|
|
81
|
+
export interface ServeOptions {
|
|
82
|
+
readonly port?: number
|
|
83
|
+
readonly entry?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function refuse(reason: ServeRefusal, detail: string): ServeRefused {
|
|
87
|
+
return { ok: false, reason, detail }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function contentType(path: string): string {
|
|
91
|
+
const dot = path.lastIndexOf('.')
|
|
92
|
+
if (dot === -1) return 'application/octet-stream'
|
|
93
|
+
const ext = path.slice(dot + 1).toLowerCase()
|
|
94
|
+
return CONTENT_TYPES[ext] ?? 'application/octet-stream'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolves a request path inside the root, or returns undefined when it escapes.
|
|
99
|
+
* The containment test compares resolved absolute paths rather than inspecting
|
|
100
|
+
* the request for `..`, because an encoded traversal survives a textual scan and
|
|
101
|
+
* does not survive resolution.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveWithin(
|
|
104
|
+
root: string,
|
|
105
|
+
requestPath: string,
|
|
106
|
+
): string | undefined {
|
|
107
|
+
let decoded: string
|
|
108
|
+
try {
|
|
109
|
+
decoded = decodeURIComponent(requestPath)
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A NUL truncates the path at the filesystem layer, so a request carrying one
|
|
115
|
+
* asks for a different file than the one the containment test read.
|
|
116
|
+
*/
|
|
117
|
+
if (decoded.includes('\0')) return undefined
|
|
118
|
+
|
|
119
|
+
const relativePath = decoded.replace(/^\/+/, '')
|
|
120
|
+
const target = resolve(root, relativePath)
|
|
121
|
+
if (target !== root && !target.startsWith(root + sep)) return undefined
|
|
122
|
+
return target
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Re-tests containment after following symlinks. `resolveWithin` is lexical
|
|
127
|
+
* and `resolve` does not follow a link, so a link inside the root clears that
|
|
128
|
+
* test while the file it points at sits outside. This repository is a live
|
|
129
|
+
* instance, since `claude/standards` and `claude/snippets` are links out of
|
|
130
|
+
* `claude/`.
|
|
131
|
+
*
|
|
132
|
+
* Only a path that exists is checked, because a link can be followed only once
|
|
133
|
+
* there is something on the other end, and a path that resolves to nothing is
|
|
134
|
+
* a 404 rather than an escape.
|
|
135
|
+
*/
|
|
136
|
+
function escapesThroughLink(root: string, target: string): boolean {
|
|
137
|
+
if (!existsSync(target)) return false
|
|
138
|
+
try {
|
|
139
|
+
const realRoot = realpathSync(root)
|
|
140
|
+
const realTarget = realpathSync(target)
|
|
141
|
+
return realTarget !== realRoot && !realTarget.startsWith(realRoot + sep)
|
|
142
|
+
} catch {
|
|
143
|
+
/* Unreadable resolves to no answer, and no answer is refused. */
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether a bind failure is contention worth trying the next port for.
|
|
150
|
+
*
|
|
151
|
+
* Lifted out of the loop so the decision is testable. Manufacturing a real
|
|
152
|
+
* non-contention bind failure needs a privileged port or an unavailable
|
|
153
|
+
* interface and neither travels between machines, where an error value does,
|
|
154
|
+
* so this is unit-tested and the bind itself is not.
|
|
155
|
+
*/
|
|
156
|
+
export function shouldWalkPast(error: unknown): boolean {
|
|
157
|
+
return (error as NodeJS.ErrnoException | null)?.code === 'EADDRINUSE'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Picks a listening port, starting at the requested one and walking forward.
|
|
162
|
+
* A busy port is the ordinary case rather than a failure, since a preview of
|
|
163
|
+
* one workspace is routinely open while another is started.
|
|
164
|
+
*/
|
|
165
|
+
function listen(
|
|
166
|
+
root: string,
|
|
167
|
+
first: number,
|
|
168
|
+
): { server: ReturnType<typeof Bun.serve>; port: number } | undefined {
|
|
169
|
+
for (let port = first; port < first + PORT_ATTEMPTS; port++) {
|
|
170
|
+
try {
|
|
171
|
+
const server = Bun.serve({
|
|
172
|
+
hostname: SERVE_HOST,
|
|
173
|
+
port,
|
|
174
|
+
fetch: (request) => respond(root, request),
|
|
175
|
+
})
|
|
176
|
+
/**
|
|
177
|
+
* The bound port rather than the requested one. Port 0 asks the OS to
|
|
178
|
+
* choose, so reporting the request builds a URL pointing at nothing.
|
|
179
|
+
* The type admits undefined for a unix socket, which a bind carrying a
|
|
180
|
+
* hostname and a port never is, and the request is the honest fallback.
|
|
181
|
+
*/
|
|
182
|
+
return { server, port: server.port ?? port }
|
|
183
|
+
} catch (error) {
|
|
184
|
+
/**
|
|
185
|
+
* Contention is the one cause worth walking past. A permission failure
|
|
186
|
+
* or an unavailable interface swallowed here would be retried twenty
|
|
187
|
+
* times and then reported as a port range being full, which names a
|
|
188
|
+
* cause nothing checked. `startServer` turns the rethrow into a refusal.
|
|
189
|
+
*/
|
|
190
|
+
if (!shouldWalkPast(error)) throw error
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function respond(
|
|
197
|
+
root: string,
|
|
198
|
+
request: Request,
|
|
199
|
+
): Promise<Response> {
|
|
200
|
+
const { pathname, search } = new URL(request.url)
|
|
201
|
+
const forbidden = () =>
|
|
202
|
+
new Response('Forbidden\n', {
|
|
203
|
+
status: 403,
|
|
204
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const target = resolveWithin(root, pathname)
|
|
208
|
+
if (!target) return forbidden()
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Tested here as well as before the read, because the redirect below answers
|
|
212
|
+
* ahead of that one. A redirect firing on an out-of-root directory reports
|
|
213
|
+
* that it exists, where one that does not answers 404, and the pair is a
|
|
214
|
+
* fact about the filesystem outside the root.
|
|
215
|
+
*/
|
|
216
|
+
if (escapesThroughLink(root, target)) return forbidden()
|
|
217
|
+
|
|
218
|
+
let path = target
|
|
219
|
+
if (existsSync(path) && statSync(path).isDirectory()) {
|
|
220
|
+
/**
|
|
221
|
+
* A browser resolves a relative asset against the last slash of the URL it
|
|
222
|
+
* is on, so answering a directory in place leaves `/lesson` asking for
|
|
223
|
+
* `/course.css` rather than `/lesson/course.css` and the page renders
|
|
224
|
+
* unstyled. The redirect moves the base before the index is served.
|
|
225
|
+
*/
|
|
226
|
+
if (!pathname.endsWith('/')) {
|
|
227
|
+
return new Response(null, {
|
|
228
|
+
status: 301,
|
|
229
|
+
headers: { location: `${pathname}/${search}` },
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
path = join(path, DEFAULT_ENTRY)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Sits immediately before the read rather than beside the path that produced
|
|
237
|
+
* it, so every path reaching `Bun.file` has been tested whatever produced
|
|
238
|
+
* it. Checking the request path alone left the appended index untested, and
|
|
239
|
+
* a real directory holding a linked index was served.
|
|
240
|
+
*/
|
|
241
|
+
if (escapesThroughLink(root, path)) return forbidden()
|
|
242
|
+
|
|
243
|
+
const file = Bun.file(path)
|
|
244
|
+
if (!(await file.exists())) {
|
|
245
|
+
return new Response(`Not found: ${pathname}\n`, {
|
|
246
|
+
status: 404,
|
|
247
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return new Response(file, {
|
|
252
|
+
headers: {
|
|
253
|
+
'content-type': contentType(path),
|
|
254
|
+
/**
|
|
255
|
+
* A preview is edited and reloaded continuously, and a cached stylesheet
|
|
256
|
+
* reads as a fix that did not work. Revalidation is the whole point of
|
|
257
|
+
* the surface, so it is not negotiable per response.
|
|
258
|
+
*/
|
|
259
|
+
'cache-control': 'no-store',
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function startServer(
|
|
265
|
+
dir: string,
|
|
266
|
+
options: ServeOptions = {},
|
|
267
|
+
): ServeOutcome {
|
|
268
|
+
const root = resolve(process.cwd(), dir)
|
|
269
|
+
if (!existsSync(root)) return refuse('no-root', `${dir} does not exist`)
|
|
270
|
+
if (!statSync(root).isDirectory())
|
|
271
|
+
return refuse('not-a-directory', `${dir} is not a directory`)
|
|
272
|
+
|
|
273
|
+
const entry = (options.entry ?? DEFAULT_ENTRY).replace(/^\/+/, '')
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Checked before a port is taken, since `url` is the field a caller is told
|
|
277
|
+
* to read and hand to a reader. An entry the containment test rejects would
|
|
278
|
+
* otherwise be reported as a link the server then refuses. An entry that is
|
|
279
|
+
* merely absent is not this case and does not refuse, which `entryExists`
|
|
280
|
+
* reports instead.
|
|
281
|
+
*/
|
|
282
|
+
const entryPath = resolveWithin(root, entry)
|
|
283
|
+
if (!entryPath) return refuse('no-entry', `${entry} escapes ${dir}`)
|
|
284
|
+
|
|
285
|
+
const first = options.port ?? DEFAULT_PORT
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* A bind failure that is not contention reaches here as a throw, and the
|
|
289
|
+
* command's own help promises a reason on stderr or in the record. A stack
|
|
290
|
+
* trace is neither, so it is caught and named.
|
|
291
|
+
*/
|
|
292
|
+
let bound: ReturnType<typeof listen>
|
|
293
|
+
try {
|
|
294
|
+
bound = listen(root, first)
|
|
295
|
+
} catch (error) {
|
|
296
|
+
const code = (error as NodeJS.ErrnoException).code ?? 'unknown'
|
|
297
|
+
return refuse(
|
|
298
|
+
'bind-failed',
|
|
299
|
+
`could not bind ${SERVE_HOST}:${first} (${code})`,
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!bound) {
|
|
304
|
+
return refuse(
|
|
305
|
+
'no-port',
|
|
306
|
+
`no free port between ${first} and ${first + PORT_ATTEMPTS - 1}`,
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
ok: true,
|
|
312
|
+
root,
|
|
313
|
+
host: SERVE_HOST,
|
|
314
|
+
port: bound.port,
|
|
315
|
+
entry,
|
|
316
|
+
url: `http://${SERVE_HOST}:${bound.port}/${entry}`,
|
|
317
|
+
entryExists: existsSync(entryPath),
|
|
318
|
+
stop: async () => {
|
|
319
|
+
await bound.server.stop(true)
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
repositoryOf,
|
|
3
|
+
resolveSessions,
|
|
4
|
+
type ResolvedSession,
|
|
5
|
+
type SessionReport,
|
|
6
|
+
} from '@/sessions/resolve'
|
|
7
|
+
import {
|
|
8
|
+
branchRefs,
|
|
9
|
+
listWorktrees,
|
|
10
|
+
type RefReport,
|
|
11
|
+
type WorktreeEntry,
|
|
12
|
+
} from '@/worktree'
|
|
13
|
+
|
|
14
|
+
export interface ClaimReport {
|
|
15
|
+
readonly claimed: boolean
|
|
16
|
+
readonly worktree: string | null
|
|
17
|
+
readonly sessions: readonly ResolvedSession[]
|
|
18
|
+
/** The refs already naming the branch, local head and `origin` remote-tracking alike. */
|
|
19
|
+
readonly refs: readonly string[]
|
|
20
|
+
/** False when the session roster could not be read, so `claimed` covers the readings around it alone and cannot be trusted as a clean "unclaimed". */
|
|
21
|
+
readonly sessionsReadable: boolean
|
|
22
|
+
/** False when the ref read failed, which is the same false clean under a different reading. */
|
|
23
|
+
readonly refsReadable: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ClaimOptions {
|
|
27
|
+
/**
|
|
28
|
+
* The repository the claim is answered about, not merely where the caller
|
|
29
|
+
* stands. Every reading below is taken against it, so handing another
|
|
30
|
+
* project's path asks about that project, which is what lets a dispatcher in
|
|
31
|
+
* one repository see a branch held in another. Defaults to the caller's own
|
|
32
|
+
* directory, so a call omitting it answers exactly as it always has.
|
|
33
|
+
*/
|
|
34
|
+
readonly cwd?: string
|
|
35
|
+
readonly resolve?: () => Promise<SessionReport>
|
|
36
|
+
readonly listWorktrees?: (cwd: string) => Promise<readonly WorktreeEntry[]>
|
|
37
|
+
readonly branchRefs?: (branch: string, cwd: string) => Promise<RefReport>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Answers whether a branch is already claimed, composing the three readings no
|
|
42
|
+
* one surface can answer alone: a worktree can outlive the session that made
|
|
43
|
+
* it, a session can hold a branch before a worktree exists for it, and a branch
|
|
44
|
+
* behind a merged pull request has neither while still being taken.
|
|
45
|
+
*
|
|
46
|
+
* The two readable flags stay separate because a caller told the roster failed
|
|
47
|
+
* looks at the roster. Folding either into `claimed` would report the failure
|
|
48
|
+
* as a clean "unclaimed", which is the answer this exists to stop giving.
|
|
49
|
+
*/
|
|
50
|
+
export async function checkClaim(
|
|
51
|
+
branch: string,
|
|
52
|
+
opts: ClaimOptions = {},
|
|
53
|
+
): Promise<ClaimReport> {
|
|
54
|
+
const cwd = opts.cwd ?? process.cwd()
|
|
55
|
+
const resolve = opts.resolve ?? resolveSessions
|
|
56
|
+
const listAll = opts.listWorktrees ?? listWorktrees
|
|
57
|
+
const readRefs = opts.branchRefs ?? branchRefs
|
|
58
|
+
|
|
59
|
+
const [repository, worktrees, report, refs] = await Promise.all([
|
|
60
|
+
repositoryOf(cwd),
|
|
61
|
+
listAll(cwd),
|
|
62
|
+
resolve(),
|
|
63
|
+
readRefs(branch, cwd),
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
const worktree =
|
|
67
|
+
worktrees.find((entry) => entry.branch === branch)?.path ?? null
|
|
68
|
+
|
|
69
|
+
const sessions =
|
|
70
|
+
report.kind === 'resolved'
|
|
71
|
+
? report.sessions.filter(
|
|
72
|
+
(session) =>
|
|
73
|
+
session.branch === branch && session.repository === repository,
|
|
74
|
+
)
|
|
75
|
+
: []
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
claimed: worktree !== null || sessions.length > 0 || refs.refs.length > 0,
|
|
79
|
+
worktree,
|
|
80
|
+
sessions,
|
|
81
|
+
refs: refs.refs,
|
|
82
|
+
sessionsReadable: report.kind === 'resolved',
|
|
83
|
+
refsReadable: refs.readable,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import type { SessionRecord } from '@/sessions/registry'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How far the liveness answer can be trusted.
|
|
6
|
+
*
|
|
7
|
+
* `confirmed` means the running process was matched against the start time the
|
|
8
|
+
* record stamped, so the pid belongs to the session that wrote the file.
|
|
9
|
+
* `unverified` means the pid answers a signal probe and nothing more, which
|
|
10
|
+
* cannot separate the original session from whatever inherited its pid.
|
|
11
|
+
*/
|
|
12
|
+
export type Confidence = 'confirmed' | 'unverified'
|
|
13
|
+
|
|
14
|
+
export interface Liveness {
|
|
15
|
+
readonly alive: boolean
|
|
16
|
+
readonly confidence: Confidence
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LivenessProbes {
|
|
20
|
+
/** The start time of a running process, or null where it cannot be read. */
|
|
21
|
+
readonly procStartOf: (pid: number) => string | null
|
|
22
|
+
/** Whether the pid answers at all, ignoring which process holds it. */
|
|
23
|
+
readonly responds: (pid: number) => boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reads a running process's start time from the process filesystem.
|
|
28
|
+
*
|
|
29
|
+
* The comm field is parenthesised and may itself contain spaces, so the split
|
|
30
|
+
* runs from the last `)` rather than over the whole line. Fields resume at the
|
|
31
|
+
* third, which puts the twenty-second at offset nineteen.
|
|
32
|
+
*/
|
|
33
|
+
function procStartOf(pid: number): string | null {
|
|
34
|
+
try {
|
|
35
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8')
|
|
36
|
+
const tail = stat.slice(stat.lastIndexOf(')') + 1).trim()
|
|
37
|
+
return tail.split(/\s+/)[19] ?? null
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Signal zero performs the permission and existence checks and delivers nothing. */
|
|
44
|
+
function responds(pid: number): boolean {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 0)
|
|
47
|
+
return true
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// A live process owned by another user answers EPERM, which is an answer.
|
|
50
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const SYSTEM_PROBES: LivenessProbes = { procStartOf, responds }
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Decides whether a record describes a session that is still running.
|
|
58
|
+
*
|
|
59
|
+
* The two paths are not interchangeable and the caller is told which one ran.
|
|
60
|
+
* Where the process filesystem answers, a start-time mismatch is a dead session
|
|
61
|
+
* whose pid was reused and the record is discarded. Where it does not, the
|
|
62
|
+
* probe alone stands, and every surviving row is marked so a caller never reads
|
|
63
|
+
* a recycled pid as a confirmed identity.
|
|
64
|
+
*/
|
|
65
|
+
export function liveness(
|
|
66
|
+
record: SessionRecord,
|
|
67
|
+
probes: LivenessProbes = SYSTEM_PROBES,
|
|
68
|
+
): Liveness {
|
|
69
|
+
const started = record.procStart
|
|
70
|
+
|
|
71
|
+
if (started !== undefined && started.length > 0) {
|
|
72
|
+
const running = probes.procStartOf(record.pid)
|
|
73
|
+
if (running !== null) {
|
|
74
|
+
return { alive: running === started, confidence: 'confirmed' }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { alive: probes.responds(record.pid), confidence: 'unverified' }
|
|
79
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One session's own record of itself, as the client writes it.
|
|
7
|
+
*
|
|
8
|
+
* Only the fields this domain reads are declared. The client writes several
|
|
9
|
+
* more, and naming them here would put a second copy of its schema in a
|
|
10
|
+
* repository that does not own it.
|
|
11
|
+
*/
|
|
12
|
+
export interface SessionRecord {
|
|
13
|
+
readonly pid: number
|
|
14
|
+
readonly cwd: string
|
|
15
|
+
readonly name: string
|
|
16
|
+
/**
|
|
17
|
+
* Optional because the guard admitting a record checks the three fields above
|
|
18
|
+
* and no more. A client predating any of these writes a record the roster
|
|
19
|
+
* still wants, so the type says what the guard actually proved.
|
|
20
|
+
*/
|
|
21
|
+
readonly sessionId: string | undefined
|
|
22
|
+
readonly kind: string | undefined
|
|
23
|
+
readonly status: string | undefined
|
|
24
|
+
/**
|
|
25
|
+
* The epoch millisecond the client last changed `status`. Measured against
|
|
26
|
+
* the live registry, 23 of 341 usable records carry it, and the one record
|
|
27
|
+
* that has ever carried `status: "waiting"` is not among them, so its
|
|
28
|
+
* absence tracks a client version rather than a record's age alone.
|
|
29
|
+
* Declared here rather than read opportunistically off the parsed object,
|
|
30
|
+
* since this file is what states what the domain reads and an undeclared
|
|
31
|
+
* field read anyway is the drift this domain exists downstream of.
|
|
32
|
+
*/
|
|
33
|
+
readonly statusUpdatedAt: number | undefined
|
|
34
|
+
/**
|
|
35
|
+
* The epoch millisecond the client last wrote the record at all, a coarser
|
|
36
|
+
* stamp than `statusUpdatedAt` that a client writes whether or not it also
|
|
37
|
+
* stamps the status change itself. Declared as the fallback dwell source
|
|
38
|
+
* for a record predating the narrower field, per the same reasoning above.
|
|
39
|
+
*/
|
|
40
|
+
readonly updatedAt: number | undefined
|
|
41
|
+
readonly startedAt: number | undefined
|
|
42
|
+
/**
|
|
43
|
+
* The process start time the client stamped at launch, compared against the
|
|
44
|
+
* running process to separate a live session from a record whose pid has
|
|
45
|
+
* been handed to something else.
|
|
46
|
+
*/
|
|
47
|
+
readonly procStart: string | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the folder holding one file per session.
|
|
52
|
+
*
|
|
53
|
+
* `CLAUDE_CONFIG_DIR` comes first because a client honouring it writes its
|
|
54
|
+
* records nowhere near the home directory, and a read that ignored it would
|
|
55
|
+
* report an empty roster on a machine running sessions.
|
|
56
|
+
*/
|
|
57
|
+
export function registryDir(): string {
|
|
58
|
+
const configured = process.env.CLAUDE_CONFIG_DIR
|
|
59
|
+
const base =
|
|
60
|
+
configured && configured.length > 0
|
|
61
|
+
? configured
|
|
62
|
+
: join(homedir(), '.claude')
|
|
63
|
+
return join(base, 'sessions')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fields whose absence leaves a row unable to answer the question asked of it.
|
|
68
|
+
*
|
|
69
|
+
* The pid has to be positive rather than merely numeric. Signal zero addresses
|
|
70
|
+
* the caller's own process group rather than a process, so a record carrying
|
|
71
|
+
* zero would answer the liveness probe and enter the roster as a live session.
|
|
72
|
+
*/
|
|
73
|
+
function isUsable(value: Partial<SessionRecord>): value is SessionRecord {
|
|
74
|
+
return (
|
|
75
|
+
typeof value.pid === 'number' &&
|
|
76
|
+
Number.isInteger(value.pid) &&
|
|
77
|
+
value.pid > 0 &&
|
|
78
|
+
typeof value.cwd === 'string' &&
|
|
79
|
+
value.cwd.length > 0 &&
|
|
80
|
+
typeof value.name === 'string' &&
|
|
81
|
+
value.name.length > 0
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* An absent folder and an empty one are separate answers.
|
|
87
|
+
*
|
|
88
|
+
* The first means no client ever wrote a record here, so the read never ran and
|
|
89
|
+
* a roster of none would report a machine with no sessions when the truth is a
|
|
90
|
+
* lookup that failed. The second is a machine whose sessions have all ended.
|
|
91
|
+
*/
|
|
92
|
+
export type Registry =
|
|
93
|
+
| { readonly kind: 'absent'; readonly dir: string }
|
|
94
|
+
| {
|
|
95
|
+
readonly kind: 'read'
|
|
96
|
+
readonly dir: string
|
|
97
|
+
readonly records: SessionRecord[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reads every session record in the folder, newest first.
|
|
102
|
+
*
|
|
103
|
+
* A file that does not parse, or that parses without the fields a row is built
|
|
104
|
+
* from, is dropped rather than reported. The folder accumulates a record per
|
|
105
|
+
* session and is never pruned, so it holds thousands of entries from clients
|
|
106
|
+
* spanning many versions, and a finding per stale shape would bury the live
|
|
107
|
+
* sessions this exists to name.
|
|
108
|
+
*/
|
|
109
|
+
export function readRegistry(dir: string = registryDir()): Registry {
|
|
110
|
+
let names: string[]
|
|
111
|
+
try {
|
|
112
|
+
names = readdirSync(dir)
|
|
113
|
+
} catch {
|
|
114
|
+
return { kind: 'absent', dir }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const records: SessionRecord[] = []
|
|
118
|
+
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
if (!name.endsWith('.json')) continue
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const parsed: unknown = JSON.parse(readFileSync(join(dir, name), 'utf8'))
|
|
124
|
+
if (typeof parsed !== 'object' || parsed === null) continue
|
|
125
|
+
const record = parsed as Partial<SessionRecord>
|
|
126
|
+
if (isUsable(record)) records.push(record)
|
|
127
|
+
} catch {
|
|
128
|
+
// Unreadable or malformed. See the note above.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
kind: 'read',
|
|
134
|
+
dir,
|
|
135
|
+
records: records.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0)),
|
|
136
|
+
}
|
|
137
|
+
}
|