@slats/claude-assets-sync 0.3.1 → 0.3.3

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.
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 @slats/claude-assets-sync --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
 
@@ -21,21 +21,48 @@ yarn add -D @slats/claude-assets-sync
21
21
  ## CLI Surface
22
22
 
23
23
  ```
24
- inject-claude-settings --package=<name> [--scope=user|project] [--dry-run] [--force] [--root=<cwd>]
24
+ <bin> --package=<name> [--scope=user|project] [--dry-run] [--force] [--root=<cwd>]
25
25
  claude-build-hashes
26
26
  ```
27
27
 
28
+ `<bin>` is one of three entry points that all dispatch to the same engine:
29
+
30
+ | Bin | Use when |
31
+ |---|---|
32
+ | `claude-assets-sync` | invoking via `npx` — matches the package's unscoped name so `npx @slats/claude-assets-sync ...` works directly |
33
+ | `inject-claude-settings` | the engine is installed (`yarn add -D` / `npm i -g`) and you prefer a descriptive command name |
34
+ | `claude-build-hashes` | build-time helper for consumer packages (run from `package.json` scripts) |
35
+
28
36
  ### End-user invocation
29
37
 
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.
38
+ The engine is not shipped as a runtime dependency of consumers. The canonical npx form is:
39
+
40
+ ```bash
41
+ # Single consumer:
42
+ npx @slats/claude-assets-sync --package=@canard/schema-form --scope=user
43
+
44
+ # Scope alias — every installed @winglet/* that declares claude.assetPath:
45
+ npx @slats/claude-assets-sync --package=@winglet --scope=user
46
+ ```
47
+
48
+ The dispatcher walks `node_modules` from the current working directory (or `--root <path>`) up to the filesystem root, so it works as long as the target package is installed somewhere in the host project's hoisting chain.
49
+
50
+ #### Installed CLI (alternative)
31
51
 
32
52
  ```bash
33
- npx -p @slats/claude-assets-sync inject-claude-settings --package=@canard/schema-form --scope=user
53
+ yarn add -D @slats/claude-assets-sync
54
+ yarn inject-claude-settings --package=@canard/schema-form --scope=user
55
+
56
+ # or globally:
57
+ npm i -g @slats/claude-assets-sync
58
+ inject-claude-settings --package=@canard/schema-form --scope=user
34
59
  ```
35
60
 
61
+ The legacy explicit form `npx -p @slats/claude-assets-sync inject-claude-settings ...` continues to work for backward compatibility.
62
+
36
63
  | Flag | Meaning |
37
64
  |---|---|
38
- | `--package <name>` | **Required.** Scoped npm name of a consumer that declares `claude.assetPath`. |
65
+ | `--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`). |
39
66
  | `--scope=user` | `~/.claude` (applies globally). |
40
67
  | `--scope=project` | Nearest ancestor `.claude` directory, or `<cwd>/.claude` if none found. |
41
68
  | `--dry-run` | Print the copy / skip / warn plan, no writes. |
@@ -85,7 +112,7 @@ Ship the resulting `dist/` (including `claude-hashes.json`) alongside `docs/` wh
85
112
  - The engine is used at two moments only: (1) the consumer's own build, where `claude-build-hashes` produces `dist/claude-hashes.json`, and (2) the end user's one-off `inject-claude-settings` invocation. Neither is runtime behaviour of the consumer library.
86
113
  - Putting the engine in `dependencies` would force every downstream installer of the consumer to pull `commander`, `@inquirer/prompts`, and their transitive trees into their production `node_modules` — dead weight for anyone who never sets up Claude Code assets.
87
114
  - The workspace build chain still resolves `.bin/claude-build-hashes` from `devDependencies` at `yarn install` time; yarn workspaces link devDeps and deps identically for workspace-local builds.
88
- - End users never rely on a hoisted `inject-claude-settings` bin. The canonical invocation is `npx -p @slats/claude-assets-sync inject-claude-settings --package=<THIS>`, which fetches the engine on demand and caches it.
115
+ - End users never rely on a hoisted `inject-claude-settings` bin. The canonical invocation is `npx @slats/claude-assets-sync --package=<THIS>`, which fetches the engine on demand and caches it.
89
116
  - Bundle isolation is enforced by the import graph (`src/**` in the consumer never references the engine), not by dependency-type.
90
117
 
91
118
  ## Authoring `docs/claude/`
@@ -2,9 +2,9 @@
2
2
  "schemaVersion": 1,
3
3
  "package": {
4
4
  "name": "@slats/claude-assets-sync",
5
- "version": "0.3.1"
5
+ "version": "0.3.3"
6
6
  },
7
- "generatedAt": "2026-04-24T16:37:59.339Z",
7
+ "generatedAt": "2026-04-26T11:57:30.854Z",
8
8
  "algorithm": "sha256",
9
9
  "assetRoot": "docs/claude",
10
10
  "files": {
@@ -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,3 +1,4 @@
1
+ import { basename } from 'node:path';
1
2
  import { Command } from 'commander';
2
3
  import { logger } from '../../utils/logger.mjs';
3
4
  import { VERSION } from '../../utils/version.mjs';
@@ -5,10 +6,11 @@ import { renderOrFallback } from './utils/renderOrFallback.mjs';
5
6
  import { resolveTargets } from './utils/resolveTargets.mjs';
6
7
  import { toConsumerPackages } from './utils/toConsumerPackages.mjs';
7
8
 
9
+ const FALLBACK_PROGRAM_NAME = 'inject-claude-settings';
8
10
  async function runCli(argv = process.argv) {
9
11
  const cmd = new Command();
10
12
  cmd
11
- .name('inject-claude-settings')
13
+ .name(deriveProgramName(argv))
12
14
  .description("Inject target consumer(s)' Claude assets into the selected .claude directory")
13
15
  .version(VERSION)
14
16
  .option('--package <name...>', 'Target(s). "@<scope>" = whole npm scope; "@<scope>/<name>" or "<name>" = one package. Repeat the flag or comma-separate values.', collectPackageValues, [])
@@ -52,5 +54,13 @@ function collectPackageValues(value, previous = []) {
52
54
  .filter(Boolean),
53
55
  ];
54
56
  }
57
+ function deriveProgramName(argv) {
58
+ const argv1 = argv[1];
59
+ if (typeof argv1 !== 'string' || argv1.length === 0) {
60
+ return FALLBACK_PROGRAM_NAME;
61
+ }
62
+ const base = basename(argv1).replace(/\.(mjs|cjs|js)$/, '');
63
+ return base.length > 0 ? base : FALLBACK_PROGRAM_NAME;
64
+ }
55
65
 
56
66
  export { runCli };
@@ -13,4 +13,4 @@ export interface ResolvePackageOptions {
13
13
  */
14
14
  skipMissingAsset?: boolean;
15
15
  }
16
- export declare function resolvePackage(name: string, options?: ResolvePackageOptions): Promise<ResolvedMetadata | null>;
16
+ export declare function resolvePackage(name: string, options?: ResolvePackageOptions, originCwd?: string): Promise<ResolvedMetadata | null>;
@@ -2,10 +2,11 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
4
  import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { logger } from '../../../utils/logger.mjs';
6
7
 
7
- async function resolvePackage(name, options = {}) {
8
- const pkgJsonPath = resolvePackageJsonPath(name);
8
+ async function resolvePackage(name, options = {}, originCwd = process.cwd()) {
9
+ const pkgJsonPath = resolvePackageJsonPath(name, originCwd);
9
10
  if (!pkgJsonPath) {
10
11
  logger.error(`cannot resolve package "${name}". Install it in the current project or pass the correct name.`);
11
12
  process.exit(2);
@@ -37,8 +38,14 @@ async function resolvePackage(name, options = {}) {
37
38
  assetPath,
38
39
  };
39
40
  }
40
- function resolvePackageJsonPath(name) {
41
- const require = createRequire(import.meta.url);
41
+ function resolvePackageJsonPath(name, originCwd) {
42
+ const fromCwd = tryResolveFrom(name, resolve(originCwd, '__resolve-base__'));
43
+ if (fromCwd)
44
+ return fromCwd;
45
+ return tryResolveFrom(name, fileURLToPath(import.meta.url));
46
+ }
47
+ function tryResolveFrom(name, baseFilename) {
48
+ const require = createRequire(baseFilename);
42
49
  try {
43
50
  return require.resolve(`${name}/package.json`);
44
51
  }
@@ -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 }, rootCwd);
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 };
@@ -20,9 +20,7 @@ async function resolveTargets(targets, rootCwd) {
20
20
  candidates = await resolveScopeAlias(classification.scope, rootCwd);
21
21
  }
22
22
  else {
23
- const meta = await resolvePackage(classification.name, {
24
- skipMissingAsset: !isSingleTarget,
25
- });
23
+ const meta = await resolvePackage(classification.name, { skipMissingAsset: !isSingleTarget }, rootCwd);
26
24
  candidates = meta ? [meta] : [];
27
25
  }
28
26
  for (const meta of candidates) {
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { useCallback } from 'react';
3
2
  import { Box, Text } from 'ink';
3
+ import { useCallback } from 'react';
4
+ import { VERSION } from '../../utils/version.mjs';
4
5
  import { Banner } from '../components/Banner.mjs';
5
6
  import { ConfirmForce } from '../components/ConfirmForce.mjs';
6
7
  import { ErrorPanel } from '../components/ErrorPanel.mjs';
@@ -18,7 +19,6 @@ import { colors } from '../theme/colors.mjs';
18
19
  import { icons } from '../theme/icons.mjs';
19
20
  import { scopeLabel, etaSeconds } from './utils/eventSelectors.mjs';
20
21
 
21
- const VERSION = '0.3.0';
22
22
  function InjectApp(props) {
23
23
  const { targets, flags, originCwd, onExit } = props;
24
24
  const [phase, dispatch] = usePhase({ kind: 'resolving', targets });
@@ -14,7 +14,10 @@ function phaseReducer(phase, event) {
14
14
  return phase;
15
15
  }
16
16
  case 'planning-started': {
17
- const progress = new Map(event.targets.map((t) => [t.name, { packageName: t.name, status: 'pending' }]));
17
+ const progress = new Map(event.targets.map((t) => [
18
+ t.name,
19
+ { packageName: t.name, status: 'pending' },
20
+ ]));
18
21
  return {
19
22
  kind: 'planning',
20
23
  targets: event.targets,
@@ -1,5 +1,5 @@
1
- import { useEffect } from 'react';
2
1
  import { useApp } from 'ink';
2
+ import { useEffect } from 'react';
3
3
 
4
4
  function useExitApp({ enabled, exitCode, onExit, delayMs = 0, }) {
5
5
  const { exit } = useApp();
@@ -46,7 +46,8 @@ function usePlanStep({ targets, scope, originCwd, force, dispatch, onPlansReady,
46
46
  });
47
47
  results.push({ target, scope: scopeResolution, plan });
48
48
  for (const action of plan.actions) {
49
- if (action.kind === 'warn-diverged' || action.kind === 'warn-orphan') {
49
+ if (action.kind === 'warn-diverged' ||
50
+ action.kind === 'warn-orphan') {
50
51
  warnings.push({
51
52
  packageName: target.name,
52
53
  kind: action.kind,
package/dist/ui/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { renderInjectApp } from './InjectApp/utils/renderInjectApp.mjs';
2
2
  import 'react/jsx-runtime';
3
- import 'react';
4
3
  import 'ink';
4
+ import 'react';
5
5
  import 'ink-select-input';
6
6
  import 'ink-spinner';
7
7
  import 'node:fs/promises';
@@ -1,6 +1,6 @@
1
+ import type { ConsumerPackage } from '../../commands/runCli/type.js';
1
2
  import type { InjectPlan } from '../../core/buildPlan/index.js';
2
3
  import type { ScopeResolution } from '../../core/index.js';
3
- import type { ConsumerPackage } from '../../commands/runCli/type.js';
4
4
  export interface PlanStepState {
5
5
  readonly packageName: string;
6
6
  readonly status: 'pending' | 'running' | 'done' | 'failed';
@@ -2,4 +2,4 @@
2
2
  * Current package version from package.json
3
3
  * Automatically synchronized during build process
4
4
  */
5
- export declare const VERSION = "0.3.1";
5
+ export declare const VERSION = "0.3.3";
@@ -1,3 +1,3 @@
1
- const VERSION = '0.3.1';
1
+ const VERSION = '0.3.3';
2
2
 
3
3
  export { VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slats/claude-assets-sync",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Shared CLI engine that lets consumer packages inject their Claude docs (skills, rules, commands) into a user's .claude directory via a thin bin/inject-docs wrapper.",
5
5
  "keywords": [
6
6
  "claude",
@@ -36,6 +36,7 @@
36
36
  "module": "dist/index.mjs",
37
37
  "types": "dist/index.d.ts",
38
38
  "bin": {
39
+ "claude-assets-sync": "./bin/inject-claude-settings.mjs",
39
40
  "claude-build-hashes": "./scripts/claude-build-hashes.mjs",
40
41
  "inject-claude-settings": "./bin/inject-claude-settings.mjs"
41
42
  },
@@ -15,11 +15,10 @@ import { TargetCard } from '../src/ui/components/TargetCard.js';
15
15
  import { colors } from '../src/ui/theme/colors.js';
16
16
  import { icons } from '../src/ui/theme/icons.js';
17
17
  import type { Phase } from '../src/ui/types/index.js';
18
+ import { VERSION } from '../src/utils/version.js';
18
19
 
19
20
  import { buildPhase, PHASES, type PhaseKey } from './dev-ui-fixtures.js';
20
21
 
21
- const VERSION = '0.3.0-dev';
22
-
23
22
  interface CliArgs {
24
23
  mode: 'usage' | 'phase' | 'tour';
25
24
  phase?: PhaseKey;