@paleo/workspace 0.11.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.
@@ -0,0 +1,141 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ export function patchEnvFile(content, patches) {
4
+ const lines = content.trimEnd().split("\n");
5
+ for (const [key, value] of Object.entries(patches)) {
6
+ const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
7
+ if (idx !== -1) {
8
+ lines[idx] = `${key}=${value}`;
9
+ }
10
+ else {
11
+ lines.push(`${key}=${value}`);
12
+ }
13
+ }
14
+ return `${lines.join("\n")}\n`;
15
+ }
16
+ export function extractHost(content, key, fallback = "localhost") {
17
+ const re = new RegExp(`^${key}=(?:https?://)?([^:\\s]+)`, "m");
18
+ const m = content.match(re);
19
+ return m ? m[1] : fallback;
20
+ }
21
+ /**
22
+ * Reads `<varName>=<value>` from a dotenv-style file and parses it as a port.
23
+ * Exits with code 1 on missing file, missing variable, or non-numeric value.
24
+ */
25
+ export function readPortFromEnvFile(file, varName) {
26
+ if (!existsSync(file)) {
27
+ console.error(`Error: ${file} not found. Run \`workspace setup\` first.`);
28
+ process.exit(1);
29
+ }
30
+ const content = readFileSync(file, "utf-8");
31
+ const match = content.match(new RegExp(`^${varName}=(.+)`, "m"));
32
+ if (!match) {
33
+ console.error(`Error: ${varName} not found in ${file}.`);
34
+ process.exit(1);
35
+ }
36
+ return toPort(match[1].trim(), file);
37
+ }
38
+ /**
39
+ * Reads a dotted path (e.g. `server.port`) from a JSON file and parses it as a port.
40
+ * Exits with code 1 on missing file, missing path, or non-numeric value.
41
+ */
42
+ export function readPortFromJsonFile(file, jsonPath) {
43
+ if (!existsSync(file)) {
44
+ console.error(`Error: ${file} not found. Run \`workspace setup\` first.`);
45
+ process.exit(1);
46
+ }
47
+ const data = JSON.parse(readFileSync(file, "utf-8"));
48
+ let cur = data;
49
+ for (const seg of jsonPath.split(".")) {
50
+ if (cur === null || cur === undefined || typeof cur !== "object") {
51
+ console.error(`Error: ${jsonPath} not found in ${file}.`);
52
+ process.exit(1);
53
+ }
54
+ cur = cur[seg];
55
+ }
56
+ if (cur === undefined || cur === null) {
57
+ console.error(`Error: ${jsonPath} not found in ${file}.`);
58
+ process.exit(1);
59
+ }
60
+ return toPort(String(cur), file);
61
+ }
62
+ export function copyAndPatchFile(ctx, relPath, patchFn, label, force, optional = false) {
63
+ const targetPath = join(ctx.currentWorktree, relPath);
64
+ const sourcePath = join(ctx.mainWorktree, relPath);
65
+ const alreadyExists = existsSync(targetPath);
66
+ if (alreadyExists && !force) {
67
+ ctx.log(`Skipped ${label} (already exists; use --force to overwrite).`);
68
+ return;
69
+ }
70
+ if (!existsSync(sourcePath)) {
71
+ if (!optional) {
72
+ console.error(`Error: ${relPath} not found in main worktree. Bootstrap the main worktree first ` +
73
+ "(`workspace setup`), or mark the entry as optional.");
74
+ process.exit(1);
75
+ }
76
+ ctx.log(`Warning: ${relPath} not found in main worktree, skipping (optional).`);
77
+ return;
78
+ }
79
+ const content = readFileSync(sourcePath, "utf-8");
80
+ const patched = patchFn(content);
81
+ mkdirSync(dirname(targetPath), { recursive: true });
82
+ writeFileSync(targetPath, patched);
83
+ ctx.log(`${alreadyExists ? "Overwritten" : "Created"} ${label}.`);
84
+ }
85
+ /**
86
+ * Detects common fatal JS startup failures in a log buffer. Returns a short marker string
87
+ * naming the matched pattern, or `false` when none match. Used as the default `detectError`
88
+ * for spawn servers that don't supply one. A custom `detectError` can compose with this:
89
+ * `detectError: (log) => myDetector(log) || helpers.detectCommonJsError(log)`.
90
+ */
91
+ /**
92
+ * Formats a millisecond duration as the two largest units among `d`/`h`/`m`/`s`.
93
+ * Drops the smaller unit when zero (`5d` instead of `5d 0h`). Sub-second values
94
+ * round up to `1s` (zero stays `0s`). Negative input returns `0s`.
95
+ */
96
+ export function formatDuration(ms) {
97
+ if (ms <= 0)
98
+ return "0s";
99
+ if (ms < 1000)
100
+ return "1s";
101
+ const totalSec = Math.floor(ms / 1000);
102
+ const d = Math.floor(totalSec / 86400);
103
+ const h = Math.floor((totalSec % 86400) / 3600);
104
+ const m = Math.floor((totalSec % 3600) / 60);
105
+ const s = totalSec % 60;
106
+ const units = [
107
+ ["d", d],
108
+ ["h", h],
109
+ ["m", m],
110
+ ["s", s],
111
+ ];
112
+ const topIdx = units.findIndex(([, v]) => v > 0);
113
+ if (topIdx === -1)
114
+ return "0s";
115
+ const [topLabel, topVal] = units[topIdx];
116
+ const next = units[topIdx + 1];
117
+ if (!next || next[1] === 0)
118
+ return `${topVal}${topLabel}`;
119
+ return `${topVal}${topLabel} ${next[1]}${next[0]}`;
120
+ }
121
+ export function detectCommonJsError(log) {
122
+ if (log.includes("[nodemon] app crashed"))
123
+ return "[nodemon] app crashed";
124
+ if (/^Node\.js v/m.test(log))
125
+ return "Node.js v";
126
+ if (log.includes("Error: Cannot find module "))
127
+ return "Error: Cannot find module";
128
+ if (/^SyntaxError: /m.test(log))
129
+ return "SyntaxError";
130
+ if (log.includes("UnhandledPromiseRejection"))
131
+ return "UnhandledPromiseRejection";
132
+ return false;
133
+ }
134
+ function toPort(raw, file) {
135
+ const port = Number(raw);
136
+ if (!Number.isFinite(port)) {
137
+ console.error(`Error: invalid port "${raw}" in ${file}.`);
138
+ process.exit(1);
139
+ }
140
+ return port;
141
+ }
@@ -0,0 +1,10 @@
1
+ export { runWorkspace } from "./workspace.js";
2
+ export { defaultWorktreeDirName } from "./worktree.js";
3
+ export type { WorktreeDirNameFn } from "./worktree.js";
4
+ export type { WorkspaceConfig, SetupContext, SummaryContext, PatchContext, ConfigFileEntry, PurgeContext, } from "./workspace.js";
5
+ export { runDevServer } from "./dev-server.js";
6
+ export type { DevServerConfig, DevServerSummaryContext, ServerDescriptor, ServerContext, SpawnServer, CallbackServer, } from "./dev-server.js";
7
+ export type { ResolvedSlot } from "./slots.js";
8
+ import * as helpers from "./helpers.js";
9
+ export { helpers };
10
+ export { StartupError, ConfigError } from "./errors.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { runWorkspace } from "./workspace.js";
2
+ export { defaultWorktreeDirName } from "./worktree.js";
3
+ export { runDevServer } from "./dev-server.js";
4
+ import * as helpers from "./helpers.js";
5
+ export { helpers };
6
+ export { StartupError, ConfigError } from "./errors.js";
@@ -0,0 +1,17 @@
1
+ import { StartupError } from "./errors.js";
2
+ export declare const LOG_TAIL_LINES = 30;
3
+ export declare const POLL_INTERVAL_MS = 500;
4
+ export declare const TIMEOUT_MS = 120000;
5
+ export interface PollableServer {
6
+ name: string;
7
+ logFile: string;
8
+ detectSuccess: (logContent: string) => boolean;
9
+ detectError?: (logContent: string) => string | false;
10
+ }
11
+ export interface AwaitOptions {
12
+ timeoutMs?: number;
13
+ pollIntervalMs?: number;
14
+ isAlive?: (pid: number) => boolean;
15
+ }
16
+ export declare function awaitAllReady(servers: PollableServer[], pids: number[], options?: AwaitOptions): Promise<void>;
17
+ export declare function handleStartupFailure(err: StartupError): void;
@@ -0,0 +1,41 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { StartupError } from "./errors.js";
4
+ import { isProcessAlive as defaultIsAlive } from "./process-control.js";
5
+ export const LOG_TAIL_LINES = 30;
6
+ export const POLL_INTERVAL_MS = 500;
7
+ export const TIMEOUT_MS = 120_000;
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 = {}) {
21
+ const timeoutMs = options.timeoutMs ?? TIMEOUT_MS;
22
+ const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
23
+ const isAlive = options.isAlive ?? defaultIsAlive;
24
+ const deadline = Date.now() + timeoutMs;
25
+ while (Date.now() < deadline) {
26
+ if (!isAlive(pid)) {
27
+ throw new StartupError(server.name, "process exited unexpectedly", server.logFile);
28
+ }
29
+ if (existsSync(server.logFile)) {
30
+ const logContent = readFileSync(server.logFile, "utf-8");
31
+ if (server.detectSuccess(logContent))
32
+ return;
33
+ const matched = server.detectError?.(logContent);
34
+ if (matched) {
35
+ throw new StartupError(server.name, `error detected (${matched})`, server.logFile);
36
+ }
37
+ }
38
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
39
+ }
40
+ throw new StartupError(server.name, `did not become ready within ${timeoutMs / 1000}s`, server.logFile);
41
+ }
@@ -0,0 +1,25 @@
1
+ import type { ServerDescriptor, SpawnServer } from "./server-descriptor.js";
2
+ export interface PortHolder {
3
+ pid: number;
4
+ /** Process group id. Absent when `ps` output couldn't be parsed; callers must skip group kills. */
5
+ pgid?: number;
6
+ cmd: string;
7
+ cwd?: string;
8
+ }
9
+ export type PortConflict = {
10
+ kind: "ours";
11
+ server: SpawnServer;
12
+ holder: PortHolder;
13
+ } | {
14
+ kind: "foreign";
15
+ server: SpawnServer;
16
+ holder?: PortHolder;
17
+ };
18
+ export declare function canonicalCwd(cwd: string): string;
19
+ export declare function findPortHolder(port: number): PortHolder | undefined;
20
+ export declare function isPidOurs(holder: PortHolder, ourCanonicalCwd: string): boolean;
21
+ export declare function isPortBusy(port: number): Promise<boolean>;
22
+ export declare function detectPortConflicts(servers: ServerDescriptor[], ourCanonicalCwd: string): Promise<PortConflict[]>;
23
+ /** Polls the given ports until all are free or `timeoutMs` elapses. Returns ports still busy. */
24
+ export declare function waitForPortsFree(ports: number[], timeoutMs: number): Promise<number[]>;
25
+ export declare function sweepStalePorts(servers: ServerDescriptor[], cwd: string): Promise<void>;
@@ -0,0 +1,147 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
3
+ import { createConnection } from "node:net";
4
+ import { platform } from "node:os";
5
+ import { stopProcessGroup } from "./process-control.js";
6
+ export function canonicalCwd(cwd) {
7
+ try {
8
+ return realpathSync(cwd);
9
+ }
10
+ catch {
11
+ return cwd;
12
+ }
13
+ }
14
+ export function findPortHolder(port) {
15
+ if (platform() === "win32")
16
+ return;
17
+ const pid = listenerPid(port);
18
+ if (pid === undefined)
19
+ return;
20
+ const psInfo = pidPgidAndCommand(pid);
21
+ const cwd = pidCwd(pid);
22
+ return { pid, pgid: psInfo?.pgid, cmd: psInfo?.cmd ?? "", cwd };
23
+ }
24
+ export function isPidOurs(holder, ourCanonicalCwd) {
25
+ if (holder.cwd === undefined)
26
+ return false;
27
+ const holderCwd = canonicalCwd(holder.cwd);
28
+ return holderCwd === ourCanonicalCwd || holderCwd.startsWith(`${ourCanonicalCwd}/`);
29
+ }
30
+ export function isPortBusy(port) {
31
+ return new Promise((resolve) => {
32
+ const socket = createConnection({ port, host: "127.0.0.1" });
33
+ socket.setTimeout(500);
34
+ socket.once("connect", () => {
35
+ socket.destroy();
36
+ resolve(true);
37
+ });
38
+ socket.once("timeout", () => {
39
+ socket.destroy();
40
+ resolve(false);
41
+ });
42
+ socket.once("error", () => {
43
+ socket.destroy();
44
+ resolve(false);
45
+ });
46
+ });
47
+ }
48
+ export async function detectPortConflicts(servers, ourCanonicalCwd) {
49
+ const conflicts = [];
50
+ for (const server of spawnServersOf(servers)) {
51
+ if (!(await isPortBusy(server.port)))
52
+ continue;
53
+ const holder = findPortHolder(server.port);
54
+ if (holder && isPidOurs(holder, ourCanonicalCwd)) {
55
+ conflicts.push({ kind: "ours", server, holder });
56
+ }
57
+ else {
58
+ conflicts.push({ kind: "foreign", server, holder });
59
+ }
60
+ }
61
+ return conflicts;
62
+ }
63
+ /** Polls the given ports until all are free or `timeoutMs` elapses. Returns ports still busy. */
64
+ export async function waitForPortsFree(ports, timeoutMs) {
65
+ const intervalMs = 100;
66
+ const deadline = Date.now() + timeoutMs;
67
+ let pending = [...ports];
68
+ while (pending.length > 0) {
69
+ const results = await Promise.all(pending.map(async (p) => ({ p, busy: await isPortBusy(p) })));
70
+ pending = results.filter((r) => r.busy).map((r) => r.p);
71
+ if (pending.length === 0 || Date.now() >= deadline)
72
+ break;
73
+ await new Promise((r) => setTimeout(r, intervalMs));
74
+ }
75
+ return pending;
76
+ }
77
+ export async function sweepStalePorts(servers, cwd) {
78
+ if (platform() === "win32")
79
+ return;
80
+ const ourCwd = canonicalCwd(cwd);
81
+ for (const server of spawnServersOf(servers)) {
82
+ await sweepOnePort(server, ourCwd);
83
+ }
84
+ }
85
+ function spawnServersOf(servers) {
86
+ return servers.filter((s) => s.kind === "spawn");
87
+ }
88
+ async function sweepOnePort(server, ourCanonicalCwd) {
89
+ const holder = findPortHolder(server.port);
90
+ if (holder === undefined)
91
+ return;
92
+ if (isPidOurs(holder, ourCanonicalCwd)) {
93
+ if (holder.pgid === undefined) {
94
+ console.warn(`Leaked ${server.name} on port ${server.port} (PID ${holder.pid}: ${holder.cmd}); pgid unknown, left untouched.`);
95
+ return;
96
+ }
97
+ console.warn(`Sweeping leaked ${server.name} on port ${server.port} (PID ${holder.pid}: ${holder.cmd}).`);
98
+ await stopProcessGroup(holder.pgid);
99
+ }
100
+ else {
101
+ const cwdPart = holder.cwd ? ` (cwd ${holder.cwd})` : "";
102
+ console.warn(`Port ${server.port} (${server.name}) still in use by PID ${holder.pid}: ${holder.cmd}${cwdPart}. Left untouched.`);
103
+ }
104
+ }
105
+ function listenerPid(port) {
106
+ if (!Number.isInteger(port) || port <= 0)
107
+ return;
108
+ const out = tryExec("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]);
109
+ if (out === undefined)
110
+ return;
111
+ const trimmed = out.trim();
112
+ if (trimmed === "")
113
+ return;
114
+ const pid = Number(trimmed.split(/\s+/)[0]);
115
+ return Number.isFinite(pid) ? pid : undefined;
116
+ }
117
+ function pidPgidAndCommand(pid) {
118
+ if (!Number.isInteger(pid) || pid <= 0)
119
+ return;
120
+ const out = tryExec("ps", ["-p", String(pid), "-o", "pgid=,command="]);
121
+ if (out === undefined)
122
+ return;
123
+ const match = out.trim().match(/^(\d+)\s+(.+)$/);
124
+ if (match === null)
125
+ return;
126
+ const parsed = Number(match[1]);
127
+ if (!Number.isFinite(parsed))
128
+ return;
129
+ return { pgid: parsed, cmd: match[2] };
130
+ }
131
+ function pidCwd(pid) {
132
+ if (!Number.isInteger(pid) || pid <= 0)
133
+ return;
134
+ const out = tryExec("lsof", ["-p", String(pid), "-a", "-d", "cwd", "-Fn"]);
135
+ if (out === undefined)
136
+ return;
137
+ const match = out.match(/^n(.+)$/m);
138
+ return match === null ? undefined : match[1];
139
+ }
140
+ function tryExec(file, args) {
141
+ try {
142
+ return execFileSync(file, args, { stdio: ["ignore", "pipe", "ignore"] }).toString();
143
+ }
144
+ catch {
145
+ return;
146
+ }
147
+ }
@@ -0,0 +1,16 @@
1
+ export interface PortScheme {
2
+ basePort: number;
3
+ portStep: number;
4
+ maxSlotCount: number;
5
+ minPort: number;
6
+ maxPort: number;
7
+ }
8
+ export interface PortSchemeOptions {
9
+ basePort: number;
10
+ portStep?: number;
11
+ maxSlotCount?: number;
12
+ }
13
+ export declare function resolvePortScheme(opts: PortSchemeOptions): PortScheme;
14
+ export declare function isValidPort(port: number, scheme: PortScheme): boolean;
15
+ export declare function allPorts(scheme: PortScheme): number[];
16
+ export declare function defaultComputePorts(portNames: string[]): (slot: number) => Record<string, number>;
package/dist/ports.js ADDED
@@ -0,0 +1,37 @@
1
+ export function resolvePortScheme(opts) {
2
+ const portStep = opts.portStep ?? 10;
3
+ const maxSlotCount = opts.maxSlotCount ?? 19;
4
+ const basePort = opts.basePort;
5
+ return {
6
+ basePort,
7
+ portStep,
8
+ maxSlotCount,
9
+ minPort: basePort + portStep,
10
+ maxPort: basePort + maxSlotCount * portStep,
11
+ };
12
+ }
13
+ export function isValidPort(port, scheme) {
14
+ return (Number.isInteger(port) &&
15
+ port >= scheme.minPort &&
16
+ port <= scheme.maxPort &&
17
+ (port - scheme.basePort) % scheme.portStep === 0);
18
+ }
19
+ export function allPorts(scheme) {
20
+ const ports = [];
21
+ for (let p = scheme.minPort; p <= scheme.maxPort; p += scheme.portStep) {
22
+ ports.push(p);
23
+ }
24
+ return ports;
25
+ }
26
+ export function defaultComputePorts(portNames) {
27
+ if (portNames.length === 0) {
28
+ throw new Error("portNames must not be empty");
29
+ }
30
+ return (slot) => {
31
+ const out = {};
32
+ portNames.forEach((name, i) => {
33
+ out[name] = slot + i;
34
+ });
35
+ return out;
36
+ };
37
+ }
@@ -0,0 +1,4 @@
1
+ export declare function stopProcessGroup(pid: number, timeoutMs?: number): Promise<void>;
2
+ export declare function isProcessAlive(pid: number): boolean;
3
+ export declare function isProcessGroupAlive(pid: number): boolean;
4
+ export declare function killProcessGroup(pid: number, signal: NodeJS.Signals): void;
@@ -0,0 +1,41 @@
1
+ export async function stopProcessGroup(pid, timeoutMs = 10_000) {
2
+ killProcessGroup(pid, "SIGTERM");
3
+ const deadline = Date.now() + timeoutMs;
4
+ while (Date.now() < deadline) {
5
+ await new Promise((r) => setTimeout(r, 300));
6
+ if (!isProcessGroupAlive(pid))
7
+ return;
8
+ }
9
+ killProcessGroup(pid, "SIGKILL");
10
+ }
11
+ export function isProcessAlive(pid) {
12
+ try {
13
+ process.kill(pid, 0);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export function isProcessGroupAlive(pid) {
21
+ try {
22
+ process.kill(-pid, 0);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ export function killProcessGroup(pid, signal) {
30
+ try {
31
+ process.kill(-pid, signal);
32
+ }
33
+ catch {
34
+ try {
35
+ process.kill(pid, signal);
36
+ }
37
+ catch {
38
+ // already dead
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,45 @@
1
+ /** Context threaded into every callback-managed server lifecycle hook. */
2
+ export interface ServerContext {
3
+ /**
4
+ * Worktree directory for this lifecycle call. Equals `process.cwd()` at start time for local
5
+ * starts/stops; equals the victim entry's worktree for cross-worktree stops (eviction,
6
+ * `dev:down --all`). Callbacks MUST thread this into every child-process call
7
+ * (`{ cwd: ctx.cwd }` on `execSync`, `spawn`, etc.) and resolve relative paths against it.
8
+ */
9
+ cwd: string;
10
+ }
11
+ /** One process spawned and tracked by the runner. */
12
+ export interface SpawnServer {
13
+ kind: "spawn";
14
+ /** Short label used in logs and the registry. Derives `<runtimeDir>/logs/<name>.log`. */
15
+ name: string;
16
+ /** Command and arguments passed to `child_process.spawn`. */
17
+ exec: {
18
+ command: string;
19
+ args: string[];
20
+ };
21
+ /** Port the process will listen on. Use `helpers.readPortFromEnvFile` / `readPortFromJsonFile`. */
22
+ port: number;
23
+ /** Returns `true` once the log content indicates the server is ready. */
24
+ detectSuccess: (logContent: string) => boolean;
25
+ /**
26
+ * Returns a non-empty marker string when the log content indicates a fatal error, or `false`.
27
+ * When omitted, `helpers.detectCommonJsError` is used as a default. To disable detection,
28
+ * pass `() => false`.
29
+ */
30
+ detectError?: (logContent: string) => string | false;
31
+ }
32
+ /**
33
+ * A resource whose lifecycle the user owns (typically Docker / databases). The runner only
34
+ * invokes `start` and `stop`; it never spawns, polls logs, or tracks PIDs for callback servers.
35
+ */
36
+ export interface CallbackServer {
37
+ kind: "callback";
38
+ /** Short label used in logs. */
39
+ name: string;
40
+ /** Must resolve only once the resource is ready. Thread `ctx.cwd` into every child-process call. */
41
+ start: (ctx: ServerContext) => Promise<void>;
42
+ /** Tears down the resource. Thread `ctx.cwd` into every child-process call. */
43
+ stop: (ctx: ServerContext) => Promise<void>;
44
+ }
45
+ export type ServerDescriptor = SpawnServer | CallbackServer;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { type PortScheme } from "./ports.js";
2
+ export interface ResolvedSlot {
3
+ slot: number;
4
+ worktree: string;
5
+ owner?: string;
6
+ /** `true` when this slot is the main worktree. */
7
+ main?: boolean;
8
+ }
9
+ export type SlotStatus = "pending" | "ready" | "failed";
10
+ export interface SlotEntry {
11
+ worktree: string;
12
+ owner?: string;
13
+ createdAt: string;
14
+ status: SlotStatus;
15
+ failure?: {
16
+ at: string;
17
+ message: string;
18
+ };
19
+ /** `true` for the main-worktree entry. Absent on linked entries. */
20
+ main?: boolean;
21
+ }
22
+ export interface SlotsRegistry {
23
+ slots: Record<string, SlotEntry>;
24
+ }
25
+ export declare function readSlots(mainWorktree: string, registryDir: string): SlotsRegistry;
26
+ export declare function writeSlots(mainWorktree: string, registryDir: string, registry: SlotsRegistry): void;
27
+ export interface RegisterSlotInput {
28
+ slot?: string;
29
+ currentWorktree: string;
30
+ mainWorktree: string;
31
+ registryDir: string;
32
+ scheme: PortScheme;
33
+ requestedOwner?: string;
34
+ /** When `true`, the slot is forced to `scheme.basePort` regardless of `slot` arg. */
35
+ isMainWorktree: boolean;
36
+ /** When `true`, an existing `ready` slot is reset to `pending` so the re-finalize is observable. */
37
+ force?: boolean;
38
+ }
39
+ export declare function resolveAndRegisterSlot(input: RegisterSlotInput): {
40
+ port: number;
41
+ owner: string | undefined;
42
+ status: SlotStatus;
43
+ };
44
+ export declare function markSlotReady(mainWorktree: string, registryDir: string, slotPort: number): void;
45
+ export declare function markSlotFailed(mainWorktree: string, registryDir: string, slotPort: number, message: string): void;
46
+ export declare function validateSlotAvailability(slotArg: string | undefined, ctx: {
47
+ currentWorktree: string;
48
+ mainWorktree: string;
49
+ registryDir: string;
50
+ scheme: PortScheme;
51
+ }): void;
52
+ export declare function resolveCurrentSlot(basePort: number, registryDir: string): ResolvedSlot;
53
+ export interface SetOwnerInput {
54
+ newOwner: string | undefined;
55
+ currentWorktree: string;
56
+ mainWorktree: string;
57
+ registryDir: string;
58
+ isMainWorktree: boolean;
59
+ }
60
+ export declare function handleSetOwner(input: SetOwnerInput): {
61
+ slotPort: string;
62
+ owner: string | undefined;
63
+ };