sdtk-kit 1.2.0 → 1.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.
@@ -0,0 +1,95 @@
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
+ "project-path": "string",
20
+ force: "boolean",
21
+ "skip-runtime-assets": "boolean",
22
+ "keep-going": "boolean",
23
+ verbose: "boolean",
24
+ });
25
+
26
+ function parseFlags(args) {
27
+ const flags = {};
28
+ for (let i = 0; i < args.length; i += 1) {
29
+ const arg = args[i];
30
+ if (!arg.startsWith("--")) {
31
+ throw new Error(`Unexpected argument: ${arg}`);
32
+ }
33
+ let key = arg.slice(2);
34
+ let value;
35
+ const eq = key.indexOf("=");
36
+ if (eq !== -1) {
37
+ value = key.slice(eq + 1);
38
+ key = key.slice(0, eq);
39
+ }
40
+ const type = FLAG_DEFS[key];
41
+ if (!type) {
42
+ throw new Error(`Unknown flag: --${key}`);
43
+ }
44
+ if (type === "boolean") {
45
+ flags[key] = true;
46
+ } else {
47
+ if (value === undefined) {
48
+ value = args[i + 1];
49
+ i += 1;
50
+ }
51
+ if (value === undefined) {
52
+ throw new Error(`Flag --${key} requires a value.`);
53
+ }
54
+ flags[key] = value;
55
+ }
56
+ }
57
+ return flags;
58
+ }
59
+
60
+ function cmdInit(args) {
61
+ let flags;
62
+ try {
63
+ flags = parseFlags(args);
64
+ } catch (err) {
65
+ console.error(`Error: ${err.message}`);
66
+ return 2;
67
+ }
68
+
69
+ const opts = {
70
+ runtime: flags.runtime,
71
+ runtimeScope: flags["runtime-scope"],
72
+ projectPath: flags["project-path"],
73
+ force: Boolean(flags.force),
74
+ skipRuntimeAssets: Boolean(flags["skip-runtime-assets"]),
75
+ keepGoing: Boolean(flags["keep-going"]),
76
+ verbose: Boolean(flags.verbose),
77
+ };
78
+
79
+ const deps = {
80
+ spawn(binPath, argv) {
81
+ return spawnSync(process.execPath, [binPath, ...argv], { stdio: "inherit" });
82
+ },
83
+ resolveBin: resolveToolkitBin,
84
+ powershellCheck: () => checkPowerShellAvailable(spawnSync),
85
+ log: (line) => console.log(line),
86
+ };
87
+
88
+ const { exitCode } = runUnifiedInit(opts, deps);
89
+ return exitCode;
90
+ }
91
+
92
+ module.exports = {
93
+ cmdInit,
94
+ parseFlags,
95
+ };
@@ -0,0 +1,274 @@
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
+ // `acceptsRuntime: false` toolkits run their own non-runtime init and receive
22
+ // only the flags they accept (--project-path / --force / --verbose).
23
+ const TOOLKITS = Object.freeze([
24
+ { name: "sdtk-spec", kitPkg: "sdtk-spec-kit", binName: "sdtk-spec", acceptsRuntime: true },
25
+ { name: "sdtk-ops", kitPkg: "sdtk-ops-kit", binName: "sdtk-ops", acceptsRuntime: true },
26
+ { name: "sdtk-code", kitPkg: "sdtk-code-kit", binName: "sdtk-code", acceptsRuntime: true },
27
+ { name: "sdtk-design", kitPkg: "sdtk-design-kit", binName: "sdtk-design", acceptsRuntime: false },
28
+ { name: "sdtk-wiki", kitPkg: "sdtk-wiki-kit", binName: "sdtk-wiki", acceptsRuntime: false },
29
+ ]);
30
+
31
+ // Mirrors sdtk-{spec,ops,code} scope.js: claude defaults to project, codex to user.
32
+ // Used only for the honest scope label in the summary; the orchestrator forwards
33
+ // --runtime-scope only when the user supplies it, so each kit applies this same
34
+ // default independently.
35
+ function defaultScope(runtime) {
36
+ return runtime === "claude" ? "project" : "user";
37
+ }
38
+
39
+ class ToolkitResolveError extends Error {
40
+ constructor(kitPkg) {
41
+ super(`Toolkit '${kitPkg}' could not be resolved. Is it installed as a dependency of sdtk-kit?`);
42
+ this.name = "ToolkitResolveError";
43
+ this.kitPkg = kitPkg;
44
+ this.exitCode = 4;
45
+ }
46
+ }
47
+
48
+ // Resolve a toolkit's executable bin via its package.json `bin` map (drift-safe).
49
+ // Default `deps.resolveBin`. Throws ToolkitResolveError when the kit or its bin
50
+ // entry cannot be found.
51
+ function resolveToolkitBin(kitPkg, binName) {
52
+ let pkgJsonPath;
53
+ try {
54
+ pkgJsonPath = require.resolve(`${kitPkg}/package.json`);
55
+ } catch (err) {
56
+ throw new ToolkitResolveError(kitPkg);
57
+ }
58
+ // eslint-disable-next-line global-require
59
+ const pkg = require(pkgJsonPath);
60
+ const binField = pkg.bin;
61
+ let rel;
62
+ if (typeof binField === "string") {
63
+ rel = binField;
64
+ } else if (binField && typeof binField === "object") {
65
+ rel = binField[binName] || Object.values(binField)[0];
66
+ }
67
+ if (!rel) {
68
+ throw new ToolkitResolveError(kitPkg);
69
+ }
70
+ return path.resolve(path.dirname(pkgJsonPath), rel);
71
+ }
72
+
73
+ // Default `deps.powershellCheck`. Single pre-flight availability probe mirroring
74
+ // sdtk-code/src/lib/powershell.js resolution (win32 → powershell.exe, else pwsh).
75
+ // Runs a no-op command; returns { ok, exe } and never throws.
76
+ function checkPowerShellAvailable(spawnSync) {
77
+ const exe = process.platform === "win32" ? "powershell.exe" : "pwsh";
78
+ try {
79
+ const res = spawnSync(
80
+ exe,
81
+ ["-NoProfile", "-NonInteractive", "-Command", "$null"],
82
+ { stdio: "ignore" }
83
+ );
84
+ if (res && res.error && res.error.code === "ENOENT") {
85
+ return { ok: false, exe };
86
+ }
87
+ return { ok: true, exe };
88
+ } catch (err) {
89
+ return { ok: false, exe };
90
+ }
91
+ }
92
+
93
+ // Build the forwarded `init` flag args for one toolkit (no leading "init").
94
+ // Runtime kits get --runtime (+ runtime-scope / skip-runtime-assets); non-runtime
95
+ // kits receive only the accepted subset.
96
+ function buildInitArgs(toolkit, opts) {
97
+ const args = [];
98
+ if (toolkit.acceptsRuntime) {
99
+ args.push("--runtime", opts.runtime);
100
+ if (opts.runtimeScope) {
101
+ args.push("--runtime-scope", opts.runtimeScope);
102
+ }
103
+ }
104
+ if (opts.projectPath) {
105
+ args.push("--project-path", opts.projectPath);
106
+ }
107
+ if (opts.force) {
108
+ args.push("--force");
109
+ }
110
+ if (toolkit.acceptsRuntime && opts.skipRuntimeAssets) {
111
+ args.push("--skip-runtime-assets");
112
+ }
113
+ if (opts.verbose) {
114
+ args.push("--verbose");
115
+ }
116
+ return args;
117
+ }
118
+
119
+ function normalizeExitCode(res) {
120
+ if (res && typeof res.status === "number") {
121
+ return res.status;
122
+ }
123
+ if (res && typeof res.exitCode === "number") {
124
+ return res.exitCode;
125
+ }
126
+ // Spawn failure (e.g. error without numeric status) → non-zero.
127
+ return 1;
128
+ }
129
+
130
+ // Render the final per-toolkit summary table (spec §5). Pure.
131
+ function renderSummary(results, opts, scopeLabel) {
132
+ const rows = results.map((r) => {
133
+ const runtimeCol = r.acceptsRuntime ? opts.runtime : "-";
134
+ const scopeCol = r.acceptsRuntime ? scopeLabel : "-";
135
+ return { toolkit: r.name, runtime: runtimeCol, scope: scopeCol, status: r.statusLabel || r.status };
136
+ });
137
+ const headers = { toolkit: "toolkit", runtime: "runtime", scope: "scope", status: "status" };
138
+ const width = (key) =>
139
+ Math.max(headers[key].length, ...rows.map((row) => String(row[key]).length));
140
+ const w = {
141
+ toolkit: width("toolkit"),
142
+ runtime: width("runtime"),
143
+ scope: width("scope"),
144
+ status: width("status"),
145
+ };
146
+ const pad = (val, key) => String(val).padEnd(w[key]);
147
+ const line = (row) =>
148
+ ` ${pad(row.toolkit, "toolkit")} ${pad(row.runtime, "runtime")} ${pad(row.scope, "scope")} ${pad(
149
+ row.status,
150
+ "status"
151
+ )}`.replace(/\s+$/, "");
152
+ const out = ["Summary", line(headers)];
153
+ for (const row of rows) {
154
+ out.push(line(row));
155
+ }
156
+ return out.join("\n");
157
+ }
158
+
159
+ // Core orchestrator. `deps` = { spawn, resolveBin, powershellCheck, log }.
160
+ // spawn(binPath, argv, toolkit) → { status|exitCode, stderr? }
161
+ // resolveBin(kitPkg, binName) → absolute bin path (throws ToolkitResolveError)
162
+ // powershellCheck() → { ok, exe }
163
+ // log(line) → progress/summary sink
164
+ // Returns { exitCode, results }. Never writes files / opens sockets itself.
165
+ function runUnifiedInit(opts, deps) {
166
+ const spawn = deps.spawn;
167
+ const resolveBin = deps.resolveBin || resolveToolkitBin;
168
+ const powershellCheck = deps.powershellCheck;
169
+ const log = deps.log || (() => {});
170
+
171
+ // 1. Required, validated --runtime (no spawns on failure). Exit 2.
172
+ if (!opts.runtime || !VALID_RUNTIMES.includes(opts.runtime)) {
173
+ log(`Error: --runtime is required and must be one of: ${VALID_RUNTIMES.join(", ")}.`);
174
+ return { exitCode: 2, results: [] };
175
+ }
176
+
177
+ // 2. Single PowerShell pre-flight, fail-closed (no spawns on failure). Exit 3.
178
+ const ps = powershellCheck();
179
+ if (!ps || !ps.ok) {
180
+ const exe = (ps && ps.exe) || "pwsh";
181
+ log(
182
+ `Error: PowerShell not found (tried: ${exe}). All runtime toolkit inits require ` +
183
+ "PowerShell. Install PowerShell and ensure it is on PATH, then retry."
184
+ );
185
+ return { exitCode: 3, results: [] };
186
+ }
187
+
188
+ const scopeLabel = opts.runtimeScope || defaultScope(opts.runtime);
189
+ log(`SDTK unified init — runtime: ${opts.runtime}, scope: ${scopeLabel}`);
190
+
191
+ const results = [];
192
+ let firstFailure = 0;
193
+ const total = TOOLKITS.length;
194
+
195
+ for (let i = 0; i < total; i += 1) {
196
+ const toolkit = TOOLKITS[i];
197
+ const idx = `[${i + 1}/${total}]`;
198
+ const suffix = toolkit.acceptsRuntime ? "" : " (not runtime-aware)";
199
+
200
+ let binPath;
201
+ try {
202
+ binPath = resolveBin(toolkit.kitPkg, toolkit.binName);
203
+ } catch (err) {
204
+ const code = typeof err.exitCode === "number" ? err.exitCode : 4;
205
+ results.push({
206
+ name: toolkit.name,
207
+ acceptsRuntime: toolkit.acceptsRuntime,
208
+ status: "FAILED",
209
+ statusLabel: `FAILED (kit '${toolkit.kitPkg}' not found)`,
210
+ exitCode: code,
211
+ });
212
+ log(` ${idx} ${toolkit.name} … FAILED — kit '${toolkit.kitPkg}' not resolvable`);
213
+ if (!firstFailure) {
214
+ firstFailure = code;
215
+ }
216
+ if (!opts.keepGoing) {
217
+ break;
218
+ }
219
+ continue;
220
+ }
221
+
222
+ const argv = ["init", ...buildInitArgs(toolkit, opts)];
223
+ const res = spawn(binPath, argv, toolkit);
224
+ const code = normalizeExitCode(res);
225
+
226
+ if (code === 0) {
227
+ results.push({
228
+ name: toolkit.name,
229
+ acceptsRuntime: toolkit.acceptsRuntime,
230
+ status: "OK",
231
+ exitCode: 0,
232
+ });
233
+ log(` ${idx} ${toolkit.name} … OK${suffix}`);
234
+ } else {
235
+ results.push({
236
+ name: toolkit.name,
237
+ acceptsRuntime: toolkit.acceptsRuntime,
238
+ status: "FAILED",
239
+ statusLabel: `FAILED (exit ${code})`,
240
+ exitCode: code,
241
+ });
242
+ log(` ${idx} ${toolkit.name} … FAILED (exit ${code})`);
243
+ if (res && res.stderr) {
244
+ log(String(res.stderr).trimEnd());
245
+ }
246
+ if (!firstFailure) {
247
+ firstFailure = code;
248
+ }
249
+ if (!opts.keepGoing) {
250
+ break;
251
+ }
252
+ }
253
+ }
254
+
255
+ log("");
256
+ log(renderSummary(results, opts, scopeLabel));
257
+ const exitCode = firstFailure || 0;
258
+ if (exitCode === 0) {
259
+ log(`All toolkits initialised for the ${opts.runtime} runtime.`);
260
+ }
261
+ return { exitCode, results };
262
+ }
263
+
264
+ module.exports = {
265
+ VALID_RUNTIMES,
266
+ TOOLKITS,
267
+ ToolkitResolveError,
268
+ defaultScope,
269
+ resolveToolkitBin,
270
+ checkPowerShellAvailable,
271
+ buildInitArgs,
272
+ renderSummary,
273
+ runUnifiedInit,
274
+ };