@slats/claude-assets-sync 0.1.4 → 0.2.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.
Files changed (227) hide show
  1. package/README.md +111 -587
  2. package/bin/claude-sync.mjs +24 -0
  3. package/dist/claude-hashes.json +20 -0
  4. package/dist/commands/index.d.ts +1 -112
  5. package/dist/commands/runCli/index.d.ts +2 -0
  6. package/dist/commands/runCli/runCli.cjs +31 -0
  7. package/dist/commands/runCli/runCli.d.ts +10 -0
  8. package/dist/commands/runCli/runCli.mjs +29 -0
  9. package/dist/commands/runCli/type.d.ts +28 -0
  10. package/dist/commands/runCli/utils/injectOne.cjs +48 -0
  11. package/dist/commands/runCli/utils/injectOne.d.ts +3 -0
  12. package/dist/commands/runCli/utils/injectOne.mjs +46 -0
  13. package/dist/commands/runCli/utils/resolveScopeFlag.cjs +28 -0
  14. package/dist/commands/runCli/utils/resolveScopeFlag.d.ts +2 -0
  15. package/dist/commands/runCli/utils/resolveScopeFlag.mjs +26 -0
  16. package/dist/commands/runCli/utils/runInject.cjs +36 -0
  17. package/dist/commands/runCli/utils/runInject.d.ts +2 -0
  18. package/dist/commands/runCli/utils/runInject.mjs +34 -0
  19. package/dist/core/buildPlan/buildPlan.cjs +42 -0
  20. package/dist/core/buildPlan/buildPlan.d.ts +2 -0
  21. package/dist/core/buildPlan/buildPlan.mjs +40 -0
  22. package/dist/core/buildPlan/index.d.ts +2 -0
  23. package/dist/core/buildPlan/type.d.ts +32 -0
  24. package/dist/core/buildPlan/utils/toPosix.cjs +9 -0
  25. package/dist/core/buildPlan/utils/toPosix.d.ts +1 -0
  26. package/dist/core/buildPlan/utils/toPosix.mjs +7 -0
  27. package/dist/core/buildPlan/utils/walkFiles.cjs +25 -0
  28. package/dist/core/buildPlan/utils/walkFiles.d.ts +1 -0
  29. package/dist/core/buildPlan/utils/walkFiles.mjs +23 -0
  30. package/dist/core/hash/hash.cjs +30 -0
  31. package/dist/core/hash/hash.d.ts +4 -0
  32. package/dist/core/hash/hash.mjs +26 -0
  33. package/dist/core/hash/index.d.ts +1 -0
  34. package/dist/core/hashManifest/hashManifest.cjs +27 -0
  35. package/dist/core/hashManifest/hashManifest.d.ts +17 -0
  36. package/dist/core/hashManifest/hashManifest.mjs +23 -0
  37. package/dist/core/hashManifest/index.d.ts +1 -0
  38. package/dist/core/index.d.ts +5 -0
  39. package/dist/core/injectDocs/index.d.ts +2 -0
  40. package/dist/core/injectDocs/injectDocs.cjs +43 -0
  41. package/dist/core/injectDocs/injectDocs.d.ts +2 -0
  42. package/dist/core/injectDocs/injectDocs.mjs +41 -0
  43. package/dist/core/injectDocs/type.d.ts +30 -0
  44. package/dist/core/injectDocs/utils/applyAction.cjs +21 -0
  45. package/dist/core/injectDocs/utils/applyAction.d.ts +2 -0
  46. package/dist/core/injectDocs/utils/applyAction.mjs +19 -0
  47. package/dist/core/injectDocs/utils/emitCiForceList.cjs +10 -0
  48. package/dist/core/injectDocs/utils/emitCiForceList.d.ts +2 -0
  49. package/dist/core/injectDocs/utils/emitCiForceList.mjs +8 -0
  50. package/dist/core/injectDocs/utils/printPlan.cjs +20 -0
  51. package/dist/core/injectDocs/utils/printPlan.d.ts +2 -0
  52. package/dist/core/injectDocs/utils/printPlan.mjs +18 -0
  53. package/dist/core/injectDocs/utils/summarize.cjs +27 -0
  54. package/dist/core/injectDocs/utils/summarize.d.ts +3 -0
  55. package/dist/core/injectDocs/utils/summarize.mjs +25 -0
  56. package/dist/core/scope/index.d.ts +1 -0
  57. package/dist/core/scope/scope.cjs +46 -0
  58. package/dist/core/scope/scope.d.ts +16 -0
  59. package/dist/core/scope/scope.mjs +41 -0
  60. package/dist/core/scope/utils/isDirectory.cjs +14 -0
  61. package/dist/core/scope/utils/isDirectory.d.ts +1 -0
  62. package/dist/core/scope/utils/isDirectory.mjs +12 -0
  63. package/dist/index.cjs +15 -9
  64. package/dist/index.d.ts +3 -5
  65. package/dist/index.mjs +7 -3
  66. package/dist/prompts/confirmForce.cjs +27 -0
  67. package/dist/prompts/confirmForce.d.ts +1 -0
  68. package/dist/prompts/confirmForce.mjs +25 -0
  69. package/dist/prompts/index.d.ts +2 -0
  70. package/dist/prompts/selectScope.cjs +30 -0
  71. package/dist/prompts/selectScope.d.ts +2 -0
  72. package/dist/prompts/selectScope.mjs +28 -0
  73. package/dist/utils/heartbeat.cjs +25 -0
  74. package/dist/utils/heartbeat.d.ts +16 -0
  75. package/dist/utils/heartbeat.mjs +23 -0
  76. package/dist/utils/logger.cjs +7 -0
  77. package/dist/utils/logger.d.ts +8 -0
  78. package/dist/utils/logger.mjs +7 -0
  79. package/dist/utils/types.d.ts +1 -252
  80. package/dist/utils/version.cjs +2 -14
  81. package/dist/utils/version.d.ts +3 -53
  82. package/dist/utils/version.mjs +2 -13
  83. package/docs/bundle-size-decision.md +36 -0
  84. package/docs/claude/skills/claude-sync-applier/SKILL.md +195 -0
  85. package/docs/claude/skills/claude-sync-applier/knowledge/claude-md-template.md +77 -0
  86. package/docs/claude/skills/claude-sync-applier/knowledge/dependency-cruiser.md +126 -0
  87. package/docs/claude/skills/claude-sync-applier/knowledge/gotchas.md +139 -0
  88. package/docs/claude/skills/claude-sync-applier/knowledge/package-json-patches.md +130 -0
  89. package/docs/claude/skills/claude-sync-applier/knowledge/reference-files.md +120 -0
  90. package/docs/claude/skills/claude-sync-applier/knowledge/smoke-tests.md +102 -0
  91. package/docs/consumer-integration.md +153 -0
  92. package/package.json +24 -16
  93. package/scripts/build-hashes.mjs +30 -0
  94. package/scripts/buildHashes.d.mts +15 -0
  95. package/scripts/buildHashes.mjs +82 -0
  96. package/scripts/claude-build-hashes.mjs +42 -0
  97. package/scripts/inject-version.js +112 -0
  98. package/dist/cli.cjs +0 -8
  99. package/dist/cli.d.ts +0 -1
  100. package/dist/cli.mjs +0 -7
  101. package/dist/commands/add.cjs +0 -80
  102. package/dist/commands/add.d.ts +0 -8
  103. package/dist/commands/add.mjs +0 -78
  104. package/dist/commands/list.cjs +0 -94
  105. package/dist/commands/list.d.ts +0 -15
  106. package/dist/commands/list.mjs +0 -91
  107. package/dist/commands/migrate.cjs +0 -9
  108. package/dist/commands/migrate.d.ts +0 -6
  109. package/dist/commands/migrate.mjs +0 -7
  110. package/dist/commands/remove.cjs +0 -127
  111. package/dist/commands/remove.d.ts +0 -6
  112. package/dist/commands/remove.mjs +0 -105
  113. package/dist/commands/status.cjs +0 -193
  114. package/dist/commands/status.d.ts +0 -6
  115. package/dist/commands/status.mjs +0 -171
  116. package/dist/commands/sync.cjs +0 -28
  117. package/dist/commands/sync.d.ts +0 -6
  118. package/dist/commands/sync.mjs +0 -26
  119. package/dist/commands/types.d.ts +0 -89
  120. package/dist/commands/update.cjs +0 -209
  121. package/dist/commands/update.d.ts +0 -29
  122. package/dist/commands/update.mjs +0 -206
  123. package/dist/components/add/AddCommand.cjs +0 -103
  124. package/dist/components/add/AddCommand.d.ts +0 -14
  125. package/dist/components/add/AddCommand.mjs +0 -101
  126. package/dist/components/add/BulkAddView.cjs +0 -165
  127. package/dist/components/add/BulkAddView.d.ts +0 -11
  128. package/dist/components/add/BulkAddView.mjs +0 -163
  129. package/dist/components/add/index.d.ts +0 -2
  130. package/dist/components/index.d.ts +0 -2
  131. package/dist/components/list/EditableTreeItem.d.ts +0 -13
  132. package/dist/components/list/ListCommand.cjs +0 -651
  133. package/dist/components/list/ListCommand.d.ts +0 -5
  134. package/dist/components/list/ListCommand.mjs +0 -649
  135. package/dist/components/list/SyncedPackageTree.d.ts +0 -14
  136. package/dist/components/list/index.d.ts +0 -10
  137. package/dist/components/list/types.d.ts +0 -14
  138. package/dist/components/primitives/Box.d.ts +0 -4
  139. package/dist/components/primitives/Spinner.d.ts +0 -6
  140. package/dist/components/primitives/Text.d.ts +0 -4
  141. package/dist/components/primitives/index.d.ts +0 -3
  142. package/dist/components/remove/RemoveConfirm.cjs +0 -18
  143. package/dist/components/remove/RemoveConfirm.d.ts +0 -11
  144. package/dist/components/remove/RemoveConfirm.mjs +0 -16
  145. package/dist/components/shared/Confirm.cjs +0 -30
  146. package/dist/components/shared/Confirm.d.ts +0 -8
  147. package/dist/components/shared/Confirm.mjs +0 -28
  148. package/dist/components/shared/MenuItem.cjs +0 -18
  149. package/dist/components/shared/MenuItem.d.ts +0 -7
  150. package/dist/components/shared/MenuItem.mjs +0 -16
  151. package/dist/components/shared/ProgressBar.d.ts +0 -7
  152. package/dist/components/shared/StepRunner.cjs +0 -58
  153. package/dist/components/shared/StepRunner.d.ts +0 -15
  154. package/dist/components/shared/StepRunner.mjs +0 -56
  155. package/dist/components/shared/Table.cjs +0 -19
  156. package/dist/components/shared/Table.d.ts +0 -8
  157. package/dist/components/shared/Table.mjs +0 -17
  158. package/dist/components/shared/index.d.ts +0 -6
  159. package/dist/components/status/PackageStatusCard.d.ts +0 -10
  160. package/dist/components/status/StatusDisplay.cjs +0 -26
  161. package/dist/components/status/StatusDisplay.d.ts +0 -23
  162. package/dist/components/status/StatusDisplay.mjs +0 -24
  163. package/dist/components/status/StatusTreeNode.cjs +0 -40
  164. package/dist/components/status/StatusTreeNode.d.ts +0 -15
  165. package/dist/components/status/StatusTreeNode.mjs +0 -38
  166. package/dist/components/status/index.d.ts +0 -6
  167. package/dist/components/tree/AssetTreeNode.cjs +0 -54
  168. package/dist/components/tree/AssetTreeNode.d.ts +0 -12
  169. package/dist/components/tree/AssetTreeNode.mjs +0 -52
  170. package/dist/components/tree/TreeSelect.cjs +0 -129
  171. package/dist/components/tree/TreeSelect.d.ts +0 -12
  172. package/dist/components/tree/TreeSelect.mjs +0 -127
  173. package/dist/components/tree/index.d.ts +0 -4
  174. package/dist/core/assetStructure.cjs +0 -30
  175. package/dist/core/assetStructure.d.ts +0 -36
  176. package/dist/core/assetStructure.mjs +0 -27
  177. package/dist/core/cli.cjs +0 -106
  178. package/dist/core/cli.d.ts +0 -9
  179. package/dist/core/cli.mjs +0 -103
  180. package/dist/core/constants.cjs +0 -28
  181. package/dist/core/constants.d.ts +0 -94
  182. package/dist/core/constants.mjs +0 -21
  183. package/dist/core/filesystem.cjs +0 -98
  184. package/dist/core/filesystem.d.ts +0 -94
  185. package/dist/core/filesystem.mjs +0 -88
  186. package/dist/core/github.cjs +0 -115
  187. package/dist/core/github.d.ts +0 -61
  188. package/dist/core/github.mjs +0 -107
  189. package/dist/core/io.cjs +0 -46
  190. package/dist/core/io.d.ts +0 -40
  191. package/dist/core/io.mjs +0 -39
  192. package/dist/core/listOperations.cjs +0 -228
  193. package/dist/core/listOperations.d.ts +0 -43
  194. package/dist/core/listOperations.mjs +0 -205
  195. package/dist/core/localSource.cjs +0 -126
  196. package/dist/core/localSource.d.ts +0 -33
  197. package/dist/core/localSource.mjs +0 -120
  198. package/dist/core/migration.cjs +0 -201
  199. package/dist/core/migration.d.ts +0 -57
  200. package/dist/core/migration.mjs +0 -198
  201. package/dist/core/packageScanner.cjs +0 -360
  202. package/dist/core/packageScanner.d.ts +0 -22
  203. package/dist/core/packageScanner.mjs +0 -356
  204. package/dist/core/sync.cjs +0 -400
  205. package/dist/core/sync.d.ts +0 -21
  206. package/dist/core/sync.mjs +0 -397
  207. package/dist/core/syncMeta.cjs +0 -242
  208. package/dist/core/syncMeta.d.ts +0 -75
  209. package/dist/core/syncMeta.mjs +0 -229
  210. package/dist/utils/dependencies.cjs +0 -57
  211. package/dist/utils/dependencies.d.ts +0 -10
  212. package/dist/utils/dependencies.mjs +0 -34
  213. package/dist/utils/nameTransform.cjs +0 -13
  214. package/dist/utils/nameTransform.d.ts +0 -65
  215. package/dist/utils/nameTransform.mjs +0 -11
  216. package/dist/utils/package.cjs +0 -170
  217. package/dist/utils/package.d.ts +0 -105
  218. package/dist/utils/package.mjs +0 -157
  219. package/dist/utils/packageName.cjs +0 -24
  220. package/dist/utils/packageName.d.ts +0 -32
  221. package/dist/utils/packageName.mjs +0 -21
  222. package/dist/utils/paths.cjs +0 -18
  223. package/dist/utils/paths.d.ts +0 -55
  224. package/dist/utils/paths.mjs +0 -15
  225. package/dist/version.cjs +0 -5
  226. package/dist/version.d.ts +0 -5
  227. package/dist/version.mjs +0 -3
@@ -0,0 +1,120 @@
1
+ # Reference Files
2
+
3
+ The two stub files are **identical across all consumers**. Copy verbatim, do
4
+ no substitution — the stubs discover package metadata at runtime via
5
+ `import.meta.url`.
6
+
7
+ Reference consumer: `packages/canard/schema-form`.
8
+
9
+ ---
10
+
11
+ ## `bin/claude-sync.mjs`
12
+
13
+ Source of truth: `packages/canard/schema-form/bin/claude-sync.mjs`.
14
+
15
+ Expected content (must match exactly):
16
+
17
+ ```js
18
+ #!/usr/bin/env node
19
+ import { runCli } from '@slats/claude-assets-sync';
20
+ import { readFile } from 'node:fs/promises';
21
+ import { dirname, resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
25
+ const pkg = JSON.parse(
26
+ await readFile(resolve(packageRoot, 'package.json'), 'utf-8'),
27
+ );
28
+
29
+ if (typeof pkg.claude?.assetPath === 'string') {
30
+ runCli(process.argv, {
31
+ packageRoot,
32
+ packageName: pkg.name,
33
+ packageVersion: pkg.version,
34
+ assetPath: pkg.claude.assetPath,
35
+ }).catch((err) => {
36
+ process.stderr.write(
37
+ `[${pkg.name}] claude-sync failed: ${err instanceof Error ? err.message : String(err)}\n`,
38
+ );
39
+ process.exit(1);
40
+ });
41
+ }
42
+ ```
43
+
44
+ **Notes**
45
+
46
+ - `import.meta.url` is resolved at runtime to the installed file path, so
47
+ `packageRoot` works both during local development and when consumed as a
48
+ dependency.
49
+ - `pkg.claude?.assetPath` missing → the stub is a silent no-op. That is the
50
+ feature that lets a package opt out without shipping broken bins.
51
+ - Emit to stderr on failure and exit 1. Do not `throw` — the CLI is the
52
+ process boundary.
53
+
54
+ After writing, make it executable:
55
+
56
+ ```bash
57
+ chmod +x ${TARGET_PATH}/bin/claude-sync.mjs
58
+ ```
59
+
60
+ ---
61
+
62
+ ## `scripts/build-hashes.mjs`
63
+
64
+ Source of truth: `packages/canard/schema-form/scripts/build-hashes.mjs`.
65
+
66
+ Expected content (must match exactly):
67
+
68
+ ```js
69
+ #!/usr/bin/env node
70
+ // Thin wrapper — parses this package's package.json and delegates to
71
+ // @slats/claude-assets-sync/buildHashes. The `claude.assetPath` convention
72
+ // lives here, in the consumer; the library is generic.
73
+ import { buildHashes } from '@slats/claude-assets-sync/buildHashes';
74
+ import { readFile } from 'node:fs/promises';
75
+ import { resolve } from 'node:path';
76
+
77
+ const packageRoot = process.cwd();
78
+ const pkg = JSON.parse(
79
+ await readFile(resolve(packageRoot, 'package.json'), 'utf-8'),
80
+ );
81
+
82
+ if (typeof pkg.claude?.assetPath === 'string') {
83
+ try {
84
+ const { outPath, fileCount } = await buildHashes({
85
+ packageRoot,
86
+ packageName: pkg.name,
87
+ packageVersion: pkg.version,
88
+ assetPath: pkg.claude.assetPath,
89
+ });
90
+ console.log(
91
+ `✓ claude-hashes.json written: ${fileCount} file(s) → ${outPath}`,
92
+ );
93
+ } catch (err) {
94
+ console.error('❌ build-hashes failed:', err?.message ?? err);
95
+ process.exit(1);
96
+ }
97
+ }
98
+ ```
99
+
100
+ **Notes**
101
+
102
+ - `packageRoot = process.cwd()` intentionally — this script runs via
103
+ `yarn <shortcut> build:hashes` from the package directory.
104
+ - Same no-op behavior when `claude.assetPath` is missing.
105
+ - Emits to stdout on success (humans read it during the build).
106
+
107
+ No `chmod +x` needed — it's invoked via `node scripts/build-hashes.mjs`.
108
+
109
+ ---
110
+
111
+ ## Verification
112
+
113
+ Before declaring Step 1/Step 2 complete, diff against the reference:
114
+
115
+ ```bash
116
+ diff packages/canard/schema-form/bin/claude-sync.mjs ${TARGET_PATH}/bin/claude-sync.mjs
117
+ diff packages/canard/schema-form/scripts/build-hashes.mjs ${TARGET_PATH}/scripts/build-hashes.mjs
118
+ ```
119
+
120
+ Both diffs must be empty.
@@ -0,0 +1,102 @@
1
+ # E2E Smoke Tests — 6-path matrix
2
+
3
+ **Run from `/tmp/...` — never from the monorepo root or `${TARGET_PATH}/`.**
4
+
5
+ `--scope=project` walks `cwd` upward looking for an existing `.claude`
6
+ directory. Running from the monorepo would reuse or mutate the real repo's
7
+ `.claude`, which is a destructive error.
8
+
9
+ No fake `node_modules` needed — the bin resolves everything via
10
+ `import.meta.url`.
11
+
12
+ ---
13
+
14
+ ## Setup
15
+
16
+ ```bash
17
+ BIN=$PWD/${TARGET_PATH}/bin/claude-sync.mjs
18
+ DIR=/tmp/claude-sync-smoke-${SHORTCUT:-target}
19
+ [ -d "$DIR" ] && find "$DIR" -mindepth 1 -delete
20
+ mkdir -p "$DIR" && cd "$DIR"
21
+ ```
22
+
23
+ `[ -d ... ] && find -delete` keeps the setup idempotent. **Never** use
24
+ `rm -rf` or unquoted `*` globs — too easy to nuke the wrong directory.
25
+
26
+ ---
27
+
28
+ ## Matrix
29
+
30
+ Execute sequentially. `EXIT=$?` after each so the value is captured before the
31
+ next command overwrites `$?`.
32
+
33
+ | # | Command | Expected exit | Purpose |
34
+ |---|-----------------------------------------------------------------------------|---------------|----------------------------------------------------------|
35
+ | 1 | `node "$BIN" --scope=project --dry-run` | 0 | Dry run on empty dir — previews actions, no writes. |
36
+ | 2 | `node "$BIN" --scope=project` | 0 | First real install — writes `.claude/` under `$DIR`. |
37
+ | 3 | `node "$BIN" --scope=project` | 0 | Re-run — no-op because everything is already up-to-date. |
38
+ | 4 | (after tampering) `CI=true node "$BIN" --scope=project` | **2** | CI + tampered content → refuse to overwrite. |
39
+ | 5 | `CI=true node "$BIN" --scope=project --force` | 0 | `--force` overrides the refusal. |
40
+ | 6 | `CI=true node "$BIN"` | **2** | Missing `--scope` in non-TTY context → exit 2. |
41
+
42
+ ### Tamper step (between path 3 and path 4)
43
+
44
+ ```bash
45
+ find .claude -name SKILL.md -exec sh -c 'echo tampered >> "$1"' _ {} \;
46
+ ```
47
+
48
+ Appends `tampered` to every `SKILL.md` under the local `.claude/`. This
49
+ simulates a human edit that the CI-mode bin must detect and refuse to clobber.
50
+
51
+ ---
52
+
53
+ ## Execution Shape
54
+
55
+ Split into **two bash calls** because `cwd` resets between Bash tool
56
+ invocations.
57
+
58
+ **First call** — paths 1–3:
59
+
60
+ ```bash
61
+ BIN=$PWD/${TARGET_PATH}/bin/claude-sync.mjs
62
+ DIR=/tmp/claude-sync-smoke-${SHORTCUT:-target}
63
+ [ -d "$DIR" ] && find "$DIR" -mindepth 1 -delete
64
+ mkdir -p "$DIR" && cd "$DIR"
65
+
66
+ node "$BIN" --scope=project --dry-run; echo "EXIT=$?" # expect 0
67
+ node "$BIN" --scope=project; echo "EXIT=$?" # expect 0
68
+ node "$BIN" --scope=project; echo "EXIT=$?" # expect 0
69
+ ```
70
+
71
+ **Second call** — paths 4–6 (re-enter `$DIR`, reuse state from first call):
72
+
73
+ ```bash
74
+ DIR=/tmp/claude-sync-smoke-${SHORTCUT:-target}
75
+ BIN=$PWD/${TARGET_PATH}/bin/claude-sync.mjs
76
+ cd "$DIR"
77
+
78
+ find .claude -name SKILL.md -exec sh -c 'echo tampered >> "$1"' _ {} \;
79
+ CI=true node "$BIN" --scope=project; echo "EXIT=$?" # expect 2
80
+ CI=true node "$BIN" --scope=project --force; echo "EXIT=$?" # expect 0
81
+ CI=true node "$BIN"; echo "EXIT=$?" # expect 2
82
+ ```
83
+
84
+ Note: `$PWD` in the second call is the parent shell's cwd (monorepo root), so
85
+ `BIN` resolves correctly. `cd "$DIR"` then moves into the smoke directory
86
+ before invoking.
87
+
88
+ ---
89
+
90
+ ## Failure Handling
91
+
92
+ | Observed | Meaning | Action |
93
+ |----------|-------------------------------------------------------------------------|--------------------------------------------------------------------|
94
+ | 1 ≠ 0 | Dry-run crashed. Likely a stub or engine bug. | Stop, capture stderr, report. |
95
+ | 2 ≠ 0 | First write failed. Likely permissions, engine bug, or manifest issue. | Stop, inspect `dist/claude-hashes.json`, report. |
96
+ | 3 ≠ 0 | Idempotency broken — re-run should be no-op. | Stop, diff `$DIR/.claude` before/after, report. |
97
+ | 4 = 0 | CI mode did not refuse tampered files. Safety regression. | Stop — the engine's CI gate is broken. |
98
+ | 5 ≠ 0 | `--force` failed to override. Check engine. | Stop, report. |
99
+ | 6 = 0 | Engine defaulted a scope in non-TTY context. Should require `--scope`. | Stop, report. |
100
+
101
+ Do not attempt to "make the tests pass" by altering expectations. The matrix
102
+ encodes invariants of the engine — a mismatch is a real regression upstream.
@@ -0,0 +1,153 @@
1
+ # Consumer Integration Template
2
+
3
+ How to make a package "claude-sync aware" so `claude-sync` discovers and injects its `docs/claude/` tree into end-user `.claude` directories.
4
+
5
+ ## 1. `package.json` additions
6
+
7
+ ```jsonc
8
+ {
9
+ "name": "@your-scope/your-package",
10
+ "version": "…",
11
+ "bin": {
12
+ "claude-sync": "./bin/claude-sync.mjs"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "docs",
17
+ "dist/claude-hashes.json",
18
+ "bin",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "… && yarn build:hashes",
23
+ "build:hashes": "node scripts/build-hashes.mjs"
24
+ },
25
+ "dependencies": {
26
+ "@slats/claude-assets-sync": "workspace:^",
27
+ },
28
+ "claude": {
29
+ "assetPath": "docs/claude"
30
+ }
31
+ }
32
+ ```
33
+
34
+ Do **not** expose `./bin/*` in `exports`. That would let consumer bundlers accidentally pull CLI code into app bundles.
35
+
36
+ ## 2. `bin/claude-sync.mjs` (3-line re-export stub)
37
+
38
+ ```javascript
39
+ #!/usr/bin/env node
40
+ import { runCli } from '@slats/claude-assets-sync';
41
+
42
+ runCli(process.argv, { invokedFromBin: import.meta.url }).catch((err) => {
43
+ process.stderr.write(
44
+ `[@your-scope/your-package] claude-sync failed: ${err instanceof Error ? err.message : String(err)}\n`,
45
+ );
46
+ process.exit(1);
47
+ });
48
+ ```
49
+
50
+ `runCli` determines the implicit `--package` target in this priority order:
51
+
52
+ 1. `--all` or `--package=<name>` (explicit)
53
+ 2. The consumer that owns `process.cwd()` (i.e. the terminal you launched from). When you `cd` into a consumer's package directory and run `yarn claude-sync`, that consumer is picked automatically.
54
+ 3. The consumer that owns `invokedFromBin` (fallback). This keeps bare `npx <pkg> claude-sync` working from arbitrary cwds — including from inside another consumer's directory where option 2 would have picked a different package.
55
+ 4. The sole discovered consumer, if exactly one exists.
56
+ 5. Otherwise an error asking for `--package=<name>` or `--all`.
57
+
58
+ Passing `invokedFromBin: import.meta.url` remains the mechanism for the fallback case. Omit it in slats's own global bin so it behaves as a cross-consumer dispatcher.
59
+
60
+ Remember `chmod +x bin/claude-sync.mjs` (or rely on `files` entry to ship executable bit via npm).
61
+
62
+ ## 3. `scripts/build-hashes.mjs` (one line import)
63
+
64
+ ```javascript
65
+ #!/usr/bin/env node
66
+ import { buildHashes } from '@slats/claude-assets-sync/buildHashes';
67
+
68
+ try {
69
+ const { outPath, fileCount } = await buildHashes();
70
+ console.log(`✓ claude-hashes.json written: ${fileCount} file(s) → ${outPath}`);
71
+ } catch (err) {
72
+ console.error('❌ buildHashes failed:', err?.message ?? err);
73
+ process.exit(1);
74
+ }
75
+ ```
76
+
77
+ This reads the current `package.json` + its `claude.assetPath`, hashes every file under the asset root (ignoring `.omc/**`, `*.log`, `.DS_Store`), and writes `dist/claude-hashes.json`.
78
+
79
+ ## 4. Isolation guardrails (optional but recommended)
80
+
81
+ In a `.dependency-cruiser.cjs`:
82
+
83
+ ```javascript
84
+ {
85
+ name: 'src-no-bin',
86
+ severity: 'error',
87
+ from: { path: '^src/' },
88
+ to: { path: '^bin/' },
89
+ },
90
+ {
91
+ name: 'src-no-docs',
92
+ severity: 'error',
93
+ from: { path: '^src/' },
94
+ to: { path: '^docs/' },
95
+ },
96
+ {
97
+ name: 'src-no-claude-assets-sync',
98
+ severity: 'error',
99
+ from: { path: '^src/' },
100
+ to: { path: 'node_modules/@slats/claude-assets-sync' },
101
+ },
102
+ ```
103
+
104
+ Plus `"sideEffects": false` in `package.json`. The guardrails ensure the CLI engine never leaks into consumer runtime bundles.
105
+
106
+ ## 5. End-user invocations
107
+
108
+ | Install topology | Working invocations |
109
+ |---|---|
110
+ | Consumer is a **direct dep** of the user's project | `npx claude-sync --scope=user` *(bare)* |
111
+ | Always works (preferred in docs) | `npx -p @your-scope/your-package claude-sync --scope=user` |
112
+ | Consumer is a **transitive dep** | `npx -p @your-scope/your-package claude-sync --scope=user` |
113
+ | User has no consumer installed | `npx @slats/claude-assets-sync --package=@your-scope/your-package --scope=user` |
114
+ | Multiple consumers discovered | `npx claude-sync --package=@your-scope/your-package` *or* `npx claude-sync --all` |
115
+
116
+ ### Scope resolution (project)
117
+
118
+ For `--scope=project`, the target `.claude` directory is resolved by walking up from `process.cwd()` and reusing the first existing `.claude` directory found. Only if no ancestor owns a `.claude` does the CLI fall back to `process.cwd()/.claude`.
119
+
120
+ ```
121
+ workspace/
122
+ .claude/ ← reused target (auto-located)
123
+ packages/
124
+ @your-scope/your-package/ ← cd here and run claude-sync
125
+ bin/claude-sync.mjs
126
+ ```
127
+
128
+ Running `yarn claude-sync --scope=project` from `packages/@your-scope/your-package/` injects into `workspace/.claude`, not `packages/@your-scope/your-package/.claude`. The CLI logs `(auto-located)` in its resolution line when this happens.
129
+
130
+ ## 6. Authoring `docs/claude/`
131
+
132
+ Any file tree works, but the recommended layout is:
133
+
134
+ ```
135
+ docs/claude/
136
+ ├── skills/
137
+ │ └── <skill-name>/
138
+ │ ├── SKILL.md
139
+ │ └── knowledge/...
140
+ ├── rules/...
141
+ └── commands/...
142
+ ```
143
+
144
+ The hash manifest tracks every file under `docs/claude/` relative to the asset root. On inject, the CLI copies skills/rules/commands into the matching subtree under `.claude/`.
145
+
146
+ ## 7. Verification checklist
147
+
148
+ - [ ] `yarn build` succeeds and emits `dist/claude-hashes.json` alongside the rest of `dist/`.
149
+ - [ ] `node bin/claude-sync.mjs --help` prints the `claude-sync` subcommand tree.
150
+ - [ ] `node bin/claude-sync.mjs list --json` emits an entry for your package with `hashesPresent: true`.
151
+ - [ ] `node bin/claude-sync.mjs --scope=project --dry-run --package=@your-scope/your-package` emits a copy plan.
152
+ - [ ] `yarn depcheck` (or whatever your dep-cruiser invocation is named) reports zero new violations.
153
+ - [ ] Consumer bundler tree-shakes away CLI code (verify by greping the built bundle: should contain zero references to `@slats/claude-assets-sync`).
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@slats/claude-assets-sync",
3
- "version": "0.1.4",
4
- "description": "CLI tool to sync Claude commands and skills from npm packages to your project's .claude directory",
3
+ "version": "0.2.0",
4
+ "description": "Shared CLI engine that lets consumer packages inject their Claude docs (skills, rules, commands) into a user's .claude directory via a thin bin/inject-docs wrapper.",
5
5
  "keywords": [
6
6
  "claude",
7
7
  "claude-code",
8
8
  "cli",
9
- "sync",
10
- "commands",
9
+ "inject",
10
+ "docs",
11
11
  "skills",
12
12
  "assets"
13
13
  ],
@@ -29,23 +29,34 @@
29
29
  "source": "./src/index.ts",
30
30
  "import": "./dist/index.mjs",
31
31
  "require": "./dist/index.cjs"
32
+ },
33
+ "./buildHashes": {
34
+ "import": "./scripts/buildHashes.mjs"
32
35
  }
33
36
  },
34
37
  "main": "dist/index.cjs",
35
38
  "module": "dist/index.mjs",
36
39
  "types": "dist/index.d.ts",
37
- "bin": "./dist/cli.mjs",
40
+ "bin": {
41
+ "claude-build-hashes": "./scripts/claude-build-hashes.mjs",
42
+ "claude-sync": "./bin/claude-sync.mjs"
43
+ },
38
44
  "files": [
39
45
  "dist",
46
+ "docs",
47
+ "bin",
48
+ "scripts",
40
49
  "README.md"
41
50
  ],
42
51
  "scripts": {
43
- "build": "node scripts/inject-version.js && rollup -c && yarn build:types",
52
+ "build": "node scripts/inject-version.js && rollup -c && yarn build:types && yarn build:hashes",
53
+ "build:hashes": "node scripts/build-hashes.mjs",
44
54
  "build:publish:npm": "yarn build && yarn publish:npm",
45
55
  "build:types": "node ../../aileron/script/build/buildTypes.mjs",
46
- "dev": "node scripts/inject-version.js && tsx src/cli.ts",
56
+ "dev": "node scripts/inject-version.js && tsx src/main.ts",
47
57
  "format": "prettier --write \"src/**/*.ts\"",
48
58
  "lint": "eslint \"src/**/*.ts\"",
59
+ "prepublishOnly": "yarn build",
49
60
  "publish:npm": "yarn npm publish --access public",
50
61
  "test": "vitest",
51
62
  "version:major": "yarn version major",
@@ -53,17 +64,14 @@
53
64
  "version:patch": "yarn version patch"
54
65
  },
55
66
  "dependencies": {
56
- "@winglet/react-utils": "^0.11.2",
67
+ "@inquirer/prompts": "^8.4.2",
57
68
  "commander": "^12.1.0",
58
- "ink": "^6.6.0",
59
- "ink-select-input": "^6.2.0",
60
- "ink-spinner": "^5.0.0",
61
- "picocolors": "^1.1.1",
62
- "react": "^19.1.1",
63
- "react-dom": "^19.1.1"
69
+ "picocolors": "^1.1.1"
64
70
  },
65
71
  "devDependencies": {
66
- "@types/react": "^19.0.0",
67
- "ink-testing-library": "^4.0.0"
72
+ "@inquirer/testing": "^3.3.5"
73
+ },
74
+ "claude": {
75
+ "assetPath": "docs/claude"
68
76
  }
69
77
  }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ // Thin wrapper — parses this package's package.json and delegates to
3
+ // @slats/claude-assets-sync/buildHashes. The `claude.assetPath` convention
4
+ // lives here, in the consumer; the library is generic.
5
+ import { buildHashes } from '@slats/claude-assets-sync/buildHashes';
6
+ import { readFile } from 'node:fs/promises';
7
+ import { dirname, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
11
+ const pkg = JSON.parse(
12
+ await readFile(resolve(packageRoot, 'package.json'), 'utf-8'),
13
+ );
14
+
15
+ if (typeof pkg.claude?.assetPath === 'string') {
16
+ try {
17
+ const { outPath, fileCount } = await buildHashes({
18
+ packageRoot,
19
+ packageName: pkg.name,
20
+ packageVersion: pkg.version,
21
+ assetPath: pkg.claude.assetPath,
22
+ });
23
+ console.log(
24
+ `✓ claude-hashes.json written: ${fileCount} file(s) → ${outPath}`,
25
+ );
26
+ } catch (err) {
27
+ console.error('❌ build-hashes failed:', err?.message ?? err);
28
+ process.exit(1);
29
+ }
30
+ }
@@ -0,0 +1,15 @@
1
+ export interface BuildHashesOptions {
2
+ packageRoot?: string;
3
+ packageName?: string;
4
+ packageVersion?: string;
5
+ assetPathRel?: string;
6
+ }
7
+
8
+ export interface BuildHashesResult {
9
+ outPath: string;
10
+ fileCount: number;
11
+ }
12
+
13
+ export function buildHashes(
14
+ opts?: BuildHashesOptions,
15
+ ): Promise<BuildHashesResult>;
@@ -0,0 +1,82 @@
1
+ // Library + importable implementation for the hash manifest builder.
2
+ //
3
+ // Self-executing CLI behavior lives in `./claude-build-hashes.mjs` so this
4
+ // file stays free of top-level await and can be bundled into CJS/ESM outputs
5
+ // via Rollup without format errors.
6
+ //
7
+ // Exported for:
8
+ // - Consumer packages: `import { buildHashes } from '@slats/claude-assets-sync/buildHashes'`
9
+ // - Standalone bin: `./claude-build-hashes.mjs`
10
+ //
11
+ // The caller owns all package metadata. This function does not read
12
+ // package.json — consumers parse their own manifest and pass a ready-made
13
+ // set of values so the library stays free of field-shape assumptions.
14
+
15
+ import { createHash } from 'node:crypto';
16
+ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
17
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
18
+
19
+ const MANIFEST_FILENAME = 'claude-hashes.json';
20
+ const NOISE = [/(^|\/)\.omc(\/|$)/, /(^|\/)\.DS_Store$/, /\.log$/];
21
+
22
+ const toPosix = (p) => (sep === '/' ? p : p.split(sep).join('/'));
23
+
24
+ async function* walk(root) {
25
+ let entries;
26
+ try {
27
+ entries = await readdir(root, { withFileTypes: true });
28
+ } catch (err) {
29
+ if (err.code === 'ENOENT') return;
30
+ throw err;
31
+ }
32
+ for (const entry of entries) {
33
+ const abs = join(root, entry.name);
34
+ if (entry.isDirectory()) yield* walk(abs);
35
+ else if (entry.isFile()) yield abs;
36
+ }
37
+ }
38
+
39
+ export async function buildHashes(opts) {
40
+ if (
41
+ !opts ||
42
+ typeof opts.packageRoot !== 'string' ||
43
+ typeof opts.packageName !== 'string' ||
44
+ typeof opts.packageVersion !== 'string' ||
45
+ typeof opts.assetPath !== 'string'
46
+ ) {
47
+ throw new Error(
48
+ 'buildHashes requires { packageRoot, packageName, packageVersion, assetPath }.',
49
+ );
50
+ }
51
+ const { packageRoot, packageName, packageVersion, assetPath } = opts;
52
+ if (!isAbsolute(packageRoot)) {
53
+ throw new Error(
54
+ `packageRoot must be an absolute path; received: ${packageRoot}`,
55
+ );
56
+ }
57
+ const assetRoot = resolve(packageRoot, assetPath);
58
+ const files = {};
59
+ for await (const abs of walk(assetRoot)) {
60
+ const rel = toPosix(relative(assetRoot, abs));
61
+ if (NOISE.some((re) => re.test(rel))) continue;
62
+ const buf = await readFile(abs);
63
+ files[rel] = createHash('sha256').update(buf).digest('hex');
64
+ }
65
+ const sorted = Object.fromEntries(
66
+ Object.entries(files).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
67
+ );
68
+ const manifest = {
69
+ schemaVersion: 1,
70
+ package: { name: packageName, version: packageVersion },
71
+ generatedAt: new Date().toISOString(),
72
+ algorithm: 'sha256',
73
+ assetRoot: assetPath,
74
+ files: sorted,
75
+ previousVersions: {},
76
+ };
77
+ const distDir = resolve(packageRoot, 'dist');
78
+ await mkdir(distDir, { recursive: true });
79
+ const outPath = join(distDir, MANIFEST_FILENAME);
80
+ await writeFile(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
81
+ return { outPath, fileCount: Object.keys(sorted).length };
82
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ // Standalone CLI for `claude-build-hashes`.
3
+ //
4
+ // Reads the consumer's package.json at process.cwd() to extract the asset
5
+ // path, then delegates to `buildHashes`. Consumers who want a different
6
+ // manifest layout can skip this bin and ship a one-line
7
+ // `scripts/build-hashes.mjs` that calls `buildHashes` directly with their
8
+ // own parsed values.
9
+ //
10
+ // Convention: `pkg.claude?.assetPath` with a fallback of `'claude'`.
11
+ // This convention is purely consumer-side — the library itself enforces
12
+ // nothing about package.json shape.
13
+ import { readFile } from 'node:fs/promises';
14
+ import { resolve } from 'node:path';
15
+
16
+ import { buildHashes } from './buildHashes.mjs';
17
+
18
+ try {
19
+ const packageRoot = process.cwd();
20
+ const pkg = JSON.parse(
21
+ await readFile(resolve(packageRoot, 'package.json'), 'utf-8'),
22
+ );
23
+ if (typeof pkg.name !== 'string' || typeof pkg.version !== 'string') {
24
+ throw new Error(
25
+ `${packageRoot}/package.json must define "name" and "version".`,
26
+ );
27
+ }
28
+ const assetPath =
29
+ typeof pkg.claude?.assetPath === 'string' && pkg.claude.assetPath.length > 0
30
+ ? pkg.claude.assetPath
31
+ : 'claude';
32
+ const { outPath, fileCount } = await buildHashes({
33
+ packageRoot,
34
+ packageName: pkg.name,
35
+ packageVersion: pkg.version,
36
+ assetPath,
37
+ });
38
+ console.log(`✓ claude-hashes.json written: ${fileCount} file(s) → ${outPath}`);
39
+ } catch (err) {
40
+ console.error('❌ buildHashes failed:', err?.message ?? err);
41
+ process.exit(1);
42
+ }