@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
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
+ ```bash
31
+ # universal — every PM (pnpm strict / yarn-berry PnP included)
32
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@canard/schema-form --scope=user
33
+
34
+ # simple — npm / yarn-classic only (relies on transitive bin hoist from the consumer's dependencies)
35
+ npx inject-claude-settings --package=@canard/schema-form --scope=user
36
+ ```
26
37
 
27
38
  | Flag | Meaning |
28
39
  |---|---|
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) |
40
+ | `--package <name>` | **Required.** Scoped npm name of a consumer that declares `claude.assetPath`. |
41
+ | `--scope=user` | `~/.claude` (applies globally). |
42
+ | `--scope=project` | Nearest ancestor `.claude` directory, or `<cwd>/.claude` if none found. |
43
+ | `--dry-run` | Print the copy / skip / warn plan, no writes. |
44
+ | `--force` | Overwrite diverged files & delete orphans (interactive confirm on TTY). |
45
+ | `--root <path>` | Override scope-resolution cwd. |
33
46
 
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`).
47
+ **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
48
 
36
49
  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
50
 
38
- ## Consumer Integration (3 steps)
51
+ ## Consumer Integration (2 steps)
39
52
 
40
53
  ### 1. `package.json`
41
54
 
42
55
  ```jsonc
43
56
  {
44
57
  "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
58
  "scripts": {
48
59
  "build": "… && yarn build:hashes",
49
- "build:hashes": "node scripts/build-hashes.mjs"
60
+ "build:hashes": "claude-build-hashes"
50
61
  },
51
62
  "dependencies": {
52
63
  "@slats/claude-assets-sync": "workspace:^"
53
64
  },
65
+ "files": ["dist", "docs", "README.md"],
54
66
  "claude": { "assetPath": "docs/claude" }
55
67
  }
56
68
  ```
57
69
 
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
- );
70
+ - `@slats/claude-assets-sync` MUST be in `dependencies`, not `devDependencies`see Rationale below.
71
+ - Do **not** add any `bin` field. The engine is the sole CLI surface; per-consumer bins would collide under `node_modules/.bin/`.
72
+ - Do **not** expose `./bin/*` or `./docs/*` in `exports`. That would let consumer bundlers pull CLI code or the asset tree into app bundles.
73
+ - Do **not** create a `bin/` or `scripts/` directory in the consumer.
73
74
 
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
- }
75
+ ### 2. Build
80
76
 
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
- });
77
+ ```bash
78
+ yarn build
79
+ # rolls up the library, emits types, then `claude-build-hashes` hashes every
80
+ # file under `claude.assetPath` and writes dist/claude-hashes.json
92
81
  ```
93
82
 
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';
83
+ Ship the resulting `dist/` (including `claude-hashes.json`) alongside `docs/` when you publish.
101
84
 
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
- ```
85
+ ### Rationale: `dependencies`, not `devDependencies`
110
86
 
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`.
87
+ - The monorepo build chain needs `.bin/claude-build-hashes` resolved, which requires the engine as a direct dep.
88
+ - For end users on npm / yarn-classic, listing the engine in `dependencies` makes `inject-claude-settings` transitively hoisted into `node_modules/.bin/`, enabling the short invocation `npx inject-claude-settings --package=<THIS>`. Pnpm strict users do not get the transitive hoist and must use the universal form `npx -p @slats/claude-assets-sync inject-claude-settings --package=<THIS>`.
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.0"
6
6
  },
7
- "generatedAt": "2026-04-24T04:25:43.386Z",
7
+ "generatedAt": "2026-04-24T14:51:13.873Z",
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": "1984ed9ea93be0e7b8eec8f77d57c455e670989c3948546d017343e321fe1b80",
12
+ "skills/claude-docs-asset-wiring/knowledge/claude-md-template.md": "affd92189e9d367c8717068695aca8acbadf68989a4073a8c4b1e2f4a622b72d",
13
+ "skills/claude-docs-asset-wiring/knowledge/dependency-cruiser.md": "18e994bd16242f72a4dc6cfa508493f4b63853a7a16c4910c7b7abd0368bd103",
14
+ "skills/claude-docs-asset-wiring/knowledge/gotchas.md": "9957464329cc970f6717179fad907ffad45ee4433b1d6151e20b5b3bee75b775",
15
+ "skills/claude-docs-asset-wiring/knowledge/package-json-patches.md": "426dcfdfdd5c02405c3d8a753b800fc90e1e9286d6f1c3c29320cf13e2645878",
16
+ "skills/claude-docs-asset-wiring/knowledge/reference-files.md": "3a63a095826bbfd07fae232568faf4d415dbdae0970ee4466c962ba285c3a01d",
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';
@@ -3,20 +3,33 @@
3
3
  var commander = require('commander');
4
4
  var logger = require('../../utils/logger.cjs');
5
5
  var version = require('../../utils/version.cjs');
6
+ var resolveTargets = require('./utils/resolveTargets.cjs');
6
7
  var runInject = require('./utils/runInject.cjs');
7
8
 
8
- async function runCli(argv = process.argv, options) {
9
+ async function runCli(argv = process.argv) {
9
10
  const cmd = new commander.Command();
10
11
  cmd
11
- .name('claude-sync')
12
- .description("Inject this package's assets into the target .claude directory")
13
- .version(options.version ?? version.VERSION)
12
+ .name('inject-claude-settings')
13
+ .description("Inject target consumer(s)' Claude assets into the selected .claude directory")
14
+ .version(version.VERSION)
15
+ .option('--package <name...>', 'Target(s). "@<scope>" = whole npm scope; "@<scope>/<name>" or "<name>" = one package. Repeat the flag or comma-separate values.', collectPackageValues, [])
14
16
  .option('--scope <scope>', 'Target scope: user (~/.claude) | project (nearest ancestor .claude or <cwd>/.claude)')
15
17
  .option('--dry-run', 'Preview without writing', false)
16
18
  .option('--force', 'Overwrite user modifications', false)
17
19
  .option('--root <path>', 'Override scope resolution cwd (default: cwd)')
18
20
  .action(async (flags) => {
19
- await runInject.runInject(flags, options);
21
+ const targets = flags.package ?? [];
22
+ if (targets.length === 0) {
23
+ logger.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.resolveTargets(targets, originCwd);
28
+ if (metadataList.length === 0) {
29
+ logger.logger.warn(`no packages resolved from --package target(s): ${targets.join(', ')}`);
30
+ return;
31
+ }
32
+ await runInject.runInject(flags, metadataList);
20
33
  });
21
34
  try {
22
35
  await cmd.parseAsync([...argv]);
@@ -27,5 +40,14 @@ async function runCli(argv = process.argv, options) {
27
40
  process.exit(1);
28
41
  }
29
42
  }
43
+ function collectPackageValues(value, previous = []) {
44
+ return [
45
+ ...previous,
46
+ ...value
47
+ .split(',')
48
+ .map((s) => s.trim())
49
+ .filter(Boolean),
50
+ ];
51
+ }
30
52
 
31
53
  exports.runCli = runCli;
@@ -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,33 @@
1
1
  import { Command } from 'commander';
2
2
  import { logger } from '../../utils/logger.mjs';
3
3
  import { VERSION } from '../../utils/version.mjs';
4
+ import { resolveTargets } from './utils/resolveTargets.mjs';
4
5
  import { runInject } from './utils/runInject.mjs';
5
6
 
6
- async function runCli(argv = process.argv, options) {
7
+ async function runCli(argv = process.argv) {
7
8
  const cmd = new Command();
8
9
  cmd
9
- .name('claude-sync')
10
- .description("Inject this package's assets into the target .claude directory")
11
- .version(options.version ?? VERSION)
10
+ .name('inject-claude-settings')
11
+ .description("Inject target consumer(s)' Claude assets into the selected .claude directory")
12
+ .version(VERSION)
13
+ .option('--package <name...>', 'Target(s). "@<scope>" = whole npm scope; "@<scope>/<name>" or "<name>" = one package. Repeat the flag or comma-separate values.', collectPackageValues, [])
12
14
  .option('--scope <scope>', 'Target scope: user (~/.claude) | project (nearest ancestor .claude or <cwd>/.claude)')
13
15
  .option('--dry-run', 'Preview without writing', false)
14
16
  .option('--force', 'Overwrite user modifications', false)
15
17
  .option('--root <path>', 'Override scope resolution cwd (default: cwd)')
16
18
  .action(async (flags) => {
17
- await runInject(flags, options);
19
+ const targets = flags.package ?? [];
20
+ if (targets.length === 0) {
21
+ logger.error('missing required flag: --package <name> (e.g. --package=@canard/schema-form or --package=@canard)');
22
+ process.exit(2);
23
+ }
24
+ const originCwd = flags.root ?? process.cwd();
25
+ const metadataList = await resolveTargets(targets, originCwd);
26
+ if (metadataList.length === 0) {
27
+ logger.warn(`no packages resolved from --package target(s): ${targets.join(', ')}`);
28
+ return;
29
+ }
30
+ await runInject(flags, metadataList);
18
31
  });
19
32
  try {
20
33
  await cmd.parseAsync([...argv]);
@@ -25,5 +38,14 @@ async function runCli(argv = process.argv, options) {
25
38
  process.exit(1);
26
39
  }
27
40
  }
41
+ function collectPackageValues(value, previous = []) {
42
+ return [
43
+ ...previous,
44
+ ...value
45
+ .split(',')
46
+ .map((s) => s.trim())
47
+ .filter(Boolean),
48
+ ];
49
+ }
28
50
 
29
51
  export { runCli };
@@ -1,23 +1,14 @@
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[];
17
7
  }
18
8
  /**
19
9
  * Resolved consumer metadata passed to the injection pipeline.
20
- * The caller owns the definition; the library does not read `package.json`.
10
+ * The dispatcher bin populates this by resolving a single explicitly-named
11
+ * target package — `core/**` still never reads `package.json` itself.
21
12
  */
22
13
  export interface ConsumerPackage {
23
14
  name: string;
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const NPM_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
4
+ function classifyTarget(target) {
5
+ if (typeof target !== 'string' || target.length === 0) {
6
+ return { kind: 'invalid', reason: 'empty --package value' };
7
+ }
8
+ if (target.startsWith('@')) {
9
+ const body = target.slice(1);
10
+ const slashIndex = body.indexOf('/');
11
+ if (slashIndex === -1) {
12
+ if (!NPM_NAME_PATTERN.test(body)) {
13
+ return {
14
+ kind: 'invalid',
15
+ reason: `invalid scope alias "${target}" — expected "@<scope>" with lowercase alphanumerics, ".", "-", or "_"`,
16
+ };
17
+ }
18
+ return { kind: 'scope', scope: body };
19
+ }
20
+ const scopePart = body.slice(0, slashIndex);
21
+ const namePart = body.slice(slashIndex + 1);
22
+ if (!NPM_NAME_PATTERN.test(scopePart) ||
23
+ namePart.length === 0 ||
24
+ namePart.includes('/') ||
25
+ !NPM_NAME_PATTERN.test(namePart)) {
26
+ return {
27
+ kind: 'invalid',
28
+ reason: `invalid scoped package "${target}" — expected "@<scope>/<name>"`,
29
+ };
30
+ }
31
+ return { kind: 'package', name: target };
32
+ }
33
+ if (target.includes('/')) {
34
+ return {
35
+ kind: 'invalid',
36
+ reason: `invalid target "${target}" — unscoped package names cannot contain "/"`,
37
+ };
38
+ }
39
+ if (!NPM_NAME_PATTERN.test(target)) {
40
+ return {
41
+ kind: 'invalid',
42
+ reason: `invalid package name "${target}" — expected lowercase alphanumerics, ".", "-", or "_"`,
43
+ };
44
+ }
45
+ return { kind: 'package', name: target };
46
+ }
47
+
48
+ exports.classifyTarget = classifyTarget;
@@ -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 };
@@ -15,7 +15,7 @@ var logger = require('../../../utils/logger.cjs');
15
15
  async function injectOne(target, scope, flags, originCwd) {
16
16
  if (!target.hashesPresent) {
17
17
  logger.logger.warn(`${target.name}: dist/claude-hashes.json missing — build the package (e.g. yarn build) to regenerate the hash manifest first.`);
18
- return;
18
+ return 0;
19
19
  }
20
20
  logger.logger.heading(`${target.name}@${target.version}`);
21
21
  const stopHeartbeat = heartbeat.startHeartbeat({
@@ -37,8 +37,7 @@ async function injectOne(target, scope, flags, originCwd) {
37
37
  return confirmForce.confirmForceAsync(diverged.length, orphans.length, [...diverged, ...orphans].map((a) => a.relPath).slice(0, 3));
38
38
  },
39
39
  });
40
- if (report.exitCode !== 0)
41
- process.exit(report.exitCode);
40
+ return report.exitCode;
42
41
  }
43
42
  finally {
44
43
  stopHeartbeat();
@@ -1,3 +1,3 @@
1
1
  import { type Scope } from '../../../core/index.js';
2
2
  import type { ConsumerPackage, DefaultFlags } from '../type.js';
3
- export declare function injectOne(target: ConsumerPackage, scope: Scope, flags: DefaultFlags, originCwd: string): Promise<void>;
3
+ export declare function injectOne(target: ConsumerPackage, scope: Scope, flags: DefaultFlags, originCwd: string): Promise<number>;
@@ -13,7 +13,7 @@ import { logger } from '../../../utils/logger.mjs';
13
13
  async function injectOne(target, scope, flags, originCwd) {
14
14
  if (!target.hashesPresent) {
15
15
  logger.warn(`${target.name}: dist/claude-hashes.json missing — build the package (e.g. yarn build) to regenerate the hash manifest first.`);
16
- return;
16
+ return 0;
17
17
  }
18
18
  logger.heading(`${target.name}@${target.version}`);
19
19
  const stopHeartbeat = startHeartbeat({
@@ -35,8 +35,7 @@ async function injectOne(target, scope, flags, originCwd) {
35
35
  return confirmForceAsync(diverged.length, orphans.length, [...diverged, ...orphans].map((a) => a.relPath).slice(0, 3));
36
36
  },
37
37
  });
38
- if (report.exitCode !== 0)
39
- process.exit(report.exitCode);
38
+ return report.exitCode;
40
39
  }
41
40
  finally {
42
41
  stopHeartbeat();
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ var node_fs = require('node:fs');
4
+ var promises = require('node:fs/promises');
5
+ var node_module = require('node:module');
6
+ var node_path = require('node:path');
7
+ var logger = require('../../../utils/logger.cjs');
8
+
9
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
10
+ async function resolvePackage(name, options = {}) {
11
+ const pkgJsonPath = resolvePackageJsonPath(name);
12
+ if (!pkgJsonPath) {
13
+ logger.logger.error(`cannot resolve package "${name}". Install it in the current project or pass the correct name.`);
14
+ process.exit(2);
15
+ }
16
+ const packageRoot = node_path.dirname(pkgJsonPath);
17
+ const raw = await promises.readFile(pkgJsonPath, 'utf-8');
18
+ const pkg = JSON.parse(raw);
19
+ if (typeof pkg.name !== 'string' || typeof pkg.version !== 'string') {
20
+ if (options.skipMissingAsset) {
21
+ logger.logger.warn(`"${name}" package.json is missing a string "name" or "version" — skipping.`);
22
+ return null;
23
+ }
24
+ logger.logger.error(`${pkgJsonPath} must define string "name" and "version".`);
25
+ process.exit(2);
26
+ }
27
+ const assetPath = pkg.claude?.assetPath;
28
+ if (typeof assetPath !== 'string' || assetPath.length === 0) {
29
+ if (options.skipMissingAsset) {
30
+ logger.logger.warn(`"${name}" is missing "claude.assetPath" — skipping (the package does not ship Claude assets).`);
31
+ return null;
32
+ }
33
+ logger.logger.error(`"${name}" is missing "claude.assetPath" in its package.json — the package does not ship Claude assets.`);
34
+ process.exit(2);
35
+ }
36
+ return {
37
+ packageRoot,
38
+ packageName: pkg.name,
39
+ packageVersion: pkg.version,
40
+ assetPath,
41
+ };
42
+ }
43
+ function resolvePackageJsonPath(name) {
44
+ const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('commands/runCli/utils/resolvePackage.cjs', document.baseURI).href)));
45
+ try {
46
+ return require$1.resolve(`${name}/package.json`);
47
+ }
48
+ catch (err) {
49
+ const code = err?.code;
50
+ if (code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED')
51
+ return null;
52
+ }
53
+ let mainEntry;
54
+ try {
55
+ mainEntry = require$1.resolve(name);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ let dir = node_path.dirname(mainEntry);
61
+ while (dir && dir !== node_path.dirname(dir)) {
62
+ const candidate = node_path.resolve(dir, 'package.json');
63
+ if (node_fs.existsSync(candidate)) {
64
+ try {
65
+ const pkg = JSON.parse(node_fs.readFileSync(candidate, 'utf-8'));
66
+ if (pkg.name === name)
67
+ return candidate;
68
+ }
69
+ catch {
70
+ }
71
+ }
72
+ dir = node_path.dirname(dir);
73
+ }
74
+ return null;
75
+ }
76
+
77
+ exports.resolvePackage = resolvePackage;
@@ -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>;