spec-layer 0.1.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 +75 -0
- package/dist/cli.js +346 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# spec-layer
|
|
2
|
+
|
|
3
|
+
Pull design-system context published by the [Spec Layer](https://spec-layer.com)
|
|
4
|
+
Figma plugin into your repository, so a coding agent reads the same component
|
|
5
|
+
and token facts your designers see in Figma.
|
|
6
|
+
|
|
7
|
+
This CLI is delivery only. It never talks to Figma, never re-derives anything,
|
|
8
|
+
and has zero runtime dependencies: it fetches the bundle the plugin published
|
|
9
|
+
and writes it to disk.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
After publishing a library from the plugin's Library screen, it shows a setup
|
|
14
|
+
command. Run it in your repository:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
SPEC_LAYER_KEY=sl_... npx spec-layer pull --id lib_...
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That writes `.speclayer/` and is enough on its own. To avoid repeating the
|
|
21
|
+
library id, record it once:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx spec-layer init --id lib_...
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
| Command | What it does |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `init --id lib_... [--out DIR]` | Writes `speclayer.json` so later commands need no flags. |
|
|
32
|
+
| `pull [--id lib_...] [--key sl_...]` | Fetches the library and writes it into `DIR` (default `.speclayer`). |
|
|
33
|
+
| `status [--id lib_...] [--key sl_...]` | Checks freshness without writing. Exits `2` when the local copy is behind. |
|
|
34
|
+
|
|
35
|
+
`--api URL` overrides the API origin (default `https://api.spec-layer.com`).
|
|
36
|
+
|
|
37
|
+
## The pull key
|
|
38
|
+
|
|
39
|
+
Every command reads the pull key from `SPEC_LAYER_KEY` or `--key`. It is never
|
|
40
|
+
written to disk, and `speclayer.json` never contains it. Treat it as a secret:
|
|
41
|
+
it grants read access to the published bundle. If it leaks, rotate it from the
|
|
42
|
+
plugin's Library screen, which invalidates the old key immediately.
|
|
43
|
+
|
|
44
|
+
## What `pull` writes
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
.speclayer/
|
|
48
|
+
bundle.json the published bundle, verbatim
|
|
49
|
+
manifest.json every artifact indexed by content hash and ai path
|
|
50
|
+
ai/foundation.yaml tokens, styles, and modes (when the library has a Foundation)
|
|
51
|
+
ai/components/<name>.yaml one file per documented component
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Point your agent at `.speclayer/ai/`. The YAML there is the same compact form
|
|
55
|
+
the plugin's **Copy for AI** puts on your clipboard; `bundle.json` additionally
|
|
56
|
+
holds the full canonical artifacts if you need them.
|
|
57
|
+
|
|
58
|
+
Writes stage into `.speclayer.partial` and rename into place, so an
|
|
59
|
+
interrupted pull never leaves a half-written directory.
|
|
60
|
+
|
|
61
|
+
## Exit codes
|
|
62
|
+
|
|
63
|
+
| Code | Meaning |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `0` | Success, or `status` found the local copy up to date. |
|
|
66
|
+
| `1` | Usage error, bad key or id, or a network or server failure. |
|
|
67
|
+
| `2` | `status` only: the local copy is behind, or no local pull exists yet. |
|
|
68
|
+
|
|
69
|
+
`status` is safe in CI: it writes nothing, and exit `2` is the signal to run
|
|
70
|
+
`pull`.
|
|
71
|
+
|
|
72
|
+
## Requirements
|
|
73
|
+
|
|
74
|
+
Node 22 or newer. Publishing requires a Spec Layer Pro license; pulling does
|
|
75
|
+
not.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
|
|
6
|
+
// src/commands.ts
|
|
7
|
+
import { join as join3 } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/bundle.ts
|
|
10
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11
|
+
function contentHash(artifact) {
|
|
12
|
+
if (!isRecord(artifact) || !isRecord(artifact.spec_layer)) return null;
|
|
13
|
+
const exp = artifact.spec_layer.export;
|
|
14
|
+
if (!isRecord(exp) || typeof exp.content_hash !== "string") return null;
|
|
15
|
+
return exp.content_hash;
|
|
16
|
+
}
|
|
17
|
+
function entry(v, where) {
|
|
18
|
+
if (!isRecord(v) || typeof v.name !== "string" || typeof v.ai !== "string" || contentHash(v.artifact) === null) {
|
|
19
|
+
throw new Error(`The ${where} entry in this bundle is malformed.`);
|
|
20
|
+
}
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
function parseBundle(raw) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error("The server response is not valid JSON.");
|
|
29
|
+
}
|
|
30
|
+
if (!isRecord(parsed) || parsed.schema !== "spec-layer-library-bundle") {
|
|
31
|
+
throw new Error("The server response is not a Spec Layer library bundle.");
|
|
32
|
+
}
|
|
33
|
+
if (typeof parsed.version !== "string" || typeof parsed.extractorVersion !== "string" || !Array.isArray(parsed.components)) {
|
|
34
|
+
throw new Error("This bundle is missing required fields.");
|
|
35
|
+
}
|
|
36
|
+
const foundation = parsed.foundation ?? null;
|
|
37
|
+
if (foundation !== null) {
|
|
38
|
+
if (!isRecord(foundation) || typeof foundation.ai !== "string" || contentHash(foundation.artifact) === null) {
|
|
39
|
+
throw new Error("The foundation entry in this bundle is malformed.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const components = parsed.components.map((c, i) => entry(c, `component ${i}`));
|
|
43
|
+
return {
|
|
44
|
+
schema: "spec-layer-library-bundle",
|
|
45
|
+
version: parsed.version,
|
|
46
|
+
fileName: typeof parsed.fileName === "string" ? parsed.fileName : null,
|
|
47
|
+
pluginVersion: typeof parsed.pluginVersion === "string" ? parsed.pluginVersion : null,
|
|
48
|
+
extractorVersion: parsed.extractorVersion,
|
|
49
|
+
foundation,
|
|
50
|
+
components
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/config.ts
|
|
55
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
56
|
+
import { join } from "node:path";
|
|
57
|
+
var DEFAULT_API = "https://api.spec-layer.com";
|
|
58
|
+
var DEFAULT_OUT_DIR = ".speclayer";
|
|
59
|
+
var CONFIG_NAME = "speclayer.json";
|
|
60
|
+
function readConfig(cwd) {
|
|
61
|
+
const path = join(cwd, CONFIG_NAME);
|
|
62
|
+
if (!existsSync(path)) return null;
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(`${CONFIG_NAME} is not valid JSON. Fix or delete it, then retry.`);
|
|
68
|
+
}
|
|
69
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
70
|
+
throw new Error(`${CONFIG_NAME} is not valid JSON. Fix or delete it, then retry.`);
|
|
71
|
+
}
|
|
72
|
+
const record = parsed;
|
|
73
|
+
return {
|
|
74
|
+
...typeof record.libraryId === "string" ? { libraryId: record.libraryId } : {},
|
|
75
|
+
...typeof record.outDir === "string" ? { outDir: record.outDir } : {}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function writeConfig(cwd, config) {
|
|
79
|
+
writeFileSync(join(cwd, CONFIG_NAME), `${JSON.stringify(config, null, 2)}
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
function resolveOptions(cwd, flags, env, manifestLibraryId2) {
|
|
83
|
+
const config = readConfig(cwd);
|
|
84
|
+
const outDir = flags.out ?? config?.outDir ?? DEFAULT_OUT_DIR;
|
|
85
|
+
const libraryId = flags.id ?? config?.libraryId ?? manifestLibraryId2(join(cwd, outDir));
|
|
86
|
+
return {
|
|
87
|
+
libraryId,
|
|
88
|
+
outDir,
|
|
89
|
+
api: flags.api ?? env.SPEC_LAYER_API ?? DEFAULT_API,
|
|
90
|
+
key: flags.key ?? env.SPEC_LAYER_KEY ?? null
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/api.ts
|
|
95
|
+
import { createHash } from "node:crypto";
|
|
96
|
+
async function fetchBundle(opts) {
|
|
97
|
+
const doFetch = opts.fetcher ?? fetch;
|
|
98
|
+
let res;
|
|
99
|
+
try {
|
|
100
|
+
res = await doFetch(`${opts.api}/v1/libraries/${opts.libraryId}`, {
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${opts.key}`,
|
|
103
|
+
...opts.etag ? { "If-None-Match": `"${opts.etag}"` } : {}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
return { kind: "error", message: `Could not reach ${opts.api}.` };
|
|
108
|
+
}
|
|
109
|
+
if (res.status === 304) return { kind: "not_modified" };
|
|
110
|
+
if (res.status === 401) {
|
|
111
|
+
return { kind: "error", message: "Key was rotated or revoked. Ask the publisher for the current key." };
|
|
112
|
+
}
|
|
113
|
+
if (res.status === 404) return { kind: "error", message: "Library not found. It may have been unpublished." };
|
|
114
|
+
if (!res.ok) return { kind: "error", message: `Request failed with HTTP ${res.status}.` };
|
|
115
|
+
const raw = await res.text();
|
|
116
|
+
return {
|
|
117
|
+
kind: "ok",
|
|
118
|
+
raw,
|
|
119
|
+
publishedAt: res.headers.get("X-Published-At") ?? "unknown",
|
|
120
|
+
bundleHash: createHash("sha256").update(raw).digest("hex")
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/files.ts
|
|
125
|
+
import { mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, rmSync, renameSync, existsSync as existsSync2 } from "node:fs";
|
|
126
|
+
import { join as join2, dirname } from "node:path";
|
|
127
|
+
function slugify(name) {
|
|
128
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
129
|
+
return slug || "component";
|
|
130
|
+
}
|
|
131
|
+
function readManifest(outDir) {
|
|
132
|
+
const path = join2(outDir, "manifest.json");
|
|
133
|
+
if (!existsSync2(path)) return null;
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function writeBundleFiles(opts) {
|
|
141
|
+
const staging = `${opts.outDir}.partial`;
|
|
142
|
+
rmSync(staging, { recursive: true, force: true });
|
|
143
|
+
const written = [];
|
|
144
|
+
const put = (rel, content) => {
|
|
145
|
+
const path = join2(staging, rel);
|
|
146
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
147
|
+
writeFileSync2(path, content);
|
|
148
|
+
written.push(rel);
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
put("bundle.json", opts.raw);
|
|
152
|
+
const artifacts = [];
|
|
153
|
+
if (opts.bundle.foundation) {
|
|
154
|
+
put("ai/foundation.yaml", opts.bundle.foundation.ai);
|
|
155
|
+
artifacts.push({
|
|
156
|
+
kind: "foundation",
|
|
157
|
+
name: "foundation",
|
|
158
|
+
contentHash: opts.bundle.foundation.artifact.spec_layer.export.content_hash,
|
|
159
|
+
aiPath: "ai/foundation.yaml"
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const usedSlugs = /* @__PURE__ */ new Set();
|
|
163
|
+
const nextSuffix = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const component of opts.bundle.components) {
|
|
165
|
+
const base = slugify(component.name);
|
|
166
|
+
let slug = base;
|
|
167
|
+
if (usedSlugs.has(slug)) {
|
|
168
|
+
let n = (nextSuffix.get(base) ?? 1) + 1;
|
|
169
|
+
slug = `${base}-${n}`;
|
|
170
|
+
while (usedSlugs.has(slug)) {
|
|
171
|
+
n += 1;
|
|
172
|
+
slug = `${base}-${n}`;
|
|
173
|
+
}
|
|
174
|
+
nextSuffix.set(base, n);
|
|
175
|
+
} else {
|
|
176
|
+
nextSuffix.set(base, 1);
|
|
177
|
+
}
|
|
178
|
+
usedSlugs.add(slug);
|
|
179
|
+
const aiPath = `ai/components/${slug}.yaml`;
|
|
180
|
+
put(aiPath, component.ai);
|
|
181
|
+
artifacts.push({
|
|
182
|
+
kind: "component",
|
|
183
|
+
name: component.name,
|
|
184
|
+
contentHash: component.artifact.spec_layer.export.content_hash,
|
|
185
|
+
aiPath
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const manifest = {
|
|
189
|
+
libraryId: opts.libraryId,
|
|
190
|
+
publishedAt: opts.publishedAt,
|
|
191
|
+
bundleHash: opts.bundleHash,
|
|
192
|
+
pluginVersion: opts.bundle.pluginVersion,
|
|
193
|
+
extractorVersion: opts.bundle.extractorVersion,
|
|
194
|
+
artifacts
|
|
195
|
+
};
|
|
196
|
+
put("manifest.json", `${JSON.stringify(manifest, null, 2)}
|
|
197
|
+
`);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
rmSync(staging, { recursive: true, force: true });
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
202
|
+
rmSync(opts.outDir, { recursive: true, force: true });
|
|
203
|
+
renameSync(staging, opts.outDir);
|
|
204
|
+
return written;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/commands.ts
|
|
208
|
+
var manifestLibraryId = (outDir) => readManifest(outDir)?.libraryId ?? null;
|
|
209
|
+
function runInit(cwd, flags, io2) {
|
|
210
|
+
if (!flags.id) {
|
|
211
|
+
io2.err("spec-layer init needs --id lib_... (shown in the plugin after publishing).");
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
const outDir = flags.out ?? DEFAULT_OUT_DIR;
|
|
215
|
+
writeConfig(cwd, { libraryId: flags.id, outDir });
|
|
216
|
+
io2.out(`Wrote speclayer.json (library ${flags.id}, output ${outDir}).`);
|
|
217
|
+
io2.out("The pull key is never stored here. Set SPEC_LAYER_KEY in your environment or pass --key.");
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
function resolved(cwd, flags, env, io2) {
|
|
221
|
+
let opts;
|
|
222
|
+
try {
|
|
223
|
+
opts = resolveOptions(cwd, flags, env, manifestLibraryId);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
if (!opts.libraryId) {
|
|
229
|
+
io2.err("No library id. Pass --id lib_..., or run spec-layer init first.");
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (!opts.key) {
|
|
233
|
+
io2.err("No pull key. Set SPEC_LAYER_KEY or pass --key.");
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
return opts;
|
|
237
|
+
}
|
|
238
|
+
async function runPull(cwd, flags, env, io2, fetcher) {
|
|
239
|
+
const opts = resolved(cwd, flags, env, io2);
|
|
240
|
+
if (!opts) return 1;
|
|
241
|
+
const result = await fetchBundle({
|
|
242
|
+
api: opts.api,
|
|
243
|
+
libraryId: opts.libraryId,
|
|
244
|
+
key: opts.key,
|
|
245
|
+
...fetcher ? { fetcher } : {}
|
|
246
|
+
});
|
|
247
|
+
if (result.kind === "error") {
|
|
248
|
+
io2.err(result.message);
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
if (result.kind === "not_modified") {
|
|
252
|
+
io2.out("Already up to date.");
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
let written;
|
|
256
|
+
try {
|
|
257
|
+
const bundle = parseBundle(result.raw);
|
|
258
|
+
written = writeBundleFiles({
|
|
259
|
+
outDir: join3(cwd, opts.outDir),
|
|
260
|
+
raw: result.raw,
|
|
261
|
+
bundle,
|
|
262
|
+
libraryId: opts.libraryId,
|
|
263
|
+
publishedAt: result.publishedAt,
|
|
264
|
+
bundleHash: result.bundleHash
|
|
265
|
+
});
|
|
266
|
+
const components = bundle.components.length;
|
|
267
|
+
io2.out(
|
|
268
|
+
`Pulled ${bundle.fileName ?? opts.libraryId}: ${bundle.foundation ? "foundation + " : ""}${components} component${components === 1 ? "" : "s"} (published ${result.publishedAt}).`
|
|
269
|
+
);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
io2.err(err instanceof Error ? err.message : String(err));
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
io2.out(`Wrote ${written.length} files under ${opts.outDir}/.`);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
async function runStatus(cwd, flags, env, io2, fetcher) {
|
|
278
|
+
const opts = resolved(cwd, flags, env, io2);
|
|
279
|
+
if (!opts) return 1;
|
|
280
|
+
const manifest = readManifest(join3(cwd, opts.outDir));
|
|
281
|
+
if (!manifest) {
|
|
282
|
+
io2.err("No local pull found. Run spec-layer pull.");
|
|
283
|
+
return 2;
|
|
284
|
+
}
|
|
285
|
+
const result = await fetchBundle({
|
|
286
|
+
api: opts.api,
|
|
287
|
+
libraryId: opts.libraryId,
|
|
288
|
+
key: opts.key,
|
|
289
|
+
etag: manifest.bundleHash,
|
|
290
|
+
...fetcher ? { fetcher } : {}
|
|
291
|
+
});
|
|
292
|
+
if (result.kind === "error") {
|
|
293
|
+
io2.err(result.message);
|
|
294
|
+
return 1;
|
|
295
|
+
}
|
|
296
|
+
if (result.kind === "not_modified") {
|
|
297
|
+
io2.out(`Up to date (published ${manifest.publishedAt}).`);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
io2.out(`Behind: remote published ${result.publishedAt}. Run spec-layer pull.`);
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/cli.ts
|
|
305
|
+
var USAGE = `spec-layer <command>
|
|
306
|
+
|
|
307
|
+
Commands:
|
|
308
|
+
init --id lib_... [--out DIR] write speclayer.json
|
|
309
|
+
pull [--id lib_...] [--key sl_...] fetch the library into DIR (default .speclayer)
|
|
310
|
+
status [--id lib_...] [--key sl_...] check freshness; exits 2 when behind
|
|
311
|
+
|
|
312
|
+
Options:
|
|
313
|
+
--api URL override the API origin (default https://api.spec-layer.com)
|
|
314
|
+
The pull key comes from --key or the SPEC_LAYER_KEY environment variable.`;
|
|
315
|
+
var io = { out: (l) => console.log(l), err: (l) => console.error(l) };
|
|
316
|
+
async function main() {
|
|
317
|
+
let values;
|
|
318
|
+
let positionals;
|
|
319
|
+
try {
|
|
320
|
+
({ values, positionals } = parseArgs({
|
|
321
|
+
allowPositionals: true,
|
|
322
|
+
options: {
|
|
323
|
+
id: { type: "string" },
|
|
324
|
+
out: { type: "string" },
|
|
325
|
+
key: { type: "string" },
|
|
326
|
+
api: { type: "string" }
|
|
327
|
+
}
|
|
328
|
+
}));
|
|
329
|
+
} catch {
|
|
330
|
+
io.err(USAGE);
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
const command = positionals[0];
|
|
334
|
+
const cwd = process.cwd();
|
|
335
|
+
try {
|
|
336
|
+
if (command === "init") return runInit(cwd, values, io);
|
|
337
|
+
if (command === "pull") return await runPull(cwd, values, process.env, io);
|
|
338
|
+
if (command === "status") return await runStatus(cwd, values, process.env, io);
|
|
339
|
+
io.err(USAGE);
|
|
340
|
+
return 1;
|
|
341
|
+
} catch (err) {
|
|
342
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
343
|
+
return 1;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
process.exitCode = await main();
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spec-layer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pull design-system context published by the Spec Layer Figma plugin",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"spec-layer": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.0.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "node build.mjs",
|
|
18
|
+
"prepublishOnly": "node build.mjs"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"esbuild": "^0.28.1"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/SamsonHD/spec-layer.git",
|
|
26
|
+
"directory": "packages/cli"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://spec-layer.com",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"figma",
|
|
31
|
+
"design-system",
|
|
32
|
+
"design-tokens",
|
|
33
|
+
"documentation",
|
|
34
|
+
"ai-context"
|
|
35
|
+
]
|
|
36
|
+
}
|