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 +64 -9
- package/bin/sdtk.js +90 -0
- package/package.json +65 -60
- package/scripts/install-smoke.js +190 -0
- package/scripts/postinstall.js +5 -2
- package/scripts/unified-init.test.js +242 -0
- package/src/commands/init.js +95 -0
- package/src/lib/unified-init.js +274 -0
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
|
-
#
|
|
23
|
-
sdtk
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
8
|
-
"sdtk-
|
|
9
|
-
"sdtk-
|
|
10
|
-
"sdtk-
|
|
11
|
-
"sdtk-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"sdtk
|
|
40
|
-
"sdtk-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
"url": "https://github.com/codexsdtk/sdtk-toolkit
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
-
"
|
|
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
|
+
};
|