@motion-proto/live-tokens 0.75.0 → 0.77.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.
@@ -13,7 +13,7 @@ Create a component whose structure and behavior serve the user's purpose. Give e
13
13
  2. Design the properties: separate the component's parts, variants, and states, then write one row per editable role with its token and the CSS it controls, named the way the shipped components name the same role.
14
14
  3. Write the runtime file: the usage comment and the `:global(:root)` block. A structural choice is an intrinsic. Every component joins the sketch layer, and a fixed overlay portals to `<body>`.
15
15
  4. Write the editor file: the schema, the preview props, and the markup. Variants that share a value are linked.
16
- 5. Register the component in the module `src/main.ts` and `live-tokens.testing.ts` both name.
16
+ 5. Register the component in the module `src/main.ts` and `live-tokens.testing.ts` both name, and write its contract in the module `contractsModule` names.
17
17
  6. Run **live-tokens-check-compliance**, then `npx live-tokens check-component <id> --tests --strict --json` until exit 0 with complete applicable coverage, then the Svelte check and the build.
18
18
  7. Reply with the files, the id, the props, and each check's result. Then place the component on a page with **live-tokens-create-page**.
19
19
 
@@ -204,7 +204,7 @@ registerComponent({
204
204
  });
205
205
  ```
206
206
 
207
- Import the module from `src/main.ts`, before `bootLiveTokens` or `mount`, and name it as `registrySetup` in `live-tokens.testing.ts`. One module then serves the running app and `check-component --tests`, which imports it to see the registration without mounting the app: read `references/contract-tests.md`. A component that declares intrinsics adds `intrinsics` to the entry. `check-component` finds the registration by the id literal inside the call.
207
+ Import the module from `src/main.ts`, before `bootLiveTokens` or `mount`, and name it as `registrySetup` in `live-tokens.testing.ts`. One module then serves the running app and `check-component --tests`, which imports it to see the registration without mounting the app. The component's contract, one `ComponentContract` in the module `contractsModule` names, is what the eight component suites run; without it `--tests` reports `contract-missing`. Read `references/contract-tests.md` for both. A component that declares intrinsics adds `intrinsics` to the entry. `check-component` finds the registration by the id literal inside the call.
208
208
 
209
209
  At boot the plugin reads the `:global(:root)` block and writes `component-configs/<id>/default.json`, one token per property. An edit in the editor writes `_working.json`; Save As writes a named config. The assignments stay token references through that flow.
210
210
 
@@ -218,7 +218,7 @@ A fixed overlay portals to `<body>`: read `references/fixed-overlays.md`. A cont
218
218
 
219
219
  ## Verification
220
220
 
221
- 1. Run **live-tokens-check-compliance** and address its findings with **live-tokens-fix-findings**. Then run `npx live-tokens check-component <id> --tests --strict --json`. Inside the live-tokens repository, run `node bin/cli.mjs check-component <id> --tests --strict --json`. Each finding carries a rule id and a line; `--off=<rule>` silences a rule for one run. `--tests` runs the registry contract and the component contract suites, and its JSON reports coverage by rule. A `tests-not-installed` finding names the missing package; install `@playwright/test`, `vitest`, and `happy-dom` as devDependencies, then `npx playwright install chromium`. Fix every finding and rerun until exit 0 with complete applicable coverage and no disabled checks.
221
+ 1. Run **live-tokens-check-compliance** and address its findings with **live-tokens-fix-findings**. Then run `npx live-tokens check-component <id> --tests --strict --json`. Inside the live-tokens repository, run `node bin/cli.mjs check-component <id> --tests --strict --json`. Each finding carries a rule id and a line; `--off=<rule>` silences a rule for one run. `--tests` runs the registry contract and, for a component with a contract in the module `contractsModule` names, the component contract suites; its JSON reports coverage by rule. A `tests-not-installed` finding names the missing package; install `@playwright/test`, `vitest`, and `happy-dom` as devDependencies, then `npx playwright install chromium`. Fix every finding and rerun until exit 0 with complete applicable coverage and no disabled checks.
222
222
  2. Run the project's Svelte check and its build.
223
223
  3. Reply with the files, the component id, the props, and the results of steps 1 and 2, naming any check the environment prevented.
224
224
 
@@ -17,9 +17,11 @@ It resolves a shipped component's `sourceFile` against the package and yours
17
17
  against your project, and reports a component that exists as files and never
18
18
  reached a registration.
19
19
 
20
- Add `@playwright/test`, `vitest`, and `happy-dom` as devDependencies, then
21
- `npx playwright install chromium`. A missing one is a `tests-not-installed`
22
- finding naming the install command.
20
+ A project scaffolded by `create` has `@playwright/test`, `vitest`, and
21
+ `happy-dom` installed, with `live-tokens.testing.ts`, `src/registerComponents.ts`,
22
+ and `tests/contracts.ts` in place. Any other project adds the three as
23
+ devDependencies. Both run `npx playwright install chromium` once. A missing
24
+ one is a `tests-not-installed` finding naming the install command.
23
25
 
24
26
  Name the module that registers your components, in `live-tokens.testing.ts` at
25
27
  the project root, as a plain quoted string:
@@ -60,6 +62,100 @@ of their own.
60
62
  you; it narrows the run to one component and fails when no component is
61
63
  registered under that id.
62
64
 
65
+ ## The component contract
66
+
67
+ The eight Playwright suites drive one `ComponentContract` per component. The
68
+ package ships one for each of its own components and none for yours. A run
69
+ with `LIVE_TOKENS_COMPONENT=<id>` and no contract for `<id>` reports one
70
+ `contract-missing` finding, which names the setting that unlocks the suites.
71
+
72
+ Name the module that exports your contracts as `contractsModule`, next to
73
+ `registrySetup`:
74
+
75
+ ```ts
76
+ // live-tokens.testing.ts
77
+ export default defineTestingConfig({
78
+ registrySetup: 'src/registerComponents.ts',
79
+ contractsModule: 'tests/contracts.ts',
80
+ });
81
+ ```
82
+
83
+ The module exports a `ComponentContract[]`, as the default export or a named
84
+ `contracts` export. The type ships from `@motion-proto/live-tokens/testing`,
85
+ and the doc comment on each field is the reference for it. A contract
86
+ declares the component's parts as selectors inside the preview, then one
87
+ expectation per obligation:
88
+
89
+ ```ts
90
+ // tests/contracts.ts
91
+ import type { ComponentContract } from '@motion-proto/live-tokens/testing';
92
+
93
+ export const contracts: ComponentContract[] = [
94
+ {
95
+ id: 'statcard',
96
+ origin: 'custom',
97
+ root: 'root',
98
+ parts: { root: '.statcard', value: '.statcard-value' },
99
+ properties: [
100
+ {
101
+ paints: {
102
+ root: {
103
+ backgroundColor: '--statcard-surface',
104
+ borderTopColor: '--statcard-border',
105
+ borderTopWidth: '--statcard-border-width',
106
+ borderRadius: '--statcard-radius',
107
+ paddingTop: '--statcard-padding',
108
+ },
109
+ value: { color: '--statcard-value-text', fontSize: '--statcard-value-font-size' },
110
+ },
111
+ },
112
+ ],
113
+ states: { applicable: false, reason: 'a stat card renders one state' },
114
+ persistence: {
115
+ cases: [
116
+ { shape: 'token', variable: '--statcard-radius', observe: { part: 'root', css: 'borderRadius' } },
117
+ ],
118
+ resetVariable: '--statcard-radius',
119
+ },
120
+ theme: {
121
+ theme: 'brand',
122
+ changed: ['--statcard-radius'],
123
+ unchanged: ['--statcard-padding'],
124
+ aliasedTo: { '--statcard-radius': '--radius-none' },
125
+ observe: { part: 'root', css: 'borderRadius', variable: '--statcard-radius' },
126
+ },
127
+ interaction: { applicable: false, reason: 'a stat card carries no interactive role' },
128
+ sketch: {
129
+ style: 'pencil',
130
+ parts: [{ part: 'root', fill: '--statcard-surface', stroke: '--statcard-border' }],
131
+ },
132
+ },
133
+ ];
134
+ ```
135
+
136
+ Each obligation and what it needs from the project:
137
+
138
+ - `properties`: part key, then CSS property, then the semantic property that
139
+ drives it. One entry per variant tab and state tab the editor renders;
140
+ omit `variant` and `state` when the editor renders one view. The paint maps
141
+ and `uncovered` together name every property in `default.json`. A property
142
+ no computed style shows verbatim, one consumed inside `calc()` or a gradient
143
+ function, goes in `uncovered` with its reason.
144
+ - `states`: one entry per state tab, with the class or attributes the preview
145
+ forces. `applicable: false` when the editor renders no state strip.
146
+ - `persistence`: an edit the run makes through the editor, saves, reloads, and
147
+ reads back. `resetVariable` is a property one of the cases moves.
148
+ - `theme`: a theme in the project's data tree whose block for the component
149
+ reassigns a property. Save one with the editor before the first run.
150
+ `changed` names the properties the theme moves, `unchanged` the ones it
151
+ leaves, and `aliasedTo` the design token each reassigned property resolves
152
+ to under it.
153
+ - `interaction`: the interactive part's role and its cases. `applicable:
154
+ false` for a component with no interactive role.
155
+ - `sketch`: a sketch style id and the fill and stroke each reserved-class part
156
+ resolves to under it. `applicable: false` only for a component
157
+ `references/sketch-mode.md` exempts.
158
+
63
159
  ## Running vitest yourself
64
160
 
65
161
  `check-component --tests` covers the shipped path. Run vitest directly only
package/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.77.0 — A consumer component runs the same suites
4
+
5
+ ### Added
6
+
7
+ - **A consumer component's contract reaches the Playwright suites.**
8
+ `live-tokens.testing.ts` has carried a `contractsModule` setting since
9
+ 0.75.0, naming a module that exports the `ComponentContract[]` a project
10
+ adds to the shipped list, and nothing documented it. A consumer following
11
+ live-tokens-create-component reached the Verification step with the eight
12
+ component suites reported as incomplete and the skill giving no way to
13
+ complete them. The skill now names the contract as a deliverable beside the
14
+ registration, and `references/contract-tests.md` shows the setting, a
15
+ complete contract for the skill's own example, and what each obligation
16
+ needs from the project. `src/testing/contracts/contracts.test.ts` covers the
17
+ loader.
18
+ - **`contract-missing` is its own finding.** A run narrowed to a component
19
+ with no contract used to throw from the Playwright config, so the runner
20
+ saw no report and surfaced a `tests-setup` finding holding a tail of
21
+ stderr. The editor suite now emits one failing test tagged
22
+ `contract-missing`, the JSON report carries it, and the runner maps it like
23
+ any other violation: one finding at `live-tokens.testing.ts` that names the
24
+ setting and lists the declared ids, with the `coverage` fix slug. The render
25
+ and alias suites still run for that component, and the failed rule explains
26
+ its seven unrun obligations, so reconciliation adds nothing on top.
27
+
28
+ ### Changed
29
+
30
+ - **The `create` template installs the test tooling.** `@playwright/test`,
31
+ `vitest`, and `happy-dom` are devDependencies of a scaffolded project, and
32
+ the scaffold ships `live-tokens.testing.ts` naming `src/registerComponents.ts`
33
+ and `tests/contracts.ts`, both present and empty, with `main.ts` importing
34
+ the registration module before boot. `npm run test:design` therefore works
35
+ once `npx playwright install chromium` has run, and the create-component
36
+ skill lands its Registration and contract in files that already exist. The
37
+ three stay optional peers of the package. The create smoke test asserts the
38
+ tooling resolves in a scaffold from the packed tarball.
39
+
40
+ ## 0.76.0 — The skills directory matches the release
41
+
42
+ ### Changed (breaking)
43
+
44
+ - **`setup-claude --force` now makes `.claude/skills/` match the release.** It
45
+ replaced each bundled skill by merging over it, which left two kinds of stale
46
+ text behind: a skill a release renamed kept its old directory, where it
47
+ shadowed the replacement, and a reference file a release dropped survived
48
+ inside a skill that was otherwise current. Both are invisible until a model
49
+ reads them. A forced run now replaces each bundled directory outright and
50
+ removes any `live-tokens-` skill the release no longer ships, naming each one
51
+ it removed. A skill under any other name is the project's own and is never
52
+ touched, and an unforced run still deletes nothing. 0.75.0's rename of
53
+ `live-tokens-build-page` to `live-tokens-create-page` is the case that
54
+ prompted this; the changelog then told a reader to delete the directory by
55
+ hand.
56
+
57
+ Before a forced run, copy any bundled skill you edited in place to a name of
58
+ your own. A `live-tokens-` directory this release does not ship is deleted
59
+ without a prompt, and one it does ship is replaced rather than merged.
60
+
61
+ `setup-claude` moves out of `bin/cli.mjs` into `bin/setup-claude.mjs` as
62
+ `runSetupClaude` and `formatSetupResult`, matching `create.mjs`, and gains
63
+ `bin/setup-claude.test.ts`.
64
+
65
+ ### Changed
66
+
67
+ - **The `create` template installs the skills from `postinstall` and gitignores
68
+ them.** A scaffolded project no longer commits nine skill directories the
69
+ package owns, so a version bump stops showing twenty changed files that
70
+ nobody wrote. The skills instead refresh on every `npm install`. The
71
+ postinstall ends in `|| exit 0`: `setup-claude` refuses to run on Windows,
72
+ and a skills copy must never fail an install. A project created before this
73
+ release adds the two lines itself; the README carries them.
74
+
3
75
  ## 0.75.0 — A component passes when the tests pass
4
76
 
5
77
  ### Added
package/README.md CHANGED
@@ -322,7 +322,7 @@ npx @motion-proto/live-tokens <command>
322
322
  | Command | What it does |
323
323
  |---|---|
324
324
  | `create <dir> [--force]` | Scaffold a new Svelte + Vite app wired up with live-tokens. |
325
- | `setup-claude [--force]` | Install the bundled Claude Code skills into `./.claude/skills/`. |
325
+ | `setup-claude [--force]` | Install the bundled Claude Code skills into `./.claude/skills/`; `--force` makes the directory match this release. |
326
326
  | `components [id] [--json]` | List every component the project has, shipped and its own, with the props each takes; with an id, its props, variants, tokens, and defaults. |
327
327
  | `tokens [--scale <name>] [--json]` | List every design token the project's `tokens.css` declares, by scale, with its value. |
328
328
  | `report [--json]` | The project as facts: pending migrations, tokens each component reads, which page renders which component, and both checkers' findings by rule. Always exits 0. |
@@ -346,10 +346,20 @@ The package bundles nine Claude Code skills. They encode the conventions this RE
346
346
  npx @motion-proto/live-tokens setup-claude
347
347
  ```
348
348
 
349
- This copies every bundled skill into `./.claude/skills/` in the current directory. Re-run it after upgrading the package to pick up new and changed skills, adding `--force` to overwrite. macOS and Linux only. The equivalent by hand:
349
+ This copies every bundled skill into `./.claude/skills/` in the current directory, leaving any that already exist. macOS and Linux only.
350
350
 
351
- ```bash
352
- mkdir -p .claude/skills && cp -R node_modules/@motion-proto/live-tokens/.claude/skills/. .claude/skills/
351
+ Re-run it with `--force` after upgrading. That makes the directory match the release: each bundled skill is replaced outright, so a reference file the release dropped goes with it, and a `live-tokens-` skill the release no longer ships is removed. A skill under any other name is the project's own and is never touched. Without `--force` nothing is deleted and an existing skill is left alone, so a renamed skill keeps shadowing its replacement until a forced run clears it.
352
+
353
+ A project created with `create` runs the forced form from `postinstall` and gitignores the copy, so the skills track the installed version with nothing to commit. To do the same in an existing project:
354
+
355
+ ```jsonc
356
+ // package.json
357
+ "postinstall": "live-tokens setup-claude --force || exit 0"
358
+ ```
359
+
360
+ ```
361
+ # .gitignore
362
+ .claude/skills/live-tokens-*/
353
363
  ```
354
364
 
355
365
  ### `live-tokens-pick-component`
@@ -55,6 +55,7 @@ export const COMPONENT_RULES = {
55
55
  'contract-interaction': 'error',
56
56
  'contract-listed': 'error',
57
57
  'contract-sketch': 'error',
58
+ 'contract-missing': 'error',
58
59
  'tests-not-installed': 'error',
59
60
  'tests-setup': 'error',
60
61
  'tests-incomplete': 'error',
@@ -94,6 +95,7 @@ export const COMPONENT_RULE_FIX = {
94
95
  'contract-registry': 'registration',
95
96
  'contract-listed': 'registration',
96
97
  'contract-sketch': 'sketch',
98
+ 'contract-missing': 'coverage',
97
99
  'tests-not-installed': 'tooling',
98
100
  'tests-setup': 'tooling',
99
101
  'tests-incomplete': 'coverage',
package/bin/cli.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
  // save-theme <name> Compose the live state into themes/<slug>.json and open it.
15
15
  // migrate [...] Reconcile tokens.css, the data tree, and route references.
16
16
 
17
- import { cpSync, existsSync, mkdirSync, readdirSync, statSync, writeSync } from 'node:fs';
17
+ import { writeSync } from 'node:fs';
18
18
  import { dirname, join, resolve } from 'node:path';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import process from 'node:process';
@@ -40,6 +40,7 @@ import {
40
40
  } from './migrate.mjs';
41
41
  import { runMigrateRoutes, formatRouteResult } from './migrate-routes.mjs';
42
42
  import { runCreate, formatCreateResult } from './create.mjs';
43
+ import { runSetupClaude, formatSetupResult } from './setup-claude.mjs';
43
44
  import { runSetColors, formatSetColorsResult } from './set-colors.mjs';
44
45
  import { runSetGeometry, formatSetGeometryResult } from './set-geometry.mjs';
45
46
  import { runSetType, formatSetTypeResult } from './set-type.mjs';
@@ -407,61 +408,14 @@ if (process.platform === 'win32') {
407
408
  fail('setup-claude is macOS/Linux only.');
408
409
  }
409
410
 
410
- const force = rest.includes('--force');
411
-
412
- const srcSkills = join(pkgRoot, '.claude', 'skills');
413
-
414
- if (!existsSync(srcSkills)) {
415
- fail(`No bundled skills found at ${srcSkills}. Is the package installed correctly?`);
416
- }
417
-
418
- const skills = readdirSync(srcSkills).filter((name) =>
419
- statSync(join(srcSkills, name)).isDirectory(),
420
- );
421
-
422
- if (skills.length === 0) {
423
- fail('No bundled skills to install.');
424
- }
425
-
426
- const destSkills = join(process.cwd(), '.claude', 'skills');
427
- mkdirSync(destSkills, { recursive: true });
428
-
429
- let installed = 0;
430
- let skipped = 0;
431
- for (const skill of skills) {
432
- const src = join(srcSkills, skill);
433
- const dest = join(destSkills, skill);
434
- if (existsSync(dest) && !force) {
435
- console.log(` skip ${skill} (already exists; pass --force to overwrite)`);
436
- skipped++;
437
- continue;
438
- }
439
- cpSync(src, dest, { recursive: true });
440
- console.log(` ok ${skill}`);
441
- installed++;
442
- }
443
-
444
- console.log(`\n${installed} installed, ${skipped} skipped, in ${destSkills}`);
445
-
446
- const SAMPLE_PROMPTS = {
447
- 'live-tokens-create-page': 'build a pricing page using live-tokens components',
448
- 'live-tokens-pick-component': "what's the difference between TabBar and SegmentedControl?",
449
- 'live-tokens-create-component': 'author a new Toggle component for my live-tokens project',
450
- 'live-tokens-create-theme': 'make me a bright and cheerful theme',
451
- 'live-tokens-set-colors': 'give me a cooler palette, same fonts',
452
- 'live-tokens-set-type': 'pair some fonts for this theme',
453
- 'live-tokens-set-geometry': 'make the buttons pill shaped',
454
- 'live-tokens-fix-findings': 'make check:design pass',
455
- 'live-tokens-check-compliance': 'check this project against the design system',
456
- };
457
-
458
- const installedSamples = skills
459
- .map((s) => SAMPLE_PROMPTS[s] && [s, SAMPLE_PROMPTS[s]])
460
- .filter(Boolean);
461
-
462
- if (installedSamples.length > 0) {
463
- console.log(`\nIn Claude Code, prompts like these auto-trigger the matching skill:`);
464
- for (const [skill, prompt] of installedSamples) {
465
- console.log(` "${prompt}"\n ${skill}`);
466
- }
411
+ try {
412
+ const result = runSetupClaude({
413
+ pkgRoot,
414
+ cwd: process.cwd(),
415
+ force: rest.includes('--force'),
416
+ });
417
+ console.log(formatSetupResult(result));
418
+ process.exit(0);
419
+ } catch (err) {
420
+ fail(err instanceof Error ? err.message : String(err));
467
421
  }
@@ -552,10 +552,15 @@ export function readPlaywrightTests(report) {
552
552
  * the shipped `.contract.ts` suite (absent from the tarball, and Playwright
553
553
  * reports no assertion location for it once compiled — see the plan's Wave
554
554
  * 3b/4 notes). `contract-alias`/`persist`/`theme` name the shipped config;
555
- * `contract-listed` names the editor; render/preview/sketch name the runtime.
555
+ * `contract-listed` names the editor; render/preview/sketch name the runtime;
556
+ * `contract-missing` names the settings file the contract module is declared in.
556
557
  */
557
558
  export function artifactForContractRule(root, sourceDataDir, rule, componentId, token) {
558
559
  if (!componentId) return { file: 'package.json', line: 1 };
560
+ if (rule === 'contract-missing') {
561
+ const settings = settingsFilePath(root);
562
+ return { file: settings ? relative(root, settings) : 'package.json', line: 1 };
563
+ }
559
564
  const paths = resolveComponentPaths(componentId, root);
560
565
  if (rule === 'contract-alias' || rule === 'contract-persist' || rule === 'contract-theme') {
561
566
  const target = componentConfigPath(sourceDataDir, componentId);
@@ -0,0 +1,110 @@
1
+ // Installs the bundled Claude Code skills into a project's ./.claude/skills/.
2
+ //
3
+ // Claude Code discovers skills only under .claude/skills/, never inside
4
+ // node_modules, so the package's own copy has to be duplicated into each
5
+ // project that wants them. That makes this command the only thing keeping the
6
+ // two trees in step, and `--force` has to mean "make the destination match this
7
+ // release" rather than "write over the files I happen to ship today":
8
+ //
9
+ // - a skill this release renamed leaves its old directory behind, where it
10
+ // shadows the new one with stale instructions;
11
+ // - a reference file this release dropped survives inside a skill that is
12
+ // otherwise current, because cpSync merges into an existing directory.
13
+ //
14
+ // Both are invisible until a model reads the stale text, so --force replaces
15
+ // each directory outright and prunes the ones this release no longer ships.
16
+
17
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+
20
+ // The package owns this prefix in a project's skills directory. A skill without
21
+ // it is the project's own and is never touched, whatever the flags say.
22
+ const SKILL_PREFIX = 'live-tokens-';
23
+
24
+ const SAMPLE_PROMPTS = {
25
+ 'live-tokens-create-page': 'build a pricing page using live-tokens components',
26
+ 'live-tokens-pick-component': "what's the difference between TabBar and SegmentedControl?",
27
+ 'live-tokens-create-component': 'author a new Toggle component for my live-tokens project',
28
+ 'live-tokens-create-theme': 'make me a bright and cheerful theme',
29
+ 'live-tokens-set-colors': 'give me a cooler palette, same fonts',
30
+ 'live-tokens-set-type': 'pair some fonts for this theme',
31
+ 'live-tokens-set-geometry': 'make the buttons pill shaped',
32
+ 'live-tokens-fix-findings': 'make check:design pass',
33
+ 'live-tokens-check-compliance': 'check this project against the design system',
34
+ };
35
+
36
+ function directoriesIn(dir) {
37
+ if (!existsSync(dir)) return [];
38
+ return readdirSync(dir).filter((name) => statSync(join(dir, name)).isDirectory());
39
+ }
40
+
41
+ export function runSetupClaude({ pkgRoot, cwd, force = false }) {
42
+ const srcSkills = join(pkgRoot, '.claude', 'skills');
43
+
44
+ if (!existsSync(srcSkills)) {
45
+ throw new Error(`No bundled skills found at ${srcSkills}. Is the package installed correctly?`);
46
+ }
47
+
48
+ const shipped = directoriesIn(srcSkills);
49
+ if (shipped.length === 0) {
50
+ throw new Error('No bundled skills to install.');
51
+ }
52
+
53
+ const destSkills = join(cwd, '.claude', 'skills');
54
+ mkdirSync(destSkills, { recursive: true });
55
+
56
+ const installed = [];
57
+ const skipped = [];
58
+ const removed = [];
59
+
60
+ for (const skill of shipped) {
61
+ const dest = join(destSkills, skill);
62
+ if (existsSync(dest) && !force) {
63
+ skipped.push(skill);
64
+ continue;
65
+ }
66
+ // Replace rather than merge: a file this release dropped must not survive
67
+ // inside a directory that otherwise looks current.
68
+ rmSync(dest, { recursive: true, force: true });
69
+ cpSync(join(srcSkills, skill), dest, { recursive: true });
70
+ installed.push(skill);
71
+ }
72
+
73
+ if (force) {
74
+ for (const name of directoriesIn(destSkills)) {
75
+ if (!name.startsWith(SKILL_PREFIX) || shipped.includes(name)) continue;
76
+ rmSync(join(destSkills, name), { recursive: true, force: true });
77
+ removed.push(name);
78
+ }
79
+ }
80
+
81
+ return { installed, skipped, removed, destSkills, shipped };
82
+ }
83
+
84
+ export function formatSetupResult({ installed, skipped, removed, destSkills, shipped }) {
85
+ const lines = [];
86
+
87
+ for (const skill of shipped) {
88
+ if (installed.includes(skill)) lines.push(` ok ${skill}`);
89
+ else lines.push(` skip ${skill} (already exists; pass --force to overwrite)`);
90
+ }
91
+ for (const skill of removed) {
92
+ lines.push(` gone ${skill} (this release no longer ships it; removed)`);
93
+ }
94
+
95
+ const counts = [`${installed.length} installed`, `${skipped.length} skipped`];
96
+ if (removed.length > 0) counts.push(`${removed.length} removed`);
97
+ lines.push(`\n${counts.join(', ')}, in ${destSkills}`);
98
+
99
+ if (skipped.length > 0 && removed.length === 0) {
100
+ lines.push(`\nRe-run with --force to bring every skill up to this release.`);
101
+ }
102
+
103
+ const samples = shipped.map((s) => SAMPLE_PROMPTS[s] && [s, SAMPLE_PROMPTS[s]]).filter(Boolean);
104
+ if (samples.length > 0) {
105
+ lines.push(`\nIn Claude Code, prompts like these auto-trigger the matching skill:`);
106
+ for (const [skill, prompt] of samples) lines.push(` "${prompt}"\n ${skill}`);
107
+ }
108
+
109
+ return lines.join('\n');
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.75.0",
3
+ "version": "0.77.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [