@svenroth-ai/shipwright 0.24.2-next.0 → 0.24.3-next.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/bin/shipwright.mjs +48 -4
- package/lib/server.mjs +49 -3
- package/package.json +2 -1
package/bin/shipwright.mjs
CHANGED
|
@@ -142,7 +142,32 @@ export function printSummary(log, { plugin, server, port }) {
|
|
|
142
142
|
log(`========================================`);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Decide whether to boot the Command Center after the plugin phase.
|
|
147
|
+
*
|
|
148
|
+
* The cold-start failure this closes: when a hard prerequisite (Claude / uv /
|
|
149
|
+
* Python) is missing, the plugin phase is correctly SKIPPED — but the bin then
|
|
150
|
+
* booted the Command Center anyway, i.e. a WebUI that launches a Claude with NO
|
|
151
|
+
* `/shipwright-*` commands. That is useless, so the default flow now STOPS.
|
|
152
|
+
* `--webui-only` is the explicit opt-out (it never runs the plugin phase, so
|
|
153
|
+
* `pluginSkipped` is false there and it still boots).
|
|
154
|
+
*
|
|
155
|
+
* @param {{ pluginsOnly: boolean, webuiOnly: boolean, pluginSkipped: boolean }} s
|
|
156
|
+
* @returns {{ boot: boolean, reason: "plugins-only" | "prereqs-missing" | "ok" }}
|
|
157
|
+
*/
|
|
158
|
+
export function serverBootDecision({ pluginsOnly, webuiOnly, pluginSkipped }) {
|
|
159
|
+
if (pluginsOnly) return { boot: false, reason: "plugins-only" };
|
|
160
|
+
if (pluginSkipped && !webuiOnly) return { boot: false, reason: "prereqs-missing" };
|
|
161
|
+
return { boot: true, reason: "ok" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function main(argv = process.argv.slice(2), log = (m) => console.log(m), deps = {}) {
|
|
165
|
+
// Seams (default to the real implementations) so main's control flow — the
|
|
166
|
+
// prereqs-missing hard-stop especially — is unit-testable without a real
|
|
167
|
+
// toolchain or a live :3847.
|
|
168
|
+
const runPluginPhase = deps.pluginPhase ?? pluginPhase;
|
|
169
|
+
const runEnsureServer = deps.ensureServer ?? ensureServer;
|
|
170
|
+
|
|
146
171
|
const args = parseArgs(argv);
|
|
147
172
|
if (args.version) { log(SELF_VERSION); return 0; }
|
|
148
173
|
if (args.help) { log(HELP); return 0; }
|
|
@@ -161,13 +186,32 @@ export async function main(argv = process.argv.slice(2), log = (m) => console.lo
|
|
|
161
186
|
let server = null;
|
|
162
187
|
|
|
163
188
|
if (!args.webuiOnly) {
|
|
164
|
-
plugin = await
|
|
189
|
+
plugin = await runPluginPhase(log);
|
|
165
190
|
exitCode = Math.max(exitCode, plugin.exitCode ?? 0);
|
|
166
191
|
}
|
|
167
192
|
|
|
168
|
-
|
|
193
|
+
const bootDecision = serverBootDecision({
|
|
194
|
+
pluginsOnly: args.pluginsOnly,
|
|
195
|
+
webuiOnly: args.webuiOnly,
|
|
196
|
+
pluginSkipped: plugin?.skipped === true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (bootDecision.reason === "prereqs-missing") {
|
|
200
|
+
// Hard-stop: a Command Center with no plugins would launch a Claude with no
|
|
201
|
+
// /shipwright-* commands. The per-tool install hints already printed above
|
|
202
|
+
// (renderVerdict); here we say why we stopped and how to override.
|
|
203
|
+
log(`\n${MARK.fail} Not starting the Command Center — required tools are missing.`);
|
|
204
|
+
log(` Shipwright needs Claude Code, Python 3.11+ with uv, and git installed first;`);
|
|
205
|
+
log(` without them the plugins cannot install and the Command Center would be empty.`);
|
|
206
|
+
log(` Install whichever tools are marked missing above, then re-run npx @svenroth-ai/shipwright@latest`);
|
|
207
|
+
log(` (To start the Command Center anyway, without plugins: add --webui-only.)`);
|
|
208
|
+
printSummary(log, { plugin, server: null, port: null });
|
|
209
|
+
return Math.max(exitCode, 1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (bootDecision.boot) {
|
|
169
213
|
try {
|
|
170
|
-
server = await
|
|
214
|
+
server = await runEnsureServer({ port, pkgRoot: PKG_ROOT, packageVersion: SELF_VERSION, noOpen: args.noOpen, log });
|
|
171
215
|
} catch (e) {
|
|
172
216
|
log(`${MARK.fail} ${String(e?.message ?? e)}`);
|
|
173
217
|
exitCode = Math.max(exitCode, 1);
|
package/lib/server.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { spawn } from "node:child_process";
|
|
19
|
-
import { readFileSync } from "node:fs";
|
|
19
|
+
import { readFileSync, openSync, closeSync, mkdirSync } from "node:fs";
|
|
20
20
|
import os from "node:os";
|
|
21
21
|
|
|
22
22
|
import { compareSemver } from "./util.mjs";
|
|
@@ -149,7 +149,52 @@ function runPlan(plan) {
|
|
|
149
149
|
return child.pid ?? null;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
/**
|
|
153
|
+
* The file the DETACHED packaged server's stdout+stderr is redirected to — and
|
|
154
|
+
* the SAME path the boot readiness-timeout error tells the user to read. Kept as
|
|
155
|
+
* one exported function so the two can never drift apart again: the boot used to
|
|
156
|
+
* spawn with `stdio:"ignore"` (output discarded) while the error pointed the
|
|
157
|
+
* user at this log, which nothing wrote — an undiagnosable dead end.
|
|
158
|
+
*/
|
|
159
|
+
export function bootLogPath() {
|
|
160
|
+
return path.join(os.homedir(), ".shipwright-webui", "server-manual.log");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Open the boot log for append, creating its dir. Returns an fd, or null when
|
|
164
|
+
* it cannot be opened — logging is best-effort and must NEVER block the boot. */
|
|
165
|
+
function defaultOpenBootLog() {
|
|
166
|
+
try {
|
|
167
|
+
mkdirSync(path.dirname(bootLogPath()), { recursive: true });
|
|
168
|
+
return openSync(bootLogPath(), "a");
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve the `stdio` for the detached boot: route BOTH stdout and stderr at the
|
|
176
|
+
* log fd when it opens, else fall back to `"ignore"` (the pre-fix behaviour, now
|
|
177
|
+
* only the degraded path). Pure over its `open` seam so both branches are
|
|
178
|
+
* unit-testable without touching the filesystem.
|
|
179
|
+
* @param {() => number | null} [open]
|
|
180
|
+
*/
|
|
181
|
+
export function resolveBootStdio(open = defaultOpenBootLog) {
|
|
182
|
+
const fd = open();
|
|
183
|
+
return fd == null ? "ignore" : ["ignore", fd, fd];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const defaultBootServer = (port, pkgRoot) => {
|
|
187
|
+
const plan = bootSpawnPlan(port, pkgRoot);
|
|
188
|
+
const stdio = resolveBootStdio();
|
|
189
|
+
const child = spawn(plan.command, plan.args, { ...plan.options, stdio });
|
|
190
|
+
child.unref();
|
|
191
|
+
// The child dups the fd at spawn time; drop the parent's copy so the
|
|
192
|
+
// short-lived npx process leaves no handle open (both slots share one fd).
|
|
193
|
+
if (Array.isArray(stdio) && typeof stdio[1] === "number") {
|
|
194
|
+
try { closeSync(stdio[1]); } catch { /* already gone — nothing to do */ }
|
|
195
|
+
}
|
|
196
|
+
return child.pid ?? null;
|
|
197
|
+
};
|
|
153
198
|
const defaultSpawnSwapper = (port, pkgRoot) => runPlan(swapperSpawnPlan(port, pkgRoot));
|
|
154
199
|
|
|
155
200
|
/** Default: read the swapper's durable verdict (pid + ok) if present. */
|
|
@@ -279,7 +324,8 @@ export async function ensureServer(opts) {
|
|
|
279
324
|
if (!ready.ok) {
|
|
280
325
|
throw new Error(
|
|
281
326
|
`the packaged server did not become ready on ${url} within ${timeoutMs} ms — ` +
|
|
282
|
-
`check
|
|
327
|
+
`check ${bootLogPath()} (if that file is empty or absent, its directory was unwritable — ` +
|
|
328
|
+
`re-run from a terminal to see the server's output directly)`,
|
|
283
329
|
);
|
|
284
330
|
}
|
|
285
331
|
result = { ...result, pid, version: ready.probe.version };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svenroth-ai/shipwright",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.3-next.0",
|
|
4
4
|
"description": "One command to install AND update Shipwright — the /shipwright-* SDLC plugins and the Command Center. Run `npx @svenroth-ai/shipwright@latest`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@lydell/node-pty": "^1.1.0",
|
|
51
51
|
"@xterm/addon-serialize": "0.14.0",
|
|
52
52
|
"@xterm/headless": "6.0.0",
|
|
53
|
+
"cron-parser": "^5.5.0",
|
|
53
54
|
"hono": "^4.12.34",
|
|
54
55
|
"ignore": "^5.3.2",
|
|
55
56
|
"p-queue": "^9.2.0",
|