@paleo/worktree-env 0.4.1 → 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/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
- export const WORKTREES_DIR = ".local/worktrees";
6
- export const SLOTS_FILE = ".local/worktrees/slots.json";
7
- export function readSlots(mainWorktree) {
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, SLOTS_FILE);
15
- mkdirSync(join(mainWorktree, WORKTREES_DIR), { recursive: true });
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 lookupSlotForCwd() {
74
- const cwd = resolve(process.cwd());
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
+ }
@@ -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 computeWorktreePath(mainWorktree, branch) {
16
- const repoName = basename(mainWorktree);
17
- const sanitized = branch.replaceAll("/", "-");
18
- return join(dirname(mainWorktree), `${repoName}-${sanitized}`);
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
- catch {
31
- return false;
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
  }
@@ -72,20 +63,29 @@ export function getCurrentBranch(worktreePath) {
72
63
  cwd: worktreePath,
73
64
  }).trim();
74
65
  }
75
- export function enforceWorktreeMode(args, ctx) {
76
- if (args.use || args.create) {
77
- if (!ctx.isMainWorktree) {
78
- console.error("Error: --use and --create must be run from the main worktree.");
79
- process.exit(1);
80
- }
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;
81
78
  }
82
- else if (args.here) {
83
- if (ctx.isMainWorktree) {
84
- console.error("Error: --here must be run from a linked worktree, not from the main worktree.");
85
- process.exit(1);
79
+ catch {
80
+ try {
81
+ execFileSync("git", ["rev-parse", "--verify", `origin/${branch}`], { stdio: "pipe" });
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
86
  }
87
87
  }
88
88
  }
89
- export function removeWorktree(worktreePath, run) {
90
- execFileSync("git", ["worktree", "remove", "--force", worktreePath], { stdio: stdioFor(run) });
89
+ function stdioFor(ctx) {
90
+ return ctx.verbose ? "inherit" : "pipe";
91
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paleo/worktree-env",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Worktree-based concurrent local environment kernel.",
5
5
  "keywords": [
6
6
  "worktree",