@paleo/workspace 0.12.0 → 0.14.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 +12 -8
- package/dist/cli.d.ts +24 -12
- package/dist/cli.js +125 -60
- package/dist/dev-server.d.ts +1 -1
- package/dist/dev-server.js +173 -66
- package/dist/dev-servers-registry.d.ts +2 -0
- package/dist/dev-servers-registry.js +10 -0
- package/dist/server-descriptor.d.ts +1 -1
- package/dist/slots.js +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.js +17 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Run multiple local dev environments side by side, one per git worktree, with iso
|
|
|
5
5
|
Each project writes two custom scripts on top, using these entry points:
|
|
6
6
|
|
|
7
7
|
- `runWorkspace(config)` — worktree lifecycle (setup / remove / set-owner).
|
|
8
|
-
- `runDevServer(config)` —
|
|
8
|
+
- `runDevServer(config)` — dev-server start (foreground or background) / stop / list.
|
|
9
9
|
|
|
10
10
|
## Setup
|
|
11
11
|
|
|
@@ -27,11 +27,14 @@ The agent reads the skill, adapts the reference scripts to your stack, installs
|
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
29
|
npm run workspace -- setup feat/42 -c # new branch + worktree + isolated env
|
|
30
|
-
npm run dev
|
|
31
|
-
npm run dev
|
|
32
|
-
npm run dev
|
|
33
|
-
npm run dev
|
|
34
|
-
npm run dev
|
|
30
|
+
npm run dev # start in the foreground; holds the terminal, stops on CTRL+C
|
|
31
|
+
npm run dev -- up # start in the background (no-op if already running here)
|
|
32
|
+
npm run dev -- up --restart # stop the dev-server in this worktree if running, then start fresh
|
|
33
|
+
npm run dev -- up --evict # if devLimit is reached, evict the oldest dev-server and start
|
|
34
|
+
npm run dev -- restart # stop the dev-server in this worktree if running, then start in the background
|
|
35
|
+
npm run dev -- status # report whether this worktree's dev-server is UP or DOWN
|
|
36
|
+
npm run dev -- list # active dev-servers across all worktrees
|
|
37
|
+
npm run dev -- down # stop dev server (infrastructure stays up)
|
|
35
38
|
npm run workspace -- remove feat/42 # full teardown
|
|
36
39
|
```
|
|
37
40
|
|
|
@@ -78,7 +81,7 @@ Setup runs in two phases: a fast foreground Part 1 creates the worktree and conf
|
|
|
78
81
|
|
|
79
82
|
**Bootstrap the main worktree first.** Linked-worktree setup copies config sources from the main worktree, so the main must already have those files. Run `workspace setup` once on the main checkout. Use `preSetup` (with `isMainWorktree === true`) to seed sources from examples or templates. `configFiles` entries are required by default; mark `optional: true` for sources that may legitimately be missing.
|
|
80
83
|
|
|
81
|
-
`--evict` is best-effort: the cap check and the subsequent register are not atomic, so two concurrent `dev
|
|
84
|
+
`--evict` is best-effort: the cap check and the subsequent register are not atomic, so two concurrent `dev up --evict` from different worktrees can both pass the check and end up at `devLimit + 1` live servers. The window is narrow; if it matters, `dev list` + `dev down` deterministically.
|
|
82
85
|
|
|
83
86
|
```ts
|
|
84
87
|
import { runDevServer, helpers } from "@paleo/workspace";
|
|
@@ -92,7 +95,8 @@ await runDevServer({
|
|
|
92
95
|
{
|
|
93
96
|
kind: "spawn",
|
|
94
97
|
name: "dev",
|
|
95
|
-
|
|
98
|
+
// Not `dev` — the workspace wrapper script is named `dev`, so `npm run dev` would recurse.
|
|
99
|
+
exec: { command: "npm", args: ["run", "dev:app"] },
|
|
96
100
|
port: helpers.readPortFromEnvFile(".env", "PORT"),
|
|
97
101
|
detectSuccess: (log) => log.includes("Server is ready on port"),
|
|
98
102
|
},
|
package/dist/cli.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type WorkspaceCommand = {
|
|
|
13
13
|
} | {
|
|
14
14
|
kind: "list";
|
|
15
15
|
} | {
|
|
16
|
-
kind: "
|
|
16
|
+
kind: "status";
|
|
17
17
|
slot?: string;
|
|
18
18
|
} | {
|
|
19
19
|
kind: "wait";
|
|
@@ -34,14 +34,26 @@ export interface ParsedWorkspaceArgs {
|
|
|
34
34
|
}
|
|
35
35
|
export declare function parseWorkspaceArgs(argv?: string[]): ParsedWorkspaceArgs;
|
|
36
36
|
export declare function printWorkspaceHelp(): void;
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
export type DevCommand = {
|
|
38
|
+
kind: "foreground";
|
|
39
|
+
evict: boolean;
|
|
40
|
+
restart: boolean;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "up";
|
|
43
|
+
evict: boolean;
|
|
44
|
+
restart: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
kind: "restart";
|
|
47
|
+
evict: boolean;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "down";
|
|
50
|
+
all: boolean;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "list";
|
|
53
|
+
} | {
|
|
54
|
+
kind: "status";
|
|
55
|
+
} | {
|
|
56
|
+
kind: "help";
|
|
57
|
+
};
|
|
58
|
+
export declare function parseDevArgs(argv?: string[]): DevCommand;
|
|
59
|
+
export declare function printDevHelp(): void;
|
package/dist/cli.js
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { ConfigError } from "./errors.js";
|
|
3
|
-
const DEV_SERVER_OPTIONS = {
|
|
4
|
-
help: { type: "boolean", short: "h", description: "Show this help message" },
|
|
5
|
-
stop: { type: "boolean", description: "Stop dev servers in the current worktree" },
|
|
6
|
-
list: { type: "boolean", description: "List active dev-servers across all worktrees" },
|
|
7
|
-
all: { type: "boolean", description: "Apply --stop to every active dev-server" },
|
|
8
|
-
evict: { type: "boolean", description: "Evict the oldest dev-server when the cap is reached" },
|
|
9
|
-
restart: {
|
|
10
|
-
type: "boolean",
|
|
11
|
-
description: "If a dev-server is already running in this worktree, stop it first, then start",
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
3
|
export function parseWorkspaceArgs(argv = process.argv.slice(2)) {
|
|
15
4
|
const [subcommand, ...tokens] = argv;
|
|
16
|
-
if (subcommand ===
|
|
5
|
+
if (subcommand === "--help" || subcommand === "-h") {
|
|
17
6
|
return { command: { kind: "help" }, verbose: false };
|
|
18
7
|
}
|
|
8
|
+
if (subcommand === undefined)
|
|
9
|
+
throw new ConfigError("No command given.");
|
|
19
10
|
try {
|
|
20
11
|
return parseSubcommand(subcommand, tokens);
|
|
21
12
|
}
|
|
22
13
|
catch (err) {
|
|
23
14
|
if (err instanceof ConfigError)
|
|
24
15
|
throw err;
|
|
25
|
-
throw new ConfigError(
|
|
16
|
+
throw new ConfigError(err.message);
|
|
26
17
|
}
|
|
27
18
|
}
|
|
28
19
|
function parseSubcommand(subcommand, tokens) {
|
|
@@ -33,8 +24,8 @@ function parseSubcommand(subcommand, tokens) {
|
|
|
33
24
|
return parseRemove(tokens);
|
|
34
25
|
case "list":
|
|
35
26
|
return parseList(tokens);
|
|
36
|
-
case "
|
|
37
|
-
return
|
|
27
|
+
case "status":
|
|
28
|
+
return parseStatus(tokens);
|
|
38
29
|
case "wait":
|
|
39
30
|
return parseWait(tokens);
|
|
40
31
|
case "set-owner":
|
|
@@ -42,7 +33,7 @@ function parseSubcommand(subcommand, tokens) {
|
|
|
42
33
|
case "__finalize":
|
|
43
34
|
return parseFinalize(tokens);
|
|
44
35
|
default:
|
|
45
|
-
throw new ConfigError(`
|
|
36
|
+
throw new ConfigError(`Unknown command "${subcommand}". Run \`workspace --help\`.`);
|
|
46
37
|
}
|
|
47
38
|
}
|
|
48
39
|
function parseSetup(tokens) {
|
|
@@ -62,7 +53,7 @@ function parseSetup(tokens) {
|
|
|
62
53
|
const branch = takeOptionalPositional(positionals, "setup");
|
|
63
54
|
const newBranch = values["new-branch"] ?? false;
|
|
64
55
|
if (newBranch && branch === undefined) {
|
|
65
|
-
throw new ConfigError("
|
|
56
|
+
throw new ConfigError("`workspace setup <branch> -c` requires a branch name.");
|
|
66
57
|
}
|
|
67
58
|
return {
|
|
68
59
|
command: {
|
|
@@ -103,7 +94,7 @@ function parseList(tokens) {
|
|
|
103
94
|
rejectPositionals(positionals, "list");
|
|
104
95
|
return { command: { kind: "list" }, verbose: values.verbose ?? false };
|
|
105
96
|
}
|
|
106
|
-
function
|
|
97
|
+
function parseStatus(tokens) {
|
|
107
98
|
const { values, positionals } = parseArgs({
|
|
108
99
|
args: tokens,
|
|
109
100
|
options: {
|
|
@@ -113,8 +104,8 @@ function parseInfo(tokens) {
|
|
|
113
104
|
allowPositionals: true,
|
|
114
105
|
strict: true,
|
|
115
106
|
});
|
|
116
|
-
rejectPositionals(positionals, "
|
|
117
|
-
return { command: { kind: "
|
|
107
|
+
rejectPositionals(positionals, "status");
|
|
108
|
+
return { command: { kind: "status", slot: values.slot }, verbose: values.verbose ?? false };
|
|
118
109
|
}
|
|
119
110
|
function parseWait(tokens) {
|
|
120
111
|
const { values, positionals } = parseArgs({
|
|
@@ -151,19 +142,19 @@ function parseFinalize(tokens) {
|
|
|
151
142
|
}
|
|
152
143
|
function takeOptionalPositional(positionals, command) {
|
|
153
144
|
if (positionals.length > 1) {
|
|
154
|
-
throw new ConfigError(
|
|
145
|
+
throw new ConfigError(`\`workspace ${command}\` accepts at most one positional argument.`);
|
|
155
146
|
}
|
|
156
147
|
return positionals[0];
|
|
157
148
|
}
|
|
158
149
|
function takeRequiredPositional(positionals, command, label) {
|
|
159
150
|
if (positionals.length !== 1) {
|
|
160
|
-
throw new ConfigError(
|
|
151
|
+
throw new ConfigError(`\`workspace ${command}\` requires exactly one ${label}.`);
|
|
161
152
|
}
|
|
162
153
|
return positionals[0];
|
|
163
154
|
}
|
|
164
155
|
function rejectPositionals(positionals, command) {
|
|
165
156
|
if (positionals.length > 0) {
|
|
166
|
-
throw new ConfigError(
|
|
157
|
+
throw new ConfigError(`\`workspace ${command}\` takes no positional arguments.`);
|
|
167
158
|
}
|
|
168
159
|
}
|
|
169
160
|
export function printWorkspaceHelp() {
|
|
@@ -182,8 +173,8 @@ export function printWorkspaceHelp() {
|
|
|
182
173
|
" Remove a workspace by branch, or the current one when omitted.",
|
|
183
174
|
" list",
|
|
184
175
|
" List all registered workspaces (slot, status, branch, path, owner, created).",
|
|
185
|
-
"
|
|
186
|
-
" Print a workspace summary (ports, branch, readiness).",
|
|
176
|
+
" status [-s|--slot <port>]",
|
|
177
|
+
" Print a workspace summary (ports, branch, readiness, dev-server).",
|
|
187
178
|
" wait [-s|--slot <port>]",
|
|
188
179
|
" Block until the background finalize reaches READY (exit 0) or FAILED (exit 1).",
|
|
189
180
|
" set-owner <name>",
|
|
@@ -193,44 +184,118 @@ export function printWorkspaceHelp() {
|
|
|
193
184
|
" -v, --verbose Show intermediate output.",
|
|
194
185
|
].join("\n"));
|
|
195
186
|
}
|
|
196
|
-
export function
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
throw new ConfigError("Error: --all requires --stop.");
|
|
205
|
-
}
|
|
206
|
-
if (args.list && (args.stop || args.all)) {
|
|
207
|
-
throw new ConfigError("Error: --list is mutually exclusive with --stop and --all.");
|
|
187
|
+
export function parseDevArgs(argv = process.argv.slice(2)) {
|
|
188
|
+
const [first] = argv;
|
|
189
|
+
if (first === "--help" || first === "-h")
|
|
190
|
+
return { kind: "help" };
|
|
191
|
+
try {
|
|
192
|
+
if (first === undefined || first.startsWith("-"))
|
|
193
|
+
return parseForeground(argv);
|
|
194
|
+
return parseDevSubcommand(first, argv.slice(1));
|
|
208
195
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (err instanceof ConfigError)
|
|
198
|
+
throw err;
|
|
199
|
+
throw new ConfigError(err.message);
|
|
212
200
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
201
|
+
}
|
|
202
|
+
function parseDevSubcommand(subcommand, tokens) {
|
|
203
|
+
switch (subcommand) {
|
|
204
|
+
case "up":
|
|
205
|
+
return parseUp(tokens);
|
|
206
|
+
case "restart":
|
|
207
|
+
return parseRestart(tokens);
|
|
208
|
+
case "down":
|
|
209
|
+
return parseDown(tokens);
|
|
210
|
+
case "list":
|
|
211
|
+
return parseDevList(tokens);
|
|
212
|
+
case "status":
|
|
213
|
+
return parseDevStatus(tokens);
|
|
214
|
+
default:
|
|
215
|
+
throw new ConfigError(`Unknown command "${subcommand}". Run \`dev --help\`.`);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
-
function
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
218
|
+
function parseForeground(tokens) {
|
|
219
|
+
const { evict, restart } = parseEvictRestart(tokens, "dev");
|
|
220
|
+
return { kind: "foreground", evict, restart };
|
|
221
|
+
}
|
|
222
|
+
function parseUp(tokens) {
|
|
223
|
+
const { evict, restart } = parseEvictRestart(tokens, "dev up");
|
|
224
|
+
return { kind: "up", evict, restart };
|
|
225
|
+
}
|
|
226
|
+
function parseEvictRestart(tokens, command) {
|
|
227
|
+
const { values, positionals } = parseArgs({
|
|
228
|
+
args: tokens,
|
|
229
|
+
options: { evict: { type: "boolean" }, restart: { type: "boolean" } },
|
|
230
|
+
allowPositionals: true,
|
|
231
|
+
strict: true,
|
|
232
|
+
});
|
|
233
|
+
rejectDevPositionals(positionals, command);
|
|
234
|
+
return { evict: values.evict ?? false, restart: values.restart ?? false };
|
|
235
|
+
}
|
|
236
|
+
function parseRestart(tokens) {
|
|
237
|
+
const { values, positionals } = parseArgs({
|
|
238
|
+
args: tokens,
|
|
239
|
+
options: { evict: { type: "boolean" } },
|
|
240
|
+
allowPositionals: true,
|
|
241
|
+
strict: true,
|
|
242
|
+
});
|
|
243
|
+
rejectDevPositionals(positionals, "dev restart");
|
|
244
|
+
return { kind: "restart", evict: values.evict ?? false };
|
|
245
|
+
}
|
|
246
|
+
function parseDown(tokens) {
|
|
247
|
+
const { values, positionals } = parseArgs({
|
|
248
|
+
args: tokens,
|
|
249
|
+
options: { all: { type: "boolean" } },
|
|
250
|
+
allowPositionals: true,
|
|
251
|
+
strict: true,
|
|
252
|
+
});
|
|
253
|
+
rejectDevPositionals(positionals, "dev down");
|
|
254
|
+
return { kind: "down", all: values.all ?? false };
|
|
255
|
+
}
|
|
256
|
+
function parseDevList(tokens) {
|
|
257
|
+
const { positionals } = parseArgs({
|
|
258
|
+
args: tokens,
|
|
259
|
+
options: {},
|
|
260
|
+
allowPositionals: true,
|
|
261
|
+
strict: true,
|
|
262
|
+
});
|
|
263
|
+
rejectDevPositionals(positionals, "dev list");
|
|
264
|
+
return { kind: "list" };
|
|
265
|
+
}
|
|
266
|
+
function parseDevStatus(tokens) {
|
|
267
|
+
const { positionals } = parseArgs({
|
|
268
|
+
args: tokens,
|
|
269
|
+
options: {},
|
|
270
|
+
allowPositionals: true,
|
|
271
|
+
strict: true,
|
|
272
|
+
});
|
|
273
|
+
rejectDevPositionals(positionals, "dev status");
|
|
274
|
+
return { kind: "status" };
|
|
275
|
+
}
|
|
276
|
+
function rejectDevPositionals(positionals, command) {
|
|
277
|
+
if (positionals.length > 0) {
|
|
278
|
+
throw new ConfigError(`\`${command}\` takes no positional arguments.`);
|
|
234
279
|
}
|
|
235
|
-
|
|
280
|
+
}
|
|
281
|
+
export function printDevHelp() {
|
|
282
|
+
console.log([
|
|
283
|
+
"Usage: dev [command] [options]",
|
|
284
|
+
"",
|
|
285
|
+
"Start, stop, or list dev-server processes for worktree-based environments.",
|
|
286
|
+
"",
|
|
287
|
+
"Commands:",
|
|
288
|
+
" dev Start in the foreground; holds the terminal, stops on CTRL+C.",
|
|
289
|
+
" dev up Start in the background and return once ready.",
|
|
290
|
+
" dev restart Stop this worktree's dev-server if running, then start in the background.",
|
|
291
|
+
" dev down [--all] Stop this worktree's dev-server, or every dev-server with --all.",
|
|
292
|
+
" dev list List active dev-servers across all worktrees.",
|
|
293
|
+
" dev status Report whether this worktree's dev-server is UP or DOWN.",
|
|
294
|
+
" dev --help Show this help message.",
|
|
295
|
+
"",
|
|
296
|
+
"Options (dev, dev up, dev restart):",
|
|
297
|
+
" --evict Evict the oldest dev-server when the cap is reached.",
|
|
298
|
+
"Options (dev, dev up):",
|
|
299
|
+
" --restart If a dev-server is already running here, stop it first, then start.",
|
|
300
|
+
].join("\n"));
|
|
236
301
|
}
|
package/dist/dev-server.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type WorktreeReadyCheck = {
|
|
|
40
40
|
message: string;
|
|
41
41
|
};
|
|
42
42
|
/**
|
|
43
|
-
* Pure builder for the `dev
|
|
43
|
+
* Pure builder for the `dev` worktree-readiness gate. Returns `ok` when the slot is `ready`
|
|
44
44
|
* or absent (synthesized main); otherwise returns the user-facing error message.
|
|
45
45
|
*/
|
|
46
46
|
export declare function buildWorktreeReadyMessage(input: {
|
package/dist/dev-server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { closeSync, mkdirSync, openSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { parseDevArgs, printDevHelp } from "./cli.js";
|
|
5
5
|
import { evictOldest, findOwnEntry, listDevServers, printActiveServers, pruneAndPersist, registerDevServer, removeDevServerEntryByWorktree, stopAllRegistered, unregisterDevServer, } from "./dev-servers-registry.js";
|
|
6
6
|
import { ConfigError, StartupError } from "./errors.js";
|
|
7
7
|
import { detectCommonJsError, formatDuration, setupLogPath } from "./helpers.js";
|
|
@@ -14,92 +14,167 @@ function logFileFor(runtimeDir, name) {
|
|
|
14
14
|
return join(runtimeDir, "logs", `${name}.log`);
|
|
15
15
|
}
|
|
16
16
|
export async function runDevServer(config) {
|
|
17
|
-
let
|
|
17
|
+
let command;
|
|
18
18
|
try {
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
console.error(err.message);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
if (args.help) {
|
|
26
|
-
printDevServerHelp();
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
try {
|
|
30
|
-
validateDevServerFlags(args);
|
|
19
|
+
command = parseDevArgs();
|
|
31
20
|
}
|
|
32
21
|
catch (err) {
|
|
33
22
|
if (err instanceof ConfigError) {
|
|
34
|
-
console.error(err.message);
|
|
35
|
-
|
|
23
|
+
console.error(`Warning: ${err.message}`);
|
|
24
|
+
printDevHelp();
|
|
25
|
+
process.exit(1);
|
|
36
26
|
}
|
|
37
27
|
throw err;
|
|
38
28
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
listDevServers(mainWorktree, config.registryDir);
|
|
29
|
+
if (command.kind === "help") {
|
|
30
|
+
printDevHelp();
|
|
42
31
|
return;
|
|
43
32
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
33
|
+
const { mainWorktree } = detectWorktree();
|
|
34
|
+
switch (command.kind) {
|
|
35
|
+
case "list":
|
|
36
|
+
listDevServers(mainWorktree, config.registryDir);
|
|
37
|
+
return;
|
|
38
|
+
case "down":
|
|
39
|
+
if (command.all) {
|
|
40
|
+
await stopAllRegistered({
|
|
41
|
+
mainWorktree,
|
|
42
|
+
registryDir: config.registryDir,
|
|
43
|
+
callbackServers: callbackServersOf(config),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await stopLocal(config, mainWorktree);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
case "up":
|
|
51
|
+
await start(config, mainWorktree, { evict: command.evict, restart: command.restart });
|
|
52
|
+
return;
|
|
53
|
+
case "restart":
|
|
54
|
+
await start(config, mainWorktree, { evict: command.evict, restart: true });
|
|
55
|
+
return;
|
|
56
|
+
case "status":
|
|
57
|
+
printStatus(config, mainWorktree);
|
|
58
|
+
return;
|
|
59
|
+
case "foreground":
|
|
60
|
+
await runForeground(config, mainWorktree, {
|
|
61
|
+
evict: command.evict,
|
|
62
|
+
restart: command.restart,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
51
65
|
}
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
}
|
|
67
|
+
function printStatus(config, mainWorktree) {
|
|
68
|
+
const entry = findOwnEntry(mainWorktree, config.registryDir, process.cwd());
|
|
69
|
+
if (!entry || !Object.values(entry.pids).some(isProcessAlive)) {
|
|
70
|
+
console.log("Dev-server status: DOWN.");
|
|
54
71
|
return;
|
|
55
72
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
73
|
+
console.log("Dev-server status: UP.");
|
|
74
|
+
const slot = resolveCurrentSlot(config.basePort, config.registryDir);
|
|
75
|
+
printStartSummary(config, slot, entry.pids);
|
|
60
76
|
}
|
|
61
77
|
function callbackServersOf(config) {
|
|
62
78
|
return config.servers.filter((s) => s.kind === "callback");
|
|
63
79
|
}
|
|
64
|
-
async function start(config, mainWorktree,
|
|
80
|
+
async function start(config, mainWorktree, options) {
|
|
65
81
|
const ctx = { cwd: process.cwd() };
|
|
66
|
-
|
|
67
|
-
if (await handleAlreadyRunning(config, mainWorktree, ctx, restart))
|
|
82
|
+
if (await runStartChecks(config, mainWorktree, ctx, options))
|
|
68
83
|
return;
|
|
69
|
-
|
|
70
|
-
checkNoLocalRegistryConflict(config, mainWorktree, ctx.cwd);
|
|
71
|
-
await checkPortsFree(config.servers, ctx.cwd);
|
|
72
|
-
const spawnPids = {};
|
|
73
|
-
const startedCallbacks = [];
|
|
84
|
+
const state = { spawnPids: {}, startedCallbacks: [] };
|
|
74
85
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
startedCallbacks.push(server);
|
|
83
|
-
}
|
|
86
|
+
await spawnAndAwait(config, ctx, state);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
await rollbackStart(state.spawnPids, state.startedCallbacks, ctx);
|
|
90
|
+
if (err instanceof StartupError) {
|
|
91
|
+
handleStartupFailure(err);
|
|
92
|
+
process.exit(1);
|
|
84
93
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
const slot = registerStartedServer(config, mainWorktree, state.spawnPids);
|
|
97
|
+
printStartSummary(config, slot, state.spawnPids);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Foreground start: hold the terminal and tail logs until CTRL+C, then stop cleanly. Signal
|
|
101
|
+
* handlers are installed before starting so an interrupt during startup rolls back; after a
|
|
102
|
+
* successful start they switch to the local stop sequence.
|
|
103
|
+
*/
|
|
104
|
+
async function runForeground(config, mainWorktree, options) {
|
|
105
|
+
const ctx = { cwd: process.cwd() };
|
|
106
|
+
const state = { spawnPids: {}, startedCallbacks: [] };
|
|
107
|
+
let started = false;
|
|
108
|
+
let shuttingDown = false;
|
|
109
|
+
const onSignal = () => {
|
|
110
|
+
if (shuttingDown)
|
|
111
|
+
return;
|
|
112
|
+
shuttingDown = true;
|
|
113
|
+
if (started) {
|
|
114
|
+
void shutdownForeground(config, mainWorktree);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
void rollbackStart(state.spawnPids, state.startedCallbacks, ctx).then(() => process.exit(130));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
process.on("SIGINT", onSignal);
|
|
121
|
+
process.on("SIGTERM", onSignal);
|
|
122
|
+
if (await runStartChecks(config, mainWorktree, ctx, options))
|
|
123
|
+
process.exit(0);
|
|
124
|
+
try {
|
|
125
|
+
await spawnAndAwait(config, ctx, state);
|
|
94
126
|
}
|
|
95
127
|
catch (err) {
|
|
96
|
-
await rollbackStart(spawnPids, startedCallbacks, ctx);
|
|
128
|
+
await rollbackStart(state.spawnPids, state.startedCallbacks, ctx);
|
|
97
129
|
if (err instanceof StartupError) {
|
|
98
130
|
handleStartupFailure(err);
|
|
99
131
|
process.exit(1);
|
|
100
132
|
}
|
|
101
133
|
throw err;
|
|
102
134
|
}
|
|
135
|
+
const slot = registerStartedServer(config, mainWorktree, state.spawnPids);
|
|
136
|
+
started = true;
|
|
137
|
+
printStartSummary(config, slot, state.spawnPids);
|
|
138
|
+
tailLogs(config, state.spawnPids);
|
|
139
|
+
await new Promise(() => { });
|
|
140
|
+
}
|
|
141
|
+
async function shutdownForeground(config, mainWorktree) {
|
|
142
|
+
console.log("\nStopping dev servers...");
|
|
143
|
+
await stopLocal(config, mainWorktree);
|
|
144
|
+
console.log("Stopped.");
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
async function runStartChecks(config, mainWorktree, ctx, { evict, restart }) {
|
|
148
|
+
checkWorktreeReady(config, mainWorktree, ctx.cwd);
|
|
149
|
+
if (await handleAlreadyRunning(config, mainWorktree, ctx, restart))
|
|
150
|
+
return true;
|
|
151
|
+
await enforceCap(config, mainWorktree, evict);
|
|
152
|
+
checkNoLocalRegistryConflict(config, mainWorktree, ctx.cwd);
|
|
153
|
+
await checkPortsFree(config.servers, ctx.cwd);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
async function spawnAndAwait(config, ctx, state) {
|
|
157
|
+
for (const server of config.servers) {
|
|
158
|
+
console.log(`Starting ${server.name} dev server...`);
|
|
159
|
+
if (server.kind === "spawn") {
|
|
160
|
+
state.spawnPids[server.name] = spawnServer(server, config.runtimeDir, ctx.cwd);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await server.start(ctx);
|
|
164
|
+
state.startedCallbacks.push(server);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const spawnEntries = config.servers.filter((s) => s.kind === "spawn");
|
|
168
|
+
const pollables = spawnEntries.map((s) => ({
|
|
169
|
+
name: s.name,
|
|
170
|
+
logFile: logFileFor(config.runtimeDir, s.name),
|
|
171
|
+
detectSuccess: s.detectSuccess,
|
|
172
|
+
detectError: s.detectError ?? detectCommonJsError,
|
|
173
|
+
}));
|
|
174
|
+
const pollPids = spawnEntries.map((s) => state.spawnPids[s.name]);
|
|
175
|
+
await awaitAllReady(pollables, pollPids);
|
|
176
|
+
}
|
|
177
|
+
function registerStartedServer(config, mainWorktree, spawnPids) {
|
|
103
178
|
const slot = resolveCurrentSlot(config.basePort, config.registryDir);
|
|
104
179
|
const devEntry = {
|
|
105
180
|
slot: slot.slot,
|
|
@@ -111,6 +186,9 @@ async function start(config, mainWorktree, { evict, restart }) {
|
|
|
111
186
|
if (slot.main)
|
|
112
187
|
devEntry.main = true;
|
|
113
188
|
registerDevServer(mainWorktree, config.registryDir, devEntry);
|
|
189
|
+
return slot;
|
|
190
|
+
}
|
|
191
|
+
function printStartSummary(config, slot, spawnPids) {
|
|
114
192
|
const summaryServers = config.servers.map((server) => {
|
|
115
193
|
if (server.kind === "spawn") {
|
|
116
194
|
return { server, port: server.port, pid: spawnPids[server.name] };
|
|
@@ -124,6 +202,35 @@ async function start(config, mainWorktree, { evict, restart }) {
|
|
|
124
202
|
defaultPrintSummary(slot, summaryServers, config.runtimeDir);
|
|
125
203
|
}
|
|
126
204
|
}
|
|
205
|
+
const TAIL_INTERVAL_MS = 300;
|
|
206
|
+
function tailLogs(config, spawnPids) {
|
|
207
|
+
const names = Object.keys(spawnPids);
|
|
208
|
+
const prefixed = names.length > 1;
|
|
209
|
+
for (const name of names) {
|
|
210
|
+
const path = join(process.cwd(), logFileFor(config.runtimeDir, name));
|
|
211
|
+
followLogFile(path, prefixed ? `[${name}] ` : "");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function followLogFile(path, prefix) {
|
|
215
|
+
let offset = existsSync(path) ? statSync(path).size : 0;
|
|
216
|
+
setInterval(() => {
|
|
217
|
+
if (!existsSync(path))
|
|
218
|
+
return;
|
|
219
|
+
const size = statSync(path).size;
|
|
220
|
+
if (size < offset)
|
|
221
|
+
offset = 0;
|
|
222
|
+
if (size <= offset)
|
|
223
|
+
return;
|
|
224
|
+
const length = size - offset;
|
|
225
|
+
const fd = openSync(path, "r");
|
|
226
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
227
|
+
const bytesRead = readSync(fd, buffer, 0, length, offset);
|
|
228
|
+
closeSync(fd);
|
|
229
|
+
offset += bytesRead;
|
|
230
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
231
|
+
process.stdout.write(prefix === "" ? text : text.replace(/^(?=.)/gm, prefix));
|
|
232
|
+
}, TAIL_INTERVAL_MS);
|
|
233
|
+
}
|
|
127
234
|
async function rollbackStart(spawnPids, startedCallbacks, ctx) {
|
|
128
235
|
console.error("\nStopping dev servers...");
|
|
129
236
|
for (const pid of Object.values(spawnPids)) {
|
|
@@ -145,7 +252,7 @@ async function rollbackStart(spawnPids, startedCallbacks, ctx) {
|
|
|
145
252
|
}
|
|
146
253
|
}
|
|
147
254
|
/**
|
|
148
|
-
* Pure builder for the `dev
|
|
255
|
+
* Pure builder for the `dev` worktree-readiness gate. Returns `ok` when the slot is `ready`
|
|
149
256
|
* or absent (synthesized main); otherwise returns the user-facing error message.
|
|
150
257
|
*/
|
|
151
258
|
export function buildWorktreeReadyMessage(input) {
|
|
@@ -159,7 +266,7 @@ export function buildWorktreeReadyMessage(input) {
|
|
|
159
266
|
ok: false,
|
|
160
267
|
message: `Error: Worktree setup is still in progress (slot ${slotPort}, started ${elapsed} ago).\n` +
|
|
161
268
|
`Tail: ${logPath}\n` +
|
|
162
|
-
"Run `workspace wait` to block until it finishes, or retry `dev
|
|
269
|
+
"Run `workspace wait` to block until it finishes, or retry `dev` once ready.",
|
|
163
270
|
};
|
|
164
271
|
}
|
|
165
272
|
const failureAt = entry.failure?.at ?? entry.createdAt;
|
|
@@ -206,10 +313,10 @@ async function handleAlreadyRunning(config, mainWorktree, ctx, restart) {
|
|
|
206
313
|
}
|
|
207
314
|
const pidList = livePids.map(([name, pid]) => `${name}=${pid}`).join(", ");
|
|
208
315
|
console.log(`dev-server already running for this worktree (slot ${entry.slot}, pids: ${pidList}).`);
|
|
209
|
-
console.log("Run `dev
|
|
316
|
+
console.log("Run `dev down` to stop it, or re-run with `--restart` to restart.");
|
|
210
317
|
return true;
|
|
211
318
|
}
|
|
212
|
-
// TOCTOU: the cap check and the subsequent register are not atomic. Two concurrent `dev
|
|
319
|
+
// TOCTOU: the cap check and the subsequent register are not atomic. Two concurrent `dev up --evict`
|
|
213
320
|
// from different worktrees can both pass the cap check and both register, exceeding the limit by
|
|
214
321
|
// one. Accepted: the race window is narrow and the consequence is bounded (one extra dev-server).
|
|
215
322
|
async function enforceCap(config, mainWorktree, evict) {
|
|
@@ -222,8 +329,8 @@ async function enforceCap(config, mainWorktree, evict) {
|
|
|
222
329
|
if (!evict) {
|
|
223
330
|
console.error(`Error: dev-server cap reached (${active.length}/${limit}). Active dev-servers:`);
|
|
224
331
|
printActiveServers(active);
|
|
225
|
-
console.error("Run `dev
|
|
226
|
-
console.error("Re-run with
|
|
332
|
+
console.error("Run `dev down` in another worktree, or `dev down --all`.");
|
|
333
|
+
console.error("Re-run with `--evict` to evict the oldest.");
|
|
227
334
|
process.exit(1);
|
|
228
335
|
}
|
|
229
336
|
const toEvict = active.length - limit + 1;
|
|
@@ -38,5 +38,7 @@ export declare function removeDevServerEntryByWorktree(mainWorktree: string, reg
|
|
|
38
38
|
export declare function findOwnEntry(mainWorktree: string, registryDir: string, worktreePath: string): DevServerEntry | undefined;
|
|
39
39
|
export declare function pruneAndPersist(mainWorktree: string, registryDir: string, isAlive?: IsAliveFn): DevServersData;
|
|
40
40
|
export declare function pruneDeadServers(data: DevServersData, isAlive?: IsAliveFn): DevServersData;
|
|
41
|
+
/** Resolved worktree paths whose dev-server entry has at least one live PID. */
|
|
42
|
+
export declare function liveWorktrees(data: DevServersData, isAlive?: IsAliveFn): Set<string>;
|
|
41
43
|
export declare function readDevServers(mainWorktree: string, registryDir: string): DevServersData;
|
|
42
44
|
export declare function writeDevServers(mainWorktree: string, registryDir: string, data: DevServersData): void;
|
|
@@ -115,6 +115,16 @@ export function pruneDeadServers(data, isAlive = isProcessAlive) {
|
|
|
115
115
|
const live = data.servers.filter((entry) => Object.values(entry.pids).some((pid) => isAlive(pid)));
|
|
116
116
|
return { servers: live };
|
|
117
117
|
}
|
|
118
|
+
/** Resolved worktree paths whose dev-server entry has at least one live PID. */
|
|
119
|
+
export function liveWorktrees(data, isAlive = isProcessAlive) {
|
|
120
|
+
const live = new Set();
|
|
121
|
+
for (const entry of data.servers) {
|
|
122
|
+
if (Object.values(entry.pids).some((pid) => isAlive(pid))) {
|
|
123
|
+
live.add(resolve(entry.worktree));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return live;
|
|
127
|
+
}
|
|
118
128
|
export function readDevServers(mainWorktree, registryDir) {
|
|
119
129
|
const fp = filePath(mainWorktree, registryDir);
|
|
120
130
|
if (!existsSync(fp))
|
|
@@ -3,7 +3,7 @@ export interface ServerContext {
|
|
|
3
3
|
/**
|
|
4
4
|
* Worktree directory for this lifecycle call. Equals `process.cwd()` at start time for local
|
|
5
5
|
* starts/stops; equals the victim entry's worktree for cross-worktree stops (eviction,
|
|
6
|
-
* `dev
|
|
6
|
+
* `dev down --all`). Callbacks MUST thread this into every child-process call
|
|
7
7
|
* (`{ cwd: ctx.cwd }` on `execSync`, `spawn`, etc.) and resolve relative paths against it.
|
|
8
8
|
*/
|
|
9
9
|
cwd: string;
|
package/dist/slots.js
CHANGED
|
@@ -22,7 +22,7 @@ export function resolveAndRegisterSlot(input) {
|
|
|
22
22
|
const owner = input.requestedOwner ?? existing?.owner;
|
|
23
23
|
const createdAt = existing?.createdAt ?? new Date().toISOString();
|
|
24
24
|
// Re-runs of `workspace setup` keep a previously finalized slot ready, unless `--force` is set —
|
|
25
|
-
// then we reset to pending so `workspace wait` blocks and `dev
|
|
25
|
+
// then we reset to pending so `workspace wait` blocks and `dev` refuses during the re-finalize.
|
|
26
26
|
const status = existing?.status === "ready" && !input.force ? "ready" : "pending";
|
|
27
27
|
const entry = {
|
|
28
28
|
worktree: input.currentWorktree,
|
package/dist/workspace.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface WorkspaceConfig {
|
|
|
10
10
|
scriptPath: string;
|
|
11
11
|
/**
|
|
12
12
|
* Absolute path to your dev-server script (the file that calls `runDevServer`). On
|
|
13
|
-
* `workspace remove`, the kernel shells out to `node <devServerScript>
|
|
13
|
+
* `workspace remove`, the kernel shells out to `node <devServerScript> down` with
|
|
14
14
|
* `cwd: <target worktree>`. Typically
|
|
15
15
|
* `fileURLToPath(new URL('./dev-server.mjs', import.meta.url))` from your `workspace.mjs`.
|
|
16
16
|
*/
|
package/dist/workspace.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { parseWorkspaceArgs, printWorkspaceHelp } from "./cli.js";
|
|
5
|
-
import { findOwnEntry, removeDevServerEntryByWorktree } from "./dev-servers-registry.js";
|
|
5
|
+
import { findOwnEntry, liveWorktrees, readDevServers, removeDevServerEntryByWorktree, } from "./dev-servers-registry.js";
|
|
6
6
|
import { ConfigError } from "./errors.js";
|
|
7
7
|
import { copyAndPatchFile, formatDuration, setupLogPath } from "./helpers.js";
|
|
8
8
|
import { isProcessAlive } from "./process-control.js";
|
|
@@ -17,8 +17,9 @@ export async function runWorkspace(config) {
|
|
|
17
17
|
}
|
|
18
18
|
catch (err) {
|
|
19
19
|
if (err instanceof ConfigError) {
|
|
20
|
-
console.error(err.message);
|
|
21
|
-
|
|
20
|
+
console.error(`Warning: ${err.message}`);
|
|
21
|
+
printWorkspaceHelp();
|
|
22
|
+
process.exit(1);
|
|
22
23
|
}
|
|
23
24
|
throw err;
|
|
24
25
|
}
|
|
@@ -38,8 +39,8 @@ export async function runWorkspace(config) {
|
|
|
38
39
|
case "wait":
|
|
39
40
|
await runWait(command, config);
|
|
40
41
|
return;
|
|
41
|
-
case "
|
|
42
|
-
|
|
42
|
+
case "status":
|
|
43
|
+
runStatus(command, config);
|
|
43
44
|
return;
|
|
44
45
|
case "list":
|
|
45
46
|
runList(config);
|
|
@@ -271,7 +272,7 @@ function printDevServerBlock(config, mainWorktree, targetWorktree, now) {
|
|
|
271
272
|
console.log(` log: ${join(targetWorktree, config.runtimeDir, "logs", `${name}.log`)}`);
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
|
-
function
|
|
275
|
+
function runStatus(command, config) {
|
|
275
276
|
if (command.slot !== undefined) {
|
|
276
277
|
const slot = resolveTargetSlot(command.slot, config);
|
|
277
278
|
const ctx = detectWorktree();
|
|
@@ -295,10 +296,12 @@ function runList(config) {
|
|
|
295
296
|
console.log("No workspaces registered.");
|
|
296
297
|
return;
|
|
297
298
|
}
|
|
299
|
+
const liveSet = liveWorktrees(readDevServers(ctx.mainWorktree, config.registryDir));
|
|
298
300
|
const rows = entries.map(([port, e]) => ({
|
|
299
301
|
slot: port,
|
|
300
302
|
type: e.main ? "main" : "linked",
|
|
301
303
|
status: e.status,
|
|
304
|
+
dev: liveSet.has(resolve(e.worktree)) ? "up" : "-",
|
|
302
305
|
branch: getWorktreeBranch(e.worktree) ?? "(detached)",
|
|
303
306
|
worktree: e.worktree,
|
|
304
307
|
owner: e.owner ?? "-",
|
|
@@ -308,6 +311,7 @@ function runList(config) {
|
|
|
308
311
|
slot: "SLOT",
|
|
309
312
|
type: "TYPE",
|
|
310
313
|
status: "STATUS",
|
|
314
|
+
dev: "DEV",
|
|
311
315
|
branch: "BRANCH",
|
|
312
316
|
worktree: "PATH",
|
|
313
317
|
owner: "OWNER",
|
|
@@ -317,11 +321,12 @@ function runList(config) {
|
|
|
317
321
|
slot: Math.max(headers.slot.length, ...rows.map((r) => r.slot.length)),
|
|
318
322
|
type: Math.max(headers.type.length, ...rows.map((r) => r.type.length)),
|
|
319
323
|
status: Math.max(headers.status.length, ...rows.map((r) => r.status.length)),
|
|
324
|
+
dev: Math.max(headers.dev.length, ...rows.map((r) => r.dev.length)),
|
|
320
325
|
branch: Math.max(headers.branch.length, ...rows.map((r) => r.branch.length)),
|
|
321
326
|
worktree: Math.max(headers.worktree.length, ...rows.map((r) => r.worktree.length)),
|
|
322
327
|
owner: Math.max(headers.owner.length, ...rows.map((r) => r.owner.length)),
|
|
323
328
|
};
|
|
324
|
-
const fmt = (r) => `${r.slot.padEnd(widths.slot)} ${r.type.padEnd(widths.type)} ${r.status.padEnd(widths.status)} ${r.branch.padEnd(widths.branch)} ${r.worktree.padEnd(widths.worktree)} ${r.owner.padEnd(widths.owner)} ${r.created}`;
|
|
329
|
+
const fmt = (r) => `${r.slot.padEnd(widths.slot)} ${r.type.padEnd(widths.type)} ${r.status.padEnd(widths.status)} ${r.dev.padEnd(widths.dev)} ${r.branch.padEnd(widths.branch)} ${r.worktree.padEnd(widths.worktree)} ${r.owner.padEnd(widths.owner)} ${r.created}`;
|
|
325
330
|
console.log(fmt(headers));
|
|
326
331
|
for (const r of rows)
|
|
327
332
|
console.log(fmt(r));
|
|
@@ -394,7 +399,7 @@ async function handleRemove(command, ctx, run, config) {
|
|
|
394
399
|
stopTargetDevServer(config.devServerScript, target.worktreePath, verboseLog);
|
|
395
400
|
}
|
|
396
401
|
else {
|
|
397
|
-
verboseLog(`No dev-server running in ${target.worktreePath}; skipping
|
|
402
|
+
verboseLog(`No dev-server running in ${target.worktreePath}; skipping stop.`);
|
|
398
403
|
}
|
|
399
404
|
if (config.purgeInfrastructure) {
|
|
400
405
|
await config.purgeInfrastructure({
|
|
@@ -516,21 +521,21 @@ function resolveRemoveTarget(command, ctx, registry) {
|
|
|
516
521
|
}
|
|
517
522
|
/**
|
|
518
523
|
* Stops the dev-server running in the target worktree by shelling out to
|
|
519
|
-
* `node <devServerScript>
|
|
524
|
+
* `node <devServerScript> down` with `cwd: worktreePath`. The subprocess runs the target's
|
|
520
525
|
* own stop flow — registry-based spawn-PID kill + callback `stop()` from the target's branch.
|
|
521
526
|
*/
|
|
522
527
|
function stopTargetDevServer(devServerScript, worktreePath, log) {
|
|
523
528
|
log(`Stopping dev-server in ${worktreePath}...`);
|
|
524
|
-
const result = spawnSync(process.execPath, [devServerScript, "
|
|
529
|
+
const result = spawnSync(process.execPath, [devServerScript, "down"], {
|
|
525
530
|
cwd: worktreePath,
|
|
526
531
|
stdio: "inherit",
|
|
527
532
|
timeout: 30_000,
|
|
528
533
|
});
|
|
529
534
|
if (result.error) {
|
|
530
|
-
console.warn(`Warning: failed to run dev
|
|
535
|
+
console.warn(`Warning: failed to run dev down: ${result.error.message}`);
|
|
531
536
|
}
|
|
532
537
|
else if (result.status !== 0) {
|
|
533
|
-
console.warn(`Warning: dev
|
|
538
|
+
console.warn(`Warning: dev down exited with code ${result.status}.`);
|
|
534
539
|
}
|
|
535
540
|
}
|
|
536
541
|
function resolvePortsFn(config) {
|