spec-layer 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +69 -1
  2. package/dist/cli.js +758 -44
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -55,6 +55,7 @@ also keeps `.speclayer/manifest.json` on one format: 0.1.0 wrote no
55
55
  versions have no such command, so the setup command the plugin copies fails
56
56
  against them. The Foundation landing under `tokens/` as Design Tokens Format
57
57
  Module 2025.10 files, rather than `ai/foundation.yaml`, needs 0.4.0 or later.
58
+ `tools` and `skill` need 0.5.0 or later.
58
59
 
59
60
  ## Commands
60
61
 
@@ -67,9 +68,76 @@ Module 2025.10 files, rather than `ai/foundation.yaml`, needs 0.4.0 or later.
67
68
  | `list` | Lists every artifact in the last pull, with its file path or `not written`. |
68
69
  | `show foundation [--canonical]` | Prints the Foundation's DTCG document to stdout. |
69
70
  | `show component NAME [--canonical]` | Prints one component's AI YAML to stdout. |
71
+ | `tools [--json]` | Lists every command with what it reaches, needs, and writes. |
72
+ | `skill [--install] [--agent HOST]... [--platform P] [--json]` | Prints a guide for a coding agent, adapted to this repository and the last pull; `--install` writes it where the agent reads instructions. |
70
73
 
71
74
  `--api URL` overrides the API origin (default `https://api.spec-layer.com`).
72
75
 
76
+ ## For a coding agent
77
+
78
+ The setup command is usually handed to a coding agent, and a bare command
79
+ tells the agent nothing about what it just wrote or how to read it. Two
80
+ local commands close that gap; neither needs a key or the network.
81
+
82
+ `spec-layer tools` lists every command with the facts an agent needs before
83
+ running one: whether it reaches the network, whether it needs the pull key,
84
+ what it writes, and what each exit code means. `--json` prints the same list
85
+ in a stable shape for machines.
86
+
87
+ `spec-layer skill` prints a guide to the pulled files, and `--install` writes
88
+ it where the agent reads project instructions:
89
+
90
+ ```bash
91
+ npx spec-layer skill --install
92
+ ```
93
+
94
+ `setup` names this command as the next step after a successful pull, and the
95
+ plugin's Publish screen has a **Copy for an AI agent** button that copies the
96
+ setup command already followed by it.
97
+
98
+ The guide is built from three things and nothing else:
99
+
100
+ - The tool list above.
101
+ - **What the last pull wrote.** Every component with its file path, every
102
+ token collection with its modes and default, the token files, the
103
+ `report.json` counts, and how many tokens landed as plain numbers because
104
+ their Figma scopes state no unit. Before a pull the guide says so and names
105
+ nothing.
106
+ - **What the repository root says about the codebase.** Detection reads only
107
+ the top level of the working directory (`package.json` dependency names,
108
+ build files, agent configuration directories) and names the file behind
109
+ every conclusion. A signal it cannot find is reported as absent, never
110
+ guessed. The platform decides which Figma `code_syntax` key the guide points
111
+ at (`WEB`, `iOS`, `ANDROID`; Flutter has none) and which token pipeline
112
+ advice it gives: Tailwind, Style Dictionary (with the `legacy` value form
113
+ suggested when package.json declares a major version below 5), Swift,
114
+ Kotlin or Compose, or Dart.
115
+
116
+ `--platform web|ios|android|flutter` overrides the detected target, which is
117
+ the way to get platform advice in a repository that carries no signal (a new
118
+ directory, a monorepo whose apps sit one level down). `--json` prints the
119
+ detection, the pull summary, and the install targets instead of the guide.
120
+
121
+ `--install` writes to every agent host detected at the root, or to the hosts
122
+ named with a repeatable `--agent`, or to `AGENTS.md` when nothing is detected
123
+ and nothing is named. Each run says which files it wrote, updated, or left
124
+ unchanged.
125
+
126
+ | Host | `--agent` | File |
127
+ |---|---|---|
128
+ | Claude Code | `claude` | `.claude/skills/spec-layer/SKILL.md` |
129
+ | Cursor | `cursor` | `.cursor/rules/spec-layer.mdc` |
130
+ | GitHub Copilot | `copilot` | `.github/instructions/spec-layer.instructions.md` |
131
+ | Windsurf | `windsurf` | `.windsurf/rules/spec-layer.md` |
132
+ | Gemini CLI | `gemini` | `GEMINI.md`, between `<!-- spec-layer:begin -->` and `<!-- spec-layer:end -->` |
133
+ | Anything that reads `AGENTS.md` | `agents-md` | `AGENTS.md`, between the same markers |
134
+
135
+ The dedicated files are replaced whole. The shared files (`AGENTS.md`,
136
+ `GEMINI.md`) are yours: only the marked block is replaced, and a file without
137
+ the markers gets the block appended. Re-run `skill --install` after a pull
138
+ that adds components or when the codebase changes stack. The written files
139
+ carry no key and are meant to be committed with the rest of the repository.
140
+
73
141
  ## Pulling part of a library
74
142
 
75
143
  By default `pull` writes the Foundation and every documented component. When
@@ -232,7 +300,7 @@ the report are not token files; exclude them from token globs.
232
300
  | Code | Meaning |
233
301
  |---|---|
234
302
  | `0` | Success, or `status` found the local copy up to date. |
235
- | `1` | Usage error, bad key or id, unknown component name, or a network or server failure. |
303
+ | `1` | Usage error, bad key or id, unknown component name, a network or server failure, or a file `skill --install` could not write. |
236
304
  | `2` | `status` only: the local copy is behind, or no local pull exists yet. |
237
305
 
238
306
  `status` is safe in CI: it writes nothing, and exit `2` is the signal to run
package/dist/cli.js CHANGED
@@ -262,7 +262,7 @@ var require_sha256 = __commonJS({
262
262
  }
263
263
  notString = true;
264
264
  }
265
- var code, index = 0, i, length = message.length, blocks2 = this.blocks;
265
+ var code2, index = 0, i, length = message.length, blocks2 = this.blocks;
266
266
  while (index < length) {
267
267
  if (this.hashed) {
268
268
  this.hashed = false;
@@ -275,22 +275,22 @@ var require_sha256 = __commonJS({
275
275
  }
276
276
  } else {
277
277
  for (i = this.start; index < length && i < 64; ++index) {
278
- code = message.charCodeAt(index);
279
- if (code < 128) {
280
- blocks2[i >>> 2] |= code << SHIFT[i++ & 3];
281
- } else if (code < 2048) {
282
- blocks2[i >>> 2] |= (192 | code >>> 6) << SHIFT[i++ & 3];
283
- blocks2[i >>> 2] |= (128 | code & 63) << SHIFT[i++ & 3];
284
- } else if (code < 55296 || code >= 57344) {
285
- blocks2[i >>> 2] |= (224 | code >>> 12) << SHIFT[i++ & 3];
286
- blocks2[i >>> 2] |= (128 | code >>> 6 & 63) << SHIFT[i++ & 3];
287
- blocks2[i >>> 2] |= (128 | code & 63) << SHIFT[i++ & 3];
278
+ code2 = message.charCodeAt(index);
279
+ if (code2 < 128) {
280
+ blocks2[i >>> 2] |= code2 << SHIFT[i++ & 3];
281
+ } else if (code2 < 2048) {
282
+ blocks2[i >>> 2] |= (192 | code2 >>> 6) << SHIFT[i++ & 3];
283
+ blocks2[i >>> 2] |= (128 | code2 & 63) << SHIFT[i++ & 3];
284
+ } else if (code2 < 55296 || code2 >= 57344) {
285
+ blocks2[i >>> 2] |= (224 | code2 >>> 12) << SHIFT[i++ & 3];
286
+ blocks2[i >>> 2] |= (128 | code2 >>> 6 & 63) << SHIFT[i++ & 3];
287
+ blocks2[i >>> 2] |= (128 | code2 & 63) << SHIFT[i++ & 3];
288
288
  } else {
289
- code = 65536 + ((code & 1023) << 10 | message.charCodeAt(++index) & 1023);
290
- blocks2[i >>> 2] |= (240 | code >>> 18) << SHIFT[i++ & 3];
291
- blocks2[i >>> 2] |= (128 | code >>> 12 & 63) << SHIFT[i++ & 3];
292
- blocks2[i >>> 2] |= (128 | code >>> 6 & 63) << SHIFT[i++ & 3];
293
- blocks2[i >>> 2] |= (128 | code & 63) << SHIFT[i++ & 3];
289
+ code2 = 65536 + ((code2 & 1023) << 10 | message.charCodeAt(++index) & 1023);
290
+ blocks2[i >>> 2] |= (240 | code2 >>> 18) << SHIFT[i++ & 3];
291
+ blocks2[i >>> 2] |= (128 | code2 >>> 12 & 63) << SHIFT[i++ & 3];
292
+ blocks2[i >>> 2] |= (128 | code2 >>> 6 & 63) << SHIFT[i++ & 3];
293
+ blocks2[i >>> 2] |= (128 | code2 & 63) << SHIFT[i++ & 3];
294
294
  }
295
295
  }
296
296
  }
@@ -472,24 +472,24 @@ var require_sha256 = __commonJS({
472
472
  function HmacSha256(key, is224, sharedMemory) {
473
473
  var i, type = typeof key;
474
474
  if (type === "string") {
475
- var bytes = [], length = key.length, index = 0, code;
475
+ var bytes = [], length = key.length, index = 0, code2;
476
476
  for (i = 0; i < length; ++i) {
477
- code = key.charCodeAt(i);
478
- if (code < 128) {
479
- bytes[index++] = code;
480
- } else if (code < 2048) {
481
- bytes[index++] = 192 | code >>> 6;
482
- bytes[index++] = 128 | code & 63;
483
- } else if (code < 55296 || code >= 57344) {
484
- bytes[index++] = 224 | code >>> 12;
485
- bytes[index++] = 128 | code >>> 6 & 63;
486
- bytes[index++] = 128 | code & 63;
477
+ code2 = key.charCodeAt(i);
478
+ if (code2 < 128) {
479
+ bytes[index++] = code2;
480
+ } else if (code2 < 2048) {
481
+ bytes[index++] = 192 | code2 >>> 6;
482
+ bytes[index++] = 128 | code2 & 63;
483
+ } else if (code2 < 55296 || code2 >= 57344) {
484
+ bytes[index++] = 224 | code2 >>> 12;
485
+ bytes[index++] = 128 | code2 >>> 6 & 63;
486
+ bytes[index++] = 128 | code2 & 63;
487
487
  } else {
488
- code = 65536 + ((code & 1023) << 10 | key.charCodeAt(++i) & 1023);
489
- bytes[index++] = 240 | code >>> 18;
490
- bytes[index++] = 128 | code >>> 12 & 63;
491
- bytes[index++] = 128 | code >>> 6 & 63;
492
- bytes[index++] = 128 | code & 63;
488
+ code2 = 65536 + ((code2 & 1023) << 10 | key.charCodeAt(++i) & 1023);
489
+ bytes[index++] = 240 | code2 >>> 18;
490
+ bytes[index++] = 128 | code2 >>> 12 & 63;
491
+ bytes[index++] = 128 | code2 >>> 6 & 63;
492
+ bytes[index++] = 128 | code2 & 63;
493
493
  }
494
494
  }
495
495
  key = bytes;
@@ -559,7 +559,7 @@ var require_sha256 = __commonJS({
559
559
  import { parseArgs } from "node:util";
560
560
 
561
561
  // src/commands.ts
562
- import { join as join5 } from "node:path";
562
+ import { join as join7 } from "node:path";
563
563
 
564
564
  // ../extractor/src/statesMatrix.ts
565
565
  var STATE_ORDER = [
@@ -642,10 +642,10 @@ var DEFAULT_SEVERITY = {
642
642
  EXPORT_SCOPED: "info"
643
643
  };
644
644
  var compareCodeUnits = (a, b) => a < b ? -1 : a > b ? 1 : 0;
645
- function diagnostic(code, fields) {
645
+ function diagnostic(code2, fields) {
646
646
  return {
647
- code,
648
- severity: DEFAULT_SEVERITY[code],
647
+ code: code2,
648
+ severity: DEFAULT_SEVERITY[code2],
649
649
  entity_id: fields.entity_id,
650
650
  ...fields.mode_id !== void 0 ? { mode_id: fields.mode_id } : {},
651
651
  message: fields.message,
@@ -2039,10 +2039,10 @@ var import_js_sha2563 = __toESM(require_sha256(), 1);
2039
2039
  // ../extractor/src/libraryBundle.ts
2040
2040
  var LIBRARY_BUNDLE_SCHEMA = "spec-layer-library-bundle";
2041
2041
  var LibraryBundleError = class extends Error {
2042
- constructor(code, message) {
2042
+ constructor(code2, message) {
2043
2043
  super(message);
2044
2044
  this.name = "LibraryBundleError";
2045
- this.code = code;
2045
+ this.code = code2;
2046
2046
  }
2047
2047
  };
2048
2048
  var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -2484,6 +2484,619 @@ ${fileName}
2484
2484
  return existed ? { kind: "added" } : { kind: "created" };
2485
2485
  }
2486
2486
 
2487
+ // src/detect.ts
2488
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
2489
+ import { join as join5 } from "node:path";
2490
+ var CODE_SYNTAX_KEY = {
2491
+ web: "WEB",
2492
+ ios: "iOS",
2493
+ android: "ANDROID",
2494
+ flutter: null
2495
+ };
2496
+ var PLATFORMS = ["web", "ios", "android", "flutter"];
2497
+ var AGENT_HOSTS = ["claude", "cursor", "copilot", "windsurf", "gemini", "agents-md"];
2498
+ var uniq = (xs) => [...new Set(xs)];
2499
+ function readPackageJson(cwd) {
2500
+ const path = join5(cwd, "package.json");
2501
+ if (!existsSync5(path)) return null;
2502
+ let parsed;
2503
+ try {
2504
+ parsed = JSON.parse(readFileSync5(path, "utf8"));
2505
+ } catch {
2506
+ return null;
2507
+ }
2508
+ if (typeof parsed !== "object" || parsed === null) return null;
2509
+ const record = parsed;
2510
+ const deps = {};
2511
+ for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
2512
+ const block = record[field];
2513
+ if (typeof block !== "object" || block === null) continue;
2514
+ for (const [name, range] of Object.entries(block)) {
2515
+ if (typeof range === "string") deps[name] = range;
2516
+ }
2517
+ }
2518
+ return { deps };
2519
+ }
2520
+ function majorOf(range) {
2521
+ const m = /^[\^~>=<\s]*v?(\d+)/.exec(range.trim());
2522
+ return m ? Number(m[1]) : null;
2523
+ }
2524
+ var DEP_SIGNALS = [
2525
+ { dep: "react", platform: "web", framework: "react" },
2526
+ { dep: "next", platform: "web", framework: "next" },
2527
+ { dep: "vue", platform: "web", framework: "vue" },
2528
+ { dep: "nuxt", platform: "web", framework: "nuxt" },
2529
+ { dep: "svelte", platform: "web", framework: "svelte" },
2530
+ { dep: "@sveltejs/kit", platform: "web", framework: "sveltekit" },
2531
+ { dep: "@angular/core", platform: "web", framework: "angular" },
2532
+ { dep: "solid-js", platform: "web", framework: "solid" },
2533
+ { dep: "lit", platform: "web", framework: "lit" },
2534
+ { dep: "astro", platform: "web", framework: "astro" },
2535
+ { dep: "react-native", platform: "ios", framework: "react-native" },
2536
+ { dep: "expo", platform: "ios", framework: "expo" },
2537
+ { dep: "tailwindcss", platform: "web", tokenTool: "tailwind" },
2538
+ { dep: "styled-components", platform: "web", framework: "styled-components" },
2539
+ { dep: "@emotion/react", platform: "web", framework: "emotion" },
2540
+ { dep: "sass", platform: "web", framework: "sass" },
2541
+ { dep: "@vanilla-extract/css", platform: "web", framework: "vanilla-extract" },
2542
+ { dep: "@stitches/react", platform: "web", framework: "stitches" },
2543
+ { dep: "@pandacss/dev", platform: "web", framework: "panda" },
2544
+ { dep: "style-dictionary", tokenTool: "style-dictionary" },
2545
+ { dep: "@tokens-studio/sd-transforms", tokenTool: "tokens-studio" },
2546
+ { dep: "typescript", language: "typescript" }
2547
+ ];
2548
+ var FILE_SIGNALS = [
2549
+ { test: (n) => n === "Package.swift", signal: "Swift package", platform: "ios", language: "swift" },
2550
+ { test: (n) => n.endsWith(".xcodeproj") || n.endsWith(".xcworkspace"), signal: "Xcode project", platform: "ios", language: "swift" },
2551
+ { test: (n) => n === "Podfile", signal: "CocoaPods", platform: "ios" },
2552
+ { test: (n) => /^build\.gradle(\.kts)?$/.test(n) || /^settings\.gradle(\.kts)?$/.test(n), signal: "Gradle build", platform: "android", language: "kotlin" },
2553
+ { test: (n) => n === "AndroidManifest.xml", signal: "Android manifest", platform: "android" },
2554
+ { test: (n) => n === "pubspec.yaml", signal: "Flutter or Dart package", platform: "flutter", language: "dart" },
2555
+ { test: (n) => n === "tsconfig.json", signal: "TypeScript config", language: "typescript" },
2556
+ { test: (n) => n === "package.json", signal: "npm package", language: "javascript" },
2557
+ { test: (n) => n === "deno.json" || n === "deno.jsonc", signal: "Deno config", language: "typescript" },
2558
+ { test: (n) => n === "Cargo.toml", signal: "Cargo manifest", language: "rust" },
2559
+ { test: (n) => n === "go.mod", signal: "Go module", language: "go" },
2560
+ { test: (n) => n === "pyproject.toml" || n === "requirements.txt", signal: "Python project", language: "python" },
2561
+ { test: (n) => n === "Gemfile", signal: "Ruby bundle", language: "ruby" },
2562
+ { test: (n) => n === "composer.json", signal: "Composer package", language: "php" },
2563
+ { test: (n) => n.endsWith(".csproj") || n.endsWith(".sln"), signal: ".NET project", language: "csharp" },
2564
+ { test: (n) => n === "pom.xml", signal: "Maven build", language: "java" },
2565
+ { test: (n) => /^tailwind\.config\.(js|cjs|mjs|ts)$/.test(n), signal: "Tailwind config", platform: "web", tokenTool: "tailwind" },
2566
+ { test: (n) => /^(style-dictionary\.config|sd\.config)\.(js|cjs|mjs|ts|json)$/.test(n), signal: "Style Dictionary config", tokenTool: "style-dictionary" },
2567
+ { test: (n) => n === "index.html" || n === "vite.config.ts" || n === "vite.config.js", signal: "web entry", platform: "web" },
2568
+ { test: (n) => n === "CLAUDE.md" || n === ".claude", signal: "Claude Code", agent: "claude" },
2569
+ { test: (n) => n === ".cursor" || n === ".cursorrules", signal: "Cursor", agent: "cursor" },
2570
+ { test: (n) => n === ".windsurf" || n === ".windsurfrules", signal: "Windsurf", agent: "windsurf" },
2571
+ { test: (n) => n === "GEMINI.md", signal: "Gemini CLI", agent: "gemini" },
2572
+ { test: (n) => n === "AGENTS.md", signal: "AGENTS.md", agent: "agents-md" }
2573
+ ];
2574
+ function detectRepo(cwd) {
2575
+ const platforms = [];
2576
+ const languages = [];
2577
+ const frameworks = [];
2578
+ const tokenTools = [];
2579
+ const agents = [];
2580
+ const evidence = [];
2581
+ let styleDictionaryMajor = null;
2582
+ let names = [];
2583
+ try {
2584
+ names = readdirSync2(cwd).sort();
2585
+ } catch {
2586
+ names = [];
2587
+ }
2588
+ for (const name of names) {
2589
+ for (const rule of FILE_SIGNALS) {
2590
+ if (!rule.test(name)) continue;
2591
+ evidence.push({ signal: rule.signal, file: name });
2592
+ if (rule.platform) platforms.push(rule.platform);
2593
+ if (rule.language) languages.push(rule.language);
2594
+ if (rule.framework) frameworks.push(rule.framework);
2595
+ if (rule.tokenTool) tokenTools.push(rule.tokenTool);
2596
+ if (rule.agent) agents.push(rule.agent);
2597
+ }
2598
+ }
2599
+ if (existsSync5(join5(cwd, ".github", "copilot-instructions.md")) || existsSync5(join5(cwd, ".github", "instructions"))) {
2600
+ evidence.push({ signal: "GitHub Copilot", file: ".github/copilot-instructions.md" });
2601
+ agents.push("copilot");
2602
+ }
2603
+ const pkg = readPackageJson(cwd);
2604
+ if (pkg) {
2605
+ for (const rule of DEP_SIGNALS) {
2606
+ const range = pkg.deps[rule.dep];
2607
+ if (range === void 0) continue;
2608
+ evidence.push({ signal: `${rule.dep} dependency`, file: "package.json" });
2609
+ if (rule.platform) platforms.push(rule.platform);
2610
+ if (rule.framework) frameworks.push(rule.framework);
2611
+ if (rule.tokenTool) tokenTools.push(rule.tokenTool);
2612
+ if (rule.language) languages.push(rule.language);
2613
+ if (rule.dep === "style-dictionary") styleDictionaryMajor = majorOf(range);
2614
+ }
2615
+ if (pkg.deps["react-native"] !== void 0 || pkg.deps.expo !== void 0) platforms.push("android");
2616
+ }
2617
+ const order = (p) => PLATFORMS.indexOf(p);
2618
+ const hostOrder = (a) => AGENT_HOSTS.indexOf(a);
2619
+ return {
2620
+ platforms: uniq(platforms).sort((a, b) => order(a) - order(b)),
2621
+ languages: uniq(languages).sort(),
2622
+ frameworks: uniq(frameworks).sort(),
2623
+ tokenTools: uniq(tokenTools).sort(),
2624
+ agents: uniq(agents).sort((a, b) => hostOrder(a) - hostOrder(b)),
2625
+ styleDictionaryMajor,
2626
+ evidence
2627
+ };
2628
+ }
2629
+ function isPlatform(value) {
2630
+ return PLATFORMS.includes(value);
2631
+ }
2632
+ function isAgentHost(value) {
2633
+ return AGENT_HOSTS.includes(value);
2634
+ }
2635
+
2636
+ // src/skill.ts
2637
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
2638
+ import { dirname as dirname3, join as join6 } from "node:path";
2639
+
2640
+ // src/tools.ts
2641
+ var OK_OR_ERROR = { "0": "success", "1": "usage error, bad key or id, or a network or server failure" };
2642
+ var LOCAL_ONLY = { "0": "success", "1": "no local pull, or a usage error" };
2643
+ var TOOLS = [
2644
+ {
2645
+ name: "setup",
2646
+ usage: "spec-layer setup --id lib_... --key sl_... [--out DIR] [--only foundation|components] [--component NAME]...",
2647
+ summary: "Records the library id, stores the pull key in a gitignored speclayer.local.json, then pulls.",
2648
+ when: "Once, with the command the plugin's Publish screen hands out. Re-run it after the key is rotated.",
2649
+ network: true,
2650
+ needsKey: true,
2651
+ writes: ["speclayer.json", "speclayer.local.json", ".gitignore (one line, when inside a git repo)", "<outDir>/"],
2652
+ exits: OK_OR_ERROR
2653
+ },
2654
+ {
2655
+ name: "init",
2656
+ usage: "spec-layer init --id lib_... [--out DIR] [--only foundation|components] [--component NAME]...",
2657
+ summary: "Writes speclayer.json so later commands need no flags. Stores no key and reaches no server.",
2658
+ when: "A repo that supplies the key from SPEC_LAYER_KEY instead of a stored file.",
2659
+ network: false,
2660
+ needsKey: false,
2661
+ writes: ["speclayer.json"],
2662
+ exits: { "0": "success", "1": "usage error" }
2663
+ },
2664
+ {
2665
+ name: "pull",
2666
+ usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--only foundation|components] [--component NAME]...",
2667
+ summary: "Fetches the published library and writes it under the output directory (default .speclayer/).",
2668
+ when: "After setup, whenever status says the local copy is behind, or after changing the include or dtcg block.",
2669
+ network: true,
2670
+ needsKey: true,
2671
+ writes: ["<outDir>/"],
2672
+ exits: OK_OR_ERROR
2673
+ },
2674
+ {
2675
+ name: "status",
2676
+ usage: "spec-layer status [--id lib_...] [--key sl_...] [--out DIR]",
2677
+ summary: "Checks whether the local pull is current without writing anything.",
2678
+ when: "Before reading the pulled files, or in CI; exit 2 means run pull.",
2679
+ network: true,
2680
+ needsKey: true,
2681
+ writes: [],
2682
+ exits: { "0": "up to date", "1": "usage error, bad key or id, or a network or server failure", "2": "behind, or no local pull yet" }
2683
+ },
2684
+ {
2685
+ name: "list",
2686
+ usage: "spec-layer list [--out DIR]",
2687
+ summary: 'Lists every artifact in the last pull with its file path, or "not written" when the selection skipped it.',
2688
+ when: "To learn which components the library documents and where each file is.",
2689
+ network: false,
2690
+ needsKey: false,
2691
+ writes: [],
2692
+ exits: LOCAL_ONLY
2693
+ },
2694
+ {
2695
+ name: "show",
2696
+ usage: "spec-layer show foundation | component NAME [--canonical] [--out DIR]",
2697
+ summary: "Prints one artifact to stdout: the Foundation DTCG document, or one component's AI YAML; --canonical prints the v5 JSON.",
2698
+ when: "To read one component or the token document without opening files; it pipes cleanly.",
2699
+ network: false,
2700
+ needsKey: false,
2701
+ writes: [],
2702
+ exits: LOCAL_ONLY
2703
+ },
2704
+ {
2705
+ name: "tools",
2706
+ usage: "spec-layer tools [--json]",
2707
+ summary: "Prints this list of commands, with what each reaches and writes.",
2708
+ when: "A coding agent deciding which command to run; --json is stable for machines.",
2709
+ network: false,
2710
+ needsKey: false,
2711
+ writes: [],
2712
+ exits: { "0": "success" }
2713
+ },
2714
+ {
2715
+ name: "skill",
2716
+ usage: "spec-layer skill [--install] [--agent claude|cursor|copilot|windsurf|gemini|agents-md]... [--platform web|ios|android|flutter] [--json] [--out DIR]",
2717
+ summary: "Prints a guide for a coding agent, adapted to this repository's stack and to the last pull; --install writes it where the agent reads instructions.",
2718
+ when: "Right after setup, and again after a pull that adds components or after the codebase changes stack.",
2719
+ network: false,
2720
+ needsKey: false,
2721
+ writes: ["agent instruction files (only with --install; each path is printed)"],
2722
+ exits: { "0": "success", "1": "usage error, or a file could not be written" }
2723
+ }
2724
+ ];
2725
+ var GLOBAL_FLAGS = [
2726
+ { flag: "--api URL", summary: "Override the API origin (default https://api.spec-layer.com). Also SPEC_LAYER_API." },
2727
+ { flag: "--out DIR", summary: "Output directory (default .speclayer, or the outDir in speclayer.json)." }
2728
+ ];
2729
+ var KEY_RESOLUTION = "The pull key resolves from --key, then SPEC_LAYER_KEY, then speclayer.local.json written by setup. No command ever prints it.";
2730
+ function toolsText() {
2731
+ const lines = ["spec-layer commands", ""];
2732
+ for (const tool of TOOLS) {
2733
+ lines.push(tool.usage);
2734
+ lines.push(` ${tool.summary}`);
2735
+ lines.push(` When: ${tool.when}`);
2736
+ lines.push(` Network: ${tool.network ? "yes" : "no"}. Key: ${tool.needsKey ? "required" : "not needed"}. Writes: ${tool.writes.length ? tool.writes.join(", ") : "nothing"}.`);
2737
+ lines.push(` Exits: ${Object.entries(tool.exits).map(([code2, meaning]) => `${code2} ${meaning}`).join("; ")}.`);
2738
+ lines.push("");
2739
+ }
2740
+ lines.push("Flags every command accepts where they apply:");
2741
+ for (const f of GLOBAL_FLAGS) lines.push(` ${f.flag} ${f.summary}`);
2742
+ lines.push("");
2743
+ lines.push(KEY_RESOLUTION);
2744
+ return lines.join("\n");
2745
+ }
2746
+ function toolsJson(version) {
2747
+ return `${JSON.stringify({
2748
+ cli: "spec-layer",
2749
+ version,
2750
+ tools: TOOLS.map((t) => ({ ...t })),
2751
+ flags: GLOBAL_FLAGS,
2752
+ key_resolution: KEY_RESOLUTION
2753
+ }, null, 2)}
2754
+ `;
2755
+ }
2756
+
2757
+ // src/skill.ts
2758
+ var RESERVED = /* @__PURE__ */ new Set(["resolver.json", "spec-layer.meta.json", "report.json"]);
2759
+ function countNumberTokens(tree) {
2760
+ if (typeof tree !== "object" || tree === null || Array.isArray(tree)) return 0;
2761
+ const record = tree;
2762
+ if (record.$type === "number" && "$value" in record) return 1;
2763
+ let n = 0;
2764
+ for (const [key, value] of Object.entries(record)) {
2765
+ if (key.startsWith("$")) continue;
2766
+ n += countNumberTokens(value);
2767
+ }
2768
+ return n;
2769
+ }
2770
+ function readJson(path) {
2771
+ if (!existsSync6(path)) return null;
2772
+ try {
2773
+ return JSON.parse(readFileSync6(path, "utf8"));
2774
+ } catch {
2775
+ return null;
2776
+ }
2777
+ }
2778
+ function summarizePull(cwd, outDir, manifest) {
2779
+ if (!manifest) return null;
2780
+ const absOut = join6(cwd, outDir);
2781
+ const components = manifest.artifacts.filter((a) => a.kind === "component").map((a) => ({ name: a.name, path: a.aiPath ? `${outDir}/${a.aiPath}` : null }));
2782
+ const foundationEntry = manifest.artifacts.find((a) => a.kind === "foundation") ?? null;
2783
+ let foundation = null;
2784
+ if (foundationEntry) {
2785
+ const tokensDir = join6(absOut, "tokens");
2786
+ const resolver = readJson(join6(tokensDir, "resolver.json"));
2787
+ const report = readJson(join6(tokensDir, "report.json"));
2788
+ let tokenFiles = [];
2789
+ try {
2790
+ tokenFiles = readdirSync3(tokensDir).filter((f) => f.endsWith(".json") && !RESERVED.has(f)).sort();
2791
+ } catch {
2792
+ tokenFiles = [];
2793
+ }
2794
+ let unitlessNumbers = 0;
2795
+ for (const file of tokenFiles) {
2796
+ if (file.startsWith("styles.")) continue;
2797
+ unitlessNumbers += countNumberTokens(readJson(join6(tokensDir, file)));
2798
+ }
2799
+ const reportCounts = {};
2800
+ if (Array.isArray(report)) {
2801
+ for (const entry2 of report) {
2802
+ if (typeof entry2?.code === "string") reportCounts[entry2.code] = (reportCounts[entry2.code] ?? 0) + 1;
2803
+ }
2804
+ }
2805
+ foundation = {
2806
+ written: foundationEntry.aiPath !== null && resolver !== null,
2807
+ sets: resolver ? Object.keys(resolver.sets ?? {}) : [],
2808
+ modifiers: resolver ? Object.entries(resolver.modifiers ?? {}).map(([name, m]) => ({
2809
+ name,
2810
+ contexts: Object.keys(m.contexts ?? {}),
2811
+ default: m.default ?? null
2812
+ })) : [],
2813
+ tokenFiles,
2814
+ unitlessNumbers,
2815
+ reportCounts
2816
+ };
2817
+ }
2818
+ return {
2819
+ outDir,
2820
+ libraryId: manifest.libraryId,
2821
+ publishedAt: manifest.publishedAt,
2822
+ pluginVersion: manifest.pluginVersion,
2823
+ components,
2824
+ foundation
2825
+ };
2826
+ }
2827
+ var code = (s) => `\`${s}\``;
2828
+ function stackSection(input) {
2829
+ const { profile, platforms, platformSource, pull } = input;
2830
+ const lines = ["## This codebase", ""];
2831
+ if (profile.evidence.length === 0) {
2832
+ lines.push(
2833
+ "Nothing at the root of this directory identified a language, framework, or platform. "
2834
+ );
2835
+ } else {
2836
+ lines.push("Detected from the repository root (the file that carries each signal is named, and nothing deeper was read):", "");
2837
+ for (const e of profile.evidence) lines.push(`- ${e.signal} (${code(e.file)})`);
2838
+ lines.push("");
2839
+ const facts = [];
2840
+ if (profile.languages.length) facts.push(`Languages: ${profile.languages.join(", ")}.`);
2841
+ if (profile.frameworks.length) facts.push(`Frameworks: ${profile.frameworks.join(", ")}.`);
2842
+ if (profile.tokenTools.length) facts.push(`Token tooling: ${profile.tokenTools.join(", ")}.`);
2843
+ if (facts.length) lines.push(facts.join(" "), "");
2844
+ }
2845
+ if (platformSource === "none") {
2846
+ lines.push(
2847
+ `No target platform was detected, so the token advice below is generic. Re-run ${code("spec-layer skill --platform web|ios|android|flutter")} to write it for a platform, or pass ` + code("--install") + " with the same flag to update the installed copy.",
2848
+ ""
2849
+ );
2850
+ } else {
2851
+ const label = platformSource === "flag" ? "chosen with --platform" : "detected";
2852
+ lines.push(`Target platform${platforms.length > 1 ? "s" : ""} (${label}): ${platforms.join(", ")}.`, "");
2853
+ }
2854
+ for (const platform of platforms) {
2855
+ const key = CODE_SYNTAX_KEY[platform];
2856
+ const tokensDir = `${input.outDir}/tokens/`;
2857
+ if (platform === "web") {
2858
+ lines.push("### Web", "");
2859
+ lines.push(
2860
+ `Token identifiers for code live in ${code(`${tokensDir}spec-layer.meta.json`)} under each token's ${code("code_syntax.WEB")}, when the designer declared one in Figma. Use that identifier as the CSS custom property or theme key. When a token has no WEB entry, derive nothing: use the DTCG path as it appears in the token file (for example ${code("{Collection.group.name}")}) and say in your change that the code name is not declared in Figma.`
2861
+ );
2862
+ lines.push("");
2863
+ if (profile.tokenTools.includes("tailwind")) {
2864
+ lines.push(
2865
+ `Tailwind is present (${code("tailwindcss")}). Map DTCG ${code("color")} tokens to the theme's color scale and ${code("dimension")} tokens to spacing, radius, or font size by the collection and group they sit in. Keep the mapping in one place and reference token paths, not copied values, so a republish moves the code with it.`,
2866
+ ""
2867
+ );
2868
+ }
2869
+ if (profile.tokenTools.includes("style-dictionary")) {
2870
+ const major = profile.styleDictionaryMajor;
2871
+ lines.push(
2872
+ `Style Dictionary is present${major !== null ? ` (major version ${major} in package.json)` : ""}. Point it at ${code(tokensDir)} and load the files ${code("resolver.json")} names for the mode you build. Exclude ${code("spec-layer.meta.json")} and ${code("report.json")} from token globs; they are not token files.`
2873
+ );
2874
+ if (major !== null && major < 5) {
2875
+ lines.push(
2876
+ "",
2877
+ `Style Dictionary ${major} reads the string value forms, not the 2025.10 object forms. Set ${code('"dtcg": { "values": "legacy" }')} in ${code("speclayer.json")} and run ${code("spec-layer pull")}; the change re-projects tokens/ without a republish.`
2878
+ );
2879
+ }
2880
+ lines.push("");
2881
+ }
2882
+ } else if (platform === "ios") {
2883
+ lines.push("### iOS", "");
2884
+ lines.push(
2885
+ `Token identifiers for code live in ${code(`${tokensDir}spec-layer.meta.json`)} under each token's ${code(`code_syntax.${key}`)}, when the designer declared one in Figma. Use that as the Swift symbol. Colors arrive as hex strings or DTCG color objects with RGBA components; dimensions carry an explicit px or rem unit. A modifier with more than one context (for example a light and a dark mode) maps to a color-scheme-dependent value; a set without modes is a constant. Do not invent a dark variant for a collection that has one mode.`,
2886
+ ""
2887
+ );
2888
+ } else if (platform === "android") {
2889
+ lines.push("### Android", "");
2890
+ lines.push(
2891
+ `Token identifiers for code live in ${code(`${tokensDir}spec-layer.meta.json`)} under each token's ${code(`code_syntax.${key}`)}, when the designer declared one in Figma. Use that as the Kotlin or resource name. Dimensions carry an explicit px or rem unit and no density assumption; a value is only dp when your own convention says so, and that convention belongs in your code, not in the token file. Modes map to resource qualifiers or a Compose theme switch.`,
2892
+ ""
2893
+ );
2894
+ } else {
2895
+ lines.push("### Flutter", "");
2896
+ lines.push(
2897
+ `Figma declares no code syntax for Flutter, so no identifier is provided for Dart. Name symbols after the DTCG path (for example ${code("Collection.group.name")} becomes a nested class or a camelCase constant) and keep the path in a comment so the source token stays traceable. Modes map to theme variants.`,
2898
+ ""
2899
+ );
2900
+ }
2901
+ }
2902
+ if (pull?.foundation && pull.foundation.unitlessNumbers > 0) {
2903
+ const n = pull.foundation.unitlessNumbers;
2904
+ lines.push(
2905
+ `${n} token${n === 1 ? " is" : "s are"} exported as ${code('$type: "number"')} because the Figma scopes state no unit. If your code needs them as px or rem, declare it in ${code("speclayer.json")}: ${code('"dtcg": { "units": { "<Collection>/<name glob>": "px" } }')}, then run ${code("spec-layer pull")}. Nothing is inferred from a name; an override that contradicts a stated scope is ignored and listed in ${code("report.json")}.`,
2906
+ ""
2907
+ );
2908
+ }
2909
+ return lines;
2910
+ }
2911
+ function pullSection(input) {
2912
+ const { pull, outDir } = input;
2913
+ const lines = ["## What is on disk", ""];
2914
+ if (!pull) {
2915
+ lines.push(
2916
+ `No pull has been made in this directory yet, so nothing under ${code(outDir + "/")} can be described. Run ${code("npx spec-layer pull")} (or the setup command from the plugin if there is no ${code("speclayer.json")}), then ${code("npx spec-layer skill --install")} again to list the components and token collections here.`,
2917
+ ""
2918
+ );
2919
+ return lines;
2920
+ }
2921
+ lines.push(
2922
+ `Library ${code(pull.libraryId)}, published ${pull.publishedAt}${pull.pluginVersion ? ` by plugin ${pull.pluginVersion}` : ""}. Run ${code("npx spec-layer status")} first; exit code 2 means a newer publish exists and ${code("npx spec-layer pull")} fetches it.`,
2923
+ ""
2924
+ );
2925
+ lines.push(`- ${code(`${outDir}/manifest.json`)}: every artifact with its content hash and file path.`);
2926
+ lines.push(`- ${code(`${outDir}/bundle.json`)}: the whole published library, including the canonical v5 JSON of every artifact.`);
2927
+ if (pull.foundation) {
2928
+ if (pull.foundation.written) {
2929
+ lines.push(`- ${code(`${outDir}/tokens/`)}: the Foundation as Design Tokens Format Module 2025.10 files.`);
2930
+ lines.push(` - ${code("resolver.json")}: sets, modifiers, and resolution order. Start here.`);
2931
+ lines.push(` - ${code("spec-layer.meta.json")}: Figma ids, scopes, publication, and ${code("code_syntax")} per DTCG path.`);
2932
+ lines.push(` - ${code("report.json")}: what the format could not express, with reasons. Never fill these gaps with a guess.`);
2933
+ for (const f of pull.foundation.tokenFiles) lines.push(` - ${code(f)}`);
2934
+ } else {
2935
+ lines.push(`- Foundation: present in the library but not written, because the selection excludes it. ${code("spec-layer show foundation")} still prints it.`);
2936
+ }
2937
+ } else {
2938
+ lines.push("- This library has no Foundation, so there is no tokens/ directory.");
2939
+ }
2940
+ lines.push(`- ${code(`${outDir}/ai/components/`)}: one YAML per component.`);
2941
+ lines.push("");
2942
+ if (pull.foundation && (pull.foundation.sets.length || pull.foundation.modifiers.length)) {
2943
+ lines.push("### Token collections", "");
2944
+ for (const set of pull.foundation.sets) lines.push(`- ${code(set)}: one mode, always applied.`);
2945
+ for (const m of pull.foundation.modifiers) {
2946
+ lines.push(`- ${code(m.name)}: modes ${m.contexts.map(code).join(", ")}${m.default ? `, default ${code(m.default)}` : ""}.`);
2947
+ }
2948
+ lines.push("");
2949
+ const counts = Object.entries(pull.foundation.reportCounts);
2950
+ if (counts.length) {
2951
+ lines.push(
2952
+ `${code("report.json")} lists ${counts.map(([c, n]) => `${n} ${code(c)}`).join(", ")}. Read it before assuming a token is missing.`,
2953
+ ""
2954
+ );
2955
+ }
2956
+ }
2957
+ lines.push("### Components", "");
2958
+ if (pull.components.length === 0) {
2959
+ lines.push("The library documents no components.", "");
2960
+ } else {
2961
+ for (const c of pull.components) {
2962
+ lines.push(c.path ? `- ${c.name}: ${code(c.path)}` : `- ${c.name}: not written (excluded by the selection). ${code(`spec-layer show component "${c.name}"`)} prints it.`);
2963
+ }
2964
+ lines.push("");
2965
+ }
2966
+ return lines;
2967
+ }
2968
+ function commandsSection() {
2969
+ const lines = ["## Commands", ""];
2970
+ const cell = (s) => s.replace(/\|/g, "\\|");
2971
+ lines.push("| Command | What it does | When | Network | Key | Writes |", "|---|---|---|---|---|---|");
2972
+ for (const t of TOOLS) {
2973
+ lines.push(`| ${code(cell(t.usage))} | ${cell(t.summary)} | ${cell(t.when)} | ${t.network ? "yes" : "no"} | ${t.needsKey ? "required" : "no"} | ${t.writes.length ? t.writes.map((w) => code(cell(w))).join(", ") : "nothing"} |`);
2974
+ }
2975
+ lines.push("");
2976
+ lines.push("Exit codes:", "");
2977
+ for (const t of TOOLS) {
2978
+ lines.push(`- ${code(t.name)}: ${Object.entries(t.exits).map(([c, m]) => `${c} = ${m}`).join("; ")}.`);
2979
+ }
2980
+ lines.push("");
2981
+ for (const f of GLOBAL_FLAGS) lines.push(`- ${code(f.flag)}: ${f.summary}`);
2982
+ lines.push("", KEY_RESOLUTION, "");
2983
+ lines.push(`Run ${code("npx --yes spec-layer <command>")} in an unattended session so npx does not stop to ask before downloading the package. ${code("spec-layer tools --json")} prints this table for machines.`, "");
2984
+ return lines;
2985
+ }
2986
+ function buildSkillGuide(input) {
2987
+ const { outDir } = input;
2988
+ const lines = [];
2989
+ lines.push("# Spec Layer: design-system context for this repository", "");
2990
+ lines.push(
2991
+ `The Spec Layer Figma plugin publishes a design system's components, variables, and styles as data. The ${code("spec-layer")} CLI (version ${input.version}) pulls that data into this repository under ${code(outDir + "/")}. Everything in those files is extracted deterministically from Figma and validated against a published schema; no model wrote any of it. Treat it as the source of truth for what the design system contains, and treat anything it does not state as unknown rather than as something to infer.`,
2992
+ ""
2993
+ );
2994
+ lines.push("## How to use it", "");
2995
+ lines.push(`1. Run ${code("npx spec-layer status")}. Exit 0 means the local copy is current; exit 2 means run ${code("npx spec-layer pull")} first.`);
2996
+ lines.push(`2. Building or changing a component: read its YAML under ${code(`${outDir}/ai/components/`)}, or ${code("npx spec-layer show component NAME")}. ${code("api")} gives variants, states, booleans, and slots; ${code("anatomy")} names the parts; ${code("references.bindings")} says which token each part's property uses and under which ${code("when")} conditions; ${code("unbound")} lists values that are hardcoded in Figma.`);
2997
+ lines.push(`3. Working with colors, spacing, type, or effects: start at ${code(`${outDir}/tokens/resolver.json`)}, load the set and mode files it names, and look up ${code("code_syntax")} in ${code("spec-layer.meta.json")} for the name the designer declared for your platform.`);
2998
+ lines.push(`4. Reference tokens by name in code; never paste a resolved value where a token exists. A value the design system does not define is not a token: say so in your change rather than adding one.`);
2999
+ lines.push(`5. An ${code("unbound")} entry is design debt reported from Figma. Do not silently promote it to a token; keep the literal and note that Figma has no binding for it.`);
3000
+ lines.push(`6. Never edit files under ${code(outDir + "/")}: the next pull replaces the whole directory. Configuration lives in ${code("speclayer.json")}. Never commit ${code(CREDENTIALS_NAME)}, and never print or copy the pull key.`);
3001
+ lines.push("");
3002
+ lines.push(...pullSection(input));
3003
+ lines.push(...stackSection(input));
3004
+ lines.push(...commandsSection());
3005
+ lines.push(`Generated by ${code("spec-layer skill")}. Re-run ${code("npx spec-layer skill --install")} after a pull that adds components or when the codebase changes stack; the file is replaced, not appended.`);
3006
+ return `${lines.join("\n")}
3007
+ `;
3008
+ }
3009
+ var SKILL_DESCRIPTION = "Use the design-system context the Spec Layer Figma plugin published into this repository: component variants, states, anatomy, token bindings, and design tokens. Read this before building or changing UI, using tokens, or running the spec-layer CLI.";
3010
+ function installTarget(host) {
3011
+ switch (host) {
3012
+ case "claude":
3013
+ return { host, path: ".claude/skills/spec-layer/SKILL.md", mode: "file" };
3014
+ case "cursor":
3015
+ return { host, path: ".cursor/rules/spec-layer.mdc", mode: "file" };
3016
+ case "copilot":
3017
+ return { host, path: ".github/instructions/spec-layer.instructions.md", mode: "file" };
3018
+ case "windsurf":
3019
+ return { host, path: ".windsurf/rules/spec-layer.md", mode: "file" };
3020
+ case "gemini":
3021
+ return { host, path: "GEMINI.md", mode: "block" };
3022
+ case "agents-md":
3023
+ return { host, path: "AGENTS.md", mode: "block" };
3024
+ }
3025
+ }
3026
+ function renderForHost(host, guide) {
3027
+ const yamlString = (s) => JSON.stringify(s);
3028
+ switch (host) {
3029
+ case "claude":
3030
+ return `---
3031
+ name: spec-layer
3032
+ description: ${yamlString(SKILL_DESCRIPTION)}
3033
+ ---
3034
+
3035
+ ${guide}`;
3036
+ case "cursor":
3037
+ return `---
3038
+ description: ${yamlString(SKILL_DESCRIPTION)}
3039
+ alwaysApply: false
3040
+ ---
3041
+
3042
+ ${guide}`;
3043
+ case "copilot":
3044
+ return `---
3045
+ applyTo: "**"
3046
+ ---
3047
+
3048
+ ${guide}`;
3049
+ case "windsurf":
3050
+ return `---
3051
+ trigger: model_decision
3052
+ description: ${yamlString(SKILL_DESCRIPTION)}
3053
+ ---
3054
+
3055
+ ${guide}`;
3056
+ case "gemini":
3057
+ case "agents-md":
3058
+ return guide;
3059
+ }
3060
+ }
3061
+ var BLOCK_BEGIN = "<!-- spec-layer:begin -->";
3062
+ var BLOCK_END = "<!-- spec-layer:end -->";
3063
+ function upsertBlock(existing, guide) {
3064
+ const block = `${BLOCK_BEGIN}
3065
+ ${guide.trimEnd()}
3066
+ ${BLOCK_END}
3067
+ `;
3068
+ if (existing === null) return block;
3069
+ const begin = existing.indexOf(BLOCK_BEGIN);
3070
+ const end = existing.indexOf(BLOCK_END);
3071
+ if (begin !== -1 && end !== -1 && end > begin) {
3072
+ const after = existing.slice(end + BLOCK_END.length).replace(/^\n/, "");
3073
+ return `${existing.slice(0, begin)}${block}${after}`;
3074
+ }
3075
+ const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
3076
+ return `${existing}${sep}${block}`;
3077
+ }
3078
+ function installSkill(cwd, host, guide) {
3079
+ const target = installTarget(host);
3080
+ const abs = join6(cwd, target.path);
3081
+ const existing = existsSync6(abs) ? readFileSync6(abs, "utf8") : null;
3082
+ const next = target.mode === "file" ? renderForHost(host, guide) : upsertBlock(existing, renderForHost(host, guide));
3083
+ if (existing === next) return { path: target.path, result: "unchanged" };
3084
+ mkdirSync2(dirname3(abs), { recursive: true });
3085
+ writeFileSync5(abs, next);
3086
+ return { path: target.path, result: existing === null ? "created" : "updated" };
3087
+ }
3088
+
3089
+ // src/version.ts
3090
+ import { readFileSync as readFileSync7 } from "node:fs";
3091
+ function cliVersion() {
3092
+ try {
3093
+ const parsed = JSON.parse(readFileSync7(new URL("../package.json", import.meta.url), "utf8"));
3094
+ return typeof parsed.version === "string" ? parsed.version : "unknown";
3095
+ } catch {
3096
+ return "unknown";
3097
+ }
3098
+ }
3099
+
2487
3100
  // src/commands.ts
2488
3101
  var NO_LOCAL_PULL = "No local pull found. Run spec-layer pull.";
2489
3102
  function manifestReader() {
@@ -2544,7 +3157,7 @@ function resolved(cwd, flags, env, io2, manifestAt) {
2544
3157
  }
2545
3158
  function resolvedOutDir(cwd, flags, io2) {
2546
3159
  try {
2547
- return join5(cwd, flags.out ?? readConfig(cwd)?.outDir ?? DEFAULT_OUT_DIR);
3160
+ return join7(cwd, flags.out ?? readConfig(cwd)?.outDir ?? DEFAULT_OUT_DIR);
2548
3161
  } catch (err) {
2549
3162
  io2.err(errorText(err));
2550
3163
  return null;
@@ -2627,7 +3240,14 @@ git rm --cached ${ignored.line}`);
2627
3240
  }
2628
3241
  const { replaced } = writeCredentials(cwd, { libraryId: flags.id, key });
2629
3242
  io2.out(replaced ? `Replaced the stored key in ${CREDENTIALS_NAME}.` : `Stored the pull key in ${CREDENTIALS_NAME}.`);
2630
- return runPull(cwd, { ...flags, key }, env, io2, fetcher);
3243
+ const code2 = await runPull(cwd, { ...flags, key }, env, io2, fetcher);
3244
+ if (code2 !== 0) return code2;
3245
+ const hosts = detectRepo(cwd).agents;
3246
+ io2.out("");
3247
+ io2.out("Next step for a coding agent: npx spec-layer skill --install");
3248
+ io2.out(hosts.length > 0 ? `That writes a guide to the pulled files, adapted to this codebase, to ${hosts.map((h) => installTarget(h).path).join(", ")}.` : `That writes a guide to the pulled files, adapted to this codebase, into ${installTarget("agents-md").path}; --agent ${AGENT_HOSTS.join("|")} chooses where.`);
3249
+ io2.out("spec-layer skill prints the same guide; spec-layer tools lists every command.");
3250
+ return 0;
2631
3251
  }
2632
3252
  async function runPull(cwd, flags, env, io2, fetcher) {
2633
3253
  const manifestAt = manifestReader();
@@ -2640,7 +3260,7 @@ async function runPull(cwd, flags, env, io2, fetcher) {
2640
3260
  io2.err(errorText(err));
2641
3261
  return 1;
2642
3262
  }
2643
- const manifest = manifestAt(join5(cwd, opts.outDir));
3263
+ const manifest = manifestAt(join7(cwd, opts.outDir));
2644
3264
  const etag = manifest && sameOutput(
2645
3265
  { selection: manifest.selection ?? DEFAULT_SELECTION, dtcg: manifest.dtcg },
2646
3266
  { selection, dtcg: opts.dtcg }
@@ -2665,7 +3285,7 @@ async function runPull(cwd, flags, env, io2, fetcher) {
2665
3285
  const bundle = parseBundle(result.raw);
2666
3286
  const selected = selectComponents(bundle, selection);
2667
3287
  written = writeBundleFiles({
2668
- outDir: join5(cwd, opts.outDir),
3288
+ outDir: join7(cwd, opts.outDir),
2669
3289
  cwd,
2670
3290
  raw: result.raw,
2671
3291
  bundle,
@@ -2689,7 +3309,7 @@ async function runStatus(cwd, flags, env, io2, fetcher) {
2689
3309
  const manifestAt = manifestReader();
2690
3310
  const opts = resolved(cwd, flags, env, io2, manifestAt);
2691
3311
  if (!opts) return 1;
2692
- const manifest = manifestAt(join5(cwd, opts.outDir));
3312
+ const manifest = manifestAt(join7(cwd, opts.outDir));
2693
3313
  if (!manifest) {
2694
3314
  io2.err(NO_LOCAL_PULL);
2695
3315
  return 2;
@@ -2775,6 +3395,90 @@ Available: ${available || "none"}.`);
2775
3395
  ` : entry2.ai);
2776
3396
  return 0;
2777
3397
  }
3398
+ function runTools(flags, io2) {
3399
+ if (flags.json) io2.write(toolsJson(cliVersion()));
3400
+ else io2.out(toolsText());
3401
+ return 0;
3402
+ }
3403
+ function collectSkillInput(cwd, flags, io2) {
3404
+ let config = null;
3405
+ try {
3406
+ config = readConfig(cwd);
3407
+ } catch (err) {
3408
+ io2.err(errorText(err));
3409
+ return null;
3410
+ }
3411
+ const outDir = flags.out ?? config?.outDir ?? DEFAULT_OUT_DIR;
3412
+ const profile = detectRepo(cwd);
3413
+ let platforms;
3414
+ let platformSource;
3415
+ if (flags.platform !== void 0) {
3416
+ if (!isPlatform(flags.platform)) {
3417
+ io2.err(`--platform takes ${PLATFORMS.join(", ")}, not "${flags.platform}".`);
3418
+ return null;
3419
+ }
3420
+ platforms = [flags.platform];
3421
+ platformSource = "flag";
3422
+ } else {
3423
+ platforms = profile.platforms;
3424
+ platformSource = platforms.length > 0 ? "detected" : "none";
3425
+ }
3426
+ const pull = summarizePull(cwd, outDir, readManifest(join7(cwd, outDir)));
3427
+ return { profile, platforms, platformSource, outDir, config, pull, version: cliVersion() };
3428
+ }
3429
+ function skillHosts(flags, input, io2) {
3430
+ const named = flags.agent ?? [];
3431
+ if (named.length > 0) {
3432
+ const hosts = [];
3433
+ for (const value of named) {
3434
+ if (!isAgentHost(value)) {
3435
+ io2.err(`--agent takes ${AGENT_HOSTS.join(", ")}, not "${value}".`);
3436
+ return null;
3437
+ }
3438
+ if (!hosts.includes(value)) hosts.push(value);
3439
+ }
3440
+ return hosts;
3441
+ }
3442
+ return input.profile.agents.length > 0 ? input.profile.agents : ["agents-md"];
3443
+ }
3444
+ function runSkill(cwd, flags, io2) {
3445
+ const input = collectSkillInput(cwd, flags, io2);
3446
+ if (!input) return 1;
3447
+ const hosts = skillHosts(flags, input, io2);
3448
+ if (!hosts) return 1;
3449
+ if (flags.json) {
3450
+ io2.write(`${JSON.stringify({
3451
+ cli_version: input.version,
3452
+ detected: input.profile,
3453
+ platforms: input.platforms,
3454
+ platform_source: input.platformSource,
3455
+ pull: input.pull,
3456
+ install_targets: hosts.map((h) => installTarget(h))
3457
+ }, null, 2)}
3458
+ `);
3459
+ return 0;
3460
+ }
3461
+ const guide = buildSkillGuide(input);
3462
+ if (!flags.install) {
3463
+ io2.write(guide);
3464
+ return 0;
3465
+ }
3466
+ const chosen = flags.agent && flags.agent.length > 0 ? "named with --agent" : input.profile.agents.length > 0 ? "detected in this repository" : "the default when no agent is detected";
3467
+ for (const host of hosts) {
3468
+ let outcome;
3469
+ try {
3470
+ outcome = installSkill(cwd, host, guide);
3471
+ } catch (err) {
3472
+ io2.err(`Could not write ${installTarget(host).path}: ${errorText(err)}`);
3473
+ return 1;
3474
+ }
3475
+ const verb = outcome.result === "created" ? "Wrote" : outcome.result === "updated" ? "Updated" : "Unchanged:";
3476
+ io2.out(`${verb} ${outcome.path} (${host}, ${chosen}).`);
3477
+ }
3478
+ if (!input.pull) io2.out(`No local pull yet, so the guide lists no components. Run spec-layer pull, then spec-layer skill --install again.`);
3479
+ if (input.platformSource === "none") io2.out(`No target platform detected. Pass --platform ${PLATFORMS.join("|")} to write platform-specific token advice.`);
3480
+ return 0;
3481
+ }
2778
3482
 
2779
3483
  // src/cli.ts
2780
3484
  var USAGE = `spec-layer <command>
@@ -2789,6 +3493,10 @@ Commands:
2789
3493
  list list every artifact in the last pull
2790
3494
  show foundation | component NAME [--canonical]
2791
3495
  print one artifact (foundation: the DTCG document; component: its AI YAML; --canonical for JSON)
3496
+ tools [--json] list every command with what it reaches and writes
3497
+ skill [--install] [--agent HOST]... [--platform P] [--json]
3498
+ print a guide for a coding agent, adapted to this repo and the last pull;
3499
+ --install writes it for claude, cursor, copilot, windsurf, gemini, or agents-md
2792
3500
 
2793
3501
  Selection (setup, pull and init; flags replace the include block in speclayer.json):
2794
3502
  --only foundation | components write just the foundation, or just components
@@ -2817,7 +3525,11 @@ async function main() {
2817
3525
  api: { type: "string" },
2818
3526
  only: { type: "string" },
2819
3527
  component: { type: "string", multiple: true },
2820
- canonical: { type: "boolean" }
3528
+ canonical: { type: "boolean" },
3529
+ json: { type: "boolean" },
3530
+ install: { type: "boolean" },
3531
+ agent: { type: "string", multiple: true },
3532
+ platform: { type: "string" }
2821
3533
  }
2822
3534
  }));
2823
3535
  } catch {
@@ -2833,6 +3545,8 @@ async function main() {
2833
3545
  if (command === "status") return await runStatus(cwd, values, process.env, io);
2834
3546
  if (command === "list") return runList(cwd, values, io);
2835
3547
  if (command === "show") return runShow(cwd, values, positionals.slice(1), io);
3548
+ if (command === "tools") return runTools(values, io);
3549
+ if (command === "skill") return runSkill(cwd, values, io);
2836
3550
  io.err(USAGE);
2837
3551
  return 1;
2838
3552
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-layer",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Pull design-system context published by the Spec Layer Figma plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "git+https://github.com/SamsonHD/spec-layer.git",
26
+ "url": "git+https://github.com/sandroleks/spec-layer.git",
27
27
  "directory": "packages/cli"
28
28
  },
29
29
  "homepage": "https://spec-layer.com",