@paleo/worktree-env 0.2.0 → 0.4.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 +9 -9
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +13 -13
- package/dist/dev-server.d.ts +21 -12
- package/dist/dev-server.js +6 -47
- package/dist/dev-servers-registry.d.ts +1 -1
- package/dist/dev-servers-registry.js +4 -2
- package/dist/helpers.d.ts +10 -0
- package/dist/helpers.js +49 -0
- package/dist/index.d.ts +1 -1
- package/dist/setup-worktree.d.ts +29 -4
- package/dist/setup-worktree.js +17 -21
- package/dist/slots.d.ts +5 -5
- package/dist/slots.js +14 -15
- package/dist/worktree.d.ts +1 -1
- package/dist/worktree.js +2 -2
- package/package.json +4 -2
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
|
-
|
|
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}
|
|
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
|
-
|
|
69
|
+
devLimit: 5,
|
|
69
70
|
servers: [
|
|
70
71
|
{
|
|
71
72
|
name: "dev",
|
|
72
|
-
command: "npm",
|
|
73
|
-
|
|
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.d.ts
CHANGED
|
@@ -2,11 +2,11 @@ export interface SetupArgs {
|
|
|
2
2
|
help?: boolean;
|
|
3
3
|
use?: string;
|
|
4
4
|
create?: string;
|
|
5
|
-
|
|
5
|
+
here?: boolean;
|
|
6
6
|
owner?: string;
|
|
7
7
|
"set-owner"?: string;
|
|
8
8
|
remove?: string;
|
|
9
|
-
"remove-
|
|
9
|
+
"remove-here"?: boolean;
|
|
10
10
|
"no-remote-check"?: boolean;
|
|
11
11
|
slot?: string;
|
|
12
12
|
force?: boolean;
|
package/dist/cli.js
CHANGED
|
@@ -12,14 +12,14 @@ const SETUP_OPTIONS = {
|
|
|
12
12
|
arg: "branch",
|
|
13
13
|
description: "Create a new branch + worktree, then set up the local environment. If the branch already exists, appends a numeric suffix (-2, -3, ...)",
|
|
14
14
|
},
|
|
15
|
-
|
|
15
|
+
here: {
|
|
16
16
|
type: "boolean",
|
|
17
17
|
description: "Set up the local environment in the current linked worktree",
|
|
18
18
|
},
|
|
19
19
|
owner: {
|
|
20
20
|
type: "string",
|
|
21
21
|
arg: "name",
|
|
22
|
-
description:
|
|
22
|
+
description: "Owner of the slot (free-form label, optional)",
|
|
23
23
|
},
|
|
24
24
|
"set-owner": {
|
|
25
25
|
type: "string",
|
|
@@ -31,13 +31,13 @@ const SETUP_OPTIONS = {
|
|
|
31
31
|
arg: "branch",
|
|
32
32
|
description: "Remove a worktree by branch name (stop dev server, free slot, delete directory)",
|
|
33
33
|
},
|
|
34
|
-
"remove-
|
|
34
|
+
"remove-here": {
|
|
35
35
|
type: "boolean",
|
|
36
36
|
description: "Remove the current linked worktree (same as --remove, but for the worktree you are in)",
|
|
37
37
|
},
|
|
38
38
|
"no-remote-check": {
|
|
39
39
|
type: "boolean",
|
|
40
|
-
description: "Skip remote branch verification when removing (use with --remove or --remove-
|
|
40
|
+
description: "Skip remote branch verification when removing (use with --remove or --remove-here)",
|
|
41
41
|
},
|
|
42
42
|
slot: {
|
|
43
43
|
type: "string",
|
|
@@ -87,10 +87,10 @@ export function printDevServerHelp() {
|
|
|
87
87
|
console.log(formatHelp("dev-server [options]", "Start, stop, or list background dev-server processes.", DEV_SERVER_OPTIONS));
|
|
88
88
|
}
|
|
89
89
|
export function isSetupMode(args) {
|
|
90
|
-
return args.use !== undefined || args.create !== undefined || Boolean(args.
|
|
90
|
+
return args.use !== undefined || args.create !== undefined || Boolean(args.here);
|
|
91
91
|
}
|
|
92
92
|
export function isRemoveMode(args) {
|
|
93
|
-
return args.remove !== undefined || Boolean(args["remove-
|
|
93
|
+
return args.remove !== undefined || Boolean(args["remove-here"]);
|
|
94
94
|
}
|
|
95
95
|
export function isSetOwnerMode(args) {
|
|
96
96
|
return args["set-owner"] !== undefined;
|
|
@@ -99,24 +99,24 @@ export function validateSetupFlags(args) {
|
|
|
99
99
|
const modeFlags = [
|
|
100
100
|
args.use,
|
|
101
101
|
args.create,
|
|
102
|
-
args.
|
|
102
|
+
args.here,
|
|
103
103
|
isRemoveMode(args),
|
|
104
104
|
isSetOwnerMode(args),
|
|
105
105
|
].filter(Boolean);
|
|
106
106
|
if (modeFlags.length > 1) {
|
|
107
|
-
throw new ConfigError("Error: --use, --create, --
|
|
107
|
+
throw new ConfigError("Error: --use, --create, --here, --remove, --remove-here, and --set-owner are mutually exclusive.");
|
|
108
108
|
}
|
|
109
|
-
if (args.remove !== undefined && args["remove-
|
|
110
|
-
throw new ConfigError("Error: --remove and --remove-
|
|
109
|
+
if (args.remove !== undefined && args["remove-here"]) {
|
|
110
|
+
throw new ConfigError("Error: --remove and --remove-here are mutually exclusive.");
|
|
111
111
|
}
|
|
112
112
|
if ((args.slot !== undefined || args.force) && !isSetupMode(args)) {
|
|
113
|
-
throw new ConfigError("Error: --slot and --force can only be used with --use, --create, or --
|
|
113
|
+
throw new ConfigError("Error: --slot and --force can only be used with --use, --create, or --here.");
|
|
114
114
|
}
|
|
115
115
|
if (args.owner !== undefined && !isSetupMode(args)) {
|
|
116
|
-
throw new ConfigError("Error: --owner is only valid with --use, --create, or --
|
|
116
|
+
throw new ConfigError("Error: --owner is only valid with --use, --create, or --here.");
|
|
117
117
|
}
|
|
118
118
|
if (args["no-remote-check"] && !isRemoveMode(args)) {
|
|
119
|
-
throw new ConfigError("Error: --no-remote-check is only valid with --remove or --remove-
|
|
119
|
+
throw new ConfigError("Error: --no-remote-check is only valid with --remove or --remove-here.");
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
export function validateDevServerFlags(args) {
|
package/dist/dev-server.d.ts
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
import { type ResolvedSlot } from "./slots.js";
|
|
2
|
-
|
|
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
|
-
/** Maximum concurrent dev-servers across all worktrees.
|
|
12
|
-
devLimit
|
|
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
|
-
|
|
20
|
-
|
|
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: {
|
package/dist/dev-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { closeSync,
|
|
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";
|
|
@@ -9,48 +9,6 @@ import { awaitAllReady, handleStartupFailure } from "./log-polling.js";
|
|
|
9
9
|
import { cleanupPidFile, isProcessAlive, readPid, stopByPidFile } from "./process-control.js";
|
|
10
10
|
import { resolveCurrentSlot } from "./slots.js";
|
|
11
11
|
import { detectWorktree } from "./worktree.js";
|
|
12
|
-
function readEnvFileVar(filePath, varName) {
|
|
13
|
-
const content = readFileSync(filePath, "utf-8");
|
|
14
|
-
const match = content.match(new RegExp(`^${varName}=(.+)`, "m"));
|
|
15
|
-
if (!match) {
|
|
16
|
-
console.error(`Error: ${varName} not found in ${filePath}.`);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
return match[1].trim();
|
|
20
|
-
}
|
|
21
|
-
function readJsonPath(filePath, jsonPath) {
|
|
22
|
-
const content = readFileSync(filePath, "utf-8");
|
|
23
|
-
const data = JSON.parse(content);
|
|
24
|
-
const segments = jsonPath.split(".");
|
|
25
|
-
let cur = data;
|
|
26
|
-
for (const seg of segments) {
|
|
27
|
-
if (cur === null || cur === undefined || typeof cur !== "object") {
|
|
28
|
-
console.error(`Error: ${jsonPath} not found in ${filePath}.`);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
cur = cur[seg];
|
|
32
|
-
}
|
|
33
|
-
if (cur === undefined || cur === null) {
|
|
34
|
-
console.error(`Error: ${jsonPath} not found in ${filePath}.`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
return String(cur);
|
|
38
|
-
}
|
|
39
|
-
function readPortFromConfig(portConfig) {
|
|
40
|
-
if (!existsSync(portConfig.file)) {
|
|
41
|
-
console.error(`Error: ${portConfig.file} not found. Run setup-worktree first.`);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
const raw = "var" in portConfig
|
|
45
|
-
? readEnvFileVar(portConfig.file, portConfig.var)
|
|
46
|
-
: readJsonPath(portConfig.file, portConfig.jsonPath);
|
|
47
|
-
const port = Number(raw);
|
|
48
|
-
if (!Number.isFinite(port)) {
|
|
49
|
-
console.error(`Error: invalid port "${raw}" in ${portConfig.file}.`);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
return port;
|
|
53
|
-
}
|
|
54
12
|
function isPortBusy(port) {
|
|
55
13
|
return new Promise((resolve) => {
|
|
56
14
|
const socket = createConnection({ port, host: "127.0.0.1" });
|
|
@@ -72,7 +30,7 @@ function spawnServer(server) {
|
|
|
72
30
|
mkdirSync(dirname(server.logFile), { recursive: true });
|
|
73
31
|
mkdirSync(dirname(server.pidFile), { recursive: true });
|
|
74
32
|
const logFd = openSync(server.logFile, "w");
|
|
75
|
-
const child = spawn(server.command, server.args, {
|
|
33
|
+
const child = spawn(server.exec.command, server.exec.args, {
|
|
76
34
|
detached: true,
|
|
77
35
|
stdio: ["ignore", logFd, logFd],
|
|
78
36
|
});
|
|
@@ -130,7 +88,7 @@ export async function runDevServer(config) {
|
|
|
130
88
|
async function start(config, mainWorktree) {
|
|
131
89
|
const limit = config.devLimit;
|
|
132
90
|
const active = pruneAndPersist(mainWorktree).servers;
|
|
133
|
-
if (limit
|
|
91
|
+
if (limit !== undefined && active.length >= limit) {
|
|
134
92
|
console.error(`Error: dev-server cap reached (${active.length}/${limit}). Active dev-servers:`);
|
|
135
93
|
printActiveServers(active);
|
|
136
94
|
console.error("Run `dev:down` in another worktree, or `dev:down --all`.");
|
|
@@ -138,7 +96,7 @@ async function start(config, mainWorktree) {
|
|
|
138
96
|
}
|
|
139
97
|
const serverPorts = config.servers.map((server) => [
|
|
140
98
|
server,
|
|
141
|
-
|
|
99
|
+
server.port,
|
|
142
100
|
]);
|
|
143
101
|
const busyResults = await Promise.all(serverPorts.map(([, port]) => isPortBusy(port)));
|
|
144
102
|
let anyBusy = false;
|
|
@@ -213,7 +171,8 @@ async function start(config, mainWorktree) {
|
|
|
213
171
|
}
|
|
214
172
|
function defaultPrintSummary(slot, servers, ports, pids) {
|
|
215
173
|
console.log("\nDev servers started!");
|
|
216
|
-
|
|
174
|
+
const ownerSuffix = slot.owner ? `, owner ${slot.owner}` : "";
|
|
175
|
+
console.log(` Worktree: slot ${slot.slot}${ownerSuffix}`);
|
|
217
176
|
servers.forEach((server, i) => {
|
|
218
177
|
const url = `http://localhost:${ports[i]}/`;
|
|
219
178
|
const logPath = join(process.cwd(), server.logFile);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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 };
|
package/dist/setup-worktree.d.ts
CHANGED
|
@@ -1,51 +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
|
|
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
|
|
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
|
-
|
|
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[];
|
|
59
|
+
/** Config files copied from the main worktree and patched per slot. */
|
|
44
60
|
configFiles: ConfigFileEntry[];
|
|
45
|
-
|
|
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. */
|
|
46
68
|
teardownInfrastructure?: (ctx: TeardownContext) => Promise<void> | void;
|
|
69
|
+
/** Runs after `setupWorktreeData`. Typically `npm install && npm run build`. */
|
|
47
70
|
installAndBuild: (ctx: SetupContext) => Promise<void> | void;
|
|
71
|
+
/** Runs after `installAndBuild`. Typically migrations and seeds. */
|
|
48
72
|
afterDatabase?: (ctx: SetupContext) => Promise<void> | void;
|
|
73
|
+
/** Builds the post-setup summary printed to stdout. */
|
|
49
74
|
printSummary: (ctx: SummaryContext) => string;
|
|
50
75
|
}
|
|
51
76
|
export declare function runSetupWorktree(config: SetupWorktreeConfig): Promise<void>;
|
package/dist/setup-worktree.js
CHANGED
|
@@ -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.
|
|
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);
|
|
@@ -154,8 +147,8 @@ function generateConfigFiles(ctx, entries, slot, ports, force, log) {
|
|
|
154
147
|
}), entry.path, force, entry.required ?? false);
|
|
155
148
|
}
|
|
156
149
|
}
|
|
157
|
-
function resolveRemoveTarget(args, ctx, registry,
|
|
158
|
-
if (
|
|
150
|
+
function resolveRemoveTarget(args, ctx, registry, removeHere) {
|
|
151
|
+
if (removeHere) {
|
|
159
152
|
if (ctx.isMainWorktree) {
|
|
160
153
|
console.error("Error: Cannot remove the main worktree.");
|
|
161
154
|
process.exit(1);
|
|
@@ -170,7 +163,7 @@ function resolveRemoveTarget(args, ctx, registry, removeSelf) {
|
|
|
170
163
|
slotPort: entry[0],
|
|
171
164
|
branch: entry[1].branch,
|
|
172
165
|
worktreePath: ctx.currentWorktree,
|
|
173
|
-
owner: entry[1].owner
|
|
166
|
+
owner: entry[1].owner,
|
|
174
167
|
};
|
|
175
168
|
}
|
|
176
169
|
const branch = args.remove ?? "";
|
|
@@ -181,10 +174,10 @@ function resolveRemoveTarget(args, ctx, registry, removeSelf) {
|
|
|
181
174
|
}
|
|
182
175
|
const worktreePath = entry[1].worktree;
|
|
183
176
|
if (resolve(ctx.currentWorktree) === resolve(worktreePath)) {
|
|
184
|
-
console.error("Error: You are currently in this worktree. Use --remove-
|
|
177
|
+
console.error("Error: You are currently in this worktree. Use --remove-here instead.");
|
|
185
178
|
process.exit(1);
|
|
186
179
|
}
|
|
187
|
-
return { slotPort: entry[0], branch, worktreePath, owner: entry[1].owner
|
|
180
|
+
return { slotPort: entry[0], branch, worktreePath, owner: entry[1].owner };
|
|
188
181
|
}
|
|
189
182
|
async function stopDevServerByPidFiles(worktreePath, pidFiles, log) {
|
|
190
183
|
for (const pidFileRel of pidFiles) {
|
|
@@ -215,13 +208,13 @@ async function stopDevServerByPidFiles(worktreePath, pidFiles, log) {
|
|
|
215
208
|
}
|
|
216
209
|
async function handleRemove(args, ctx, run, config) {
|
|
217
210
|
const log = makeLog(run.verbose);
|
|
218
|
-
const
|
|
211
|
+
const removeHere = Boolean(args["remove-here"]);
|
|
219
212
|
const registry = readSlots(ctx.mainWorktree);
|
|
220
|
-
const target = resolveRemoveTarget(args, ctx, registry,
|
|
213
|
+
const target = resolveRemoveTarget(args, ctx, registry, removeHere);
|
|
221
214
|
if (!args["no-remote-check"]) {
|
|
222
215
|
verifyBranchAbsentFromRemote(target.branch, run);
|
|
223
216
|
}
|
|
224
|
-
const ownerSuffix = target.owner
|
|
217
|
+
const ownerSuffix = target.owner ? `, owner ${target.owner}` : "";
|
|
225
218
|
if (!existsSync(target.worktreePath)) {
|
|
226
219
|
console.warn(`Warning: Worktree directory ${target.worktreePath} not found. Cleaning up registry only.`);
|
|
227
220
|
delete registry.slots[target.slotPort];
|
|
@@ -240,17 +233,17 @@ async function handleRemove(args, ctx, run, config) {
|
|
|
240
233
|
delete registry.slots[target.slotPort];
|
|
241
234
|
writeSlots(ctx.mainWorktree, registry);
|
|
242
235
|
removeDevServerEntryByWorktree(ctx.mainWorktree, target.worktreePath);
|
|
243
|
-
if (
|
|
236
|
+
if (removeHere) {
|
|
244
237
|
process.chdir(ctx.mainWorktree);
|
|
245
238
|
}
|
|
246
239
|
removeWorktree(target.worktreePath, run);
|
|
247
240
|
console.log(`Removed worktree for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}).`);
|
|
248
|
-
if (
|
|
241
|
+
if (removeHere) {
|
|
249
242
|
console.log(`Now run: cd ${ctx.mainWorktree}`);
|
|
250
243
|
}
|
|
251
244
|
}
|
|
252
245
|
function handleSetOwnerMode(args, ctx) {
|
|
253
|
-
const newOwner = args["set-owner"]
|
|
246
|
+
const newOwner = args["set-owner"];
|
|
254
247
|
const { slotPort } = handleSetOwner({
|
|
255
248
|
newOwner,
|
|
256
249
|
currentWorktree: ctx.currentWorktree,
|
|
@@ -265,7 +258,10 @@ function handleSetOwnerMode(args, ctx) {
|
|
|
265
258
|
const resolvedCurrent = resolve(ctx.currentWorktree);
|
|
266
259
|
for (const server of data.servers) {
|
|
267
260
|
if (resolve(server.worktree) === resolvedCurrent) {
|
|
268
|
-
|
|
261
|
+
if (newOwner !== undefined)
|
|
262
|
+
server.owner = newOwner;
|
|
263
|
+
else
|
|
264
|
+
delete server.owner;
|
|
269
265
|
changed = true;
|
|
270
266
|
}
|
|
271
267
|
}
|
|
@@ -274,5 +270,5 @@ function handleSetOwnerMode(args, ctx) {
|
|
|
274
270
|
writeFileSync(devServersPath, `${JSON.stringify(data, undefined, 2)}\n`);
|
|
275
271
|
}
|
|
276
272
|
}
|
|
277
|
-
console.log(`Owner for slot ${slotPort}: ${newOwner}`);
|
|
273
|
+
console.log(`Owner for slot ${slotPort}: ${newOwner ?? "(none)"}`);
|
|
278
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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/dist/worktree.d.ts
CHANGED
|
@@ -16,6 +16,6 @@ export declare function getCurrentBranch(worktreePath: string): string;
|
|
|
16
16
|
export declare function enforceWorktreeMode(args: {
|
|
17
17
|
use?: string;
|
|
18
18
|
create?: string;
|
|
19
|
-
|
|
19
|
+
here?: boolean;
|
|
20
20
|
}, ctx: WorktreeContext): void;
|
|
21
21
|
export declare function removeWorktree(worktreePath: string, run: RunCtx): void;
|
package/dist/worktree.js
CHANGED
|
@@ -80,9 +80,9 @@ export function enforceWorktreeMode(args, ctx) {
|
|
|
80
80
|
process.exit(1);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
else if (args.
|
|
83
|
+
else if (args.here) {
|
|
84
84
|
if (ctx.isMainWorktree) {
|
|
85
|
-
console.error("Error: --
|
|
85
|
+
console.error("Error: --here must be run from a linked worktree, not from the main worktree.");
|
|
86
86
|
process.exit(1);
|
|
87
87
|
}
|
|
88
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paleo/worktree-env",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|