@paleo/worktree-env 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/dist/cli.d.ts +10 -2
- package/dist/cli.js +60 -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 +259 -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 +31 -32
- package/package.json +1 -1
package/dist/slots.js
CHANGED
|
@@ -2,59 +2,56 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
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
|
-
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
const filePath = join(mainWorktree, SLOTS_FILE);
|
|
5
|
+
const SLOTS_FILENAME = "slots.json";
|
|
6
|
+
export function readSlots(mainWorktree, registryDir) {
|
|
7
|
+
const filePath = join(mainWorktree, registryDir, SLOTS_FILENAME);
|
|
9
8
|
if (!existsSync(filePath))
|
|
10
9
|
return { slots: {} };
|
|
11
10
|
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
12
11
|
}
|
|
13
|
-
export function writeSlots(mainWorktree, registry) {
|
|
14
|
-
const filePath = join(mainWorktree,
|
|
15
|
-
mkdirSync(join(mainWorktree,
|
|
12
|
+
export function writeSlots(mainWorktree, registryDir, registry) {
|
|
13
|
+
const filePath = join(mainWorktree, registryDir, SLOTS_FILENAME);
|
|
14
|
+
mkdirSync(join(mainWorktree, registryDir), { recursive: true });
|
|
16
15
|
writeFileSync(filePath, `${JSON.stringify(registry, undefined, 2)}\n`);
|
|
17
16
|
}
|
|
18
|
-
export function pickSlotPort(args, registry) {
|
|
19
|
-
const resolvedCurrent = resolve(args.currentWorktree);
|
|
20
|
-
if (args.slot !== undefined) {
|
|
21
|
-
const port = Number(args.slot);
|
|
22
|
-
if (!isValidPort(port, args.scheme)) {
|
|
23
|
-
console.error(`Error: Slot must be a valid port: ${allPorts(args.scheme).join(", ")}.`);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
const existing = registry.slots[String(port)];
|
|
27
|
-
if (existing && resolve(existing.worktree) !== resolvedCurrent) {
|
|
28
|
-
console.error(`Error: Slot ${port} is already taken by ${existing.worktree} (branch: ${existing.branch}).`);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
return port;
|
|
32
|
-
}
|
|
33
|
-
const existingEntry = Object.entries(registry.slots).find(([, v]) => resolve(v.worktree) === resolvedCurrent);
|
|
34
|
-
if (existingEntry)
|
|
35
|
-
return Number(existingEntry[0]);
|
|
36
|
-
for (const port of allPorts(args.scheme)) {
|
|
37
|
-
if (!registry.slots[String(port)])
|
|
38
|
-
return port;
|
|
39
|
-
}
|
|
40
|
-
console.error("Error: All slots are taken. Remove a worktree with --remove first.");
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
17
|
export function resolveAndRegisterSlot(input) {
|
|
44
|
-
const registry = readSlots(input.mainWorktree);
|
|
18
|
+
const registry = readSlots(input.mainWorktree, input.registryDir);
|
|
45
19
|
const port = pickSlotPort(input, registry);
|
|
46
20
|
const existing = registry.slots[String(port)];
|
|
47
21
|
const owner = input.requestedOwner ?? existing?.owner;
|
|
22
|
+
const createdAt = existing?.createdAt ?? new Date().toISOString();
|
|
23
|
+
// Re-runs of `--here` keep a previously finalized slot ready; otherwise reset to pending.
|
|
24
|
+
const status = existing?.status === "ready" ? "ready" : "pending";
|
|
48
25
|
const entry = {
|
|
49
26
|
worktree: input.currentWorktree,
|
|
50
27
|
branch: input.branch,
|
|
28
|
+
createdAt,
|
|
29
|
+
status,
|
|
51
30
|
};
|
|
52
31
|
if (owner !== undefined)
|
|
53
32
|
entry.owner = owner;
|
|
54
33
|
registry.slots[String(port)] = entry;
|
|
55
|
-
writeSlots(input.mainWorktree, registry);
|
|
34
|
+
writeSlots(input.mainWorktree, input.registryDir, registry);
|
|
56
35
|
return { port, owner };
|
|
57
36
|
}
|
|
37
|
+
export function markSlotReady(mainWorktree, registryDir, slotPort) {
|
|
38
|
+
const registry = readSlots(mainWorktree, registryDir);
|
|
39
|
+
const entry = registry.slots[String(slotPort)];
|
|
40
|
+
if (!entry)
|
|
41
|
+
return;
|
|
42
|
+
entry.status = "ready";
|
|
43
|
+
delete entry.failure;
|
|
44
|
+
writeSlots(mainWorktree, registryDir, registry);
|
|
45
|
+
}
|
|
46
|
+
export function markSlotFailed(mainWorktree, registryDir, slotPort, message) {
|
|
47
|
+
const registry = readSlots(mainWorktree, registryDir);
|
|
48
|
+
const entry = registry.slots[String(slotPort)];
|
|
49
|
+
if (!entry)
|
|
50
|
+
return;
|
|
51
|
+
entry.status = "failed";
|
|
52
|
+
entry.failure = { at: new Date().toISOString(), message };
|
|
53
|
+
writeSlots(mainWorktree, registryDir, registry);
|
|
54
|
+
}
|
|
58
55
|
export function validateSlotAvailability(slotArg, ctx) {
|
|
59
56
|
if (slotArg === undefined)
|
|
60
57
|
return;
|
|
@@ -63,43 +60,15 @@ export function validateSlotAvailability(slotArg, ctx) {
|
|
|
63
60
|
console.error(`Error: Slot must be a valid port: ${allPorts(ctx.scheme).join(", ")}.`);
|
|
64
61
|
process.exit(1);
|
|
65
62
|
}
|
|
66
|
-
const registry = readSlots(ctx.mainWorktree);
|
|
63
|
+
const registry = readSlots(ctx.mainWorktree, ctx.registryDir);
|
|
67
64
|
const existing = registry.slots[String(port)];
|
|
68
65
|
if (existing && resolve(existing.worktree) !== resolve(ctx.currentWorktree)) {
|
|
69
66
|
console.error(`Error: Slot ${port} is already taken by ${existing.worktree} (branch: ${existing.branch}).`);
|
|
70
67
|
process.exit(1);
|
|
71
68
|
}
|
|
72
69
|
}
|
|
73
|
-
export function
|
|
74
|
-
const
|
|
75
|
-
// Reads slots.json relative to cwd's `.local` symlink (so works in linked worktrees too).
|
|
76
|
-
const filePath = SLOTS_FILE;
|
|
77
|
-
if (!existsSync(filePath))
|
|
78
|
-
return undefined;
|
|
79
|
-
const registry = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
80
|
-
for (const [port, entry] of Object.entries(registry.slots)) {
|
|
81
|
-
if (resolve(entry.worktree) === cwd) {
|
|
82
|
-
return {
|
|
83
|
-
slot: Number(port),
|
|
84
|
-
worktree: entry.worktree,
|
|
85
|
-
branch: entry.branch,
|
|
86
|
-
owner: entry.owner,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
export function synthesizeMainSlot(basePort) {
|
|
93
|
-
const gitCommonDir = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf-8" }).trim();
|
|
94
|
-
const mainWorktree = dirname(gitCommonDir);
|
|
95
|
-
const cwd = resolve(process.cwd());
|
|
96
|
-
if (resolve(mainWorktree) !== cwd)
|
|
97
|
-
return undefined;
|
|
98
|
-
const branch = execFileSync("git", ["branch", "--show-current"], { encoding: "utf-8" }).trim();
|
|
99
|
-
return { slot: basePort, worktree: cwd, branch };
|
|
100
|
-
}
|
|
101
|
-
export function resolveCurrentSlot(basePort) {
|
|
102
|
-
const slot = lookupSlotForCwd() ?? synthesizeMainSlot(basePort);
|
|
70
|
+
export function resolveCurrentSlot(basePort, registryDir) {
|
|
71
|
+
const slot = lookupSlotForCwd(registryDir) ?? synthesizeMainSlot(basePort);
|
|
103
72
|
if (!slot) {
|
|
104
73
|
console.error("Error: No slot found for this worktree. Run setup-worktree first.");
|
|
105
74
|
process.exit(1);
|
|
@@ -111,7 +80,7 @@ export function handleSetOwner(input) {
|
|
|
111
80
|
console.error("Error: --set-owner must be run from a linked worktree.");
|
|
112
81
|
process.exit(1);
|
|
113
82
|
}
|
|
114
|
-
const registry = readSlots(input.mainWorktree);
|
|
83
|
+
const registry = readSlots(input.mainWorktree, input.registryDir);
|
|
115
84
|
const resolvedCurrent = resolve(input.currentWorktree);
|
|
116
85
|
const entry = Object.entries(registry.slots).find(([, v]) => resolve(v.worktree) === resolvedCurrent);
|
|
117
86
|
if (!entry) {
|
|
@@ -122,10 +91,67 @@ export function handleSetOwner(input) {
|
|
|
122
91
|
const updated = {
|
|
123
92
|
worktree: slotData.worktree,
|
|
124
93
|
branch: slotData.branch,
|
|
94
|
+
createdAt: slotData.createdAt,
|
|
95
|
+
status: slotData.status,
|
|
125
96
|
};
|
|
97
|
+
if (slotData.failure)
|
|
98
|
+
updated.failure = slotData.failure;
|
|
126
99
|
if (input.newOwner !== undefined)
|
|
127
100
|
updated.owner = input.newOwner;
|
|
128
101
|
registry.slots[slotPort] = updated;
|
|
129
|
-
writeSlots(input.mainWorktree, registry);
|
|
102
|
+
writeSlots(input.mainWorktree, input.registryDir, registry);
|
|
130
103
|
return { slotPort, owner: input.newOwner };
|
|
131
104
|
}
|
|
105
|
+
function pickSlotPort(args, registry) {
|
|
106
|
+
const resolvedCurrent = resolve(args.currentWorktree);
|
|
107
|
+
if (args.slot !== undefined) {
|
|
108
|
+
const port = Number(args.slot);
|
|
109
|
+
if (!isValidPort(port, args.scheme)) {
|
|
110
|
+
console.error(`Error: Slot must be a valid port: ${allPorts(args.scheme).join(", ")}.`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const existing = registry.slots[String(port)];
|
|
114
|
+
if (existing && resolve(existing.worktree) !== resolvedCurrent) {
|
|
115
|
+
console.error(`Error: Slot ${port} is already taken by ${existing.worktree} (branch: ${existing.branch}).`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
return port;
|
|
119
|
+
}
|
|
120
|
+
const existingEntry = Object.entries(registry.slots).find(([, v]) => resolve(v.worktree) === resolvedCurrent);
|
|
121
|
+
if (existingEntry)
|
|
122
|
+
return Number(existingEntry[0]);
|
|
123
|
+
for (const port of allPorts(args.scheme)) {
|
|
124
|
+
if (!registry.slots[String(port)])
|
|
125
|
+
return port;
|
|
126
|
+
}
|
|
127
|
+
console.error("Error: All slots are taken. Remove a worktree with --remove first.");
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
function lookupSlotForCwd(registryDir) {
|
|
131
|
+
const cwd = resolve(process.cwd());
|
|
132
|
+
// Reads slots.json relative to cwd's shared-dir symlink (so works in linked worktrees too).
|
|
133
|
+
const filePath = join(registryDir, SLOTS_FILENAME);
|
|
134
|
+
if (!existsSync(filePath))
|
|
135
|
+
return undefined;
|
|
136
|
+
const registry = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
137
|
+
for (const [port, entry] of Object.entries(registry.slots)) {
|
|
138
|
+
if (resolve(entry.worktree) === cwd) {
|
|
139
|
+
return {
|
|
140
|
+
slot: Number(port),
|
|
141
|
+
worktree: entry.worktree,
|
|
142
|
+
branch: entry.branch,
|
|
143
|
+
owner: entry.owner,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
function synthesizeMainSlot(basePort) {
|
|
150
|
+
const gitCommonDir = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf-8" }).trim();
|
|
151
|
+
const mainWorktree = dirname(gitCommonDir);
|
|
152
|
+
const cwd = resolve(process.cwd());
|
|
153
|
+
if (resolve(mainWorktree) !== cwd)
|
|
154
|
+
return undefined;
|
|
155
|
+
const branch = execFileSync("git", ["branch", "--show-current"], { encoding: "utf-8" }).trim();
|
|
156
|
+
return { slot: basePort, worktree: cwd, branch };
|
|
157
|
+
}
|
package/dist/worktree.d.ts
CHANGED
|
@@ -7,15 +7,14 @@ export interface RunCtx {
|
|
|
7
7
|
verbose: boolean;
|
|
8
8
|
}
|
|
9
9
|
export declare function detectWorktree(): WorktreeContext;
|
|
10
|
-
export declare function computeWorktreePath(mainWorktree: string, branch: string): string;
|
|
11
|
-
export declare function branchExists(branch: string): boolean;
|
|
12
|
-
export declare function useExistingBranch(branch: string, ctx: WorktreeContext, run: RunCtx): WorktreeContext;
|
|
13
|
-
export declare function createBranch(requestedBranch: string, ctx: WorktreeContext, run: RunCtx): WorktreeContext;
|
|
14
|
-
export declare function verifyBranchAbsentFromRemote(branch: string, run: RunCtx): void;
|
|
15
|
-
export declare function getCurrentBranch(worktreePath: string): string;
|
|
16
10
|
export declare function enforceWorktreeMode(args: {
|
|
17
11
|
use?: string;
|
|
18
12
|
create?: string;
|
|
19
13
|
here?: boolean;
|
|
20
14
|
}, ctx: WorktreeContext): void;
|
|
15
|
+
export declare function useExistingBranch(branch: string, ctx: WorktreeContext, run: RunCtx): WorktreeContext;
|
|
16
|
+
export declare function createBranch(requestedBranch: string, ctx: WorktreeContext, run: RunCtx): WorktreeContext;
|
|
17
|
+
export declare function verifyBranchAbsentFromRemote(branch: string, run: RunCtx): void;
|
|
18
|
+
export declare function getCurrentBranch(worktreePath: string): string;
|
|
21
19
|
export declare function removeWorktree(worktreePath: string, run: RunCtx): void;
|
|
20
|
+
export declare function computeWorktreePath(mainWorktree: string, branch: string): string;
|
package/dist/worktree.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
-
function stdioFor(ctx) {
|
|
4
|
-
return ctx.verbose ? "inherit" : "pipe";
|
|
5
|
-
}
|
|
6
3
|
export function detectWorktree() {
|
|
7
4
|
const currentWorktree = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
8
5
|
encoding: "utf-8",
|
|
@@ -12,23 +9,17 @@ export function detectWorktree() {
|
|
|
12
9
|
const isMainWorktree = resolve(currentWorktree) === resolve(mainWorktree);
|
|
13
10
|
return { currentWorktree, mainWorktree, isMainWorktree };
|
|
14
11
|
}
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export function branchExists(branch) {
|
|
21
|
-
try {
|
|
22
|
-
execFileSync("git", ["rev-parse", "--verify", branch], { stdio: "pipe" });
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
try {
|
|
27
|
-
execFileSync("git", ["rev-parse", "--verify", `origin/${branch}`], { stdio: "pipe" });
|
|
28
|
-
return true;
|
|
12
|
+
export function enforceWorktreeMode(args, ctx) {
|
|
13
|
+
if (args.use || args.create) {
|
|
14
|
+
if (!ctx.isMainWorktree) {
|
|
15
|
+
console.error("Error: --use and --create must be run from the main worktree.");
|
|
16
|
+
process.exit(1);
|
|
29
17
|
}
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
}
|
|
19
|
+
else if (args.here) {
|
|
20
|
+
if (ctx.isMainWorktree) {
|
|
21
|
+
console.error("Error: --here must be run from a linked worktree, not from the main worktree.");
|
|
22
|
+
process.exit(1);
|
|
32
23
|
}
|
|
33
24
|
}
|
|
34
25
|
}
|
|
@@ -54,7 +45,6 @@ export function createBranch(requestedBranch, ctx, run) {
|
|
|
54
45
|
execFileSync("git", ["worktree", "add", "-b", finalBranch, worktreePath], {
|
|
55
46
|
stdio: stdioFor(run),
|
|
56
47
|
});
|
|
57
|
-
console.log(`Branch: ${finalBranch}`);
|
|
58
48
|
return { ...ctx, currentWorktree: worktreePath, isMainWorktree: false };
|
|
59
49
|
}
|
|
60
50
|
export function verifyBranchAbsentFromRemote(branch, run) {
|
|
@@ -73,20 +63,29 @@ export function getCurrentBranch(worktreePath) {
|
|
|
73
63
|
cwd: worktreePath,
|
|
74
64
|
}).trim();
|
|
75
65
|
}
|
|
76
|
-
export function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
66
|
+
export function removeWorktree(worktreePath, run) {
|
|
67
|
+
execFileSync("git", ["worktree", "remove", "--force", worktreePath], { stdio: stdioFor(run) });
|
|
68
|
+
}
|
|
69
|
+
export function computeWorktreePath(mainWorktree, branch) {
|
|
70
|
+
const repoName = basename(mainWorktree);
|
|
71
|
+
const sanitized = branch.replaceAll("/", "-");
|
|
72
|
+
return join(dirname(mainWorktree), `${repoName}-${sanitized}`);
|
|
73
|
+
}
|
|
74
|
+
function branchExists(branch) {
|
|
75
|
+
try {
|
|
76
|
+
execFileSync("git", ["rev-parse", "--verify", branch], { stdio: "pipe" });
|
|
77
|
+
return true;
|
|
82
78
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
catch {
|
|
80
|
+
try {
|
|
81
|
+
execFileSync("git", ["rev-parse", "--verify", `origin/${branch}`], { stdio: "pipe" });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
function stdioFor(ctx) {
|
|
90
|
+
return ctx.verbose ? "inherit" : "pipe";
|
|
92
91
|
}
|