@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
|
@@ -1,76 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { isProcessAlive, stopProcessGroup } from "./process-control.js";
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
return join(mainWorktree, DEV_SERVERS_FILE);
|
|
8
|
-
}
|
|
9
|
-
export function readDevServers(mainWorktree) {
|
|
10
|
-
const fp = filePath(mainWorktree);
|
|
11
|
-
if (!existsSync(fp))
|
|
12
|
-
return { servers: [] };
|
|
13
|
-
return JSON.parse(readFileSync(fp, "utf-8"));
|
|
14
|
-
}
|
|
15
|
-
export function writeDevServers(mainWorktree, data) {
|
|
16
|
-
const fp = filePath(mainWorktree);
|
|
17
|
-
mkdirSync(join(mainWorktree, WORKTREES_DIR), { recursive: true });
|
|
18
|
-
writeFileSync(fp, `${JSON.stringify(data, undefined, 2)}\n`);
|
|
19
|
-
}
|
|
20
|
-
export function pruneDeadServers(data, isAlive = isProcessAlive) {
|
|
21
|
-
const live = data.servers.filter((entry) => Object.values(entry.pids).some((pid) => isAlive(pid)));
|
|
22
|
-
return { servers: live };
|
|
23
|
-
}
|
|
24
|
-
export function pruneAndPersist(mainWorktree, isAlive = isProcessAlive) {
|
|
25
|
-
const data = readDevServers(mainWorktree);
|
|
26
|
-
const pruned = pruneDeadServers(data, isAlive);
|
|
27
|
-
if (pruned.servers.length !== data.servers.length) {
|
|
28
|
-
writeDevServers(mainWorktree, pruned);
|
|
29
|
-
}
|
|
30
|
-
return pruned;
|
|
31
|
-
}
|
|
32
|
-
export function registerDevServer(mainWorktree, entry) {
|
|
33
|
-
const data = pruneAndPersist(mainWorktree);
|
|
34
|
-
data.servers.push(entry);
|
|
35
|
-
writeDevServers(mainWorktree, data);
|
|
36
|
-
}
|
|
37
|
-
export function unregisterDevServer(mainWorktree, worktreePath) {
|
|
38
|
-
const fp = filePath(mainWorktree);
|
|
39
|
-
if (!existsSync(fp))
|
|
40
|
-
return;
|
|
41
|
-
const data = pruneAndPersist(mainWorktree);
|
|
42
|
-
const target = resolve(worktreePath);
|
|
43
|
-
const filtered = data.servers.filter((entry) => resolve(entry.worktree) !== target);
|
|
44
|
-
if (filtered.length === data.servers.length)
|
|
45
|
-
return;
|
|
46
|
-
writeDevServers(mainWorktree, { servers: filtered });
|
|
47
|
-
}
|
|
48
|
-
export function removeDevServerEntryByWorktree(mainWorktree, worktreePath) {
|
|
49
|
-
const fp = filePath(mainWorktree);
|
|
50
|
-
if (!existsSync(fp))
|
|
51
|
-
return;
|
|
52
|
-
const data = readDevServers(mainWorktree);
|
|
53
|
-
const target = resolve(worktreePath);
|
|
54
|
-
const filtered = data.servers.filter((entry) => resolve(entry.worktree) !== target);
|
|
55
|
-
if (filtered.length === data.servers.length)
|
|
56
|
-
return;
|
|
57
|
-
writeDevServers(mainWorktree, { servers: filtered });
|
|
58
|
-
}
|
|
59
|
-
function formatEntry(entry) {
|
|
60
|
-
const pids = Object.entries(entry.pids)
|
|
61
|
-
.map(([name, pid]) => `${name}=${pid}`)
|
|
62
|
-
.join(",");
|
|
63
|
-
const ownerPart = entry.owner ? ` owner=${entry.owner}` : "";
|
|
64
|
-
return ` slot ${entry.slot} branch=${entry.branch}${ownerPart} pids=${pids} startedAt=${entry.startedAt} worktree=${entry.worktree}`;
|
|
65
|
-
}
|
|
66
|
-
export function printActiveServers(active) {
|
|
67
|
-
const sorted = [...active].sort((a, b) => a.slot - b.slot);
|
|
68
|
-
for (const entry of sorted) {
|
|
69
|
-
process.stderr.write(`${formatEntry(entry)}\n`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
export function listDevServers(mainWorktree) {
|
|
73
|
-
const data = pruneAndPersist(mainWorktree);
|
|
4
|
+
const DEV_SERVERS_FILENAME = "dev-servers.json";
|
|
5
|
+
export function listDevServers(mainWorktree, registryDir) {
|
|
6
|
+
const data = pruneAndPersist(mainWorktree, registryDir);
|
|
74
7
|
if (data.servers.length === 0) {
|
|
75
8
|
console.log("No dev-servers running.");
|
|
76
9
|
return;
|
|
@@ -80,8 +13,14 @@ export function listDevServers(mainWorktree) {
|
|
|
80
13
|
console.log(formatEntry(entry));
|
|
81
14
|
}
|
|
82
15
|
}
|
|
16
|
+
export function printActiveServers(active) {
|
|
17
|
+
const sorted = [...active].sort((a, b) => a.slot - b.slot);
|
|
18
|
+
for (const entry of sorted) {
|
|
19
|
+
process.stderr.write(`${formatEntry(entry)}\n`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
83
22
|
export async function stopAllRegistered(input) {
|
|
84
|
-
const data = pruneAndPersist(input.mainWorktree);
|
|
23
|
+
const data = pruneAndPersist(input.mainWorktree, input.registryDir);
|
|
85
24
|
if (data.servers.length === 0) {
|
|
86
25
|
console.log("No dev-servers running.");
|
|
87
26
|
return;
|
|
@@ -95,12 +34,91 @@ export async function stopAllRegistered(input) {
|
|
|
95
34
|
console.log(` ${name} (PID ${pid})`);
|
|
96
35
|
await stopProcessGroup(pid);
|
|
97
36
|
}
|
|
98
|
-
|
|
99
|
-
|
|
37
|
+
// `runtimeDir` is repo-wide, so each entry's PID files live at the same relative path.
|
|
38
|
+
// Server names vary per entry, hence reading them from `entry.pids` rather than caller config.
|
|
39
|
+
for (const name of Object.keys(entry.pids)) {
|
|
40
|
+
const fp = join(entry.worktree, input.runtimeDir, `${name}.pid`);
|
|
100
41
|
if (existsSync(fp))
|
|
101
42
|
unlinkSync(fp);
|
|
102
43
|
}
|
|
103
44
|
}
|
|
104
|
-
writeDevServers(input.mainWorktree, { servers: [] });
|
|
45
|
+
writeDevServers(input.mainWorktree, input.registryDir, { servers: [] });
|
|
105
46
|
console.log(`Stopped ${data.servers.length} dev-server(s).`);
|
|
106
47
|
}
|
|
48
|
+
export async function evictOldest(mainWorktree, registryDir, count, deps = {}) {
|
|
49
|
+
const isAlive = deps.isAlive ?? isProcessAlive;
|
|
50
|
+
const stop = deps.stop ?? stopProcessGroup;
|
|
51
|
+
const data = pruneDeadServers(readDevServers(mainWorktree, registryDir), isAlive);
|
|
52
|
+
const sorted = [...data.servers].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
53
|
+
const victims = sorted.slice(0, count);
|
|
54
|
+
for (const entry of victims) {
|
|
55
|
+
for (const pid of Object.values(entry.pids)) {
|
|
56
|
+
if (isAlive(pid))
|
|
57
|
+
await stop(pid);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const victimSlots = new Set(victims.map((v) => v.slot));
|
|
61
|
+
const filtered = data.servers.filter((entry) => !victimSlots.has(entry.slot));
|
|
62
|
+
writeDevServers(mainWorktree, registryDir, { servers: filtered });
|
|
63
|
+
return victims;
|
|
64
|
+
}
|
|
65
|
+
export function registerDevServer(mainWorktree, registryDir, entry) {
|
|
66
|
+
const data = pruneAndPersist(mainWorktree, registryDir);
|
|
67
|
+
data.servers.push(entry);
|
|
68
|
+
writeDevServers(mainWorktree, registryDir, data);
|
|
69
|
+
}
|
|
70
|
+
export function unregisterDevServer(mainWorktree, registryDir, worktreePath) {
|
|
71
|
+
const fp = filePath(mainWorktree, registryDir);
|
|
72
|
+
if (!existsSync(fp))
|
|
73
|
+
return;
|
|
74
|
+
const data = pruneAndPersist(mainWorktree, registryDir);
|
|
75
|
+
const target = resolve(worktreePath);
|
|
76
|
+
const filtered = data.servers.filter((entry) => resolve(entry.worktree) !== target);
|
|
77
|
+
if (filtered.length === data.servers.length)
|
|
78
|
+
return;
|
|
79
|
+
writeDevServers(mainWorktree, registryDir, { servers: filtered });
|
|
80
|
+
}
|
|
81
|
+
export function removeDevServerEntryByWorktree(mainWorktree, registryDir, worktreePath) {
|
|
82
|
+
const fp = filePath(mainWorktree, registryDir);
|
|
83
|
+
if (!existsSync(fp))
|
|
84
|
+
return;
|
|
85
|
+
const data = readDevServers(mainWorktree, registryDir);
|
|
86
|
+
const target = resolve(worktreePath);
|
|
87
|
+
const filtered = data.servers.filter((entry) => resolve(entry.worktree) !== target);
|
|
88
|
+
if (filtered.length === data.servers.length)
|
|
89
|
+
return;
|
|
90
|
+
writeDevServers(mainWorktree, registryDir, { servers: filtered });
|
|
91
|
+
}
|
|
92
|
+
export function pruneAndPersist(mainWorktree, registryDir, isAlive = isProcessAlive) {
|
|
93
|
+
const data = readDevServers(mainWorktree, registryDir);
|
|
94
|
+
const pruned = pruneDeadServers(data, isAlive);
|
|
95
|
+
if (pruned.servers.length !== data.servers.length) {
|
|
96
|
+
writeDevServers(mainWorktree, registryDir, pruned);
|
|
97
|
+
}
|
|
98
|
+
return pruned;
|
|
99
|
+
}
|
|
100
|
+
export function pruneDeadServers(data, isAlive = isProcessAlive) {
|
|
101
|
+
const live = data.servers.filter((entry) => Object.values(entry.pids).some((pid) => isAlive(pid)));
|
|
102
|
+
return { servers: live };
|
|
103
|
+
}
|
|
104
|
+
export function readDevServers(mainWorktree, registryDir) {
|
|
105
|
+
const fp = filePath(mainWorktree, registryDir);
|
|
106
|
+
if (!existsSync(fp))
|
|
107
|
+
return { servers: [] };
|
|
108
|
+
return JSON.parse(readFileSync(fp, "utf-8"));
|
|
109
|
+
}
|
|
110
|
+
export function writeDevServers(mainWorktree, registryDir, data) {
|
|
111
|
+
const fp = filePath(mainWorktree, registryDir);
|
|
112
|
+
mkdirSync(join(mainWorktree, registryDir), { recursive: true });
|
|
113
|
+
writeFileSync(fp, `${JSON.stringify(data, undefined, 2)}\n`);
|
|
114
|
+
}
|
|
115
|
+
function filePath(mainWorktree, registryDir) {
|
|
116
|
+
return join(mainWorktree, registryDir, DEV_SERVERS_FILENAME);
|
|
117
|
+
}
|
|
118
|
+
function formatEntry(entry) {
|
|
119
|
+
const pids = Object.entries(entry.pids)
|
|
120
|
+
.map(([name, pid]) => `${name}=${pid}`)
|
|
121
|
+
.join(",");
|
|
122
|
+
const ownerPart = entry.owner ? ` owner=${entry.owner}` : "";
|
|
123
|
+
return ` slot ${entry.slot} branch=${entry.branch}${ownerPart} pids=${pids} startedAt=${entry.startedAt} worktree=${entry.worktree}`;
|
|
124
|
+
}
|
package/dist/errors.d.ts
CHANGED
package/dist/errors.js
CHANGED
package/dist/helpers.js
CHANGED
|
@@ -59,14 +59,6 @@ export function readPortFromJsonFile(file, jsonPath) {
|
|
|
59
59
|
}
|
|
60
60
|
return toPort(String(cur), file);
|
|
61
61
|
}
|
|
62
|
-
function toPort(raw, file) {
|
|
63
|
-
const port = Number(raw);
|
|
64
|
-
if (!Number.isFinite(port)) {
|
|
65
|
-
console.error(`Error: invalid port "${raw}" in ${file}.`);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
return port;
|
|
69
|
-
}
|
|
70
62
|
export function copyAndPatchFile(ctx, relPath, patchFn, label, force, required = false) {
|
|
71
63
|
const targetPath = join(ctx.currentWorktree, relPath);
|
|
72
64
|
const sourcePath = join(ctx.mainWorktree, relPath);
|
|
@@ -89,3 +81,11 @@ export function copyAndPatchFile(ctx, relPath, patchFn, label, force, required =
|
|
|
89
81
|
writeFileSync(targetPath, patched);
|
|
90
82
|
ctx.log(`${alreadyExists ? "Overwritten" : "Created"} ${label}.`);
|
|
91
83
|
}
|
|
84
|
+
function toPort(raw, file) {
|
|
85
|
+
const port = Number(raw);
|
|
86
|
+
if (!Number.isFinite(port)) {
|
|
87
|
+
console.error(`Error: invalid port "${raw}" in ${file}.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
return port;
|
|
91
|
+
}
|
package/dist/log-polling.d.ts
CHANGED
|
@@ -13,6 +13,5 @@ export interface AwaitOptions {
|
|
|
13
13
|
pollIntervalMs?: number;
|
|
14
14
|
isAlive?: (pid: number) => boolean;
|
|
15
15
|
}
|
|
16
|
-
export declare function waitForReady(server: PollableServer, pid: number, options?: AwaitOptions): Promise<void>;
|
|
17
16
|
export declare function awaitAllReady(servers: PollableServer[], pids: number[], options?: AwaitOptions): Promise<void>;
|
|
18
17
|
export declare function handleStartupFailure(err: StartupError): void;
|
package/dist/log-polling.js
CHANGED
|
@@ -5,7 +5,19 @@ import { isProcessAlive as defaultIsAlive } from "./process-control.js";
|
|
|
5
5
|
export const LOG_TAIL_LINES = 30;
|
|
6
6
|
export const POLL_INTERVAL_MS = 500;
|
|
7
7
|
export const TIMEOUT_MS = 120_000;
|
|
8
|
-
export async function
|
|
8
|
+
export async function awaitAllReady(servers, pids, options) {
|
|
9
|
+
await Promise.all(servers.map((server, i) => waitForReady(server, pids[i], options)));
|
|
10
|
+
}
|
|
11
|
+
export function handleStartupFailure(err) {
|
|
12
|
+
console.error(`\nError: ${err.label} ${err.reason}.`);
|
|
13
|
+
if (err.logFile && existsSync(err.logFile)) {
|
|
14
|
+
const lines = readFileSync(err.logFile, "utf-8").split("\n").slice(-LOG_TAIL_LINES);
|
|
15
|
+
console.error(`\n--- ${err.label} log tail (last ${LOG_TAIL_LINES} lines) ---`);
|
|
16
|
+
console.error(lines.join("\n"));
|
|
17
|
+
console.error(`--- end ---\nFull log: ${join(process.cwd(), err.logFile)}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function waitForReady(server, pid, options = {}) {
|
|
9
21
|
const timeoutMs = options.timeoutMs ?? TIMEOUT_MS;
|
|
10
22
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
11
23
|
const isAlive = options.isAlive ?? defaultIsAlive;
|
|
@@ -27,15 +39,3 @@ export async function waitForReady(server, pid, options = {}) {
|
|
|
27
39
|
}
|
|
28
40
|
throw new StartupError(server.name, `did not become ready within ${timeoutMs / 1000}s`, server.logFile);
|
|
29
41
|
}
|
|
30
|
-
export async function awaitAllReady(servers, pids, options) {
|
|
31
|
-
await Promise.all(servers.map((server, i) => waitForReady(server, pids[i], options)));
|
|
32
|
-
}
|
|
33
|
-
export function handleStartupFailure(err) {
|
|
34
|
-
console.error(`\nError: ${err.label} ${err.reason}.`);
|
|
35
|
-
if (err.logFile && existsSync(err.logFile)) {
|
|
36
|
-
const lines = readFileSync(err.logFile, "utf-8").split("\n").slice(-LOG_TAIL_LINES);
|
|
37
|
-
console.error(`\n--- ${err.label} log tail (last ${LOG_TAIL_LINES} lines) ---`);
|
|
38
|
-
console.error(lines.join("\n"));
|
|
39
|
-
console.error(`--- end ---\nFull log: ${join(process.cwd(), err.logFile)}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
package/dist/ports.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
export interface PortSchemeOptions {
|
|
2
|
-
basePort: number;
|
|
3
|
-
portStep?: number;
|
|
4
|
-
maxSlotCount?: number;
|
|
5
|
-
}
|
|
6
1
|
export interface PortScheme {
|
|
7
2
|
basePort: number;
|
|
8
3
|
portStep: number;
|
|
@@ -10,6 +5,11 @@ export interface PortScheme {
|
|
|
10
5
|
minPort: number;
|
|
11
6
|
maxPort: number;
|
|
12
7
|
}
|
|
8
|
+
export interface PortSchemeOptions {
|
|
9
|
+
basePort: number;
|
|
10
|
+
portStep?: number;
|
|
11
|
+
maxSlotCount?: number;
|
|
12
|
+
}
|
|
13
13
|
export declare function resolvePortScheme(opts: PortSchemeOptions): PortScheme;
|
|
14
14
|
export declare function isValidPort(port: number, scheme: PortScheme): boolean;
|
|
15
15
|
export declare function allPorts(scheme: PortScheme): number[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
export declare function stopByPidFile(pidFile: string, label: string, log?: (msg: string) => void): Promise<void>;
|
|
2
|
+
export declare function stopProcessGroup(pid: number, timeoutMs?: number): Promise<void>;
|
|
1
3
|
export declare function readPid(pidFile: string): number | undefined;
|
|
2
4
|
export declare function isProcessAlive(pid: number): boolean;
|
|
3
5
|
export declare function isProcessGroupAlive(pid: number): boolean;
|
|
4
6
|
export declare function killProcessGroup(pid: number, signal: NodeJS.Signals): void;
|
|
5
7
|
export declare function cleanupPidFile(pidFile: string): void;
|
|
6
|
-
export declare function stopProcessGroup(pid: number, timeoutMs?: number): Promise<void>;
|
|
7
|
-
export declare function stopByPidFile(pidFile: string, label: string, log?: (msg: string) => void): Promise<void>;
|
package/dist/process-control.js
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
export async function stopByPidFile(pidFile, label, log = () => { }) {
|
|
3
|
+
const pid = readPid(pidFile);
|
|
4
|
+
if (pid === undefined || !isProcessAlive(pid)) {
|
|
5
|
+
cleanupPidFile(pidFile);
|
|
6
|
+
log(`No ${label} process is running.`);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
log(`Stopping ${label} (PID ${pid})...`);
|
|
10
|
+
await stopProcessGroup(pid);
|
|
11
|
+
cleanupPidFile(pidFile);
|
|
12
|
+
log(`${label} stopped.`);
|
|
13
|
+
}
|
|
14
|
+
export async function stopProcessGroup(pid, timeoutMs = 10_000) {
|
|
15
|
+
killProcessGroup(pid, "SIGTERM");
|
|
16
|
+
const deadline = Date.now() + timeoutMs;
|
|
17
|
+
while (Date.now() < deadline) {
|
|
18
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
19
|
+
if (!isProcessGroupAlive(pid))
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
killProcessGroup(pid, "SIGKILL");
|
|
23
|
+
}
|
|
2
24
|
export function readPid(pidFile) {
|
|
3
25
|
if (!existsSync(pidFile))
|
|
4
26
|
return undefined;
|
|
@@ -43,25 +65,3 @@ export function cleanupPidFile(pidFile) {
|
|
|
43
65
|
if (existsSync(pidFile))
|
|
44
66
|
unlinkSync(pidFile);
|
|
45
67
|
}
|
|
46
|
-
export async function stopProcessGroup(pid, timeoutMs = 10_000) {
|
|
47
|
-
killProcessGroup(pid, "SIGTERM");
|
|
48
|
-
const deadline = Date.now() + timeoutMs;
|
|
49
|
-
while (Date.now() < deadline) {
|
|
50
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
51
|
-
if (!isProcessGroupAlive(pid))
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
killProcessGroup(pid, "SIGKILL");
|
|
55
|
-
}
|
|
56
|
-
export async function stopByPidFile(pidFile, label, log = () => { }) {
|
|
57
|
-
const pid = readPid(pidFile);
|
|
58
|
-
if (pid === undefined || !isProcessAlive(pid)) {
|
|
59
|
-
cleanupPidFile(pidFile);
|
|
60
|
-
log(`No ${label} process is running.`);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
log(`Stopping ${label} (PID ${pid})...`);
|
|
64
|
-
await stopProcessGroup(pid);
|
|
65
|
-
cleanupPidFile(pidFile);
|
|
66
|
-
log(`${label} stopped.`);
|
|
67
|
-
}
|
package/dist/setup-worktree.d.ts
CHANGED
|
@@ -1,4 +1,49 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Configuration accepted by {@link runSetupWorktree}. */
|
|
2
|
+
export interface SetupWorktreeConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Absolute path to the wrapper script that calls `runSetupWorktree`. The package re-spawns this
|
|
5
|
+
* file as a detached child for the finalize phase, so it must point at a runnable Node entrypoint
|
|
6
|
+
* — typically `fileURLToPath(import.meta.url)` from your `setup-worktree.mjs`.
|
|
7
|
+
*/
|
|
8
|
+
scriptPath: string;
|
|
9
|
+
/** Anchor port for the slot range. Slots are derived from this value. */
|
|
10
|
+
basePort: number;
|
|
11
|
+
/** Distance between consecutive slots. Defaults to `10`. */
|
|
12
|
+
portStep?: number;
|
|
13
|
+
/** Maximum number of slots. Defaults to `19`. */
|
|
14
|
+
maxSlotCount?: number;
|
|
15
|
+
/** Custom port computation; takes precedence over `portNames`. */
|
|
16
|
+
ports?: (slot: number) => Record<string, number>;
|
|
17
|
+
/** Named offsets `[name0, name1, ...]` mapped to `slot+0`, `slot+1`, ... Required if `ports` is omitted. */
|
|
18
|
+
portNames?: string[];
|
|
19
|
+
/** Directories symlinked from the main worktree. */
|
|
20
|
+
sharedDirs: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Per-worktree runtime directory, relative to the worktree root (e.g. `.local-wt`).
|
|
23
|
+
* Holds the setup log, dev-server PID files, and dev-server logs.
|
|
24
|
+
*/
|
|
25
|
+
runtimeDir: string;
|
|
26
|
+
/**
|
|
27
|
+
* Shared registry directory, relative to a worktree root (e.g. `.local/wt-registry`).
|
|
28
|
+
* Holds `slots.json` and `dev-servers.json`. Must resolve to the same physical directory
|
|
29
|
+
* across linked worktrees — typically via a symlink listed in `sharedDirs` (e.g. `.local`).
|
|
30
|
+
*/
|
|
31
|
+
registryDir: string;
|
|
32
|
+
/** Config files copied from the main worktree and patched per slot. */
|
|
33
|
+
configFiles: ConfigFileEntry[];
|
|
34
|
+
/**
|
|
35
|
+
* MUST be idempotent. After a failure, the user re-runs `setup-worktree --here` from inside
|
|
36
|
+
* the worktree — this callback will be invoked again with the same context. Re-runs must not
|
|
37
|
+
* error on pre-existing state (created directories, started containers, ran migrations,
|
|
38
|
+
* installed deps, etc.).
|
|
39
|
+
*/
|
|
40
|
+
finalizeWorktree: (ctx: SetupContext) => Promise<void> | void;
|
|
41
|
+
/** Tears down infrastructure on `--remove` (e.g. `docker compose down -v`). Best-effort; errors should be swallowed. */
|
|
42
|
+
teardownInfrastructure?: (ctx: TeardownContext) => Promise<void> | void;
|
|
43
|
+
/** Builds the post-setup summary printed to stdout. */
|
|
44
|
+
printSummary: (ctx: SummaryContext) => string;
|
|
45
|
+
}
|
|
46
|
+
/** Context passed to {@link SetupWorktreeConfig.finalizeWorktree}. */
|
|
2
47
|
export interface SetupContext {
|
|
3
48
|
currentWorktree: string;
|
|
4
49
|
mainWorktree: string;
|
|
@@ -9,12 +54,20 @@ export interface SetupContext {
|
|
|
9
54
|
force: boolean;
|
|
10
55
|
verbose: boolean;
|
|
11
56
|
}
|
|
12
|
-
/** Context passed to {@link
|
|
13
|
-
export interface
|
|
57
|
+
/** Context passed to {@link SetupWorktreeConfig.printSummary}. */
|
|
58
|
+
export interface SummaryContext {
|
|
14
59
|
slot: number;
|
|
60
|
+
branch: string;
|
|
61
|
+
owner?: string;
|
|
15
62
|
ports: Record<string, number>;
|
|
16
|
-
mainWorktree: string;
|
|
17
63
|
currentWorktree: string;
|
|
64
|
+
mainWorktree: string;
|
|
65
|
+
}
|
|
66
|
+
/** Context passed to {@link SetupWorktreeConfig.teardownInfrastructure}. */
|
|
67
|
+
export interface TeardownContext {
|
|
68
|
+
worktree: string;
|
|
69
|
+
mainWorktree: string;
|
|
70
|
+
verbose: boolean;
|
|
18
71
|
}
|
|
19
72
|
/** One config file copied from the main worktree and patched per slot. */
|
|
20
73
|
export interface ConfigFileEntry {
|
|
@@ -25,52 +78,11 @@ export interface ConfigFileEntry {
|
|
|
25
78
|
/** When `true`, abort if the source file is missing in the main worktree. Defaults to `false`. */
|
|
26
79
|
required?: boolean;
|
|
27
80
|
}
|
|
28
|
-
/** Context passed to {@link
|
|
29
|
-
export interface
|
|
81
|
+
/** Context passed to {@link ConfigFileEntry.patch}. */
|
|
82
|
+
export interface PatchContext {
|
|
30
83
|
slot: number;
|
|
31
|
-
branch: string;
|
|
32
|
-
owner?: string;
|
|
33
84
|
ports: Record<string, number>;
|
|
34
|
-
currentWorktree: string;
|
|
35
85
|
mainWorktree: string;
|
|
36
|
-
|
|
37
|
-
/** Context passed to {@link SetupWorktreeConfig.teardownInfrastructure}. */
|
|
38
|
-
export interface TeardownContext {
|
|
39
|
-
worktree: string;
|
|
40
|
-
mainWorktree: string;
|
|
41
|
-
verbose: boolean;
|
|
42
|
-
}
|
|
43
|
-
/** Configuration accepted by {@link runSetupWorktree}. */
|
|
44
|
-
export interface SetupWorktreeConfig {
|
|
45
|
-
/** Anchor port for the slot range. Slots are derived from this value. */
|
|
46
|
-
basePort: number;
|
|
47
|
-
/** Distance between consecutive slots. Defaults to `10`. */
|
|
48
|
-
portStep?: number;
|
|
49
|
-
/** Maximum number of slots. Defaults to `9`. */
|
|
50
|
-
maxSlotCount?: number;
|
|
51
|
-
/** Custom port computation; takes precedence over `portNames`. */
|
|
52
|
-
ports?: (slot: number) => Record<string, number>;
|
|
53
|
-
/** Named offsets `[name0, name1, ...]` mapped to `slot+0`, `slot+1`, ... Required if `ports` is omitted. */
|
|
54
|
-
portNames?: string[];
|
|
55
|
-
/** Directories symlinked from the main worktree. Defaults to `[".local", ".plans"]`. */
|
|
56
|
-
sharedDirs?: string[];
|
|
57
|
-
/** PID files written by `dev-server`, used by `--remove` to stop processes before teardown. */
|
|
58
|
-
devServerPidFiles: string[];
|
|
59
|
-
/** Config files copied from the main worktree and patched per slot. */
|
|
60
|
-
configFiles: ConfigFileEntry[];
|
|
61
|
-
/**
|
|
62
|
-
* Runs after symlinks and config files. Owns per-worktree data setup:
|
|
63
|
-
* create any required directories (e.g. `.local-data/...`), copy or
|
|
64
|
-
* provision databases / file storage, start infrastructure containers.
|
|
65
|
-
*/
|
|
66
|
-
setupWorktreeData: (ctx: SetupContext) => Promise<void> | void;
|
|
67
|
-
/** Tears down infrastructure on `--remove` (e.g. `docker compose down -v`). Best-effort; errors should be swallowed. */
|
|
68
|
-
teardownInfrastructure?: (ctx: TeardownContext) => Promise<void> | void;
|
|
69
|
-
/** Runs after `setupWorktreeData`. Typically `npm install && npm run build`. */
|
|
70
|
-
installAndBuild: (ctx: SetupContext) => Promise<void> | void;
|
|
71
|
-
/** Runs after `installAndBuild`. Typically migrations and seeds. */
|
|
72
|
-
afterDatabase?: (ctx: SetupContext) => Promise<void> | void;
|
|
73
|
-
/** Builds the post-setup summary printed to stdout. */
|
|
74
|
-
printSummary: (ctx: SummaryContext) => string;
|
|
86
|
+
currentWorktree: string;
|
|
75
87
|
}
|
|
76
88
|
export declare function runSetupWorktree(config: SetupWorktreeConfig): Promise<void>;
|