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.
- package/README.md +69 -1
- package/dist/cli.js +758 -44
- 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,
|
|
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
|
|
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
|
-
|
|
279
|
-
if (
|
|
280
|
-
blocks2[i >>> 2] |=
|
|
281
|
-
} else if (
|
|
282
|
-
blocks2[i >>> 2] |= (192 |
|
|
283
|
-
blocks2[i >>> 2] |= (128 |
|
|
284
|
-
} else if (
|
|
285
|
-
blocks2[i >>> 2] |= (224 |
|
|
286
|
-
blocks2[i >>> 2] |= (128 |
|
|
287
|
-
blocks2[i >>> 2] |= (128 |
|
|
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
|
-
|
|
290
|
-
blocks2[i >>> 2] |= (240 |
|
|
291
|
-
blocks2[i >>> 2] |= (128 |
|
|
292
|
-
blocks2[i >>> 2] |= (128 |
|
|
293
|
-
blocks2[i >>> 2] |= (128 |
|
|
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,
|
|
475
|
+
var bytes = [], length = key.length, index = 0, code2;
|
|
476
476
|
for (i = 0; i < length; ++i) {
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
bytes[index++] =
|
|
480
|
-
} else if (
|
|
481
|
-
bytes[index++] = 192 |
|
|
482
|
-
bytes[index++] = 128 |
|
|
483
|
-
} else if (
|
|
484
|
-
bytes[index++] = 224 |
|
|
485
|
-
bytes[index++] = 128 |
|
|
486
|
-
bytes[index++] = 128 |
|
|
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
|
-
|
|
489
|
-
bytes[index++] = 240 |
|
|
490
|
-
bytes[index++] = 128 |
|
|
491
|
-
bytes[index++] = 128 |
|
|
492
|
-
bytes[index++] = 128 |
|
|
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
|
|
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(
|
|
645
|
+
function diagnostic(code2, fields) {
|
|
646
646
|
return {
|
|
647
|
-
code,
|
|
648
|
-
severity: DEFAULT_SEVERITY[
|
|
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(
|
|
2042
|
+
constructor(code2, message) {
|
|
2043
2043
|
super(message);
|
|
2044
2044
|
this.name = "LibraryBundleError";
|
|
2045
|
-
this.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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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.
|
|
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/
|
|
26
|
+
"url": "git+https://github.com/sandroleks/spec-layer.git",
|
|
27
27
|
"directory": "packages/cli"
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://spec-layer.com",
|