@erclx/aitk 3.20.0 → 3.21.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "3.20.0",
4
+ "version": "3.21.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -107,13 +107,13 @@ AITK_NON_INTERACTIVE=1 aitk tooling sync vite-react ./frontend --skip base --wri
107
107
  AITK_NON_INTERACTIVE=1 aitk tooling sync python ./backend --skip base --write
108
108
  ```
109
109
 
110
- Without `--skip base`, each subtree re-drops husky, and git honors only one `core.hooksPath`, so the extra hook dirs silently break. Each subtree keeps its own framework configs and its own `.claude/tooling/<stack>.md` audit docs.
110
+ Without `--skip base`, each subtree re-drops husky, and git honors only one `core.hooksPath`, so the extra hook dirs silently break. Each subtree keeps its own framework configs, and its own stack reference reads through `aitk tooling reference <stack>`.
111
111
 
112
112
  Step 3: post-sync fixups. Golden configs arrive from sync, so no config generation is required. But a few items may need a one-time touch:
113
113
 
114
114
  - **ESLint version pin.** If `bun create vite` installed `eslint@^10` and the manifest pins `eslint@^9`, sync does not override a present dep. Run `bun add -d eslint@^9` if `bun run lint:fix` fails with `Class extends value undefined`.
115
115
  - **File naming.** `bun create vite`'s `App.tsx` violates the `KEBAB_CASE` rule. Rename to `app.tsx` and update the import in `main.tsx`.
116
- - **Docs.** Open `<target>/.claude/tooling/<tooling-stack>.md` and `<target>/.claude/tooling/web.md` for any stack-specific follow-ups (Chrome extension overrides, setup script details).
116
+ - **Docs.** Run `aitk tooling reference <tooling-stack>` and `aitk tooling reference web` for any stack-specific follow-ups (Chrome extension overrides, setup script details).
117
117
 
118
118
  Do not generate ESLint, Vitest, or Playwright configs. They ship as golden files. Generating from prose duplicates what sync already installed.
119
119
 
@@ -9,17 +9,17 @@ What each `aitk` sync or install command does to existing files in a target proj
9
9
 
10
10
  ## Overwrite contract
11
11
 
12
- | Surface | Command | Effect on existing files |
13
- | -------------------------------------------------------- | ------------------- | ----------------------------------------------------------------- |
14
- | Golden configs, listed in full below | `aitk tooling sync` | Overwritten once `--write` is passed. Local edits are lost. |
15
- | Dictionary seeds (`.cspell/*.txt`) | `aitk tooling sync` | Merged and sorted. Existing terms preserved. |
16
- | Other seeds (`cspell.json`, `.lintstagedrc`, state docs) | `aitk tooling sync` | Copy-once. Dropped on first install, untouched after. |
17
- | Standards | none | Nothing installs. `aitk standards <name>` reads and never writes. |
18
- | Seed docs and `CLAUDE.md` | `aitk claude init` | Skipped when present. Never overwritten. |
19
- | Seed docs | `aitk claude sync` | Never touched. Only `.gitignore` is written. |
20
- | References (`.claude/tooling/<stack>.md`) | `aitk tooling ref` | Overwritten. |
21
- | `.gitignore`, deps, scripts | any sync | Additive. Existing entries preserved. Deps re-pin on major skew. |
22
- | Generated `index.md` | any sync or regen | Rewritten from target state. Hand edits are lost. |
12
+ | Surface | Command | Effect on existing files |
13
+ | -------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------- |
14
+ | Golden configs, listed in full below | `aitk tooling sync` | Overwritten once `--write` is passed. Local edits are lost. |
15
+ | Dictionary seeds (`.cspell/*.txt`) | `aitk tooling sync` | Merged and sorted. Existing terms preserved. |
16
+ | Other seeds (`cspell.json`, `.lintstagedrc`, state docs) | `aitk tooling sync` | Copy-once. Dropped on first install, untouched after. |
17
+ | Standards | none | Nothing installs. `aitk standards <name>` reads and never writes. |
18
+ | Seed docs and `CLAUDE.md` | `aitk claude init` | Skipped when present. Never overwritten. |
19
+ | Seed docs | `aitk claude sync` | Never touched. Only `.gitignore` is written. |
20
+ | Stack references | none | Nothing installs. `aitk tooling reference <stack>` reads and never writes. |
21
+ | `.gitignore`, deps, scripts | any sync | Additive. Existing entries preserved. Deps re-pin on major skew. |
22
+ | Generated `index.md` | any sync or regen | Rewritten from target state. Hand edits are lost. |
23
23
 
24
24
  ## What a tooling sync can overwrite
25
25
 
@@ -45,8 +45,8 @@ Two steps, in order:
45
45
  The chain is:
46
46
 
47
47
  - `aitk init` installs base tooling, Claude seeds, and governance rules into `.claude/rules/` in the same pass
48
- - `aitk tooling sync <stack> --write` adds stack-specific deps, scripts, gitignore entries, and drops `.claude/tooling/<stack>.md` (plus parents) as the agent's audit context
49
- - The agent follows the reference to generate eslint, vitest, playwright configs and the stack's setup script, and extends `.claude/context/ci.md` and `.claude/context/development.md` per the reference's extend sections <!-- audit-ignore-citations -->
48
+ - `aitk tooling sync <stack> --write` adds stack-specific deps, scripts, and gitignore entries
49
+ - The agent reads `aitk tooling reference <stack>` (plus parents) as its audit context, follows it to generate eslint, vitest, playwright configs and the stack's setup script, and extends `.claude/context/ci.md` and `.claude/context/development.md` per the reference's extend sections <!-- audit-ignore-citations -->
50
50
  - `setup-verify` runs the installed `package.json` scripts (lint, typecheck, check, test, build) and reports pass or fail
51
51
 
52
52
  The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `aitk:toolkit-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `aitk claude init` for the seed docs and then `aitk:setup-indexes` for the index system. A language the toolkit ships no stack for is the one of the three the chain still runs for, on `base`, with the fallback marked in the preview so it can be declined there.
@@ -175,7 +175,7 @@ Reconcile the configs with `aitk tooling sync <stack> <path> --check` to read wh
175
175
 
176
176
  `aitk sync <path>` runs every installed domain's sync in sequence. Safe to run on a cadence.
177
177
 
178
- It never touches user-owned seed files. Governance rules in `.claude/rules/`, tooling configs, and reference docs refresh in place. Stale `.claude/GOV.md` from earlier installs is removed.
178
+ It never touches user-owned seed files. Governance rules in `.claude/rules/` and tooling configs refresh in place. Stale `.claude/GOV.md` from earlier installs is removed.
179
179
 
180
180
  Standards take no part in that run. Nothing installed them, so there is no copy to reconcile and no `aitk standards sync` to reach for.
181
181
 
@@ -184,7 +184,7 @@ Standards take no part in that run. Nothing installed them, so there is no copy
184
184
  - Claude seed docs such as `CLAUDE.md` and `.claude/REQUIREMENTS.md`: invoke `aitk:claude-seed-sync`. The skill splits each file into a preamble (between the H1 and the first H2) plus one part per `##` section, then diffs part by part and proposes per-part edits. User customizations are preserved.
185
185
  - Governance rules already installed: `aitk gov sync <path>` diffs and applies, and never adds new rules. A rule your recorded stack lists reports as `missing` instead.
186
186
  - Tooling configs and seeds: `aitk tooling <stack> <path>` overwrites golden configs and merges seeds
187
- - Reference docs for a stack: `aitk tooling ref <stack> <path>`
187
+ - Reference docs for a stack: `aitk tooling reference <stack>` reads and never writes, so there is nothing to sync
188
188
  - Index regeneration after markdown edits: `aitk indexes regen`
189
189
 
190
190
  Use a targeted entry point when only one surface moved upstream. Use the catch-all when the toolkit lands a bundled release.
@@ -233,7 +233,7 @@ aitk tooling sync vite-react ./frontend --skip base --write
233
233
  aitk tooling sync python ./backend --skip base --write
234
234
  ```
235
235
 
236
- `--skip base` drops the `base` layer from each subtree sync, so husky, prettier, cspell, commitlint, and CI stay single at the repo root. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite) and its own `.claude/tooling/<stack>.md` audit docs.
236
+ `--skip base` drops the `base` layer from each subtree sync, so husky, prettier, cspell, commitlint, and CI stay single at the repo root. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite), and its own stack reference reads through `aitk tooling reference <stack>`.
237
237
 
238
238
  ## Running sync from an agent session
239
239
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "3.20.0",
4
+ "version": "3.21.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -3,7 +3,6 @@ import type { Command } from 'commander'
3
3
  import { execScript } from '@/exec'
4
4
  import { PROJECT_ROOT } from '@/project-root'
5
5
  import {
6
- applyReferences,
7
6
  injectConfigs,
8
7
  injectGitignore,
9
8
  injectManifest,
@@ -18,12 +17,14 @@ import {
18
17
  resolveChain,
19
18
  stackExists,
20
19
  } from '@/tooling/manifest'
20
+ import { readReference, resolveReference } from '@/tooling/read'
21
21
  import { scan, type ScanResult } from '@/tooling/scan'
22
22
  import { recordToolingChain } from '@/tooling/stamp'
23
23
  import {
24
24
  intro,
25
25
  isNonInteractive,
26
26
  logAdd,
27
+ logError,
27
28
  logInfo,
28
29
  logStep,
29
30
  logWarn,
@@ -32,10 +33,9 @@ import {
32
33
  select,
33
34
  } from '@/ui'
34
35
 
35
- const PASS_THROUGH_VERBS = ['ref', 'create', 'verify'] as const
36
+ const PASS_THROUGH_VERBS = ['create', 'verify'] as const
36
37
 
37
38
  interface SyncOptions {
38
- readonly ref?: boolean
39
39
  readonly skip?: string
40
40
  readonly check?: boolean
41
41
  readonly write?: boolean
@@ -64,7 +64,7 @@ type Prepared =
64
64
  export function register(program: Command): void {
65
65
  const tooling = program
66
66
  .command('tooling')
67
- .description('Manage tooling stacks (sync, ref, create)')
67
+ .description('Manage tooling stacks (sync, reference, create)')
68
68
  .helpOption('-h, --help', 'Show this help message')
69
69
 
70
70
  tooling
@@ -73,7 +73,6 @@ export function register(program: Command): void {
73
73
  .argument('[stack]', 'Tooling stack name (e.g. base, vite-react)')
74
74
  .argument('[target]', 'Target directory', '.')
75
75
  .helpOption('-h, --help', 'Show this help message')
76
- .option('--no-ref', 'Skip dropping reference docs')
77
76
  .option('--skip <stack>', 'Drop a layer from the extends chain')
78
77
  .option('--check', 'Report what would change and write nothing')
79
78
  .option('--write', 'Apply every change without prompting')
@@ -134,6 +133,29 @@ export function register(program: Command): void {
134
133
  process.exitCode = runList(opts)
135
134
  })
136
135
 
136
+ tooling
137
+ .command('reference')
138
+ .description("Print a stack's reference doc")
139
+ .argument('<stack>', 'Tooling stack name (e.g. base, vite-react)')
140
+ .helpOption('-h, --help', 'Show this help message')
141
+ .addHelpText(
142
+ 'after',
143
+ [
144
+ '',
145
+ 'A stack resolves under tooling/ at the working root, then the corpus',
146
+ 'inside the aitk package. No reference installs into a project, so the',
147
+ 'package corpus is what answers there. The frame names the copy it read.',
148
+ '',
149
+ 'Examples:',
150
+ ' aitk tooling reference base',
151
+ ' aitk tooling reference vite-react',
152
+ '',
153
+ ].join('\n'),
154
+ )
155
+ .action((stack: string) => {
156
+ process.exitCode = printReference(stack)
157
+ })
158
+
137
159
  for (const verb of PASS_THROUGH_VERBS) {
138
160
  tooling
139
161
  .command(verb)
@@ -172,6 +194,32 @@ function runList(opts: ListOptions): number {
172
194
  return 0
173
195
  }
174
196
 
197
+ /**
198
+ * Writes the reference to stdout and every frame line to stderr, so a caller
199
+ * capturing the output with `$(...)` receives the document alone. Mirrors
200
+ * `print` in `src/commands/standards.ts`.
201
+ */
202
+ function printReference(stack: string): number {
203
+ intro('aitk tooling reference')
204
+
205
+ const root = process.cwd()
206
+ const resolved = resolveReference(root, stack)
207
+
208
+ if (!resolved) {
209
+ logWarn(`Unknown stack: ${stack}`)
210
+ logStep('Available stacks')
211
+ for (const each of listStacks(PROJECT_ROOT)) logInfo(each)
212
+ logError("Run 'aitk tooling list' for descriptions.")
213
+ outro()
214
+ return 1
215
+ }
216
+
217
+ logStep(resolved.source)
218
+ process.stdout.write(readReference(resolved))
219
+ outro()
220
+ return 0
221
+ }
222
+
175
223
  /**
176
224
  * Resolves the chain and rejects the same inputs the bash rejected, so a bad
177
225
  * stack name fails before anything touches the target.
@@ -248,10 +296,9 @@ async function runSync(
248
296
  return 1
249
297
  }
250
298
 
251
- const includeReferences = opts.ref !== false
252
- const result = scan(prepared.chain, prepared.target, { includeReferences })
299
+ const result = scan(prepared.chain, prepared.target)
253
300
 
254
- report(result, includeReferences)
301
+ report(result)
255
302
 
256
303
  const mode = resolveWriteMode(opts)
257
304
  const { GREEN, NC } = palette(process.stderr)
@@ -290,15 +337,6 @@ async function runSync(
290
337
  await injectSeeds(prepared.chain, prepared.target)
291
338
  await injectManifest(prepared.chain, prepared.target)
292
339
 
293
- const pending = result.references
294
- .filter((entry) => entry.state === 'pending')
295
- .map((entry) => entry.stack)
296
-
297
- if (pending.length > 0) {
298
- logStep('Applying references')
299
- await applyReferences(prepared.chain, prepared.target, pending)
300
- }
301
-
302
340
  await stampChain(prepared.chain, prepared.target)
303
341
 
304
342
  outro()
@@ -448,7 +486,7 @@ async function promptForStack(): Promise<string | undefined> {
448
486
  })
449
487
  }
450
488
 
451
- function report(result: ScanResult, includeReferences: boolean): void {
489
+ function report(result: ScanResult): void {
452
490
  logStep('Scanning configs')
453
491
  for (const entry of result.configs) {
454
492
  if (entry.state === 'matching') logInfo(entry.rel)
@@ -483,16 +521,6 @@ function report(result: ScanResult, includeReferences: boolean): void {
483
521
  for (const entry of result.gitignore) {
484
522
  if (entry.state === 'missing') logAdd(entry.entry)
485
523
  }
486
-
487
- if (!includeReferences) return
488
-
489
- logStep('Scanning references')
490
- for (const entry of result.references) {
491
- if (entry.state === 'matching') logInfo(`.claude/tooling/${entry.stack}.md`)
492
- }
493
- for (const entry of result.references) {
494
- if (entry.state === 'pending') logAdd(`.claude/tooling/${entry.stack}.md`)
495
- }
496
524
  }
497
525
 
498
526
  function reportPackage(result: ScanResult): void {
@@ -540,10 +568,6 @@ function summarize(result: ScanResult): string {
540
568
  result.gitignore.filter((entry) => entry.state === 'missing').length,
541
569
  'gitignore',
542
570
  )
543
- add(
544
- result.references.filter((entry) => entry.state === 'pending').length,
545
- 'refs',
546
- )
547
571
 
548
572
  return parts.join(', ')
549
573
  }
package/src/sync/check.ts CHANGED
@@ -103,7 +103,6 @@ export interface ToolingCounts {
103
103
  readonly scripts: number
104
104
  readonly deps: number
105
105
  readonly gitignore: number
106
- readonly references: number
107
106
  }
108
107
 
109
108
  /**
@@ -134,7 +133,6 @@ const UNMEASURED_TOOLING: ToolingReport = {
134
133
  scripts: 0,
135
134
  deps: 0,
136
135
  gitignore: 0,
137
- references: 0,
138
136
  },
139
137
  changes: 0,
140
138
  }
@@ -219,7 +217,7 @@ export function buildToolingReport(
219
217
 
220
218
  if (manifests.length === 0) return { ...UNMEASURED_TOOLING, chain }
221
219
 
222
- const result = scan(manifests, target, { includeReferences: true })
220
+ const result = scan(manifests, target)
223
221
  const record = stamp?.domains.tooling
224
222
 
225
223
  return {
@@ -236,8 +234,6 @@ export function buildToolingReport(
236
234
  deps: result.deps.filter((entry) => entry.state === 'missing').length,
237
235
  gitignore: result.gitignore.filter((entry) => entry.state === 'missing')
238
236
  .length,
239
- references: result.references.filter((entry) => entry.state === 'pending')
240
- .length,
241
237
  },
242
238
  changes: result.totalChanges,
243
239
  }
@@ -1,12 +1,5 @@
1
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'
2
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'
10
3
  import { dirname, join } from 'node:path'
11
4
  import { $ } from 'bun'
12
5
  import { copyPreservingMode } from '@/copy'
@@ -198,33 +191,3 @@ export async function injectManifest(
198
191
 
199
192
  await injectGitignore(chain, target)
200
193
  }
201
-
202
- /**
203
- * Drops reference docs into `.claude/tooling/`, clearing the legacy
204
- * `tooling/` location the earlier layout used.
205
- */
206
- export async function applyReferences(
207
- chain: readonly Manifest[],
208
- target: string,
209
- stacks: readonly string[],
210
- ): Promise<string[]> {
211
- const destDir = join(target, '.claude', 'tooling')
212
- await mkdir(destDir, { recursive: true })
213
-
214
- const applied: string[] = []
215
- const byName = new Map(chain.map((manifest) => [manifest.name, manifest]))
216
-
217
- for (const stack of stacks) {
218
- const manifest = byName.get(stack)
219
- if (!manifest) continue
220
-
221
- await copyFile(manifest.referenceFile, join(destDir, `${stack}.md`))
222
- logAdd(`.claude/tooling/${stack}.md`)
223
- applied.push(stack)
224
- await rm(join(target, 'tooling', `${stack}.md`), { force: true })
225
- }
226
-
227
- await rmdir(join(target, 'tooling')).catch(() => {})
228
-
229
- return applied
230
- }
@@ -14,7 +14,6 @@ export interface Manifest {
14
14
  readonly dir: string
15
15
  readonly configsDir: string
16
16
  readonly seedsDir: string
17
- readonly referenceFile: string
18
17
  readonly scripts: Readonly<Record<string, string>>
19
18
  readonly scriptOverrides: Readonly<Record<string, string>>
20
19
  readonly gitignore: readonly GitignoreSection[]
@@ -100,7 +99,6 @@ export function loadManifest(
100
99
  dir,
101
100
  configsDir: join(dir, 'configs'),
102
101
  seedsDir: join(dir, 'seeds'),
103
- referenceFile: join(dir, 'reference.md'),
104
102
  scripts: pickStrings(scriptsTable),
105
103
  scriptOverrides: pickStrings(asTable(scriptsTable.override)),
106
104
  gitignore: readGitignoreSections(asTable(parsed.gitignore)),
@@ -0,0 +1,65 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { PROJECT_ROOT } from '@/project-root'
4
+
5
+ /**
6
+ * Spells the package root in a report where every other root spells a
7
+ * project-relative path. The `source` field promises a path inside the project
8
+ * and a package copy is the one source that promise cannot cover, so the
9
+ * spelling has to be one nothing will join to a project root.
10
+ */
11
+ const PACKAGE_LABEL = '<aitk>'
12
+
13
+ export interface ReferenceRoot {
14
+ /** Absolute `tooling/` directory to search. */
15
+ readonly dir: string
16
+ /** How a report spells a copy found under this root. */
17
+ readonly label: string
18
+ }
19
+
20
+ export interface ResolvedReference {
21
+ readonly path: string
22
+ /** The label of the root that won, joined to the stack and filename. */
23
+ readonly source: string
24
+ }
25
+
26
+ function toolingSourceDir(root: string): string {
27
+ return join(root, 'tooling')
28
+ }
29
+
30
+ /**
31
+ * The roots a stack reference resolves against, mirroring `standardRoots` in
32
+ * `src/standards/read.ts`. No stack reference installs into a project any
33
+ * more, so the package corpus is the only root that answers there. The
34
+ * authoring root stays ahead of it for the toolkit's own repository, which
35
+ * authors the corpus this reads, and for a project authoring stacks of its
36
+ * own under the same layout.
37
+ */
38
+ export function referenceRoots(root: string): ReferenceRoot[] {
39
+ return [
40
+ { dir: toolingSourceDir(root), label: 'tooling' },
41
+ {
42
+ dir: toolingSourceDir(PROJECT_ROOT),
43
+ label: join(PACKAGE_LABEL, 'tooling'),
44
+ },
45
+ ]
46
+ }
47
+
48
+ /** Resolves a stack's reference doc, working root first. */
49
+ export function resolveReference(
50
+ root: string,
51
+ stack: string,
52
+ ): ResolvedReference | undefined {
53
+ for (const { dir, label } of referenceRoots(root)) {
54
+ const path = join(dir, stack, 'reference.md')
55
+ if (existsSync(path)) {
56
+ return { path, source: join(label, stack, 'reference.md') }
57
+ }
58
+ }
59
+
60
+ return undefined
61
+ }
62
+
63
+ export function readReference(reference: ResolvedReference): string {
64
+ return readFileSync(reference.path, 'utf8')
65
+ }
@@ -26,26 +26,16 @@ export interface EntryState {
26
26
  readonly state: 'missing' | 'present'
27
27
  }
28
28
 
29
- export interface ReferenceState {
30
- readonly stack: string
31
- readonly state: 'pending' | 'matching'
32
- }
33
-
34
29
  export interface ScanResult {
35
30
  readonly configs: readonly ConfigState[]
36
31
  readonly seeds: readonly SeedState[]
37
32
  readonly scripts: readonly ScriptState[]
38
33
  readonly deps: readonly DepState[]
39
34
  readonly gitignore: readonly EntryState[]
40
- readonly references: readonly ReferenceState[]
41
35
  readonly hasPackageJson: boolean
42
36
  readonly totalChanges: number
43
37
  }
44
38
 
45
- export interface ScanOptions {
46
- readonly includeReferences: boolean
47
- }
48
-
49
39
  function isIdentical(a: string, b: string): boolean {
50
40
  if (!existsSync(b)) return false
51
41
  return readFileSync(a).equals(readFileSync(b))
@@ -55,14 +45,10 @@ function isIdentical(a: string, b: string): boolean {
55
45
  * Compares every stack in the chain against the target and reports what would
56
46
  * change. Nothing is written. Which stack wins a duplicate differs per
57
47
  * category and mirrors the bash: configs, seeds, and scripts resolve nearest
58
- * stack first, while dependencies, gitignore entries, and references resolve
59
- * from the furthest ancestor inward.
48
+ * stack first, while dependencies and gitignore entries resolve from the
49
+ * furthest ancestor inward.
60
50
  */
61
- export function scan(
62
- chain: readonly Manifest[],
63
- target: string,
64
- options: ScanOptions,
65
- ): ScanResult {
51
+ export function scan(chain: readonly Manifest[], target: string): ScanResult {
66
52
  const configs: ConfigState[] = []
67
53
  const seenConfigs = new Set<string>()
68
54
 
@@ -109,17 +95,12 @@ export function scan(
109
95
  : ''
110
96
  const gitignore = scanGitignore(chain, gitignoreContent)
111
97
 
112
- const references = options.includeReferences
113
- ? scanReferences(chain, target)
114
- : []
115
-
116
98
  const totalChanges =
117
99
  configs.filter((entry) => entry.state !== 'matching').length +
118
100
  seeds.filter((entry) => entry.state === 'missing').length +
119
101
  scripts.filter((entry) => entry.state !== 'matching').length +
120
102
  deps.filter((entry) => entry.state === 'missing').length +
121
- gitignore.filter((entry) => entry.state === 'missing').length +
122
- references.filter((entry) => entry.state === 'pending').length
103
+ gitignore.filter((entry) => entry.state === 'missing').length
123
104
 
124
105
  return {
125
106
  configs,
@@ -127,7 +108,6 @@ export function scan(
127
108
  scripts,
128
109
  deps,
129
110
  gitignore,
130
- references,
131
111
  hasPackageJson: pkg !== undefined,
132
112
  totalChanges,
133
113
  }
@@ -165,22 +145,3 @@ function scanGitignore(
165
145
 
166
146
  return states
167
147
  }
168
-
169
- function scanReferences(
170
- chain: readonly Manifest[],
171
- target: string,
172
- ): ReferenceState[] {
173
- const states: ReferenceState[] = []
174
-
175
- for (const manifest of ancestorsFirst(chain)) {
176
- if (!existsSync(manifest.referenceFile)) continue
177
-
178
- const dest = join(target, '.claude', 'tooling', `${manifest.name}.md`)
179
- states.push({
180
- stack: manifest.name,
181
- state: isIdentical(manifest.referenceFile, dest) ? 'matching' : 'pending',
182
- })
183
- }
184
-
185
- return states
186
- }
@@ -1,147 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
- set -o pipefail
4
-
5
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
- PROJECT_ROOT="${PROJECT_ROOT:-$(dirname "$(dirname "$SCRIPT_DIR")")}"
7
-
8
- source "$PROJECT_ROOT/scripts/lib/ui.sh"
9
- source "$PROJECT_ROOT/scripts/lib/tooling.sh"
10
- trap close_timeline EXIT
11
-
12
- show_help() {
13
- echo -e "${GREY}┌${NC}"
14
- log_step "Tooling ref usage"
15
- echo -e "${GREY}│${NC} ${WHITE}Usage:${NC} aitk tooling ref [stack] [target-path]"
16
- echo -e "${GREY}│${NC}"
17
- echo -e "${GREY}│${NC} Drops reference docs only. No config or dependency changes."
18
- echo -e "${GREY}│${NC}"
19
- echo -e "${GREY}│${NC} ${WHITE}Arguments:${NC}"
20
- echo -e "${GREY}│${NC} stack Name of the tooling stack (e.g., base, vite-react)"
21
- echo -e "${GREY}│${NC} target-path Target directory (default: current directory)"
22
- echo -e "${GREY}│${NC}"
23
- echo -e "${GREY}│${NC} ${WHITE}Options:${NC}"
24
- echo -e "${GREY}│${NC} -h, --help ${GREY}# Show this help message${NC}"
25
- echo -e "${GREY}└${NC}"
26
- exit 0
27
- }
28
-
29
- select_stack() {
30
- local stacks=()
31
- mapfile -t stacks < <(list_tooling_stacks)
32
-
33
- if [ ${#stacks[@]} -eq 0 ]; then
34
- log_error "No tooling stacks found in $PROJECT_ROOT/tooling"
35
- fi
36
-
37
- select_option "Select tooling stack:" "${stacks[@]}"
38
- echo "$SELECTED_OPTION"
39
- }
40
-
41
- collect_references() {
42
- local stack="$1"
43
- local target="$2"
44
- local -n _pending=$3
45
-
46
- local manifest="$PROJECT_ROOT/tooling/$stack/manifest.toml"
47
- [ ! -f "$manifest" ] && return
48
-
49
- local extends
50
- extends=$(grep '^extends' "$manifest" 2>/dev/null | cut -d'"' -f2)
51
-
52
- if [ -n "$extends" ]; then
53
- collect_references "$extends" "$target" "$3"
54
- fi
55
-
56
- local reference_file="$PROJECT_ROOT/tooling/$stack/reference.md"
57
- [ ! -f "$reference_file" ] && return
58
-
59
- local dest="$target/.claude/tooling/$stack.md"
60
-
61
- if [ -f "$dest" ] && diff -q "$reference_file" "$dest" >/dev/null 2>&1; then
62
- log_info ".claude/tooling/$stack.md"
63
- return
64
- fi
65
-
66
- log_add ".claude/tooling/$stack.md"
67
- _pending+=("$stack")
68
- }
69
-
70
- apply_references() {
71
- local target="$1"
72
- shift
73
- local stacks=("$@")
74
-
75
- mkdir -p "$target/.claude/tooling"
76
-
77
- for stack in "${stacks[@]}"; do
78
- local src="$PROJECT_ROOT/tooling/$stack/reference.md"
79
- cp "$src" "$target/.claude/tooling/$stack.md"
80
- log_add ".claude/tooling/$stack.md"
81
- rm -f "$target/tooling/$stack.md"
82
- done
83
- rmdir "$target/tooling" 2>/dev/null || true
84
- }
85
-
86
- main() {
87
- if [[ "$1" == "-h" || "$1" == "--help" ]]; then
88
- show_help
89
- fi
90
-
91
- local stack="$1"
92
- local target="${2:-.}"
93
-
94
- # Every frame below goes to stderr, so it asks about that stream rather than
95
- # reading the source-time answer, which is for stdout and is what `show_help`
96
- # above wants. Declared after the help branch so that call keeps it.
97
- local GREEN RED YELLOW WHITE GREY NC
98
- set_palette 2
99
-
100
- echo -e "${GREY}┌${NC}" >&2
101
- echo -e "${GREY}│${NC} ${WHITE}aitk tooling ref${NC}" >&2
102
-
103
- if [ -z "$stack" ]; then
104
- stack=$(select_stack)
105
- fi
106
-
107
- if is_tooling_stack_excluded "$stack"; then
108
- log_error "Claude is managed by \`aitk claude\`, not \`aitk tooling\`."
109
- fi
110
-
111
- if [ ! -d "$PROJECT_ROOT/tooling/$stack" ]; then
112
- log_error "Stack not found: $stack"
113
- fi
114
-
115
- guard_root "$target"
116
-
117
- local pending=()
118
-
119
- echo -e "${GREY}├${NC} ${WHITE}Scanning references: $stack${NC}" >&2
120
- collect_references "$stack" "$target" pending
121
-
122
- if [ "${#pending[@]}" -eq 0 ]; then
123
- trap - EXIT
124
- echo -e "${GREY}└${NC}\n" >&2
125
- echo -e "${GREEN}✓ References up to date${NC}" >&2
126
- exit 0
127
- fi
128
-
129
- local dest_display="$target/.claude/tooling/"
130
- dest_display="${dest_display#./}"
131
-
132
- select_option "Sync ${#pending[@]} reference(s) to $dest_display?" "Yes" "No"
133
-
134
- if [ "$SELECTED_OPTION" = "No" ]; then
135
- log_warn "Cancelled"
136
- exit 0
137
- fi
138
-
139
- log_step "Applying changes"
140
- apply_references "$target" "${pending[@]}"
141
-
142
- trap - EXIT
143
- echo -e "${GREY}└${NC}\n" >&2
144
- echo -e "${GREEN}✓ References synced${NC}" >&2
145
- }
146
-
147
- main "$@"