sdtk-kit 1.1.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.
package/README.md CHANGED
@@ -6,10 +6,11 @@
6
6
  npm install -g sdtk-kit
7
7
  ```
8
8
 
9
- After install, all five SDTK CLI tools are available globally:
9
+ After install, all five SDTK CLI tools — plus the unified `sdtk` orchestrator — are available globally:
10
10
 
11
11
  | CLI | Toolkit | Purpose |
12
12
  |---------------|----------------|--------------------------------------------------|
13
+ | `sdtk` | (all five) | One-command setup: `sdtk init --runtime <r>` |
13
14
  | `sdtk-spec` | SDTK-SPEC | Spec-first SDLC: PM → BA → ARCH → DEV → QA |
14
15
  | `sdtk-design` | SDTK-DESIGN | MVP design: idea → prototype → handoff |
15
16
  | `sdtk-code` | SDTK-CODE | Governed coding: handoff → PR with review gates |
@@ -19,13 +20,41 @@ After install, all five SDTK CLI tools are available globally:
19
20
  ## Quick start
20
21
 
21
22
  ```bash
22
- # Initialize the spec toolkit in your project
23
- sdtk-spec init --runtime claude # or --runtime codex
23
+ # Set up the chosen runtime for the whole suite in one command
24
+ sdtk init --runtime claude # or --runtime codex
24
25
 
25
26
  # Generate a 17-file spec scaffold for your first feature
26
27
  sdtk-spec generate --feature-key MY_FEATURE
27
28
  ```
28
29
 
30
+ ## Unified init: `sdtk init`
31
+
32
+ `sdtk init --runtime <claude|codex>` initialises **all five toolkits** in one step
33
+ — mirroring how `npm install -g sdtk-kit` installs every toolkit at once. It runs
34
+ each toolkit's own, already-shipped `init` in order:
35
+
36
+ ```
37
+ sdtk-spec → sdtk-ops → sdtk-code (with --runtime <r>)
38
+ sdtk-design → sdtk-wiki (their own non-runtime init)
39
+ ```
40
+
41
+ ```bash
42
+ sdtk init --runtime claude
43
+ sdtk init --runtime codex --project-path ./my-app
44
+ sdtk init --runtime claude --keep-going # continue past a failing toolkit
45
+ ```
46
+
47
+ Options: `--runtime <claude|codex>` (required), `--runtime-scope <scope>`,
48
+ `--project-path <path>`, `--force`, `--skip-runtime-assets`, `--keep-going`
49
+ (default is fail-fast — stop at the first failing toolkit), `--verbose`.
50
+ `--runtime`, `--runtime-scope`, and `--skip-runtime-assets` apply to the runtime
51
+ toolkits (spec/ops/code); `sdtk-design` and `sdtk-wiki` receive only
52
+ `--project-path`, `--force`, and `--verbose`.
53
+
54
+ `sdtk init` is a thin orchestrator: it re-implements no init logic, writes no files
55
+ itself, and runs no PowerShell of its own — every side effect happens inside the
56
+ delegated per-toolkit init. The per-toolkit CLIs remain fully available standalone.
57
+
29
58
  ## Unified vs standalone install
30
59
 
31
60
  You have two install options:
@@ -60,9 +89,14 @@ because `npm install -g <pkg>` only links the bin entries of the top-level
60
89
  package, never of its dependencies. A pure deps-only meta-package would install
61
90
  the sub-toolkits but leave zero CLIs on PATH.
62
91
 
63
- Do not remove the `bin` field or the `bin/` shims — they are the mechanism that
64
- puts `sdtk-spec`, `sdtk-code`, `sdtk-ops`, `sdtk-design`, and `sdtk-wiki` on PATH
65
- after a global install.
92
+ The umbrella also ships its own `sdtk` orchestrator bin (`bin/sdtk.js` + `src/`),
93
+ which delegates `sdtk init` to each sub-toolkit's published bin via subprocess
94
+ (`require.resolve` of the kit's `package.json` `bin` map, spawned with the current
95
+ Node). It adds no new dependency and re-implements no init logic.
96
+
97
+ Do not remove the `bin` field, the `bin/` shims, or `bin/sdtk.js` + `src/` — they
98
+ are the mechanism that puts `sdtk`, `sdtk-spec`, `sdtk-code`, `sdtk-ops`,
99
+ `sdtk-design`, and `sdtk-wiki` on PATH after a global install.
66
100
 
67
101
  ## Version pinning model
68
102
 
@@ -72,15 +106,15 @@ after a global install.
72
106
  - **Major version bumps** in any sub-toolkit require a coordinated `sdtk-kit` major-bump and re-publish.
73
107
  - If you need exact version control per toolkit, use standalone packages instead.
74
108
 
75
- Current pinned versions (as of sdtk-kit v1.0.0):
109
+ Current dependency ranges (as of sdtk-kit v1.3.0):
76
110
 
77
111
  | Package | Version |
78
112
  |------------------|---------|
79
113
  | sdtk-spec-kit | ^0.4.7 |
80
- | sdtk-code-kit | ^0.1.3 |
114
+ | sdtk-code-kit | ^0.3.0 |
81
115
  | sdtk-ops-kit | ^0.2.4 |
82
116
  | sdtk-design-kit | ^0.3.0 |
83
- | sdtk-wiki-kit | ^0.1.4 |
117
+ | sdtk-wiki-kit | ^0.2.0 |
84
118
 
85
119
  ## Updating
86
120
 
@@ -114,6 +148,27 @@ npm config get prefix
114
148
 
115
149
  Ensure `<prefix>/bin` is in `PATH`.
116
150
 
151
+ **Only three CLIs are visible after installing sdtk-kit**
152
+
153
+ If `sdtk-spec`, `sdtk-code`, and `sdtk-ops` are visible but `sdtk-design` or `sdtk-wiki` are missing, the usual cause is a stale global install, an old npm prefix on `PATH`, or a shell command cache rather than the current `sdtk-kit` package.
154
+
155
+ Reset and verify from a fresh shell:
156
+
157
+ ```bash
158
+ npm uninstall -g sdtk-kit sdtk-spec-kit sdtk-code-kit sdtk-ops-kit sdtk-design-kit sdtk-wiki-kit
159
+ npm cache verify
160
+ npm install -g sdtk-kit@latest
161
+ hash -r
162
+ command -v sdtk-spec sdtk-code sdtk-ops sdtk-design sdtk-wiki
163
+ sdtk-spec --version
164
+ sdtk-code --version
165
+ sdtk-ops --version
166
+ sdtk-design --version
167
+ sdtk-wiki --version
168
+ ```
169
+
170
+ On Windows PowerShell, close and reopen the terminal after reinstalling, then use `where.exe sdtk-spec`, `where.exe sdtk-design`, and the same `--version` commands.
171
+
117
172
  **Windows PATH issues**
118
173
 
119
174
  On Windows, npm global binaries land in `%APPDATA%\npm`. Make sure this is in your `PATH`. Run `npm config get prefix` to confirm the location.
package/bin/sdtk.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // `sdtk` — unified entry point for the SDTK suite (BK-268).
5
+ //
6
+ // sdtk init --runtime <claude|codex> one-command setup for all five toolkits
7
+ // sdtk --help top-level help
8
+ // sdtk --version sdtk-kit version + resolved per-kit versions
9
+ // sdtk no args → help
10
+ //
11
+ // `init` delegates to each toolkit's own shipped init (see src/lib/unified-init.js).
12
+
13
+ const SUB_KITS = [
14
+ "sdtk-spec-kit",
15
+ "sdtk-ops-kit",
16
+ "sdtk-code-kit",
17
+ "sdtk-design-kit",
18
+ "sdtk-wiki-kit",
19
+ ];
20
+
21
+ function helpText() {
22
+ return [
23
+ "sdtk — unified SDTK suite CLI",
24
+ "",
25
+ "Usage:",
26
+ " sdtk init --runtime <claude|codex> [options] Initialise all five toolkits",
27
+ " sdtk --help Show this help",
28
+ " sdtk --version Show versions",
29
+ "",
30
+ "init options:",
31
+ " --runtime <claude|codex> Required. Runtime to install for spec/ops/code.",
32
+ " --runtime-scope <scope> Optional. Forwarded to the runtime toolkits.",
33
+ " --project-path <path> Optional. Target project directory.",
34
+ " --force Re-initialise existing assets.",
35
+ " --skip-runtime-assets Skip runtime asset install (runtime toolkits).",
36
+ " --keep-going Continue past a failing toolkit (default: fail-fast).",
37
+ " --verbose Verbose per-toolkit output.",
38
+ "",
39
+ "Runs: sdtk-spec → sdtk-ops → sdtk-code (with --runtime) → sdtk-design → sdtk-wiki",
40
+ "(sdtk-design and sdtk-wiki run their own non-runtime init).",
41
+ "",
42
+ "Standalone per-toolkit CLIs remain available: sdtk-spec, sdtk-ops, sdtk-code,",
43
+ "sdtk-design, sdtk-wiki.",
44
+ "",
45
+ "Docs: https://sdtk.dev",
46
+ ].join("\n");
47
+ }
48
+
49
+ function versionText() {
50
+ // eslint-disable-next-line global-require
51
+ const self = require("../package.json");
52
+ const lines = [`sdtk-kit ${self.version}`];
53
+ for (const kit of SUB_KITS) {
54
+ let version = "not resolved";
55
+ try {
56
+ // eslint-disable-next-line global-require
57
+ version = require(`${kit}/package.json`).version;
58
+ } catch (err) {
59
+ version = "not resolved";
60
+ }
61
+ lines.push(` ${kit.padEnd(15)} ${version}`);
62
+ }
63
+ return lines.join("\n");
64
+ }
65
+
66
+ function main(argv) {
67
+ const command = argv[0];
68
+
69
+ if (!command || command === "--help" || command === "-h" || command === "help") {
70
+ console.log(helpText());
71
+ return 0;
72
+ }
73
+
74
+ if (command === "--version" || command === "-v") {
75
+ console.log(versionText());
76
+ return 0;
77
+ }
78
+
79
+ if (command === "init") {
80
+ // eslint-disable-next-line global-require
81
+ const { cmdInit } = require("../src/commands/init");
82
+ return cmdInit(argv.slice(1));
83
+ }
84
+
85
+ console.error(`sdtk: unknown command '${command}'.`);
86
+ console.error("Run `sdtk --help` for usage.");
87
+ return 2;
88
+ }
89
+
90
+ process.exitCode = main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,60 +1,65 @@
1
- {
2
- "name": "sdtk-kit",
3
- "version": "1.1.0",
4
- "description": "Install all five SDTK toolkits in one command. Meta-package for sdtk-spec-kit, sdtk-code-kit, sdtk-ops-kit, sdtk-design-kit, and sdtk-wiki-kit.",
5
- "type": "commonjs",
6
- "bin": {
7
- "sdtk-spec": "bin/sdtk-spec.js",
8
- "sdtk-code": "bin/sdtk-code.js",
9
- "sdtk-ops": "bin/sdtk-ops.js",
10
- "sdtk-design": "bin/sdtk-design.js",
11
- "sdtk-wiki": "bin/sdtk-wiki.js"
12
- },
13
- "files": [
14
- "bin/",
15
- "scripts/",
16
- "README.md",
17
- "LICENSE"
18
- ],
19
- "scripts": {
20
- "postinstall": "node scripts/postinstall.js",
21
- "pack:smoke": "npm pack --dry-run"
22
- },
23
- "dependencies": {
24
- "sdtk-spec-kit": "^0.4.7",
25
- "sdtk-code-kit": "^0.2.0",
26
- "sdtk-ops-kit": "^0.2.4",
27
- "sdtk-design-kit": "^0.3.0",
28
- "sdtk-wiki-kit": "^0.2.0"
29
- },
30
- "engines": {
31
- "node": ">=18.13.0"
32
- },
33
- "keywords": [
34
- "sdtk",
35
- "sdtk-kit",
36
- "sdtk-spec",
37
- "sdtk-code",
38
- "sdtk-ops",
39
- "sdtk-design",
40
- "sdtk-wiki",
41
- "cli",
42
- "toolkit",
43
- "ai-workflow",
44
- "solo-founder",
45
- "mvp"
46
- ],
47
- "license": "MIT",
48
- "repository": {
49
- "type": "git",
50
- "url": "git+https://github.com/codexsdtk/sdtk-toolkit.git",
51
- "directory": "products/sdtk-kit/distribution/sdtk-kit"
52
- },
53
- "homepage": "https://sdtk.dev",
54
- "bugs": {
55
- "url": "https://github.com/codexsdtk/sdtk-toolkit/issues"
56
- },
57
- "publishConfig": {
58
- "access": "public"
59
- }
60
- }
1
+ {
2
+ "name": "sdtk-kit",
3
+ "version": "1.3.0",
4
+ "description": "Install all five SDTK toolkits in one command. Meta-package for sdtk-spec-kit, sdtk-code-kit, sdtk-ops-kit, sdtk-design-kit, and sdtk-wiki-kit.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "sdtk": "bin/sdtk.js",
8
+ "sdtk-spec": "bin/sdtk-spec.js",
9
+ "sdtk-code": "bin/sdtk-code.js",
10
+ "sdtk-ops": "bin/sdtk-ops.js",
11
+ "sdtk-design": "bin/sdtk-design.js",
12
+ "sdtk-wiki": "bin/sdtk-wiki.js"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "src/",
17
+ "scripts/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "postinstall": "node scripts/postinstall.js",
23
+ "test": "node scripts/unified-init.test.js",
24
+ "install:smoke:local": "node scripts/install-smoke.js --mode local",
25
+ "install:smoke:published": "node scripts/install-smoke.js --mode published --package sdtk-kit@latest",
26
+ "pack:smoke": "npm pack --dry-run"
27
+ },
28
+ "dependencies": {
29
+ "sdtk-spec-kit": "^0.4.7",
30
+ "sdtk-code-kit": "^0.3.0",
31
+ "sdtk-ops-kit": "^0.2.4",
32
+ "sdtk-design-kit": "^0.3.0",
33
+ "sdtk-wiki-kit": "^0.2.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.13.0"
37
+ },
38
+ "keywords": [
39
+ "sdtk",
40
+ "sdtk-kit",
41
+ "sdtk-spec",
42
+ "sdtk-code",
43
+ "sdtk-ops",
44
+ "sdtk-design",
45
+ "sdtk-wiki",
46
+ "cli",
47
+ "toolkit",
48
+ "ai-workflow",
49
+ "solo-founder",
50
+ "mvp"
51
+ ],
52
+ "license": "MIT",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/codexsdtk/sdtk-toolkit.git",
56
+ "directory": "products/sdtk-kit/distribution/sdtk-kit"
57
+ },
58
+ "homepage": "https://sdtk.dev",
59
+ "bugs": {
60
+ "url": "https://github.com/codexsdtk/sdtk-toolkit/issues"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ }
65
+ }
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+
9
+ const PACKAGE_ROOT = path.resolve(__dirname, "..");
10
+ const PACKAGE_JSON = path.join(PACKAGE_ROOT, "package.json");
11
+ const TOOLKITS = Object.freeze([
12
+ { cli: "sdtk-spec", packageName: "sdtk-spec-kit" },
13
+ { cli: "sdtk-code", packageName: "sdtk-code-kit" },
14
+ { cli: "sdtk-ops", packageName: "sdtk-ops-kit" },
15
+ { cli: "sdtk-design", packageName: "sdtk-design-kit" },
16
+ { cli: "sdtk-wiki", packageName: "sdtk-wiki-kit" },
17
+ ]);
18
+
19
+ function usage() {
20
+ return [
21
+ "Usage:",
22
+ " node scripts/install-smoke.js --mode local [--package ./sdtk-kit-1.2.0.tgz] [--keep-prefix]",
23
+ " node scripts/install-smoke.js --mode published [--package sdtk-kit@latest] [--keep-prefix]",
24
+ "",
25
+ "Modes:",
26
+ " local Packs the local sdtk-kit package when --package is omitted, then installs it into a clean prefix.",
27
+ " published Installs a published package spec, defaulting to sdtk-kit@latest, into a clean prefix.",
28
+ ].join("\n");
29
+ }
30
+
31
+ function parseArgs(argv) {
32
+ const args = { mode: "local", packageSpec: null, keepPrefix: false };
33
+ for (let i = 0; i < argv.length; i += 1) {
34
+ const arg = argv[i];
35
+ if (arg === "--mode") {
36
+ args.mode = argv[++i];
37
+ } else if (arg === "--package") {
38
+ args.packageSpec = argv[++i];
39
+ } else if (arg === "--keep-prefix") {
40
+ args.keepPrefix = true;
41
+ } else if (arg === "--help" || arg === "-h") {
42
+ console.log(usage());
43
+ process.exit(0);
44
+ } else {
45
+ throw new Error(`Unknown argument: ${arg}\n${usage()}`);
46
+ }
47
+ }
48
+ if (!new Set(["local", "published"]).has(args.mode)) {
49
+ throw new Error(`Invalid --mode: ${args.mode}\n${usage()}`);
50
+ }
51
+ return args;
52
+ }
53
+
54
+ function run(command, args, options = {}) {
55
+ const result = spawnSync(command, args, {
56
+ cwd: options.cwd || PACKAGE_ROOT,
57
+ env: options.env || process.env,
58
+ encoding: "utf8",
59
+ shell: options.shell || false,
60
+ });
61
+ if (result.status !== 0) {
62
+ throw new Error([
63
+ `Command failed: ${command} ${args.join(" ")}`,
64
+ `exit=${result.status}`,
65
+ result.stdout ? `stdout:\n${result.stdout}` : "stdout: <empty>",
66
+ result.stderr ? `stderr:\n${result.stderr}` : "stderr: <empty>",
67
+ ].join("\n"));
68
+ }
69
+ return result;
70
+ }
71
+
72
+ function readJson(file) {
73
+ return JSON.parse(fs.readFileSync(file, "utf8"));
74
+ }
75
+
76
+ function assertPackageMetadata(packageJson) {
77
+ const missingBins = [];
78
+ const missingDeps = [];
79
+ for (const toolkit of TOOLKITS) {
80
+ if (!packageJson.bin || packageJson.bin[toolkit.cli] !== `bin/${toolkit.cli}.js`) {
81
+ missingBins.push(toolkit.cli);
82
+ }
83
+ if (!packageJson.dependencies || !packageJson.dependencies[toolkit.packageName]) {
84
+ missingDeps.push(toolkit.packageName);
85
+ }
86
+ }
87
+ if (missingBins.length || missingDeps.length) {
88
+ throw new Error(`sdtk-kit metadata is incomplete: missingBins=${missingBins.join(",") || "none"}; missingDeps=${missingDeps.join(",") || "none"}`);
89
+ }
90
+ }
91
+
92
+ function packLocalPackage() {
93
+ const result = run("npm", ["pack", "--json"], { cwd: PACKAGE_ROOT });
94
+ const entries = JSON.parse(result.stdout);
95
+ if (!Array.isArray(entries) || entries.length !== 1 || !entries[0].filename) {
96
+ throw new Error(`Unexpected npm pack --json output: ${result.stdout}`);
97
+ }
98
+ return path.join(PACKAGE_ROOT, entries[0].filename);
99
+ }
100
+
101
+ function npmBinPath(prefix, cli) {
102
+ const binDir = path.join(prefix, "node_modules", ".bin");
103
+ const candidates = process.platform === "win32"
104
+ ? [path.join(binDir, `${cli}.cmd`), path.join(binDir, `${cli}.ps1`), path.join(binDir, cli)]
105
+ : [path.join(binDir, cli)];
106
+ return candidates.find((candidate) => fs.existsSync(candidate));
107
+ }
108
+
109
+ function installedPackageJson(prefix, packageName) {
110
+ const candidates = [
111
+ path.join(prefix, "node_modules", packageName, "package.json"),
112
+ path.join(prefix, "node_modules", "sdtk-kit", "node_modules", packageName, "package.json"),
113
+ ];
114
+ const found = candidates.find((candidate) => fs.existsSync(candidate));
115
+ if (!found) {
116
+ throw new Error(`Cannot find installed package.json for ${packageName} under ${prefix}`);
117
+ }
118
+ return readJson(found);
119
+ }
120
+
121
+ function runCliVersion(binPath) {
122
+ return run(binPath, ["--version"], { shell: process.platform === "win32" });
123
+ }
124
+
125
+ function smokeInstall(packageSpec, mode, keepPrefix) {
126
+ const prefix = fs.mkdtempSync(path.join(os.tmpdir(), "sdtk-kit-install-smoke-"));
127
+ let packedTarball = null;
128
+ try {
129
+ const localPackageJson = readJson(PACKAGE_JSON);
130
+ assertPackageMetadata(localPackageJson);
131
+
132
+ let installSpec = packageSpec;
133
+ if (mode === "local" && !installSpec) {
134
+ packedTarball = packLocalPackage();
135
+ installSpec = packedTarball;
136
+ }
137
+ if (mode === "published" && !installSpec) {
138
+ installSpec = "sdtk-kit@latest";
139
+ }
140
+ if (!installSpec) {
141
+ throw new Error("No package spec resolved for install smoke.");
142
+ }
143
+
144
+ console.log(`[sdtk-kit smoke] mode=${mode}`);
145
+ console.log(`[sdtk-kit smoke] prefix=${prefix}`);
146
+ console.log(`[sdtk-kit smoke] install=${installSpec}`);
147
+ run("npm", ["install", "--prefix", prefix, installSpec]);
148
+
149
+ const installedMeta = installedPackageJson(prefix, "sdtk-kit");
150
+ assertPackageMetadata(installedMeta);
151
+ console.log(`[sdtk-kit smoke] installed sdtk-kit ${installedMeta.version}`);
152
+
153
+ for (const toolkit of TOOLKITS) {
154
+ const binPath = npmBinPath(prefix, toolkit.cli);
155
+ if (!binPath) {
156
+ throw new Error(`Missing CLI shim for ${toolkit.cli} in ${path.join(prefix, "node_modules", ".bin")}`);
157
+ }
158
+ const expected = installedPackageJson(prefix, toolkit.packageName).version;
159
+ const result = runCliVersion(binPath);
160
+ const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
161
+ if (!output.includes(expected)) {
162
+ throw new Error(`${toolkit.cli} --version did not include installed ${toolkit.packageName} version ${expected}. Output: ${output}`);
163
+ }
164
+ console.log(`[sdtk-kit smoke] ${toolkit.cli} -> ${output}`);
165
+ }
166
+
167
+ console.log("[sdtk-kit smoke] PASS all five CLI shims exist and --version checks passed.");
168
+ } finally {
169
+ if (packedTarball && fs.existsSync(packedTarball)) {
170
+ fs.rmSync(packedTarball, { force: true });
171
+ }
172
+ if (keepPrefix) {
173
+ console.log(`[sdtk-kit smoke] kept prefix: ${prefix}`);
174
+ } else {
175
+ fs.rmSync(prefix, { recursive: true, force: true });
176
+ }
177
+ }
178
+ }
179
+
180
+ function main() {
181
+ const args = parseArgs(process.argv.slice(2));
182
+ smokeInstall(args.packageSpec, args.mode, args.keepPrefix);
183
+ }
184
+
185
+ try {
186
+ main();
187
+ } catch (error) {
188
+ console.error(`[sdtk-kit smoke] FAIL ${error.message}`);
189
+ process.exit(1);
190
+ }
@@ -28,9 +28,12 @@ const lines = [
28
28
  " sdtk-ops — Operations: deploy → smoke → sign-off",
29
29
  " sdtk-wiki — Local second brain: your project memory",
30
30
  "",
31
- " Start with the SPEC toolkit:",
31
+ " Set up the whole suite in one command:",
32
+ "",
33
+ " sdtk init --runtime claude (or --runtime codex)",
34
+ "",
35
+ " Then generate your first feature spec:",
32
36
  "",
33
- " sdtk-spec init --runtime claude (or --runtime codex)",
34
37
  " sdtk-spec generate --feature-key <YOUR_FEATURE>",
35
38
  "",
36
39
  " Docs: https://sdtk.dev",
@@ -0,0 +1,242 @@
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
+
16
+ const RUNTIME_KITS = ["sdtk-spec", "sdtk-ops", "sdtk-code"];
17
+ const NON_RUNTIME_KITS = ["sdtk-design", "sdtk-wiki"];
18
+
19
+ // Build an injected deps object with a recording spawn spy.
20
+ // `failures` maps toolkit name → exit code to return (default 0).
21
+ // `unresolvable` is a Set of kit package names that resolveBin should reject.
22
+ function makeDeps({ failures = {}, unresolvable = new Set(), psOk = true } = {}) {
23
+ const spawnCalls = [];
24
+ const resolveCalls = [];
25
+ const logs = [];
26
+ return {
27
+ spawnCalls,
28
+ resolveCalls,
29
+ logs,
30
+ deps: {
31
+ spawn(binPath, argv, toolkit) {
32
+ spawnCalls.push({ binPath, argv, toolkit: toolkit.name });
33
+ const code = failures[toolkit.name] || 0;
34
+ return { status: code, stderr: code ? `stub stderr for ${toolkit.name}` : "" };
35
+ },
36
+ resolveBin(kitPkg, binName) {
37
+ resolveCalls.push(kitPkg);
38
+ if (unresolvable.has(kitPkg)) {
39
+ throw new ToolkitResolveError(kitPkg);
40
+ }
41
+ return `/stub/node_modules/${kitPkg}/bin/${binName}.js`;
42
+ },
43
+ powershellCheck() {
44
+ return { ok: psOk, exe: "pwsh" };
45
+ },
46
+ log(line) {
47
+ logs.push(line);
48
+ },
49
+ },
50
+ };
51
+ }
52
+
53
+ // Pull the argv recorded for one toolkit's spawn.
54
+ function argvFor(spawnCalls, name) {
55
+ const call = spawnCalls.find((c) => c.toolkit === name);
56
+ return call ? call.argv : null;
57
+ }
58
+
59
+ const tests = [];
60
+ function test(name, fn) {
61
+ tests.push({ name, fn });
62
+ }
63
+
64
+ // ── T1 — runtime claude forwards correctly to runtime kits ──────────────────
65
+ test("T1 runtime=claude spawns spec/ops/code with init --runtime claude", () => {
66
+ const h = makeDeps();
67
+ const { exitCode } = runUnifiedInit({ runtime: "claude" }, h.deps);
68
+ assert.strictEqual(exitCode, 0);
69
+ for (const kit of RUNTIME_KITS) {
70
+ const argv = argvFor(h.spawnCalls, kit);
71
+ assert.deepStrictEqual(argv, ["init", "--runtime", "claude"], `${kit} argv`);
72
+ }
73
+ });
74
+
75
+ // ── T2 — runtime codex ──────────────────────────────────────────────────────
76
+ test("T2 runtime=codex forwards --runtime codex", () => {
77
+ const h = makeDeps();
78
+ const { exitCode } = runUnifiedInit({ runtime: "codex" }, h.deps);
79
+ assert.strictEqual(exitCode, 0);
80
+ for (const kit of RUNTIME_KITS) {
81
+ assert.deepStrictEqual(argvFor(h.spawnCalls, kit), ["init", "--runtime", "codex"]);
82
+ }
83
+ });
84
+
85
+ // ── T3 — flag forwarding (runtime subset vs full) ───────────────────────────
86
+ test("T3 forwards shared flags to runtime kits; only subset to design/wiki", () => {
87
+ const h = makeDeps();
88
+ const opts = {
89
+ runtime: "claude",
90
+ runtimeScope: "user",
91
+ projectPath: "/tmp/proj",
92
+ force: true,
93
+ skipRuntimeAssets: true,
94
+ verbose: true,
95
+ };
96
+ runUnifiedInit(opts, h.deps);
97
+
98
+ for (const kit of RUNTIME_KITS) {
99
+ const argv = argvFor(h.spawnCalls, kit);
100
+ assert.deepStrictEqual(
101
+ argv,
102
+ [
103
+ "init",
104
+ "--runtime",
105
+ "claude",
106
+ "--runtime-scope",
107
+ "user",
108
+ "--project-path",
109
+ "/tmp/proj",
110
+ "--force",
111
+ "--skip-runtime-assets",
112
+ "--verbose",
113
+ ],
114
+ `${kit} full forward`
115
+ );
116
+ }
117
+ for (const kit of NON_RUNTIME_KITS) {
118
+ const argv = argvFor(h.spawnCalls, kit);
119
+ assert.deepStrictEqual(
120
+ argv,
121
+ ["init", "--project-path", "/tmp/proj", "--force", "--verbose"],
122
+ `${kit} subset`
123
+ );
124
+ assert.ok(!argv.includes("--runtime"), `${kit} must not get --runtime`);
125
+ assert.ok(!argv.includes("--runtime-scope"), `${kit} must not get --runtime-scope`);
126
+ assert.ok(!argv.includes("--skip-runtime-assets"), `${kit} must not get --skip-runtime-assets`);
127
+ }
128
+ });
129
+
130
+ // ── T4 — missing/invalid runtime → exit 2, zero spawns ──────────────────────
131
+ test("T4 missing runtime → exit 2, no spawns", () => {
132
+ const h = makeDeps();
133
+ const { exitCode } = runUnifiedInit({}, h.deps);
134
+ assert.strictEqual(exitCode, 2);
135
+ assert.strictEqual(h.spawnCalls.length, 0);
136
+ });
137
+ test("T4b invalid runtime → exit 2, no spawns", () => {
138
+ const h = makeDeps();
139
+ const { exitCode } = runUnifiedInit({ runtime: "bogus" }, h.deps);
140
+ assert.strictEqual(exitCode, 2);
141
+ assert.strictEqual(h.spawnCalls.length, 0);
142
+ });
143
+
144
+ // ── T5 — PowerShell missing → exit 3, zero spawns ───────────────────────────
145
+ test("T5 powershell missing → exit 3, no spawns", () => {
146
+ const h = makeDeps({ psOk: false });
147
+ const { exitCode } = runUnifiedInit({ runtime: "claude" }, h.deps);
148
+ assert.strictEqual(exitCode, 3);
149
+ assert.strictEqual(h.spawnCalls.length, 0);
150
+ });
151
+
152
+ // ── T6 — order spec → ops → code → design → wiki ────────────────────────────
153
+ test("T6 runs all five in registry order", () => {
154
+ const h = makeDeps();
155
+ runUnifiedInit({ runtime: "claude" }, h.deps);
156
+ const order = h.spawnCalls.map((c) => c.toolkit);
157
+ assert.deepStrictEqual(order, [
158
+ "sdtk-spec",
159
+ "sdtk-ops",
160
+ "sdtk-code",
161
+ "sdtk-design",
162
+ "sdtk-wiki",
163
+ ]);
164
+ });
165
+
166
+ // ── T7 — fail-fast default ──────────────────────────────────────────────────
167
+ test("T7 fail-fast: middle failure stops subsequent toolkits, exit = child code", () => {
168
+ const h = makeDeps({ failures: { "sdtk-code": 7 } });
169
+ const { exitCode } = runUnifiedInit({ runtime: "claude" }, h.deps);
170
+ assert.strictEqual(exitCode, 7);
171
+ const order = h.spawnCalls.map((c) => c.toolkit);
172
+ assert.deepStrictEqual(order, ["sdtk-spec", "sdtk-ops", "sdtk-code"]);
173
+ assert.ok(!order.includes("sdtk-design"), "design must not be spawned after fail");
174
+ assert.ok(!order.includes("sdtk-wiki"), "wiki must not be spawned after fail");
175
+ });
176
+
177
+ // ── T8 — --keep-going ───────────────────────────────────────────────────────
178
+ test("T8 keep-going: failure does not stop the rest; aggregate exit non-zero", () => {
179
+ const h = makeDeps({ failures: { "sdtk-code": 5 } });
180
+ const { exitCode, results } = runUnifiedInit({ runtime: "claude", keepGoing: true }, h.deps);
181
+ assert.strictEqual(exitCode, 5);
182
+ assert.strictEqual(h.spawnCalls.length, 5);
183
+ const failed = results.find((r) => r.name === "sdtk-code");
184
+ assert.strictEqual(failed.status, "FAILED");
185
+ const wiki = results.find((r) => r.name === "sdtk-wiki");
186
+ assert.strictEqual(wiki.status, "OK");
187
+ });
188
+
189
+ // ── T9 — unresolvable kit → exit 4 naming the kit ───────────────────────────
190
+ test("T9 unresolvable kit → exit 4, names the kit, fail-fast no spawn", () => {
191
+ const h = makeDeps({ unresolvable: new Set(["sdtk-spec-kit"]) });
192
+ const { exitCode, results } = runUnifiedInit({ runtime: "claude" }, h.deps);
193
+ assert.strictEqual(exitCode, 4);
194
+ assert.strictEqual(h.spawnCalls.length, 0, "no spawns once first kit unresolvable (fail-fast)");
195
+ assert.ok(results[0].statusLabel.includes("sdtk-spec-kit"), "kit named in status");
196
+ });
197
+ test("T9b unresolvable kit with --keep-going continues, exit 4", () => {
198
+ const h = makeDeps({ unresolvable: new Set(["sdtk-ops-kit"]) });
199
+ const { exitCode } = runUnifiedInit({ runtime: "claude", keepGoing: true }, h.deps);
200
+ assert.strictEqual(exitCode, 4);
201
+ // spec + code + design + wiki spawn; ops is skipped (unresolvable)
202
+ const order = h.spawnCalls.map((c) => c.toolkit);
203
+ assert.deepStrictEqual(order, ["sdtk-spec", "sdtk-code", "sdtk-design", "sdtk-wiki"]);
204
+ });
205
+
206
+ // ── T10 — orchestrator only touches injected deps (no real fs/network/spawn) ─
207
+ test("T10 orchestrator uses only injected deps (no real host access)", () => {
208
+ const h = makeDeps();
209
+ // Spy spawn/resolveBin do no real I/O. A successful run that records exactly
210
+ // the expected spy invocations proves the orchestrator never bypassed the seam.
211
+ runUnifiedInit({ runtime: "claude" }, h.deps);
212
+ assert.strictEqual(h.spawnCalls.length, 5, "all process work went through injected spawn");
213
+ assert.deepStrictEqual(h.resolveCalls, [
214
+ "sdtk-spec-kit",
215
+ "sdtk-ops-kit",
216
+ "sdtk-code-kit",
217
+ "sdtk-design-kit",
218
+ "sdtk-wiki-kit",
219
+ ]);
220
+ // buildInitArgs is pure: same input → same output, no side effects.
221
+ const a = buildInitArgs(TOOLKITS[0], { runtime: "claude" });
222
+ const b = buildInitArgs(TOOLKITS[0], { runtime: "claude" });
223
+ assert.deepStrictEqual(a, b);
224
+ });
225
+
226
+ // ── runner ──────────────────────────────────────────────────────────────────
227
+ let passed = 0;
228
+ let failed = 0;
229
+ for (const t of tests) {
230
+ try {
231
+ t.fn();
232
+ passed += 1;
233
+ console.log(` ok ${t.name}`);
234
+ } catch (err) {
235
+ failed += 1;
236
+ console.error(`FAIL ${t.name}`);
237
+ console.error(` ${err.message}`);
238
+ }
239
+ }
240
+ console.log("");
241
+ console.log(`unified-init tests: ${passed} passed, ${failed} failed`);
242
+ process.exit(failed === 0 ? 0 : 1);
@@ -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
+ };