@slats/claude-assets-sync 0.2.0 → 0.3.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 (50) hide show
  1. package/README.md +46 -62
  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.cjs +27 -5
  7. package/dist/commands/runCli/runCli.d.ts +10 -6
  8. package/dist/commands/runCli/runCli.mjs +27 -5
  9. package/dist/commands/runCli/type.d.ts +3 -12
  10. package/dist/commands/runCli/utils/classifyTarget.cjs +48 -0
  11. package/dist/commands/runCli/utils/classifyTarget.d.ts +19 -0
  12. package/dist/commands/runCli/utils/classifyTarget.mjs +46 -0
  13. package/dist/commands/runCli/utils/injectOne.cjs +2 -3
  14. package/dist/commands/runCli/utils/injectOne.d.ts +1 -1
  15. package/dist/commands/runCli/utils/injectOne.mjs +2 -3
  16. package/dist/commands/runCli/utils/resolvePackage.cjs +77 -0
  17. package/dist/commands/runCli/utils/resolvePackage.d.ts +16 -0
  18. package/dist/commands/runCli/utils/resolvePackage.mjs +74 -0
  19. package/dist/commands/runCli/utils/resolveScopeAlias.cjs +69 -0
  20. package/dist/commands/runCli/utils/resolveScopeAlias.d.ts +2 -0
  21. package/dist/commands/runCli/utils/resolveScopeAlias.mjs +67 -0
  22. package/dist/commands/runCli/utils/resolveTargets.cjs +40 -0
  23. package/dist/commands/runCli/utils/resolveTargets.d.ts +15 -0
  24. package/dist/commands/runCli/utils/resolveTargets.mjs +38 -0
  25. package/dist/commands/runCli/utils/runInject.cjs +38 -22
  26. package/dist/commands/runCli/utils/runInject.d.ts +3 -2
  27. package/dist/commands/runCli/utils/runInject.mjs +38 -22
  28. package/dist/core/injectDocs/utils/applyAction.cjs +1 -1
  29. package/dist/core/injectDocs/utils/applyAction.mjs +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/utils/version.cjs +1 -1
  32. package/dist/utils/version.d.ts +1 -1
  33. package/dist/utils/version.mjs +1 -1
  34. package/docs/claude/skills/claude-docs-asset-wiring/SKILL.md +159 -0
  35. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/claude-md-template.md +86 -0
  36. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/dependency-cruiser.md +54 -0
  37. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/gotchas.md +122 -0
  38. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/package-json-patches.md +145 -0
  39. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/reference-files.md +37 -0
  40. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/smoke-tests.md +111 -0
  41. package/docs/consumer-integration.md +41 -100
  42. package/package.json +2 -2
  43. package/bin/claude-sync.mjs +0 -24
  44. package/docs/claude/skills/claude-sync-applier/SKILL.md +0 -195
  45. package/docs/claude/skills/claude-sync-applier/knowledge/claude-md-template.md +0 -77
  46. package/docs/claude/skills/claude-sync-applier/knowledge/dependency-cruiser.md +0 -126
  47. package/docs/claude/skills/claude-sync-applier/knowledge/gotchas.md +0 -139
  48. package/docs/claude/skills/claude-sync-applier/knowledge/package-json-patches.md +0 -130
  49. package/docs/claude/skills/claude-sync-applier/knowledge/reference-files.md +0 -120
  50. package/docs/claude/skills/claude-sync-applier/knowledge/smoke-tests.md +0 -102
@@ -0,0 +1,86 @@
1
+ # `CLAUDE.md` — `## Claude Docs Injector` section
2
+
3
+ Reference: `packages/canard/schema-form/CLAUDE.md`.
4
+
5
+ Append the section below to `${TARGET_PATH}/CLAUDE.md` if the file
6
+ exists. Substitute the sample package name in the chosen template
7
+ with `${PACKAGE_NAME}` — six occurrences. Skip the entire step if
8
+ `CLAUDE.md` does not exist (do not create one).
9
+
10
+ The template is intentionally terse: CLI usage + essential isolation
11
+ warnings. Architectural rationale lives in `knowledge/gotchas.md` —
12
+ do not duplicate it into every consumer's `CLAUDE.md`.
13
+
14
+ ---
15
+
16
+ ## Template (Korean — used by all consumers except `@winglet/style-utils`)
17
+
18
+ ````markdown
19
+ ## Claude Docs Injector
20
+
21
+ `docs/claude/**` 자산을 사용자 `.claude/` 에 주입. 엔진: `@slats/claude-assets-sync` (bin: `inject-claude-settings`).
22
+
23
+ ```bash
24
+ # universal — 모든 PM (pnpm strict / yarn-berry PnP 포함)
25
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@canard/schema-form --scope=user
26
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@canard/schema-form --scope=project
27
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@canard/schema-form --scope=user --dry-run
28
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@canard/schema-form --scope=user --force
29
+
30
+ # 간편 — npm / yarn-classic 에서만 (transitive bin hoist 기반)
31
+ npx inject-claude-settings --package=@canard/schema-form --scope=user
32
+ ```
33
+
34
+ ### Isolation Guardrails
35
+
36
+ - `src/**` 는 `docs/**` 와 `@slats/claude-assets-sync` 어느 것도 import 금지.
37
+ - **절대 `exports` 에 `./docs/*` 를 추가하지 말 것.**
38
+ ````
39
+
40
+ ---
41
+
42
+ ## Template (English — `@winglet/style-utils` convention)
43
+
44
+ ````markdown
45
+ ## Claude Docs Injector
46
+
47
+ Inject `docs/claude/**` into the user's `.claude/`. Engine: `@slats/claude-assets-sync` (bin: `inject-claude-settings`).
48
+
49
+ ```bash
50
+ # universal — every PM (pnpm strict / yarn-berry PnP included)
51
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@winglet/style-utils --scope=user
52
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@winglet/style-utils --scope=project
53
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@winglet/style-utils --scope=user --dry-run
54
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@winglet/style-utils --scope=user --force
55
+
56
+ # simple — npm / yarn-classic only (relies on transitive bin hoist)
57
+ npx inject-claude-settings --package=@winglet/style-utils --scope=user
58
+ ```
59
+
60
+ ### Isolation Guardrails
61
+
62
+ - `src/**` MUST NOT import from `docs/**` or `@slats/claude-assets-sync`.
63
+ - **Never add `./docs/*` to `exports`.**
64
+ ````
65
+
66
+ ---
67
+
68
+ ## Substitution Rules
69
+
70
+ - Replace the sample package name in the chosen template with
71
+ `${PACKAGE_NAME}` — six occurrences.
72
+ - Preserve the Isolation Guardrails bullets verbatim — these are
73
+ the sharp invariants that must stay consistent across consumers.
74
+
75
+ ---
76
+
77
+ ## Placement & Skip Conditions
78
+
79
+ - Append to end of `CLAUDE.md`. Ensure one blank line before the
80
+ injected section.
81
+ - `${TARGET_PATH}/CLAUDE.md` does not exist → skip, report
82
+ "skipped (no CLAUDE.md)".
83
+ - Section already present with identical content → skip, report
84
+ "unchanged".
85
+ - Section present with different content → ask user, do not
86
+ clobber.
@@ -0,0 +1,54 @@
1
+ # Dependency-Cruiser Isolation Rule (optional)
2
+
3
+ Skip this step unless `${TARGET_PATH}/.dependency-cruiser.cjs`
4
+ already exists or the user explicitly asks. This guardrail is a
5
+ CI-time check that the consumer's `src/**` never reaches the
6
+ Claude assets tree.
7
+
8
+ The post-v0.3.0 layout removes the bin stub entirely, so the
9
+ former `src-no-bin` and `src-no-claude-assets-sync` rules are no
10
+ longer load-bearing — the consumer no longer imports any of that
11
+ from anywhere. The one remaining invariant is `src/**` must not
12
+ import from `docs/**`.
13
+
14
+ ## Rule
15
+
16
+ ```javascript
17
+ module.exports = {
18
+ forbidden: [
19
+ {
20
+ name: 'src-no-docs',
21
+ severity: 'error',
22
+ comment:
23
+ 'src/ must not import from docs/. docs/claude/** contains pure markdown ' +
24
+ 'assets meant only for the engine dispatcher, not for the library runtime.',
25
+ from: { path: '^src/' },
26
+ to: { path: '^docs/' },
27
+ },
28
+ ],
29
+ options: {
30
+ doNotFollow: { path: 'node_modules' },
31
+ includeOnly: '^(src|docs)',
32
+ },
33
+ };
34
+ ```
35
+
36
+ ## Optional script
37
+
38
+ ```json
39
+ "scripts": {
40
+ "depcheck": "depcruise src docs --config .dependency-cruiser.cjs --no-progress"
41
+ }
42
+ ```
43
+
44
+ Zero errors expected. Orphan warnings on `docs/**` are
45
+ acceptable — the docs tree never imports anything.
46
+
47
+ ## Legacy rules removed
48
+
49
+ Previous revisions of this skill had three forbidden rules
50
+ (`src-no-bin`, `src-no-docs`, `src-no-claude-assets-sync`), a
51
+ `no-orphans` adjustment excluding `^bin/`, and an `includeOnly`
52
+ covering `^src` and `^bin`. All of those assumed the consumer
53
+ owned a `bin/` directory. The new layout owns no `bin/`, so the
54
+ extra rules are dead. Do not reintroduce them.
@@ -0,0 +1,122 @@
1
+ # Invariants and Gotchas
2
+
3
+ Hard-earned rules. Each one reflects a previous incident or a design
4
+ constraint of the `@slats/claude-assets-sync` engine.
5
+
6
+ ---
7
+
8
+ ## The engine is the only CLI surface
9
+
10
+ `inject-claude-settings` lives in one place: the engine package. No
11
+ consumer has a bin. No consumer has a `bin/` directory. No consumer
12
+ has a `scripts/` directory. If you find yourself "adapting" a stub
13
+ for a new consumer, stop — wiring does not need code; it needs two
14
+ fields in `package.json`.
15
+
16
+ ---
17
+
18
+ ## `claude.assetPath` is the opt-in marker
19
+
20
+ The engine's `claude-build-hashes` bin silently no-ops when
21
+ `claude.assetPath` is missing or not a string. The dispatcher
22
+ (`inject-claude-settings`) exits 2 with a clear error when a target
23
+ lacks the field. Both behaviors are intentional: missing = opt-out.
24
+
25
+ Do not add "helpful" error messages at build time for the opt-out
26
+ case — it would break silently-disabled packages.
27
+
28
+ ---
29
+
30
+ ## `@slats/claude-assets-sync` must be in `dependencies`
31
+
32
+ Not `devDependencies`, not `peerDependencies`. Reasons:
33
+
34
+ 1. Monorepo build chain needs `.bin/claude-build-hashes` resolved,
35
+ which requires the engine as a direct dep of each consumer.
36
+ 2. For end users on npm / yarn-classic, listing the engine in
37
+ `dependencies` makes `inject-claude-settings` transitively
38
+ hoisted into `node_modules/.bin/`, enabling the short
39
+ invocation `npx inject-claude-settings --package=<THIS>`.
40
+ 3. Bundle isolation is enforced by the import graph (`src/**`
41
+ never references the engine), not by dependency-type.
42
+
43
+ Pnpm strict users do not get the transitive hoist and must use the
44
+ universal form `npx -p @slats/claude-assets-sync inject-claude-settings
45
+ --package=<THIS>`. Every consumer's CLAUDE.md documents both paths.
46
+
47
+ ---
48
+
49
+ ## Never add `./bin/*` or `./docs/*` to `exports`
50
+
51
+ The `exports` map in `package.json` controls which subpaths a
52
+ consumer's bundler can resolve. Keeping `./docs/*` out of
53
+ `exports` is what prevents a bundler from deep-importing the docs
54
+ tree into app bundles.
55
+
56
+ ---
57
+
58
+ ## Do not commit `dist/claude-hashes.json`
59
+
60
+ It is a build artifact. The `yarn build` chain regenerates it via
61
+ `build:hashes`. It should be in `.gitignore` (usually via a
62
+ catch-all `dist/` rule). If you see it in `git status`, stop —
63
+ something is misconfigured.
64
+
65
+ ---
66
+
67
+ ## `yarn workspace ${PACKAGE_NAME} build` can fail with `rollup: command not found`
68
+
69
+ Yarn v4 workspace dispatch does not always propagate the
70
+ workspace-local PATH. Prefer `yarn ${SHORTCUT} build` from the
71
+ monorepo root, where `${SHORTCUT}` is the root-level script alias
72
+ (e.g. `yarn schemaForm`, `yarn claudeAssetsSync`).
73
+
74
+ If no shortcut exists, the full form may still work depending on
75
+ yarn version and cache state — but if it fails with `rollup:
76
+ command not found`, add a shortcut to the root `package.json`
77
+ rather than debugging the nested call.
78
+
79
+ ---
80
+
81
+ ## `--scope=project` walks upward
82
+
83
+ `--scope=project` walks `process.cwd()` upward looking for an
84
+ existing `.claude` directory. The first one found is reused; if
85
+ none is found, the engine creates one at `cwd`.
86
+
87
+ Consequence: running the smoke tests from the monorepo root would
88
+ reuse the monorepo's real `.claude`, corrupting it. Always run
89
+ smoke tests from `/tmp/...` with a fresh directory.
90
+
91
+ ---
92
+
93
+ ## Dispatcher exception to the `src/core` purity rule
94
+
95
+ `src/core/**` never reads `package.json` or walks the filesystem.
96
+ The engine's `bin/inject-claude-settings.mjs` and
97
+ `src/commands/runCli/utils/resolvePackage.ts` are allowed to
98
+ `createRequire().resolve(`${name}/package.json`)` for exactly one
99
+ target — the one named in `--package=<name>`. The dispatcher never
100
+ enumerates, never walks `node_modules` for siblings. Preserve this
101
+ boundary: extensions like `--all` or workspace scan require
102
+ explicit re-architecture.
103
+
104
+ ---
105
+
106
+ ## Commit this change alone
107
+
108
+ The change set from this skill touches the consumer's
109
+ `package.json` and possibly its `CLAUDE.md`. It should land in a
110
+ single commit, with no unrelated changes interleaved.
111
+
112
+ Reasons:
113
+
114
+ - Easier to revert as a unit if an issue appears downstream.
115
+ - The CI signal (smoke tests) is bound to the state of these files
116
+ and nothing else.
117
+ - Reviewers can skim-verify against the reference consumer without
118
+ reviewing business logic.
119
+
120
+ If the user asks to bundle with other work, push back once:
121
+ recommend a separate commit. If they still want it bundled,
122
+ proceed but note it in the Step 6 report.
@@ -0,0 +1,145 @@
1
+ # `package.json` Patches
2
+
3
+ All edits below are **additive**. Existing non-conflicting values
4
+ remain untouched. On any conflicting existing value, stop and ask the
5
+ user — do not overwrite.
6
+
7
+ Reference: `packages/canard/schema-form/package.json`.
8
+
9
+ ---
10
+
11
+ ## 1. `claude.assetPath`
12
+
13
+ ```json
14
+ "claude": {
15
+ "assetPath": "docs/claude"
16
+ }
17
+ ```
18
+
19
+ Consumer-side convention — the engine does not enforce it. Relative
20
+ to the consumer's package root. If the field already exists with a
21
+ non-default value, preserve it.
22
+
23
+ A missing or non-string value is an intentional opt-out: the
24
+ dispatcher will exit 2 with a clear error, and `claude-build-hashes`
25
+ will silently no-op. Do not remove the opt-out path.
26
+
27
+ ---
28
+
29
+ ## 2. `scripts.build`
30
+
31
+ Ensure the build chain invokes `yarn build:hashes` at the end:
32
+
33
+ ```json
34
+ "scripts": {
35
+ "build": "rollup -c && yarn build:types && yarn build:hashes"
36
+ }
37
+ ```
38
+
39
+ **Guard against double-append.** If the existing value already
40
+ contains `build:hashes`, leave it alone.
41
+
42
+ ---
43
+
44
+ ## 3. `scripts.build:hashes`
45
+
46
+ Point to the engine's bin (NOT a local stub):
47
+
48
+ ```json
49
+ "scripts": {
50
+ "build:hashes": "claude-build-hashes"
51
+ }
52
+ ```
53
+
54
+ `claude-build-hashes` reads `process.cwd()/package.json` and picks
55
+ up `claude.assetPath`. Works because `@slats/claude-assets-sync` is
56
+ in `dependencies`, so `node_modules/.bin/claude-build-hashes` is
57
+ linked.
58
+
59
+ If a different `build:hashes` script exists, ask.
60
+
61
+ ---
62
+
63
+ ## 4. `scripts.prepublishOnly`
64
+
65
+ ```json
66
+ "scripts": {
67
+ "prepublishOnly": "yarn build"
68
+ }
69
+ ```
70
+
71
+ Guarantees `dist/claude-hashes.json` is regenerated before publish.
72
+ If the target already has a `prepublishOnly` that calls `yarn build`
73
+ (directly or transitively), leave it alone.
74
+
75
+ ---
76
+
77
+ ## 5. `dependencies."@slats/claude-assets-sync"`
78
+
79
+ **Must be in `dependencies`, never `devDependencies` or
80
+ `peerDependencies`.**
81
+
82
+ ```json
83
+ "dependencies": {
84
+ "@slats/claude-assets-sync": "workspace:^"
85
+ }
86
+ ```
87
+
88
+ Reasons:
89
+
90
+ - Monorepo build chain needs `.bin/claude-build-hashes` resolved,
91
+ which requires the engine as a direct dep.
92
+ - On npm / yarn-classic, listing the engine in `dependencies` makes
93
+ `inject-claude-settings` transitively hoisted into
94
+ `node_modules/.bin/` for end users, enabling the short
95
+ invocation `npx inject-claude-settings --package=<THIS>`.
96
+ - Bundle isolation is enforced by the import graph (`src/**` never
97
+ references the engine), not by dependency-type.
98
+
99
+ If the target already has it in `devDependencies` or
100
+ `peerDependencies`, move it to `dependencies`. Do not duplicate.
101
+
102
+ ---
103
+
104
+ ## 6. `files`
105
+
106
+ Ship the published artifact surface. Keep `"dist"`, `"docs"`, and
107
+ `"README.md"` (plus whatever else the package needs). Do NOT
108
+ include `"bin"` or `"scripts"`:
109
+
110
+ ```json
111
+ "files": [
112
+ "dist",
113
+ "docs",
114
+ "README.md"
115
+ ]
116
+ ```
117
+
118
+ If `files` is absent, create it with at least `["dist", "docs", "README.md"]`.
119
+
120
+ ---
121
+
122
+ ## 7. `bin` — MUST be ABSENT
123
+
124
+ Never add a `bin` field. Bin names collide across consumers under
125
+ `node_modules/.bin/` and the engine is the sole CLI surface.
126
+
127
+ ---
128
+
129
+ ## 8. `exports` — never add `./bin/*` or `./docs/*`
130
+
131
+ Exports control which subpaths a consumer's bundler can resolve.
132
+ Keeping `./bin/*` and `./docs/*` out of `exports` is what prevents
133
+ consumer bundlers from pulling the CLI or the asset tree into app
134
+ bundles.
135
+
136
+ ---
137
+
138
+ ## Full Reference
139
+
140
+ See `packages/canard/schema-form/package.json` for the canonical
141
+ shape. The relevant keys are `scripts.build`,
142
+ `scripts.build:hashes`, `scripts.prepublishOnly`,
143
+ `dependencies."@slats/claude-assets-sync"`, `claude.assetPath`, and
144
+ `files`. Everything else in that file is schema-form-specific and
145
+ must not be copied.
@@ -0,0 +1,37 @@
1
+ # Reference Files
2
+
3
+ Consumers do **not** own any runtime files for the injector. The whole
4
+ CLI surface lives in `@slats/claude-assets-sync`. A consumer is wired
5
+ up by editing `package.json` and (optionally) `CLAUDE.md` — nothing
6
+ else.
7
+
8
+ Reference consumer: `packages/canard/schema-form`.
9
+
10
+ ## What the consumer MUST own
11
+
12
+ - `docs/claude/**` — the assets to ship (skills / rules / commands).
13
+ - `package.json.claude.assetPath` — string, usually `"docs/claude"`.
14
+
15
+ ## What the consumer MUST NOT own
16
+
17
+ - Any `bin/` directory or stub file. The engine owns the dispatcher.
18
+ - Any `scripts/build-hashes.mjs` wrapper. Use the engine's
19
+ `claude-build-hashes` bin directly in `scripts.build:hashes`.
20
+ - Any `"bin"` entry in `package.json`.
21
+ - `./bin/*` or `./docs/*` exposed in `exports`. Exposing them would
22
+ let bundlers pull CLI code or the docs tree into app bundles.
23
+
24
+ ## What the engine provides
25
+
26
+ - `inject-claude-settings` bin — dispatcher. Invoked as
27
+ `npx -p @slats/claude-assets-sync inject-claude-settings --package=<name> --scope=<scope>`,
28
+ or (on npm / yarn-classic) `npx inject-claude-settings --package=<name> --scope=<scope>`
29
+ once the engine is installed as a transitive dependency of the
30
+ consumer.
31
+ - `claude-build-hashes` bin — reads `process.cwd()/package.json`,
32
+ picks up `claude.assetPath`, hashes every file beneath it, and
33
+ writes `dist/claude-hashes.json`. Run via `yarn build:hashes` in
34
+ the consumer build chain.
35
+ - `buildHashes()` + `injectDocs()` — headless programmatic APIs.
36
+
37
+ No content mirroring across consumers. No stub drift to manage.
@@ -0,0 +1,111 @@
1
+ # E2E Smoke Tests — 8-path matrix via engine dispatcher
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
7
+ repo's `.claude`, which is a destructive error.
8
+
9
+ No fake `node_modules` needed — the engine uses
10
+ `createRequire(import.meta.url).resolve(`${PACKAGE_NAME}/package.json`)`
11
+ from the engine's own installed location.
12
+
13
+ ---
14
+
15
+ ## Setup
16
+
17
+ ```bash
18
+ BIN="$PWD/packages/slats/claude-assets-sync/bin/inject-claude-settings.mjs"
19
+ DIR=/tmp/inject-smoke-${SHORTCUT:-target}
20
+ [ -d "$DIR" ] && find "$DIR" -mindepth 1 -delete
21
+ mkdir -p "$DIR" && cd "$DIR"
22
+ ```
23
+
24
+ `[ -d ... ] && find -delete` keeps the setup idempotent. **Never** use
25
+ `rm -rf` or unquoted `*` globs — too easy to nuke the wrong directory.
26
+
27
+ ---
28
+
29
+ ## Matrix
30
+
31
+ Execute sequentially. `EXIT=$?` after each so the value is captured
32
+ before the next command overwrites `$?`.
33
+
34
+ | # | Command | Expected exit | Purpose |
35
+ |----|--------------------------------------------------------------------------------------------------|---------------|--------------------------------------------------------------|
36
+ | 1 | `node "$BIN" --package=${PACKAGE_NAME} --scope=project --dry-run` | 0 | Dry run — previews actions, no writes. |
37
+ | 2 | `node "$BIN" --package=${PACKAGE_NAME} --scope=project` | 0 | First real install — writes `.claude/` under `$DIR`. |
38
+ | 3 | `node "$BIN" --package=${PACKAGE_NAME} --scope=project` | 0 | Re-run — no-op (idempotent). |
39
+ | 4 | (after tampering) `CI=true node "$BIN" --package=${PACKAGE_NAME} --scope=project` | **2** | CI + tampered content → refuse to overwrite. |
40
+ | 5 | `CI=true node "$BIN" --package=${PACKAGE_NAME} --scope=project --force` | 0 | `--force` overrides the refusal. |
41
+ | 6 | `CI=true node "$BIN" --package=${PACKAGE_NAME}` | **2** | Missing `--scope` in non-TTY context. |
42
+ | 7 | `node "$BIN"` | **2** | Missing `--package` (dispatcher-specific). |
43
+ | 8 | `node "$BIN" --package=@does/not-exist` | **2** | Unresolvable package (dispatcher-specific). |
44
+
45
+ ### Tamper step (between path 3 and path 4)
46
+
47
+ ```bash
48
+ find .claude -name SKILL.md -exec sh -c 'echo tampered >> "$1"' _ {} \;
49
+ ```
50
+
51
+ Appends `tampered` to every `SKILL.md` under the local `.claude/`.
52
+ Simulates a human edit that the CI-mode dispatcher must detect and
53
+ refuse to clobber.
54
+
55
+ ---
56
+
57
+ ## Execution Shape
58
+
59
+ Split into **two bash calls** because `cwd` resets between Bash tool
60
+ invocations.
61
+
62
+ **First call** — paths 1–3:
63
+
64
+ ```bash
65
+ BIN="$PWD/packages/slats/claude-assets-sync/bin/inject-claude-settings.mjs"
66
+ DIR=/tmp/inject-smoke-${SHORTCUT:-target}
67
+ [ -d "$DIR" ] && find "$DIR" -mindepth 1 -delete
68
+ mkdir -p "$DIR" && cd "$DIR"
69
+
70
+ node "$BIN" --package=${PACKAGE_NAME} --scope=project --dry-run; echo "EXIT=$?"
71
+ node "$BIN" --package=${PACKAGE_NAME} --scope=project; echo "EXIT=$?"
72
+ node "$BIN" --package=${PACKAGE_NAME} --scope=project; echo "EXIT=$?"
73
+ ```
74
+
75
+ **Second call** — paths 4–8:
76
+
77
+ ```bash
78
+ DIR=/tmp/inject-smoke-${SHORTCUT:-target}
79
+ BIN="$PWD/packages/slats/claude-assets-sync/bin/inject-claude-settings.mjs"
80
+ cd "$DIR"
81
+
82
+ find .claude -name SKILL.md -exec sh -c 'echo tampered >> "$1"' _ {} \;
83
+ CI=true node "$BIN" --package=${PACKAGE_NAME} --scope=project; echo "EXIT=$?"
84
+ CI=true node "$BIN" --package=${PACKAGE_NAME} --scope=project --force; echo "EXIT=$?"
85
+ CI=true node "$BIN" --package=${PACKAGE_NAME}; echo "EXIT=$?"
86
+ node "$BIN"; echo "EXIT=$?"
87
+ node "$BIN" --package=@does/not-exist; echo "EXIT=$?"
88
+ ```
89
+
90
+ Note: `$PWD` in the second call is the parent shell's cwd (monorepo
91
+ root), so `BIN` resolves correctly. `cd "$DIR"` then moves into the
92
+ smoke directory before invoking.
93
+
94
+ ---
95
+
96
+ ## Failure Handling
97
+
98
+ | Observed | Meaning | Action |
99
+ |----------|-------------------------------------------------------------------------|--------------------------------------------------------------------|
100
+ | 1 ≠ 0 | Dry-run crashed. Likely an engine bug or bad `claude.assetPath`. | Stop, capture stderr, report. |
101
+ | 2 ≠ 0 | First write failed. Permissions, engine bug, or manifest issue. | Stop, inspect `dist/claude-hashes.json`, report. |
102
+ | 3 ≠ 0 | Idempotency broken — re-run should be no-op. | Stop, diff `$DIR/.claude` before/after, report. |
103
+ | 4 = 0 | CI mode did not refuse tampered files. Safety regression. | Stop — the engine's CI gate is broken. |
104
+ | 5 ≠ 0 | `--force` failed to override. Check engine. | Stop, report. |
105
+ | 6 = 0 | Engine defaulted a scope in non-TTY context. Should require `--scope`. | Stop, report. |
106
+ | 7 = 0 | Dispatcher accepted no `--package`. Violates contract. | Stop — dispatcher bug. |
107
+ | 8 = 0 | Dispatcher succeeded on unresolvable package. Violates contract. | Stop — dispatcher bug. |
108
+
109
+ Do not attempt to "make the tests pass" by altering expectations. The
110
+ matrix encodes invariants of the engine — a mismatch is a real
111
+ regression upstream.