@paleo/worktree-env 0.4.1 → 0.5.1
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 -6
- package/dist/cli.d.ts +10 -2
- package/dist/cli.js +63 -29
- package/dist/dev-server.d.ts +9 -5
- package/dist/dev-server.js +113 -81
- package/dist/dev-servers-registry.d.ts +15 -11
- package/dist/dev-servers-registry.js +92 -74
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +0 -4
- package/dist/helpers.js +8 -8
- package/dist/log-polling.d.ts +0 -1
- package/dist/log-polling.js +13 -13
- package/dist/ports.d.ts +5 -5
- package/dist/process-control.d.ts +2 -2
- package/dist/process-control.js +22 -22
- package/dist/setup-worktree.d.ts +60 -48
- package/dist/setup-worktree.js +280 -103
- package/dist/slots.d.ts +20 -19
- package/dist/slots.js +93 -67
- package/dist/worktree.d.ts +5 -6
- package/dist/worktree.js +32 -31
- package/package.json +1 -1
package/dist/setup-worktree.js
CHANGED
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
2
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
-
import { isRemoveMode, isSetOwnerMode, isSetupMode, parseSetupArgs, printSetupHelp, validateSetupFlags, } from "./cli.js";
|
|
4
|
+
import { isFinalizeMode, isInfoMode, isRemoveMode, isSetOwnerMode, isSetupMode, isWaitMode, parseSetupArgs, printSetupHelp, validateSetupFlags, } from "./cli.js";
|
|
4
5
|
import { removeDevServerEntryByWorktree } from "./dev-servers-registry.js";
|
|
5
6
|
import { ConfigError } from "./errors.js";
|
|
6
7
|
import { copyAndPatchFile } from "./helpers.js";
|
|
7
|
-
import { defaultComputePorts, resolvePortScheme } from "./ports.js";
|
|
8
|
-
import { handleSetOwner, readSlots, resolveAndRegisterSlot, validateSlotAvailability, writeSlots, } from "./slots.js";
|
|
9
|
-
import { createBranch, detectWorktree, enforceWorktreeMode, getCurrentBranch, removeWorktree, useExistingBranch, verifyBranchAbsentFromRemote, } from "./worktree.js";
|
|
8
|
+
import { defaultComputePorts, isValidPort, resolvePortScheme } from "./ports.js";
|
|
10
9
|
import { cleanupPidFile, isProcessAlive, isProcessGroupAlive, killProcessGroup, readPid, } from "./process-control.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (verbose)
|
|
14
|
-
console.log(msg);
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
function resolvePortsFn(config) {
|
|
18
|
-
if (config.ports)
|
|
19
|
-
return config.ports;
|
|
20
|
-
if (config.portNames && config.portNames.length > 0) {
|
|
21
|
-
return defaultComputePorts(config.portNames);
|
|
22
|
-
}
|
|
23
|
-
throw new ConfigError("Config error: provide either `ports` (function) or `portNames` (array).");
|
|
24
|
-
}
|
|
10
|
+
import { handleSetOwner, markSlotFailed, markSlotReady, readSlots, resolveAndRegisterSlot, resolveCurrentSlot, validateSlotAvailability, writeSlots, } from "./slots.js";
|
|
11
|
+
import { createBranch, detectWorktree, enforceWorktreeMode, getCurrentBranch, removeWorktree, useExistingBranch, verifyBranchAbsentFromRemote, } from "./worktree.js";
|
|
25
12
|
export async function runSetupWorktree(config) {
|
|
26
13
|
let args;
|
|
27
14
|
try {
|
|
@@ -36,6 +23,11 @@ export async function runSetupWorktree(config) {
|
|
|
36
23
|
printSetupHelp();
|
|
37
24
|
return;
|
|
38
25
|
}
|
|
26
|
+
if (!existsSync(config.scriptPath)) {
|
|
27
|
+
console.error(`Error: scriptPath does not exist: ${config.scriptPath}. ` +
|
|
28
|
+
"Pass `fileURLToPath(import.meta.url)` from your wrapper script.");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
39
31
|
try {
|
|
40
32
|
validateSetupFlags(args);
|
|
41
33
|
}
|
|
@@ -46,6 +38,18 @@ export async function runSetupWorktree(config) {
|
|
|
46
38
|
}
|
|
47
39
|
throw err;
|
|
48
40
|
}
|
|
41
|
+
if (isFinalizeMode(args)) {
|
|
42
|
+
await runFinalize(args, config);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (isWaitMode(args) && !isSetupMode(args)) {
|
|
46
|
+
await runWait(args, config);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (isInfoMode(args)) {
|
|
50
|
+
runInfo(args, config);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
49
53
|
if (!isSetupMode(args) && !isRemoveMode(args) && !isSetOwnerMode(args)) {
|
|
50
54
|
printSetupHelp();
|
|
51
55
|
return;
|
|
@@ -58,18 +62,21 @@ export async function runSetupWorktree(config) {
|
|
|
58
62
|
return;
|
|
59
63
|
}
|
|
60
64
|
if (isSetOwnerMode(args)) {
|
|
61
|
-
handleSetOwnerMode(args, ctx);
|
|
65
|
+
handleSetOwnerMode(args, ctx, config);
|
|
62
66
|
return;
|
|
63
67
|
}
|
|
64
|
-
await runSetup(args, ctx, run, config);
|
|
68
|
+
const { slot } = await runSetup(args, ctx, run, config);
|
|
69
|
+
if (isWaitMode(args)) {
|
|
70
|
+
await waitForSlot(slot, config);
|
|
71
|
+
}
|
|
65
72
|
}
|
|
66
73
|
async function runSetup(args, ctx, run, config) {
|
|
67
|
-
const verboseLog = makeVerboseLog(run.verbose);
|
|
68
74
|
const scheme = resolvePortScheme(config);
|
|
69
75
|
const portsFn = resolvePortsFn(config);
|
|
70
76
|
validateSlotAvailability(args.slot, {
|
|
71
77
|
currentWorktree: ctx.currentWorktree,
|
|
72
78
|
mainWorktree: ctx.mainWorktree,
|
|
79
|
+
registryDir: config.registryDir,
|
|
73
80
|
scheme,
|
|
74
81
|
});
|
|
75
82
|
const setupCtx = ensureWorktree(args, ctx, run);
|
|
@@ -78,42 +85,254 @@ async function runSetup(args, ctx, run, config) {
|
|
|
78
85
|
slot: args.slot,
|
|
79
86
|
currentWorktree: setupCtx.currentWorktree,
|
|
80
87
|
mainWorktree: setupCtx.mainWorktree,
|
|
88
|
+
registryDir: config.registryDir,
|
|
81
89
|
scheme,
|
|
82
90
|
branch,
|
|
83
91
|
requestedOwner: args.owner,
|
|
84
92
|
});
|
|
85
93
|
const ports = portsFn(slot);
|
|
94
|
+
const runtimeDir = join(setupCtx.currentWorktree, config.runtimeDir);
|
|
95
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
96
|
+
const logPath = join(runtimeDir, "wt-setup.log");
|
|
97
|
+
// Truncate any prior log so `--here` retries start with a clean record (the previous run's
|
|
98
|
+
// FAILED: banner would otherwise linger and produce false positives for grep-based tooling).
|
|
99
|
+
writeFileSync(logPath, "");
|
|
100
|
+
// Opened "a" so the same fd can be inherited by the detached finalize child below.
|
|
101
|
+
const logFd = openSync(logPath, "a");
|
|
102
|
+
const teeLog = (message) => {
|
|
103
|
+
console.log(message);
|
|
104
|
+
appendFileSync(logFd, `${message}\n`);
|
|
105
|
+
};
|
|
106
|
+
const verboseLog = (msg) => {
|
|
107
|
+
if (run.verbose)
|
|
108
|
+
teeLog(msg);
|
|
109
|
+
else
|
|
110
|
+
appendFileSync(logFd, `${msg}\n`);
|
|
111
|
+
};
|
|
86
112
|
verboseLog(`Using slot ${slot} (${Object.entries(ports)
|
|
87
113
|
.map(([k, v]) => `${k}: ${v}`)
|
|
88
114
|
.join(", ")})`);
|
|
89
|
-
|
|
90
|
-
const sharedDirs = config.sharedDirs ?? [".local", ".plans"];
|
|
91
|
-
linkSharedDirectories(setupCtx, sharedDirs, verboseLog);
|
|
115
|
+
linkSharedDirectories(setupCtx, config.sharedDirs, verboseLog);
|
|
92
116
|
generateConfigFiles(setupCtx, config.configFiles, slot, ports, args.force ?? false, verboseLog);
|
|
93
|
-
|
|
94
|
-
const setupContext = {
|
|
95
|
-
currentWorktree: setupCtx.currentWorktree,
|
|
96
|
-
mainWorktree: setupCtx.mainWorktree,
|
|
117
|
+
teeLog(config.printSummary({
|
|
97
118
|
slot,
|
|
98
119
|
branch,
|
|
99
120
|
owner,
|
|
100
121
|
ports,
|
|
101
|
-
|
|
102
|
-
|
|
122
|
+
currentWorktree: setupCtx.currentWorktree,
|
|
123
|
+
mainWorktree: setupCtx.mainWorktree,
|
|
124
|
+
}));
|
|
125
|
+
teeLog(`WORKTREE_CREATED path=${setupCtx.currentWorktree} branch=${branch} slot=${slot}`);
|
|
126
|
+
teeLog(`Setup continuing in background. Tail: ${logPath}`);
|
|
127
|
+
const child = spawn(process.execPath, [config.scriptPath, "--__finalize", String(slot)], {
|
|
128
|
+
detached: true,
|
|
129
|
+
stdio: ["ignore", logFd, logFd],
|
|
130
|
+
cwd: setupCtx.currentWorktree,
|
|
131
|
+
});
|
|
132
|
+
child.unref();
|
|
133
|
+
closeSync(logFd);
|
|
134
|
+
return { slot };
|
|
135
|
+
}
|
|
136
|
+
async function runFinalize(args, config) {
|
|
137
|
+
const slot = Number(args.__finalize);
|
|
138
|
+
const ctx = detectWorktree();
|
|
139
|
+
const logPath = join(ctx.currentWorktree, config.runtimeDir, "wt-setup.log");
|
|
140
|
+
const appendLog = (message) => {
|
|
141
|
+
appendFileSync(logPath, `${message}\n`);
|
|
142
|
+
};
|
|
143
|
+
const registry = readSlots(ctx.mainWorktree, config.registryDir);
|
|
144
|
+
const entry = registry.slots[String(slot)];
|
|
145
|
+
if (!entry || resolve(entry.worktree) !== resolve(ctx.currentWorktree)) {
|
|
146
|
+
appendLog(`FAILED: No matching slot ${slot} for worktree ${ctx.currentWorktree}.`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
if (entry.status === "ready" && !args.force) {
|
|
150
|
+
appendLog(`READY: branch ${entry.branch} (slot ${slot}) already finalized; skipping.`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const portsFn = resolvePortsFn(config);
|
|
154
|
+
const ports = portsFn(slot);
|
|
155
|
+
appendLog(`--- finalizing slot ${slot} at ${new Date().toISOString()} ---`);
|
|
156
|
+
const setupContext = {
|
|
157
|
+
currentWorktree: ctx.currentWorktree,
|
|
158
|
+
mainWorktree: ctx.mainWorktree,
|
|
159
|
+
slot,
|
|
160
|
+
branch: entry.branch,
|
|
161
|
+
owner: entry.owner,
|
|
162
|
+
ports,
|
|
163
|
+
force: args.force ?? false,
|
|
164
|
+
verbose: false,
|
|
103
165
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
166
|
+
try {
|
|
167
|
+
await config.finalizeWorktree(setupContext);
|
|
168
|
+
markSlotReady(ctx.mainWorktree, config.registryDir, slot);
|
|
169
|
+
appendLog("============================================================");
|
|
170
|
+
appendLog(`READY: branch ${entry.branch} (slot ${slot})`);
|
|
171
|
+
appendLog("============================================================");
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
const message = err.message;
|
|
175
|
+
const stack = err.stack ?? "";
|
|
176
|
+
markSlotFailed(ctx.mainWorktree, config.registryDir, slot, message);
|
|
177
|
+
appendLog(`FAILED: ${message}`);
|
|
178
|
+
if (stack)
|
|
179
|
+
appendLog(stack);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function resolveTargetSlot(args, config) {
|
|
184
|
+
if (args.slot !== undefined) {
|
|
185
|
+
const slot = Number(args.slot);
|
|
186
|
+
const scheme = resolvePortScheme(config);
|
|
187
|
+
if (!isValidPort(slot, scheme)) {
|
|
188
|
+
console.error(`Error: --slot expects a port in [${scheme.minPort}, ${scheme.maxPort}] stepped by ${scheme.portStep}; got "${args.slot}".`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
return slot;
|
|
192
|
+
}
|
|
193
|
+
return resolveCurrentSlot(config.basePort, config.registryDir).slot;
|
|
194
|
+
}
|
|
195
|
+
function printWorktreeInfo(config, slot, worktreeForLog, fallback) {
|
|
196
|
+
const ctx = detectWorktree();
|
|
197
|
+
const registry = readSlots(ctx.mainWorktree, config.registryDir);
|
|
198
|
+
const entry = registry.slots[String(slot)];
|
|
199
|
+
const ports = resolvePortsFn(config)(slot);
|
|
200
|
+
const branch = entry?.branch ?? fallback.branch;
|
|
201
|
+
const owner = entry?.owner ?? fallback.owner;
|
|
202
|
+
// Main worktree has no slot entry by design — treat it as ready when the registry has no row.
|
|
203
|
+
const slotStatus = entry?.status ?? (ctx.isMainWorktree ? "ready" : "pending");
|
|
204
|
+
const logHint = ` (tail ${join(worktreeForLog, config.runtimeDir, "wt-setup.log")})`;
|
|
205
|
+
const display = slotStatus === "ready"
|
|
206
|
+
? "ready"
|
|
207
|
+
: slotStatus === "failed"
|
|
208
|
+
? `failed: ${entry?.failure?.message ?? "(no message)"}${logHint}`
|
|
209
|
+
: `pending${logHint}`;
|
|
210
|
+
console.log(`Status: ${display}`);
|
|
108
211
|
console.log(config.printSummary({
|
|
109
212
|
slot,
|
|
110
213
|
branch,
|
|
111
214
|
owner,
|
|
112
215
|
ports,
|
|
113
|
-
currentWorktree:
|
|
114
|
-
mainWorktree:
|
|
216
|
+
currentWorktree: entry?.worktree ?? ctx.currentWorktree,
|
|
217
|
+
mainWorktree: ctx.mainWorktree,
|
|
115
218
|
}));
|
|
116
219
|
}
|
|
220
|
+
function runInfo(args, config) {
|
|
221
|
+
if (args.slot !== undefined) {
|
|
222
|
+
const slot = resolveTargetSlot(args, config);
|
|
223
|
+
const ctx = detectWorktree();
|
|
224
|
+
const entry = readSlots(ctx.mainWorktree, config.registryDir).slots[String(slot)];
|
|
225
|
+
if (!entry) {
|
|
226
|
+
console.error(`Error: No slot ${slot} in registry.`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
printWorktreeInfo(config, slot, entry.worktree, { branch: entry.branch, owner: entry.owner });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const resolved = resolveCurrentSlot(config.basePort, config.registryDir);
|
|
233
|
+
printWorktreeInfo(config, resolved.slot, ".", { branch: resolved.branch, owner: resolved.owner });
|
|
234
|
+
}
|
|
235
|
+
async function runWait(args, config) {
|
|
236
|
+
const slot = resolveTargetSlot(args, config);
|
|
237
|
+
await waitForSlot(slot, config);
|
|
238
|
+
}
|
|
239
|
+
async function waitForSlot(slot, config) {
|
|
240
|
+
const ctx = detectWorktree();
|
|
241
|
+
const initial = readSlots(ctx.mainWorktree, config.registryDir).slots[String(slot)];
|
|
242
|
+
if (!initial) {
|
|
243
|
+
console.error(`Error: No slot ${slot} in registry.`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const pollMs = 500;
|
|
247
|
+
// Poll slots.json — the finalize child writes `status` on success or failure. Tiny file, no
|
|
248
|
+
// log-tailing race.
|
|
249
|
+
for (;;) {
|
|
250
|
+
const entry = readSlots(ctx.mainWorktree, config.registryDir).slots[String(slot)];
|
|
251
|
+
if (!entry) {
|
|
252
|
+
console.error(`Error: Slot ${slot} disappeared from registry.`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
if (entry.status === "ready") {
|
|
256
|
+
printWorktreeInfo(config, slot, entry.worktree, { branch: entry.branch, owner: entry.owner });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (entry.status === "failed") {
|
|
260
|
+
const logPath = join(entry.worktree, config.runtimeDir, "wt-setup.log");
|
|
261
|
+
console.error(`FAILED: ${entry.failure?.message ?? "(no message)"}`);
|
|
262
|
+
console.error(`Full log: ${logPath}`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function handleRemove(args, ctx, run, config) {
|
|
269
|
+
const verboseLog = makeVerboseLog(run.verbose);
|
|
270
|
+
const removeHere = Boolean(args["remove-here"]);
|
|
271
|
+
const registry = readSlots(ctx.mainWorktree, config.registryDir);
|
|
272
|
+
const target = resolveRemoveTarget(args, ctx, registry, removeHere);
|
|
273
|
+
if (!args["no-remote-check"]) {
|
|
274
|
+
verifyBranchAbsentFromRemote(target.branch, run);
|
|
275
|
+
}
|
|
276
|
+
const ownerSuffix = target.owner ? `, owner ${target.owner}` : "";
|
|
277
|
+
if (!existsSync(target.worktreePath)) {
|
|
278
|
+
console.warn(`Warning: Worktree directory ${target.worktreePath} not found. Cleaning up registry only.`);
|
|
279
|
+
delete registry.slots[target.slotPort];
|
|
280
|
+
writeSlots(ctx.mainWorktree, config.registryDir, registry);
|
|
281
|
+
console.log(`Removed registry entry for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}).`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await stopAllDevServersInRuntimeDir(target.worktreePath, config.runtimeDir, verboseLog);
|
|
285
|
+
if (config.teardownInfrastructure) {
|
|
286
|
+
await config.teardownInfrastructure({
|
|
287
|
+
worktree: target.worktreePath,
|
|
288
|
+
mainWorktree: ctx.mainWorktree,
|
|
289
|
+
verbose: run.verbose,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
delete registry.slots[target.slotPort];
|
|
293
|
+
writeSlots(ctx.mainWorktree, config.registryDir, registry);
|
|
294
|
+
removeDevServerEntryByWorktree(ctx.mainWorktree, config.registryDir, target.worktreePath);
|
|
295
|
+
if (removeHere) {
|
|
296
|
+
process.chdir(ctx.mainWorktree);
|
|
297
|
+
}
|
|
298
|
+
removeWorktree(target.worktreePath, run);
|
|
299
|
+
console.log(`Removed worktree for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}). ` +
|
|
300
|
+
`Branch "${target.branch}" kept.`);
|
|
301
|
+
if (removeHere) {
|
|
302
|
+
console.log(`Now run: cd ${ctx.mainWorktree}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function handleSetOwnerMode(args, ctx, config) {
|
|
306
|
+
const newOwner = args["set-owner"];
|
|
307
|
+
const { slotPort } = handleSetOwner({
|
|
308
|
+
newOwner,
|
|
309
|
+
currentWorktree: ctx.currentWorktree,
|
|
310
|
+
mainWorktree: ctx.mainWorktree,
|
|
311
|
+
registryDir: config.registryDir,
|
|
312
|
+
isMainWorktree: ctx.isMainWorktree,
|
|
313
|
+
});
|
|
314
|
+
// Propagate to dev-servers.json entries for this worktree.
|
|
315
|
+
const devServersPath = join(ctx.mainWorktree, config.registryDir, "dev-servers.json");
|
|
316
|
+
if (existsSync(devServersPath)) {
|
|
317
|
+
const data = JSON.parse(readFileSync(devServersPath, "utf-8"));
|
|
318
|
+
let changed = false;
|
|
319
|
+
const resolvedCurrent = resolve(ctx.currentWorktree);
|
|
320
|
+
for (const server of data.servers) {
|
|
321
|
+
if (resolve(server.worktree) === resolvedCurrent) {
|
|
322
|
+
if (newOwner !== undefined)
|
|
323
|
+
server.owner = newOwner;
|
|
324
|
+
else
|
|
325
|
+
delete server.owner;
|
|
326
|
+
changed = true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (changed) {
|
|
330
|
+
mkdirSync(dirname(devServersPath), { recursive: true });
|
|
331
|
+
writeFileSync(devServersPath, `${JSON.stringify(data, undefined, 2)}\n`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
console.log(`Owner for slot ${slotPort}: ${newOwner ?? "(none)"}`);
|
|
335
|
+
}
|
|
117
336
|
function ensureWorktree(args, ctx, run) {
|
|
118
337
|
if (args.use)
|
|
119
338
|
return useExistingBranch(args.use, ctx, run);
|
|
@@ -180,9 +399,19 @@ function resolveRemoveTarget(args, ctx, registry, removeHere) {
|
|
|
180
399
|
}
|
|
181
400
|
return { slotPort: entry[0], branch, worktreePath, owner: entry[1].owner };
|
|
182
401
|
}
|
|
183
|
-
async function
|
|
184
|
-
|
|
185
|
-
|
|
402
|
+
async function stopAllDevServersInRuntimeDir(worktreePath, runtimeDir, log) {
|
|
403
|
+
const dir = join(worktreePath, runtimeDir);
|
|
404
|
+
let entries;
|
|
405
|
+
try {
|
|
406
|
+
entries = readdirSync(dir);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
for (const name of entries) {
|
|
412
|
+
if (!name.endsWith(".pid"))
|
|
413
|
+
continue;
|
|
414
|
+
const pidFile = join(dir, name);
|
|
186
415
|
const pid = readPid(pidFile);
|
|
187
416
|
if (pid === undefined)
|
|
188
417
|
continue;
|
|
@@ -207,69 +436,17 @@ async function stopDevServerByPidFiles(worktreePath, pidFiles, log) {
|
|
|
207
436
|
cleanupPidFile(pidFile);
|
|
208
437
|
}
|
|
209
438
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!args["no-remote-check"]) {
|
|
216
|
-
verifyBranchAbsentFromRemote(target.branch, run);
|
|
217
|
-
}
|
|
218
|
-
const ownerSuffix = target.owner ? `, owner ${target.owner}` : "";
|
|
219
|
-
if (!existsSync(target.worktreePath)) {
|
|
220
|
-
console.warn(`Warning: Worktree directory ${target.worktreePath} not found. Cleaning up registry only.`);
|
|
221
|
-
delete registry.slots[target.slotPort];
|
|
222
|
-
writeSlots(ctx.mainWorktree, registry);
|
|
223
|
-
console.log(`Removed registry entry for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}).`);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
await stopDevServerByPidFiles(target.worktreePath, config.devServerPidFiles, verboseLog);
|
|
227
|
-
if (config.teardownInfrastructure) {
|
|
228
|
-
await config.teardownInfrastructure({
|
|
229
|
-
worktree: target.worktreePath,
|
|
230
|
-
mainWorktree: ctx.mainWorktree,
|
|
231
|
-
verbose: run.verbose,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
delete registry.slots[target.slotPort];
|
|
235
|
-
writeSlots(ctx.mainWorktree, registry);
|
|
236
|
-
removeDevServerEntryByWorktree(ctx.mainWorktree, target.worktreePath);
|
|
237
|
-
if (removeHere) {
|
|
238
|
-
process.chdir(ctx.mainWorktree);
|
|
239
|
-
}
|
|
240
|
-
removeWorktree(target.worktreePath, run);
|
|
241
|
-
console.log(`Removed worktree for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}).`);
|
|
242
|
-
if (removeHere) {
|
|
243
|
-
console.log(`Now run: cd ${ctx.mainWorktree}`);
|
|
439
|
+
function resolvePortsFn(config) {
|
|
440
|
+
if (config.ports)
|
|
441
|
+
return config.ports;
|
|
442
|
+
if (config.portNames && config.portNames.length > 0) {
|
|
443
|
+
return defaultComputePorts(config.portNames);
|
|
244
444
|
}
|
|
445
|
+
throw new ConfigError("Config error: provide either `ports` (function) or `portNames` (array).");
|
|
245
446
|
}
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
mainWorktree: ctx.mainWorktree,
|
|
252
|
-
isMainWorktree: ctx.isMainWorktree,
|
|
253
|
-
});
|
|
254
|
-
// Propagate to dev-servers.json entries for this worktree.
|
|
255
|
-
const devServersPath = join(ctx.mainWorktree, ".local/worktrees/dev-servers.json");
|
|
256
|
-
if (existsSync(devServersPath)) {
|
|
257
|
-
const data = JSON.parse(readFileSync(devServersPath, "utf-8"));
|
|
258
|
-
let changed = false;
|
|
259
|
-
const resolvedCurrent = resolve(ctx.currentWorktree);
|
|
260
|
-
for (const server of data.servers) {
|
|
261
|
-
if (resolve(server.worktree) === resolvedCurrent) {
|
|
262
|
-
if (newOwner !== undefined)
|
|
263
|
-
server.owner = newOwner;
|
|
264
|
-
else
|
|
265
|
-
delete server.owner;
|
|
266
|
-
changed = true;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (changed) {
|
|
270
|
-
mkdirSync(dirname(devServersPath), { recursive: true });
|
|
271
|
-
writeFileSync(devServersPath, `${JSON.stringify(data, undefined, 2)}\n`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
console.log(`Owner for slot ${slotPort}: ${newOwner ?? "(none)"}`);
|
|
447
|
+
function makeVerboseLog(verbose) {
|
|
448
|
+
return (msg) => {
|
|
449
|
+
if (verbose)
|
|
450
|
+
console.log(msg);
|
|
451
|
+
};
|
|
275
452
|
}
|
package/dist/slots.d.ts
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
import { type PortScheme } from "./ports.js";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
export interface SlotEntry {
|
|
2
|
+
export interface ResolvedSlot {
|
|
3
|
+
slot: number;
|
|
5
4
|
worktree: string;
|
|
6
5
|
branch: string;
|
|
7
6
|
owner?: string;
|
|
8
7
|
}
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
export interface ResolvedSlot {
|
|
13
|
-
slot: number;
|
|
8
|
+
export type SlotStatus = "pending" | "ready" | "failed";
|
|
9
|
+
export interface SlotEntry {
|
|
14
10
|
worktree: string;
|
|
15
11
|
branch: string;
|
|
16
12
|
owner?: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
status: SlotStatus;
|
|
15
|
+
failure?: {
|
|
16
|
+
at: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
17
19
|
}
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
export interface PickSlotArgs {
|
|
21
|
-
slot?: string;
|
|
22
|
-
currentWorktree: string;
|
|
23
|
-
mainWorktree: string;
|
|
24
|
-
scheme: PortScheme;
|
|
20
|
+
export interface SlotsRegistry {
|
|
21
|
+
slots: Record<string, SlotEntry>;
|
|
25
22
|
}
|
|
26
|
-
export declare function
|
|
23
|
+
export declare function readSlots(mainWorktree: string, registryDir: string): SlotsRegistry;
|
|
24
|
+
export declare function writeSlots(mainWorktree: string, registryDir: string, registry: SlotsRegistry): void;
|
|
27
25
|
export interface RegisterSlotInput {
|
|
28
26
|
slot?: string;
|
|
29
27
|
currentWorktree: string;
|
|
30
28
|
mainWorktree: string;
|
|
29
|
+
registryDir: string;
|
|
31
30
|
scheme: PortScheme;
|
|
32
31
|
branch: string;
|
|
33
32
|
requestedOwner?: string;
|
|
@@ -36,18 +35,20 @@ export declare function resolveAndRegisterSlot(input: RegisterSlotInput): {
|
|
|
36
35
|
port: number;
|
|
37
36
|
owner: string | undefined;
|
|
38
37
|
};
|
|
38
|
+
export declare function markSlotReady(mainWorktree: string, registryDir: string, slotPort: number): void;
|
|
39
|
+
export declare function markSlotFailed(mainWorktree: string, registryDir: string, slotPort: number, message: string): void;
|
|
39
40
|
export declare function validateSlotAvailability(slotArg: string | undefined, ctx: {
|
|
40
41
|
currentWorktree: string;
|
|
41
42
|
mainWorktree: string;
|
|
43
|
+
registryDir: string;
|
|
42
44
|
scheme: PortScheme;
|
|
43
45
|
}): void;
|
|
44
|
-
export declare function
|
|
45
|
-
export declare function synthesizeMainSlot(basePort: number): ResolvedSlot | undefined;
|
|
46
|
-
export declare function resolveCurrentSlot(basePort: number): ResolvedSlot;
|
|
46
|
+
export declare function resolveCurrentSlot(basePort: number, registryDir: string): ResolvedSlot;
|
|
47
47
|
export interface SetOwnerInput {
|
|
48
48
|
newOwner: string | undefined;
|
|
49
49
|
currentWorktree: string;
|
|
50
50
|
mainWorktree: string;
|
|
51
|
+
registryDir: string;
|
|
51
52
|
isMainWorktree: boolean;
|
|
52
53
|
}
|
|
53
54
|
export declare function handleSetOwner(input: SetOwnerInput): {
|