spec-layer 0.2.0 → 0.3.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 +77 -10
  2. package/dist/cli.js +202 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,19 +11,48 @@ and writes it to disk.
11
11
  ## Quick start
12
12
 
13
13
  After publishing a library from the plugin's Library screen, it shows a setup
14
- command. Run it in your repository:
14
+ command. Run it once in your repository:
15
15
 
16
16
  ```bash
17
- SPEC_LAYER_KEY=sl_... npx spec-layer pull --id lib_...
17
+ npx spec-layer setup --id lib_... --key sl_...
18
18
  ```
19
19
 
20
- That writes `.speclayer/` and is enough on its own. To avoid repeating the
21
- library id, record it once:
20
+ That records the library id, stores the key in a gitignored
21
+ `speclayer.local.json`, and writes `.speclayer/`. Every later command needs no
22
+ flags at all:
22
23
 
23
24
  ```bash
24
- npx spec-layer init --id lib_...
25
+ npx spec-layer pull
25
26
  ```
26
27
 
28
+ `init` still writes the config without a key or a network call, for a repo
29
+ that supplies the key from the environment instead.
30
+
31
+ ## Installing, or not
32
+
33
+ `npx` needs no install step: it fetches the package and runs it. That is the
34
+ right choice for trying this once, and for a repo that pulls by hand.
35
+
36
+ Two cases want a real install instead. On a cold cache `npx` has to fetch the
37
+ package first, and confirming that is a prompt you do not want in an
38
+ unattended run. `--yes` answers it up front:
39
+
40
+ ```bash
41
+ npx --yes spec-layer status
42
+ ```
43
+
44
+ And a repo that pulls on a schedule should pin the version rather than
45
+ resolving the latest release on every run, so a CLI update never changes a
46
+ build you did not touch:
47
+
48
+ ```bash
49
+ npm install --save-dev spec-layer
50
+ ```
51
+
52
+ `npx spec-layer` then runs the pinned local copy, no `--yes` needed. Pinning
53
+ also keeps `.speclayer/manifest.json` on one format: 0.1.0 wrote no
54
+ `selection` field, and 0.2.0 does.
55
+
27
56
  ## Commands
28
57
 
29
58
  | Command | What it does |
@@ -89,11 +118,49 @@ name, `show` refuses to guess and points you at `list`.
89
118
 
90
119
  ## The pull key
91
120
 
92
- Every command that talks to the server reads the pull key from `SPEC_LAYER_KEY`
93
- or `--key`. It is never written to disk, and `speclayer.json` never contains
94
- it. Treat it as a secret: it grants read access to the published bundle. If it
95
- leaks, rotate it from the plugin's Library screen. The old key stops working
96
- once the change propagates, which can take up to about a minute.
121
+ `spec-layer setup` stores the key in `speclayer.local.json` next to
122
+ `speclayer.json` and makes sure git ignores it before writing it. Every later
123
+ command in that directory needs no key. On POSIX systems the file is written
124
+ at mode `0600`; on Windows there is no equivalent permission bit, so it
125
+ inherits whatever the directory allows.
126
+
127
+ Commands that talk to the server resolve the key in this order:
128
+
129
+ 1. `--key sl_...`
130
+ 2. `SPEC_LAYER_KEY` in the environment
131
+ 3. `speclayer.local.json`, when it was issued for the same library
132
+
133
+ Environment sits above the file so CI can supply a key without touching the
134
+ working tree. A stored key issued for a different library is ignored, and the
135
+ CLI says which library it belongs to rather than letting the server answer 401.
136
+
137
+ Treat the key as a secret: it grants read access to the published bundle.
138
+ `speclayer.local.json` is gitignored, never printed by any command, and never
139
+ copied into `speclayer.json`, `bundle.json`, `manifest.json`, or anything under
140
+ the output directory. If it leaks, rotate it from the plugin's Library screen,
141
+ then run the new setup command. The old key stops working once the change
142
+ propagates, which can take up to about a minute.
143
+
144
+ Re-running `setup` replaces the stored key and keeps the rest of your setup:
145
+ with no `--out` and no selection flag it preserves the output directory and the
146
+ `include` block already in `speclayer.json` rather than resetting them to the
147
+ defaults. Pass `--out` or a selection flag to change them. (`init` still
148
+ overwrites `speclayer.json` outright, which is what a first run is for.)
149
+
150
+ Outside a git working tree, the key is still stored and the CLI says it left
151
+ `.gitignore` alone. Inside one, `setup` refuses to write the key whenever it
152
+ cannot confirm the file will be ignored, and says what to do instead. Three
153
+ cases refuse:
154
+
155
+ - `.gitignore` cannot be written.
156
+ - git itself could not be run, anywhere inside a working tree.
157
+ - the entry is in `.gitignore`, but git still does not ignore the file. That
158
+ almost always means `speclayer.local.json` is already tracked, and the CLI
159
+ names `git rm --cached speclayer.local.json` as the way out.
160
+
161
+ git decides in every case. The entry sitting in `.gitignore` is not taken as
162
+ proof, because `git check-ignore` does not report a tracked file as ignored no
163
+ matter what the ignore rules say.
97
164
 
98
165
  ## What `pull` writes
99
166
 
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ const require = createRequire(import.meta.url);
2
4
  var __create = Object.create;
3
5
  var __defProp = Object.defineProperty;
4
6
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -557,7 +559,7 @@ var require_sha256 = __commonJS({
557
559
  import { parseArgs } from "node:util";
558
560
 
559
561
  // src/commands.ts
560
- import { join as join3 } from "node:path";
562
+ import { join as join5 } from "node:path";
561
563
 
562
564
  // ../extractor/src/statesMatrix.ts
563
565
  var STATE_ORDER = [
@@ -822,8 +824,41 @@ function parseBundle(raw) {
822
824
  }
823
825
 
824
826
  // src/config.ts
825
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
827
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
828
+ import { join as join2 } from "node:path";
829
+
830
+ // src/credentials.ts
831
+ import { readFileSync, writeFileSync, existsSync, chmodSync } from "node:fs";
826
832
  import { join } from "node:path";
833
+ var CREDENTIALS_NAME = "speclayer.local.json";
834
+ var unreadable = () => new Error(
835
+ `${CREDENTIALS_NAME} cannot be read. Delete it, then run the setup command from the plugin's Library screen.`
836
+ );
837
+ function readCredentials(cwd) {
838
+ const path = join(cwd, CREDENTIALS_NAME);
839
+ if (!existsSync(path)) return null;
840
+ let parsed;
841
+ try {
842
+ parsed = JSON.parse(readFileSync(path, "utf8"));
843
+ } catch {
844
+ throw unreadable();
845
+ }
846
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw unreadable();
847
+ const record = parsed;
848
+ if (typeof record.libraryId !== "string" || typeof record.key !== "string") throw unreadable();
849
+ return { libraryId: record.libraryId, key: record.key };
850
+ }
851
+ function writeCredentials(cwd, stored) {
852
+ const path = join(cwd, CREDENTIALS_NAME);
853
+ const replaced = existsSync(path);
854
+ const body = { libraryId: stored.libraryId, key: stored.key };
855
+ writeFileSync(path, `${JSON.stringify(body, null, 2)}
856
+ `, { mode: 384 });
857
+ chmodSync(path, 384);
858
+ return { replaced };
859
+ }
860
+
861
+ // src/config.ts
827
862
  var DEFAULT_API = "https://api.spec-layer.com";
828
863
  var DEFAULT_OUT_DIR = ".speclayer";
829
864
  var CONFIG_NAME = "speclayer.json";
@@ -841,11 +876,11 @@ function parseInclude(value) {
841
876
  };
842
877
  }
843
878
  function readConfig(cwd) {
844
- const path = join(cwd, CONFIG_NAME);
845
- if (!existsSync(path)) return null;
879
+ const path = join2(cwd, CONFIG_NAME);
880
+ if (!existsSync2(path)) return null;
846
881
  let parsed;
847
882
  try {
848
- parsed = JSON.parse(readFileSync(path, "utf8"));
883
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
849
884
  } catch {
850
885
  throw invalidConfig();
851
886
  }
@@ -863,20 +898,31 @@ function writeConfig(cwd, config) {
863
898
  outDir: config.outDir,
864
899
  ...config.include ? { include: config.include } : {}
865
900
  };
866
- writeFileSync(join(cwd, CONFIG_NAME), `${JSON.stringify(body, null, 2)}
901
+ writeFileSync2(join2(cwd, CONFIG_NAME), `${JSON.stringify(body, null, 2)}
867
902
  `);
868
903
  }
869
904
  function resolveOptions(cwd, flags, env, manifestLibraryId) {
870
905
  const config = readConfig(cwd);
871
906
  const outDir = flags.out ?? config?.outDir ?? DEFAULT_OUT_DIR;
872
- const libraryId = flags.id ?? config?.libraryId ?? manifestLibraryId(join(cwd, outDir));
907
+ const libraryId = flags.id ?? config?.libraryId ?? manifestLibraryId(join2(cwd, outDir));
908
+ const supplied = flags.key || env.SPEC_LAYER_KEY || null;
909
+ let storedKey = null;
910
+ let storedKeyFor;
911
+ if (!supplied) {
912
+ const stored = readCredentials(cwd);
913
+ if (stored) {
914
+ if (libraryId && stored.libraryId === libraryId) storedKey = stored.key || null;
915
+ else storedKeyFor = stored.libraryId;
916
+ }
917
+ }
873
918
  return {
874
919
  libraryId,
875
920
  outDir,
876
921
  // A trailing slash would build "//v1/..." paths the proxy router 404s on.
877
922
  api: (flags.api ?? env.SPEC_LAYER_API ?? DEFAULT_API).replace(/\/+$/, ""),
878
- key: flags.key ?? env.SPEC_LAYER_KEY ?? null,
879
- ...config?.include ? { include: config.include } : {}
923
+ key: supplied ?? storedKey,
924
+ ...config?.include ? { include: config.include } : {},
925
+ ...storedKeyFor ? { storedKeyFor } : {}
880
926
  };
881
927
  }
882
928
 
@@ -897,7 +943,10 @@ async function fetchBundle(opts) {
897
943
  }
898
944
  if (res.status === 304) return { kind: "not_modified" };
899
945
  if (res.status === 401) {
900
- return { kind: "error", message: "Key was rotated or revoked. Ask the publisher for the current key." };
946
+ return {
947
+ kind: "error",
948
+ message: "Key was rotated or revoked. Run the setup command from the plugin's Library screen to store the current key."
949
+ };
901
950
  }
902
951
  if (res.status === 404) return { kind: "error", message: "Library not found. It may have been unpublished." };
903
952
  if (!res.ok) return { kind: "error", message: `Request failed with HTTP ${res.status}.` };
@@ -911,8 +960,8 @@ async function fetchBundle(opts) {
911
960
  }
912
961
 
913
962
  // src/files.ts
914
- import { mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync, rmSync, renameSync, existsSync as existsSync2 } from "node:fs";
915
- import { join as join2, dirname, relative, resolve, isAbsolute } from "node:path";
963
+ import { mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync3, readdirSync, rmSync, renameSync, existsSync as existsSync3 } from "node:fs";
964
+ import { join as join3, dirname, relative, resolve, isAbsolute } from "node:path";
916
965
 
917
966
  // src/selection.ts
918
967
  var DEFAULT_SELECTION = { foundation: true, components: null };
@@ -951,19 +1000,19 @@ function slugify(name) {
951
1000
  return slug || "component";
952
1001
  }
953
1002
  function readManifest(outDir) {
954
- const path = join2(outDir, "manifest.json");
955
- if (!existsSync2(path)) return null;
1003
+ const path = join3(outDir, "manifest.json");
1004
+ if (!existsSync3(path)) return null;
956
1005
  try {
957
- return JSON.parse(readFileSync2(path, "utf8"));
1006
+ return JSON.parse(readFileSync3(path, "utf8"));
958
1007
  } catch {
959
1008
  return null;
960
1009
  }
961
1010
  }
962
1011
  function readLocalBundle(outDir) {
963
- const path = join2(outDir, "bundle.json");
964
- if (!existsSync2(path)) return null;
1012
+ const path = join3(outDir, "bundle.json");
1013
+ if (!existsSync3(path)) return null;
965
1014
  try {
966
- return parseBundle(readFileSync2(path, "utf8"));
1015
+ return parseBundle(readFileSync3(path, "utf8"));
967
1016
  } catch {
968
1017
  throw new Error(`${path} could not be read as a library bundle. Run spec-layer pull again.`);
969
1018
  }
@@ -994,7 +1043,7 @@ function assertReplaceable(outDir, cwd) {
994
1043
  if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
995
1044
  throw new Error('The output directory must sit inside the current directory, not be "." or a parent of it.');
996
1045
  }
997
- if (existsSync2(outDir) && !existsSync2(join2(outDir, "manifest.json")) && readdirSync(outDir).length > 0) {
1046
+ if (existsSync3(outDir) && !existsSync3(join3(outDir, "manifest.json")) && readdirSync(outDir).length > 0) {
998
1047
  throw new Error(`${outDir} exists and was not written by spec-layer pull. Choose an empty or new directory.`);
999
1048
  }
1000
1049
  }
@@ -1007,9 +1056,9 @@ function writeBundleFiles(opts) {
1007
1056
  rmSync(staging, { recursive: true, force: true });
1008
1057
  const written = [];
1009
1058
  const put = (rel, content) => {
1010
- const path = join2(staging, rel);
1059
+ const path = join3(staging, rel);
1011
1060
  mkdirSync(dirname(path), { recursive: true });
1012
- writeFileSync2(path, content);
1061
+ writeFileSync3(path, content);
1013
1062
  written.push(rel);
1014
1063
  };
1015
1064
  try {
@@ -1055,6 +1104,60 @@ function writeBundleFiles(opts) {
1055
1104
  return written;
1056
1105
  }
1057
1106
 
1107
+ // src/gitignore.ts
1108
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4 } from "node:fs";
1109
+ import { spawnSync } from "node:child_process";
1110
+ import { join as join4, dirname as dirname2, resolve as resolve2 } from "node:path";
1111
+ var COMMENT = "# Spec Layer pull key, not for committing";
1112
+ function git(cwd, args) {
1113
+ const res = spawnSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
1114
+ if (res.error || res.status === null) return { ranGit: false };
1115
+ return { ranGit: true, status: res.status, stdout: res.stdout ?? "" };
1116
+ }
1117
+ function insideWorkTreeWithoutGit(cwd) {
1118
+ let dir = resolve2(cwd);
1119
+ for (; ; ) {
1120
+ if (existsSync4(join4(dir, ".git"))) return true;
1121
+ const parent = dirname2(dir);
1122
+ if (parent === dir) return false;
1123
+ dir = parent;
1124
+ }
1125
+ }
1126
+ function hasEntryLine(body, fileName) {
1127
+ return body.split("\n").some((line) => line.trim() === fileName);
1128
+ }
1129
+ function ensureIgnored(cwd, fileName) {
1130
+ const inWorkTree = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
1131
+ if (!inWorkTree.ranGit) {
1132
+ return insideWorkTreeWithoutGit(cwd) ? { kind: "no-git", line: fileName } : { kind: "not-a-repo" };
1133
+ }
1134
+ if (inWorkTree.status !== 0 || inWorkTree.stdout.trim() !== "true") return { kind: "not-a-repo" };
1135
+ const checkIgnore = git(cwd, ["check-ignore", "-q", fileName]);
1136
+ if (checkIgnore.ranGit && checkIgnore.status === 0) return { kind: "already" };
1137
+ const path = join4(cwd, ".gitignore");
1138
+ const existed = existsSync4(path);
1139
+ try {
1140
+ if (!existed) {
1141
+ writeFileSync4(path, `${COMMENT}
1142
+ ${fileName}
1143
+ `);
1144
+ } else {
1145
+ const body = readFileSync4(path, "utf8");
1146
+ if (!hasEntryLine(body, fileName)) {
1147
+ const lead = body.length === 0 || body.endsWith("\n") ? "" : "\n";
1148
+ writeFileSync4(path, `${body}${lead}${COMMENT}
1149
+ ${fileName}
1150
+ `);
1151
+ }
1152
+ }
1153
+ } catch {
1154
+ return { kind: "refused", line: fileName };
1155
+ }
1156
+ const recheck = git(cwd, ["check-ignore", "-q", fileName]);
1157
+ if (!recheck.ranGit || recheck.status !== 0) return { kind: "still-not-ignored", line: fileName };
1158
+ return existed ? { kind: "added" } : { kind: "created" };
1159
+ }
1160
+
1058
1161
  // src/commands.ts
1059
1162
  var NO_LOCAL_PULL = "No local pull found. Run spec-layer pull.";
1060
1163
  function manifestReader() {
@@ -1084,7 +1187,7 @@ function runInit(cwd, flags, io2) {
1084
1187
  const outDir = flags.out ?? DEFAULT_OUT_DIR;
1085
1188
  writeConfig(cwd, { libraryId: flags.id, outDir, ...include ? { include } : {} });
1086
1189
  io2.out(`Wrote speclayer.json (library ${flags.id}, output ${outDir}).`);
1087
- io2.out("The pull key is never stored here. Set SPEC_LAYER_KEY in your environment or pass --key.");
1190
+ io2.out(`The pull key is not stored here. Run spec-layer setup to store it in ${CREDENTIALS_NAME}, or set SPEC_LAYER_KEY.`);
1088
1191
  return 0;
1089
1192
  }
1090
1193
  function resolved(cwd, flags, env, io2, manifestAt) {
@@ -1096,18 +1199,18 @@ function resolved(cwd, flags, env, io2, manifestAt) {
1096
1199
  return null;
1097
1200
  }
1098
1201
  if (!opts.libraryId) {
1099
- io2.err("No library id. Pass --id lib_..., or run spec-layer init first.");
1202
+ io2.err(opts.storedKeyFor ? `No library id. ${CREDENTIALS_NAME} holds a key for library ${opts.storedKeyFor}. Pass --id ${opts.storedKeyFor}, or run spec-layer init first.` : "No library id. Pass --id lib_..., or run spec-layer init first.");
1100
1203
  return null;
1101
1204
  }
1102
1205
  if (!opts.key) {
1103
- io2.err("No pull key. Set SPEC_LAYER_KEY or pass --key.");
1206
+ io2.err(opts.storedKeyFor ? `The key in ${CREDENTIALS_NAME} was issued for library ${opts.storedKeyFor}, not ${opts.libraryId}. Run the setup command from the plugin's Library screen.` : "No pull key. Run the setup command from the plugin's Library screen, or set SPEC_LAYER_KEY.");
1104
1207
  return null;
1105
1208
  }
1106
1209
  return opts;
1107
1210
  }
1108
1211
  function resolvedOutDir(cwd, flags, io2) {
1109
1212
  try {
1110
- return join3(cwd, flags.out ?? readConfig(cwd)?.outDir ?? DEFAULT_OUT_DIR);
1213
+ return join5(cwd, flags.out ?? readConfig(cwd)?.outDir ?? DEFAULT_OUT_DIR);
1111
1214
  } catch (err) {
1112
1215
  io2.err(errorText(err));
1113
1216
  return null;
@@ -1120,6 +1223,72 @@ function describePull(bundle, selection, selected) {
1120
1223
  if (!bundle.foundation) return components;
1121
1224
  return selection.foundation ? `foundation + ${components}` : `${components}, no foundation`;
1122
1225
  }
1226
+ async function runSetup(cwd, flags, env, io2, fetcher) {
1227
+ if (!flags.id) {
1228
+ io2.err("spec-layer setup needs --id lib_... (shown in the plugin after publishing).");
1229
+ return 1;
1230
+ }
1231
+ const key = flags.key ?? env.SPEC_LAYER_KEY;
1232
+ if (!key) {
1233
+ io2.err("spec-layer setup needs --key sl_..., or SPEC_LAYER_KEY in the environment.");
1234
+ return 1;
1235
+ }
1236
+ let include;
1237
+ try {
1238
+ include = selectionFromFlags(flags);
1239
+ } catch (err) {
1240
+ io2.err(errorText(err));
1241
+ return 1;
1242
+ }
1243
+ let existing = null;
1244
+ try {
1245
+ existing = readConfig(cwd);
1246
+ } catch {
1247
+ existing = null;
1248
+ }
1249
+ const outDir = flags.out ?? existing?.outDir ?? DEFAULT_OUT_DIR;
1250
+ const keptInclude = include ?? existing?.include ?? null;
1251
+ writeConfig(cwd, { libraryId: flags.id, outDir, ...keptInclude ? { include: keptInclude } : {} });
1252
+ io2.out(`Wrote speclayer.json (library ${flags.id}, output ${outDir}).`);
1253
+ const ignored = ensureIgnored(cwd, CREDENTIALS_NAME);
1254
+ switch (ignored.kind) {
1255
+ case "refused":
1256
+ io2.err(`Could not add ${CREDENTIALS_NAME} to .gitignore, so the key was not written.`);
1257
+ io2.err(`Add this line to .gitignore, then run the command again:
1258
+ ${ignored.line}`);
1259
+ return 1;
1260
+ case "no-git":
1261
+ io2.err(`Could not run git, so it could not confirm ${CREDENTIALS_NAME} would be ignored. The key was not written.`);
1262
+ io2.err(`Add this line to .gitignore, then run the command again:
1263
+ ${ignored.line}`);
1264
+ return 1;
1265
+ case "still-not-ignored":
1266
+ io2.err(`${ignored.line} is listed in .gitignore, but git still does not ignore it, so the key was not written.`);
1267
+ io2.err(`The most likely reason is that ${ignored.line} is already tracked. Run this, then run the command again:
1268
+ git rm --cached ${ignored.line}`);
1269
+ return 1;
1270
+ case "created":
1271
+ io2.out(`Created .gitignore with ${CREDENTIALS_NAME}.`);
1272
+ break;
1273
+ case "added":
1274
+ io2.out(`Added ${CREDENTIALS_NAME} to .gitignore.`);
1275
+ break;
1276
+ case "already":
1277
+ io2.out(`${CREDENTIALS_NAME} is already ignored by git.`);
1278
+ break;
1279
+ case "not-a-repo":
1280
+ io2.out("Not a git repository, so .gitignore was left alone.");
1281
+ break;
1282
+ default: {
1283
+ const exhaustive = ignored;
1284
+ void exhaustive;
1285
+ return 1;
1286
+ }
1287
+ }
1288
+ const { replaced } = writeCredentials(cwd, { libraryId: flags.id, key });
1289
+ io2.out(replaced ? `Replaced the stored key in ${CREDENTIALS_NAME}.` : `Stored the pull key in ${CREDENTIALS_NAME}.`);
1290
+ return runPull(cwd, { ...flags, key }, env, io2, fetcher);
1291
+ }
1123
1292
  async function runPull(cwd, flags, env, io2, fetcher) {
1124
1293
  const manifestAt = manifestReader();
1125
1294
  const opts = resolved(cwd, flags, env, io2, manifestAt);
@@ -1131,7 +1300,7 @@ async function runPull(cwd, flags, env, io2, fetcher) {
1131
1300
  io2.err(errorText(err));
1132
1301
  return 1;
1133
1302
  }
1134
- const manifest = manifestAt(join3(cwd, opts.outDir));
1303
+ const manifest = manifestAt(join5(cwd, opts.outDir));
1135
1304
  const etag = manifest && sameSelection(manifest.selection ?? DEFAULT_SELECTION, selection) ? manifest.bundleHash : void 0;
1136
1305
  const result = await fetchBundle({
1137
1306
  api: opts.api,
@@ -1153,7 +1322,7 @@ async function runPull(cwd, flags, env, io2, fetcher) {
1153
1322
  const bundle = parseBundle(result.raw);
1154
1323
  const selected = selectComponents(bundle, selection);
1155
1324
  written = writeBundleFiles({
1156
- outDir: join3(cwd, opts.outDir),
1325
+ outDir: join5(cwd, opts.outDir),
1157
1326
  cwd,
1158
1327
  raw: result.raw,
1159
1328
  bundle,
@@ -1176,7 +1345,7 @@ async function runStatus(cwd, flags, env, io2, fetcher) {
1176
1345
  const manifestAt = manifestReader();
1177
1346
  const opts = resolved(cwd, flags, env, io2, manifestAt);
1178
1347
  if (!opts) return 1;
1179
- const manifest = manifestAt(join3(cwd, opts.outDir));
1348
+ const manifest = manifestAt(join5(cwd, opts.outDir));
1180
1349
  if (!manifest) {
1181
1350
  io2.err(NO_LOCAL_PULL);
1182
1351
  return 2;
@@ -1267,6 +1436,8 @@ Available: ${available || "none"}.`);
1267
1436
  var USAGE = `spec-layer <command>
1268
1437
 
1269
1438
  Commands:
1439
+ setup --id lib_... --key sl_... [--out DIR] [selection]
1440
+ store the key, then pull
1270
1441
  init --id lib_... [--out DIR] [selection] write speclayer.json
1271
1442
  pull [--id lib_...] [--key sl_...] [selection]
1272
1443
  fetch the library into DIR (default .speclayer)
@@ -1275,13 +1446,13 @@ Commands:
1275
1446
  show foundation | component NAME [--canonical]
1276
1447
  print one artifact's AI YAML (or canonical JSON)
1277
1448
 
1278
- Selection (pull and init; flags replace the include block in speclayer.json):
1449
+ Selection (setup, pull and init; flags replace the include block in speclayer.json):
1279
1450
  --only foundation | components write just the foundation, or just components
1280
1451
  --component NAME write only this component (repeatable, matched by slug)
1281
1452
 
1282
1453
  Options:
1283
1454
  --api URL override the API origin (default https://api.spec-layer.com)
1284
- The pull key comes from --key or the SPEC_LAYER_KEY environment variable.`;
1455
+ The pull key comes from --key, SPEC_LAYER_KEY, or speclayer.local.json written by setup.`;
1285
1456
  var io = {
1286
1457
  out: (l) => console.log(l),
1287
1458
  err: (l) => console.error(l),
@@ -1312,6 +1483,7 @@ async function main() {
1312
1483
  const command = positionals[0];
1313
1484
  const cwd = process.cwd();
1314
1485
  try {
1486
+ if (command === "setup") return await runSetup(cwd, values, process.env, io);
1315
1487
  if (command === "init") return runInit(cwd, values, io);
1316
1488
  if (command === "pull") return await runPull(cwd, values, process.env, io);
1317
1489
  if (command === "status") return await runStatus(cwd, values, process.env, io);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-layer",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pull design-system context published by the Spec Layer Figma plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",