@slats/claude-assets-sync 0.3.0 → 0.3.2

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 (141) hide show
  1. package/README.md +15 -11
  2. package/dist/claude-hashes.json +7 -7
  3. package/dist/commands/runCli/runCli.d.ts +4 -2
  4. package/dist/commands/runCli/runCli.mjs +7 -2
  5. package/dist/commands/runCli/type.d.ts +1 -0
  6. package/dist/commands/runCli/utils/renderOrFallback.d.ts +6 -0
  7. package/dist/commands/runCli/utils/renderOrFallback.mjs +12 -0
  8. package/dist/commands/runCli/utils/renderPlain.d.ts +11 -0
  9. package/dist/commands/runCli/utils/renderPlain.mjs +89 -0
  10. package/dist/commands/runCli/utils/resolveScopeAlias.mjs +30 -33
  11. package/dist/commands/runCli/utils/resolveScopeFlag.d.ts +9 -1
  12. package/dist/commands/runCli/utils/resolveScopeFlag.mjs +5 -11
  13. package/dist/commands/runCli/utils/toConsumerPackages.d.ts +9 -0
  14. package/dist/commands/runCli/utils/toConsumerPackages.mjs +26 -0
  15. package/dist/core/index.d.ts +2 -2
  16. package/dist/core/injectDocs/index.d.ts +3 -2
  17. package/dist/core/injectDocs/type.d.ts +0 -19
  18. package/dist/core/scope/index.d.ts +1 -1
  19. package/dist/core/scope/scope.d.ts +0 -1
  20. package/dist/core/scope/scope.mjs +1 -4
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.mjs +2 -2
  23. package/dist/ui/InjectApp/InjectApp.d.ts +2 -0
  24. package/dist/ui/InjectApp/InjectApp.mjs +82 -0
  25. package/dist/ui/InjectApp/index.d.ts +2 -0
  26. package/dist/ui/InjectApp/utils/eventSelectors.d.ts +5 -0
  27. package/dist/ui/InjectApp/utils/eventSelectors.mjs +24 -0
  28. package/dist/ui/InjectApp/utils/phaseReducer.d.ts +2 -0
  29. package/dist/ui/InjectApp/utils/phaseReducer.mjs +133 -0
  30. package/dist/ui/InjectApp/utils/renderInjectApp.d.ts +2 -0
  31. package/dist/ui/InjectApp/utils/renderInjectApp.mjs +19 -0
  32. package/dist/ui/InjectApp/utils/type.d.ts +5 -0
  33. package/dist/ui/components/ActionRow.d.ts +7 -0
  34. package/dist/ui/components/ActionRow.mjs +45 -0
  35. package/dist/ui/components/Banner.d.ts +7 -0
  36. package/dist/ui/components/Banner.mjs +9 -0
  37. package/dist/ui/components/ConfirmForce.d.ts +8 -0
  38. package/dist/ui/components/ConfirmForce.mjs +35 -0
  39. package/dist/ui/components/ErrorPanel.d.ts +6 -0
  40. package/dist/ui/components/ErrorPanel.mjs +14 -0
  41. package/dist/ui/components/Footer.d.ts +8 -0
  42. package/dist/ui/components/Footer.mjs +27 -0
  43. package/dist/ui/components/PlanTable.d.ts +8 -0
  44. package/dist/ui/components/PlanTable.mjs +15 -0
  45. package/dist/ui/components/ProgressBar.d.ts +10 -0
  46. package/dist/ui/components/ProgressBar.mjs +28 -0
  47. package/dist/ui/components/ScopePicker.d.ts +7 -0
  48. package/dist/ui/components/ScopePicker.mjs +26 -0
  49. package/dist/ui/components/Spinner.d.ts +8 -0
  50. package/dist/ui/components/Spinner.mjs +10 -0
  51. package/dist/ui/components/StatusBadge.d.ts +8 -0
  52. package/dist/ui/components/StepTracker.d.ts +9 -0
  53. package/dist/ui/components/StepTracker.mjs +43 -0
  54. package/dist/ui/components/Summary.d.ts +9 -0
  55. package/dist/ui/components/Summary.mjs +30 -0
  56. package/dist/ui/components/TargetCard.d.ts +11 -0
  57. package/dist/ui/components/TargetCard.mjs +29 -0
  58. package/dist/ui/hooks/useApplyStep.d.ts +12 -0
  59. package/dist/ui/hooks/useApplyStep.mjs +30 -0
  60. package/dist/ui/hooks/useExitApp.d.ts +8 -0
  61. package/dist/ui/hooks/useExitApp.mjs +19 -0
  62. package/dist/ui/hooks/useForceConfirmStep.d.ts +9 -0
  63. package/dist/ui/hooks/useForceConfirmStep.mjs +24 -0
  64. package/dist/ui/hooks/useInjectSession.d.ts +10 -0
  65. package/dist/ui/hooks/useInjectSession.mjs +63 -0
  66. package/dist/ui/hooks/useInterval.d.ts +1 -0
  67. package/dist/ui/hooks/usePhase.d.ts +2 -0
  68. package/dist/ui/hooks/usePhase.mjs +9 -0
  69. package/dist/ui/hooks/usePlanStep.d.ts +13 -0
  70. package/dist/ui/hooks/usePlanStep.mjs +95 -0
  71. package/dist/ui/hooks/useResolveStep.d.ts +18 -0
  72. package/dist/ui/hooks/useResolveStep.mjs +21 -0
  73. package/dist/ui/hooks/useTerminalWidth.d.ts +1 -0
  74. package/dist/ui/index.d.ts +2 -0
  75. package/dist/ui/index.mjs +16 -0
  76. package/dist/ui/theme/colors.d.ts +12 -0
  77. package/dist/ui/theme/colors.mjs +9 -0
  78. package/dist/ui/theme/icons.d.ts +29 -0
  79. package/dist/ui/theme/icons.mjs +17 -0
  80. package/dist/ui/theme/layout.d.ts +20 -0
  81. package/dist/ui/theme/layout.mjs +9 -0
  82. package/dist/ui/types/event.d.ts +45 -0
  83. package/dist/ui/types/index.d.ts +4 -0
  84. package/dist/ui/types/phase.d.ts +44 -0
  85. package/dist/ui/types/render.d.ts +6 -0
  86. package/dist/ui/types/target.d.ts +25 -0
  87. package/dist/utils/version.d.ts +1 -1
  88. package/dist/utils/version.mjs +1 -1
  89. package/docs/claude/skills/claude-docs-asset-wiring/SKILL.md +1 -1
  90. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/claude-md-template.md +4 -12
  91. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/gotchas.md +17 -14
  92. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/package-json-patches.md +18 -13
  93. package/docs/claude/skills/claude-docs-asset-wiring/knowledge/reference-files.md +4 -4
  94. package/docs/consumer-integration.md +9 -8
  95. package/package.json +12 -7
  96. package/scripts/dev-ui-fixtures.ts +288 -0
  97. package/scripts/dev-ui.tsx +288 -0
  98. package/dist/commands/runCli/runCli.cjs +0 -53
  99. package/dist/commands/runCli/utils/classifyTarget.cjs +0 -48
  100. package/dist/commands/runCli/utils/injectOne.cjs +0 -47
  101. package/dist/commands/runCli/utils/injectOne.d.ts +0 -3
  102. package/dist/commands/runCli/utils/injectOne.mjs +0 -45
  103. package/dist/commands/runCli/utils/resolvePackage.cjs +0 -77
  104. package/dist/commands/runCli/utils/resolveScopeAlias.cjs +0 -69
  105. package/dist/commands/runCli/utils/resolveScopeFlag.cjs +0 -28
  106. package/dist/commands/runCli/utils/resolveTargets.cjs +0 -40
  107. package/dist/commands/runCli/utils/runInject.cjs +0 -52
  108. package/dist/commands/runCli/utils/runInject.d.ts +0 -3
  109. package/dist/commands/runCli/utils/runInject.mjs +0 -50
  110. package/dist/core/buildPlan/buildPlan.cjs +0 -42
  111. package/dist/core/buildPlan/utils/toPosix.cjs +0 -9
  112. package/dist/core/buildPlan/utils/walkFiles.cjs +0 -25
  113. package/dist/core/hash/hash.cjs +0 -30
  114. package/dist/core/hashManifest/hashManifest.cjs +0 -27
  115. package/dist/core/injectDocs/injectDocs.cjs +0 -43
  116. package/dist/core/injectDocs/injectDocs.d.ts +0 -2
  117. package/dist/core/injectDocs/injectDocs.mjs +0 -41
  118. package/dist/core/injectDocs/utils/applyAction.cjs +0 -21
  119. package/dist/core/injectDocs/utils/emitCiForceList.cjs +0 -10
  120. package/dist/core/injectDocs/utils/emitCiForceList.d.ts +0 -2
  121. package/dist/core/injectDocs/utils/emitCiForceList.mjs +0 -8
  122. package/dist/core/injectDocs/utils/printPlan.cjs +0 -20
  123. package/dist/core/injectDocs/utils/printPlan.d.ts +0 -2
  124. package/dist/core/injectDocs/utils/printPlan.mjs +0 -18
  125. package/dist/core/injectDocs/utils/summarize.cjs +0 -27
  126. package/dist/core/scope/scope.cjs +0 -46
  127. package/dist/core/scope/utils/isDirectory.cjs +0 -14
  128. package/dist/index.cjs +0 -20
  129. package/dist/prompts/confirmForce.cjs +0 -27
  130. package/dist/prompts/confirmForce.d.ts +0 -1
  131. package/dist/prompts/confirmForce.mjs +0 -25
  132. package/dist/prompts/index.d.ts +0 -2
  133. package/dist/prompts/selectScope.cjs +0 -30
  134. package/dist/prompts/selectScope.d.ts +0 -2
  135. package/dist/prompts/selectScope.mjs +0 -28
  136. package/dist/utils/asyncPool.cjs +0 -26
  137. package/dist/utils/heartbeat.cjs +0 -25
  138. package/dist/utils/heartbeat.d.ts +0 -16
  139. package/dist/utils/heartbeat.mjs +0 -23
  140. package/dist/utils/logger.cjs +0 -74
  141. package/dist/utils/version.cjs +0 -5
package/README.md CHANGED
@@ -4,9 +4,9 @@ Engine + dispatcher CLI that lets any npm package ship its own Claude Code docs
4
4
 
5
5
  ## Overview
6
6
 
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.
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 each consumer's metadata, compares its hash manifest against the target `.claude/`, and copies only what is out of date.
8
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.
9
+ `--package` accepts a scoped name (`@scope/pkg`), an unscoped name (`pkg`), or a **scope alias** (`@scope` with no slash) that fans out to every installed `node_modules/@scope/*` package declaring `claude.assetPath`. Single-target resolution uses `createRequire`; scope-alias enumeration walks ancestor `node_modules/@<scope>/` directories from `cwd` upward and is isolated to `runCli/utils/resolveScopeAlias.ts`.
10
10
 
11
11
  No GitHub fetch, no `.sync-meta.json`, no migrations — the consumer's `dist/claude-hashes.json` is the single source of truth.
12
12
 
@@ -27,17 +27,19 @@ claude-build-hashes
27
27
 
28
28
  ### End-user invocation
29
29
 
30
+ The engine is not shipped as a runtime dependency of consumers. Always invoke via `npx -p @slats/claude-assets-sync ...`; the package manager fetches and caches the engine on demand.
31
+
30
32
  ```bash
31
- # universal — every PM (pnpm strict / yarn-berry PnP included)
33
+ # Single consumer:
32
34
  npx -p @slats/claude-assets-sync inject-claude-settings --package=@canard/schema-form --scope=user
33
35
 
34
- # simplenpm / 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
+ # Scope alias every installed @winglet/* that declares claude.assetPath:
37
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@winglet --scope=user
36
38
  ```
37
39
 
38
40
  | Flag | Meaning |
39
41
  |---|---|
40
- | `--package <name>` | **Required.** Scoped npm name of a consumer that declares `claude.assetPath`. |
42
+ | `--package <name>` | **Required.** Repeatable/comma-separable. Accepts `@scope/pkg`, `pkg`, or a scope alias `@scope` (fans out to every installed `node_modules/@scope/*` with `claude.assetPath`). |
41
43
  | `--scope=user` | `~/.claude` (applies globally). |
42
44
  | `--scope=project` | Nearest ancestor `.claude` directory, or `<cwd>/.claude` if none found. |
43
45
  | `--dry-run` | Print the copy / skip / warn plan, no writes. |
@@ -59,7 +61,7 @@ For `--scope=project` the target `.claude` directory is resolved by walking up f
59
61
  "build": "… && yarn build:hashes",
60
62
  "build:hashes": "claude-build-hashes"
61
63
  },
62
- "dependencies": {
64
+ "devDependencies": {
63
65
  "@slats/claude-assets-sync": "workspace:^"
64
66
  },
65
67
  "files": ["dist", "docs", "README.md"],
@@ -67,7 +69,7 @@ For `--scope=project` the target `.claude` directory is resolved by walking up f
67
69
  }
68
70
  ```
69
71
 
70
- - `@slats/claude-assets-sync` MUST be in `dependencies`, not `devDependencies` — see Rationale below.
72
+ - `@slats/claude-assets-sync` MUST be in `devDependencies` — the engine is a CLI-only tool and must not leak into end-user production installs. See Rationale below.
71
73
  - Do **not** add any `bin` field. The engine is the sole CLI surface; per-consumer bins would collide under `node_modules/.bin/`.
72
74
  - Do **not** expose `./bin/*` or `./docs/*` in `exports`. That would let consumer bundlers pull CLI code or the asset tree into app bundles.
73
75
  - Do **not** create a `bin/` or `scripts/` directory in the consumer.
@@ -82,10 +84,12 @@ yarn build
82
84
 
83
85
  Ship the resulting `dist/` (including `claude-hashes.json`) alongside `docs/` when you publish.
84
86
 
85
- ### Rationale: `dependencies`, not `devDependencies`
87
+ ### Rationale: `devDependencies`, not `dependencies`
86
88
 
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
+ - The engine is used at two moments only: (1) the consumer's own build, where `claude-build-hashes` produces `dist/claude-hashes.json`, and (2) the end user's one-off `inject-claude-settings` invocation. Neither is runtime behaviour of the consumer library.
90
+ - Putting the engine in `dependencies` would force every downstream installer of the consumer to pull `commander`, `@inquirer/prompts`, and their transitive trees into their production `node_modules` dead weight for anyone who never sets up Claude Code assets.
91
+ - The workspace build chain still resolves `.bin/claude-build-hashes` from `devDependencies` at `yarn install` time; yarn workspaces link devDeps and deps identically for workspace-local builds.
92
+ - End users never rely on a hoisted `inject-claude-settings` bin. The canonical invocation is `npx -p @slats/claude-assets-sync inject-claude-settings --package=<THIS>`, which fetches the engine on demand and caches it.
89
93
  - Bundle isolation is enforced by the import graph (`src/**` in the consumer never references the engine), not by dependency-type.
90
94
 
91
95
  ## Authoring `docs/claude/`
@@ -2,18 +2,18 @@
2
2
  "schemaVersion": 1,
3
3
  "package": {
4
4
  "name": "@slats/claude-assets-sync",
5
- "version": "0.3.0"
5
+ "version": "0.3.2"
6
6
  },
7
- "generatedAt": "2026-04-24T14:51:13.873Z",
7
+ "generatedAt": "2026-04-24T17:08:31.012Z",
8
8
  "algorithm": "sha256",
9
9
  "assetRoot": "docs/claude",
10
10
  "files": {
11
- "skills/claude-docs-asset-wiring/SKILL.md": "1984ed9ea93be0e7b8eec8f77d57c455e670989c3948546d017343e321fe1b80",
12
- "skills/claude-docs-asset-wiring/knowledge/claude-md-template.md": "affd92189e9d367c8717068695aca8acbadf68989a4073a8c4b1e2f4a622b72d",
11
+ "skills/claude-docs-asset-wiring/SKILL.md": "4864a5a1aba0813fb64ad19111f3db4369df6cc025e485d03d17cdc9d245bce5",
12
+ "skills/claude-docs-asset-wiring/knowledge/claude-md-template.md": "b0274c5a37076a7e80bd7beccd739b10cc91ca5395fe425be25d9129206c684a",
13
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",
14
+ "skills/claude-docs-asset-wiring/knowledge/gotchas.md": "70a6c75ea09816ec457615c060e6a87197bc5ed332a377452b3cacd974b44eee",
15
+ "skills/claude-docs-asset-wiring/knowledge/package-json-patches.md": "063469932191843a2f7f9156a513717947bcad584c4084a0c8300897683885e7",
16
+ "skills/claude-docs-asset-wiring/knowledge/reference-files.md": "1b5f76d262f47835c4d0150a48fa25dfc644609fd4ecad260273a4cab57e41a2",
17
17
  "skills/claude-docs-asset-wiring/knowledge/smoke-tests.md": "c90fc82d46bf4b55d794c2ce463b4c1e199d13d6b1161028486fb4bbbd6a60fe"
18
18
  },
19
19
  "previousVersions": {}
@@ -3,12 +3,14 @@
3
3
  *
4
4
  * The `inject-claude-settings` dispatcher parses `--package <name...>`
5
5
  * from argv and classifies each value:
6
- * - `@<scope>` — enumerate every workspace package under that scope
6
+ * - `@<scope>` — enumerate every installed `node_modules/@<scope>/*`
7
+ * package that declares `claude.assetPath`
7
8
  * - `@<scope>/<name>` — one scoped package
8
9
  * - `<name>` — one unscoped package
9
10
  *
10
11
  * Targets are resolved via Node module resolution (`resolvePackage`)
11
12
  * except for scope aliases, which are the only path allowed to walk
12
- * the monorepo — that exception is isolated to `resolveScopeAlias.ts`.
13
+ * `node_modules` siblings — that exception is isolated to
14
+ * `resolveScopeAlias.ts`.
13
15
  */
14
16
  export declare function runCli(argv?: readonly string[]): Promise<void>;
@@ -1,8 +1,9 @@
1
1
  import { Command } from 'commander';
2
2
  import { logger } from '../../utils/logger.mjs';
3
3
  import { VERSION } from '../../utils/version.mjs';
4
+ import { renderOrFallback } from './utils/renderOrFallback.mjs';
4
5
  import { resolveTargets } from './utils/resolveTargets.mjs';
5
- import { runInject } from './utils/runInject.mjs';
6
+ import { toConsumerPackages } from './utils/toConsumerPackages.mjs';
6
7
 
7
8
  async function runCli(argv = process.argv) {
8
9
  const cmd = new Command();
@@ -15,6 +16,7 @@ async function runCli(argv = process.argv) {
15
16
  .option('--dry-run', 'Preview without writing', false)
16
17
  .option('--force', 'Overwrite user modifications', false)
17
18
  .option('--root <path>', 'Override scope resolution cwd (default: cwd)')
19
+ .option('--json', 'Emit structured JSON output (forces non-interactive legacy logger path)', false)
18
20
  .action(async (flags) => {
19
21
  const targets = flags.package ?? [];
20
22
  if (targets.length === 0) {
@@ -27,7 +29,10 @@ async function runCli(argv = process.argv) {
27
29
  logger.warn(`no packages resolved from --package target(s): ${targets.join(', ')}`);
28
30
  return;
29
31
  }
30
- await runInject(flags, metadataList);
32
+ const consumerPackages = await toConsumerPackages(metadataList);
33
+ const exitCode = await renderOrFallback(consumerPackages, flags, originCwd);
34
+ if (exitCode !== 0)
35
+ process.exit(exitCode);
31
36
  });
32
37
  try {
33
38
  await cmd.parseAsync([...argv]);
@@ -4,6 +4,7 @@ export interface DefaultFlags {
4
4
  force?: boolean;
5
5
  root?: string;
6
6
  package?: string[];
7
+ json?: boolean;
7
8
  }
8
9
  /**
9
10
  * Resolved consumer metadata passed to the injection pipeline.
@@ -0,0 +1,6 @@
1
+ import type { ConsumerPackage, DefaultFlags } from '../type.js';
2
+ interface RenderEnv {
3
+ readonly isTTY?: boolean;
4
+ }
5
+ export declare function renderOrFallback(targets: readonly ConsumerPackage[], flags: DefaultFlags, originCwd: string, env?: RenderEnv): Promise<number>;
6
+ export {};
@@ -0,0 +1,12 @@
1
+ import { renderPlain } from './renderPlain.mjs';
2
+
3
+ async function renderOrFallback(targets, flags, originCwd, env = {}) {
4
+ const isTTY = env.isTTY ?? Boolean(process.stdout.isTTY);
5
+ if (flags.json || !isTTY) {
6
+ return renderPlain(targets, flags, originCwd);
7
+ }
8
+ const ui = (await import('../../../ui/index.mjs'));
9
+ return ui.renderInjectApp({ targets, flags, originCwd });
10
+ }
11
+
12
+ export { renderOrFallback };
@@ -0,0 +1,11 @@
1
+ import type { ConsumerPackage, DefaultFlags } from '../type.js';
2
+ /**
3
+ * Plain (picocolors) renderer for non-TTY / `--json` invocations.
4
+ *
5
+ * Composes the same `core/**` primitives that the Ink `useInjectSession`
6
+ * pipeline uses — no legacy `injectDocs` orchestrator in between.
7
+ * Missing `--scope` on non-TTY exits via `resolveScopeFlag` with code 2.
8
+ * Divergent/orphan files still respect `--force`: absence of `--force`
9
+ * returns 2; presence logs the list to stderr and proceeds.
10
+ */
11
+ export declare function renderPlain(targets: readonly ConsumerPackage[], flags: DefaultFlags, originCwd: string): Promise<number>;
@@ -0,0 +1,89 @@
1
+ import 'node:crypto';
2
+ import 'node:fs/promises';
3
+ import { readHashManifest, computeNamespacePrefixes } from '../../../core/hashManifest/hashManifest.mjs';
4
+ import { applyAction } from '../../../core/injectDocs/utils/applyAction.mjs';
5
+ import { summarize } from '../../../core/injectDocs/utils/summarize.mjs';
6
+ import { buildPlan } from '../../../core/buildPlan/buildPlan.mjs';
7
+ import { resolveScope } from '../../../core/scope/scope.mjs';
8
+ import { asyncPool } from '../../../utils/asyncPool.mjs';
9
+ import { logger } from '../../../utils/logger.mjs';
10
+ import { resolveScopeFlag } from './resolveScopeFlag.mjs';
11
+
12
+ async function renderPlain(targets, flags, originCwd) {
13
+ if (targets.length === 0)
14
+ return 0;
15
+ const scope = resolveScopeFlag(flags.scope);
16
+ const fatalOnError = targets.length === 1;
17
+ let failureCount = 0;
18
+ for (const target of targets) {
19
+ if (!target.hashesPresent) {
20
+ logger.warn(`${target.name}: dist/claude-hashes.json missing — build the package (e.g. yarn build) to regenerate the hash manifest first.`);
21
+ continue;
22
+ }
23
+ logger.heading(`${target.name}@${target.version}`);
24
+ let exitCode;
25
+ try {
26
+ exitCode = await renderOneTarget(target, scope, flags, originCwd);
27
+ }
28
+ catch (err) {
29
+ const msg = err instanceof Error ? err.message : String(err);
30
+ logger.error(`${target.name}: ${msg}`);
31
+ exitCode = 1;
32
+ }
33
+ if (exitCode !== 0) {
34
+ if (fatalOnError)
35
+ return exitCode;
36
+ failureCount += 1;
37
+ }
38
+ }
39
+ return failureCount > 0 ? 1 : 0;
40
+ }
41
+ async function renderOneTarget(target, scope, flags, originCwd) {
42
+ const manifest = await readHashManifest(target.packageRoot);
43
+ const resolution = resolveScope(scope, originCwd);
44
+ const plan = await buildPlan({
45
+ sourceHashes: manifest.files,
46
+ targetRoot: resolution.targetRoot,
47
+ namespacePrefixes: computeNamespacePrefixes(manifest),
48
+ force: flags.force ?? false,
49
+ });
50
+ logger.info(`${target.name}@${target.version} → ${resolution.description}`);
51
+ printPlan(plan);
52
+ if (plan.requiresForce && !flags.force) {
53
+ logger.error('Re-run with --force to proceed, or inspect with --dry-run.');
54
+ return 2;
55
+ }
56
+ if (flags.force &&
57
+ plan.actions.some((a) => a.kind === 'warn-diverged' || a.kind === 'warn-orphan')) {
58
+ emitForceList(plan);
59
+ }
60
+ if (flags.dryRun) {
61
+ logger.warn('[DRY RUN] No files will be created, overwritten, or deleted.');
62
+ return 0;
63
+ }
64
+ await asyncPool(8, plan.actions, (action) => applyAction(action, target.assetRoot));
65
+ const report = summarize(plan, 0);
66
+ return report.exitCode;
67
+ }
68
+ function printPlan(plan) {
69
+ for (const action of plan.actions) {
70
+ if (action.kind === 'copy')
71
+ logger.file('create', action.relPath);
72
+ else if (action.kind === 'skip-uptodate')
73
+ logger.file('skip', `${action.relPath} (up-to-date)`);
74
+ else if (action.kind === 'warn-diverged')
75
+ logger.warn(`${action.relPath} — local differs from source (user edit or version change)`);
76
+ else if (action.kind === 'warn-orphan')
77
+ logger.warn(`${action.relPath} — present locally, absent in source`);
78
+ else if (action.kind === 'delete')
79
+ logger.file('update', `${action.relPath} (deleting)`);
80
+ }
81
+ }
82
+ function emitForceList(plan) {
83
+ const divergent = plan.actions.filter((action) => action.kind === 'warn-diverged' || action.kind === 'warn-orphan');
84
+ process.stderr.write(`[claude-assets-sync] --force overwriting ${divergent.length} file(s) in non-TTY mode:\n`);
85
+ for (const action of divergent)
86
+ process.stderr.write(` ${action.relPath}\n`);
87
+ }
88
+
89
+ export { renderPlain };
@@ -1,27 +1,45 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readdir, readFile } from 'node:fs/promises';
3
- import { join, resolve, dirname } from 'node:path';
3
+ import { resolve, join, dirname } from 'node:path';
4
4
  import { logger } from '../../../utils/logger.mjs';
5
5
  import { resolvePackage } from './resolvePackage.mjs';
6
6
 
7
7
  async function resolveScopeAlias(scope, rootCwd) {
8
- const packagesRoot = findPackagesRoot(rootCwd);
9
- if (!packagesRoot) {
10
- logger.error(`cannot locate a monorepo root with a "packages/" directory starting from "${rootCwd}". Scope alias "@${scope}" requires a workspace root.`);
8
+ const expectedPrefix = `@${scope}/`;
9
+ const seen = new Set();
10
+ const matchedNames = [];
11
+ let cur = resolve(rootCwd);
12
+ while (true) {
13
+ const scopeDir = join(cur, 'node_modules', `@${scope}`);
14
+ await collectScopeDir(scopeDir, expectedPrefix, seen, matchedNames);
15
+ const parent = dirname(cur);
16
+ if (parent === cur)
17
+ break;
18
+ cur = parent;
19
+ }
20
+ if (matchedNames.length === 0) {
21
+ logger.error(`scope alias "@${scope}" matched no installed packages in any ancestor "node_modules/@${scope}/" walking up from ${rootCwd}.`);
11
22
  process.exit(2);
12
23
  }
13
- const scopeDir = join(packagesRoot, 'packages', scope);
24
+ const resolved = [];
25
+ for (const name of matchedNames) {
26
+ const meta = await resolvePackage(name, { skipMissingAsset: true });
27
+ if (meta)
28
+ resolved.push(meta);
29
+ }
30
+ return resolved;
31
+ }
32
+ async function collectScopeDir(scopeDir, expectedPrefix, seen, matchedNames) {
14
33
  let entries;
15
34
  try {
16
35
  entries = await readdir(scopeDir);
17
36
  }
18
37
  catch {
19
- logger.error(`scope alias "@${scope}" has no matching directory at ${scopeDir}.`);
20
- process.exit(2);
38
+ return;
21
39
  }
22
- const matchedNames = [];
23
- const expectedPrefix = `@${scope}/`;
24
40
  for (const entry of entries) {
41
+ if (entry.startsWith('.'))
42
+ continue;
25
43
  const pkgJsonPath = join(scopeDir, entry, 'package.json');
26
44
  if (!existsSync(pkgJsonPath))
27
45
  continue;
@@ -35,33 +53,12 @@ async function resolveScopeAlias(scope, rootCwd) {
35
53
  }
36
54
  if (typeof parsed.name === 'string' &&
37
55
  parsed.name.startsWith(expectedPrefix) &&
38
- parsed.name.length > expectedPrefix.length) {
56
+ parsed.name.length > expectedPrefix.length &&
57
+ !seen.has(parsed.name)) {
58
+ seen.add(parsed.name);
39
59
  matchedNames.push(parsed.name);
40
60
  }
41
61
  }
42
- if (matchedNames.length === 0) {
43
- logger.warn(`scope alias "@${scope}" matched no workspace packages under ${scopeDir}.`);
44
- return [];
45
- }
46
- const resolved = [];
47
- for (const name of matchedNames) {
48
- const meta = await resolvePackage(name, { skipMissingAsset: true });
49
- if (meta)
50
- resolved.push(meta);
51
- }
52
- return resolved;
53
- }
54
- function findPackagesRoot(start) {
55
- let cur = resolve(start);
56
- while (true) {
57
- if (existsSync(join(cur, 'package.json')) && existsSync(join(cur, 'packages'))) {
58
- return cur;
59
- }
60
- const parent = dirname(cur);
61
- if (parent === cur)
62
- return null;
63
- cur = parent;
64
- }
65
62
  }
66
63
 
67
64
  export { resolveScopeAlias };
@@ -1,2 +1,10 @@
1
1
  import { type Scope } from '../../../core/index.js';
2
- export declare function resolveScopeFlag(flag: string | undefined): Promise<Scope>;
2
+ /**
3
+ * Legacy (non-TTY / --json) scope resolver.
4
+ *
5
+ * The TTY Ink path owns its own scope picker via `ui/components/ScopePicker`.
6
+ * This helper runs only after `renderOrFallback` has chosen the legacy path,
7
+ * where prompting is not appropriate — either stdout is piped or the caller
8
+ * asked for structured `--json` output. Missing flag → exit 2.
9
+ */
10
+ export declare function resolveScopeFlag(flag: string | undefined): Scope;
@@ -2,12 +2,9 @@ import 'node:crypto';
2
2
  import 'node:fs/promises';
3
3
  import 'node:path';
4
4
  import { logger } from '../../../utils/logger.mjs';
5
- import { isValidScope, isInteractive } from '../../../core/scope/scope.mjs';
6
- import { selectScopeAsync } from '../../../prompts/selectScope.mjs';
7
- import '@inquirer/prompts';
8
- import 'picocolors';
5
+ import { isValidScope } from '../../../core/scope/scope.mjs';
9
6
 
10
- async function resolveScopeFlag(flag) {
7
+ function resolveScopeFlag(flag) {
11
8
  if (flag) {
12
9
  if (!isValidScope(flag)) {
13
10
  logger.error(`Invalid --scope: ${flag}. Expected user | project.`);
@@ -15,12 +12,9 @@ async function resolveScopeFlag(flag) {
15
12
  }
16
13
  return flag;
17
14
  }
18
- if (!isInteractive()) {
19
- logger.error('--scope is required in non-interactive environments.');
20
- logger.error(' Pass --scope=user | --scope=project.');
21
- process.exit(2);
22
- }
23
- return selectScopeAsync();
15
+ logger.error('--scope is required in non-interactive environments.');
16
+ logger.error(' Pass --scope=user or --scope=project.');
17
+ process.exit(2);
24
18
  }
25
19
 
26
20
  export { resolveScopeFlag };
@@ -0,0 +1,9 @@
1
+ import type { ConsumerPackage } from '../type.js';
2
+ import type { ResolvedMetadata } from './resolvePackage.js';
3
+ /**
4
+ * Convert dispatcher `ResolvedMetadata` into runtime `ConsumerPackage`.
5
+ * Resolves the asset root against `packageRoot` and probes for
6
+ * `dist/claude-hashes.json` presence so both the Ink and legacy paths
7
+ * can treat the target uniformly.
8
+ */
9
+ export declare function toConsumerPackages(metadataList: readonly ResolvedMetadata[]): Promise<ConsumerPackage[]>;
@@ -0,0 +1,26 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { isAbsolute, resolve, join } from 'node:path';
3
+ import { logger } from '../../../utils/logger.mjs';
4
+
5
+ async function toConsumerPackages(metadataList) {
6
+ const result = [];
7
+ for (const metadata of metadataList) {
8
+ if (!isAbsolute(metadata.packageRoot)) {
9
+ logger.error(`packageRoot must be an absolute path; received: ${metadata.packageRoot}`);
10
+ process.exit(2);
11
+ }
12
+ const assetRoot = resolve(metadata.packageRoot, metadata.assetPath);
13
+ const hashesPath = join(metadata.packageRoot, 'dist', 'claude-hashes.json');
14
+ const hashesPresent = await stat(hashesPath).then(() => true, () => false);
15
+ result.push({
16
+ name: metadata.packageName,
17
+ version: metadata.packageVersion,
18
+ packageRoot: metadata.packageRoot,
19
+ assetRoot,
20
+ hashesPresent,
21
+ });
22
+ }
23
+ return result;
24
+ }
25
+
26
+ export { toConsumerPackages };
@@ -1,5 +1,5 @@
1
1
  export { hashContent, hashEquals, hashFile, type Sha256Hex, } from './hash/index.js';
2
2
  export { HASH_MANIFEST_FILENAME, computeNamespacePrefixes, readHashManifest, type HashManifest, } from './hashManifest/index.js';
3
- export { injectDocs, type InjectOptions, type InjectReport, } from './injectDocs/index.js';
3
+ export { applyAction, summarize, type InjectReport, } from './injectDocs/index.js';
4
4
  export { buildPlan, type Action, type InjectPlan, type PlanInput, } from './buildPlan/index.js';
5
- export { findNearestDotClaudeAncestor, isInteractive, isValidScope, resolveScope, type Scope, type ScopeResolution, } from './scope/index.js';
5
+ export { findNearestDotClaudeAncestor, isValidScope, resolveScope, type Scope, type ScopeResolution, } from './scope/index.js';
@@ -1,2 +1,3 @@
1
- export { injectDocs } from './injectDocs.js';
2
- export type { InjectOptions, InjectReport } from './type.js';
1
+ export { applyAction } from './utils/applyAction.js';
2
+ export { summarize } from './utils/summarize.js';
3
+ export type { InjectReport } from './type.js';
@@ -1,22 +1,3 @@
1
- import type { InjectPlan } from '../buildPlan/index.js';
2
- import type { Scope } from '../scope/index.js';
3
- export interface InjectOptions {
4
- packageName: string;
5
- packageVersion: string;
6
- packageRoot: string;
7
- assetRoot: string;
8
- scope: Scope;
9
- dryRun: boolean;
10
- force: boolean;
11
- /**
12
- * Origin directory used to resolve project/local scope targets. When set,
13
- * `resolveScope` walks up from this path to find the nearest existing
14
- * `.claude` ancestor. Defaults to `process.cwd()`.
15
- */
16
- originCwd?: string;
17
- /** Called AFTER plan is built but BEFORE apply. Return false to abort. */
18
- confirmForce?: (plan: InjectPlan) => Promise<boolean>;
19
- }
20
1
  export interface InjectReport {
21
2
  created: string[];
22
3
  updated: string[];
@@ -1 +1 @@
1
- export { findNearestDotClaudeAncestor, isInteractive, isValidScope, resolveScope, type Scope, type ScopeResolution, } from './scope.js';
1
+ export { findNearestDotClaudeAncestor, isValidScope, resolveScope, type Scope, type ScopeResolution, } from './scope.js';
@@ -13,4 +13,3 @@ export declare function isValidScope(value: unknown): value is Scope;
13
13
  */
14
14
  export declare function findNearestDotClaudeAncestor(start: string): string | null;
15
15
  export declare function resolveScope(scope: Scope, cwd?: string): ScopeResolution;
16
- export declare function isInteractive(): boolean;
@@ -34,8 +34,5 @@ function resolveScope(scope, cwd = process.cwd()) {
34
34
  : `${base}/.claude (project)`,
35
35
  };
36
36
  }
37
- function isInteractive() {
38
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
39
- }
40
37
 
41
- export { findNearestDotClaudeAncestor, isInteractive, isValidScope, resolveScope };
38
+ export { findNearestDotClaudeAncestor, isValidScope, resolveScope };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { runCli } from './commands/index.js';
2
- export { HASH_MANIFEST_FILENAME, computeNamespacePrefixes, injectDocs, isInteractive, isValidScope, readHashManifest, resolveScope, type HashManifest, type InjectOptions, type InjectReport, type Scope, type ScopeResolution, } from './core/index.js';
2
+ export { HASH_MANIFEST_FILENAME, computeNamespacePrefixes, isValidScope, readHashManifest, resolveScope, type HashManifest, type InjectReport, type Scope, type ScopeResolution, } from './core/index.js';
3
3
  export type { AssetType } from './utils/types.js';
package/dist/index.mjs CHANGED
@@ -2,6 +2,6 @@ export { runCli } from './commands/runCli/runCli.mjs';
2
2
  import 'node:crypto';
3
3
  import 'node:fs/promises';
4
4
  export { HASH_MANIFEST_FILENAME, computeNamespacePrefixes, readHashManifest } from './core/hashManifest/hashManifest.mjs';
5
- export { injectDocs } from './core/injectDocs/injectDocs.mjs';
6
5
  import 'node:path';
7
- export { isInteractive, isValidScope, resolveScope } from './core/scope/scope.mjs';
6
+ import 'picocolors';
7
+ export { isValidScope, resolveScope } from './core/scope/scope.mjs';
@@ -0,0 +1,2 @@
1
+ import type { InjectAppProps } from './utils/type.js';
2
+ export declare function InjectApp(props: InjectAppProps): React.ReactElement;
@@ -0,0 +1,82 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { Box, Text } from 'ink';
3
+ import { useCallback } from 'react';
4
+ import { VERSION } from '../../utils/version.mjs';
5
+ import { Banner } from '../components/Banner.mjs';
6
+ import { ConfirmForce } from '../components/ConfirmForce.mjs';
7
+ import { ErrorPanel } from '../components/ErrorPanel.mjs';
8
+ import { Footer } from '../components/Footer.mjs';
9
+ import { ProgressBar } from '../components/ProgressBar.mjs';
10
+ import { ScopePicker } from '../components/ScopePicker.mjs';
11
+ import { Spinner } from '../components/Spinner.mjs';
12
+ import { StepTracker } from '../components/StepTracker.mjs';
13
+ import { Summary } from '../components/Summary.mjs';
14
+ import { TargetCard } from '../components/TargetCard.mjs';
15
+ import { useExitApp } from '../hooks/useExitApp.mjs';
16
+ import { useInjectSession } from '../hooks/useInjectSession.mjs';
17
+ import { usePhase } from '../hooks/usePhase.mjs';
18
+ import { colors } from '../theme/colors.mjs';
19
+ import { icons } from '../theme/icons.mjs';
20
+ import { scopeLabel, etaSeconds } from './utils/eventSelectors.mjs';
21
+
22
+ function InjectApp(props) {
23
+ const { targets, flags, originCwd, onExit } = props;
24
+ const [phase, dispatch] = usePhase({ kind: 'resolving', targets });
25
+ useInjectSession({ targets, flags, originCwd, dispatch });
26
+ const handleExit = useCallback((code) => {
27
+ onExit(code);
28
+ }, [onExit]);
29
+ useExitApp({
30
+ enabled: phase.kind === 'summary',
31
+ exitCode: phase.kind === 'summary' ? phase.exitCode : 0,
32
+ onExit: handleExit,
33
+ delayMs: 100,
34
+ });
35
+ useExitApp({
36
+ enabled: phase.kind === 'error',
37
+ exitCode: 1,
38
+ onExit: handleExit,
39
+ delayMs: 100,
40
+ });
41
+ return (jsxs(Box, { flexDirection: "column", children: [jsx(Banner, { version: VERSION, scope: scopeLabel(phase) }), jsx(Box, { marginTop: 1, children: jsx(StepTracker, { phase: phase, targetCount: targets.length, scope: scopeLabel(phase) }) }), jsx(Box, { flexDirection: "column", marginTop: 1, children: renderPhaseBody(phase) }), jsx(Footer, { phase: phase, version: VERSION })] }));
42
+ }
43
+ function renderPhaseBody(phase) {
44
+ switch (phase.kind) {
45
+ case 'booting':
46
+ case 'resolving':
47
+ return jsx(Spinner, { label: "resolving targets\u2026" });
48
+ case 'scope-select':
49
+ return jsx(ScopePicker, { onSelect: phase.pending });
50
+ case 'planning':
51
+ return (jsxs(Box, { flexDirection: "column", children: [jsx(Spinner, { label: "building plans\u2026" }), jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [...phase.progress.values()].map((step) => (jsxs(Box, { children: [jsxs(Text, { color: step.status === 'done'
52
+ ? colors.success
53
+ : step.status === 'running'
54
+ ? colors.warn
55
+ : step.status === 'failed'
56
+ ? colors.danger
57
+ : colors.muted, bold: step.status === 'running', children: [step.status === 'done'
58
+ ? icons.check
59
+ : step.status === 'failed'
60
+ ? icons.cross
61
+ : step.status === 'running'
62
+ ? icons.bulletActive
63
+ : icons.bulletPending, ' '] }), jsx(Text, { children: step.packageName }), step.error ? (jsxs(Text, { color: colors.danger, dimColor: true, children: [' ', "(", step.error, ")"] })) : null] }, step.packageName))) })] }));
64
+ case 'diff-review':
65
+ return (jsxs(Box, { flexDirection: "column", children: [phase.plans.map((tp, idx) => (jsx(TargetCard, { target: tp.target, plan: tp.plan, expanded: true, highlighted: idx === phase.focusedIndex }, tp.target.name))), jsx(Box, { marginTop: 1, children: jsxs(Text, { color: colors.muted, dimColor: true, children: ["Applying ", phase.plans.length, " plan(s)\u2026"] }) })] }));
66
+ case 'force-confirm':
67
+ return (jsx(ConfirmForce, { warnings: phase.warnings, onAnswer: phase.pending }));
68
+ case 'applying': {
69
+ const eta = etaSeconds(phase.progress.startedAt, phase.progress.done, phase.progress.total);
70
+ return (jsxs(Box, { flexDirection: "column", children: [jsx(ProgressBar, { done: phase.progress.done, total: phase.progress.total, etaSeconds: eta, label: phase.progress.current }), jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: phase.plans.map((tp) => (jsx(TargetCard, { target: tp.target, plan: tp.plan, expanded: false }, tp.target.name))) })] }));
71
+ }
72
+ case 'summary':
73
+ return (jsxs(Box, { flexDirection: "column", children: [phase.plans.map((tp) => (jsx(TargetCard, { target: tp.target, plan: tp.plan, expanded: false }, tp.target.name))), jsx(Summary, { reports: phase.reports, exitCode: phase.exitCode, dryRun: phase.dryRun })] }));
74
+ case 'error':
75
+ return jsx(ErrorPanel, { error: phase.error });
76
+ default: {
77
+ return null;
78
+ }
79
+ }
80
+ }
81
+
82
+ export { InjectApp };
@@ -0,0 +1,2 @@
1
+ export { renderInjectApp } from './utils/renderInjectApp.js';
2
+ export { InjectApp } from './InjectApp.js';
@@ -0,0 +1,5 @@
1
+ import type { ConsumerPackage } from '../../../commands/runCli/type.js';
2
+ import type { Phase } from '../../types/index.js';
3
+ export declare function scopeLabel(phase: Phase): string | undefined;
4
+ export declare function targetsOf(phase: Phase): readonly ConsumerPackage[];
5
+ export declare function etaSeconds(startedAt: number, done: number, total: number, now?: number): number | undefined;