@erclx/aitk 0.8.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 +99 -0
- package/claude/.claude-plugin/plugin.json +19 -0
- package/claude/skills/bash-script/SKILL.md +96 -0
- package/claude/skills/bash-script/references/patterns.md +349 -0
- package/claude/skills/ci-workflow/SKILL.md +52 -0
- package/claude/skills/ci-workflow/references/workflows.md +74 -0
- package/claude/skills/claude-address-review/SKILL.md +107 -0
- package/claude/skills/claude-autoship/SKILL.md +137 -0
- package/claude/skills/claude-design-extract/SKILL.md +69 -0
- package/claude/skills/claude-design-propose/SKILL.md +75 -0
- package/claude/skills/claude-diagram/SKILL.md +126 -0
- package/claude/skills/claude-docs/SKILL.md +204 -0
- package/claude/skills/claude-feature/REQUIREMENT.md +35 -0
- package/claude/skills/claude-feature/SKILL.md +148 -0
- package/claude/skills/claude-groundwork/SKILL.md +148 -0
- package/claude/skills/claude-groundwork/references/folder-format.md +107 -0
- package/claude/skills/claude-memory-capture/SKILL.md +80 -0
- package/claude/skills/claude-memory-review/REQUIREMENT.md +37 -0
- package/claude/skills/claude-memory-review/SKILL.md +206 -0
- package/claude/skills/claude-orchestrate/SKILL.md +125 -0
- package/claude/skills/claude-pr-review/SKILL.md +150 -0
- package/claude/skills/claude-review/SKILL.md +136 -0
- package/claude/skills/claude-roadmap/SKILL.md +61 -0
- package/claude/skills/claude-roadmap/references/roadmap.md +52 -0
- package/claude/skills/claude-screencast/SKILL.md +160 -0
- package/claude/skills/claude-seed-sync/SKILL.md +141 -0
- package/claude/skills/claude-slides-draft/SKILL.md +62 -0
- package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
- package/claude/skills/claude-standards-audit/SKILL.md +79 -0
- package/claude/skills/claude-standards-audit/references/branch.md +49 -0
- package/claude/skills/claude-standards-audit/references/pr.md +124 -0
- package/claude/skills/claude-tasks/SKILL.md +123 -0
- package/claude/skills/claude-ui-test/SKILL.md +75 -0
- package/claude/skills/claude-ux-audit/SKILL.md +78 -0
- package/claude/skills/claude-worktree/SKILL.md +79 -0
- package/claude/skills/cli-script/SKILL.md +47 -0
- package/claude/skills/cli-script/references/template.md +43 -0
- package/claude/skills/create-rule/SKILL.md +68 -0
- package/claude/skills/create-skill/REQUIREMENT.md +30 -0
- package/claude/skills/create-skill/SKILL.md +24 -0
- package/claude/skills/create-snippet/SKILL.md +28 -0
- package/claude/skills/create-snippet/references/snippets.md +57 -0
- package/claude/skills/create-standard/SKILL.md +31 -0
- package/claude/skills/create-standard/references/standard.md +52 -0
- package/claude/skills/docs-sync/SKILL.md +87 -0
- package/claude/skills/git-branch/REQUIREMENT.md +38 -0
- package/claude/skills/git-branch/SKILL.md +60 -0
- package/claude/skills/git-branch/references/branch.md +49 -0
- package/claude/skills/git-commit/REQUIREMENT.md +33 -0
- package/claude/skills/git-commit/SKILL.md +51 -0
- package/claude/skills/git-commit/references/commit.md +63 -0
- package/claude/skills/git-followup/REQUIREMENT.md +38 -0
- package/claude/skills/git-followup/SKILL.md +35 -0
- package/claude/skills/git-issue/REQUIREMENT.md +38 -0
- package/claude/skills/git-issue/SKILL.md +69 -0
- package/claude/skills/git-issue/references/issue.md +84 -0
- package/claude/skills/git-pr/REQUIREMENT.md +37 -0
- package/claude/skills/git-pr/SKILL.md +119 -0
- package/claude/skills/git-pr/references/branch.md +49 -0
- package/claude/skills/git-pr/references/pr.md +124 -0
- package/claude/skills/git-ship/REQUIREMENT.md +34 -0
- package/claude/skills/git-ship/SKILL.md +39 -0
- package/claude/skills/git-split/REQUIREMENT.md +38 -0
- package/claude/skills/git-split/SKILL.md +160 -0
- package/claude/skills/git-split/references/branch.md +49 -0
- package/claude/skills/git-split/references/pr.md +124 -0
- package/claude/skills/git-stage/REQUIREMENT.md +36 -0
- package/claude/skills/git-stage/SKILL.md +74 -0
- package/claude/skills/git-stage/references/commit.md +63 -0
- package/claude/skills/git-worktree/REQUIREMENT.md +37 -0
- package/claude/skills/git-worktree/SKILL.md +120 -0
- package/claude/skills/migration-claude-md/SKILL.md +74 -0
- package/claude/skills/migration-context/SKILL.md +93 -0
- package/claude/skills/migration-standards/SKILL.md +75 -0
- package/claude/skills/project-commands/SKILL.md +73 -0
- package/claude/skills/session-resume/SKILL.md +36 -0
- package/claude/skills/setup-gov/SKILL.md +71 -0
- package/claude/skills/setup-indexes/SKILL.md +148 -0
- package/claude/skills/setup-init/SKILL.md +114 -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/SKILL.md +51 -0
- package/claude/skills/systematic-debugging/SKILL.md +70 -0
- package/claude/skills/toolkit-cli/SKILL.md +38 -0
- package/claude/skills/toolkit-feedback/SKILL.md +80 -0
- package/claude/skills/toolkit-operator/SKILL.md +45 -0
- package/claude/skills/toolkit-triage/SKILL.md +63 -0
- package/claude/skills/youtube-transcripts/SKILL.md +34 -0
- package/docs/agents.md +371 -0
- package/docs/ai-workflow.md +157 -0
- package/docs/index.md +20 -0
- package/docs/operating-model.md +106 -0
- package/docs/target-projects.md +183 -0
- package/docs/visual-design-workflow.md +136 -0
- package/docs/zshrc-aliases.md +65 -0
- package/governance/rules/claude/500-prose.md +12 -0
- package/governance/rules/claude/510-context.md +16 -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/560-diagrams.md +11 -0
- package/governance/rules/claude/570-skill.md +18 -0
- package/governance/rules/claude/580-readme.md +11 -0
- package/governance/rules/claude/590-rule-authoring.md +12 -0
- package/governance/rules/core/000-constitution.md +30 -0
- package/governance/rules/core/010-testing.md +35 -0
- package/governance/rules/core/020-concurrency.md +22 -0
- package/governance/rules/core/030-error-handling.md +31 -0
- package/governance/rules/core/040-performance.md +20 -0
- package/governance/rules/core/050-logging.md +20 -0
- package/governance/rules/core/060-naming.md +19 -0
- package/governance/rules/core/070-planning.md +13 -0
- package/governance/rules/core/080-config-comments.md +22 -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/lib/300-testing-ts.md +38 -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 +31 -0
- package/governance/rules/ui/400-ui.md +34 -0
- package/governance/rules/ui/410-a11y.md +44 -0
- package/governance/rules/ui/420-forms.md +34 -0
- package/governance/rules/ui/430-ux-completeness.md +62 -0
- package/governance/stacks/astro.toml +2 -0
- package/governance/stacks/base.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 +67 -0
- package/scripts/config.sh +11 -0
- package/scripts/core/bootstrap.sh +81 -0
- package/scripts/core/check-plugin-boundary.sh +45 -0
- package/scripts/core/check-skill-paths.sh +25 -0
- package/scripts/core/clean.sh +36 -0
- package/scripts/core/install-check.sh +93 -0
- package/scripts/core/regen-claude-copies.sh +26 -0
- package/scripts/core/regen-indexes.sh +10 -0
- package/scripts/core/regen-skill-references.sh +27 -0
- package/scripts/core/snapshot.sh +132 -0
- package/scripts/core/update.sh +35 -0
- package/scripts/core/verify.sh +201 -0
- package/scripts/docs/list.sh +129 -0
- package/scripts/gov/list.sh +234 -0
- package/scripts/lib/frontmatter.sh +30 -0
- package/scripts/lib/gov.sh +14 -0
- package/scripts/lib/sandbox-fixtures.sh +109 -0
- package/scripts/lib/sandbox-git.sh +73 -0
- package/scripts/lib/tooling.sh +24 -0
- package/scripts/lib/ui.sh +173 -0
- package/scripts/lib/worktree.sh +20 -0
- package/scripts/manage-sandbox.sh +453 -0
- package/scripts/snippets/create.sh +156 -0
- package/scripts/standards/list.sh +91 -0
- package/scripts/tooling/create.sh +109 -0
- package/scripts/tooling/ref.sh +141 -0
- package/scripts/tooling/verify.sh +179 -0
- package/snippets/claude/decision-memo.md +39 -0
- package/snippets/claude/feature-recap.md +19 -0
- package/snippets/claude/figma-steps.md +18 -0
- package/snippets/claude/orchestrator-resume.md +23 -0
- package/snippets/claude/orchestrator-sweep.md +10 -0
- package/snippets/compact-summary.md +5 -0
- package/snippets/create-snippet.md +7 -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 +6 -0
- package/snippets/step-by-step.md +10 -0
- package/snippets/web-research.md +21 -0
- package/src/claude/gitignore.ts +51 -0
- package/src/claude/seeds-list.ts +47 -0
- package/src/claude/seeds.ts +124 -0
- package/src/claude/settings.ts +151 -0
- package/src/cli-run.ts +43 -0
- package/src/cli.ts +111 -0
- package/src/commands/claude.ts +349 -0
- package/src/commands/design.ts +39 -0
- package/src/commands/docs.ts +59 -0
- package/src/commands/feedback-format.ts +23 -0
- package/src/commands/feedback.ts +103 -0
- package/src/commands/gov.ts +268 -0
- package/src/commands/indexes.ts +182 -0
- package/src/commands/init.ts +117 -0
- package/src/commands/pass-through.ts +39 -0
- package/src/commands/sandbox.ts +315 -0
- package/src/commands/slides.ts +106 -0
- package/src/commands/snippets.ts +236 -0
- package/src/commands/standards.ts +89 -0
- package/src/commands/sync.ts +211 -0
- package/src/commands/tasks.ts +185 -0
- package/src/commands/tooling.ts +432 -0
- package/src/commands/transcripts.ts +48 -0
- package/src/commands/wiki.ts +101 -0
- package/src/copy.ts +30 -0
- package/src/design/parse.ts +79 -0
- package/src/design/render.ts +170 -0
- package/src/docs/read.ts +59 -0
- package/src/exec.ts +17 -0
- package/src/frontmatter.ts +13 -0
- package/src/github-format.ts +13 -0
- package/src/github.ts +20 -0
- package/src/gov/adapter.ts +73 -0
- package/src/gov/install.ts +103 -0
- package/src/gov/payload.ts +39 -0
- package/src/gov/stacks.ts +124 -0
- package/src/indexes/frontmatter.ts +46 -0
- package/src/indexes/regen.ts +84 -0
- package/src/indexes/render.ts +181 -0
- package/src/indexes/walk.ts +111 -0
- package/src/init/flags.ts +66 -0
- package/src/init/plan.ts +128 -0
- package/src/init/run.ts +46 -0
- package/src/init/steps.ts +91 -0
- package/src/sandbox/coverage.ts +115 -0
- package/src/sandbox/expect.ts +412 -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/adapter.ts +34 -0
- package/src/snippets/categories.ts +59 -0
- package/src/snippets/install.ts +125 -0
- package/src/snippets/list.ts +32 -0
- package/src/snippets/presets.ts +50 -0
- package/src/standards/adapter.ts +51 -0
- package/src/standards/index-refresh.ts +44 -0
- package/src/standards/install.ts +52 -0
- package/src/sync/check.ts +269 -0
- package/src/sync/engine.ts +416 -0
- package/src/sync/git.ts +204 -0
- package/src/sync/stamp.ts +202 -0
- package/src/sync/target.ts +79 -0
- package/src/sync/workflow.ts +201 -0
- package/src/target.ts +43 -0
- package/src/tasks/archive.ts +304 -0
- package/src/tooling/gitignore.ts +122 -0
- package/src/tooling/inject.ts +214 -0
- package/src/tooling/list.ts +39 -0
- package/src/tooling/manifest.ts +178 -0
- package/src/tooling/package.ts +166 -0
- package/src/tooling/scan.ts +186 -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 +187 -0
- package/src/wiki/init.ts +85 -0
- package/standards/architecture.md +41 -0
- package/standards/bundled/branch.md +49 -0
- package/standards/bundled/commit.md +63 -0
- package/standards/bundled/issue.md +84 -0
- package/standards/bundled/pr.md +124 -0
- package/standards/bundled/roadmap.md +52 -0
- package/standards/bundled/snippets.md +57 -0
- package/standards/bundled/standard.md +52 -0
- package/standards/context.md +132 -0
- package/standards/design.md +31 -0
- package/standards/diagrams.md +55 -0
- package/standards/index.md +21 -0
- package/standards/prose.md +122 -0
- package/standards/readme.md +163 -0
- package/standards/requirements.md +61 -0
- package/standards/rule.md +79 -0
- package/standards/skill.md +257 -0
- package/standards/tasks.md +140 -0
- package/standards/versioning.md +47 -0
- package/standards/wireframes.md +67 -0
- package/tooling/astro/configs/astro.config.mjs +17 -0
- package/tooling/astro/configs/eslint.config.js +79 -0
- package/tooling/astro/configs/playwright.config.ts +25 -0
- package/tooling/astro/configs/tsconfig.json +12 -0
- package/tooling/astro/configs/vitest.config.ts +22 -0
- package/tooling/astro/manifest.toml +32 -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/clean.sh +45 -0
- package/tooling/base/configs/scripts/update.sh +49 -0
- package/tooling/base/configs/scripts/verify.sh +64 -0
- package/tooling/base/manifest.toml +32 -0
- package/tooling/base/reference.md +114 -0
- package/tooling/base/seeds/.claude/context/ci.md +27 -0
- package/tooling/base/seeds/.claude/context/development.md +32 -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 +11 -0
- package/tooling/claude/reference.md +62 -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/context/index.md +8 -0
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +43 -0
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +45 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +61 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +60 -0
- package/tooling/claude/seeds/.claude/settings.json +39 -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 +87 -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 +25 -0
- package/tooling/vite-react/configs/tsconfig.json +35 -0
- package/tooling/vite-react/configs/vite.config.ts +14 -0
- package/tooling/vite-react/configs/vitest.config.ts +26 -0
- package/tooling/vite-react/manifest.toml +21 -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 +41 -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/src/test/setup.ts +8 -0
- package/tooling/web/manifest.toml +57 -0
- package/tooling/web/reference.md +95 -0
- package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join, relative, resolve, sep } from 'node:path'
|
|
4
|
+
import { regenOne } from '@/indexes/regen'
|
|
5
|
+
|
|
6
|
+
const TASKS_DIR = join('.claude', 'tasks')
|
|
7
|
+
const ARCHIVE_DIR = join('.claude', '.tmp', 'task-archive')
|
|
8
|
+
const PLANS_DIR = join('.claude', 'plans')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generated and hand-maintained siblings that sit on the board without being
|
|
12
|
+
* tasks. The post-merge hook skips the same two names.
|
|
13
|
+
*/
|
|
14
|
+
const RESERVED_STEMS = ['index', 'priority'] as const
|
|
15
|
+
|
|
16
|
+
export const ARCHIVE_REFUSALS = [
|
|
17
|
+
'no-board',
|
|
18
|
+
'no-match',
|
|
19
|
+
'ambiguous',
|
|
20
|
+
'no-outcomes',
|
|
21
|
+
'open-outcomes',
|
|
22
|
+
'plan-unswept',
|
|
23
|
+
] as const
|
|
24
|
+
|
|
25
|
+
export type ArchiveRefusal = (typeof ARCHIVE_REFUSALS)[number]
|
|
26
|
+
|
|
27
|
+
export type TaskSelector =
|
|
28
|
+
| { readonly kind: 'stem'; readonly stem: string }
|
|
29
|
+
| { readonly kind: 'pull-request'; readonly number: number }
|
|
30
|
+
|
|
31
|
+
export interface ArchiveSuccess {
|
|
32
|
+
readonly ok: true
|
|
33
|
+
readonly stem: string
|
|
34
|
+
readonly from: string
|
|
35
|
+
readonly to: string
|
|
36
|
+
readonly priorityRowRemoved: boolean
|
|
37
|
+
readonly indexRegenerated: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ArchiveRefused {
|
|
41
|
+
readonly ok: false
|
|
42
|
+
readonly reason: ArchiveRefusal
|
|
43
|
+
readonly message: string
|
|
44
|
+
readonly detail: readonly string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type ArchiveOutcome = ArchiveSuccess | ArchiveRefused
|
|
48
|
+
|
|
49
|
+
export interface TaskOutcomes {
|
|
50
|
+
readonly open: readonly string[]
|
|
51
|
+
readonly closed: readonly string[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function tasksDir(root: string): string {
|
|
55
|
+
return join(root, TASKS_DIR)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function archiveDir(root: string): string {
|
|
59
|
+
return join(root, ARCHIVE_DIR)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Splits a task's outcome list by checkbox state. The board format puts every
|
|
64
|
+
* outcome at the top level of `## Outcomes`, so an anchored match is enough and
|
|
65
|
+
* no heading tracking is needed.
|
|
66
|
+
*/
|
|
67
|
+
export function readOutcomes(text: string): TaskOutcomes {
|
|
68
|
+
const open: string[] = []
|
|
69
|
+
const closed: string[] = []
|
|
70
|
+
|
|
71
|
+
for (const line of text.split('\n')) {
|
|
72
|
+
const match = /^- \[([ xX])\] ?(.*)$/.exec(line)
|
|
73
|
+
if (!match) continue
|
|
74
|
+
|
|
75
|
+
const [, box, body] = match
|
|
76
|
+
if (box === ' ') {
|
|
77
|
+
open.push(body.trim())
|
|
78
|
+
} else {
|
|
79
|
+
closed.push(body.trim())
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { open, closed }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Reads the `Pull request:` line `git-pr` writes when a pull request opens.
|
|
88
|
+
* The number is a bare `#NNN` the way `Issue:` is, since neither is a path and
|
|
89
|
+
* a full URL would write the remote into a gitignored file.
|
|
90
|
+
*/
|
|
91
|
+
export function readPullRequest(text: string): number | undefined {
|
|
92
|
+
const match = /^Pull request:\s*#(\d+)\s*$/m.exec(text)
|
|
93
|
+
return match ? Number(match[1]) : undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reads the `Plan:` target out of a markdown link, falling back to the older
|
|
98
|
+
* bare-path form. The path is returned as written, relative to the board.
|
|
99
|
+
*/
|
|
100
|
+
export function readPlanTarget(text: string): string | undefined {
|
|
101
|
+
const match = /^Plan:\s*(?:\[[^\]]*\]\(([^)]+)\)|(\S+))\s*$/m.exec(text)
|
|
102
|
+
if (!match) return undefined
|
|
103
|
+
return match[1] ?? match[2]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Drops the archived task's row from the ordering table. Rows are matched by
|
|
108
|
+
* the link they carry rather than by a line pattern, because a row holds links
|
|
109
|
+
* and prose that a stream edit against it would rewrite in place.
|
|
110
|
+
*/
|
|
111
|
+
export function removePriorityRow(
|
|
112
|
+
text: string,
|
|
113
|
+
stem: string,
|
|
114
|
+
): { readonly text: string; readonly removed: boolean } {
|
|
115
|
+
const target = `](${stem}.md)`
|
|
116
|
+
const lines = text.split('\n')
|
|
117
|
+
const kept = lines.filter((line) => !isRowFor(line, target))
|
|
118
|
+
|
|
119
|
+
return { text: kept.join('\n'), removed: kept.length !== lines.length }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A row is about a task when its first cell links to it. A later cell naming
|
|
124
|
+
* another task is a reference, such as a blocker pointing at what it waits on,
|
|
125
|
+
* and matching anywhere in the line would delete that task's row alongside it.
|
|
126
|
+
*/
|
|
127
|
+
function isRowFor(line: string, target: string): boolean {
|
|
128
|
+
const trimmed = line.trimStart()
|
|
129
|
+
if (!trimmed.startsWith('|')) return false
|
|
130
|
+
|
|
131
|
+
const [, first] = trimmed.split('|')
|
|
132
|
+
return first !== undefined && first.includes(target)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Tests containment rather than a string prefix, so a sibling whose name merely
|
|
137
|
+
* extends the folder's is not read as being inside it.
|
|
138
|
+
*/
|
|
139
|
+
function isUnder(path: string, dir: string): boolean {
|
|
140
|
+
return path === dir || path.startsWith(`${dir}${sep}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolves the `Plan:` target against the board and against the project root
|
|
145
|
+
* both, which is how `claude-docs` reads the same line. It accepts `../plans/x.md`
|
|
146
|
+
* and `.claude/plans/x.md` as one file, so a gate reading only the first form
|
|
147
|
+
* would pass the second and strand the plan this exists to protect.
|
|
148
|
+
*/
|
|
149
|
+
function isLivePlan(target: string, dir: string, root: string): boolean {
|
|
150
|
+
const plans = join(root, PLANS_DIR)
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
isUnder(resolve(dir, target), plans) ||
|
|
154
|
+
isUnder(resolve(root, target), plans)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function listTaskStems(dir: string): Promise<string[]> {
|
|
159
|
+
const entries = await readdir(dir)
|
|
160
|
+
const reserved: readonly string[] = RESERVED_STEMS
|
|
161
|
+
|
|
162
|
+
return entries
|
|
163
|
+
.filter((entry) => entry.endsWith('.md'))
|
|
164
|
+
.map((entry) => entry.slice(0, -'.md'.length))
|
|
165
|
+
.filter((stem) => !reserved.includes(stem))
|
|
166
|
+
.sort()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function matchByPullRequest(
|
|
170
|
+
dir: string,
|
|
171
|
+
stems: readonly string[],
|
|
172
|
+
number: number,
|
|
173
|
+
): Promise<string[]> {
|
|
174
|
+
const read = await Promise.all(
|
|
175
|
+
stems.map(async (stem) => ({
|
|
176
|
+
stem,
|
|
177
|
+
number: readPullRequest(await readFile(join(dir, `${stem}.md`), 'utf8')),
|
|
178
|
+
})),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return read.filter((entry) => entry.number === number).map(({ stem }) => stem)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function refuse(
|
|
185
|
+
reason: ArchiveRefusal,
|
|
186
|
+
message: string,
|
|
187
|
+
detail: readonly string[] = [],
|
|
188
|
+
): ArchiveRefused {
|
|
189
|
+
return { ok: false, reason, message, detail }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function resolveStem(
|
|
193
|
+
dir: string,
|
|
194
|
+
selector: TaskSelector,
|
|
195
|
+
): Promise<string | ArchiveRefused> {
|
|
196
|
+
const stems = await listTaskStems(dir)
|
|
197
|
+
|
|
198
|
+
if (selector.kind === 'stem') {
|
|
199
|
+
if (!stems.includes(selector.stem)) {
|
|
200
|
+
return refuse(
|
|
201
|
+
'no-match',
|
|
202
|
+
`No task named ${selector.stem} on the board.`,
|
|
203
|
+
stems,
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
return selector.stem
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const matched = await matchByPullRequest(dir, stems, selector.number)
|
|
210
|
+
|
|
211
|
+
if (matched.length === 0) {
|
|
212
|
+
return refuse('no-match', `No task names pull request #${selector.number}.`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (matched.length > 1) {
|
|
216
|
+
return refuse(
|
|
217
|
+
'ambiguous',
|
|
218
|
+
`${matched.length} tasks name pull request #${selector.number}. One task, one pull request.`,
|
|
219
|
+
matched,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return matched[0]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Archives one task as a single unit: the move, the ordering-row removal, and
|
|
228
|
+
* the index regen. The hook and `claude-tasks` both call this, so every gate
|
|
229
|
+
* refuses rather than reports. A caller with nobody watching cannot act on a
|
|
230
|
+
* warning, and two callers gating differently is the drift this exists to stop.
|
|
231
|
+
*/
|
|
232
|
+
export async function archiveTask(
|
|
233
|
+
root: string,
|
|
234
|
+
selector: TaskSelector,
|
|
235
|
+
): Promise<ArchiveOutcome> {
|
|
236
|
+
const dir = tasksDir(root)
|
|
237
|
+
|
|
238
|
+
if (!existsSync(dir)) {
|
|
239
|
+
return refuse('no-board', `No task board at ${relative(root, dir)}.`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const resolved = await resolveStem(dir, selector)
|
|
243
|
+
if (typeof resolved !== 'string') return resolved
|
|
244
|
+
|
|
245
|
+
const stem = resolved
|
|
246
|
+
const from = join(dir, `${stem}.md`)
|
|
247
|
+
const text = await readFile(from, 'utf8')
|
|
248
|
+
|
|
249
|
+
const { open, closed } = readOutcomes(text)
|
|
250
|
+
|
|
251
|
+
if (open.length > 0) {
|
|
252
|
+
return refuse(
|
|
253
|
+
'open-outcomes',
|
|
254
|
+
`${stem} has ${open.length} open outcome(s). Close them or cut them from the task, then archive.`,
|
|
255
|
+
open,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (closed.length === 0) {
|
|
260
|
+
return refuse(
|
|
261
|
+
'no-outcomes',
|
|
262
|
+
`${stem} carries no outcomes, so nothing marks it shipped.`,
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const planTarget = readPlanTarget(text)
|
|
267
|
+
if (planTarget && isLivePlan(planTarget, dir, root)) {
|
|
268
|
+
return refuse(
|
|
269
|
+
'plan-unswept',
|
|
270
|
+
`${stem} still points at a live plan. Run /claude-docs to sweep it first, then archive.`,
|
|
271
|
+
[planTarget],
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const destination = archiveDir(root)
|
|
276
|
+
await mkdir(destination, { recursive: true })
|
|
277
|
+
const to = join(destination, `${stem}.md`)
|
|
278
|
+
await rename(from, to)
|
|
279
|
+
|
|
280
|
+
const priorityRowRemoved = await clearPriorityRow(dir, stem)
|
|
281
|
+
const regen = await regenOne(dir, { dryRun: false })
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
stem,
|
|
286
|
+
from,
|
|
287
|
+
to,
|
|
288
|
+
priorityRowRemoved,
|
|
289
|
+
indexRegenerated: regen.action === 'written',
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function clearPriorityRow(dir: string, stem: string): Promise<boolean> {
|
|
294
|
+
const path = join(dir, 'priority.md')
|
|
295
|
+
if (!existsSync(path)) return false
|
|
296
|
+
|
|
297
|
+
const { text, removed } = removePriorityRow(
|
|
298
|
+
await readFile(path, 'utf8'),
|
|
299
|
+
stem,
|
|
300
|
+
)
|
|
301
|
+
if (removed) await writeFile(path, text)
|
|
302
|
+
|
|
303
|
+
return removed
|
|
304
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { GitignoreSection } from '@/tooling/manifest'
|
|
2
|
+
|
|
3
|
+
export interface MergeResult {
|
|
4
|
+
readonly content: string
|
|
5
|
+
readonly added: readonly string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PruneResult {
|
|
9
|
+
readonly content: string
|
|
10
|
+
readonly removed: readonly string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Splits a gitignore body the way `while IFS= read -r line` did, so a file
|
|
15
|
+
* with or without a trailing newline yields the same logical lines.
|
|
16
|
+
*/
|
|
17
|
+
function toLines(content: string): string[] {
|
|
18
|
+
if (content === '') return []
|
|
19
|
+
const lines = content.split('\n')
|
|
20
|
+
if (lines[lines.length - 1] === '') lines.pop()
|
|
21
|
+
return lines
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fromLines(lines: readonly string[]): string {
|
|
25
|
+
if (lines.length === 0) return ''
|
|
26
|
+
return `${lines.join('\n')}\n`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function withoutTrailingSlash(entry: string): string {
|
|
30
|
+
return entry.endsWith('/') ? entry.slice(0, -1) : entry
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasEntry(lines: readonly string[], entry: string): boolean {
|
|
34
|
+
return lines.includes(entry) || lines.includes(withoutTrailingSlash(entry))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Appends every manifest entry the file does not already carry.
|
|
39
|
+
*
|
|
40
|
+
* Two behaviors are load-bearing and preserved from `merge_gitignore`. The
|
|
41
|
+
* section header is written only when the whole section is missing, so a
|
|
42
|
+
* partially present section appends its entries bare at the end of the file.
|
|
43
|
+
* Presence is re-checked against the growing file, so an entry added by an
|
|
44
|
+
* earlier section or an earlier stack in the chain is not added twice.
|
|
45
|
+
*/
|
|
46
|
+
export function mergeSections(
|
|
47
|
+
content: string,
|
|
48
|
+
sections: readonly GitignoreSection[],
|
|
49
|
+
): MergeResult {
|
|
50
|
+
let next = content
|
|
51
|
+
const added: string[] = []
|
|
52
|
+
|
|
53
|
+
for (const section of sections) {
|
|
54
|
+
const lines = toLines(next)
|
|
55
|
+
const missing = section.entries.filter((entry) => !hasEntry(lines, entry))
|
|
56
|
+
if (missing.length === 0) continue
|
|
57
|
+
|
|
58
|
+
if (missing.length === section.entries.length) {
|
|
59
|
+
next += `\n${section.header}\n`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const entry of missing) {
|
|
63
|
+
next += `${entry}\n`
|
|
64
|
+
added.push(entry)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { content: next, added }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Drops entries that sit under a managed header but are no longer in the
|
|
73
|
+
* manifest. A section runs from its header line until the first blank line or
|
|
74
|
+
* the next comment, matching `prune_gitignore_section`.
|
|
75
|
+
*/
|
|
76
|
+
export function pruneSections(
|
|
77
|
+
content: string,
|
|
78
|
+
sections: readonly GitignoreSection[],
|
|
79
|
+
): PruneResult {
|
|
80
|
+
let lines = toLines(content)
|
|
81
|
+
const removed: string[] = []
|
|
82
|
+
|
|
83
|
+
for (const section of sections) {
|
|
84
|
+
const kept: string[] = []
|
|
85
|
+
let inSection = false
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (line === section.header) {
|
|
89
|
+
inSection = true
|
|
90
|
+
kept.push(line)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!inSection) {
|
|
95
|
+
kept.push(line)
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (line === '' || line.startsWith('#')) {
|
|
100
|
+
inSection = false
|
|
101
|
+
kept.push(line)
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const isAllowed = section.entries.some(
|
|
106
|
+
(entry) =>
|
|
107
|
+
line === entry ||
|
|
108
|
+
withoutTrailingSlash(line) === withoutTrailingSlash(entry),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (isAllowed) {
|
|
112
|
+
kept.push(line)
|
|
113
|
+
} else {
|
|
114
|
+
removed.push(line)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lines = kept
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { content: removed.length > 0 ? fromLines(lines) : content, removed }
|
|
122
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import {
|
|
3
|
+
copyFile,
|
|
4
|
+
mkdir,
|
|
5
|
+
readFile,
|
|
6
|
+
rm,
|
|
7
|
+
rmdir,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from 'node:fs/promises'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
|
+
import { $ } from 'bun'
|
|
12
|
+
import { copyPreservingMode } from '@/copy'
|
|
13
|
+
import { mergeSections, pruneSections } from '@/tooling/gitignore'
|
|
14
|
+
import { ancestorsFirst, listFiles, type Manifest } from '@/tooling/manifest'
|
|
15
|
+
import {
|
|
16
|
+
applyScripts,
|
|
17
|
+
collectDeps,
|
|
18
|
+
readPackage,
|
|
19
|
+
serializePackage,
|
|
20
|
+
} from '@/tooling/package'
|
|
21
|
+
import { logAdd, logRemove, logStep } from '@/ui'
|
|
22
|
+
|
|
23
|
+
export async function injectConfigs(
|
|
24
|
+
chain: readonly Manifest[],
|
|
25
|
+
target: string,
|
|
26
|
+
): Promise<string[]> {
|
|
27
|
+
const applied: string[] = []
|
|
28
|
+
|
|
29
|
+
for (const manifest of ancestorsFirst(chain)) {
|
|
30
|
+
const files = listFiles(manifest.configsDir)
|
|
31
|
+
if (files.length === 0) continue
|
|
32
|
+
|
|
33
|
+
logStep(`Applying ${manifest.name} configs`)
|
|
34
|
+
for (const rel of files) {
|
|
35
|
+
await copyPreservingMode(
|
|
36
|
+
join(manifest.configsDir, rel),
|
|
37
|
+
join(target, rel),
|
|
38
|
+
)
|
|
39
|
+
logAdd(rel)
|
|
40
|
+
applied.push(rel)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return applied
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Copies a seed when the target lacks it. When the target already has one and
|
|
49
|
+
* the seed is a `.txt` word list, missing lines are appended and the file is
|
|
50
|
+
* re-sorted. Every other existing file is left untouched.
|
|
51
|
+
*/
|
|
52
|
+
async function mergeSeedFile(src: string, dest: string): Promise<void> {
|
|
53
|
+
await mkdir(dirname(dest), { recursive: true })
|
|
54
|
+
|
|
55
|
+
if (!existsSync(dest)) {
|
|
56
|
+
await copyFile(src, dest)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!src.endsWith('.txt')) return
|
|
61
|
+
|
|
62
|
+
const existing = (await readFile(dest, 'utf8')).split('\n')
|
|
63
|
+
const incoming = (await readFile(src, 'utf8')).split('\n')
|
|
64
|
+
const missing = incoming.filter(
|
|
65
|
+
(word) => word !== '' && !existing.includes(word),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if (missing.length === 0) return
|
|
69
|
+
|
|
70
|
+
const merged = [...existing.filter((line) => line !== ''), ...missing].sort()
|
|
71
|
+
await writeFile(dest, `${merged.join('\n')}\n`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function injectSeeds(
|
|
75
|
+
chain: readonly Manifest[],
|
|
76
|
+
target: string,
|
|
77
|
+
): Promise<string[]> {
|
|
78
|
+
const applied: string[] = []
|
|
79
|
+
|
|
80
|
+
for (const manifest of ancestorsFirst(chain)) {
|
|
81
|
+
const files = listFiles(manifest.seedsDir)
|
|
82
|
+
if (files.length === 0) continue
|
|
83
|
+
|
|
84
|
+
logStep(`Applying ${manifest.name} seeds`)
|
|
85
|
+
for (const rel of files) {
|
|
86
|
+
await mergeSeedFile(join(manifest.seedsDir, rel), join(target, rel))
|
|
87
|
+
logAdd(rel)
|
|
88
|
+
applied.push(rel)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return applied
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function injectGitignore(
|
|
96
|
+
chain: readonly Manifest[],
|
|
97
|
+
target: string,
|
|
98
|
+
): Promise<string[]> {
|
|
99
|
+
const path = join(target, '.gitignore')
|
|
100
|
+
const added: string[] = []
|
|
101
|
+
|
|
102
|
+
for (const manifest of ancestorsFirst(chain)) {
|
|
103
|
+
if (manifest.gitignore.length === 0) continue
|
|
104
|
+
|
|
105
|
+
const content = existsSync(path) ? await readFile(path, 'utf8') : ''
|
|
106
|
+
const result = mergeSections(content, manifest.gitignore)
|
|
107
|
+
if (result.added.length === 0) continue
|
|
108
|
+
|
|
109
|
+
logStep(`Applying ${manifest.name} gitignore`)
|
|
110
|
+
await writeFile(path, result.content)
|
|
111
|
+
for (const entry of result.added) {
|
|
112
|
+
logAdd(entry)
|
|
113
|
+
added.push(entry)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (added.length === 0 && !existsSync(path)) await writeFile(path, '')
|
|
118
|
+
|
|
119
|
+
return added
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function pruneGitignore(
|
|
123
|
+
chain: readonly Manifest[],
|
|
124
|
+
target: string,
|
|
125
|
+
): Promise<string[]> {
|
|
126
|
+
const path = join(target, '.gitignore')
|
|
127
|
+
if (!existsSync(path)) return []
|
|
128
|
+
|
|
129
|
+
const removed: string[] = []
|
|
130
|
+
|
|
131
|
+
for (const manifest of ancestorsFirst(chain)) {
|
|
132
|
+
if (manifest.gitignore.length === 0) continue
|
|
133
|
+
|
|
134
|
+
const content = await readFile(path, 'utf8')
|
|
135
|
+
const result = pruneSections(content, manifest.gitignore)
|
|
136
|
+
if (result.removed.length === 0) continue
|
|
137
|
+
|
|
138
|
+
await writeFile(path, result.content)
|
|
139
|
+
for (const entry of result.removed) {
|
|
140
|
+
logRemove(entry)
|
|
141
|
+
removed.push(entry)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return removed
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Installs missing dev dependencies, fills package scripts, and merges
|
|
150
|
+
* gitignore entries. This is the `inject_tooling_manifest` sequence, which
|
|
151
|
+
* assumed a target that already has a package.json.
|
|
152
|
+
*/
|
|
153
|
+
export async function injectManifest(
|
|
154
|
+
chain: readonly Manifest[],
|
|
155
|
+
target: string,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
const packagePath = join(target, 'package.json')
|
|
158
|
+
const pkg = readPackage(packagePath)
|
|
159
|
+
|
|
160
|
+
if (pkg) {
|
|
161
|
+
const missing = collectDeps(chain, pkg)
|
|
162
|
+
.filter((dep) => dep.state === 'missing')
|
|
163
|
+
.map((dep) => dep.spec)
|
|
164
|
+
|
|
165
|
+
if (missing.length > 0) {
|
|
166
|
+
logStep('Installing missing dependencies')
|
|
167
|
+
await $`bun add -D ${missing}`.cwd(target)
|
|
168
|
+
for (const spec of missing) logAdd(spec)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const fresh = readPackage(packagePath)
|
|
172
|
+
if (fresh) {
|
|
173
|
+
const result = applyScripts(fresh, chain)
|
|
174
|
+
if (result.added.length > 0 || result.overridden.length > 0) {
|
|
175
|
+
logStep('Applying scripts')
|
|
176
|
+
for (const key of result.added) logAdd(key)
|
|
177
|
+
for (const key of result.overridden) logAdd(key)
|
|
178
|
+
await writeFile(packagePath, serializePackage(result.pkg))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await injectGitignore(chain, target)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Drops reference docs into `.claude/tooling/`, clearing the legacy
|
|
188
|
+
* `tooling/` location the earlier layout used.
|
|
189
|
+
*/
|
|
190
|
+
export async function applyReferences(
|
|
191
|
+
chain: readonly Manifest[],
|
|
192
|
+
target: string,
|
|
193
|
+
stacks: readonly string[],
|
|
194
|
+
): Promise<string[]> {
|
|
195
|
+
const destDir = join(target, '.claude', 'tooling')
|
|
196
|
+
await mkdir(destDir, { recursive: true })
|
|
197
|
+
|
|
198
|
+
const applied: string[] = []
|
|
199
|
+
const byName = new Map(chain.map((manifest) => [manifest.name, manifest]))
|
|
200
|
+
|
|
201
|
+
for (const stack of stacks) {
|
|
202
|
+
const manifest = byName.get(stack)
|
|
203
|
+
if (!manifest) continue
|
|
204
|
+
|
|
205
|
+
await copyFile(manifest.referenceFile, join(destDir, `${stack}.md`))
|
|
206
|
+
logAdd(`.claude/tooling/${stack}.md`)
|
|
207
|
+
applied.push(stack)
|
|
208
|
+
await rm(join(target, 'tooling', `${stack}.md`), { force: true })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await rmdir(join(target, 'tooling')).catch(() => {})
|
|
212
|
+
|
|
213
|
+
return applied
|
|
214
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { listStacks, loadManifest } from '@/tooling/manifest'
|
|
2
|
+
|
|
3
|
+
export interface StackSummary {
|
|
4
|
+
readonly name: string
|
|
5
|
+
readonly extends: string | null
|
|
6
|
+
readonly devDeps: number
|
|
7
|
+
readonly scripts: number
|
|
8
|
+
readonly gitignoreGroups: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Summarizes each installable stack. The counts come from `loadManifest`
|
|
13
|
+
* rather than the three `awk` programs the bash carried, so the list and the
|
|
14
|
+
* sync it previews read the same parse.
|
|
15
|
+
*/
|
|
16
|
+
export function buildStackSummaries(root: string): StackSummary[] {
|
|
17
|
+
const summaries: StackSummary[] = []
|
|
18
|
+
|
|
19
|
+
for (const stack of listStacks(root)) {
|
|
20
|
+
const manifest = loadManifest(root, stack)
|
|
21
|
+
if (!manifest) continue
|
|
22
|
+
|
|
23
|
+
summaries.push({
|
|
24
|
+
name: manifest.name,
|
|
25
|
+
extends: manifest.parent ?? null,
|
|
26
|
+
devDeps: manifest.devPackages.length,
|
|
27
|
+
scripts: Object.keys(manifest.scripts).length,
|
|
28
|
+
gitignoreGroups: manifest.gitignore.length,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return summaries
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function describeStack(summary: StackSummary): string {
|
|
36
|
+
const counts = `${summary.devDeps} dev deps, ${summary.scripts} scripts, ${summary.gitignoreGroups} gitignore groups`
|
|
37
|
+
const prefix = summary.extends === null ? '' : `extends: ${summary.extends}, `
|
|
38
|
+
return `${summary.name} (${prefix}${counts})`
|
|
39
|
+
}
|