@paleo/worktree-env 0.1.0 → 0.3.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 CHANGED
@@ -41,7 +41,6 @@ import { runSetupWorktree, helpers } from "@paleo/worktree-env";
41
41
  await runSetupWorktree({
42
42
  basePort: 8100,
43
43
  portNames: ["server", "frontend", "db"],
44
- devLimitEnvVar: "MYAPP_DEV_LIMIT",
45
44
  devServerPidFiles: [".local-data/dev-server.pid"],
46
45
  configFiles: [
47
46
  {
@@ -53,32 +52,33 @@ await runSetupWorktree({
53
52
  }),
54
53
  },
55
54
  ],
56
- provisionDatabase: async () => {},
55
+ setupWorktreeData: async ({ currentWorktree }) => {
56
+ // Create per-worktree directories, copy seed data, start containers, etc.
57
+ },
57
58
  installAndBuild: async () => {},
58
59
  printSummary: ({ slot, branch, owner, ports }) =>
59
- `Slot ${slot} (${branch}, ${owner}) — server :${ports.server}`,
60
+ `Slot ${slot} (${branch}${owner ? `, ${owner}` : ""}) — server :${ports.server}`,
60
61
  });
61
62
  ```
62
63
 
63
64
  ```ts
64
- import { runDevServer } from "@paleo/worktree-env";
65
+ import { runDevServer, helpers } from "@paleo/worktree-env";
65
66
 
66
67
  await runDevServer({
67
68
  basePort: 8100,
68
- devLimitEnvVar: "MYAPP_DEV_LIMIT",
69
+ devLimit: 5,
69
70
  servers: [
70
71
  {
71
72
  name: "dev",
72
- command: "npm",
73
- args: ["run", "dev"],
73
+ exec: { command: "npm", args: ["run", "dev"] },
74
+ port: helpers.readPortFromEnvFile(".env", "PORT"),
74
75
  pidFile: ".local-data/dev-server.pid",
75
76
  logFile: ".local-data/logs/dev-server.log",
76
77
  detectSuccess: (log) => log.includes("Server is ready on port"),
77
- portConfig: { file: ".env", var: "PORT" },
78
78
  },
79
79
  ],
80
80
  printSummary: ({ slot, servers }) =>
81
- `Dev servers started in slot ${slot.slot} (${slot.owner}): ${servers
81
+ `Dev servers started in slot ${slot.slot}${slot.owner ? ` (${slot.owner})` : ""}: ${servers
82
82
  .map((s) => `${s.server.name} :${s.port} (PID ${s.pid})`)
83
83
  .join(", ")}`,
84
84
  });
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ const SETUP_OPTIONS = {
19
19
  owner: {
20
20
  type: "string",
21
21
  arg: "name",
22
- description: 'Owner of the slot (free-form label, defaults to "default")',
22
+ description: "Owner of the slot (free-form label, optional)",
23
23
  },
24
24
  "set-owner": {
25
25
  type: "string",
@@ -1,29 +1,38 @@
1
1
  import { type ResolvedSlot } from "./slots.js";
2
- export type PortConfig = {
3
- file: string;
4
- var: string;
5
- } | {
6
- file: string;
7
- jsonPath: string;
8
- };
2
+ /** Configuration accepted by {@link runDevServer}. */
9
3
  export interface DevServerConfig {
4
+ /** Anchor port for the slot range. Used to synthesize the main worktree's slot. */
10
5
  basePort: number;
11
- devLimitEnvVar: string;
12
- defaultLimit?: number;
6
+ /** Maximum concurrent dev-servers across all worktrees. Omit for no limit. */
7
+ devLimit?: number;
8
+ /** One entry per process to spawn. Started in array order. */
13
9
  servers: ServerDescriptor[];
10
+ /** Hook invoked once before any dev-server is spawned (e.g. `docker compose up -d`). */
14
11
  ensureInfrastructure?: () => Promise<void> | void;
12
+ /** Builds the post-start summary printed to stdout. Defaults to a generic layout. */
15
13
  printSummary?: (ctx: DevServerSummaryContext) => string;
16
14
  }
15
+ /** Describes one process to spawn. */
17
16
  export interface ServerDescriptor {
17
+ /** Short label used in logs and the registry. */
18
18
  name: string;
19
- command: string;
20
- args: string[];
19
+ /** Command and arguments passed to `child_process.spawn`. */
20
+ exec: {
21
+ command: string;
22
+ args: string[];
23
+ };
24
+ /** Port the process will listen on. Use `helpers.readPortFromEnvFile` / `readPortFromJsonFile` to read it from a config file. */
25
+ port: number;
26
+ /** Path (relative to cwd) where the spawned PID is written. */
21
27
  pidFile: string;
28
+ /** Path (relative to cwd) where stdout+stderr are tee'd. */
22
29
  logFile: string;
30
+ /** Returns `true` once the log content indicates the server is ready. */
23
31
  detectSuccess: (logContent: string) => boolean;
32
+ /** Returns a non-empty marker string when the log content indicates a fatal error, or `false` otherwise. */
24
33
  detectError?: (logContent: string) => string | false;
25
- portConfig: PortConfig;
26
34
  }
35
+ /** Context passed to {@link DevServerConfig.printSummary}. */
27
36
  export interface DevServerSummaryContext {
28
37
  slot: ResolvedSlot;
29
38
  servers: {
@@ -1,57 +1,14 @@
1
1
  import { spawn } from "node:child_process";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { closeSync, mkdirSync, openSync, writeFileSync } from "node:fs";
3
3
  import { createConnection } from "node:net";
4
4
  import { dirname, join } from "node:path";
5
5
  import { parseDevServerArgs, printDevServerHelp, validateDevServerFlags, } from "./cli.js";
6
- import { readDevLimit } from "./dev-limit.js";
7
6
  import { listDevServers, printActiveServers, pruneAndPersist, registerDevServer, stopAllRegistered, unregisterDevServer, } from "./dev-servers-registry.js";
8
7
  import { ConfigError, StartupError } from "./errors.js";
9
8
  import { awaitAllReady, handleStartupFailure } from "./log-polling.js";
10
9
  import { cleanupPidFile, isProcessAlive, readPid, stopByPidFile } from "./process-control.js";
11
10
  import { resolveCurrentSlot } from "./slots.js";
12
11
  import { detectWorktree } from "./worktree.js";
13
- function readEnvFileVar(filePath, varName) {
14
- const content = readFileSync(filePath, "utf-8");
15
- const match = content.match(new RegExp(`^${varName}=(.+)`, "m"));
16
- if (!match) {
17
- console.error(`Error: ${varName} not found in ${filePath}.`);
18
- process.exit(1);
19
- }
20
- return match[1].trim();
21
- }
22
- function readJsonPath(filePath, jsonPath) {
23
- const content = readFileSync(filePath, "utf-8");
24
- const data = JSON.parse(content);
25
- const segments = jsonPath.split(".");
26
- let cur = data;
27
- for (const seg of segments) {
28
- if (cur === null || cur === undefined || typeof cur !== "object") {
29
- console.error(`Error: ${jsonPath} not found in ${filePath}.`);
30
- process.exit(1);
31
- }
32
- cur = cur[seg];
33
- }
34
- if (cur === undefined || cur === null) {
35
- console.error(`Error: ${jsonPath} not found in ${filePath}.`);
36
- process.exit(1);
37
- }
38
- return String(cur);
39
- }
40
- function readPortFromConfig(portConfig) {
41
- if (!existsSync(portConfig.file)) {
42
- console.error(`Error: ${portConfig.file} not found. Run setup-worktree first.`);
43
- process.exit(1);
44
- }
45
- const raw = "var" in portConfig
46
- ? readEnvFileVar(portConfig.file, portConfig.var)
47
- : readJsonPath(portConfig.file, portConfig.jsonPath);
48
- const port = Number(raw);
49
- if (!Number.isFinite(port)) {
50
- console.error(`Error: invalid port "${raw}" in ${portConfig.file}.`);
51
- process.exit(1);
52
- }
53
- return port;
54
- }
55
12
  function isPortBusy(port) {
56
13
  return new Promise((resolve) => {
57
14
  const socket = createConnection({ port, host: "127.0.0.1" });
@@ -73,7 +30,7 @@ function spawnServer(server) {
73
30
  mkdirSync(dirname(server.logFile), { recursive: true });
74
31
  mkdirSync(dirname(server.pidFile), { recursive: true });
75
32
  const logFd = openSync(server.logFile, "w");
76
- const child = spawn(server.command, server.args, {
33
+ const child = spawn(server.exec.command, server.exec.args, {
77
34
  detached: true,
78
35
  stdio: ["ignore", logFd, logFd],
79
36
  });
@@ -129,12 +86,9 @@ export async function runDevServer(config) {
129
86
  await start(config, mainWorktree);
130
87
  }
131
88
  async function start(config, mainWorktree) {
132
- const limit = readDevLimit({
133
- projectVar: config.devLimitEnvVar,
134
- defaultLimit: config.defaultLimit,
135
- });
89
+ const limit = config.devLimit;
136
90
  const active = pruneAndPersist(mainWorktree).servers;
137
- if (limit > 0 && active.length >= limit) {
91
+ if (limit !== undefined && active.length >= limit) {
138
92
  console.error(`Error: dev-server cap reached (${active.length}/${limit}). Active dev-servers:`);
139
93
  printActiveServers(active);
140
94
  console.error("Run `dev:down` in another worktree, or `dev:down --all`.");
@@ -142,7 +96,7 @@ async function start(config, mainWorktree) {
142
96
  }
143
97
  const serverPorts = config.servers.map((server) => [
144
98
  server,
145
- readPortFromConfig(server.portConfig),
99
+ server.port,
146
100
  ]);
147
101
  const busyResults = await Promise.all(serverPorts.map(([, port]) => isPortBusy(port)));
148
102
  let anyBusy = false;
@@ -217,7 +171,8 @@ async function start(config, mainWorktree) {
217
171
  }
218
172
  function defaultPrintSummary(slot, servers, ports, pids) {
219
173
  console.log("\nDev servers started!");
220
- console.log(` Worktree: slot ${slot.slot}, owner ${slot.owner}`);
174
+ const ownerSuffix = slot.owner ? `, owner ${slot.owner}` : "";
175
+ console.log(` Worktree: slot ${slot.slot}${ownerSuffix}`);
221
176
  servers.forEach((server, i) => {
222
177
  const url = `http://localhost:${ports[i]}/`;
223
178
  const logPath = join(process.cwd(), server.logFile);
@@ -4,7 +4,7 @@ export interface DevServerEntry {
4
4
  slot: number;
5
5
  worktree: string;
6
6
  branch: string;
7
- owner: string;
7
+ owner?: string;
8
8
  pids: Record<string, number>;
9
9
  startedAt: string;
10
10
  }
@@ -60,7 +60,8 @@ function formatEntry(entry) {
60
60
  const pids = Object.entries(entry.pids)
61
61
  .map(([name, pid]) => `${name}=${pid}`)
62
62
  .join(",");
63
- return ` slot ${entry.slot} branch=${entry.branch} owner=${entry.owner} pids=${pids} startedAt=${entry.startedAt} worktree=${entry.worktree}`;
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}`;
64
65
  }
65
66
  export function printActiveServers(active) {
66
67
  const sorted = [...active].sort((a, b) => a.slot - b.slot);
@@ -86,7 +87,8 @@ export async function stopAllRegistered(input) {
86
87
  return;
87
88
  }
88
89
  for (const entry of data.servers) {
89
- console.log(`Stopping slot ${entry.slot} (${entry.branch}, owner=${entry.owner})...`);
90
+ const ownerSuffix = entry.owner ? `, owner=${entry.owner}` : "";
91
+ console.log(`Stopping slot ${entry.slot} (${entry.branch}${ownerSuffix})...`);
90
92
  for (const [name, pid] of Object.entries(entry.pids)) {
91
93
  if (!isProcessAlive(pid))
92
94
  continue;
package/dist/helpers.d.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  export declare function patchEnvFile(content: string, patches: Record<string, string>): string;
2
2
  export declare function extractHost(content: string, key: string, fallback?: string): string;
3
+ /**
4
+ * Reads `<varName>=<value>` from a dotenv-style file and parses it as a port.
5
+ * Exits with code 1 on missing file, missing variable, or non-numeric value.
6
+ */
7
+ export declare function readPortFromEnvFile(file: string, varName: string): number;
8
+ /**
9
+ * Reads a dotted path (e.g. `server.port`) from a JSON file and parses it as a port.
10
+ * Exits with code 1 on missing file, missing path, or non-numeric value.
11
+ */
12
+ export declare function readPortFromJsonFile(file: string, jsonPath: string): number;
3
13
  export interface CopyAndPatchCtx {
4
14
  currentWorktree: string;
5
15
  mainWorktree: string;
package/dist/helpers.js CHANGED
@@ -18,6 +18,55 @@ export function extractHost(content, key, fallback = "localhost") {
18
18
  const m = content.match(re);
19
19
  return m ? m[1] : fallback;
20
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 setup-worktree 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 setup-worktree 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
+ 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
+ }
21
70
  export function copyAndPatchFile(ctx, relPath, patchFn, label, force, required = false) {
22
71
  const targetPath = join(ctx.currentWorktree, relPath);
23
72
  const sourcePath = join(ctx.mainWorktree, relPath);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { runSetupWorktree } from "./setup-worktree.js";
2
2
  export type { SetupWorktreeConfig, SetupContext, SummaryContext, PatchContext, ConfigFileEntry, TeardownContext, } from "./setup-worktree.js";
3
3
  export { runDevServer } from "./dev-server.js";
4
- export type { DevServerConfig, DevServerSummaryContext, ServerDescriptor, PortConfig, } from "./dev-server.js";
4
+ export type { DevServerConfig, DevServerSummaryContext, ServerDescriptor, } from "./dev-server.js";
5
5
  export type { ResolvedSlot } from "./slots.js";
6
6
  import * as helpers from "./helpers.js";
7
7
  export { helpers };
@@ -1,52 +1,76 @@
1
+ /** Context passed to setup-time hooks (`setupWorktreeData`, `installAndBuild`, `afterDatabase`). */
1
2
  export interface SetupContext {
2
3
  currentWorktree: string;
3
4
  mainWorktree: string;
4
5
  slot: number;
5
6
  branch: string;
6
- owner: string;
7
+ owner?: string;
7
8
  ports: Record<string, number>;
8
9
  force: boolean;
9
10
  verbose: boolean;
10
11
  }
12
+ /** Context passed to {@link ConfigFileEntry.patch}. */
11
13
  export interface PatchContext {
12
14
  slot: number;
13
15
  ports: Record<string, number>;
14
16
  mainWorktree: string;
15
17
  currentWorktree: string;
16
18
  }
19
+ /** One config file copied from the main worktree and patched per slot. */
17
20
  export interface ConfigFileEntry {
21
+ /** Path relative to the worktree root. Same path is read from main and written to current. */
18
22
  path: string;
23
+ /** Returns the patched content given the source content and the slot's ports. */
19
24
  patch: (content: string, ctx: PatchContext) => string;
25
+ /** When `true`, abort if the source file is missing in the main worktree. Defaults to `false`. */
20
26
  required?: boolean;
21
27
  }
28
+ /** Context passed to {@link SetupWorktreeConfig.printSummary}. */
22
29
  export interface SummaryContext {
23
30
  slot: number;
24
31
  branch: string;
25
- owner: string;
32
+ owner?: string;
26
33
  ports: Record<string, number>;
27
34
  currentWorktree: string;
28
35
  mainWorktree: string;
29
36
  }
37
+ /** Context passed to {@link SetupWorktreeConfig.teardownInfrastructure}. */
30
38
  export interface TeardownContext {
31
39
  worktree: string;
32
40
  mainWorktree: string;
33
41
  verbose: boolean;
34
42
  }
43
+ /** Configuration accepted by {@link runSetupWorktree}. */
35
44
  export interface SetupWorktreeConfig {
45
+ /** Anchor port for the slot range. Slots are derived from this value. */
36
46
  basePort: number;
47
+ /** Distance between consecutive slots. Defaults to `10`. */
37
48
  portStep?: number;
49
+ /** Maximum number of slots. Defaults to `9`. */
38
50
  maxSlotCount?: number;
51
+ /** Custom port computation; takes precedence over `portNames`. */
39
52
  ports?: (slot: number) => Record<string, number>;
53
+ /** Named offsets `[name0, name1, ...]` mapped to `slot+0`, `slot+1`, ... Required if `ports` is omitted. */
40
54
  portNames?: string[];
41
- perWorktreeDirs?: string[];
55
+ /** Directories symlinked from the main worktree. Defaults to `[".local", ".plans"]`. */
42
56
  sharedDirs?: string[];
57
+ /** PID files written by `dev-server`, used by `--remove` to stop processes before teardown. */
43
58
  devServerPidFiles: string[];
44
- devLimitEnvVar?: string;
59
+ /** Config files copied from the main worktree and patched per slot. */
45
60
  configFiles: ConfigFileEntry[];
46
- provisionDatabase: (ctx: SetupContext) => Promise<void> | void;
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. */
47
68
  teardownInfrastructure?: (ctx: TeardownContext) => Promise<void> | void;
69
+ /** Runs after `setupWorktreeData`. Typically `npm install && npm run build`. */
48
70
  installAndBuild: (ctx: SetupContext) => Promise<void> | void;
71
+ /** Runs after `installAndBuild`. Typically migrations and seeds. */
49
72
  afterDatabase?: (ctx: SetupContext) => Promise<void> | void;
73
+ /** Builds the post-setup summary printed to stdout. */
50
74
  printSummary: (ctx: SummaryContext) => string;
51
75
  }
52
76
  export declare function runSetupWorktree(config: SetupWorktreeConfig): Promise<void>;
@@ -87,8 +87,6 @@ async function runSetup(args, ctx, run, config) {
87
87
  .map(([k, v]) => `${k}: ${v}`)
88
88
  .join(", ")})`);
89
89
  const sharedDirs = config.sharedDirs ?? [".local", ".plans"];
90
- const perWorktreeDirs = config.perWorktreeDirs ?? [".local-data"];
91
- setupLocalDirectories(setupCtx.currentWorktree, perWorktreeDirs);
92
90
  linkSharedDirectories(setupCtx, sharedDirs, log);
93
91
  generateConfigFiles(setupCtx, config.configFiles, slot, ports, args.force ?? false, log);
94
92
  const force = args.force ?? false;
@@ -102,7 +100,7 @@ async function runSetup(args, ctx, run, config) {
102
100
  force,
103
101
  verbose: run.verbose,
104
102
  };
105
- await config.provisionDatabase(setupContext);
103
+ await config.setupWorktreeData(setupContext);
106
104
  await config.installAndBuild(setupContext);
107
105
  if (config.afterDatabase)
108
106
  await config.afterDatabase(setupContext);
@@ -122,11 +120,6 @@ function ensureWorktree(args, ctx, run) {
122
120
  return createBranch(args.create, ctx, run);
123
121
  return ctx;
124
122
  }
125
- function setupLocalDirectories(worktreePath, dirs) {
126
- for (const dir of dirs) {
127
- mkdirSync(join(worktreePath, dir), { recursive: true });
128
- }
129
- }
130
123
  function linkSharedDirectories(ctx, dirs, log) {
131
124
  for (const dirName of dirs) {
132
125
  const link = join(ctx.currentWorktree, dirName);
@@ -166,7 +159,12 @@ function resolveRemoveTarget(args, ctx, registry, removeSelf) {
166
159
  console.error("Error: No slot found for this worktree in the registry.");
167
160
  process.exit(1);
168
161
  }
169
- return { slotPort: entry[0], branch: entry[1].branch, worktreePath: ctx.currentWorktree };
162
+ return {
163
+ slotPort: entry[0],
164
+ branch: entry[1].branch,
165
+ worktreePath: ctx.currentWorktree,
166
+ owner: entry[1].owner,
167
+ };
170
168
  }
171
169
  const branch = args.remove ?? "";
172
170
  const entry = Object.entries(registry.slots).find(([, v]) => v.branch === branch);
@@ -179,7 +177,7 @@ function resolveRemoveTarget(args, ctx, registry, removeSelf) {
179
177
  console.error("Error: You are currently in this worktree. Use --remove-self instead.");
180
178
  process.exit(1);
181
179
  }
182
- return { slotPort: entry[0], branch, worktreePath };
180
+ return { slotPort: entry[0], branch, worktreePath, owner: entry[1].owner };
183
181
  }
184
182
  async function stopDevServerByPidFiles(worktreePath, pidFiles, log) {
185
183
  for (const pidFileRel of pidFiles) {
@@ -216,11 +214,12 @@ async function handleRemove(args, ctx, run, config) {
216
214
  if (!args["no-remote-check"]) {
217
215
  verifyBranchAbsentFromRemote(target.branch, run);
218
216
  }
217
+ const ownerSuffix = target.owner ? `, owner ${target.owner}` : "";
219
218
  if (!existsSync(target.worktreePath)) {
220
219
  console.warn(`Warning: Worktree directory ${target.worktreePath} not found. Cleaning up registry only.`);
221
220
  delete registry.slots[target.slotPort];
222
221
  writeSlots(ctx.mainWorktree, registry);
223
- console.log(`Removed registry entry for branch "${target.branch}" (slot ${target.slotPort}).`);
222
+ console.log(`Removed registry entry for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}).`);
224
223
  return;
225
224
  }
226
225
  await stopDevServerByPidFiles(target.worktreePath, config.devServerPidFiles, log);
@@ -238,13 +237,13 @@ async function handleRemove(args, ctx, run, config) {
238
237
  process.chdir(ctx.mainWorktree);
239
238
  }
240
239
  removeWorktree(target.worktreePath, run);
241
- console.log(`Removed worktree for branch "${target.branch}" (slot ${target.slotPort}).`);
240
+ console.log(`Removed worktree for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}).`);
242
241
  if (removeSelf) {
243
242
  console.log(`Now run: cd ${ctx.mainWorktree}`);
244
243
  }
245
244
  }
246
245
  function handleSetOwnerMode(args, ctx) {
247
- const newOwner = args["set-owner"] ?? "default";
246
+ const newOwner = args["set-owner"];
248
247
  const { slotPort } = handleSetOwner({
249
248
  newOwner,
250
249
  currentWorktree: ctx.currentWorktree,
@@ -259,7 +258,10 @@ function handleSetOwnerMode(args, ctx) {
259
258
  const resolvedCurrent = resolve(ctx.currentWorktree);
260
259
  for (const server of data.servers) {
261
260
  if (resolve(server.worktree) === resolvedCurrent) {
262
- server.owner = newOwner;
261
+ if (newOwner !== undefined)
262
+ server.owner = newOwner;
263
+ else
264
+ delete server.owner;
263
265
  changed = true;
264
266
  }
265
267
  }
@@ -268,5 +270,5 @@ function handleSetOwnerMode(args, ctx) {
268
270
  writeFileSync(devServersPath, `${JSON.stringify(data, undefined, 2)}\n`);
269
271
  }
270
272
  }
271
- console.log(`Owner for slot ${slotPort}: ${newOwner}`);
273
+ console.log(`Owner for slot ${slotPort}: ${newOwner ?? "(none)"}`);
272
274
  }
package/dist/slots.d.ts CHANGED
@@ -4,7 +4,7 @@ export declare const SLOTS_FILE = ".local/worktrees/slots.json";
4
4
  export interface SlotEntry {
5
5
  worktree: string;
6
6
  branch: string;
7
- owner: string;
7
+ owner?: string;
8
8
  }
9
9
  export interface SlotsRegistry {
10
10
  slots: Record<string, SlotEntry>;
@@ -13,7 +13,7 @@ export interface ResolvedSlot {
13
13
  slot: number;
14
14
  worktree: string;
15
15
  branch: string;
16
- owner: string;
16
+ owner?: string;
17
17
  }
18
18
  export declare function readSlots(mainWorktree: string): SlotsRegistry;
19
19
  export declare function writeSlots(mainWorktree: string, registry: SlotsRegistry): void;
@@ -34,7 +34,7 @@ export interface RegisterSlotInput {
34
34
  }
35
35
  export declare function resolveAndRegisterSlot(input: RegisterSlotInput): {
36
36
  port: number;
37
- owner: string;
37
+ owner: string | undefined;
38
38
  };
39
39
  export declare function validateSlotAvailability(slotArg: string | undefined, ctx: {
40
40
  currentWorktree: string;
@@ -45,12 +45,12 @@ export declare function lookupSlotForCwd(): ResolvedSlot | undefined;
45
45
  export declare function synthesizeMainSlot(basePort: number): ResolvedSlot | undefined;
46
46
  export declare function resolveCurrentSlot(basePort: number): ResolvedSlot;
47
47
  export interface SetOwnerInput {
48
- newOwner: string;
48
+ newOwner: string | undefined;
49
49
  currentWorktree: string;
50
50
  mainWorktree: string;
51
51
  isMainWorktree: boolean;
52
52
  }
53
53
  export declare function handleSetOwner(input: SetOwnerInput): {
54
54
  slotPort: string;
55
- owner: string;
55
+ owner: string | undefined;
56
56
  };
package/dist/slots.js CHANGED
@@ -44,21 +44,14 @@ export function resolveAndRegisterSlot(input) {
44
44
  const registry = readSlots(input.mainWorktree);
45
45
  const port = pickSlotPort(input, registry);
46
46
  const existing = registry.slots[String(port)];
47
- let owner;
48
- if (input.requestedOwner !== undefined) {
49
- owner = input.requestedOwner;
50
- }
51
- else if (existing && existing.owner !== undefined) {
52
- owner = existing.owner;
53
- }
54
- else {
55
- owner = "default";
56
- }
57
- registry.slots[String(port)] = {
47
+ const owner = input.requestedOwner ?? existing?.owner;
48
+ const entry = {
58
49
  worktree: input.currentWorktree,
59
50
  branch: input.branch,
60
- owner,
61
51
  };
52
+ if (owner !== undefined)
53
+ entry.owner = owner;
54
+ registry.slots[String(port)] = entry;
62
55
  writeSlots(input.mainWorktree, registry);
63
56
  return { port, owner };
64
57
  }
@@ -90,7 +83,7 @@ export function lookupSlotForCwd() {
90
83
  slot: Number(port),
91
84
  worktree: entry.worktree,
92
85
  branch: entry.branch,
93
- owner: entry.owner ?? "default",
86
+ owner: entry.owner,
94
87
  };
95
88
  }
96
89
  }
@@ -103,7 +96,7 @@ export function synthesizeMainSlot(basePort) {
103
96
  if (resolve(mainWorktree) !== cwd)
104
97
  return undefined;
105
98
  const branch = execFileSync("git", ["branch", "--show-current"], { encoding: "utf-8" }).trim();
106
- return { slot: basePort, worktree: cwd, branch, owner: "default" };
99
+ return { slot: basePort, worktree: cwd, branch };
107
100
  }
108
101
  export function resolveCurrentSlot(basePort) {
109
102
  const slot = lookupSlotForCwd() ?? synthesizeMainSlot(basePort);
@@ -126,7 +119,13 @@ export function handleSetOwner(input) {
126
119
  process.exit(1);
127
120
  }
128
121
  const [slotPort, slotData] = entry;
129
- registry.slots[slotPort] = { ...slotData, owner: input.newOwner };
122
+ const updated = {
123
+ worktree: slotData.worktree,
124
+ branch: slotData.branch,
125
+ };
126
+ if (input.newOwner !== undefined)
127
+ updated.owner = input.newOwner;
128
+ registry.slots[slotPort] = updated;
130
129
  writeSlots(input.mainWorktree, registry);
131
130
  return { slotPort, owner: input.newOwner };
132
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paleo/worktree-env",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Worktree-based concurrent local environment kernel.",
5
5
  "keywords": [
6
6
  "worktree",
@@ -33,8 +33,10 @@
33
33
  "scripts": {
34
34
  "build": "tsc",
35
35
  "clear": "rimraf dist/*",
36
+ "lint": "biome check",
36
37
  "test": "vitest run",
37
- "test:watch": "vitest"
38
+ "test:watch": "vitest",
39
+ "prepublishOnly": "npm run clear && npm run build && npm run lint && npm run test"
38
40
  },
39
41
  "devDependencies": {
40
42
  "@types/node": "~24.12.3",
@@ -1,6 +0,0 @@
1
- export interface ReadDevLimitOptions {
2
- projectVar: string;
3
- defaultLimit?: number;
4
- env?: NodeJS.ProcessEnv;
5
- }
6
- export declare function readDevLimit(opts: ReadDevLimitOptions): number;
package/dist/dev-limit.js DELETED
@@ -1,13 +0,0 @@
1
- export function readDevLimit(opts) {
2
- const env = opts.env ?? process.env;
3
- const defaultLimit = opts.defaultLimit ?? 5;
4
- const candidates = [env[opts.projectVar], env.PROJECT_DEV_LIMIT];
5
- for (const raw of candidates) {
6
- if (raw === undefined || raw === "")
7
- continue;
8
- const parsed = Number(raw);
9
- if (Number.isInteger(parsed) && parsed >= 0)
10
- return parsed;
11
- }
12
- return defaultLimit;
13
- }