@paleo/worktree-env 0.6.1 → 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/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
- use: {
5
+ create: {
6
6
  type: "string",
7
7
  arg: "branch",
8
- description: "Create a worktree for an existing branch, then set up the local environment",
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
- create: {
10
+ use: {
11
11
  type: "string",
12
12
  arg: "branch",
13
- description: "Create a new branch + worktree, then set up the local environment. If the branch already exists, appends a numeric suffix (-2, -3, ...)",
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/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
@@ -1,4 +1,5 @@
1
1
  export { runSetupWorktree } from "./setup-worktree.js";
2
+ export { defaultWorktreeDirName } from "./worktree.js";
2
3
  export { runDevServer } from "./dev-server.js";
3
4
  import * as helpers from "./helpers.js";
4
5
  export { helpers };
@@ -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
- /** Context passed to {@link SetupWorktreeConfig.printSummary}. */
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;
@@ -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],
@@ -345,11 +346,11 @@ function handleSetOwnerMode(args, ctx, config) {
345
346
  }
346
347
  console.log(`Owner for slot ${slotPort}: ${newOwner ?? "(none)"}`);
347
348
  }
348
- function ensureWorktree(args, ctx, run) {
349
+ function ensureWorktree(args, ctx, run, dirNameFn) {
349
350
  if (args.use)
350
- return useExistingBranch(args.use, ctx, run);
351
+ return useExistingBranch(args.use, ctx, run, dirNameFn);
351
352
  if (args.create)
352
- return createBranch(args.create, ctx, run);
353
+ return createBranch(args.create, ctx, run, dirNameFn);
353
354
  return ctx;
354
355
  }
355
356
  function linkSharedDirectories(ctx, dirs, log) {
@@ -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
- export declare function computeWorktreePath(mainWorktree: string, branch: string): string;
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
- export function computeWorktreePath(mainWorktree, branch) {
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
- const sanitized = branch.replaceAll("/", "-");
73
- return join(dirname(mainWorktree), `${repoName}-${sanitized}`);
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paleo/worktree-env",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Worktree-based concurrent local environment kernel.",
5
5
  "keywords": [
6
6
  "worktree",