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.
- package/README.md +77 -10
- package/dist/cli.js +202 -30
- 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
|
-
|
|
17
|
+
npx spec-layer setup --id lib_... --key sl_...
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
That
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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 =
|
|
845
|
-
if (!
|
|
879
|
+
const path = join2(cwd, CONFIG_NAME);
|
|
880
|
+
if (!existsSync2(path)) return null;
|
|
846
881
|
let parsed;
|
|
847
882
|
try {
|
|
848
|
-
parsed = JSON.parse(
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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 {
|
|
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
|
|
915
|
-
import { join as
|
|
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 =
|
|
955
|
-
if (!
|
|
1003
|
+
const path = join3(outDir, "manifest.json");
|
|
1004
|
+
if (!existsSync3(path)) return null;
|
|
956
1005
|
try {
|
|
957
|
-
return JSON.parse(
|
|
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 =
|
|
964
|
-
if (!
|
|
1012
|
+
const path = join3(outDir, "bundle.json");
|
|
1013
|
+
if (!existsSync3(path)) return null;
|
|
965
1014
|
try {
|
|
966
|
-
return parseBundle(
|
|
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 (
|
|
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 =
|
|
1059
|
+
const path = join3(staging, rel);
|
|
1011
1060
|
mkdirSync(dirname(path), { recursive: true });
|
|
1012
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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);
|