@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/setup-init/SKILL.md +2 -2
- package/claude/skills/toolkit-cli/SKILL.md +11 -11
- package/docs/target-projects.md +5 -5
- package/package.json +1 -1
- package/src/commands/tooling.ts +56 -32
- package/src/sync/check.ts +1 -5
- package/src/tooling/inject.ts +1 -38
- package/src/tooling/manifest.ts +0 -2
- package/src/tooling/read.ts +65 -0
- package/src/tooling/scan.ts +4 -43
- package/scripts/tooling/ref.sh +0 -147
|
@@ -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
|
|
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.**
|
|
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
|
-
|
|
|
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
|
|
package/docs/target-projects.md
CHANGED
|
@@ -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
|
|
49
|
-
- The agent
|
|
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
|
|
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
|
|
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
|
|
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
package/src/commands/tooling.ts
CHANGED
|
@@ -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 = ['
|
|
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,
|
|
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
|
|
252
|
-
const result = scan(prepared.chain, prepared.target, { includeReferences })
|
|
299
|
+
const result = scan(prepared.chain, prepared.target)
|
|
253
300
|
|
|
254
|
-
report(result
|
|
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
|
|
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
|
|
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
|
}
|
package/src/tooling/inject.ts
CHANGED
|
@@ -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
|
-
}
|
package/src/tooling/manifest.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/tooling/scan.ts
CHANGED
|
@@ -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
|
|
59
|
-
*
|
|
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
|
-
}
|
package/scripts/tooling/ref.sh
DELETED
|
@@ -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 "$@"
|