@slats/claude-assets-sync 0.3.1 → 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.
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
 
@@ -30,12 +30,16 @@ claude-build-hashes
30
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
31
 
32
32
  ```bash
33
+ # Single consumer:
33
34
  npx -p @slats/claude-assets-sync inject-claude-settings --package=@canard/schema-form --scope=user
35
+
36
+ # Scope alias — every installed @winglet/* that declares claude.assetPath:
37
+ npx -p @slats/claude-assets-sync inject-claude-settings --package=@winglet --scope=user
34
38
  ```
35
39
 
36
40
  | Flag | Meaning |
37
41
  |---|---|
38
- | `--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`). |
39
43
  | `--scope=user` | `~/.claude` (applies globally). |
40
44
  | `--scope=project` | Nearest ancestor `.claude` directory, or `<cwd>/.claude` if none found. |
41
45
  | `--dry-run` | Print the copy / skip / warn plan, no writes. |
@@ -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.2"
6
6
  },
7
- "generatedAt": "2026-04-24T16:37:59.339Z",
7
+ "generatedAt": "2026-04-24T17:08:31.012Z",
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,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,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.2";
@@ -1,3 +1,3 @@
1
- const VERSION = '0.3.1';
1
+ const VERSION = '0.3.2';
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.2",
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",
@@ -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;