@slats/claude-assets-sync 0.2.0 → 0.3.1

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.
Files changed (158) hide show
  1. package/README.md +47 -63
  2. package/bin/inject-claude-settings.mjs +4 -0
  3. package/dist/claude-hashes.json +9 -9
  4. package/dist/commands/index.d.ts +1 -1
  5. package/dist/commands/runCli/index.d.ts +1 -1
  6. package/dist/commands/runCli/runCli.d.ts +10 -6
  7. package/dist/commands/runCli/runCli.mjs +33 -6
  8. package/dist/commands/runCli/type.d.ts +4 -12
  9. package/dist/commands/runCli/utils/classifyTarget.d.ts +19 -0
  10. package/dist/commands/runCli/utils/classifyTarget.mjs +46 -0
  11. package/dist/commands/runCli/utils/renderOrFallback.d.ts +6 -0
  12. package/dist/commands/runCli/utils/renderOrFallback.mjs +12 -0
  13. package/dist/commands/runCli/utils/renderPlain.d.ts +11 -0
  14. package/dist/commands/runCli/utils/renderPlain.mjs +89 -0
  15. package/dist/commands/runCli/utils/resolvePackage.d.ts +16 -0
  16. package/dist/commands/runCli/utils/resolvePackage.mjs +74 -0
  17. package/dist/commands/runCli/utils/resolveScopeAlias.d.ts +2 -0
  18. package/dist/commands/runCli/utils/resolveScopeAlias.mjs +67 -0
  19. package/dist/commands/runCli/utils/resolveScopeFlag.d.ts +9 -1
  20. package/dist/commands/runCli/utils/resolveScopeFlag.mjs +5 -11
  21. package/dist/commands/runCli/utils/resolveTargets.d.ts +15 -0
  22. package/dist/commands/runCli/utils/resolveTargets.mjs +38 -0
  23. package/dist/commands/runCli/utils/toConsumerPackages.d.ts +9 -0
  24. package/dist/commands/runCli/utils/toConsumerPackages.mjs +26 -0
  25. package/dist/core/index.d.ts +2 -2
  26. package/dist/core/injectDocs/index.d.ts +3 -2
  27. package/dist/core/injectDocs/type.d.ts +0 -19
  28. package/dist/core/injectDocs/utils/applyAction.mjs +1 -1
  29. package/dist/core/scope/index.d.ts +1 -1
  30. package/dist/core/scope/scope.d.ts +0 -1
  31. package/dist/core/scope/scope.mjs +1 -4
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.mjs +2 -2
  34. package/dist/ui/InjectApp/InjectApp.d.ts +2 -0
  35. package/dist/ui/InjectApp/InjectApp.mjs +82 -0
  36. package/dist/ui/InjectApp/index.d.ts +2 -0
  37. package/dist/ui/InjectApp/utils/eventSelectors.d.ts +5 -0
  38. package/dist/ui/InjectApp/utils/eventSelectors.mjs +24 -0
  39. package/dist/ui/InjectApp/utils/phaseReducer.d.ts +2 -0
  40. package/dist/ui/InjectApp/utils/phaseReducer.mjs +130 -0
  41. package/dist/ui/InjectApp/utils/renderInjectApp.d.ts +2 -0
  42. package/dist/ui/InjectApp/utils/renderInjectApp.mjs +19 -0
  43. package/dist/ui/InjectApp/utils/type.d.ts +5 -0
  44. package/dist/ui/components/ActionRow.d.ts +7 -0
  45. package/dist/ui/components/ActionRow.mjs +45 -0
  46. package/dist/ui/components/Banner.d.ts +7 -0
  47. package/dist/ui/components/Banner.mjs +9 -0
  48. package/dist/ui/components/ConfirmForce.d.ts +8 -0
  49. package/dist/ui/components/ConfirmForce.mjs +35 -0
  50. package/dist/ui/components/ErrorPanel.d.ts +6 -0
  51. package/dist/ui/components/ErrorPanel.mjs +14 -0
  52. package/dist/ui/components/Footer.d.ts +8 -0
  53. package/dist/ui/components/Footer.mjs +27 -0
  54. package/dist/ui/components/PlanTable.d.ts +8 -0
  55. package/dist/ui/components/PlanTable.mjs +15 -0
  56. package/dist/ui/components/ProgressBar.d.ts +10 -0
  57. package/dist/ui/components/ProgressBar.mjs +28 -0
  58. package/dist/ui/components/ScopePicker.d.ts +7 -0
  59. package/dist/ui/components/ScopePicker.mjs +26 -0
  60. package/dist/ui/components/Spinner.d.ts +8 -0
  61. package/dist/ui/components/Spinner.mjs +10 -0
  62. package/dist/ui/components/StatusBadge.d.ts +8 -0
  63. package/dist/ui/components/StepTracker.d.ts +9 -0
  64. package/dist/ui/components/StepTracker.mjs +43 -0
  65. package/dist/ui/components/Summary.d.ts +9 -0
  66. package/dist/ui/components/Summary.mjs +30 -0
  67. package/dist/ui/components/TargetCard.d.ts +11 -0
  68. package/dist/ui/components/TargetCard.mjs +29 -0
  69. package/dist/ui/hooks/useApplyStep.d.ts +12 -0
  70. package/dist/ui/hooks/useApplyStep.mjs +30 -0
  71. package/dist/ui/hooks/useExitApp.d.ts +8 -0
  72. package/dist/ui/hooks/useExitApp.mjs +19 -0
  73. package/dist/ui/hooks/useForceConfirmStep.d.ts +9 -0
  74. package/dist/ui/hooks/useForceConfirmStep.mjs +24 -0
  75. package/dist/ui/hooks/useInjectSession.d.ts +10 -0
  76. package/dist/ui/hooks/useInjectSession.mjs +63 -0
  77. package/dist/ui/hooks/useInterval.d.ts +1 -0
  78. package/dist/ui/hooks/usePhase.d.ts +2 -0
  79. package/dist/ui/hooks/usePhase.mjs +9 -0
  80. package/dist/ui/hooks/usePlanStep.d.ts +13 -0
  81. package/dist/ui/hooks/usePlanStep.mjs +94 -0
  82. package/dist/ui/hooks/useResolveStep.d.ts +18 -0
  83. package/dist/ui/hooks/useResolveStep.mjs +21 -0
  84. package/dist/ui/hooks/useTerminalWidth.d.ts +1 -0
  85. package/dist/ui/index.d.ts +2 -0
  86. package/dist/ui/index.mjs +16 -0
  87. package/dist/ui/theme/colors.d.ts +12 -0
  88. package/dist/ui/theme/colors.mjs +9 -0
  89. package/dist/ui/theme/icons.d.ts +29 -0
  90. package/dist/ui/theme/icons.mjs +17 -0
  91. package/dist/ui/theme/layout.d.ts +20 -0
  92. package/dist/ui/theme/layout.mjs +9 -0
  93. package/dist/ui/types/event.d.ts +45 -0
  94. package/dist/ui/types/index.d.ts +4 -0
  95. package/dist/ui/types/phase.d.ts +44 -0
  96. package/dist/ui/types/render.d.ts +6 -0
  97. package/dist/ui/types/target.d.ts +25 -0
  98. package/dist/utils/version.d.ts +1 -1
  99. package/dist/utils/version.mjs +1 -1
  100. package/docs/claude/skills/claude-docs-asset-wiring/SKILL.md +159 -0
  101. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/claude-md-template.md +78 -0
  102. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/dependency-cruiser.md +54 -0
  103. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/gotchas.md +125 -0
  104. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/package-json-patches.md +150 -0
  105. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/reference-files.md +37 -0
  106. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/smoke-tests.md +111 -0
  107. package/docs/consumer-integration.md +43 -101
  108. package/package.json +13 -8
  109. package/scripts/dev-ui-fixtures.ts +288 -0
  110. package/scripts/dev-ui.tsx +289 -0
  111. package/bin/claude-sync.mjs +0 -24
  112. package/dist/commands/runCli/runCli.cjs +0 -31
  113. package/dist/commands/runCli/utils/injectOne.cjs +0 -48
  114. package/dist/commands/runCli/utils/injectOne.d.ts +0 -3
  115. package/dist/commands/runCli/utils/injectOne.mjs +0 -46
  116. package/dist/commands/runCli/utils/resolveScopeFlag.cjs +0 -28
  117. package/dist/commands/runCli/utils/runInject.cjs +0 -36
  118. package/dist/commands/runCli/utils/runInject.d.ts +0 -2
  119. package/dist/commands/runCli/utils/runInject.mjs +0 -34
  120. package/dist/core/buildPlan/buildPlan.cjs +0 -42
  121. package/dist/core/buildPlan/utils/toPosix.cjs +0 -9
  122. package/dist/core/buildPlan/utils/walkFiles.cjs +0 -25
  123. package/dist/core/hash/hash.cjs +0 -30
  124. package/dist/core/hashManifest/hashManifest.cjs +0 -27
  125. package/dist/core/injectDocs/injectDocs.cjs +0 -43
  126. package/dist/core/injectDocs/injectDocs.d.ts +0 -2
  127. package/dist/core/injectDocs/injectDocs.mjs +0 -41
  128. package/dist/core/injectDocs/utils/applyAction.cjs +0 -21
  129. package/dist/core/injectDocs/utils/emitCiForceList.cjs +0 -10
  130. package/dist/core/injectDocs/utils/emitCiForceList.d.ts +0 -2
  131. package/dist/core/injectDocs/utils/emitCiForceList.mjs +0 -8
  132. package/dist/core/injectDocs/utils/printPlan.cjs +0 -20
  133. package/dist/core/injectDocs/utils/printPlan.d.ts +0 -2
  134. package/dist/core/injectDocs/utils/printPlan.mjs +0 -18
  135. package/dist/core/injectDocs/utils/summarize.cjs +0 -27
  136. package/dist/core/scope/scope.cjs +0 -46
  137. package/dist/core/scope/utils/isDirectory.cjs +0 -14
  138. package/dist/index.cjs +0 -20
  139. package/dist/prompts/confirmForce.cjs +0 -27
  140. package/dist/prompts/confirmForce.d.ts +0 -1
  141. package/dist/prompts/confirmForce.mjs +0 -25
  142. package/dist/prompts/index.d.ts +0 -2
  143. package/dist/prompts/selectScope.cjs +0 -30
  144. package/dist/prompts/selectScope.d.ts +0 -2
  145. package/dist/prompts/selectScope.mjs +0 -28
  146. package/dist/utils/asyncPool.cjs +0 -26
  147. package/dist/utils/heartbeat.cjs +0 -25
  148. package/dist/utils/heartbeat.d.ts +0 -16
  149. package/dist/utils/heartbeat.mjs +0 -23
  150. package/dist/utils/logger.cjs +0 -74
  151. package/dist/utils/version.cjs +0 -5
  152. package/docs/claude/skills/claude-sync-applier/SKILL.md +0 -195
  153. package/docs/claude/skills/claude-sync-applier/knowledge/claude-md-template.md +0 -77
  154. package/docs/claude/skills/claude-sync-applier/knowledge/dependency-cruiser.md +0 -126
  155. package/docs/claude/skills/claude-sync-applier/knowledge/gotchas.md +0 -139
  156. package/docs/claude/skills/claude-sync-applier/knowledge/package-json-patches.md +0 -130
  157. package/docs/claude/skills/claude-sync-applier/knowledge/reference-files.md +0 -120
  158. package/docs/claude/skills/claude-sync-applier/knowledge/smoke-tests.md +0 -102
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # @slats/claude-assets-sync
2
2
 
3
- Shared CLI engine that lets any npm package ship its own Claude Code docs (skills, rules, commands) and inject them into a user's `.claude/` directory through a thin `claude-sync` bin stub.
3
+ Engine + dispatcher CLI that lets any npm package ship its own Claude Code docs (skills, rules, commands) and inject them into a user's `.claude/` directory. Consumers declare `claude.assetPath` in `package.json` and the engine's `inject-claude-settings` bin handles the rest.
4
4
 
5
5
  ## Overview
6
6
 
7
- A consumer package ships a thin `bin/claude-sync.mjs` stub that reads its own `package.json` and calls `runCli(argv, { packageRoot, packageName, packageVersion, assetPath })`. End users run `npx claude-sync` (or the consumer's own bin alias) and this engine compares a per-file SHA-256 manifest against the target `.claude/`, copying only what is out of date. The library operates on exactly one consumer per invocation — the one supplied by the caller; it never walks `node_modules` or yarn workspaces.
7
+ A consumer package declares `claude.assetPath` in `package.json` and runs `claude-build-hashes` during build to emit `dist/claude-hashes.json`. End users run `npx -p @slats/claude-assets-sync inject-claude-settings --package=<name>` and this engine resolves that single package's metadata via `createRequire`, compares the hash manifest against the target `.claude/`, and copies only what is out of date.
8
+
9
+ The library operates on exactly one consumer per invocation — the one named in `--package`. It never walks `node_modules` for siblings; it never enumerates workspaces.
8
10
 
9
11
  No GitHub fetch, no `.sync-meta.json`, no migrations — the consumer's `dist/claude-hashes.json` is the single source of truth.
10
12
 
@@ -19,96 +21,72 @@ yarn add -D @slats/claude-assets-sync
19
21
  ## CLI Surface
20
22
 
21
23
  ```
22
- claude-sync [--scope=user|project] [--dry-run] [--force]
24
+ inject-claude-settings --package=<name> [--scope=user|project] [--dry-run] [--force] [--root=<cwd>]
25
+ claude-build-hashes
23
26
  ```
24
27
 
25
- Each consumer package exposes its own `claude-sync` bin entry (see [Consumer Integration](#consumer-integration-3-steps) below). When invoked, the engine operates on exactly one consumer — the one whose metadata was passed by the stub. There is no cross-package discovery.
28
+ ### End-user invocation
29
+
30
+ The engine is not shipped as a runtime dependency of consumers. Always invoke via `npx -p @slats/claude-assets-sync ...`; the package manager fetches and caches the engine on demand.
31
+
32
+ ```bash
33
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@canard/schema-form --scope=user
34
+ ```
26
35
 
27
36
  | Flag | Meaning |
28
37
  |---|---|
29
- | `--scope=user` | `~/.claude` (applies globally) |
30
- | `--scope=project` | nearest ancestor `.claude` directory, or `<cwd>/.claude` if none found |
31
- | `--dry-run` | print the copy / skip / warn plan, no writes |
32
- | `--force` | overwrite diverged files & delete orphans (interactive confirm on TTY) |
38
+ | `--package <name>` | **Required.** Scoped npm name of a consumer that declares `claude.assetPath`. |
39
+ | `--scope=user` | `~/.claude` (applies globally). |
40
+ | `--scope=project` | Nearest ancestor `.claude` directory, or `<cwd>/.claude` if none found. |
41
+ | `--dry-run` | Print the copy / skip / warn plan, no writes. |
42
+ | `--force` | Overwrite diverged files & delete orphans (interactive confirm on TTY). |
43
+ | `--root <path>` | Override scope-resolution cwd. |
33
44
 
34
- **Exit codes**: `0` success / up-to-date / dry-run, `1` runtime error, `2` user / configuration error (e.g. missing `--scope` in non-TTY, invalid `assetPath`).
45
+ **Exit codes**: `0` success / up-to-date / dry-run, `1` runtime error, `2` user / configuration error (missing `--package`, missing `--scope` in non-TTY, unresolvable package, missing `claude.assetPath`).
35
46
 
36
47
  For `--scope=project` the target `.claude` directory is resolved by walking up from `process.cwd()` to the nearest existing `.claude` ancestor; the CLI logs `(auto-located)` when this happens.
37
48
 
38
- ## Consumer Integration (3 steps)
49
+ ## Consumer Integration (2 steps)
39
50
 
40
51
  ### 1. `package.json`
41
52
 
42
53
  ```jsonc
43
54
  {
44
55
  "name": "@your-scope/your-package",
45
- "bin": { "claude-sync": "./bin/claude-sync.mjs" },
46
- "files": ["dist", "docs", "dist/claude-hashes.json", "bin", "README.md"],
47
56
  "scripts": {
48
57
  "build": "… && yarn build:hashes",
49
- "build:hashes": "node scripts/build-hashes.mjs"
58
+ "build:hashes": "claude-build-hashes"
50
59
  },
51
- "dependencies": {
60
+ "devDependencies": {
52
61
  "@slats/claude-assets-sync": "workspace:^"
53
62
  },
63
+ "files": ["dist", "docs", "README.md"],
54
64
  "claude": { "assetPath": "docs/claude" }
55
65
  }
56
66
  ```
57
67
 
58
- Do **not** expose `./bin/*` in `exports` — that would let consumer bundlers pull CLI code into app bundles.
59
-
60
- ### 2. `bin/claude-sync.mjs`
61
-
62
- ```javascript
63
- #!/usr/bin/env node
64
- import { runCli } from '@slats/claude-assets-sync';
65
- import { readFile } from 'node:fs/promises';
66
- import { dirname, resolve } from 'node:path';
67
- import { fileURLToPath } from 'node:url';
68
-
69
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
70
- const pkg = JSON.parse(
71
- await readFile(resolve(packageRoot, 'package.json'), 'utf-8'),
72
- );
68
+ - `@slats/claude-assets-sync` MUST be in `devDependencies` — the engine is a CLI-only tool and must not leak into end-user production installs. See Rationale below.
69
+ - Do **not** add any `bin` field. The engine is the sole CLI surface; per-consumer bins would collide under `node_modules/.bin/`.
70
+ - Do **not** expose `./bin/*` or `./docs/*` in `exports`. That would let consumer bundlers pull CLI code or the asset tree into app bundles.
71
+ - Do **not** create a `bin/` or `scripts/` directory in the consumer.
73
72
 
74
- if (typeof pkg.claude?.assetPath !== 'string') {
75
- process.stderr.write(
76
- `[claude-sync] missing or invalid "claude.assetPath" in ${resolve(packageRoot, 'package.json')}\n`,
77
- );
78
- process.exit(2);
79
- }
73
+ ### 2. Build
80
74
 
81
- await runCli(process.argv, {
82
- packageRoot,
83
- packageName: pkg.name,
84
- packageVersion: pkg.version,
85
- assetPath: pkg.claude.assetPath,
86
- }).catch((err) => {
87
- process.stderr.write(
88
- `[${pkg.name}] claude-sync failed: ${err instanceof Error ? err.message : String(err)}\n`,
89
- );
90
- process.exit(1);
91
- });
75
+ ```bash
76
+ yarn build
77
+ # rolls up the library, emits types, then `claude-build-hashes` hashes every
78
+ # file under `claude.assetPath` and writes dist/claude-hashes.json
92
79
  ```
93
80
 
94
- The `claude.assetPath` field in the consumer's `package.json` is a **consumer-side convention**; the library does not enforce or read it. Consumers are free to resolve `assetPath` in any way they choose and pass the result to `runCli`.
95
-
96
- ### 3. `scripts/build-hashes.mjs`
97
-
98
- ```javascript
99
- #!/usr/bin/env node
100
- import { buildHashes } from '@slats/claude-assets-sync/buildHashes';
81
+ Ship the resulting `dist/` (including `claude-hashes.json`) alongside `docs/` when you publish.
101
82
 
102
- try {
103
- const { outPath, fileCount } = await buildHashes();
104
- console.log(`✓ claude-hashes.json written: ${fileCount} file(s) → ${outPath}`);
105
- } catch (err) {
106
- console.error('❌ buildHashes failed:', err?.message ?? err);
107
- process.exit(1);
108
- }
109
- ```
83
+ ### Rationale: `devDependencies`, not `dependencies`
110
84
 
111
- `buildHashes` reads the current `package.json`'s `claude.assetPath`, hashes every file beneath it (ignoring `.omc/**`, `*.log`, `.DS_Store`), and writes `dist/claude-hashes.json`.
85
+ - The engine is used at two moments only: (1) the consumer's own build, where `claude-build-hashes` produces `dist/claude-hashes.json`, and (2) the end user's one-off `inject-claude-settings` invocation. Neither is runtime behaviour of the consumer library.
86
+ - Putting the engine in `dependencies` would force every downstream installer of the consumer to pull `commander`, `@inquirer/prompts`, and their transitive trees into their production `node_modules` — dead weight for anyone who never sets up Claude Code assets.
87
+ - The workspace build chain still resolves `.bin/claude-build-hashes` from `devDependencies` at `yarn install` time; yarn workspaces link devDeps and deps identically for workspace-local builds.
88
+ - End users never rely on a hoisted `inject-claude-settings` bin. The canonical invocation is `npx -p @slats/claude-assets-sync inject-claude-settings --package=<THIS>`, which fetches the engine on demand and caches it.
89
+ - Bundle isolation is enforced by the import graph (`src/**` in the consumer never references the engine), not by dependency-type.
112
90
 
113
91
  ## Authoring `docs/claude/`
114
92
 
@@ -138,6 +116,12 @@ Every file under the asset root is hashed and tracked in `dist/claude-hashes.jso
138
116
  - `--force` on TTY opens an interactive confirm via `@inquirer/prompts.confirm`, listing up to 3 diverged/orphan paths.
139
117
  - `--force` on non-TTY prints the divergent list to stderr and proceeds.
140
118
 
119
+ ## Architectural Invariants
120
+
121
+ - `src/core/**` never reads `package.json` or walks the filesystem. Only the `bin/` layer (and `src/commands/runCli/utils/resolvePackage.ts`, invoked from that dispatcher) is allowed to resolve a single explicitly-named target via `createRequire().resolve('${name}/package.json')`. Cross-package discovery (`--all`, workspace scan) is not supported.
122
+ - Prompts go through `@inquirer/prompts` only. No ink, no React.
123
+ - The engine assumes one consumer per invocation. That is the stable contract — extensions require explicit re-architecture.
124
+
141
125
  ## Programmatic API
142
126
 
143
127
  ```ts
@@ -156,7 +140,7 @@ See `src/index.ts` and `src/DETAIL.md` for the full export surface.
156
140
 
157
141
  ## Additional Docs
158
142
 
159
- - `docs/consumer-integration.md` — complete consumer checklist (dep-cruiser rules, verification steps, end-user install topologies)
143
+ - `docs/consumer-integration.md` — complete consumer checklist (package.json patches, verification steps, end-user install topologies)
160
144
  - `docs/bundle-size-decision.md` — why `@inquirer/prompts` over ink
161
145
 
162
146
  ## License
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '@slats/claude-assets-sync';
3
+
4
+ await runCli(process.argv);
@@ -2,19 +2,19 @@
2
2
  "schemaVersion": 1,
3
3
  "package": {
4
4
  "name": "@slats/claude-assets-sync",
5
- "version": "0.2.0"
5
+ "version": "0.3.1"
6
6
  },
7
- "generatedAt": "2026-04-24T04:25:43.386Z",
7
+ "generatedAt": "2026-04-24T16:37:59.339Z",
8
8
  "algorithm": "sha256",
9
9
  "assetRoot": "docs/claude",
10
10
  "files": {
11
- "skills/claude-sync-applier/SKILL.md": "1c846b7ff7b127217a3201028ec4e1476857e5a5bb38b92d863b92a16ac4c7d0",
12
- "skills/claude-sync-applier/knowledge/claude-md-template.md": "966fcc668b0e8f43ae83a6111ba1887d5504182e0b138e19937abb1fb43b3318",
13
- "skills/claude-sync-applier/knowledge/dependency-cruiser.md": "058de112be3c03999846a66d066be540f72f2b1c40f5a1d04a82d36236a3c562",
14
- "skills/claude-sync-applier/knowledge/gotchas.md": "310ffb416cbcaaf4b9162d0e6004dead6a8d658ff693ffe9e8580a8b83f7b6ea",
15
- "skills/claude-sync-applier/knowledge/package-json-patches.md": "3ef1c39800e541e6390575a5296a8a247772b58c126c30149cd1efa61ead5dcc",
16
- "skills/claude-sync-applier/knowledge/reference-files.md": "d0d42df1f90b17d8a35e914bfd16fe381890d71e0ea6a767d1e5e14874f2b95f",
17
- "skills/claude-sync-applier/knowledge/smoke-tests.md": "76c396e2c7d410920139c894fc52d74e9914ea59d0e41dc30ebfe106e99c4be3"
11
+ "skills/claude-docs-asset-wiring/SKILL.md": "4864a5a1aba0813fb64ad19111f3db4369df6cc025e485d03d17cdc9d245bce5",
12
+ "skills/claude-docs-asset-wiring/knowledge/claude-md-template.md": "b0274c5a37076a7e80bd7beccd739b10cc91ca5395fe425be25d9129206c684a",
13
+ "skills/claude-docs-asset-wiring/knowledge/dependency-cruiser.md": "18e994bd16242f72a4dc6cfa508493f4b63853a7a16c4910c7b7abd0368bd103",
14
+ "skills/claude-docs-asset-wiring/knowledge/gotchas.md": "70a6c75ea09816ec457615c060e6a87197bc5ed332a377452b3cacd974b44eee",
15
+ "skills/claude-docs-asset-wiring/knowledge/package-json-patches.md": "063469932191843a2f7f9156a513717947bcad584c4084a0c8300897683885e7",
16
+ "skills/claude-docs-asset-wiring/knowledge/reference-files.md": "1b5f76d262f47835c4d0150a48fa25dfc644609fd4ecad260273a4cab57e41a2",
17
+ "skills/claude-docs-asset-wiring/knowledge/smoke-tests.md": "c90fc82d46bf4b55d794c2ce463b4c1e199d13d6b1161028486fb4bbbd6a60fe"
18
18
  },
19
19
  "previousVersions": {}
20
20
  }
@@ -1 +1 @@
1
- export { runCli, type DefaultFlags, type RunCliOptions, } from './runCli/index.js';
1
+ export { runCli, type DefaultFlags } from './runCli/index.js';
@@ -1,2 +1,2 @@
1
1
  export { runCli } from './runCli.js';
2
- export type { DefaultFlags, RunCliOptions } from './type.js';
2
+ export type { DefaultFlags } from './type.js';
@@ -1,10 +1,14 @@
1
- import type { RunCliOptions } from './type.js';
2
1
  /**
3
2
  * CLI entry for `@slats/claude-assets-sync`.
4
3
  *
5
- * The caller passes its own package metadata (`packageRoot`, `packageName`,
6
- * `packageVersion`, `assetPath`) so the library never walks the filesystem
7
- * looking for consumers. Consumer bin stubs read their own `package.json`
8
- * and forward the resolved values here.
4
+ * The `inject-claude-settings` dispatcher parses `--package <name...>`
5
+ * from argv and classifies each value:
6
+ * - `@<scope>` enumerate every workspace package under that scope
7
+ * - `@<scope>/<name>` one scoped package
8
+ * - `<name>` — one unscoped package
9
+ *
10
+ * Targets are resolved via Node module resolution (`resolvePackage`)
11
+ * except for scope aliases, which are the only path allowed to walk
12
+ * the monorepo — that exception is isolated to `resolveScopeAlias.ts`.
9
13
  */
10
- export declare function runCli(argv: readonly string[] | undefined, options: RunCliOptions): Promise<void>;
14
+ export declare function runCli(argv?: readonly string[]): Promise<void>;
@@ -1,20 +1,38 @@
1
1
  import { Command } from 'commander';
2
2
  import { logger } from '../../utils/logger.mjs';
3
3
  import { VERSION } from '../../utils/version.mjs';
4
- import { runInject } from './utils/runInject.mjs';
4
+ import { renderOrFallback } from './utils/renderOrFallback.mjs';
5
+ import { resolveTargets } from './utils/resolveTargets.mjs';
6
+ import { toConsumerPackages } from './utils/toConsumerPackages.mjs';
5
7
 
6
- async function runCli(argv = process.argv, options) {
8
+ async function runCli(argv = process.argv) {
7
9
  const cmd = new Command();
8
10
  cmd
9
- .name('claude-sync')
10
- .description("Inject this package's assets into the target .claude directory")
11
- .version(options.version ?? VERSION)
11
+ .name('inject-claude-settings')
12
+ .description("Inject target consumer(s)' Claude assets into the selected .claude directory")
13
+ .version(VERSION)
14
+ .option('--package <name...>', 'Target(s). "@<scope>" = whole npm scope; "@<scope>/<name>" or "<name>" = one package. Repeat the flag or comma-separate values.', collectPackageValues, [])
12
15
  .option('--scope <scope>', 'Target scope: user (~/.claude) | project (nearest ancestor .claude or <cwd>/.claude)')
13
16
  .option('--dry-run', 'Preview without writing', false)
14
17
  .option('--force', 'Overwrite user modifications', false)
15
18
  .option('--root <path>', 'Override scope resolution cwd (default: cwd)')
19
+ .option('--json', 'Emit structured JSON output (forces non-interactive legacy logger path)', false)
16
20
  .action(async (flags) => {
17
- await runInject(flags, options);
21
+ const targets = flags.package ?? [];
22
+ if (targets.length === 0) {
23
+ logger.error('missing required flag: --package <name> (e.g. --package=@canard/schema-form or --package=@canard)');
24
+ process.exit(2);
25
+ }
26
+ const originCwd = flags.root ?? process.cwd();
27
+ const metadataList = await resolveTargets(targets, originCwd);
28
+ if (metadataList.length === 0) {
29
+ logger.warn(`no packages resolved from --package target(s): ${targets.join(', ')}`);
30
+ return;
31
+ }
32
+ const consumerPackages = await toConsumerPackages(metadataList);
33
+ const exitCode = await renderOrFallback(consumerPackages, flags, originCwd);
34
+ if (exitCode !== 0)
35
+ process.exit(exitCode);
18
36
  });
19
37
  try {
20
38
  await cmd.parseAsync([...argv]);
@@ -25,5 +43,14 @@ async function runCli(argv = process.argv, options) {
25
43
  process.exit(1);
26
44
  }
27
45
  }
46
+ function collectPackageValues(value, previous = []) {
47
+ return [
48
+ ...previous,
49
+ ...value
50
+ .split(',')
51
+ .map((s) => s.trim())
52
+ .filter(Boolean),
53
+ ];
54
+ }
28
55
 
29
56
  export { runCli };
@@ -1,23 +1,15 @@
1
- export interface RunCliOptions {
2
- version?: string;
3
- /** Absolute filesystem path to the consumer package root. */
4
- packageRoot: string;
5
- /** Consumer package name used in logs and error messages. */
6
- packageName: string;
7
- /** Consumer package version used in logs. */
8
- packageVersion: string;
9
- /** Asset directory path relative to `packageRoot`. */
10
- assetPath: string;
11
- }
12
1
  export interface DefaultFlags {
13
2
  scope?: string;
14
3
  dryRun?: boolean;
15
4
  force?: boolean;
16
5
  root?: string;
6
+ package?: string[];
7
+ json?: boolean;
17
8
  }
18
9
  /**
19
10
  * Resolved consumer metadata passed to the injection pipeline.
20
- * The caller owns the definition; the library does not read `package.json`.
11
+ * The dispatcher bin populates this by resolving a single explicitly-named
12
+ * target package — `core/**` still never reads `package.json` itself.
21
13
  */
22
14
  export interface ConsumerPackage {
23
15
  name: string;
@@ -0,0 +1,19 @@
1
+ export type ClassifiedTarget = {
2
+ kind: 'scope';
3
+ scope: string;
4
+ } | {
5
+ kind: 'package';
6
+ name: string;
7
+ } | {
8
+ kind: 'invalid';
9
+ reason: string;
10
+ };
11
+ /**
12
+ * Classify a `--package` value as a scope alias, a package name, or invalid.
13
+ *
14
+ * - `@<scope>` (no slash) — all packages under that npm scope
15
+ * - `@<scope>/<name>` — one scoped package
16
+ * - `<name>` (no `@`, no slash) — one unscoped package
17
+ * - anything else → invalid
18
+ */
19
+ export declare function classifyTarget(target: string): ClassifiedTarget;
@@ -0,0 +1,46 @@
1
+ const NPM_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
2
+ function classifyTarget(target) {
3
+ if (typeof target !== 'string' || target.length === 0) {
4
+ return { kind: 'invalid', reason: 'empty --package value' };
5
+ }
6
+ if (target.startsWith('@')) {
7
+ const body = target.slice(1);
8
+ const slashIndex = body.indexOf('/');
9
+ if (slashIndex === -1) {
10
+ if (!NPM_NAME_PATTERN.test(body)) {
11
+ return {
12
+ kind: 'invalid',
13
+ reason: `invalid scope alias "${target}" — expected "@<scope>" with lowercase alphanumerics, ".", "-", or "_"`,
14
+ };
15
+ }
16
+ return { kind: 'scope', scope: body };
17
+ }
18
+ const scopePart = body.slice(0, slashIndex);
19
+ const namePart = body.slice(slashIndex + 1);
20
+ if (!NPM_NAME_PATTERN.test(scopePart) ||
21
+ namePart.length === 0 ||
22
+ namePart.includes('/') ||
23
+ !NPM_NAME_PATTERN.test(namePart)) {
24
+ return {
25
+ kind: 'invalid',
26
+ reason: `invalid scoped package "${target}" — expected "@<scope>/<name>"`,
27
+ };
28
+ }
29
+ return { kind: 'package', name: target };
30
+ }
31
+ if (target.includes('/')) {
32
+ return {
33
+ kind: 'invalid',
34
+ reason: `invalid target "${target}" — unscoped package names cannot contain "/"`,
35
+ };
36
+ }
37
+ if (!NPM_NAME_PATTERN.test(target)) {
38
+ return {
39
+ kind: 'invalid',
40
+ reason: `invalid package name "${target}" — expected lowercase alphanumerics, ".", "-", or "_"`,
41
+ };
42
+ }
43
+ return { kind: 'package', name: target };
44
+ }
45
+
46
+ export { classifyTarget };
@@ -0,0 +1,6 @@
1
+ import type { ConsumerPackage, DefaultFlags } from '../type.js';
2
+ interface RenderEnv {
3
+ readonly isTTY?: boolean;
4
+ }
5
+ export declare function renderOrFallback(targets: readonly ConsumerPackage[], flags: DefaultFlags, originCwd: string, env?: RenderEnv): Promise<number>;
6
+ export {};
@@ -0,0 +1,12 @@
1
+ import { renderPlain } from './renderPlain.mjs';
2
+
3
+ async function renderOrFallback(targets, flags, originCwd, env = {}) {
4
+ const isTTY = env.isTTY ?? Boolean(process.stdout.isTTY);
5
+ if (flags.json || !isTTY) {
6
+ return renderPlain(targets, flags, originCwd);
7
+ }
8
+ const ui = (await import('../../../ui/index.mjs'));
9
+ return ui.renderInjectApp({ targets, flags, originCwd });
10
+ }
11
+
12
+ export { renderOrFallback };
@@ -0,0 +1,11 @@
1
+ import type { ConsumerPackage, DefaultFlags } from '../type.js';
2
+ /**
3
+ * Plain (picocolors) renderer for non-TTY / `--json` invocations.
4
+ *
5
+ * Composes the same `core/**` primitives that the Ink `useInjectSession`
6
+ * pipeline uses — no legacy `injectDocs` orchestrator in between.
7
+ * Missing `--scope` on non-TTY exits via `resolveScopeFlag` with code 2.
8
+ * Divergent/orphan files still respect `--force`: absence of `--force`
9
+ * returns 2; presence logs the list to stderr and proceeds.
10
+ */
11
+ export declare function renderPlain(targets: readonly ConsumerPackage[], flags: DefaultFlags, originCwd: string): Promise<number>;
@@ -0,0 +1,89 @@
1
+ import 'node:crypto';
2
+ import 'node:fs/promises';
3
+ import { readHashManifest, computeNamespacePrefixes } from '../../../core/hashManifest/hashManifest.mjs';
4
+ import { applyAction } from '../../../core/injectDocs/utils/applyAction.mjs';
5
+ import { summarize } from '../../../core/injectDocs/utils/summarize.mjs';
6
+ import { buildPlan } from '../../../core/buildPlan/buildPlan.mjs';
7
+ import { resolveScope } from '../../../core/scope/scope.mjs';
8
+ import { asyncPool } from '../../../utils/asyncPool.mjs';
9
+ import { logger } from '../../../utils/logger.mjs';
10
+ import { resolveScopeFlag } from './resolveScopeFlag.mjs';
11
+
12
+ async function renderPlain(targets, flags, originCwd) {
13
+ if (targets.length === 0)
14
+ return 0;
15
+ const scope = resolveScopeFlag(flags.scope);
16
+ const fatalOnError = targets.length === 1;
17
+ let failureCount = 0;
18
+ for (const target of targets) {
19
+ if (!target.hashesPresent) {
20
+ logger.warn(`${target.name}: dist/claude-hashes.json missing — build the package (e.g. yarn build) to regenerate the hash manifest first.`);
21
+ continue;
22
+ }
23
+ logger.heading(`${target.name}@${target.version}`);
24
+ let exitCode;
25
+ try {
26
+ exitCode = await renderOneTarget(target, scope, flags, originCwd);
27
+ }
28
+ catch (err) {
29
+ const msg = err instanceof Error ? err.message : String(err);
30
+ logger.error(`${target.name}: ${msg}`);
31
+ exitCode = 1;
32
+ }
33
+ if (exitCode !== 0) {
34
+ if (fatalOnError)
35
+ return exitCode;
36
+ failureCount += 1;
37
+ }
38
+ }
39
+ return failureCount > 0 ? 1 : 0;
40
+ }
41
+ async function renderOneTarget(target, scope, flags, originCwd) {
42
+ const manifest = await readHashManifest(target.packageRoot);
43
+ const resolution = resolveScope(scope, originCwd);
44
+ const plan = await buildPlan({
45
+ sourceHashes: manifest.files,
46
+ targetRoot: resolution.targetRoot,
47
+ namespacePrefixes: computeNamespacePrefixes(manifest),
48
+ force: flags.force ?? false,
49
+ });
50
+ logger.info(`${target.name}@${target.version} → ${resolution.description}`);
51
+ printPlan(plan);
52
+ if (plan.requiresForce && !flags.force) {
53
+ logger.error('Re-run with --force to proceed, or inspect with --dry-run.');
54
+ return 2;
55
+ }
56
+ if (flags.force &&
57
+ plan.actions.some((a) => a.kind === 'warn-diverged' || a.kind === 'warn-orphan')) {
58
+ emitForceList(plan);
59
+ }
60
+ if (flags.dryRun) {
61
+ logger.warn('[DRY RUN] No files will be created, overwritten, or deleted.');
62
+ return 0;
63
+ }
64
+ await asyncPool(8, plan.actions, (action) => applyAction(action, target.assetRoot));
65
+ const report = summarize(plan, 0);
66
+ return report.exitCode;
67
+ }
68
+ function printPlan(plan) {
69
+ for (const action of plan.actions) {
70
+ if (action.kind === 'copy')
71
+ logger.file('create', action.relPath);
72
+ else if (action.kind === 'skip-uptodate')
73
+ logger.file('skip', `${action.relPath} (up-to-date)`);
74
+ else if (action.kind === 'warn-diverged')
75
+ logger.warn(`${action.relPath} — local differs from source (user edit or version change)`);
76
+ else if (action.kind === 'warn-orphan')
77
+ logger.warn(`${action.relPath} — present locally, absent in source`);
78
+ else if (action.kind === 'delete')
79
+ logger.file('update', `${action.relPath} (deleting)`);
80
+ }
81
+ }
82
+ function emitForceList(plan) {
83
+ const divergent = plan.actions.filter((action) => action.kind === 'warn-diverged' || action.kind === 'warn-orphan');
84
+ process.stderr.write(`[claude-assets-sync] --force overwriting ${divergent.length} file(s) in non-TTY mode:\n`);
85
+ for (const action of divergent)
86
+ process.stderr.write(` ${action.relPath}\n`);
87
+ }
88
+
89
+ export { renderPlain };
@@ -0,0 +1,16 @@
1
+ export interface ResolvedMetadata {
2
+ packageRoot: string;
3
+ packageName: string;
4
+ packageVersion: string;
5
+ assetPath: string;
6
+ }
7
+ export interface ResolvePackageOptions {
8
+ /**
9
+ * When `true`, a package without `claude.assetPath` is warned and the
10
+ * function returns `null` instead of calling `process.exit`. Default
11
+ * `false` preserves the v0.3.0 strict behavior for single-target
12
+ * dispatcher calls.
13
+ */
14
+ skipMissingAsset?: boolean;
15
+ }
16
+ export declare function resolvePackage(name: string, options?: ResolvePackageOptions): Promise<ResolvedMetadata | null>;
@@ -0,0 +1,74 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { logger } from '../../../utils/logger.mjs';
6
+
7
+ async function resolvePackage(name, options = {}) {
8
+ const pkgJsonPath = resolvePackageJsonPath(name);
9
+ if (!pkgJsonPath) {
10
+ logger.error(`cannot resolve package "${name}". Install it in the current project or pass the correct name.`);
11
+ process.exit(2);
12
+ }
13
+ const packageRoot = dirname(pkgJsonPath);
14
+ const raw = await readFile(pkgJsonPath, 'utf-8');
15
+ const pkg = JSON.parse(raw);
16
+ if (typeof pkg.name !== 'string' || typeof pkg.version !== 'string') {
17
+ if (options.skipMissingAsset) {
18
+ logger.warn(`"${name}" package.json is missing a string "name" or "version" — skipping.`);
19
+ return null;
20
+ }
21
+ logger.error(`${pkgJsonPath} must define string "name" and "version".`);
22
+ process.exit(2);
23
+ }
24
+ const assetPath = pkg.claude?.assetPath;
25
+ if (typeof assetPath !== 'string' || assetPath.length === 0) {
26
+ if (options.skipMissingAsset) {
27
+ logger.warn(`"${name}" is missing "claude.assetPath" — skipping (the package does not ship Claude assets).`);
28
+ return null;
29
+ }
30
+ logger.error(`"${name}" is missing "claude.assetPath" in its package.json — the package does not ship Claude assets.`);
31
+ process.exit(2);
32
+ }
33
+ return {
34
+ packageRoot,
35
+ packageName: pkg.name,
36
+ packageVersion: pkg.version,
37
+ assetPath,
38
+ };
39
+ }
40
+ function resolvePackageJsonPath(name) {
41
+ const require = createRequire(import.meta.url);
42
+ try {
43
+ return require.resolve(`${name}/package.json`);
44
+ }
45
+ catch (err) {
46
+ const code = err?.code;
47
+ if (code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED')
48
+ return null;
49
+ }
50
+ let mainEntry;
51
+ try {
52
+ mainEntry = require.resolve(name);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ let dir = dirname(mainEntry);
58
+ while (dir && dir !== dirname(dir)) {
59
+ const candidate = resolve(dir, 'package.json');
60
+ if (existsSync(candidate)) {
61
+ try {
62
+ const pkg = JSON.parse(readFileSync(candidate, 'utf-8'));
63
+ if (pkg.name === name)
64
+ return candidate;
65
+ }
66
+ catch {
67
+ }
68
+ }
69
+ dir = dirname(dir);
70
+ }
71
+ return null;
72
+ }
73
+
74
+ export { resolvePackage };
@@ -0,0 +1,2 @@
1
+ import { type ResolvedMetadata } from './resolvePackage.js';
2
+ export declare function resolveScopeAlias(scope: string, rootCwd: string): Promise<ResolvedMetadata[]>;