spec-layer 0.2.1 → 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 +52 -10
  2. package/dist/cli.js +200 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,19 +11,23 @@ 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
+
27
31
  ## Installing, or not
28
32
 
29
33
  `npx` needs no install step: it fetches the package and runs it. That is the
@@ -114,11 +118,49 @@ name, `show` refuses to guess and points you at `list`.
114
118
 
115
119
  ## The pull key
116
120
 
117
- Every command that talks to the server reads the pull key from `SPEC_LAYER_KEY`
118
- or `--key`. It is never written to disk, and `speclayer.json` never contains
119
- it. Treat it as a secret: it grants read access to the published bundle. If it
120
- leaks, rotate it from the plugin's Library screen. The old key stops working
121
- 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.
122
164
 
123
165
  ## What `pull` writes
124
166
 
package/dist/cli.js CHANGED
@@ -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 join3 } from "node:path";
562
+ import { join as join5 } from "node:path";
563
563
 
564
564
  // ../extractor/src/statesMatrix.ts
565
565
  var STATE_ORDER = [
@@ -824,8 +824,41 @@ function parseBundle(raw) {
824
824
  }
825
825
 
826
826
  // src/config.ts
827
- 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";
828
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
829
862
  var DEFAULT_API = "https://api.spec-layer.com";
830
863
  var DEFAULT_OUT_DIR = ".speclayer";
831
864
  var CONFIG_NAME = "speclayer.json";
@@ -843,11 +876,11 @@ function parseInclude(value) {
843
876
  };
844
877
  }
845
878
  function readConfig(cwd) {
846
- const path = join(cwd, CONFIG_NAME);
847
- if (!existsSync(path)) return null;
879
+ const path = join2(cwd, CONFIG_NAME);
880
+ if (!existsSync2(path)) return null;
848
881
  let parsed;
849
882
  try {
850
- parsed = JSON.parse(readFileSync(path, "utf8"));
883
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
851
884
  } catch {
852
885
  throw invalidConfig();
853
886
  }
@@ -865,20 +898,31 @@ function writeConfig(cwd, config) {
865
898
  outDir: config.outDir,
866
899
  ...config.include ? { include: config.include } : {}
867
900
  };
868
- writeFileSync(join(cwd, CONFIG_NAME), `${JSON.stringify(body, null, 2)}
901
+ writeFileSync2(join2(cwd, CONFIG_NAME), `${JSON.stringify(body, null, 2)}
869
902
  `);
870
903
  }
871
904
  function resolveOptions(cwd, flags, env, manifestLibraryId) {
872
905
  const config = readConfig(cwd);
873
906
  const outDir = flags.out ?? config?.outDir ?? DEFAULT_OUT_DIR;
874
- 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
+ }
875
918
  return {
876
919
  libraryId,
877
920
  outDir,
878
921
  // A trailing slash would build "//v1/..." paths the proxy router 404s on.
879
922
  api: (flags.api ?? env.SPEC_LAYER_API ?? DEFAULT_API).replace(/\/+$/, ""),
880
- key: flags.key ?? env.SPEC_LAYER_KEY ?? null,
881
- ...config?.include ? { include: config.include } : {}
923
+ key: supplied ?? storedKey,
924
+ ...config?.include ? { include: config.include } : {},
925
+ ...storedKeyFor ? { storedKeyFor } : {}
882
926
  };
883
927
  }
884
928
 
@@ -899,7 +943,10 @@ async function fetchBundle(opts) {
899
943
  }
900
944
  if (res.status === 304) return { kind: "not_modified" };
901
945
  if (res.status === 401) {
902
- 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
+ };
903
950
  }
904
951
  if (res.status === 404) return { kind: "error", message: "Library not found. It may have been unpublished." };
905
952
  if (!res.ok) return { kind: "error", message: `Request failed with HTTP ${res.status}.` };
@@ -913,8 +960,8 @@ async function fetchBundle(opts) {
913
960
  }
914
961
 
915
962
  // src/files.ts
916
- import { mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync, rmSync, renameSync, existsSync as existsSync2 } from "node:fs";
917
- 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";
918
965
 
919
966
  // src/selection.ts
920
967
  var DEFAULT_SELECTION = { foundation: true, components: null };
@@ -953,19 +1000,19 @@ function slugify(name) {
953
1000
  return slug || "component";
954
1001
  }
955
1002
  function readManifest(outDir) {
956
- const path = join2(outDir, "manifest.json");
957
- if (!existsSync2(path)) return null;
1003
+ const path = join3(outDir, "manifest.json");
1004
+ if (!existsSync3(path)) return null;
958
1005
  try {
959
- return JSON.parse(readFileSync2(path, "utf8"));
1006
+ return JSON.parse(readFileSync3(path, "utf8"));
960
1007
  } catch {
961
1008
  return null;
962
1009
  }
963
1010
  }
964
1011
  function readLocalBundle(outDir) {
965
- const path = join2(outDir, "bundle.json");
966
- if (!existsSync2(path)) return null;
1012
+ const path = join3(outDir, "bundle.json");
1013
+ if (!existsSync3(path)) return null;
967
1014
  try {
968
- return parseBundle(readFileSync2(path, "utf8"));
1015
+ return parseBundle(readFileSync3(path, "utf8"));
969
1016
  } catch {
970
1017
  throw new Error(`${path} could not be read as a library bundle. Run spec-layer pull again.`);
971
1018
  }
@@ -996,7 +1043,7 @@ function assertReplaceable(outDir, cwd) {
996
1043
  if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
997
1044
  throw new Error('The output directory must sit inside the current directory, not be "." or a parent of it.');
998
1045
  }
999
- 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) {
1000
1047
  throw new Error(`${outDir} exists and was not written by spec-layer pull. Choose an empty or new directory.`);
1001
1048
  }
1002
1049
  }
@@ -1009,9 +1056,9 @@ function writeBundleFiles(opts) {
1009
1056
  rmSync(staging, { recursive: true, force: true });
1010
1057
  const written = [];
1011
1058
  const put = (rel, content) => {
1012
- const path = join2(staging, rel);
1059
+ const path = join3(staging, rel);
1013
1060
  mkdirSync(dirname(path), { recursive: true });
1014
- writeFileSync2(path, content);
1061
+ writeFileSync3(path, content);
1015
1062
  written.push(rel);
1016
1063
  };
1017
1064
  try {
@@ -1057,6 +1104,60 @@ function writeBundleFiles(opts) {
1057
1104
  return written;
1058
1105
  }
1059
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
+
1060
1161
  // src/commands.ts
1061
1162
  var NO_LOCAL_PULL = "No local pull found. Run spec-layer pull.";
1062
1163
  function manifestReader() {
@@ -1086,7 +1187,7 @@ function runInit(cwd, flags, io2) {
1086
1187
  const outDir = flags.out ?? DEFAULT_OUT_DIR;
1087
1188
  writeConfig(cwd, { libraryId: flags.id, outDir, ...include ? { include } : {} });
1088
1189
  io2.out(`Wrote speclayer.json (library ${flags.id}, output ${outDir}).`);
1089
- 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.`);
1090
1191
  return 0;
1091
1192
  }
1092
1193
  function resolved(cwd, flags, env, io2, manifestAt) {
@@ -1098,18 +1199,18 @@ function resolved(cwd, flags, env, io2, manifestAt) {
1098
1199
  return null;
1099
1200
  }
1100
1201
  if (!opts.libraryId) {
1101
- 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.");
1102
1203
  return null;
1103
1204
  }
1104
1205
  if (!opts.key) {
1105
- 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.");
1106
1207
  return null;
1107
1208
  }
1108
1209
  return opts;
1109
1210
  }
1110
1211
  function resolvedOutDir(cwd, flags, io2) {
1111
1212
  try {
1112
- return join3(cwd, flags.out ?? readConfig(cwd)?.outDir ?? DEFAULT_OUT_DIR);
1213
+ return join5(cwd, flags.out ?? readConfig(cwd)?.outDir ?? DEFAULT_OUT_DIR);
1113
1214
  } catch (err) {
1114
1215
  io2.err(errorText(err));
1115
1216
  return null;
@@ -1122,6 +1223,72 @@ function describePull(bundle, selection, selected) {
1122
1223
  if (!bundle.foundation) return components;
1123
1224
  return selection.foundation ? `foundation + ${components}` : `${components}, no foundation`;
1124
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
+ }
1125
1292
  async function runPull(cwd, flags, env, io2, fetcher) {
1126
1293
  const manifestAt = manifestReader();
1127
1294
  const opts = resolved(cwd, flags, env, io2, manifestAt);
@@ -1133,7 +1300,7 @@ async function runPull(cwd, flags, env, io2, fetcher) {
1133
1300
  io2.err(errorText(err));
1134
1301
  return 1;
1135
1302
  }
1136
- const manifest = manifestAt(join3(cwd, opts.outDir));
1303
+ const manifest = manifestAt(join5(cwd, opts.outDir));
1137
1304
  const etag = manifest && sameSelection(manifest.selection ?? DEFAULT_SELECTION, selection) ? manifest.bundleHash : void 0;
1138
1305
  const result = await fetchBundle({
1139
1306
  api: opts.api,
@@ -1155,7 +1322,7 @@ async function runPull(cwd, flags, env, io2, fetcher) {
1155
1322
  const bundle = parseBundle(result.raw);
1156
1323
  const selected = selectComponents(bundle, selection);
1157
1324
  written = writeBundleFiles({
1158
- outDir: join3(cwd, opts.outDir),
1325
+ outDir: join5(cwd, opts.outDir),
1159
1326
  cwd,
1160
1327
  raw: result.raw,
1161
1328
  bundle,
@@ -1178,7 +1345,7 @@ async function runStatus(cwd, flags, env, io2, fetcher) {
1178
1345
  const manifestAt = manifestReader();
1179
1346
  const opts = resolved(cwd, flags, env, io2, manifestAt);
1180
1347
  if (!opts) return 1;
1181
- const manifest = manifestAt(join3(cwd, opts.outDir));
1348
+ const manifest = manifestAt(join5(cwd, opts.outDir));
1182
1349
  if (!manifest) {
1183
1350
  io2.err(NO_LOCAL_PULL);
1184
1351
  return 2;
@@ -1269,6 +1436,8 @@ Available: ${available || "none"}.`);
1269
1436
  var USAGE = `spec-layer <command>
1270
1437
 
1271
1438
  Commands:
1439
+ setup --id lib_... --key sl_... [--out DIR] [selection]
1440
+ store the key, then pull
1272
1441
  init --id lib_... [--out DIR] [selection] write speclayer.json
1273
1442
  pull [--id lib_...] [--key sl_...] [selection]
1274
1443
  fetch the library into DIR (default .speclayer)
@@ -1277,13 +1446,13 @@ Commands:
1277
1446
  show foundation | component NAME [--canonical]
1278
1447
  print one artifact's AI YAML (or canonical JSON)
1279
1448
 
1280
- 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):
1281
1450
  --only foundation | components write just the foundation, or just components
1282
1451
  --component NAME write only this component (repeatable, matched by slug)
1283
1452
 
1284
1453
  Options:
1285
1454
  --api URL override the API origin (default https://api.spec-layer.com)
1286
- 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.`;
1287
1456
  var io = {
1288
1457
  out: (l) => console.log(l),
1289
1458
  err: (l) => console.error(l),
@@ -1314,6 +1483,7 @@ async function main() {
1314
1483
  const command = positionals[0];
1315
1484
  const cwd = process.cwd();
1316
1485
  try {
1486
+ if (command === "setup") return await runSetup(cwd, values, process.env, io);
1317
1487
  if (command === "init") return runInit(cwd, values, io);
1318
1488
  if (command === "pull") return await runPull(cwd, values, process.env, io);
1319
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.1",
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",