@paleo/workspace 0.16.0 → 0.17.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 +0 -2
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +12 -0
- package/dist/dev-server.d.ts +0 -6
- package/dist/dev-server.js +51 -46
- package/dist/dev-servers-registry.d.ts +2 -0
- package/dist/dev-servers-registry.js +11 -0
- package/dist/slots.d.ts +9 -0
- package/dist/slots.js +17 -0
- package/dist/workspace.d.ts +0 -6
- package/dist/workspace.js +139 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,6 @@ await runWorkspace({
|
|
|
51
51
|
portNames: ["server", "frontend", "db"],
|
|
52
52
|
sharedDirs: [".local", ".plans"],
|
|
53
53
|
runtimeDir: ".local-wt",
|
|
54
|
-
registryDir: ".local/_workspace-registry",
|
|
55
54
|
configFiles: [
|
|
56
55
|
{
|
|
57
56
|
path: ".env",
|
|
@@ -89,7 +88,6 @@ import { runDevServer, helpers } from "@paleo/workspace";
|
|
|
89
88
|
await runDevServer({
|
|
90
89
|
basePort: 8100,
|
|
91
90
|
runtimeDir: ".local-wt",
|
|
92
|
-
registryDir: ".local/_workspace-registry",
|
|
93
91
|
devLimit: 5,
|
|
94
92
|
servers: [
|
|
95
93
|
{
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -32,6 +32,8 @@ function parseSubcommand(subcommand, tokens) {
|
|
|
32
32
|
return parseSetOwner(tokens);
|
|
33
33
|
case "__finalize":
|
|
34
34
|
return parseFinalize(tokens);
|
|
35
|
+
case "migrate-0.16":
|
|
36
|
+
return parseMigrate(tokens);
|
|
35
37
|
default:
|
|
36
38
|
throw new ConfigError(`Unknown command "${subcommand}". Run \`workspace --help\`.`);
|
|
37
39
|
}
|
|
@@ -140,6 +142,16 @@ function parseFinalize(tokens) {
|
|
|
140
142
|
const slot = takeRequiredPositional(positionals, "__finalize", "slot");
|
|
141
143
|
return { command: { kind: "finalize", slot, force: values.force ?? false }, verbose: false };
|
|
142
144
|
}
|
|
145
|
+
function parseMigrate(tokens) {
|
|
146
|
+
const { values, positionals } = parseArgs({
|
|
147
|
+
args: tokens,
|
|
148
|
+
options: { verbose: { type: "boolean", short: "v" } },
|
|
149
|
+
allowPositionals: true,
|
|
150
|
+
strict: true,
|
|
151
|
+
});
|
|
152
|
+
const oldRegistryDir = takeRequiredPositional(positionals, "migrate-0.16", "old-registry-dir");
|
|
153
|
+
return { command: { kind: "migrate", oldRegistryDir }, verbose: values.verbose ?? false };
|
|
154
|
+
}
|
|
143
155
|
function takeOptionalPositional(positionals, command) {
|
|
144
156
|
if (positionals.length > 1) {
|
|
145
157
|
throw new ConfigError(`\`workspace ${command}\` accepts at most one positional argument.`);
|
package/dist/dev-server.d.ts
CHANGED
|
@@ -8,12 +8,6 @@ export interface DevServerConfig {
|
|
|
8
8
|
basePort: number;
|
|
9
9
|
/** Per-worktree runtime directory, relative to the worktree root (e.g. `.local-wt`). */
|
|
10
10
|
runtimeDir: string;
|
|
11
|
-
/**
|
|
12
|
-
* Shared registry directory, relative to a worktree root (e.g. `.local/_workspace-registry`).
|
|
13
|
-
* Holds `slots.json` and `dev-servers.json`. Must resolve to the same physical directory
|
|
14
|
-
* across linked worktrees — typically via a symlink (e.g. `.local`).
|
|
15
|
-
*/
|
|
16
|
-
registryDir: string;
|
|
17
11
|
/** Maximum concurrent dev-servers across all worktrees. Omit for no limit. */
|
|
18
12
|
devLimit?: number;
|
|
19
13
|
/** One entry per server to start. Started in array order; stopped in reverse order. */
|
package/dist/dev-server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { detectCommonJsError, formatDuration, lastLines, setupLogPath } from "./
|
|
|
8
8
|
import { awaitAllReady, handleStartupFailure, LOG_TAIL_LINES, } from "./log-polling.js";
|
|
9
9
|
import { canonicalCwd, detectPortConflicts, sweepStalePorts, waitForPortsFree, } from "./port-holder.js";
|
|
10
10
|
import { isProcessAlive, stopProcessGroup } from "./process-control.js";
|
|
11
|
-
import { readSlots, resolveCurrentSlot } from "./slots.js";
|
|
11
|
+
import { readSlots, registryDirFor, resolveCurrentSlot, warnLegacyRegistryDir, } from "./slots.js";
|
|
12
12
|
import { detectWorktree, getWorktreeBranch } from "./worktree.js";
|
|
13
13
|
function logFileFor(runtimeDir, name) {
|
|
14
14
|
return join(runtimeDir, "logs", `${name}.log`);
|
|
@@ -30,60 +30,65 @@ export async function runDevServer(config) {
|
|
|
30
30
|
printDevHelp();
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
+
warnLegacyRegistryDir(config);
|
|
34
|
+
const registryDir = registryDirFor(config.runtimeDir);
|
|
33
35
|
const { mainWorktree } = detectWorktree();
|
|
34
36
|
switch (command.kind) {
|
|
35
37
|
case "list":
|
|
36
|
-
listDevServers(mainWorktree,
|
|
38
|
+
listDevServers(mainWorktree, registryDir);
|
|
37
39
|
return;
|
|
38
40
|
case "down":
|
|
39
41
|
if (command.all) {
|
|
40
42
|
await stopAllRegistered({
|
|
41
43
|
mainWorktree,
|
|
42
|
-
registryDir
|
|
44
|
+
registryDir,
|
|
43
45
|
callbackServers: callbackServersOf(config),
|
|
44
46
|
});
|
|
45
47
|
}
|
|
46
48
|
else {
|
|
47
|
-
await stopLocal(config, mainWorktree);
|
|
49
|
+
await stopLocal(config, mainWorktree, registryDir);
|
|
48
50
|
}
|
|
49
51
|
return;
|
|
50
52
|
case "up":
|
|
51
|
-
await start(config, mainWorktree,
|
|
53
|
+
await start(config, mainWorktree, registryDir, {
|
|
54
|
+
evict: command.evict,
|
|
55
|
+
restart: command.restart,
|
|
56
|
+
});
|
|
52
57
|
return;
|
|
53
58
|
case "restart":
|
|
54
|
-
await start(config, mainWorktree, { evict: command.evict, restart: true });
|
|
59
|
+
await start(config, mainWorktree, registryDir, { evict: command.evict, restart: true });
|
|
55
60
|
return;
|
|
56
61
|
case "status":
|
|
57
|
-
printStatus(config, mainWorktree);
|
|
62
|
+
printStatus(config, mainWorktree, registryDir);
|
|
58
63
|
return;
|
|
59
64
|
case "foreground":
|
|
60
|
-
await runForeground(config, mainWorktree, {
|
|
65
|
+
await runForeground(config, mainWorktree, registryDir, {
|
|
61
66
|
evict: command.evict,
|
|
62
67
|
restart: command.restart,
|
|
63
68
|
});
|
|
64
69
|
return;
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
|
-
function printStatus(config, mainWorktree) {
|
|
68
|
-
const entry = findOwnEntry(mainWorktree,
|
|
72
|
+
function printStatus(config, mainWorktree, registryDir) {
|
|
73
|
+
const entry = findOwnEntry(mainWorktree, registryDir, process.cwd());
|
|
69
74
|
if (!entry || !Object.values(entry.pids).some(isProcessAlive)) {
|
|
70
75
|
console.log("Dev-server status: DOWN.");
|
|
71
76
|
return;
|
|
72
77
|
}
|
|
73
78
|
console.log("Dev-server status: UP.");
|
|
74
|
-
const slot = resolveCurrentSlot(config.basePort,
|
|
79
|
+
const slot = resolveCurrentSlot(config.basePort, registryDir);
|
|
75
80
|
printStartSummary(config, slot, entry.pids);
|
|
76
81
|
}
|
|
77
82
|
function callbackServersOf(config) {
|
|
78
83
|
return config.servers.filter((s) => s.kind === "callback");
|
|
79
84
|
}
|
|
80
|
-
async function start(config, mainWorktree, options) {
|
|
85
|
+
async function start(config, mainWorktree, registryDir, options) {
|
|
81
86
|
const ctx = { cwd: process.cwd() };
|
|
82
|
-
if (await runStartChecks(config, mainWorktree, ctx, options))
|
|
87
|
+
if (await runStartChecks(config, mainWorktree, registryDir, ctx, options))
|
|
83
88
|
return;
|
|
84
89
|
const state = { spawnPids: {}, startedCallbacks: [] };
|
|
85
90
|
await spawnWithRollback(config, ctx, state);
|
|
86
|
-
const slot = registerStartedServer(config, mainWorktree, state.spawnPids);
|
|
91
|
+
const slot = registerStartedServer(config, mainWorktree, registryDir, state.spawnPids);
|
|
87
92
|
printStartSummary(config, slot, state.spawnPids);
|
|
88
93
|
}
|
|
89
94
|
/**
|
|
@@ -91,10 +96,10 @@ async function start(config, mainWorktree, options) {
|
|
|
91
96
|
* handlers are installed before starting so an interrupt during startup rolls back; after a
|
|
92
97
|
* successful start they switch to the local stop sequence.
|
|
93
98
|
*/
|
|
94
|
-
async function runForeground(config, mainWorktree, options) {
|
|
99
|
+
async function runForeground(config, mainWorktree, registryDir, options) {
|
|
95
100
|
const ctx = { cwd: process.cwd() };
|
|
96
101
|
if (!options.restart) {
|
|
97
|
-
const running = findOwnLiveEntry(
|
|
102
|
+
const running = findOwnLiveEntry(mainWorktree, registryDir, ctx.cwd);
|
|
98
103
|
if (running)
|
|
99
104
|
return attachForeground(config, running);
|
|
100
105
|
}
|
|
@@ -106,7 +111,7 @@ async function runForeground(config, mainWorktree, options) {
|
|
|
106
111
|
return;
|
|
107
112
|
shuttingDown = true;
|
|
108
113
|
if (started) {
|
|
109
|
-
void shutdownForeground(config, mainWorktree);
|
|
114
|
+
void shutdownForeground(config, mainWorktree, registryDir);
|
|
110
115
|
}
|
|
111
116
|
else {
|
|
112
117
|
void rollbackStart(state.spawnPids, state.startedCallbacks, ctx).then(() => process.exit(130));
|
|
@@ -114,7 +119,7 @@ async function runForeground(config, mainWorktree, options) {
|
|
|
114
119
|
};
|
|
115
120
|
process.on("SIGINT", onSignal);
|
|
116
121
|
process.on("SIGTERM", onSignal);
|
|
117
|
-
if (await runStartChecks(config, mainWorktree, ctx, options))
|
|
122
|
+
if (await runStartChecks(config, mainWorktree, registryDir, ctx, options))
|
|
118
123
|
process.exit(0);
|
|
119
124
|
// Stream logs from the first byte so the whole startup (e.g. a slow build) is visible live.
|
|
120
125
|
const streamLogs = () => tailLogs(config, state.spawnPids, { fromStart: true });
|
|
@@ -123,7 +128,7 @@ async function runForeground(config, mainWorktree, options) {
|
|
|
123
128
|
if (!(await spawnWithRollback(config, ctx, state, () => shuttingDown, streamLogs))) {
|
|
124
129
|
await new Promise(() => { });
|
|
125
130
|
}
|
|
126
|
-
const slot = registerStartedServer(config, mainWorktree, state.spawnPids);
|
|
131
|
+
const slot = registerStartedServer(config, mainWorktree, registryDir, state.spawnPids);
|
|
127
132
|
started = true;
|
|
128
133
|
printStartSummary(config, slot, state.spawnPids);
|
|
129
134
|
watchForExternalStop(Object.values(state.spawnPids), () => {
|
|
@@ -135,8 +140,8 @@ async function runForeground(config, mainWorktree, options) {
|
|
|
135
140
|
});
|
|
136
141
|
await new Promise(() => { });
|
|
137
142
|
}
|
|
138
|
-
function findOwnLiveEntry(
|
|
139
|
-
const entry = findOwnEntry(mainWorktree,
|
|
143
|
+
function findOwnLiveEntry(mainWorktree, registryDir, cwd) {
|
|
144
|
+
const entry = findOwnEntry(mainWorktree, registryDir, cwd);
|
|
140
145
|
if (!entry)
|
|
141
146
|
return;
|
|
142
147
|
return Object.values(entry.pids).some(isProcessAlive) ? entry : undefined;
|
|
@@ -171,18 +176,18 @@ async function attachForeground(config, entry) {
|
|
|
171
176
|
});
|
|
172
177
|
await new Promise(() => { });
|
|
173
178
|
}
|
|
174
|
-
async function shutdownForeground(config, mainWorktree) {
|
|
179
|
+
async function shutdownForeground(config, mainWorktree, registryDir) {
|
|
175
180
|
console.log("\nStopping dev servers...");
|
|
176
|
-
await stopLocal(config, mainWorktree);
|
|
181
|
+
await stopLocal(config, mainWorktree, registryDir);
|
|
177
182
|
console.log("Stopped.");
|
|
178
183
|
process.exit(0);
|
|
179
184
|
}
|
|
180
|
-
async function runStartChecks(config, mainWorktree, ctx, { evict, restart }) {
|
|
181
|
-
checkWorktreeReady(config, mainWorktree, ctx.cwd);
|
|
182
|
-
if (await handleAlreadyRunning(config, mainWorktree, ctx, restart))
|
|
185
|
+
async function runStartChecks(config, mainWorktree, registryDir, ctx, { evict, restart }) {
|
|
186
|
+
checkWorktreeReady(config, mainWorktree, registryDir, ctx.cwd);
|
|
187
|
+
if (await handleAlreadyRunning(config, mainWorktree, registryDir, ctx, restart))
|
|
183
188
|
return true;
|
|
184
|
-
await enforceCap(config, mainWorktree, evict);
|
|
185
|
-
checkNoLocalRegistryConflict(
|
|
189
|
+
await enforceCap(config, mainWorktree, registryDir, evict);
|
|
190
|
+
checkNoLocalRegistryConflict(mainWorktree, registryDir, ctx.cwd);
|
|
186
191
|
await checkPortsFree(config.servers, ctx.cwd);
|
|
187
192
|
return false;
|
|
188
193
|
}
|
|
@@ -244,8 +249,8 @@ async function spawnAndAwait(config, ctx, state, onSpawned) {
|
|
|
244
249
|
const pollPids = spawnEntries.map((s) => state.spawnPids[s.name]);
|
|
245
250
|
await awaitAllReady(pollables, pollPids);
|
|
246
251
|
}
|
|
247
|
-
function registerStartedServer(config, mainWorktree, spawnPids) {
|
|
248
|
-
const slot = resolveCurrentSlot(config.basePort,
|
|
252
|
+
function registerStartedServer(config, mainWorktree, registryDir, spawnPids) {
|
|
253
|
+
const slot = resolveCurrentSlot(config.basePort, registryDir);
|
|
249
254
|
const devEntry = {
|
|
250
255
|
slot: slot.slot,
|
|
251
256
|
worktree: slot.worktree,
|
|
@@ -255,7 +260,7 @@ function registerStartedServer(config, mainWorktree, spawnPids) {
|
|
|
255
260
|
};
|
|
256
261
|
if (slot.main)
|
|
257
262
|
devEntry.main = true;
|
|
258
|
-
registerDevServer(mainWorktree,
|
|
263
|
+
registerDevServer(mainWorktree, registryDir, devEntry);
|
|
259
264
|
return slot;
|
|
260
265
|
}
|
|
261
266
|
function printStartSummary(config, slot, spawnPids) {
|
|
@@ -382,9 +387,9 @@ export function buildWorktreeReadyMessage(input) {
|
|
|
382
387
|
"Re-run `workspace setup` to retry the finalize.",
|
|
383
388
|
};
|
|
384
389
|
}
|
|
385
|
-
function checkWorktreeReady(config, mainWorktree, cwd) {
|
|
386
|
-
const slot = resolveCurrentSlot(config.basePort,
|
|
387
|
-
const entry = readSlots(mainWorktree,
|
|
390
|
+
function checkWorktreeReady(config, mainWorktree, registryDir, cwd) {
|
|
391
|
+
const slot = resolveCurrentSlot(config.basePort, registryDir);
|
|
392
|
+
const entry = readSlots(mainWorktree, registryDir).slots[String(slot.slot)];
|
|
388
393
|
const result = buildWorktreeReadyMessage({
|
|
389
394
|
slotPort: slot.slot,
|
|
390
395
|
worktreePath: cwd,
|
|
@@ -402,8 +407,8 @@ function checkWorktreeReady(config, mainWorktree, cwd) {
|
|
|
402
407
|
* normal start path can proceed, or print a friendly notice and return `true` to short-circuit.
|
|
403
408
|
* Returns `true` when the caller should exit cleanly without starting.
|
|
404
409
|
*/
|
|
405
|
-
async function handleAlreadyRunning(config, mainWorktree, ctx, restart) {
|
|
406
|
-
const entry = findOwnEntry(mainWorktree,
|
|
410
|
+
async function handleAlreadyRunning(config, mainWorktree, registryDir, ctx, restart) {
|
|
411
|
+
const entry = findOwnEntry(mainWorktree, registryDir, ctx.cwd);
|
|
407
412
|
if (!entry)
|
|
408
413
|
return false;
|
|
409
414
|
const livePids = Object.entries(entry.pids).filter(([, pid]) => isProcessAlive(pid));
|
|
@@ -411,7 +416,7 @@ async function handleAlreadyRunning(config, mainWorktree, ctx, restart) {
|
|
|
411
416
|
return false;
|
|
412
417
|
if (restart) {
|
|
413
418
|
console.log("Restarting dev-server in this worktree...");
|
|
414
|
-
await stopLocal(config, mainWorktree);
|
|
419
|
+
await stopLocal(config, mainWorktree, registryDir);
|
|
415
420
|
return false;
|
|
416
421
|
}
|
|
417
422
|
const pidList = livePids.map(([name, pid]) => `${name}=${pid}`).join(", ");
|
|
@@ -422,11 +427,11 @@ async function handleAlreadyRunning(config, mainWorktree, ctx, restart) {
|
|
|
422
427
|
// TOCTOU: the cap check and the subsequent register are not atomic. Two concurrent `dev up --evict`
|
|
423
428
|
// from different worktrees can both pass the cap check and both register, exceeding the limit by
|
|
424
429
|
// one. Accepted: the race window is narrow and the consequence is bounded (one extra dev-server).
|
|
425
|
-
async function enforceCap(config, mainWorktree, evict) {
|
|
430
|
+
async function enforceCap(config, mainWorktree, registryDir, evict) {
|
|
426
431
|
const limit = config.devLimit;
|
|
427
432
|
if (limit === undefined)
|
|
428
433
|
return;
|
|
429
|
-
const active = pruneAndPersist(mainWorktree,
|
|
434
|
+
const active = pruneAndPersist(mainWorktree, registryDir).servers;
|
|
430
435
|
if (active.length < limit)
|
|
431
436
|
return;
|
|
432
437
|
if (!evict) {
|
|
@@ -440,7 +445,7 @@ async function enforceCap(config, mainWorktree, evict) {
|
|
|
440
445
|
console.log(`Evicting ${toEvict} dev-server(s) to make room (cap ${limit}).`);
|
|
441
446
|
const evicted = await evictOldest({
|
|
442
447
|
mainWorktree,
|
|
443
|
-
registryDir
|
|
448
|
+
registryDir,
|
|
444
449
|
callbackServers: callbackServersOf(config),
|
|
445
450
|
count: toEvict,
|
|
446
451
|
});
|
|
@@ -482,8 +487,8 @@ async function checkPortsFree(servers, cwd) {
|
|
|
482
487
|
process.exit(1);
|
|
483
488
|
}
|
|
484
489
|
}
|
|
485
|
-
function checkNoLocalRegistryConflict(
|
|
486
|
-
const entry = findOwnEntry(mainWorktree,
|
|
490
|
+
function checkNoLocalRegistryConflict(mainWorktree, registryDir, cwd) {
|
|
491
|
+
const entry = findOwnEntry(mainWorktree, registryDir, cwd);
|
|
487
492
|
if (!entry)
|
|
488
493
|
return;
|
|
489
494
|
for (const [name, pid] of Object.entries(entry.pids)) {
|
|
@@ -493,11 +498,11 @@ function checkNoLocalRegistryConflict(config, mainWorktree, cwd) {
|
|
|
493
498
|
}
|
|
494
499
|
}
|
|
495
500
|
// Stale entry — drop it so registration overwrites cleanly.
|
|
496
|
-
removeDevServerEntryByWorktree(mainWorktree,
|
|
501
|
+
removeDevServerEntryByWorktree(mainWorktree, registryDir, cwd);
|
|
497
502
|
}
|
|
498
|
-
async function stopLocal(config, mainWorktree) {
|
|
503
|
+
async function stopLocal(config, mainWorktree, registryDir) {
|
|
499
504
|
const ctx = { cwd: process.cwd() };
|
|
500
|
-
const entry = findOwnEntry(mainWorktree,
|
|
505
|
+
const entry = findOwnEntry(mainWorktree, registryDir, ctx.cwd);
|
|
501
506
|
if (!entry) {
|
|
502
507
|
console.log("No dev-server running in this worktree.");
|
|
503
508
|
await sweepStalePorts(config.servers, ctx.cwd);
|
|
@@ -519,7 +524,7 @@ async function stopLocal(config, mainWorktree) {
|
|
|
519
524
|
console.error(` Failed to stop ${server.name}: ${err.message}`);
|
|
520
525
|
}
|
|
521
526
|
}
|
|
522
|
-
unregisterDevServer(mainWorktree,
|
|
527
|
+
unregisterDevServer(mainWorktree, registryDir, ctx.cwd);
|
|
523
528
|
await sweepStalePorts(config.servers, ctx.cwd);
|
|
524
529
|
}
|
|
525
530
|
function defaultPrintSummary(slot, servers, runtimeDir) {
|
|
@@ -42,3 +42,5 @@ export declare function pruneDeadServers(data: DevServersData, isAlive?: IsAlive
|
|
|
42
42
|
export declare function liveWorktrees(data: DevServersData, isAlive?: IsAliveFn): Set<string>;
|
|
43
43
|
export declare function readDevServers(mainWorktree: string, registryDir: string): DevServersData;
|
|
44
44
|
export declare function writeDevServers(mainWorktree: string, registryDir: string, data: DevServersData): void;
|
|
45
|
+
/** Union by resolved `worktree`; `override` wins on conflict. Base-first then override-only order. */
|
|
46
|
+
export declare function mergeDevServers(base: DevServersData, override: DevServersData): DevServersData;
|
|
@@ -136,6 +136,17 @@ export function writeDevServers(mainWorktree, registryDir, data) {
|
|
|
136
136
|
mkdirSync(join(mainWorktree, registryDir), { recursive: true });
|
|
137
137
|
writeFileSync(fp, `${JSON.stringify(data, undefined, 2)}\n`);
|
|
138
138
|
}
|
|
139
|
+
/** Union by resolved `worktree`; `override` wins on conflict. Base-first then override-only order. */
|
|
140
|
+
export function mergeDevServers(base, override) {
|
|
141
|
+
const overrideByWorktree = new Map(override.servers.map((e) => [resolve(e.worktree), e]));
|
|
142
|
+
const merged = base.servers.map((entry) => overrideByWorktree.get(resolve(entry.worktree)) ?? entry);
|
|
143
|
+
const baseWorktrees = new Set(base.servers.map((e) => resolve(e.worktree)));
|
|
144
|
+
for (const entry of override.servers) {
|
|
145
|
+
if (!baseWorktrees.has(resolve(entry.worktree)))
|
|
146
|
+
merged.push(entry);
|
|
147
|
+
}
|
|
148
|
+
return { servers: merged };
|
|
149
|
+
}
|
|
139
150
|
function filePath(mainWorktree, registryDir) {
|
|
140
151
|
return join(mainWorktree, registryDir, DEV_SERVERS_FILENAME);
|
|
141
152
|
}
|
package/dist/slots.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type PortScheme } from "./ports.js";
|
|
2
|
+
export declare const REGISTRY_SUBDIR = "shared-registry";
|
|
3
|
+
export declare function registryDirFor(runtimeDir: string): string;
|
|
4
|
+
/** `registryDir` is gone from the config types but may linger in a consumer's config file. */
|
|
5
|
+
export declare function warnLegacyRegistryDir(config: {
|
|
6
|
+
runtimeDir: string;
|
|
7
|
+
registryDir?: string;
|
|
8
|
+
}): void;
|
|
2
9
|
export interface ResolvedSlot {
|
|
3
10
|
slot: number;
|
|
4
11
|
worktree: string;
|
|
@@ -24,6 +31,8 @@ export interface SlotsRegistry {
|
|
|
24
31
|
}
|
|
25
32
|
export declare function readSlots(mainWorktree: string, registryDir: string): SlotsRegistry;
|
|
26
33
|
export declare function writeSlots(mainWorktree: string, registryDir: string, registry: SlotsRegistry): void;
|
|
34
|
+
/** Union of slots keyed by port; `override` wins on conflict. */
|
|
35
|
+
export declare function mergeSlots(base: SlotsRegistry, override: SlotsRegistry): SlotsRegistry;
|
|
27
36
|
export interface RegisterSlotInput {
|
|
28
37
|
slot?: string;
|
|
29
38
|
currentWorktree: string;
|
package/dist/slots.js
CHANGED
|
@@ -3,7 +3,20 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { allPorts, isValidPort } from "./ports.js";
|
|
5
5
|
import { getWorktreeBranch } from "./worktree.js";
|
|
6
|
+
export const REGISTRY_SUBDIR = "shared-registry";
|
|
6
7
|
const SLOTS_FILENAME = "slots.json";
|
|
8
|
+
export function registryDirFor(runtimeDir) {
|
|
9
|
+
return join(runtimeDir, REGISTRY_SUBDIR);
|
|
10
|
+
}
|
|
11
|
+
/** `registryDir` is gone from the config types but may linger in a consumer's config file. */
|
|
12
|
+
export function warnLegacyRegistryDir(config) {
|
|
13
|
+
if (config.registryDir === undefined)
|
|
14
|
+
return;
|
|
15
|
+
console.warn("Warning: `registryDir` is obsolete and ignored. The registry now lives at " +
|
|
16
|
+
`\`${registryDirFor(config.runtimeDir)}\`. Remove \`registryDir\` from your config. ` +
|
|
17
|
+
`If you have an existing registry at "${config.registryDir}", run ` +
|
|
18
|
+
`\`workspace migrate-0.16 ${config.registryDir}\` once to merge it.`);
|
|
19
|
+
}
|
|
7
20
|
export function readSlots(mainWorktree, registryDir) {
|
|
8
21
|
const filePath = join(mainWorktree, registryDir, SLOTS_FILENAME);
|
|
9
22
|
if (!existsSync(filePath))
|
|
@@ -15,6 +28,10 @@ export function writeSlots(mainWorktree, registryDir, registry) {
|
|
|
15
28
|
mkdirSync(join(mainWorktree, registryDir), { recursive: true });
|
|
16
29
|
writeFileSync(filePath, `${JSON.stringify(registry, undefined, 2)}\n`);
|
|
17
30
|
}
|
|
31
|
+
/** Union of slots keyed by port; `override` wins on conflict. */
|
|
32
|
+
export function mergeSlots(base, override) {
|
|
33
|
+
return { slots: { ...base.slots, ...override.slots } };
|
|
34
|
+
}
|
|
18
35
|
export function resolveAndRegisterSlot(input) {
|
|
19
36
|
const registry = readSlots(input.mainWorktree, input.registryDir);
|
|
20
37
|
const port = pickSlotPort(input, registry);
|
package/dist/workspace.d.ts
CHANGED
|
@@ -32,12 +32,6 @@ export interface WorkspaceConfig {
|
|
|
32
32
|
* Holds the setup log and dev-server logs.
|
|
33
33
|
*/
|
|
34
34
|
runtimeDir: string;
|
|
35
|
-
/**
|
|
36
|
-
* Shared registry directory, relative to a worktree root (e.g. `.local/_workspace-registry`).
|
|
37
|
-
* Holds `slots.json` and `dev-servers.json`. Must resolve to the same physical directory
|
|
38
|
-
* across linked worktrees — typically via a symlink listed in `sharedDirs` (e.g. `.local`).
|
|
39
|
-
*/
|
|
40
|
-
registryDir: string;
|
|
41
35
|
/** Config files copied from the main worktree and patched per slot. */
|
|
42
36
|
configFiles: ConfigFileEntry[];
|
|
43
37
|
/**
|
package/dist/workspace.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
2
|
+
import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { parseWorkspaceArgs, printWorkspaceHelp } from "./cli.js";
|
|
5
|
-
import { findOwnEntry, liveWorktrees, readDevServers, removeDevServerEntryByWorktree, } from "./dev-servers-registry.js";
|
|
5
|
+
import { findOwnEntry, liveWorktrees, mergeDevServers, readDevServers, removeDevServerEntryByWorktree, writeDevServers, } 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";
|
|
9
9
|
import { defaultComputePorts, isValidPort, resolvePortScheme } from "./ports.js";
|
|
10
|
-
import { handleSetOwner, markSlotFailed, markSlotReady, readSlots, resolveAndRegisterSlot, resolveCurrentSlot, validateSlotAvailability, writeSlots, } from "./slots.js";
|
|
10
|
+
import { handleSetOwner, markSlotFailed, markSlotReady, mergeSlots, readSlots, REGISTRY_SUBDIR, registryDirFor, resolveAndRegisterSlot, resolveCurrentSlot, validateSlotAvailability, warnLegacyRegistryDir, writeSlots, } from "./slots.js";
|
|
11
11
|
import { createBranch, detectWorktree, enforceWorktreeMode, getWorktreeBranch, removeWorktree, useExistingBranch, verifyBranchAbsentFromRemote, } from "./worktree.js";
|
|
12
12
|
export async function runWorkspace(config) {
|
|
13
13
|
let command;
|
|
@@ -27,6 +27,12 @@ export async function runWorkspace(config) {
|
|
|
27
27
|
printWorkspaceHelp();
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
+
warnLegacyRegistryDir(config);
|
|
31
|
+
const registryDir = registryDirFor(config.runtimeDir);
|
|
32
|
+
if (command.kind === "migrate") {
|
|
33
|
+
handleMigrate(command, config, registryDir);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
30
36
|
if (!existsSync(config.scriptPath)) {
|
|
31
37
|
console.error(`Error: scriptPath does not exist: ${config.scriptPath}. ` +
|
|
32
38
|
"Pass `fileURLToPath(import.meta.url)` from your wrapper script.");
|
|
@@ -34,16 +40,16 @@ export async function runWorkspace(config) {
|
|
|
34
40
|
}
|
|
35
41
|
switch (command.kind) {
|
|
36
42
|
case "finalize":
|
|
37
|
-
await runFinalize(command, config);
|
|
43
|
+
await runFinalize(command, config, registryDir);
|
|
38
44
|
return;
|
|
39
45
|
case "wait":
|
|
40
|
-
await runWait(command, config);
|
|
46
|
+
await runWait(command, config, registryDir);
|
|
41
47
|
return;
|
|
42
48
|
case "status":
|
|
43
|
-
runStatus(command, config);
|
|
49
|
+
runStatus(command, config, registryDir);
|
|
44
50
|
return;
|
|
45
51
|
case "list":
|
|
46
|
-
runList(
|
|
52
|
+
runList(registryDir);
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
49
55
|
const ctx = detectWorktree();
|
|
@@ -51,36 +57,36 @@ export async function runWorkspace(config) {
|
|
|
51
57
|
const run = { verbose };
|
|
52
58
|
switch (command.kind) {
|
|
53
59
|
case "remove":
|
|
54
|
-
await handleRemove(command, ctx, run, config);
|
|
60
|
+
await handleRemove(command, ctx, run, config, registryDir);
|
|
55
61
|
return;
|
|
56
62
|
case "set-owner":
|
|
57
|
-
handleSetOwnerMode(command, ctx,
|
|
63
|
+
handleSetOwnerMode(command, ctx, registryDir);
|
|
58
64
|
return;
|
|
59
65
|
case "setup": {
|
|
60
|
-
const { slot } = await runSetup(command, ctx, run, config);
|
|
66
|
+
const { slot } = await runSetup(command, ctx, run, config, registryDir);
|
|
61
67
|
if (command.wait)
|
|
62
|
-
await waitForSlot(slot, config, { printSummary: false });
|
|
68
|
+
await waitForSlot(slot, config, registryDir, { printSummary: false });
|
|
63
69
|
return;
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
|
-
async function runSetup(command, ctx, run, config) {
|
|
73
|
+
async function runSetup(command, ctx, run, config, registryDir) {
|
|
68
74
|
const scheme = resolvePortScheme(config);
|
|
69
75
|
const portsFn = resolvePortsFn(config);
|
|
70
76
|
validateSlotAvailability(command.slot, {
|
|
71
77
|
currentWorktree: ctx.currentWorktree,
|
|
72
78
|
mainWorktree: ctx.mainWorktree,
|
|
73
|
-
registryDir
|
|
79
|
+
registryDir,
|
|
74
80
|
scheme,
|
|
75
81
|
});
|
|
76
82
|
const setupCtx = ensureWorktree(command, ctx, run, config.worktreeDirName);
|
|
77
|
-
refuseIfFinalizePending(setupCtx,
|
|
83
|
+
refuseIfFinalizePending(setupCtx, registryDir, command.force);
|
|
78
84
|
const branch = getWorktreeBranch(setupCtx.currentWorktree) ?? "(detached)";
|
|
79
85
|
const { port: slot, owner, status, } = resolveAndRegisterSlot({
|
|
80
86
|
slot: command.slot,
|
|
81
87
|
currentWorktree: setupCtx.currentWorktree,
|
|
82
88
|
mainWorktree: setupCtx.mainWorktree,
|
|
83
|
-
registryDir
|
|
89
|
+
registryDir,
|
|
84
90
|
scheme,
|
|
85
91
|
requestedOwner: command.owner,
|
|
86
92
|
isMainWorktree: setupCtx.isMainWorktree,
|
|
@@ -117,6 +123,7 @@ async function runSetup(command, ctx, run, config) {
|
|
|
117
123
|
});
|
|
118
124
|
}
|
|
119
125
|
linkSharedDirectories(setupCtx, config.sharedDirs, verboseLog);
|
|
126
|
+
linkSharedRegistry(setupCtx, config.runtimeDir, verboseLog);
|
|
120
127
|
generateConfigFiles(setupCtx, config.configFiles, slot, ports, command.force, verboseLog);
|
|
121
128
|
teeLog(config.printSummary({
|
|
122
129
|
slot,
|
|
@@ -159,14 +166,14 @@ function refuseIfFinalizePending(ctx, registryDir, force) {
|
|
|
159
166
|
"then retry. Use --force to bypass.");
|
|
160
167
|
process.exit(1);
|
|
161
168
|
}
|
|
162
|
-
async function runFinalize(command, config) {
|
|
169
|
+
async function runFinalize(command, config, registryDir) {
|
|
163
170
|
const slot = Number(command.slot);
|
|
164
171
|
const ctx = detectWorktree();
|
|
165
172
|
const logPath = setupLogPath(ctx.currentWorktree, config.runtimeDir);
|
|
166
173
|
const appendLog = (message) => {
|
|
167
174
|
appendFileSync(logPath, `${message}\n`);
|
|
168
175
|
};
|
|
169
|
-
const registry = readSlots(ctx.mainWorktree,
|
|
176
|
+
const registry = readSlots(ctx.mainWorktree, registryDir);
|
|
170
177
|
const entry = registry.slots[String(slot)];
|
|
171
178
|
if (!entry || resolve(entry.worktree) !== resolve(ctx.currentWorktree)) {
|
|
172
179
|
appendLog(`FAILED: No matching slot ${slot} for worktree ${ctx.currentWorktree}.`);
|
|
@@ -193,7 +200,7 @@ async function runFinalize(command, config) {
|
|
|
193
200
|
};
|
|
194
201
|
try {
|
|
195
202
|
await config.finalizeWorktree(setupContext);
|
|
196
|
-
markSlotReady(ctx.mainWorktree,
|
|
203
|
+
markSlotReady(ctx.mainWorktree, registryDir, slot);
|
|
197
204
|
appendLog("============================================================");
|
|
198
205
|
appendLog(`READY: branch ${branch} (slot ${slot})`);
|
|
199
206
|
appendLog("============================================================");
|
|
@@ -201,14 +208,14 @@ async function runFinalize(command, config) {
|
|
|
201
208
|
catch (err) {
|
|
202
209
|
const message = err.message;
|
|
203
210
|
const stack = err.stack ?? "";
|
|
204
|
-
markSlotFailed(ctx.mainWorktree,
|
|
211
|
+
markSlotFailed(ctx.mainWorktree, registryDir, slot, message);
|
|
205
212
|
appendLog(`FAILED: ${message}`);
|
|
206
213
|
if (stack)
|
|
207
214
|
appendLog(stack);
|
|
208
215
|
process.exit(1);
|
|
209
216
|
}
|
|
210
217
|
}
|
|
211
|
-
function resolveTargetSlot(slotArg, config) {
|
|
218
|
+
function resolveTargetSlot(slotArg, config, registryDir) {
|
|
212
219
|
if (slotArg !== undefined) {
|
|
213
220
|
const slot = Number(slotArg);
|
|
214
221
|
const scheme = resolvePortScheme(config);
|
|
@@ -218,11 +225,11 @@ function resolveTargetSlot(slotArg, config) {
|
|
|
218
225
|
}
|
|
219
226
|
return slot;
|
|
220
227
|
}
|
|
221
|
-
return resolveCurrentSlot(config.basePort,
|
|
228
|
+
return resolveCurrentSlot(config.basePort, registryDir).slot;
|
|
222
229
|
}
|
|
223
|
-
function printWorktreeInfo(config, slot, worktreeForLog, fallback) {
|
|
230
|
+
function printWorktreeInfo(config, registryDir, slot, worktreeForLog, fallback) {
|
|
224
231
|
const ctx = detectWorktree();
|
|
225
|
-
const registry = readSlots(ctx.mainWorktree,
|
|
232
|
+
const registry = readSlots(ctx.mainWorktree, registryDir);
|
|
226
233
|
const entry = registry.slots[String(slot)];
|
|
227
234
|
const ports = resolvePortsFn(config)(slot);
|
|
228
235
|
const owner = entry?.owner ?? fallback.owner;
|
|
@@ -252,10 +259,10 @@ function printWorktreeInfo(config, slot, worktreeForLog, fallback) {
|
|
|
252
259
|
const elapsed = formatDuration(now - Date.parse(entry.createdAt));
|
|
253
260
|
console.log(`Pending since ${elapsed} ago (tail ${setupLogPath})`);
|
|
254
261
|
}
|
|
255
|
-
printDevServerBlock(config, ctx.mainWorktree, targetWorktree, now);
|
|
262
|
+
printDevServerBlock(config, registryDir, ctx.mainWorktree, targetWorktree, now);
|
|
256
263
|
}
|
|
257
|
-
function printDevServerBlock(config, mainWorktree, targetWorktree, now) {
|
|
258
|
-
const entry = findOwnEntry(mainWorktree,
|
|
264
|
+
function printDevServerBlock(config, registryDir, mainWorktree, targetWorktree, now) {
|
|
265
|
+
const entry = findOwnEntry(mainWorktree, registryDir, targetWorktree);
|
|
259
266
|
const liveEntries = entry
|
|
260
267
|
? Object.entries(entry.pids)
|
|
261
268
|
.filter(([, pid]) => isProcessAlive(pid))
|
|
@@ -272,31 +279,31 @@ function printDevServerBlock(config, mainWorktree, targetWorktree, now) {
|
|
|
272
279
|
console.log(` log: ${join(targetWorktree, config.runtimeDir, "logs", `${name}.log`)}`);
|
|
273
280
|
}
|
|
274
281
|
}
|
|
275
|
-
function runStatus(command, config) {
|
|
282
|
+
function runStatus(command, config, registryDir) {
|
|
276
283
|
if (command.slot !== undefined) {
|
|
277
|
-
const slot = resolveTargetSlot(command.slot, config);
|
|
284
|
+
const slot = resolveTargetSlot(command.slot, config, registryDir);
|
|
278
285
|
const ctx = detectWorktree();
|
|
279
|
-
const entry = readSlots(ctx.mainWorktree,
|
|
286
|
+
const entry = readSlots(ctx.mainWorktree, registryDir).slots[String(slot)];
|
|
280
287
|
if (!entry) {
|
|
281
288
|
console.error(`Error: No slot ${slot} in registry.`);
|
|
282
289
|
process.exit(1);
|
|
283
290
|
}
|
|
284
|
-
printWorktreeInfo(config, slot, entry.worktree, { owner: entry.owner });
|
|
291
|
+
printWorktreeInfo(config, registryDir, slot, entry.worktree, { owner: entry.owner });
|
|
285
292
|
return;
|
|
286
293
|
}
|
|
287
|
-
const resolved = resolveCurrentSlot(config.basePort,
|
|
288
|
-
printWorktreeInfo(config, resolved.slot, resolved.worktree, {
|
|
294
|
+
const resolved = resolveCurrentSlot(config.basePort, registryDir);
|
|
295
|
+
printWorktreeInfo(config, registryDir, resolved.slot, resolved.worktree, {
|
|
289
296
|
owner: resolved.owner,
|
|
290
297
|
});
|
|
291
298
|
}
|
|
292
|
-
function runList(
|
|
299
|
+
function runList(registryDir) {
|
|
293
300
|
const ctx = detectWorktree();
|
|
294
|
-
const entries = Object.entries(readSlots(ctx.mainWorktree,
|
|
301
|
+
const entries = Object.entries(readSlots(ctx.mainWorktree, registryDir).slots).sort(([a], [b]) => Number(a) - Number(b));
|
|
295
302
|
if (entries.length === 0) {
|
|
296
303
|
console.log("No workspaces registered.");
|
|
297
304
|
return;
|
|
298
305
|
}
|
|
299
|
-
const liveSet = liveWorktrees(readDevServers(ctx.mainWorktree,
|
|
306
|
+
const liveSet = liveWorktrees(readDevServers(ctx.mainWorktree, registryDir));
|
|
300
307
|
const rows = entries.map(([port, e]) => ({
|
|
301
308
|
slot: port,
|
|
302
309
|
type: e.main ? "main" : "linked",
|
|
@@ -331,15 +338,15 @@ function runList(config) {
|
|
|
331
338
|
for (const r of rows)
|
|
332
339
|
console.log(fmt(r));
|
|
333
340
|
}
|
|
334
|
-
async function runWait(command, config) {
|
|
341
|
+
async function runWait(command, config, registryDir) {
|
|
335
342
|
// standalone `workspace wait` (no prior setup in this invocation) → print the full summary on success.
|
|
336
|
-
const slot = resolveTargetSlot(command.slot, config);
|
|
337
|
-
await waitForSlot(slot, config);
|
|
343
|
+
const slot = resolveTargetSlot(command.slot, config, registryDir);
|
|
344
|
+
await waitForSlot(slot, config, registryDir);
|
|
338
345
|
}
|
|
339
|
-
async function waitForSlot(slot, config, options = {}) {
|
|
346
|
+
async function waitForSlot(slot, config, registryDir, options = {}) {
|
|
340
347
|
const printSummary = options.printSummary ?? true;
|
|
341
348
|
const ctx = detectWorktree();
|
|
342
|
-
const initial = readSlots(ctx.mainWorktree,
|
|
349
|
+
const initial = readSlots(ctx.mainWorktree, registryDir).slots[String(slot)];
|
|
343
350
|
if (!initial) {
|
|
344
351
|
console.error(`Error: No slot ${slot} in registry.`);
|
|
345
352
|
process.exit(1);
|
|
@@ -348,7 +355,7 @@ async function waitForSlot(slot, config, options = {}) {
|
|
|
348
355
|
// Poll slots.json — the finalize child writes `status` on success or failure. Tiny file, no
|
|
349
356
|
// log-tailing race.
|
|
350
357
|
for (;;) {
|
|
351
|
-
const entry = readSlots(ctx.mainWorktree,
|
|
358
|
+
const entry = readSlots(ctx.mainWorktree, registryDir).slots[String(slot)];
|
|
352
359
|
if (!entry) {
|
|
353
360
|
console.error(`Error: Slot ${slot} disappeared from registry.`);
|
|
354
361
|
process.exit(1);
|
|
@@ -356,7 +363,7 @@ async function waitForSlot(slot, config, options = {}) {
|
|
|
356
363
|
if (entry.status === "ready") {
|
|
357
364
|
console.log("\n… ready");
|
|
358
365
|
if (printSummary) {
|
|
359
|
-
printWorktreeInfo(config, slot, entry.worktree, {
|
|
366
|
+
printWorktreeInfo(config, registryDir, slot, entry.worktree, {
|
|
360
367
|
owner: entry.owner,
|
|
361
368
|
});
|
|
362
369
|
}
|
|
@@ -371,10 +378,10 @@ async function waitForSlot(slot, config, options = {}) {
|
|
|
371
378
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
372
379
|
}
|
|
373
380
|
}
|
|
374
|
-
async function handleRemove(command, ctx, run, config) {
|
|
381
|
+
async function handleRemove(command, ctx, run, config, registryDir) {
|
|
375
382
|
const verboseLog = makeVerboseLog(run.verbose);
|
|
376
383
|
const removeHere = command.branch === undefined;
|
|
377
|
-
const registry = readSlots(ctx.mainWorktree,
|
|
384
|
+
const registry = readSlots(ctx.mainWorktree, registryDir);
|
|
378
385
|
const target = resolveRemoveTarget(command, ctx, registry);
|
|
379
386
|
// Refuse to remove while the detached finalize is still writing to slots.json / workspace-setup.log:
|
|
380
387
|
// racing the two corrupts the registry and leaves the worktree directory orphaned.
|
|
@@ -390,11 +397,11 @@ async function handleRemove(command, ctx, run, config) {
|
|
|
390
397
|
if (!existsSync(target.worktreePath)) {
|
|
391
398
|
console.warn(`Warning: Worktree directory ${target.worktreePath} not found. Cleaning up registry only.`);
|
|
392
399
|
delete registry.slots[target.slotPort];
|
|
393
|
-
writeSlots(ctx.mainWorktree,
|
|
400
|
+
writeSlots(ctx.mainWorktree, registryDir, registry);
|
|
394
401
|
console.log(`Removed registry entry for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}).`);
|
|
395
402
|
return;
|
|
396
403
|
}
|
|
397
|
-
const targetEntry = findOwnEntry(ctx.mainWorktree,
|
|
404
|
+
const targetEntry = findOwnEntry(ctx.mainWorktree, registryDir, target.worktreePath);
|
|
398
405
|
if (targetEntry) {
|
|
399
406
|
stopTargetDevServer(config.devServerScript, target.worktreePath, verboseLog);
|
|
400
407
|
}
|
|
@@ -409,8 +416,8 @@ async function handleRemove(command, ctx, run, config) {
|
|
|
409
416
|
});
|
|
410
417
|
}
|
|
411
418
|
delete registry.slots[target.slotPort];
|
|
412
|
-
writeSlots(ctx.mainWorktree,
|
|
413
|
-
removeDevServerEntryByWorktree(ctx.mainWorktree,
|
|
419
|
+
writeSlots(ctx.mainWorktree, registryDir, registry);
|
|
420
|
+
removeDevServerEntryByWorktree(ctx.mainWorktree, registryDir, target.worktreePath);
|
|
414
421
|
if (removeHere) {
|
|
415
422
|
process.chdir(ctx.mainWorktree);
|
|
416
423
|
}
|
|
@@ -421,17 +428,17 @@ async function handleRemove(command, ctx, run, config) {
|
|
|
421
428
|
console.log(`Now run: cd ${ctx.mainWorktree}`);
|
|
422
429
|
}
|
|
423
430
|
}
|
|
424
|
-
function handleSetOwnerMode(command, ctx,
|
|
431
|
+
function handleSetOwnerMode(command, ctx, registryDir) {
|
|
425
432
|
const newOwner = command.name;
|
|
426
433
|
const { slotPort } = handleSetOwner({
|
|
427
434
|
newOwner,
|
|
428
435
|
currentWorktree: ctx.currentWorktree,
|
|
429
436
|
mainWorktree: ctx.mainWorktree,
|
|
430
|
-
registryDir
|
|
437
|
+
registryDir,
|
|
431
438
|
isMainWorktree: ctx.isMainWorktree,
|
|
432
439
|
});
|
|
433
440
|
// Propagate to dev-servers.json entries for this worktree.
|
|
434
|
-
const devServersPath = join(ctx.mainWorktree,
|
|
441
|
+
const devServersPath = join(ctx.mainWorktree, registryDir, "dev-servers.json");
|
|
435
442
|
if (existsSync(devServersPath)) {
|
|
436
443
|
const data = JSON.parse(readFileSync(devServersPath, "utf-8"));
|
|
437
444
|
let changed = false;
|
|
@@ -452,6 +459,56 @@ function handleSetOwnerMode(command, ctx, config) {
|
|
|
452
459
|
}
|
|
453
460
|
console.log(`Owner for slot ${slotPort}: ${newOwner ?? "(none)"}`);
|
|
454
461
|
}
|
|
462
|
+
/** Transitional (0.16 only): merge a pre-0.16 registry into `${runtimeDir}/shared-registry`. */
|
|
463
|
+
function handleMigrate(command, config, newRel) {
|
|
464
|
+
const ctx = detectWorktree();
|
|
465
|
+
const oldRel = command.oldRegistryDir;
|
|
466
|
+
const oldAbs = join(ctx.mainWorktree, oldRel);
|
|
467
|
+
if (resolve(oldAbs) === resolve(join(ctx.mainWorktree, newRel))) {
|
|
468
|
+
console.log(`Registry already at ${newRel}; relinking worktrees.`);
|
|
469
|
+
relinkWorktrees(readSlots(ctx.mainWorktree, newRel), ctx.mainWorktree, config.runtimeDir);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
refuseUnlessOldRegistry(oldAbs, ctx.mainWorktree);
|
|
473
|
+
const mergedSlots = mergeSlots(readSlots(ctx.mainWorktree, oldRel), readSlots(ctx.mainWorktree, newRel));
|
|
474
|
+
writeSlots(ctx.mainWorktree, newRel, mergedSlots);
|
|
475
|
+
const mergedDevServers = mergeDevServers(readDevServers(ctx.mainWorktree, oldRel), readDevServers(ctx.mainWorktree, newRel));
|
|
476
|
+
writeDevServers(ctx.mainWorktree, newRel, mergedDevServers);
|
|
477
|
+
rmSync(oldAbs, { recursive: true, force: true });
|
|
478
|
+
const relinked = relinkWorktrees(mergedSlots, ctx.mainWorktree, config.runtimeDir);
|
|
479
|
+
console.log(`Migrated ${oldRel} → ${newRel}: ${Object.keys(mergedSlots.slots).length} slot(s), ` +
|
|
480
|
+
`${mergedDevServers.servers.length} dev-server(s); ${relinked} symlink(s) recreated.`);
|
|
481
|
+
}
|
|
482
|
+
/** `oldAbs` is recursively deleted after the merge — refuse anything that isn't clearly a registry. */
|
|
483
|
+
function refuseUnlessOldRegistry(oldAbs, mainWorktree) {
|
|
484
|
+
const fromMain = relative(mainWorktree, resolve(oldAbs));
|
|
485
|
+
if (fromMain.startsWith("..") || isAbsolute(fromMain)) {
|
|
486
|
+
console.error(`Error: the old registry must be inside the main worktree; got ${oldAbs}.`);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
if (!existsSync(oldAbs)) {
|
|
490
|
+
console.error(`Error: nothing to migrate at ${oldAbs}.`);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
if (!existsSync(join(oldAbs, "slots.json")) && !existsSync(join(oldAbs, "dev-servers.json"))) {
|
|
494
|
+
console.error(`Error: ${oldAbs} does not look like a registry (no slots.json or dev-servers.json).`);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function relinkWorktrees(slots, mainWorktree, runtimeDir) {
|
|
499
|
+
let count = 0;
|
|
500
|
+
for (const entry of Object.values(slots.slots)) {
|
|
501
|
+
if (entry.main)
|
|
502
|
+
continue;
|
|
503
|
+
if (!existsSync(entry.worktree)) {
|
|
504
|
+
console.warn(`Warning: worktree ${entry.worktree} not found; skipping symlink.`);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
linkSharedRegistry({ currentWorktree: entry.worktree, mainWorktree, isMainWorktree: false }, runtimeDir, console.log, { force: true });
|
|
508
|
+
++count;
|
|
509
|
+
}
|
|
510
|
+
return count;
|
|
511
|
+
}
|
|
455
512
|
function ensureWorktree(command, ctx, run, dirNameFn) {
|
|
456
513
|
if (command.branch === undefined)
|
|
457
514
|
return ctx;
|
|
@@ -476,6 +533,36 @@ function linkSharedDirectories(ctx, dirs, log) {
|
|
|
476
533
|
}
|
|
477
534
|
}
|
|
478
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Symlinks the linked worktree's `${runtimeDir}/shared-registry` to the main worktree's, so the
|
|
538
|
+
* cwd-relative registry read in `resolveCurrentSlot` reaches main. `runtimeDir` is per-worktree and
|
|
539
|
+
* not in `sharedDirs`, so this is distinct from {@link linkSharedDirectories}.
|
|
540
|
+
*/
|
|
541
|
+
function linkSharedRegistry(ctx, runtimeDir, log, opts) {
|
|
542
|
+
if (ctx.isMainWorktree)
|
|
543
|
+
return;
|
|
544
|
+
const mainDir = join(ctx.mainWorktree, runtimeDir, REGISTRY_SUBDIR);
|
|
545
|
+
mkdirSync(mainDir, { recursive: true });
|
|
546
|
+
const runtimeRoot = join(ctx.currentWorktree, runtimeDir);
|
|
547
|
+
mkdirSync(runtimeRoot, { recursive: true });
|
|
548
|
+
const link = join(runtimeRoot, REGISTRY_SUBDIR);
|
|
549
|
+
// lstat, not existsSync: existsSync follows symlinks, so a broken one would read as absent and
|
|
550
|
+
// make symlinkSync throw EEXIST. A broken symlink is recreated even without `force`.
|
|
551
|
+
const linkStat = lstatSync(link, { throwIfNoEntry: false });
|
|
552
|
+
if (linkStat) {
|
|
553
|
+
if (!linkStat.isSymbolicLink()) {
|
|
554
|
+
log("Skipped shared-registry symlink (a real directory exists here).");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (!opts?.force && existsSync(link)) {
|
|
558
|
+
log("Skipped shared-registry symlink (already exists).");
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
rmSync(link);
|
|
562
|
+
}
|
|
563
|
+
symlinkSync(relative(runtimeRoot, mainDir), link);
|
|
564
|
+
log("Created shared-registry symlink → main worktree.");
|
|
565
|
+
}
|
|
479
566
|
function generateConfigFiles(ctx, entries, slot, ports, force, log) {
|
|
480
567
|
for (const entry of entries) {
|
|
481
568
|
const patchCtx = {
|