@paleo/worktree-env 0.6.0 → 0.6.2
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 +4 -0
- package/dist/cli.js +4 -4
- package/dist/dev-server.js +3 -1
- package/dist/dev-servers-registry.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/setup-worktree.d.ts +20 -8
- package/dist/setup-worktree.js +13 -6
- package/dist/worktree.d.ts +14 -3
- package/dist/worktree.js +37 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,9 +37,12 @@ npm run setup-worktree -- --remove feat/42 # full teardown
|
|
|
37
37
|
## API
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
40
41
|
import { runSetupWorktree, helpers } from "@paleo/worktree-env";
|
|
41
42
|
|
|
42
43
|
await runSetupWorktree({
|
|
44
|
+
scriptPath: fileURLToPath(import.meta.url),
|
|
45
|
+
devServerScript: fileURLToPath(new URL("./dev-server.mjs", import.meta.url)),
|
|
43
46
|
basePort: 8100,
|
|
44
47
|
portNames: ["server", "frontend", "db"],
|
|
45
48
|
sharedDirs: [".local", ".plans"],
|
|
@@ -77,6 +80,7 @@ await runDevServer({
|
|
|
77
80
|
devLimit: 5,
|
|
78
81
|
servers: [
|
|
79
82
|
{
|
|
83
|
+
kind: "spawn",
|
|
80
84
|
name: "dev",
|
|
81
85
|
exec: { command: "npm", args: ["run", "dev"] },
|
|
82
86
|
port: helpers.readPortFromEnvFile(".env", "PORT"),
|
package/dist/cli.js
CHANGED
|
@@ -2,15 +2,15 @@ import { parseArgs } from "node:util";
|
|
|
2
2
|
import { ConfigError } from "./errors.js";
|
|
3
3
|
const SETUP_OPTIONS = {
|
|
4
4
|
help: { type: "boolean", short: "h", description: "Show this help message" },
|
|
5
|
-
|
|
5
|
+
create: {
|
|
6
6
|
type: "string",
|
|
7
7
|
arg: "branch",
|
|
8
|
-
description: "Create a
|
|
8
|
+
description: "Create a new branch + worktree, then set up the local environment. If the branch already exists, appends a numeric suffix (-2, -3, ...)",
|
|
9
9
|
},
|
|
10
|
-
|
|
10
|
+
use: {
|
|
11
11
|
type: "string",
|
|
12
12
|
arg: "branch",
|
|
13
|
-
description: "Create a
|
|
13
|
+
description: "Create a worktree for an existing branch, then set up the local environment",
|
|
14
14
|
},
|
|
15
15
|
here: {
|
|
16
16
|
type: "boolean",
|
package/dist/dev-server.js
CHANGED
|
@@ -60,8 +60,8 @@ function callbackServersOf(config) {
|
|
|
60
60
|
async function start(config, mainWorktree, { evict }) {
|
|
61
61
|
const ctx = { cwd: process.cwd() };
|
|
62
62
|
await enforceCap(config, mainWorktree, evict);
|
|
63
|
-
await checkPortsFree(config.servers);
|
|
64
63
|
checkNoLocalRegistryConflict(config, mainWorktree, ctx.cwd);
|
|
64
|
+
await checkPortsFree(config.servers);
|
|
65
65
|
const spawnPids = {};
|
|
66
66
|
const startedCallbacks = [];
|
|
67
67
|
try {
|
|
@@ -126,6 +126,7 @@ async function rollbackStart(spawnPids, startedCallbacks, ctx) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
for (const server of [...startedCallbacks].reverse()) {
|
|
129
|
+
console.log(`Stopping ${server.name}...`);
|
|
129
130
|
try {
|
|
130
131
|
await server.stop(ctx);
|
|
131
132
|
}
|
|
@@ -205,6 +206,7 @@ async function stopLocal(config, mainWorktree) {
|
|
|
205
206
|
}
|
|
206
207
|
const callbacks = callbackServersOf(config);
|
|
207
208
|
for (const server of [...callbacks].reverse()) {
|
|
209
|
+
console.log(`Stopping ${server.name}...`);
|
|
208
210
|
try {
|
|
209
211
|
await server.stop(ctx);
|
|
210
212
|
}
|
|
@@ -59,6 +59,7 @@ export async function evictOldest(input) {
|
|
|
59
59
|
}
|
|
60
60
|
async function stopCallbacksForVictim(callbackServers, worktree) {
|
|
61
61
|
for (const server of [...callbackServers].reverse()) {
|
|
62
|
+
console.log(` ${server.name} (callback)`);
|
|
62
63
|
try {
|
|
63
64
|
await server.stop({ cwd: worktree });
|
|
64
65
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { runSetupWorktree } from "./setup-worktree.js";
|
|
2
|
+
export { defaultWorktreeDirName } from "./worktree.js";
|
|
3
|
+
export type { WorktreeDirNameFn } from "./worktree.js";
|
|
2
4
|
export type { SetupWorktreeConfig, SetupContext, SummaryContext, PatchContext, ConfigFileEntry, PurgeContext, } from "./setup-worktree.js";
|
|
3
5
|
export { runDevServer } from "./dev-server.js";
|
|
4
6
|
export type { DevServerConfig, DevServerSummaryContext, ServerDescriptor, ServerContext, SpawnServer, CallbackServer, } from "./dev-server.js";
|
package/dist/index.js
CHANGED
package/dist/setup-worktree.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WorktreeDirNameFn } from "./worktree.js";
|
|
1
2
|
/** Configuration accepted by {@link runSetupWorktree}. */
|
|
2
3
|
export interface SetupWorktreeConfig {
|
|
3
4
|
/**
|
|
@@ -6,6 +7,13 @@ export interface SetupWorktreeConfig {
|
|
|
6
7
|
* — typically `fileURLToPath(import.meta.url)` from your `setup-worktree.mjs`.
|
|
7
8
|
*/
|
|
8
9
|
scriptPath: string;
|
|
10
|
+
/**
|
|
11
|
+
* Absolute path to your dev-server script (the file that calls `runDevServer`). On `--remove`,
|
|
12
|
+
* the kernel shells out to `node <devServerScript> --stop` with `cwd: <target worktree>`.
|
|
13
|
+
* Typically `fileURLToPath(new URL('./dev-server.mjs', import.meta.url))` from your
|
|
14
|
+
* `setup-worktree.mjs`.
|
|
15
|
+
*/
|
|
16
|
+
devServerScript: string;
|
|
9
17
|
/** Anchor port for the slot range. Slots are derived from this value. */
|
|
10
18
|
basePort: number;
|
|
11
19
|
/** Distance between consecutive slots. Defaults to `10`. */
|
|
@@ -38,13 +46,6 @@ export interface SetupWorktreeConfig {
|
|
|
38
46
|
* installed deps, etc.).
|
|
39
47
|
*/
|
|
40
48
|
finalizeWorktree: (ctx: SetupContext) => Promise<void> | void;
|
|
41
|
-
/**
|
|
42
|
-
* Absolute path to your dev-server script (the file that calls `runDevServer`). On `--remove`,
|
|
43
|
-
* the kernel shells out to `node <devServerScript> --stop` with `cwd: <target worktree>`.
|
|
44
|
-
* Typically `fileURLToPath(new URL('./dev-server.mjs', import.meta.url))` from your
|
|
45
|
-
* `setup-worktree.mjs`.
|
|
46
|
-
*/
|
|
47
|
-
devServerScript: string;
|
|
48
49
|
/**
|
|
49
50
|
* Destructive infrastructure teardown on `--remove` (e.g. `docker compose down -v` to wipe
|
|
50
51
|
* volumes). Runs after the dev-server stop. Best-effort; errors should be swallowed.
|
|
@@ -52,6 +53,13 @@ export interface SetupWorktreeConfig {
|
|
|
52
53
|
purgeInfrastructure?: (ctx: PurgeContext) => Promise<void> | void;
|
|
53
54
|
/** Builds the post-setup summary printed to stdout. */
|
|
54
55
|
printSummary: (ctx: SummaryContext) => string;
|
|
56
|
+
/**
|
|
57
|
+
* Optional override for the worktree directory basename. Receives `{ branch, repoName }` and
|
|
58
|
+
* returns the basename (e.g. `myrepo-feat-ABC-123`). Defaults to {@link defaultWorktreeDirName},
|
|
59
|
+
* which strips a recognizable ticket suffix and caps the slug at 22 chars. The kernel handles
|
|
60
|
+
* deduplication (`-2`, `-3`…) when the resulting directory already exists.
|
|
61
|
+
*/
|
|
62
|
+
worktreeDirName?: WorktreeDirNameFn;
|
|
55
63
|
}
|
|
56
64
|
/** Context passed to {@link SetupWorktreeConfig.finalizeWorktree}. */
|
|
57
65
|
export interface SetupContext {
|
|
@@ -64,7 +72,11 @@ export interface SetupContext {
|
|
|
64
72
|
force: boolean;
|
|
65
73
|
verbose: boolean;
|
|
66
74
|
}
|
|
67
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Context passed to {@link SetupWorktreeConfig.printSummary}.
|
|
77
|
+
*
|
|
78
|
+
* Called after worktree creation; the dev-server is not running yet.
|
|
79
|
+
*/
|
|
68
80
|
export interface SummaryContext {
|
|
69
81
|
slot: number;
|
|
70
82
|
branch: string;
|
package/dist/setup-worktree.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { isFinalizeMode, isInfoMode, isRemoveMode, isSetOwnerMode, isSetupMode, isWaitMode, parseSetupArgs, printSetupHelp, validateSetupFlags, } from "./cli.js";
|
|
5
|
-
import { removeDevServerEntryByWorktree } from "./dev-servers-registry.js";
|
|
5
|
+
import { findOwnEntry, removeDevServerEntryByWorktree } from "./dev-servers-registry.js";
|
|
6
6
|
import { ConfigError } from "./errors.js";
|
|
7
7
|
import { copyAndPatchFile } from "./helpers.js";
|
|
8
8
|
import { defaultComputePorts, isValidPort, resolvePortScheme } from "./ports.js";
|
|
@@ -78,7 +78,7 @@ async function runSetup(args, ctx, run, config) {
|
|
|
78
78
|
registryDir: config.registryDir,
|
|
79
79
|
scheme,
|
|
80
80
|
});
|
|
81
|
-
const setupCtx = ensureWorktree(args, ctx, run);
|
|
81
|
+
const setupCtx = ensureWorktree(args, ctx, run, config.worktreeDirName);
|
|
82
82
|
const branch = getCurrentBranch(setupCtx.currentWorktree);
|
|
83
83
|
const { port: slot, owner } = resolveAndRegisterSlot({
|
|
84
84
|
slot: args.slot,
|
|
@@ -123,6 +123,7 @@ async function runSetup(args, ctx, run, config) {
|
|
|
123
123
|
}));
|
|
124
124
|
teeLog(`WORKTREE_CREATED path=${setupCtx.currentWorktree} branch=${branch} slot=${slot}`);
|
|
125
125
|
teeLog(`Setup continuing in background. Tail: ${logPath}`);
|
|
126
|
+
teeLog(`Block until ready: setup-worktree --wait --slot ${slot}`);
|
|
126
127
|
const child = spawn(process.execPath, [config.scriptPath, "--__finalize", String(slot)], {
|
|
127
128
|
detached: true,
|
|
128
129
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -287,7 +288,13 @@ async function handleRemove(args, ctx, run, config) {
|
|
|
287
288
|
console.log(`Removed registry entry for branch "${target.branch}" (slot ${target.slotPort}${ownerSuffix}).`);
|
|
288
289
|
return;
|
|
289
290
|
}
|
|
290
|
-
|
|
291
|
+
const targetEntry = findOwnEntry(ctx.mainWorktree, config.registryDir, target.worktreePath);
|
|
292
|
+
if (targetEntry) {
|
|
293
|
+
stopTargetDevServer(config.devServerScript, target.worktreePath, verboseLog);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
verboseLog(`No dev-server running in ${target.worktreePath}; skipping --stop.`);
|
|
297
|
+
}
|
|
291
298
|
if (config.purgeInfrastructure) {
|
|
292
299
|
await config.purgeInfrastructure({
|
|
293
300
|
worktree: target.worktreePath,
|
|
@@ -339,11 +346,11 @@ function handleSetOwnerMode(args, ctx, config) {
|
|
|
339
346
|
}
|
|
340
347
|
console.log(`Owner for slot ${slotPort}: ${newOwner ?? "(none)"}`);
|
|
341
348
|
}
|
|
342
|
-
function ensureWorktree(args, ctx, run) {
|
|
349
|
+
function ensureWorktree(args, ctx, run, dirNameFn) {
|
|
343
350
|
if (args.use)
|
|
344
|
-
return useExistingBranch(args.use, ctx, run);
|
|
351
|
+
return useExistingBranch(args.use, ctx, run, dirNameFn);
|
|
345
352
|
if (args.create)
|
|
346
|
-
return createBranch(args.create, ctx, run);
|
|
353
|
+
return createBranch(args.create, ctx, run, dirNameFn);
|
|
347
354
|
return ctx;
|
|
348
355
|
}
|
|
349
356
|
function linkSharedDirectories(ctx, dirs, log) {
|
package/dist/worktree.d.ts
CHANGED
|
@@ -12,9 +12,20 @@ export declare function enforceWorktreeMode(args: {
|
|
|
12
12
|
create?: string;
|
|
13
13
|
here?: boolean;
|
|
14
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;
|
|
15
|
+
export declare function useExistingBranch(branch: string, ctx: WorktreeContext, run: RunCtx, dirNameFn?: WorktreeDirNameFn): WorktreeContext;
|
|
16
|
+
export declare function createBranch(requestedBranch: string, ctx: WorktreeContext, run: RunCtx, dirNameFn?: WorktreeDirNameFn): WorktreeContext;
|
|
17
17
|
export declare function verifyBranchAbsentFromRemote(branch: string, run: RunCtx): void;
|
|
18
18
|
export declare function getCurrentBranch(worktreePath: string): string;
|
|
19
19
|
export declare function removeWorktree(worktreePath: string, run: RunCtx): void;
|
|
20
|
-
|
|
20
|
+
/** Pure function that produces the basename of a worktree directory from a branch. */
|
|
21
|
+
export type WorktreeDirNameFn = (opts: {
|
|
22
|
+
branch: string;
|
|
23
|
+
repoName: string;
|
|
24
|
+
}) => string;
|
|
25
|
+
/**
|
|
26
|
+
* Default {@link WorktreeDirNameFn}. Strips a recognizable ticket suffix from the last branch
|
|
27
|
+
* segment (`feat/ABC-123-extra` → `feat-ABC-123`), caps the result at 22 chars, and strips
|
|
28
|
+
* trailing dashes. Falls back to the full sanitized branch when no ticket pattern is found.
|
|
29
|
+
*/
|
|
30
|
+
export declare const defaultWorktreeDirName: WorktreeDirNameFn;
|
|
31
|
+
export declare function computeWorktreePath(mainWorktree: string, branch: string, dirNameFn?: WorktreeDirNameFn): string;
|
package/dist/worktree.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
4
|
export function detectWorktree() {
|
|
4
5
|
const currentWorktree = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -23,16 +24,16 @@ export function enforceWorktreeMode(args, ctx) {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
|
-
export function useExistingBranch(branch, ctx, run) {
|
|
27
|
+
export function useExistingBranch(branch, ctx, run, dirNameFn = defaultWorktreeDirName) {
|
|
27
28
|
if (!branchExists(branch)) {
|
|
28
29
|
console.error(`Error: Branch "${branch}" does not exist locally or on the remote.`);
|
|
29
30
|
process.exit(1);
|
|
30
31
|
}
|
|
31
|
-
const worktreePath = computeWorktreePath(ctx.mainWorktree, branch);
|
|
32
|
+
const worktreePath = dedupeWorktreePath(computeWorktreePath(ctx.mainWorktree, branch, dirNameFn));
|
|
32
33
|
execFileSync("git", ["worktree", "add", worktreePath, branch], { stdio: stdioFor(run) });
|
|
33
34
|
return { ...ctx, currentWorktree: worktreePath, isMainWorktree: false };
|
|
34
35
|
}
|
|
35
|
-
export function createBranch(requestedBranch, ctx, run) {
|
|
36
|
+
export function createBranch(requestedBranch, ctx, run, dirNameFn = defaultWorktreeDirName) {
|
|
36
37
|
let finalBranch = requestedBranch;
|
|
37
38
|
if (branchExists(finalBranch)) {
|
|
38
39
|
let suffix = 2;
|
|
@@ -42,7 +43,7 @@ export function createBranch(requestedBranch, ctx, run) {
|
|
|
42
43
|
finalBranch = `${requestedBranch}-${suffix}`;
|
|
43
44
|
console.warn(`Warning: Branch "${requestedBranch}" already exists; using "${finalBranch}" instead.`);
|
|
44
45
|
}
|
|
45
|
-
const worktreePath = computeWorktreePath(ctx.mainWorktree, finalBranch);
|
|
46
|
+
const worktreePath = dedupeWorktreePath(computeWorktreePath(ctx.mainWorktree, finalBranch, dirNameFn));
|
|
46
47
|
execFileSync("git", ["worktree", "add", "-b", finalBranch, worktreePath], {
|
|
47
48
|
stdio: stdioFor(run),
|
|
48
49
|
});
|
|
@@ -67,10 +68,39 @@ export function getCurrentBranch(worktreePath) {
|
|
|
67
68
|
export function removeWorktree(worktreePath, run) {
|
|
68
69
|
execFileSync("git", ["worktree", "remove", "--force", worktreePath], { stdio: stdioFor(run) });
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Default {@link WorktreeDirNameFn}. Strips a recognizable ticket suffix from the last branch
|
|
73
|
+
* segment (`feat/ABC-123-extra` → `feat-ABC-123`), caps the result at 22 chars, and strips
|
|
74
|
+
* trailing dashes. Falls back to the full sanitized branch when no ticket pattern is found.
|
|
75
|
+
*/
|
|
76
|
+
export const defaultWorktreeDirName = ({ branch, repoName }) => {
|
|
77
|
+
return `${repoName}-${shortenBranchSegment(branch)}`;
|
|
78
|
+
};
|
|
79
|
+
function shortenBranchSegment(branch) {
|
|
80
|
+
const parts = branch.split("/");
|
|
81
|
+
const last = parts[parts.length - 1] ?? "";
|
|
82
|
+
const match = last.match(/^([A-Za-z]+-\d+|\d+)/);
|
|
83
|
+
if (match) {
|
|
84
|
+
parts[parts.length - 1] = match[1];
|
|
85
|
+
}
|
|
86
|
+
let result = parts.join("-");
|
|
87
|
+
if (result.length > 22) {
|
|
88
|
+
result = result.slice(0, 22);
|
|
89
|
+
}
|
|
90
|
+
return result.replace(/-+$/, "");
|
|
91
|
+
}
|
|
92
|
+
export function computeWorktreePath(mainWorktree, branch, dirNameFn = defaultWorktreeDirName) {
|
|
71
93
|
const repoName = basename(mainWorktree);
|
|
72
|
-
|
|
73
|
-
|
|
94
|
+
return join(dirname(mainWorktree), dirNameFn({ branch, repoName }));
|
|
95
|
+
}
|
|
96
|
+
function dedupeWorktreePath(candidate) {
|
|
97
|
+
if (!existsSync(candidate))
|
|
98
|
+
return candidate;
|
|
99
|
+
let suffix = 2;
|
|
100
|
+
while (existsSync(`${candidate}-${suffix}`)) {
|
|
101
|
+
++suffix;
|
|
102
|
+
}
|
|
103
|
+
return `${candidate}-${suffix}`;
|
|
74
104
|
}
|
|
75
105
|
function branchExists(branch) {
|
|
76
106
|
try {
|