sdtk-kit 1.2.0 → 1.3.1
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/LICENSE +21 -21
- package/README.md +202 -135
- package/bin/sdtk-code.js +6 -6
- package/bin/sdtk-design.js +6 -6
- package/bin/sdtk-ops.js +6 -6
- package/bin/sdtk-spec.js +12 -12
- package/bin/sdtk-wiki.js +6 -6
- package/bin/sdtk.js +99 -0
- package/package.json +65 -60
- package/scripts/install-smoke.js +190 -0
- package/scripts/postinstall.js +43 -40
- package/scripts/unified-init.test.js +292 -0
- package/src/commands/init.js +106 -0
- package/src/lib/unified-init.js +280 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Offline unit tests for the unified-init orchestrator (BK-268).
|
|
5
|
+
// No real PowerShell, no network, no child processes — every effectful dep
|
|
6
|
+
// (spawn / resolveBin / powershellCheck / log) is injected as a spy/stub.
|
|
7
|
+
|
|
8
|
+
const assert = require("assert");
|
|
9
|
+
const {
|
|
10
|
+
runUnifiedInit,
|
|
11
|
+
buildInitArgs,
|
|
12
|
+
TOOLKITS,
|
|
13
|
+
ToolkitResolveError,
|
|
14
|
+
} = require("../src/lib/unified-init");
|
|
15
|
+
const { parseFlags, buildOpts } = require("../src/commands/init");
|
|
16
|
+
|
|
17
|
+
// spec/ops/code own the PowerShell runtime-asset payload (install.ps1) and so
|
|
18
|
+
// also accept --skip-runtime-assets. sdtk-design is runtime-aware (places the
|
|
19
|
+
// design-prototype skill under .claude/.codex) but does NOT take that flag.
|
|
20
|
+
const ASSET_KITS = ["sdtk-spec", "sdtk-ops", "sdtk-code"];
|
|
21
|
+
const RUNTIME_KITS = ["sdtk-spec", "sdtk-ops", "sdtk-code", "sdtk-design"];
|
|
22
|
+
const NON_RUNTIME_KITS = ["sdtk-wiki"];
|
|
23
|
+
|
|
24
|
+
// Build an injected deps object with a recording spawn spy.
|
|
25
|
+
// `failures` maps toolkit name → exit code to return (default 0).
|
|
26
|
+
// `unresolvable` is a Set of kit package names that resolveBin should reject.
|
|
27
|
+
function makeDeps({ failures = {}, unresolvable = new Set(), psOk = true } = {}) {
|
|
28
|
+
const spawnCalls = [];
|
|
29
|
+
const resolveCalls = [];
|
|
30
|
+
const logs = [];
|
|
31
|
+
return {
|
|
32
|
+
spawnCalls,
|
|
33
|
+
resolveCalls,
|
|
34
|
+
logs,
|
|
35
|
+
deps: {
|
|
36
|
+
spawn(binPath, argv, toolkit) {
|
|
37
|
+
spawnCalls.push({ binPath, argv, toolkit: toolkit.name });
|
|
38
|
+
const code = failures[toolkit.name] || 0;
|
|
39
|
+
return { status: code, stderr: code ? `stub stderr for ${toolkit.name}` : "" };
|
|
40
|
+
},
|
|
41
|
+
resolveBin(kitPkg, binName) {
|
|
42
|
+
resolveCalls.push(kitPkg);
|
|
43
|
+
if (unresolvable.has(kitPkg)) {
|
|
44
|
+
throw new ToolkitResolveError(kitPkg);
|
|
45
|
+
}
|
|
46
|
+
return `/stub/node_modules/${kitPkg}/bin/${binName}.js`;
|
|
47
|
+
},
|
|
48
|
+
powershellCheck() {
|
|
49
|
+
return { ok: psOk, exe: "pwsh" };
|
|
50
|
+
},
|
|
51
|
+
log(line) {
|
|
52
|
+
logs.push(line);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Pull the argv recorded for one toolkit's spawn.
|
|
59
|
+
function argvFor(spawnCalls, name) {
|
|
60
|
+
const call = spawnCalls.find((c) => c.toolkit === name);
|
|
61
|
+
return call ? call.argv : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const tests = [];
|
|
65
|
+
function test(name, fn) {
|
|
66
|
+
tests.push({ name, fn });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── T1 — runtime claude forwards correctly to runtime kits ──────────────────
|
|
70
|
+
test("T1 runtime=claude spawns spec/ops/code with init --runtime claude", () => {
|
|
71
|
+
const h = makeDeps();
|
|
72
|
+
const { exitCode } = runUnifiedInit({ runtime: "claude" }, h.deps);
|
|
73
|
+
assert.strictEqual(exitCode, 0);
|
|
74
|
+
for (const kit of RUNTIME_KITS) {
|
|
75
|
+
const argv = argvFor(h.spawnCalls, kit);
|
|
76
|
+
assert.deepStrictEqual(argv, ["init", "--runtime", "claude"], `${kit} argv`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ── T2 — runtime codex ──────────────────────────────────────────────────────
|
|
81
|
+
test("T2 runtime=codex forwards --runtime codex", () => {
|
|
82
|
+
const h = makeDeps();
|
|
83
|
+
const { exitCode } = runUnifiedInit({ runtime: "codex" }, h.deps);
|
|
84
|
+
assert.strictEqual(exitCode, 0);
|
|
85
|
+
for (const kit of RUNTIME_KITS) {
|
|
86
|
+
assert.deepStrictEqual(argvFor(h.spawnCalls, kit), ["init", "--runtime", "codex"]);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ── T3 — flag forwarding (runtime subset vs full) ───────────────────────────
|
|
91
|
+
test("T3 forwards shared flags to runtime kits; only subset to design/wiki", () => {
|
|
92
|
+
const h = makeDeps();
|
|
93
|
+
const opts = {
|
|
94
|
+
runtime: "claude",
|
|
95
|
+
runtimeScope: "user",
|
|
96
|
+
projectPath: "/tmp/proj",
|
|
97
|
+
force: true,
|
|
98
|
+
skipRuntimeAssets: true,
|
|
99
|
+
verbose: true,
|
|
100
|
+
};
|
|
101
|
+
runUnifiedInit(opts, h.deps);
|
|
102
|
+
|
|
103
|
+
for (const kit of ASSET_KITS) {
|
|
104
|
+
const argv = argvFor(h.spawnCalls, kit);
|
|
105
|
+
assert.deepStrictEqual(
|
|
106
|
+
argv,
|
|
107
|
+
[
|
|
108
|
+
"init",
|
|
109
|
+
"--runtime",
|
|
110
|
+
"claude",
|
|
111
|
+
"--runtime-scope",
|
|
112
|
+
"user",
|
|
113
|
+
"--project-path",
|
|
114
|
+
"/tmp/proj",
|
|
115
|
+
"--force",
|
|
116
|
+
"--skip-runtime-assets",
|
|
117
|
+
"--verbose",
|
|
118
|
+
],
|
|
119
|
+
`${kit} full forward`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
// design is runtime-aware (gets --runtime/--runtime-scope) but owns no
|
|
123
|
+
// PowerShell payload, so it must NOT receive --skip-runtime-assets.
|
|
124
|
+
{
|
|
125
|
+
const argv = argvFor(h.spawnCalls, "sdtk-design");
|
|
126
|
+
assert.deepStrictEqual(
|
|
127
|
+
argv,
|
|
128
|
+
[
|
|
129
|
+
"init",
|
|
130
|
+
"--runtime",
|
|
131
|
+
"claude",
|
|
132
|
+
"--runtime-scope",
|
|
133
|
+
"user",
|
|
134
|
+
"--project-path",
|
|
135
|
+
"/tmp/proj",
|
|
136
|
+
"--force",
|
|
137
|
+
"--verbose",
|
|
138
|
+
],
|
|
139
|
+
"sdtk-design runtime-aware forward (no --skip-runtime-assets)"
|
|
140
|
+
);
|
|
141
|
+
assert.ok(!argv.includes("--skip-runtime-assets"), "design must not get --skip-runtime-assets");
|
|
142
|
+
}
|
|
143
|
+
for (const kit of NON_RUNTIME_KITS) {
|
|
144
|
+
const argv = argvFor(h.spawnCalls, kit);
|
|
145
|
+
assert.deepStrictEqual(
|
|
146
|
+
argv,
|
|
147
|
+
["init", "--project-path", "/tmp/proj", "--force", "--verbose"],
|
|
148
|
+
`${kit} subset`
|
|
149
|
+
);
|
|
150
|
+
assert.ok(!argv.includes("--runtime"), `${kit} must not get --runtime`);
|
|
151
|
+
assert.ok(!argv.includes("--runtime-scope"), `${kit} must not get --runtime-scope`);
|
|
152
|
+
assert.ok(!argv.includes("--skip-runtime-assets"), `${kit} must not get --skip-runtime-assets`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── T4 — missing/invalid runtime → exit 2, zero spawns ──────────────────────
|
|
157
|
+
test("T4 missing runtime → exit 2, no spawns", () => {
|
|
158
|
+
const h = makeDeps();
|
|
159
|
+
const { exitCode } = runUnifiedInit({}, h.deps);
|
|
160
|
+
assert.strictEqual(exitCode, 2);
|
|
161
|
+
assert.strictEqual(h.spawnCalls.length, 0);
|
|
162
|
+
});
|
|
163
|
+
test("T4b invalid runtime → exit 2, no spawns", () => {
|
|
164
|
+
const h = makeDeps();
|
|
165
|
+
const { exitCode } = runUnifiedInit({ runtime: "bogus" }, h.deps);
|
|
166
|
+
assert.strictEqual(exitCode, 2);
|
|
167
|
+
assert.strictEqual(h.spawnCalls.length, 0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── T5 — PowerShell missing → exit 3, zero spawns ───────────────────────────
|
|
171
|
+
test("T5 powershell missing → exit 3, no spawns", () => {
|
|
172
|
+
const h = makeDeps({ psOk: false });
|
|
173
|
+
const { exitCode } = runUnifiedInit({ runtime: "claude" }, h.deps);
|
|
174
|
+
assert.strictEqual(exitCode, 3);
|
|
175
|
+
assert.strictEqual(h.spawnCalls.length, 0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── T6 — order spec → ops → code → design → wiki ────────────────────────────
|
|
179
|
+
test("T6 runs all five in registry order", () => {
|
|
180
|
+
const h = makeDeps();
|
|
181
|
+
runUnifiedInit({ runtime: "claude" }, h.deps);
|
|
182
|
+
const order = h.spawnCalls.map((c) => c.toolkit);
|
|
183
|
+
assert.deepStrictEqual(order, [
|
|
184
|
+
"sdtk-spec",
|
|
185
|
+
"sdtk-ops",
|
|
186
|
+
"sdtk-code",
|
|
187
|
+
"sdtk-design",
|
|
188
|
+
"sdtk-wiki",
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── T7 — fail-fast default ──────────────────────────────────────────────────
|
|
193
|
+
test("T7 fail-fast: middle failure stops subsequent toolkits, exit = child code", () => {
|
|
194
|
+
const h = makeDeps({ failures: { "sdtk-code": 7 } });
|
|
195
|
+
const { exitCode } = runUnifiedInit({ runtime: "claude" }, h.deps);
|
|
196
|
+
assert.strictEqual(exitCode, 7);
|
|
197
|
+
const order = h.spawnCalls.map((c) => c.toolkit);
|
|
198
|
+
assert.deepStrictEqual(order, ["sdtk-spec", "sdtk-ops", "sdtk-code"]);
|
|
199
|
+
assert.ok(!order.includes("sdtk-design"), "design must not be spawned after fail");
|
|
200
|
+
assert.ok(!order.includes("sdtk-wiki"), "wiki must not be spawned after fail");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── T8 — --keep-going ───────────────────────────────────────────────────────
|
|
204
|
+
test("T8 keep-going: failure does not stop the rest; aggregate exit non-zero", () => {
|
|
205
|
+
const h = makeDeps({ failures: { "sdtk-code": 5 } });
|
|
206
|
+
const { exitCode, results } = runUnifiedInit({ runtime: "claude", keepGoing: true }, h.deps);
|
|
207
|
+
assert.strictEqual(exitCode, 5);
|
|
208
|
+
assert.strictEqual(h.spawnCalls.length, 5);
|
|
209
|
+
const failed = results.find((r) => r.name === "sdtk-code");
|
|
210
|
+
assert.strictEqual(failed.status, "FAILED");
|
|
211
|
+
const wiki = results.find((r) => r.name === "sdtk-wiki");
|
|
212
|
+
assert.strictEqual(wiki.status, "OK");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ── T9 — unresolvable kit → exit 4 naming the kit ───────────────────────────
|
|
216
|
+
test("T9 unresolvable kit → exit 4, names the kit, fail-fast no spawn", () => {
|
|
217
|
+
const h = makeDeps({ unresolvable: new Set(["sdtk-spec-kit"]) });
|
|
218
|
+
const { exitCode, results } = runUnifiedInit({ runtime: "claude" }, h.deps);
|
|
219
|
+
assert.strictEqual(exitCode, 4);
|
|
220
|
+
assert.strictEqual(h.spawnCalls.length, 0, "no spawns once first kit unresolvable (fail-fast)");
|
|
221
|
+
assert.ok(results[0].statusLabel.includes("sdtk-spec-kit"), "kit named in status");
|
|
222
|
+
});
|
|
223
|
+
test("T9b unresolvable kit with --keep-going continues, exit 4", () => {
|
|
224
|
+
const h = makeDeps({ unresolvable: new Set(["sdtk-ops-kit"]) });
|
|
225
|
+
const { exitCode } = runUnifiedInit({ runtime: "claude", keepGoing: true }, h.deps);
|
|
226
|
+
assert.strictEqual(exitCode, 4);
|
|
227
|
+
// spec + code + design + wiki spawn; ops is skipped (unresolvable)
|
|
228
|
+
const order = h.spawnCalls.map((c) => c.toolkit);
|
|
229
|
+
assert.deepStrictEqual(order, ["sdtk-spec", "sdtk-code", "sdtk-design", "sdtk-wiki"]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ── T10 — orchestrator only touches injected deps (no real fs/network/spawn) ─
|
|
233
|
+
test("T10 orchestrator uses only injected deps (no real host access)", () => {
|
|
234
|
+
const h = makeDeps();
|
|
235
|
+
// Spy spawn/resolveBin do no real I/O. A successful run that records exactly
|
|
236
|
+
// the expected spy invocations proves the orchestrator never bypassed the seam.
|
|
237
|
+
runUnifiedInit({ runtime: "claude" }, h.deps);
|
|
238
|
+
assert.strictEqual(h.spawnCalls.length, 5, "all process work went through injected spawn");
|
|
239
|
+
assert.deepStrictEqual(h.resolveCalls, [
|
|
240
|
+
"sdtk-spec-kit",
|
|
241
|
+
"sdtk-ops-kit",
|
|
242
|
+
"sdtk-code-kit",
|
|
243
|
+
"sdtk-design-kit",
|
|
244
|
+
"sdtk-wiki-kit",
|
|
245
|
+
]);
|
|
246
|
+
// buildInitArgs is pure: same input → same output, no side effects.
|
|
247
|
+
const a = buildInitArgs(TOOLKITS[0], { runtime: "claude" });
|
|
248
|
+
const b = buildInitArgs(TOOLKITS[0], { runtime: "claude" });
|
|
249
|
+
assert.deepStrictEqual(a, b);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── T11 — --global shorthand maps to runtime-scope user ─────────────────────
|
|
253
|
+
test("T11 --global parses and maps to runtimeScope=user", () => {
|
|
254
|
+
const flags = parseFlags(["--runtime", "claude", "--global"]);
|
|
255
|
+
assert.strictEqual(flags.global, true);
|
|
256
|
+
const opts = buildOpts(flags);
|
|
257
|
+
assert.strictEqual(opts.runtimeScope, "user");
|
|
258
|
+
});
|
|
259
|
+
test("T11b explicit --runtime-scope wins over --global", () => {
|
|
260
|
+
const opts = buildOpts(parseFlags(["--runtime", "codex", "--global", "--runtime-scope", "project"]));
|
|
261
|
+
assert.strictEqual(opts.runtimeScope, "project");
|
|
262
|
+
});
|
|
263
|
+
test("T11c no scope flag leaves runtimeScope undefined (kits apply their default)", () => {
|
|
264
|
+
const opts = buildOpts(parseFlags(["--runtime", "codex"]));
|
|
265
|
+
assert.strictEqual(opts.runtimeScope, undefined);
|
|
266
|
+
});
|
|
267
|
+
test("T11d --global forwards --runtime-scope user to design (runtime-aware)", () => {
|
|
268
|
+
const h = makeDeps();
|
|
269
|
+
runUnifiedInit(buildOpts(parseFlags(["--runtime", "claude", "--global"])), h.deps);
|
|
270
|
+
assert.deepStrictEqual(
|
|
271
|
+
argvFor(h.spawnCalls, "sdtk-design"),
|
|
272
|
+
["init", "--runtime", "claude", "--runtime-scope", "user"]
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── runner ──────────────────────────────────────────────────────────────────
|
|
277
|
+
let passed = 0;
|
|
278
|
+
let failed = 0;
|
|
279
|
+
for (const t of tests) {
|
|
280
|
+
try {
|
|
281
|
+
t.fn();
|
|
282
|
+
passed += 1;
|
|
283
|
+
console.log(` ok ${t.name}`);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
failed += 1;
|
|
286
|
+
console.error(`FAIL ${t.name}`);
|
|
287
|
+
console.error(` ${err.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
console.log("");
|
|
291
|
+
console.log(`unified-init tests: ${passed} passed, ${failed} failed`);
|
|
292
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// `sdtk init` command (umbrella). Parses flags, wires real effectful deps, and
|
|
4
|
+
// delegates to the pure orchestrator in src/lib/unified-init.js.
|
|
5
|
+
|
|
6
|
+
const { spawnSync } = require("child_process");
|
|
7
|
+
const {
|
|
8
|
+
runUnifiedInit,
|
|
9
|
+
resolveToolkitBin,
|
|
10
|
+
checkPowerShellAvailable,
|
|
11
|
+
} = require("../lib/unified-init");
|
|
12
|
+
|
|
13
|
+
// Minimal local flag parser (the umbrella has no shared args lib, and we must not
|
|
14
|
+
// import a child kit's internal lib). Supports `--flag value`, `--flag=value`,
|
|
15
|
+
// and boolean `--flag`.
|
|
16
|
+
const FLAG_DEFS = Object.freeze({
|
|
17
|
+
runtime: "string",
|
|
18
|
+
"runtime-scope": "string",
|
|
19
|
+
global: "boolean",
|
|
20
|
+
"project-path": "string",
|
|
21
|
+
force: "boolean",
|
|
22
|
+
"skip-runtime-assets": "boolean",
|
|
23
|
+
"keep-going": "boolean",
|
|
24
|
+
verbose: "boolean",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function parseFlags(args) {
|
|
28
|
+
const flags = {};
|
|
29
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (!arg.startsWith("--")) {
|
|
32
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
33
|
+
}
|
|
34
|
+
let key = arg.slice(2);
|
|
35
|
+
let value;
|
|
36
|
+
const eq = key.indexOf("=");
|
|
37
|
+
if (eq !== -1) {
|
|
38
|
+
value = key.slice(eq + 1);
|
|
39
|
+
key = key.slice(0, eq);
|
|
40
|
+
}
|
|
41
|
+
const type = FLAG_DEFS[key];
|
|
42
|
+
if (!type) {
|
|
43
|
+
throw new Error(`Unknown flag: --${key}`);
|
|
44
|
+
}
|
|
45
|
+
if (type === "boolean") {
|
|
46
|
+
flags[key] = true;
|
|
47
|
+
} else {
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
value = args[i + 1];
|
|
50
|
+
i += 1;
|
|
51
|
+
}
|
|
52
|
+
if (value === undefined) {
|
|
53
|
+
throw new Error(`Flag --${key} requires a value.`);
|
|
54
|
+
}
|
|
55
|
+
flags[key] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return flags;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Pure flags → orchestrator opts mapping (exported for unit testing).
|
|
62
|
+
// --global is a shorthand for --runtime-scope user (installs skills under the
|
|
63
|
+
// user/global runtime home: ~/.claude/skills or ~/.codex/skills). An explicit
|
|
64
|
+
// --runtime-scope wins if both are supplied.
|
|
65
|
+
function buildOpts(flags) {
|
|
66
|
+
const runtimeScope = flags["runtime-scope"] || (flags.global ? "user" : undefined);
|
|
67
|
+
return {
|
|
68
|
+
runtime: flags.runtime,
|
|
69
|
+
runtimeScope,
|
|
70
|
+
projectPath: flags["project-path"],
|
|
71
|
+
force: Boolean(flags.force),
|
|
72
|
+
skipRuntimeAssets: Boolean(flags["skip-runtime-assets"]),
|
|
73
|
+
keepGoing: Boolean(flags["keep-going"]),
|
|
74
|
+
verbose: Boolean(flags.verbose),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cmdInit(args) {
|
|
79
|
+
let flags;
|
|
80
|
+
try {
|
|
81
|
+
flags = parseFlags(args);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`Error: ${err.message}`);
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const opts = buildOpts(flags);
|
|
88
|
+
|
|
89
|
+
const deps = {
|
|
90
|
+
spawn(binPath, argv) {
|
|
91
|
+
return spawnSync(process.execPath, [binPath, ...argv], { stdio: "inherit" });
|
|
92
|
+
},
|
|
93
|
+
resolveBin: resolveToolkitBin,
|
|
94
|
+
powershellCheck: () => checkPowerShellAvailable(spawnSync),
|
|
95
|
+
log: (line) => console.log(line),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const { exitCode } = runUnifiedInit(opts, deps);
|
|
99
|
+
return exitCode;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
cmdInit,
|
|
104
|
+
parseFlags,
|
|
105
|
+
buildOpts,
|
|
106
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Unified-init orchestrator (pure logic).
|
|
4
|
+
//
|
|
5
|
+
// BK-268: `sdtk init --runtime <claude|codex>` delegates to each toolkit's own,
|
|
6
|
+
// already-shipped `init`. This module re-implements NO init logic, performs NO
|
|
7
|
+
// filesystem writes, NO network calls, and runs NO PowerShell of its own — every
|
|
8
|
+
// side effect happens inside the delegated per-kit init it spawns.
|
|
9
|
+
//
|
|
10
|
+
// All effectful operations (process spawn, bin resolution, PowerShell pre-flight,
|
|
11
|
+
// logging) are passed in through an injected `deps` seam so the orchestrator runs
|
|
12
|
+
// fully offline under test.
|
|
13
|
+
|
|
14
|
+
const path = require("path");
|
|
15
|
+
|
|
16
|
+
const VALID_RUNTIMES = Object.freeze(["claude", "codex"]);
|
|
17
|
+
|
|
18
|
+
// Ordered target registry. `binName` is the bin key in each kit's package.json
|
|
19
|
+
// (resolved through its `bin` map at runtime, so a kit renaming its bin file
|
|
20
|
+
// across a minor version does not break us — see R1 in the implementation plan).
|
|
21
|
+
//
|
|
22
|
+
// `acceptsRuntime` toolkits receive --runtime (+ --runtime-scope) so they place
|
|
23
|
+
// their skills under the correct runtime home (.claude vs .codex). Of those,
|
|
24
|
+
// only `installsRuntimeAssets` toolkits (spec/ops/code, which run install.ps1)
|
|
25
|
+
// also accept --skip-runtime-assets. sdtk-design is runtime-aware (it installs
|
|
26
|
+
// the design-prototype skill into the matching skills dir) but does not own the
|
|
27
|
+
// PowerShell runtime-asset payload. sdtk-wiki is not runtime-aware (it installs
|
|
28
|
+
// no skills) and receives only --project-path / --force / --verbose.
|
|
29
|
+
const TOOLKITS = Object.freeze([
|
|
30
|
+
{ name: "sdtk-spec", kitPkg: "sdtk-spec-kit", binName: "sdtk-spec", acceptsRuntime: true, installsRuntimeAssets: true },
|
|
31
|
+
{ name: "sdtk-ops", kitPkg: "sdtk-ops-kit", binName: "sdtk-ops", acceptsRuntime: true, installsRuntimeAssets: true },
|
|
32
|
+
{ name: "sdtk-code", kitPkg: "sdtk-code-kit", binName: "sdtk-code", acceptsRuntime: true, installsRuntimeAssets: true },
|
|
33
|
+
{ name: "sdtk-design", kitPkg: "sdtk-design-kit", binName: "sdtk-design", acceptsRuntime: true, installsRuntimeAssets: false },
|
|
34
|
+
{ name: "sdtk-wiki", kitPkg: "sdtk-wiki-kit", binName: "sdtk-wiki", acceptsRuntime: false, installsRuntimeAssets: false },
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Mirrors sdtk-{spec,ops,code} scope.js: claude defaults to project, codex to user.
|
|
38
|
+
// Used only for the honest scope label in the summary; the orchestrator forwards
|
|
39
|
+
// --runtime-scope only when the user supplies it, so each kit applies this same
|
|
40
|
+
// default independently.
|
|
41
|
+
function defaultScope(runtime) {
|
|
42
|
+
return runtime === "claude" ? "project" : "user";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class ToolkitResolveError extends Error {
|
|
46
|
+
constructor(kitPkg) {
|
|
47
|
+
super(`Toolkit '${kitPkg}' could not be resolved. Is it installed as a dependency of sdtk-kit?`);
|
|
48
|
+
this.name = "ToolkitResolveError";
|
|
49
|
+
this.kitPkg = kitPkg;
|
|
50
|
+
this.exitCode = 4;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Resolve a toolkit's executable bin via its package.json `bin` map (drift-safe).
|
|
55
|
+
// Default `deps.resolveBin`. Throws ToolkitResolveError when the kit or its bin
|
|
56
|
+
// entry cannot be found.
|
|
57
|
+
function resolveToolkitBin(kitPkg, binName) {
|
|
58
|
+
let pkgJsonPath;
|
|
59
|
+
try {
|
|
60
|
+
pkgJsonPath = require.resolve(`${kitPkg}/package.json`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
throw new ToolkitResolveError(kitPkg);
|
|
63
|
+
}
|
|
64
|
+
// eslint-disable-next-line global-require
|
|
65
|
+
const pkg = require(pkgJsonPath);
|
|
66
|
+
const binField = pkg.bin;
|
|
67
|
+
let rel;
|
|
68
|
+
if (typeof binField === "string") {
|
|
69
|
+
rel = binField;
|
|
70
|
+
} else if (binField && typeof binField === "object") {
|
|
71
|
+
rel = binField[binName] || Object.values(binField)[0];
|
|
72
|
+
}
|
|
73
|
+
if (!rel) {
|
|
74
|
+
throw new ToolkitResolveError(kitPkg);
|
|
75
|
+
}
|
|
76
|
+
return path.resolve(path.dirname(pkgJsonPath), rel);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Default `deps.powershellCheck`. Single pre-flight availability probe mirroring
|
|
80
|
+
// sdtk-code/src/lib/powershell.js resolution (win32 → powershell.exe, else pwsh).
|
|
81
|
+
// Runs a no-op command; returns { ok, exe } and never throws.
|
|
82
|
+
function checkPowerShellAvailable(spawnSync) {
|
|
83
|
+
const exe = process.platform === "win32" ? "powershell.exe" : "pwsh";
|
|
84
|
+
try {
|
|
85
|
+
const res = spawnSync(
|
|
86
|
+
exe,
|
|
87
|
+
["-NoProfile", "-NonInteractive", "-Command", "$null"],
|
|
88
|
+
{ stdio: "ignore" }
|
|
89
|
+
);
|
|
90
|
+
if (res && res.error && res.error.code === "ENOENT") {
|
|
91
|
+
return { ok: false, exe };
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, exe };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return { ok: false, exe };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Build the forwarded `init` flag args for one toolkit (no leading "init").
|
|
100
|
+
// Runtime kits get --runtime (+ runtime-scope / skip-runtime-assets); non-runtime
|
|
101
|
+
// kits receive only the accepted subset.
|
|
102
|
+
function buildInitArgs(toolkit, opts) {
|
|
103
|
+
const args = [];
|
|
104
|
+
if (toolkit.acceptsRuntime) {
|
|
105
|
+
args.push("--runtime", opts.runtime);
|
|
106
|
+
if (opts.runtimeScope) {
|
|
107
|
+
args.push("--runtime-scope", opts.runtimeScope);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (opts.projectPath) {
|
|
111
|
+
args.push("--project-path", opts.projectPath);
|
|
112
|
+
}
|
|
113
|
+
if (opts.force) {
|
|
114
|
+
args.push("--force");
|
|
115
|
+
}
|
|
116
|
+
if (toolkit.installsRuntimeAssets && opts.skipRuntimeAssets) {
|
|
117
|
+
args.push("--skip-runtime-assets");
|
|
118
|
+
}
|
|
119
|
+
if (opts.verbose) {
|
|
120
|
+
args.push("--verbose");
|
|
121
|
+
}
|
|
122
|
+
return args;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeExitCode(res) {
|
|
126
|
+
if (res && typeof res.status === "number") {
|
|
127
|
+
return res.status;
|
|
128
|
+
}
|
|
129
|
+
if (res && typeof res.exitCode === "number") {
|
|
130
|
+
return res.exitCode;
|
|
131
|
+
}
|
|
132
|
+
// Spawn failure (e.g. error without numeric status) → non-zero.
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Render the final per-toolkit summary table (spec §5). Pure.
|
|
137
|
+
function renderSummary(results, opts, scopeLabel) {
|
|
138
|
+
const rows = results.map((r) => {
|
|
139
|
+
const runtimeCol = r.acceptsRuntime ? opts.runtime : "-";
|
|
140
|
+
const scopeCol = r.acceptsRuntime ? scopeLabel : "-";
|
|
141
|
+
return { toolkit: r.name, runtime: runtimeCol, scope: scopeCol, status: r.statusLabel || r.status };
|
|
142
|
+
});
|
|
143
|
+
const headers = { toolkit: "toolkit", runtime: "runtime", scope: "scope", status: "status" };
|
|
144
|
+
const width = (key) =>
|
|
145
|
+
Math.max(headers[key].length, ...rows.map((row) => String(row[key]).length));
|
|
146
|
+
const w = {
|
|
147
|
+
toolkit: width("toolkit"),
|
|
148
|
+
runtime: width("runtime"),
|
|
149
|
+
scope: width("scope"),
|
|
150
|
+
status: width("status"),
|
|
151
|
+
};
|
|
152
|
+
const pad = (val, key) => String(val).padEnd(w[key]);
|
|
153
|
+
const line = (row) =>
|
|
154
|
+
` ${pad(row.toolkit, "toolkit")} ${pad(row.runtime, "runtime")} ${pad(row.scope, "scope")} ${pad(
|
|
155
|
+
row.status,
|
|
156
|
+
"status"
|
|
157
|
+
)}`.replace(/\s+$/, "");
|
|
158
|
+
const out = ["Summary", line(headers)];
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
out.push(line(row));
|
|
161
|
+
}
|
|
162
|
+
return out.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Core orchestrator. `deps` = { spawn, resolveBin, powershellCheck, log }.
|
|
166
|
+
// spawn(binPath, argv, toolkit) → { status|exitCode, stderr? }
|
|
167
|
+
// resolveBin(kitPkg, binName) → absolute bin path (throws ToolkitResolveError)
|
|
168
|
+
// powershellCheck() → { ok, exe }
|
|
169
|
+
// log(line) → progress/summary sink
|
|
170
|
+
// Returns { exitCode, results }. Never writes files / opens sockets itself.
|
|
171
|
+
function runUnifiedInit(opts, deps) {
|
|
172
|
+
const spawn = deps.spawn;
|
|
173
|
+
const resolveBin = deps.resolveBin || resolveToolkitBin;
|
|
174
|
+
const powershellCheck = deps.powershellCheck;
|
|
175
|
+
const log = deps.log || (() => {});
|
|
176
|
+
|
|
177
|
+
// 1. Required, validated --runtime (no spawns on failure). Exit 2.
|
|
178
|
+
if (!opts.runtime || !VALID_RUNTIMES.includes(opts.runtime)) {
|
|
179
|
+
log(`Error: --runtime is required and must be one of: ${VALID_RUNTIMES.join(", ")}.`);
|
|
180
|
+
return { exitCode: 2, results: [] };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 2. Single PowerShell pre-flight, fail-closed (no spawns on failure). Exit 3.
|
|
184
|
+
const ps = powershellCheck();
|
|
185
|
+
if (!ps || !ps.ok) {
|
|
186
|
+
const exe = (ps && ps.exe) || "pwsh";
|
|
187
|
+
log(
|
|
188
|
+
`Error: PowerShell not found (tried: ${exe}). All runtime toolkit inits require ` +
|
|
189
|
+
"PowerShell. Install PowerShell and ensure it is on PATH, then retry."
|
|
190
|
+
);
|
|
191
|
+
return { exitCode: 3, results: [] };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const scopeLabel = opts.runtimeScope || defaultScope(opts.runtime);
|
|
195
|
+
log(`SDTK unified init — runtime: ${opts.runtime}, scope: ${scopeLabel}`);
|
|
196
|
+
|
|
197
|
+
const results = [];
|
|
198
|
+
let firstFailure = 0;
|
|
199
|
+
const total = TOOLKITS.length;
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < total; i += 1) {
|
|
202
|
+
const toolkit = TOOLKITS[i];
|
|
203
|
+
const idx = `[${i + 1}/${total}]`;
|
|
204
|
+
const suffix = toolkit.acceptsRuntime ? "" : " (not runtime-aware)";
|
|
205
|
+
|
|
206
|
+
let binPath;
|
|
207
|
+
try {
|
|
208
|
+
binPath = resolveBin(toolkit.kitPkg, toolkit.binName);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const code = typeof err.exitCode === "number" ? err.exitCode : 4;
|
|
211
|
+
results.push({
|
|
212
|
+
name: toolkit.name,
|
|
213
|
+
acceptsRuntime: toolkit.acceptsRuntime,
|
|
214
|
+
status: "FAILED",
|
|
215
|
+
statusLabel: `FAILED (kit '${toolkit.kitPkg}' not found)`,
|
|
216
|
+
exitCode: code,
|
|
217
|
+
});
|
|
218
|
+
log(` ${idx} ${toolkit.name} … FAILED — kit '${toolkit.kitPkg}' not resolvable`);
|
|
219
|
+
if (!firstFailure) {
|
|
220
|
+
firstFailure = code;
|
|
221
|
+
}
|
|
222
|
+
if (!opts.keepGoing) {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const argv = ["init", ...buildInitArgs(toolkit, opts)];
|
|
229
|
+
const res = spawn(binPath, argv, toolkit);
|
|
230
|
+
const code = normalizeExitCode(res);
|
|
231
|
+
|
|
232
|
+
if (code === 0) {
|
|
233
|
+
results.push({
|
|
234
|
+
name: toolkit.name,
|
|
235
|
+
acceptsRuntime: toolkit.acceptsRuntime,
|
|
236
|
+
status: "OK",
|
|
237
|
+
exitCode: 0,
|
|
238
|
+
});
|
|
239
|
+
log(` ${idx} ${toolkit.name} … OK${suffix}`);
|
|
240
|
+
} else {
|
|
241
|
+
results.push({
|
|
242
|
+
name: toolkit.name,
|
|
243
|
+
acceptsRuntime: toolkit.acceptsRuntime,
|
|
244
|
+
status: "FAILED",
|
|
245
|
+
statusLabel: `FAILED (exit ${code})`,
|
|
246
|
+
exitCode: code,
|
|
247
|
+
});
|
|
248
|
+
log(` ${idx} ${toolkit.name} … FAILED (exit ${code})`);
|
|
249
|
+
if (res && res.stderr) {
|
|
250
|
+
log(String(res.stderr).trimEnd());
|
|
251
|
+
}
|
|
252
|
+
if (!firstFailure) {
|
|
253
|
+
firstFailure = code;
|
|
254
|
+
}
|
|
255
|
+
if (!opts.keepGoing) {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
log("");
|
|
262
|
+
log(renderSummary(results, opts, scopeLabel));
|
|
263
|
+
const exitCode = firstFailure || 0;
|
|
264
|
+
if (exitCode === 0) {
|
|
265
|
+
log(`All toolkits initialised for the ${opts.runtime} runtime.`);
|
|
266
|
+
}
|
|
267
|
+
return { exitCode, results };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
VALID_RUNTIMES,
|
|
272
|
+
TOOLKITS,
|
|
273
|
+
ToolkitResolveError,
|
|
274
|
+
defaultScope,
|
|
275
|
+
resolveToolkitBin,
|
|
276
|
+
checkPowerShellAvailable,
|
|
277
|
+
buildInitArgs,
|
|
278
|
+
renderSummary,
|
|
279
|
+
runUnifiedInit,
|
|
280
|
+
};
|