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.
package/package.json CHANGED
@@ -1,60 +1,65 @@
1
- {
2
- "name": "sdtk-kit",
3
- "version": "1.2.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.3.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
+ }
@@ -1,40 +1,43 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- // Skip banner in CI environments and headless installs.
5
- // This avoids noisy output in automated pipelines that install sdtk-kit
6
- // as a transitive dependency.
7
- const isCI =
8
- process.env.CI === "true" ||
9
- process.env.NODE_ENV === "test" ||
10
- process.env.npm_config_loglevel === "silent" ||
11
- process.env.npm_config_loglevel === "error";
12
-
13
- if (isCI) {
14
- process.exit(0);
15
- }
16
-
17
- const lines = [
18
- "",
19
- " ╔══════════════════════════════════════════════════════╗",
20
- " ║ SDTK — All Five Toolkits Installed ║",
21
- " ╚══════════════════════════════════════════════════════╝",
22
- "",
23
- " You now have all five SDTK CLI tools available:",
24
- "",
25
- " sdtk-spec — Spec-first SDLC: PM → BA → ARCH → DEV → QA",
26
- " sdtk-design — MVP design: idea → prototype → handoff",
27
- " sdtk-code — Governed coding: handoff → PR with review gates",
28
- " sdtk-ops — Operations: deploy → smoke → sign-off",
29
- " sdtk-wiki — Local second brain: your project memory",
30
- "",
31
- " Start with the SPEC toolkit:",
32
- "",
33
- " sdtk-spec init --runtime claude (or --runtime codex)",
34
- " sdtk-spec generate --feature-key <YOUR_FEATURE>",
35
- "",
36
- " Docs: https://sdtk.dev",
37
- "",
38
- ];
39
-
40
- process.stdout.write(lines.join("\n") + "\n");
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // Skip banner in CI environments and headless installs.
5
+ // This avoids noisy output in automated pipelines that install sdtk-kit
6
+ // as a transitive dependency.
7
+ const isCI =
8
+ process.env.CI === "true" ||
9
+ process.env.NODE_ENV === "test" ||
10
+ process.env.npm_config_loglevel === "silent" ||
11
+ process.env.npm_config_loglevel === "error";
12
+
13
+ if (isCI) {
14
+ process.exit(0);
15
+ }
16
+
17
+ const lines = [
18
+ "",
19
+ " ╔══════════════════════════════════════════════════════╗",
20
+ " ║ SDTK — All Five Toolkits Installed ║",
21
+ " ╚══════════════════════════════════════════════════════╝",
22
+ "",
23
+ " You now have all five SDTK CLI tools available:",
24
+ "",
25
+ " sdtk-spec — Spec-first SDLC: PM → BA → ARCH → DEV → QA",
26
+ " sdtk-design — MVP design: idea → prototype → handoff",
27
+ " sdtk-code — Governed coding: handoff → PR with review gates",
28
+ " sdtk-ops — Operations: deploy → smoke → sign-off",
29
+ " sdtk-wiki — Local second brain: your project memory",
30
+ "",
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:",
36
+ "",
37
+ " sdtk-spec generate --feature-key <YOUR_FEATURE>",
38
+ "",
39
+ " Docs: https://sdtk.dev",
40
+ "",
41
+ ];
42
+
43
+ process.stdout.write(lines.join("\n") + "\n");
@@ -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);