@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,202 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join, sep } from 'node:path'
|
|
5
|
+
import { execa } from 'execa'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Domains the stamp can attribute. Tooling runs its own inject and manifest
|
|
9
|
+
* machinery rather than the sync engine, so it is absent by design.
|
|
10
|
+
*/
|
|
11
|
+
export const STAMP_DOMAINS = ['standards', 'snippets', 'governance'] as const
|
|
12
|
+
|
|
13
|
+
export type StampDomain = (typeof STAMP_DOMAINS)[number]
|
|
14
|
+
|
|
15
|
+
/** What a domain needs to stamp itself: its key, and the clone to date it against. */
|
|
16
|
+
export interface StampSource {
|
|
17
|
+
readonly domain: StampDomain
|
|
18
|
+
readonly toolkitRoot: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Target-relative posix path to content hash, for one domain. */
|
|
22
|
+
export type DomainHashes = Readonly<Record<string, string>>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One domain's record. The anchor sits here rather than at the top level
|
|
26
|
+
* because domains sync independently, and a shared anchor would let a gov sync
|
|
27
|
+
* advance the revision standards measures its upstream range from.
|
|
28
|
+
*/
|
|
29
|
+
export interface DomainStamp {
|
|
30
|
+
/** Toolkit revision this domain last synced from. Absent outside a git clone. */
|
|
31
|
+
readonly commit?: string
|
|
32
|
+
readonly syncedAt: string
|
|
33
|
+
readonly files: DomainHashes
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Stamp {
|
|
37
|
+
/** Domains actually stamped in this target, so an unstamped one is legible. */
|
|
38
|
+
readonly covers: readonly StampDomain[]
|
|
39
|
+
readonly domains: Readonly<Partial<Record<StampDomain, DomainStamp>>>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function stampPath(target: string): string {
|
|
43
|
+
return join(target, '.claude', 'aitk.json')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function hashContent(content: Buffer | string): string {
|
|
47
|
+
return `sha256:${createHash('sha256').update(content).digest('hex')}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function hashFile(path: string): string {
|
|
51
|
+
return hashContent(readFileSync(path))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Keys are stored posix-style so a stamp written on one platform still
|
|
56
|
+
* resolves on another.
|
|
57
|
+
*/
|
|
58
|
+
export function toStampKey(rel: string): string {
|
|
59
|
+
return rel.split(sep).join('/')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A missing or corrupt stamp reads as absent rather than failing, which is what
|
|
64
|
+
* keeps every unstamped target on the existing unattributed path.
|
|
65
|
+
*/
|
|
66
|
+
export function readStamp(target: string): Stamp | undefined {
|
|
67
|
+
const path = stampPath(target)
|
|
68
|
+
if (!existsSync(path)) return undefined
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
72
|
+
return isStamp(parsed) ? parsed : undefined
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The revision this domain last synced from, which bounds its upstream range. */
|
|
79
|
+
export function stampedCommit(
|
|
80
|
+
stamp: Stamp | undefined,
|
|
81
|
+
domain: StampDomain,
|
|
82
|
+
): string | undefined {
|
|
83
|
+
return stamp?.domains[domain]?.commit
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function stampedHashes(
|
|
87
|
+
stamp: Stamp | undefined,
|
|
88
|
+
domain: StampDomain | undefined,
|
|
89
|
+
): DomainHashes {
|
|
90
|
+
if (stamp === undefined || domain === undefined) return {}
|
|
91
|
+
return stamp.domains[domain]?.files ?? {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Replaces one domain's record and leaves the others untouched, because domains
|
|
96
|
+
* install and sync independently but share the one file.
|
|
97
|
+
*/
|
|
98
|
+
export async function writeStamp(
|
|
99
|
+
target: string,
|
|
100
|
+
source: StampSource,
|
|
101
|
+
hashes: DomainHashes,
|
|
102
|
+
now: Date,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const previous = readStamp(target)
|
|
105
|
+
const commit = await toolkitCommit(source.toolkitRoot)
|
|
106
|
+
|
|
107
|
+
const record: DomainStamp = {
|
|
108
|
+
...(commit === undefined ? {} : { commit }),
|
|
109
|
+
syncedAt: now.toISOString(),
|
|
110
|
+
files: sortKeys(hashes),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const domains = sortDomains({
|
|
114
|
+
...previous?.domains,
|
|
115
|
+
[source.domain]: record,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const stamp: Stamp = {
|
|
119
|
+
covers: STAMP_DOMAINS.filter((domain) => domains[domain] !== undefined),
|
|
120
|
+
domains,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const path = stampPath(target)
|
|
124
|
+
await mkdir(dirname(path), { recursive: true })
|
|
125
|
+
await writeFile(path, `${JSON.stringify(stamp, null, 2)}\n`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const commitCache = new Map<string, Promise<string | undefined>>()
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolved once per clone per process. A toolkit reached outside a git clone
|
|
132
|
+
* yields no commit, which costs the report its upstream range but not its
|
|
133
|
+
* per-file attribution.
|
|
134
|
+
*/
|
|
135
|
+
export function toolkitCommit(root: string): Promise<string | undefined> {
|
|
136
|
+
const cached = commitCache.get(root)
|
|
137
|
+
if (cached !== undefined) return cached
|
|
138
|
+
|
|
139
|
+
const pending = readCommit(root)
|
|
140
|
+
commitCache.set(root, pending)
|
|
141
|
+
return pending
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function readCommit(root: string): Promise<string | undefined> {
|
|
145
|
+
const result = await execa(
|
|
146
|
+
'git',
|
|
147
|
+
['-C', root, 'rev-parse', '--short', 'HEAD'],
|
|
148
|
+
{ reject: false },
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return result.exitCode === 0 && result.stdout.trim() !== ''
|
|
152
|
+
? result.stdout.trim()
|
|
153
|
+
: undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Deterministic key order keeps a re-sync diff empty and a merge conflict local. */
|
|
157
|
+
function sortKeys(hashes: DomainHashes): DomainHashes {
|
|
158
|
+
return Object.fromEntries(
|
|
159
|
+
Object.entries(hashes).sort(([left], [right]) => left.localeCompare(right)),
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function sortDomains(
|
|
164
|
+
domains: Partial<Record<StampDomain, DomainStamp>>,
|
|
165
|
+
): Partial<Record<StampDomain, DomainStamp>> {
|
|
166
|
+
const sorted: Partial<Record<StampDomain, DomainStamp>> = {}
|
|
167
|
+
for (const domain of STAMP_DOMAINS) {
|
|
168
|
+
const record = domains[domain]
|
|
169
|
+
if (record !== undefined) sorted[domain] = record
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return sorted
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validates every domain record rather than only the container. A stamp whose
|
|
177
|
+
* domain value is the wrong shape would otherwise reach `attribute`, where a
|
|
178
|
+
* bad hash lookup silently reads as a customization.
|
|
179
|
+
*/
|
|
180
|
+
function isStamp(value: unknown): value is Stamp {
|
|
181
|
+
if (!isRecord(value) || !isRecord(value.domains)) return false
|
|
182
|
+
|
|
183
|
+
return Object.entries(value.domains).every(
|
|
184
|
+
([domain, record]) =>
|
|
185
|
+
(STAMP_DOMAINS as readonly string[]).includes(domain) &&
|
|
186
|
+
isDomainStamp(record),
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isDomainStamp(value: unknown): value is DomainStamp {
|
|
191
|
+
if (!isRecord(value) || !isRecord(value.files)) return false
|
|
192
|
+
if (typeof value.syncedAt !== 'string') return false
|
|
193
|
+
if (value.commit !== undefined && typeof value.commit !== 'string') {
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return Object.values(value.files).every((hash) => typeof hash === 'string')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
201
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
202
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { isDirectory } from '@/target'
|
|
4
|
+
|
|
5
|
+
export const SYNC_DOMAINS = [
|
|
6
|
+
'standards',
|
|
7
|
+
'snippets',
|
|
8
|
+
'governance',
|
|
9
|
+
'claude',
|
|
10
|
+
] as const
|
|
11
|
+
|
|
12
|
+
export type SyncDomain = (typeof SYNC_DOMAINS)[number]
|
|
13
|
+
|
|
14
|
+
export interface DomainState {
|
|
15
|
+
readonly domain: SyncDomain
|
|
16
|
+
readonly installed: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DOMAIN_MARKERS: Record<SyncDomain, string> = {
|
|
20
|
+
standards: join('.claude', 'standards'),
|
|
21
|
+
snippets: join('.claude', 'snippets'),
|
|
22
|
+
governance: join('.claude', 'rules'),
|
|
23
|
+
claude: '.claude',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Paths whose git status decides whether a domain contributed to the sync
|
|
28
|
+
* commit. Governance carries the retired `.claude/GOV.md` because a sync that
|
|
29
|
+
* removes it is still a governance change, and `claude` watches `.gitignore`
|
|
30
|
+
* because that is the only file `aitk claude sync` writes.
|
|
31
|
+
*/
|
|
32
|
+
const DOMAIN_PATHS: Record<SyncDomain, readonly string[]> = {
|
|
33
|
+
standards: ['.claude/standards/'],
|
|
34
|
+
snippets: ['.claude/snippets/'],
|
|
35
|
+
governance: ['.claude/rules/', '.claude/GOV.md'],
|
|
36
|
+
claude: ['.gitignore'],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function domainPaths(domain: SyncDomain): readonly string[] {
|
|
40
|
+
return DOMAIN_PATHS[domain]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function detectDomains(target: string): DomainState[] {
|
|
44
|
+
return SYNC_DOMAINS.map((domain) => ({
|
|
45
|
+
domain,
|
|
46
|
+
installed: isDirectory(join(target, DOMAIN_MARKERS[domain])),
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function installedDomains(states: readonly DomainState[]): SyncDomain[] {
|
|
51
|
+
return states.filter((state) => state.installed).map((state) => state.domain)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether a domain's sync command should run, which is not the same question
|
|
56
|
+
* `detectDomains` answers. Governance reports as absent without
|
|
57
|
+
* `.claude/rules/`, yet still syncs when only the retired `.claude/GOV.md`
|
|
58
|
+
* remains, so that the sync can delete it. The bash drew the same distinction
|
|
59
|
+
* between `detect_domains` and `run_syncs`.
|
|
60
|
+
*/
|
|
61
|
+
export function shouldSync(target: string, domain: SyncDomain): boolean {
|
|
62
|
+
if (domain === 'governance') {
|
|
63
|
+
return (
|
|
64
|
+
isDirectory(join(target, '.claude', 'rules')) ||
|
|
65
|
+
existsSync(join(target, '.claude', 'GOV.md'))
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return isDirectory(join(target, DOMAIN_MARKERS[domain]))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A target with no `.git/` reports clean, because `git status` fails there and
|
|
74
|
+
* the bash swallowed that failure. The guard exists to make the sync commit
|
|
75
|
+
* survivable, and a directory outside git has no commit to protect.
|
|
76
|
+
*/
|
|
77
|
+
export function isTreeClean(status: string): boolean {
|
|
78
|
+
return status.trim() === ''
|
|
79
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import {
|
|
3
|
+
changedNames,
|
|
4
|
+
changedPaths,
|
|
5
|
+
classifyStatus,
|
|
6
|
+
commitMessage,
|
|
7
|
+
type DomainChange,
|
|
8
|
+
type GitRunner,
|
|
9
|
+
type PullRequestOpener,
|
|
10
|
+
pullRequestBody,
|
|
11
|
+
syncBranchName,
|
|
12
|
+
} from '@/sync/git'
|
|
13
|
+
import { domainPaths, SYNC_DOMAINS } from '@/sync/target'
|
|
14
|
+
import { isDirectory } from '@/target'
|
|
15
|
+
import {
|
|
16
|
+
intro,
|
|
17
|
+
logError,
|
|
18
|
+
logInfo,
|
|
19
|
+
logStep,
|
|
20
|
+
logWarn,
|
|
21
|
+
outro,
|
|
22
|
+
pipeOutput,
|
|
23
|
+
select,
|
|
24
|
+
} from '@/ui'
|
|
25
|
+
|
|
26
|
+
const GREEN = '\x1b[0;32m'
|
|
27
|
+
const NC = '\x1b[0m'
|
|
28
|
+
|
|
29
|
+
const PROTECTED_BRANCHES: readonly string[] = ['main', 'master']
|
|
30
|
+
|
|
31
|
+
export type WorkflowChoice = 'pr' | 'commit' | 'cancel'
|
|
32
|
+
|
|
33
|
+
export interface WorkflowDeps {
|
|
34
|
+
readonly git: GitRunner
|
|
35
|
+
/** Absent when `gh` is not installed, which hides the pull request option. */
|
|
36
|
+
readonly pullRequests: PullRequestOpener | undefined
|
|
37
|
+
readonly now: Date
|
|
38
|
+
readonly nonInteractive: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Defaults to the interactive prompt. A test injects it to reach the commit
|
|
41
|
+
* and pull request branches, which `select` otherwise gates behind a TTY.
|
|
42
|
+
*/
|
|
43
|
+
readonly choose?: (canOpenPullRequest: boolean) => Promise<WorkflowChoice>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Groups the target's pending changes by the domain that produced them. Each
|
|
48
|
+
* domain is read independently, so the four status calls run together.
|
|
49
|
+
*/
|
|
50
|
+
export async function collectChanges(git: GitRunner): Promise<DomainChange[]> {
|
|
51
|
+
const results = await Promise.all(
|
|
52
|
+
SYNC_DOMAINS.map(async (domain) => {
|
|
53
|
+
const status = await git.status(domainPaths(domain))
|
|
54
|
+
return { domain, status }
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const changes: DomainChange[] = []
|
|
59
|
+
|
|
60
|
+
for (const { domain, status } of results) {
|
|
61
|
+
const names = changedNames(status)
|
|
62
|
+
if (names.length === 0) continue
|
|
63
|
+
|
|
64
|
+
changes.push({
|
|
65
|
+
domain,
|
|
66
|
+
verb: classifyStatus(status),
|
|
67
|
+
names,
|
|
68
|
+
paths: changedPaths(status),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return changes
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Commits what the domain syncs wrote and optionally opens a pull request.
|
|
77
|
+
*
|
|
78
|
+
* This is the only toolkit code that writes to a git remote, which is why a
|
|
79
|
+
* headless run refuses it outright. `select_option` resolved to its first
|
|
80
|
+
* option under `AITK_NON_INTERACTIVE=1`, and the first option here was
|
|
81
|
+
* `Commit and open PR` whenever `gh` was installed, so an agent following the
|
|
82
|
+
* documented non-interactive path pushed a branch and opened a pull request on
|
|
83
|
+
* someone's repository without being asked.
|
|
84
|
+
*/
|
|
85
|
+
export async function runGitWorkflow(
|
|
86
|
+
target: string,
|
|
87
|
+
deps: WorkflowDeps,
|
|
88
|
+
): Promise<number> {
|
|
89
|
+
if (!isDirectory(join(target, '.git'))) return 0
|
|
90
|
+
|
|
91
|
+
const changes = await collectChanges(deps.git)
|
|
92
|
+
if (changes.length === 0) return 0
|
|
93
|
+
|
|
94
|
+
const domains = changes.map((change) => change.domain)
|
|
95
|
+
const message = commitMessage(domains)
|
|
96
|
+
const body = pullRequestBody(changes)
|
|
97
|
+
const branch = syncBranchName(deps.now)
|
|
98
|
+
|
|
99
|
+
intro('aitk sync → git')
|
|
100
|
+
|
|
101
|
+
const [localTaken, remoteTaken] = await Promise.all([
|
|
102
|
+
deps.git.branchExists(branch),
|
|
103
|
+
deps.git.remoteBranchExists(branch),
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
if (localTaken || remoteTaken) {
|
|
107
|
+
logWarn(
|
|
108
|
+
`${branch} already exists. Wait a minute or delete it, then re-run.`,
|
|
109
|
+
)
|
|
110
|
+
outro()
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const currentBranch = await deps.git.currentBranch()
|
|
115
|
+
const onProtected =
|
|
116
|
+
currentBranch === '' || PROTECTED_BRANCHES.includes(currentBranch)
|
|
117
|
+
|
|
118
|
+
logStep('Preview')
|
|
119
|
+
logInfo(`Domains: ${domains.join(', ')}`)
|
|
120
|
+
logInfo(
|
|
121
|
+
onProtected
|
|
122
|
+
? `Branch: ${branch} (source: ${currentBranch || 'detached'})`
|
|
123
|
+
: `Branch: ${currentBranch} (commit only) or ${branch} (PR)`,
|
|
124
|
+
)
|
|
125
|
+
logInfo(`Commit: ${message}`)
|
|
126
|
+
|
|
127
|
+
logStep('PR body')
|
|
128
|
+
pipeOutput(body)
|
|
129
|
+
|
|
130
|
+
if (deps.nonInteractive) {
|
|
131
|
+
logWarn('Refusing the git workflow in non-interactive mode.')
|
|
132
|
+
logInfo(`Would commit ${domains.join(', ')} to ${branch}.`)
|
|
133
|
+
logInfo('Run interactively to commit, push, and open the pull request.')
|
|
134
|
+
outro()
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const opener = deps.pullRequests
|
|
139
|
+
if (opener === undefined) {
|
|
140
|
+
logWarn(
|
|
141
|
+
'gh CLI not found — PR option unavailable. Install from https://cli.github.com',
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const choose = deps.choose ?? promptChoice
|
|
146
|
+
const choice = await choose(opener !== undefined)
|
|
147
|
+
|
|
148
|
+
if (choice === 'cancel') {
|
|
149
|
+
logWarn('Skipped')
|
|
150
|
+
outro()
|
|
151
|
+
return 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const paths = [...new Set(changes.flatMap((change) => change.paths))]
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
logStep('Committing')
|
|
158
|
+
|
|
159
|
+
if (choice === 'pr' || onProtected) await deps.git.createBranch(branch)
|
|
160
|
+
await deps.git.stage(paths)
|
|
161
|
+
await deps.git.commit(message)
|
|
162
|
+
|
|
163
|
+
if (choice === 'commit' || opener === undefined) {
|
|
164
|
+
return succeed(
|
|
165
|
+
onProtected
|
|
166
|
+
? `Committed to ${branch}. Push and open a PR when ready.`
|
|
167
|
+
: `Committed to ${currentBranch}.`,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await deps.git.push(branch)
|
|
172
|
+
|
|
173
|
+
logStep('Opening PR')
|
|
174
|
+
const url = await opener.create(message, body)
|
|
175
|
+
|
|
176
|
+
return succeed(`Synced: ${url}`)
|
|
177
|
+
} catch (error) {
|
|
178
|
+
logError(error instanceof Error ? error.message : String(error))
|
|
179
|
+
outro()
|
|
180
|
+
return 1
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function promptChoice(canOpenPullRequest: boolean): Promise<WorkflowChoice> {
|
|
185
|
+
return select({
|
|
186
|
+
message: 'Review changes, then?',
|
|
187
|
+
options: [
|
|
188
|
+
...(canOpenPullRequest
|
|
189
|
+
? [{ value: 'pr' as const, label: 'Commit and open PR' }]
|
|
190
|
+
: []),
|
|
191
|
+
{ value: 'commit' as const, label: 'Commit only' },
|
|
192
|
+
{ value: 'cancel' as const, label: 'Cancel' },
|
|
193
|
+
],
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function succeed(message: string): number {
|
|
198
|
+
outro()
|
|
199
|
+
process.stderr.write(`\n${GREEN}✓ ${message}${NC}\n`)
|
|
200
|
+
return 0
|
|
201
|
+
}
|
package/src/target.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { statSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { logError, outro } from '@/ui'
|
|
4
|
+
|
|
5
|
+
export function isDirectory(path: string): boolean {
|
|
6
|
+
try {
|
|
7
|
+
return statSync(path).isDirectory()
|
|
8
|
+
} catch {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stands in for `validate_target` plus `guard_root`. The bash `guard_root` read
|
|
15
|
+
* as a toolkit-root check, but its body was `cd "$target" && pwd`, so it also
|
|
16
|
+
* rejected a target that does not exist. Porting the name alone would let a
|
|
17
|
+
* typo'd path scaffold a whole new tree, since every apply step creates parents.
|
|
18
|
+
*
|
|
19
|
+
* Returns the resolved path, or the exit code the caller should hand back. The
|
|
20
|
+
* frame is already open by the time this runs, so a rejection closes it.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveTarget(
|
|
23
|
+
target: string,
|
|
24
|
+
protectedRoot: string,
|
|
25
|
+
): string | number {
|
|
26
|
+
const resolved = resolve(target)
|
|
27
|
+
|
|
28
|
+
if (!isDirectory(resolved)) {
|
|
29
|
+
logError(`Target directory not found: ${target}`)
|
|
30
|
+
outro()
|
|
31
|
+
return 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (resolved === protectedRoot) {
|
|
35
|
+
logError(
|
|
36
|
+
'Cannot run against toolkit root. Files here are the source of truth.',
|
|
37
|
+
)
|
|
38
|
+
outro()
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return resolved
|
|
43
|
+
}
|