@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,170 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { DesignDoc, Row } from '@/design/parse'
|
|
4
|
+
import { parseDesignDoc } from '@/design/parse'
|
|
5
|
+
|
|
6
|
+
export interface RenderResult {
|
|
7
|
+
htmlPath: string
|
|
8
|
+
cssPath: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function renderDesignDoc(
|
|
12
|
+
sourcePath: string,
|
|
13
|
+
outDir: string,
|
|
14
|
+
): RenderResult {
|
|
15
|
+
const doc = parseDesignDoc(sourcePath)
|
|
16
|
+
mkdirSync(outDir, { recursive: true })
|
|
17
|
+
const cssPath = join(outDir, 'design.css')
|
|
18
|
+
const htmlPath = join(outDir, 'index.html')
|
|
19
|
+
writeFileSync(cssPath, buildCss(doc))
|
|
20
|
+
writeFileSync(htmlPath, buildHtml(doc))
|
|
21
|
+
return { htmlPath, cssPath }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function slug(s: string): string {
|
|
25
|
+
return s
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
28
|
+
.replace(/(^-|-$)/g, '')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildCss(doc: DesignDoc): string {
|
|
32
|
+
const lines: string[] = [':root {']
|
|
33
|
+
for (const row of doc.color) {
|
|
34
|
+
if (row['Value']) {
|
|
35
|
+
lines.push(` --color-${slug(row['Role'])}: ${row['Value']};`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const row of doc.spacing) {
|
|
39
|
+
if (row['Value']) {
|
|
40
|
+
lines.push(` --space-${slug(row['Step'])}: ${row['Value']};`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const row of doc.typography) {
|
|
44
|
+
if (row['Size']) {
|
|
45
|
+
lines.push(` --type-${slug(row['Role'])}-size: ${row['Size']};`)
|
|
46
|
+
}
|
|
47
|
+
if (row['Line height']) {
|
|
48
|
+
lines.push(` --type-${slug(row['Role'])}-lh: ${row['Line height']};`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const row of doc.borders) {
|
|
52
|
+
if (row['Radius']) {
|
|
53
|
+
lines.push(` --radius-${slug(row['Role'])}: ${row['Radius']};`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
lines.push('}')
|
|
57
|
+
return lines.join('\n') + '\n'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function escape(s: string): string {
|
|
61
|
+
return s
|
|
62
|
+
.replace(/&/g, '&')
|
|
63
|
+
.replace(/</g, '<')
|
|
64
|
+
.replace(/>/g, '>')
|
|
65
|
+
.replace(/"/g, '"')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildHtml(doc: DesignDoc): string {
|
|
69
|
+
const sections = [
|
|
70
|
+
sectionPersonality(doc.personality),
|
|
71
|
+
sectionColor(doc.color),
|
|
72
|
+
sectionTypography(doc.typography),
|
|
73
|
+
sectionSpacing(doc.spacing),
|
|
74
|
+
sectionBorders(doc.borders),
|
|
75
|
+
sectionLine('Motion', doc.motion),
|
|
76
|
+
sectionLine('Iconography', doc.iconography),
|
|
77
|
+
]
|
|
78
|
+
return `<!doctype html>
|
|
79
|
+
<html lang="en">
|
|
80
|
+
<head>
|
|
81
|
+
<meta charset="utf-8">
|
|
82
|
+
<title>Design tokens</title>
|
|
83
|
+
<link rel="stylesheet" href="design.css">
|
|
84
|
+
<style>
|
|
85
|
+
body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 960px; color: #222; }
|
|
86
|
+
h1 { margin-top: 0; }
|
|
87
|
+
h2 { margin-top: 2rem; border-bottom: 1px solid #ddd; padding-bottom: 0.25rem; }
|
|
88
|
+
table { border-collapse: collapse; width: 100%; margin-top: 0.5rem; }
|
|
89
|
+
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #eee; font-size: 14px; }
|
|
90
|
+
th { background: #f7f7f7; font-weight: 600; }
|
|
91
|
+
.swatch { display: inline-block; width: 1.5rem; height: 1.5rem; border-radius: 4px; border: 1px solid #ddd; vertical-align: middle; margin-right: 0.5rem; }
|
|
92
|
+
.bar { display: inline-block; height: 1rem; background: #888; border-radius: 2px; vertical-align: middle; }
|
|
93
|
+
.note { color: #666; font-size: 13px; margin-top: 0.5rem; }
|
|
94
|
+
.empty { color: #999; font-style: italic; }
|
|
95
|
+
</style>
|
|
96
|
+
</head>
|
|
97
|
+
<body>
|
|
98
|
+
<h1>Design tokens</h1>
|
|
99
|
+
<p class="note">Generated from <code>.claude/DESIGN.md</code> by <code>aitk design render</code>. Token preview only, not a screen mock.</p>
|
|
100
|
+
${sections.join('\n')}
|
|
101
|
+
</body>
|
|
102
|
+
</html>
|
|
103
|
+
`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sectionPersonality(text: string): string {
|
|
107
|
+
if (!text) return ''
|
|
108
|
+
return `<h2>Personality</h2>\n<p>${escape(text)}</p>`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sectionColor(rows: Row[]): string {
|
|
112
|
+
if (!rows.length) return ''
|
|
113
|
+
const body = rows
|
|
114
|
+
.map((r) => {
|
|
115
|
+
const swatch = r['Value']
|
|
116
|
+
? `<span class="swatch" style="background:${escape(r['Value'])}"></span>`
|
|
117
|
+
: '<span class="swatch"></span>'
|
|
118
|
+
const value = r['Value'] || '<span class="empty">unset</span>'
|
|
119
|
+
return `<tr><td>${swatch}${escape(r['Role'] ?? '')}</td><td>${escape(r['Intent'] ?? '')}</td><td><code>${value}</code></td></tr>`
|
|
120
|
+
})
|
|
121
|
+
.join('\n')
|
|
122
|
+
return `<h2>Color</h2>\n<table><thead><tr><th>Role</th><th>Intent</th><th>Value</th></tr></thead><tbody>${body}</tbody></table>`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sectionTypography(rows: Row[]): string {
|
|
126
|
+
if (!rows.length) return ''
|
|
127
|
+
const body = rows
|
|
128
|
+
.map((r) => {
|
|
129
|
+
const family = r['Family'] || 'system-ui'
|
|
130
|
+
const weight = r['Weight'] || '400'
|
|
131
|
+
const size = r['Size'] || '16px'
|
|
132
|
+
const lh = r['Line height'] || '1.4'
|
|
133
|
+
const sample = `<span style="font-family:${escape(family)};font-weight:${escape(weight)};font-size:${escape(size)};line-height:${escape(lh)}">The quick brown fox</span>`
|
|
134
|
+
return `<tr><td>${escape(r['Role'] ?? '')}</td><td>${escape(family)}</td><td>${escape(weight)}</td><td>${escape(size)}</td><td>${escape(lh)}</td><td>${sample}</td></tr>`
|
|
135
|
+
})
|
|
136
|
+
.join('\n')
|
|
137
|
+
return `<h2>Typography</h2>\n<table><thead><tr><th>Role</th><th>Family</th><th>Weight</th><th>Size</th><th>Line height</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sectionSpacing(rows: Row[]): string {
|
|
141
|
+
if (!rows.length) return ''
|
|
142
|
+
const body = rows
|
|
143
|
+
.map((r) => {
|
|
144
|
+
const value = r['Value'] || ''
|
|
145
|
+
const bar = value
|
|
146
|
+
? `<span class="bar" style="width:${escape(value)}"></span>`
|
|
147
|
+
: '<span class="empty">unset</span>'
|
|
148
|
+
return `<tr><td>${escape(r['Step'] ?? '')}</td><td>${escape(r['Multiplier'] ?? '')}</td><td><code>${escape(value || 'unset')}</code></td><td>${bar}</td></tr>`
|
|
149
|
+
})
|
|
150
|
+
.join('\n')
|
|
151
|
+
return `<h2>Spacing</h2>\n<table><thead><tr><th>Step</th><th>Multiplier</th><th>Value</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sectionBorders(rows: Row[]): string {
|
|
155
|
+
if (!rows.length) return ''
|
|
156
|
+
const body = rows
|
|
157
|
+
.map((r) => {
|
|
158
|
+
const radius = r['Radius'] || '0'
|
|
159
|
+
const width = r['Width'] || '1px'
|
|
160
|
+
const sample = `<span style="display:inline-block;width:2rem;height:1.5rem;background:#eee;border:${escape(width)} solid #888;border-radius:${escape(radius)};vertical-align:middle"></span>`
|
|
161
|
+
return `<tr><td>${escape(r['Role'] ?? '')}</td><td><code>${escape(radius)}</code></td><td><code>${escape(width)}</code></td><td>${escape(r['When used'] ?? '')}</td><td>${sample}</td></tr>`
|
|
162
|
+
})
|
|
163
|
+
.join('\n')
|
|
164
|
+
return `<h2>Borders</h2>\n<table><thead><tr><th>Role</th><th>Radius</th><th>Width</th><th>When used</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sectionLine(title: string, text: string): string {
|
|
168
|
+
if (!text) return ''
|
|
169
|
+
return `<h2>${title}</h2>\n<p>${escape(text)}</p>`
|
|
170
|
+
}
|
package/src/docs/read.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
import { stripFrontmatter } from '@/frontmatter'
|
|
4
|
+
|
|
5
|
+
const INDEX_TOPIC = 'index'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The two roots a topic resolves against, in precedence order. `docs/` holds
|
|
9
|
+
* consumer-facing reference and `.claude/context/` holds per-domain internal
|
|
10
|
+
* narrative, so a name present in both resolves to the consumer-facing copy.
|
|
11
|
+
*/
|
|
12
|
+
const ROOTS: readonly string[] = ['docs', join('.claude', 'context')]
|
|
13
|
+
|
|
14
|
+
export interface ResolvedTopic {
|
|
15
|
+
readonly path: string
|
|
16
|
+
readonly rel: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveTopic(
|
|
20
|
+
root: string,
|
|
21
|
+
topic: string,
|
|
22
|
+
): ResolvedTopic | undefined {
|
|
23
|
+
for (const dir of ROOTS) {
|
|
24
|
+
const rel = join(dir, `${topic}.md`)
|
|
25
|
+
const path = join(root, rel)
|
|
26
|
+
if (existsSync(path)) return { path, rel }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Names every topic a `get` could resolve, listed per root in the order the
|
|
34
|
+
* roots are searched. Shown when a topic misses, so it doubles as the answer to
|
|
35
|
+
* what the caller should have typed.
|
|
36
|
+
*/
|
|
37
|
+
export function listTopics(root: string): string[] {
|
|
38
|
+
const topics: string[] = []
|
|
39
|
+
|
|
40
|
+
for (const dir of ROOTS) {
|
|
41
|
+
const cwd = join(root, dir)
|
|
42
|
+
if (!existsSync(cwd)) continue
|
|
43
|
+
|
|
44
|
+
const names = [
|
|
45
|
+
...new Bun.Glob('*.md').scanSync({ cwd, onlyFiles: true, dot: true }),
|
|
46
|
+
]
|
|
47
|
+
.map((name) => basename(name, '.md'))
|
|
48
|
+
.filter((name) => name !== INDEX_TOPIC)
|
|
49
|
+
.sort()
|
|
50
|
+
|
|
51
|
+
topics.push(...names)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return topics
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readTopic(topic: ResolvedTopic): string {
|
|
58
|
+
return stripFrontmatter(readFileSync(topic.path, 'utf8'))
|
|
59
|
+
}
|
package/src/exec.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
|
|
4
|
+
export const PROJECT_ROOT = resolve(import.meta.dir, '..')
|
|
5
|
+
|
|
6
|
+
export async function execScript(
|
|
7
|
+
script: string,
|
|
8
|
+
args: string[],
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const scriptPath = join(PROJECT_ROOT, 'scripts', script)
|
|
11
|
+
const result = await execa(scriptPath, args, {
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
env: { ...process.env, PROJECT_ROOT },
|
|
14
|
+
reject: false,
|
|
15
|
+
})
|
|
16
|
+
process.exit(result.exitCode ?? 1)
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const FRONTMATTER = /^---\n[\s\S]*?\n---\n?/
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drops a leading `---` block. Only a block starting on the first line counts,
|
|
5
|
+
* so a horizontal rule further down the document is left in place.
|
|
6
|
+
*
|
|
7
|
+
* The bash this replaced treated the first `---` on any line as an opening
|
|
8
|
+
* fence, which silently swallowed everything between two horizontal rules in a
|
|
9
|
+
* body. Anchoring to the first line is the correct reading.
|
|
10
|
+
*/
|
|
11
|
+
export function stripFrontmatter(source: string): string {
|
|
12
|
+
return source.replace(FRONTMATTER, '')
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CreateIssueOptions {
|
|
2
|
+
title: string
|
|
3
|
+
body: string
|
|
4
|
+
labels?: string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function buildIssueArgs(opts: CreateIssueOptions): string[] {
|
|
8
|
+
const args = ['issue', 'create', '--title', opts.title, '--body', opts.body]
|
|
9
|
+
for (const label of opts.labels ?? []) {
|
|
10
|
+
args.push('--label', label)
|
|
11
|
+
}
|
|
12
|
+
return args
|
|
13
|
+
}
|
package/src/github.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import { PROJECT_ROOT } from '@/exec'
|
|
3
|
+
import { buildIssueArgs, type CreateIssueOptions } from '@/github-format'
|
|
4
|
+
|
|
5
|
+
const GH_TIMEOUT_MS = 30_000
|
|
6
|
+
|
|
7
|
+
export async function createGithubIssue(
|
|
8
|
+
opts: CreateIssueOptions,
|
|
9
|
+
): Promise<string | null> {
|
|
10
|
+
if (Bun.which('gh') === null) return null
|
|
11
|
+
try {
|
|
12
|
+
const result = await execa('gh', buildIssueArgs(opts), {
|
|
13
|
+
cwd: PROJECT_ROOT,
|
|
14
|
+
timeout: GH_TIMEOUT_MS,
|
|
15
|
+
})
|
|
16
|
+
return result.stdout.trim() || null
|
|
17
|
+
} catch {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { basename, join, relative, resolve } from 'node:path'
|
|
3
|
+
import type { InstalledFile, RetiredSurface, SyncAdapter } from '@/sync/engine'
|
|
4
|
+
|
|
5
|
+
const RETIRED_GOV_FILE = join('.claude', 'GOV.md')
|
|
6
|
+
|
|
7
|
+
export function rulesSourceDir(root: string): string {
|
|
8
|
+
return join(root, 'governance', 'rules')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maps a rule name to its source file. The bash used
|
|
13
|
+
* `find governance/rules -name "<rule>.md" | head -n 1` once per installed
|
|
14
|
+
* rule, which walked the whole tree per file and returned whichever match
|
|
15
|
+
* the filesystem happened to yield first. Indexing once makes the walk single
|
|
16
|
+
* pass and the winner deterministic when a name appears in two subdirectories.
|
|
17
|
+
*/
|
|
18
|
+
export function indexSourceRules(root: string): Map<string, string> {
|
|
19
|
+
const dir = rulesSourceDir(root)
|
|
20
|
+
const index = new Map<string, string>()
|
|
21
|
+
if (!existsSync(dir)) return index
|
|
22
|
+
|
|
23
|
+
const paths = [
|
|
24
|
+
...new Bun.Glob('**/*.md').scanSync({
|
|
25
|
+
cwd: dir,
|
|
26
|
+
onlyFiles: true,
|
|
27
|
+
dot: true,
|
|
28
|
+
}),
|
|
29
|
+
].sort()
|
|
30
|
+
|
|
31
|
+
for (const path of paths) {
|
|
32
|
+
const name = basename(path, '.md')
|
|
33
|
+
if (!index.has(name)) index.set(name, resolve(dir, path))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return index
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Matches installed rules to sources by rule name rather than by relative
|
|
41
|
+
* path, so a rule that moved between subdirectories in the toolkit still
|
|
42
|
+
* syncs into the subdirectory the target already uses.
|
|
43
|
+
*/
|
|
44
|
+
export function createGovAdapter(root: string): SyncAdapter {
|
|
45
|
+
const index = indexSourceRules(root)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
banner: 'aitk gov sync',
|
|
49
|
+
label: 'rules',
|
|
50
|
+
missingMessage:
|
|
51
|
+
"No governance surfaces found in target. Run 'aitk gov install' first.",
|
|
52
|
+
unit: 'changes',
|
|
53
|
+
installedRoot: (target: string) => join(target, '.claude', 'rules'),
|
|
54
|
+
locateSource: (file: InstalledFile) =>
|
|
55
|
+
index.get(basename(file.path, '.md')),
|
|
56
|
+
collectRetired: (target: string) => collectRetiredGov(target),
|
|
57
|
+
stamp: { domain: 'governance', toolkitRoot: root },
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectRetiredGov(target: string): RetiredSurface[] {
|
|
62
|
+
const path = join(target, RETIRED_GOV_FILE)
|
|
63
|
+
if (!existsSync(path)) return []
|
|
64
|
+
|
|
65
|
+
const rel = relative(target, path)
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
path,
|
|
69
|
+
rel,
|
|
70
|
+
notice: `${rel} (retired surface, scheduled for removal)`,
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, relative } from 'node:path'
|
|
3
|
+
import { copyPreservingMode } from '@/copy'
|
|
4
|
+
|
|
5
|
+
export interface RuleSource {
|
|
6
|
+
readonly rule: string
|
|
7
|
+
readonly src: string
|
|
8
|
+
readonly subdir: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RuleLookup {
|
|
12
|
+
readonly found: readonly RuleSource[]
|
|
13
|
+
readonly missing: readonly string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function rulesSourceDir(root: string): string {
|
|
17
|
+
return join(root, 'governance', 'rules')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function installedRulesDir(target: string): string {
|
|
21
|
+
return join(target, '.claude', 'rules')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mirrors `rule_subdir` in `scripts/lib/gov.sh`, which stays in bash for the
|
|
26
|
+
* sandbox loops. A rule sitting directly under `governance/rules/` has no
|
|
27
|
+
* subdirectory and installs flat.
|
|
28
|
+
*/
|
|
29
|
+
export function ruleSubdir(src: string, rulesRoot: string): string {
|
|
30
|
+
const subdir = dirname(relative(rulesRoot, src))
|
|
31
|
+
return subdir === '.' ? '' : subdir
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Finds each rule's source file by name across the `governance/rules/`
|
|
36
|
+
* subfolders. A rule with no source is reported rather than dropped, matching
|
|
37
|
+
* the `(source not found, skipping)` warning the bash printed.
|
|
38
|
+
*/
|
|
39
|
+
export function lookupRules(
|
|
40
|
+
root: string,
|
|
41
|
+
rules: readonly string[],
|
|
42
|
+
): RuleLookup {
|
|
43
|
+
const rulesRoot = rulesSourceDir(root)
|
|
44
|
+
const byName = new Map<string, string>()
|
|
45
|
+
|
|
46
|
+
const relPaths = existsSync(rulesRoot)
|
|
47
|
+
? [
|
|
48
|
+
...new Bun.Glob('**/*.md').scanSync({
|
|
49
|
+
cwd: rulesRoot,
|
|
50
|
+
onlyFiles: true,
|
|
51
|
+
}),
|
|
52
|
+
].sort()
|
|
53
|
+
: []
|
|
54
|
+
|
|
55
|
+
for (const rel of relPaths) {
|
|
56
|
+
const name = rel.slice(rel.lastIndexOf('/') + 1, -'.md'.length)
|
|
57
|
+
if (!byName.has(name)) byName.set(name, join(rulesRoot, rel))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const found: RuleSource[] = []
|
|
61
|
+
const missing: string[] = []
|
|
62
|
+
|
|
63
|
+
for (const rule of rules) {
|
|
64
|
+
const src = byName.get(rule)
|
|
65
|
+
if (src === undefined) {
|
|
66
|
+
missing.push(rule)
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
found.push({ rule, src, subdir: ruleSubdir(src, rulesRoot) })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { found, missing }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Copies each rule into the subdirectory it was authored in, so the installed
|
|
77
|
+
* tree keeps the band structure `governance/rules/` carries. Returns the
|
|
78
|
+
* target-relative paths the timeline prints.
|
|
79
|
+
*/
|
|
80
|
+
export async function installRules(
|
|
81
|
+
sources: readonly RuleSource[],
|
|
82
|
+
target: string,
|
|
83
|
+
): Promise<string[]> {
|
|
84
|
+
const rulesDir = installedRulesDir(target)
|
|
85
|
+
const installed: string[] = []
|
|
86
|
+
|
|
87
|
+
for (const source of sources) {
|
|
88
|
+
const dest = join(rulesDir, source.subdir, `${source.rule}.md`)
|
|
89
|
+
await copyPreservingMode(source.src, dest)
|
|
90
|
+
installed.push(relative(target, dest))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return installed
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The bash checked `<target>/standards`, but `aitk standards install` writes
|
|
98
|
+
* `.claude/standards/`, which is also the path the rules reference and the
|
|
99
|
+
* path the warning names. The guard therefore fired on every modern install.
|
|
100
|
+
*/
|
|
101
|
+
export function hasStandards(target: string): boolean {
|
|
102
|
+
return existsSync(join(target, '.claude', 'standards'))
|
|
103
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { basename, resolve } from 'node:path'
|
|
3
|
+
import { stripFrontmatter } from '@/frontmatter'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lists installed rules in the order the payload concatenates them. Sorting by
|
|
7
|
+
* path keeps the numeric filename prefixes ordered inside each subdirectory,
|
|
8
|
+
* which is how the rule bands are meant to read.
|
|
9
|
+
*/
|
|
10
|
+
export function listRuleFiles(rulesDir: string): string[] {
|
|
11
|
+
return [
|
|
12
|
+
...new Bun.Glob('**/*.md').scanSync({
|
|
13
|
+
cwd: rulesDir,
|
|
14
|
+
onlyFiles: true,
|
|
15
|
+
dot: true,
|
|
16
|
+
}),
|
|
17
|
+
]
|
|
18
|
+
.sort()
|
|
19
|
+
.map((path) => resolve(rulesDir, path))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wraps each rule in a named tag so a model reading the payload can tell the
|
|
24
|
+
* rules apart without the file boundaries. Bodies are trimmed of blank lines
|
|
25
|
+
* at both ends and separated by exactly one blank line.
|
|
26
|
+
*/
|
|
27
|
+
export function buildRulesPayload(files: readonly string[]): string {
|
|
28
|
+
const blocks = files.map((file) => {
|
|
29
|
+
const name = basename(file, '.md')
|
|
30
|
+
const body = trimBlankLines(stripFrontmatter(readFileSync(file, 'utf8')))
|
|
31
|
+
return `<rule name="${name}">\n${body}\n</rule>`
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return `${blocks.join('\n\n')}\n`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function trimBlankLines(source: string): string {
|
|
38
|
+
return source.replace(/^(?:[ \t]*\n)+/, '').replace(/(?:\n[ \t]*)+$/, '')
|
|
39
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface GovStack {
|
|
5
|
+
readonly name: string
|
|
6
|
+
readonly parent?: string
|
|
7
|
+
readonly rules: readonly string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type RuleResolution =
|
|
11
|
+
| { readonly ok: true; readonly rules: string[] }
|
|
12
|
+
| { readonly ok: false; readonly missingStack: string }
|
|
13
|
+
|
|
14
|
+
function stacksDir(root: string): string {
|
|
15
|
+
return join(root, 'governance', 'stacks')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function govStackPath(root: string, stack: string): string {
|
|
19
|
+
return join(stacksDir(root), `${stack}.toml`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function govStackExists(root: string, stack: string): boolean {
|
|
23
|
+
return existsSync(govStackPath(root, stack))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lists stack names the way `find -name "*.toml" -exec basename {} .toml` did.
|
|
28
|
+
*/
|
|
29
|
+
export function listGovStacks(root: string): string[] {
|
|
30
|
+
const dir = stacksDir(root)
|
|
31
|
+
if (!existsSync(dir)) return []
|
|
32
|
+
|
|
33
|
+
return [...new Bun.Glob('*.toml').scanSync({ cwd: dir, onlyFiles: true })]
|
|
34
|
+
.map((entry) => entry.slice(0, -'.toml'.length))
|
|
35
|
+
.sort()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reads one stack file. `Bun.TOML.parse` replaces a `BASH_REMATCH` loop that
|
|
40
|
+
* consumed each rules line with `sed` as it walked, which is the hand-rolled
|
|
41
|
+
* parser shape the tooling manifest already moved off.
|
|
42
|
+
*/
|
|
43
|
+
export function loadGovStack(
|
|
44
|
+
root: string,
|
|
45
|
+
stack: string,
|
|
46
|
+
): GovStack | undefined {
|
|
47
|
+
const path = govStackPath(root, stack)
|
|
48
|
+
if (!existsSync(path)) return undefined
|
|
49
|
+
|
|
50
|
+
const parsed = Bun.TOML.parse(readFileSync(path, 'utf8')) as Record<
|
|
51
|
+
string,
|
|
52
|
+
unknown
|
|
53
|
+
>
|
|
54
|
+
const parent = typeof parsed.extends === 'string' ? parsed.extends : ''
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name: stack,
|
|
58
|
+
parent: parent === '' ? undefined : parent,
|
|
59
|
+
rules: Array.isArray(parsed.rules)
|
|
60
|
+
? parsed.rules.filter(
|
|
61
|
+
(rule): rule is string => typeof rule === 'string' && rule !== '',
|
|
62
|
+
)
|
|
63
|
+
: [],
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Walks `extends` ancestors first, then the stack's own rules, deduped by
|
|
69
|
+
* first appearance. Tooling's `resolveChain` returns full manifests nearest
|
|
70
|
+
* first and carries `skipStack` truncation, so the two walks stay separate
|
|
71
|
+
* rather than fitting one shape to both.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveRules(root: string, stack: string): RuleResolution {
|
|
74
|
+
const rules: string[] = []
|
|
75
|
+
const seen = new Set<string>()
|
|
76
|
+
const visited = new Set<string>()
|
|
77
|
+
|
|
78
|
+
const walk = (current: string): string | undefined => {
|
|
79
|
+
if (visited.has(current)) return undefined
|
|
80
|
+
visited.add(current)
|
|
81
|
+
|
|
82
|
+
const loaded = loadGovStack(root, current)
|
|
83
|
+
if (!loaded) return current
|
|
84
|
+
|
|
85
|
+
if (loaded.parent !== undefined) {
|
|
86
|
+
const missing = walk(loaded.parent)
|
|
87
|
+
if (missing !== undefined) return missing
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const rule of loaded.rules) {
|
|
91
|
+
if (seen.has(rule)) continue
|
|
92
|
+
seen.add(rule)
|
|
93
|
+
rules.push(rule)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const missingStack = walk(stack)
|
|
100
|
+
if (missingStack !== undefined) return { ok: false, missingStack }
|
|
101
|
+
|
|
102
|
+
return { ok: true, rules }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Layers `--add` names on top of a resolved stack. The bash trimmed a single
|
|
107
|
+
* leading and trailing space per entry; trimming fully is the same result for
|
|
108
|
+
* every input that parsed before.
|
|
109
|
+
*/
|
|
110
|
+
export function mergeExtraRules(
|
|
111
|
+
rules: readonly string[],
|
|
112
|
+
add: string,
|
|
113
|
+
): string[] {
|
|
114
|
+
const merged = [...rules]
|
|
115
|
+
|
|
116
|
+
for (const raw of add.split(',')) {
|
|
117
|
+
const extra = raw.trim()
|
|
118
|
+
if (extra === '') continue
|
|
119
|
+
if (merged.includes(extra)) continue
|
|
120
|
+
merged.push(extra)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return merged
|
|
124
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---/
|
|
2
|
+
|
|
3
|
+
export interface Frontmatter {
|
|
4
|
+
readonly raw: string
|
|
5
|
+
readonly fields: Readonly<Record<string, unknown>>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Splits a leading `---` block off a markdown source.
|
|
10
|
+
*
|
|
11
|
+
* `raw` keeps the delimiters so an index can be rewritten with its own
|
|
12
|
+
* frontmatter untouched, including comments and key order that a parse and
|
|
13
|
+
* re-emit would lose.
|
|
14
|
+
*/
|
|
15
|
+
export function parseFrontmatter(source: string): Frontmatter | undefined {
|
|
16
|
+
const match = source.match(FRONTMATTER)
|
|
17
|
+
if (!match) return undefined
|
|
18
|
+
|
|
19
|
+
const parsed = Bun.YAML.parse(match[1])
|
|
20
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
raw: match[0],
|
|
26
|
+
fields: parsed as Record<string, unknown>,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readField(
|
|
31
|
+
frontmatter: Frontmatter | undefined,
|
|
32
|
+
key: string,
|
|
33
|
+
): string | undefined {
|
|
34
|
+
const value = frontmatter?.fields[key]
|
|
35
|
+
if (typeof value === 'string') return value
|
|
36
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
37
|
+
return String(value)
|
|
38
|
+
}
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readFrontmatter(
|
|
43
|
+
path: string,
|
|
44
|
+
): Promise<Frontmatter | undefined> {
|
|
45
|
+
return parseFrontmatter(await Bun.file(path).text())
|
|
46
|
+
}
|