@zenobius/pi-worktrees 0.4.0-next.16 → 0.4.0-next.19
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 +75 -13
- package/dist/cmds/createArgs.d.ts +21 -0
- package/dist/index.js +251 -13
- package/dist/services/branchNameGenerator.d.ts +20 -0
- package/dist/services/config/config.d.ts +6 -0
- package/dist/services/config/schema.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,8 @@ When you’re doing multiple feature branches, hotfixes, or experiments, `git wo
|
|
|
15
15
|
|
|
16
16
|
`pi-worktrees` gives you a guided interface inside Pi:
|
|
17
17
|
|
|
18
|
-
- Create
|
|
18
|
+
- Create branch-first worktrees (`/worktree create <branch>`) with predictable naming
|
|
19
|
+
- Optionally generate branch names via explicit opt-in (`/worktree create --generate ...`)
|
|
19
20
|
- List and inspect active worktrees
|
|
20
21
|
- Remove worktrees safely (with confirmations)
|
|
21
22
|
- Prune stale worktree references
|
|
@@ -59,14 +60,15 @@ pi install npm:@zenobius/pi-worktrees
|
|
|
59
60
|
|
|
60
61
|
```text
|
|
61
62
|
/worktree init
|
|
62
|
-
/worktree create auth-refactor
|
|
63
|
+
/worktree create feature/auth-refactor
|
|
64
|
+
/worktree create hotfix/login-timeout --name login-timeout
|
|
63
65
|
/worktree list
|
|
64
66
|
```
|
|
65
67
|
|
|
66
68
|
3. Optional: jump into it from your shell using the printed path:
|
|
67
69
|
|
|
68
70
|
```text
|
|
69
|
-
/worktree cd auth-refactor
|
|
71
|
+
/worktree cd feature-auth-refactor
|
|
70
72
|
```
|
|
71
73
|
|
|
72
74
|
## Quick start
|
|
@@ -75,11 +77,12 @@ In Pi:
|
|
|
75
77
|
|
|
76
78
|
```text
|
|
77
79
|
/worktree init
|
|
78
|
-
/worktree create auth-refactor
|
|
80
|
+
/worktree create feature/auth-refactor
|
|
81
|
+
/worktree create spike/new-parser --name parser-spike
|
|
79
82
|
/worktree list
|
|
80
83
|
/worktree status
|
|
81
|
-
/worktree cd auth-refactor
|
|
82
|
-
/worktree remove auth-refactor
|
|
84
|
+
/worktree cd feature-auth-refactor
|
|
85
|
+
/worktree remove feature-auth-refactor
|
|
83
86
|
/worktree prune
|
|
84
87
|
```
|
|
85
88
|
|
|
@@ -114,7 +117,7 @@ This creates a new Zellij tab with Neovim and Pi running in the new worktree pat
|
|
|
114
117
|
| `/worktree settings` | Show all current settings |
|
|
115
118
|
| `/worktree settings <key>` | Get one setting (`worktreeRoot`, `parentDir` alias, `onCreate`) |
|
|
116
119
|
| `/worktree settings <key> <value>` | Set one setting |
|
|
117
|
-
| `/worktree create <
|
|
120
|
+
| `/worktree create <branch> [--name <worktree-name>]`<br/>`/worktree create --generate [--name <worktree-name>] <prompt-or-name>` | Create a new worktree from `<branch>` (default mode) or generate one via configured `branchNameGenerator` (opt-in with `--generate`) |
|
|
118
121
|
| `/worktree list` | List all worktrees (`/worktree ls` alias) |
|
|
119
122
|
| `/worktree status` | Show current repo/worktree status |
|
|
120
123
|
| `/worktree cd <name>` | Print matching worktree path |
|
|
@@ -133,11 +136,12 @@ Settings live in `~/.pi/agent/pi-worktrees-settings.json`.
|
|
|
133
136
|
"worktrees": {
|
|
134
137
|
"github.com/org/repo": {
|
|
135
138
|
"worktreeRoot": "~/work/org/repo.worktrees",
|
|
136
|
-
"onCreate": ["mise install", "bun install"]
|
|
139
|
+
"onCreate": ["mise install", "bun install"],
|
|
137
140
|
},
|
|
138
141
|
"github.com/org/*": {
|
|
139
142
|
"worktreeRoot": "~/work/org/shared.worktrees",
|
|
140
|
-
"onCreate": "mise setup"
|
|
143
|
+
"onCreate": "mise setup",
|
|
144
|
+
"branchNameGenerator": "pi -p \"branch name for $PI_WORKTREE_PROMPT\" --model local/model"
|
|
141
145
|
}
|
|
142
146
|
},
|
|
143
147
|
"matchingStrategy": "fail-on-tie",
|
|
@@ -168,6 +172,7 @@ Settings live in `~/.pi/agent/pi-worktrees-settings.json`.
|
|
|
168
172
|
| `onCreateCmdDisplayPendingColor` | `string` | `dim` | Pi theme color name for pending/running command lines. |
|
|
169
173
|
| `onCreateCmdDisplaySuccessColor` | `string` | `success` | Pi theme color name for successful command lines. |
|
|
170
174
|
| `onCreateCmdDisplayErrorColor` | `string` | `error` | Pi theme color name for failed command lines. |
|
|
175
|
+
| `worktrees[*].branchNameGenerator` | `string` | unset | Optional command used only by `/worktree create --generate ...`. Must print exactly one branch name to stdout. Receives `$PI_WORKTREE_PROMPT` env var and supports `{{prompt}}` / `{prompt}` token replacement. |
|
|
171
176
|
| `worktree` (legacy) | `WorktreeSettings` | n/a | Legacy fallback shape; migrated automatically. |
|
|
172
177
|
|
|
173
178
|
### Matching model
|
|
@@ -231,6 +236,63 @@ Where new worktrees are created.
|
|
|
231
236
|
|
|
232
237
|
> Backward compatibility: `parentDir` is still accepted as a deprecated alias for `worktreeRoot`.
|
|
233
238
|
> The extension will migrate existing `parentDir` values to `worktreeRoot` automatically.
|
|
239
|
+
### Create command naming contract
|
|
240
|
+
|
|
241
|
+
`/worktree create` is branch-first:
|
|
242
|
+
|
|
243
|
+
- Required first argument is the **branch name** to create.
|
|
244
|
+
- Default worktree folder name is `slugify(branch)`.
|
|
245
|
+
- Optional `--name <worktree-name>` overrides the derived folder name.
|
|
246
|
+
|
|
247
|
+
### Optional branch generator (safe opt-in)
|
|
248
|
+
|
|
249
|
+
Generator mode is **never automatic**. You must pass `--generate` explicitly:
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
/worktree create --generate login-flow
|
|
253
|
+
/worktree create --generate --name ui-login login-flow
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Safety behavior:
|
|
257
|
+
- Branch-first remains default source of truth.
|
|
258
|
+
- `branchNameGenerator` is ignored unless `--generate` is present.
|
|
259
|
+
- Generator command runs with a strict 5s timeout.
|
|
260
|
+
- On timeout, non-zero exit, empty stdout, or invalid branch output: command fails and no worktree is created.
|
|
261
|
+
- When a generated branch is used, Pi emits a provenance message before creation.
|
|
262
|
+
Examples:
|
|
263
|
+
|
|
264
|
+
```text
|
|
265
|
+
/worktree create feature/login
|
|
266
|
+
# branch: feature/login, worktree folder: feature-login
|
|
267
|
+
|
|
268
|
+
/worktree create feature/login --name ui-login
|
|
269
|
+
# branch: feature/login, worktree folder: ui-login
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Migration from legacy `<feature-name>` usage
|
|
273
|
+
|
|
274
|
+
Old mental model:
|
|
275
|
+
|
|
276
|
+
```text
|
|
277
|
+
/worktree create login
|
|
278
|
+
# previously implied branch feature/login
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Current behavior:
|
|
282
|
+
|
|
283
|
+
```text
|
|
284
|
+
/worktree create login
|
|
285
|
+
# branch: login, worktree folder: login
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
To preserve old semantics explicitly:
|
|
289
|
+
|
|
290
|
+
```text
|
|
291
|
+
/worktree create feature/login --name login
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Current releases emit a warning when legacy-style single tokens are detected without `--name`.
|
|
295
|
+
|
|
234
296
|
### Template variables
|
|
235
297
|
|
|
236
298
|
Available in `worktreeRoot` and `onCreate` values:
|
|
@@ -290,14 +352,14 @@ This extension does not apply a separate ad-hoc deprecation mechanism.
|
|
|
290
352
|
| v
|
|
291
353
|
| [Save settings] -----------------> [Idle]
|
|
292
354
|
|
|
|
293
|
-
+--> [create <name>] --> [Validate repo/name/branch/path]
|
|
355
|
+
+--> [create <branch> [--name <worktree-name>]] --> [Validate repo/name/branch/path]
|
|
294
356
|
| |fail
|
|
295
357
|
| v
|
|
296
358
|
| [Error] -------------> [Idle]
|
|
297
359
|
| |
|
|
298
360
|
| pass
|
|
299
361
|
| v
|
|
300
|
-
| [git worktree add -b
|
|
362
|
+
| [git worktree add -b <branch> <worktreePath>]
|
|
301
363
|
| |fail
|
|
302
364
|
| v
|
|
303
365
|
| [Error] -------------> [Idle]
|
|
@@ -384,8 +446,8 @@ This extension does not apply a separate ad-hoc deprecation mechanism.
|
|
|
384
446
|
### `Not in a git repository`
|
|
385
447
|
Run commands from inside a git repo (or one of its worktrees).
|
|
386
448
|
|
|
387
|
-
### `Branch '
|
|
388
|
-
Choose another
|
|
449
|
+
### `Branch '<branch>' already exists`
|
|
450
|
+
Choose another branch name or delete/rename the existing branch.
|
|
389
451
|
|
|
390
452
|
### Can’t remove worktree due to changes
|
|
391
453
|
Use `/worktree remove <name>`, then confirm the force remove prompt.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface CreateCommandArgsBase {
|
|
2
|
+
worktreeName: string;
|
|
3
|
+
explicitName: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface CreateCommandBranchArgs extends CreateCommandArgsBase {
|
|
6
|
+
generate: false;
|
|
7
|
+
branch: string;
|
|
8
|
+
showLegacyWarning: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface CreateCommandGenerateArgs extends CreateCommandArgsBase {
|
|
11
|
+
generate: true;
|
|
12
|
+
generatorInput: string;
|
|
13
|
+
showLegacyWarning: false;
|
|
14
|
+
}
|
|
15
|
+
export type CreateCommandArgs = CreateCommandBranchArgs | CreateCommandGenerateArgs;
|
|
16
|
+
export interface CreateCommandArgError {
|
|
17
|
+
error: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function slugifyBranch(branch: string): string;
|
|
20
|
+
export declare function parseCreateCommandArgs(args: string): CreateCommandArgs | CreateCommandArgError;
|
|
21
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,8 @@ var WorktreeSettingsSchema = TypeObject({
|
|
|
30
30
|
parentDir: Optional(TypeString()),
|
|
31
31
|
onCreate: Optional(HookCommandsSchema),
|
|
32
32
|
onSwitch: Optional(HookCommandsSchema),
|
|
33
|
-
onBeforeRemove: Optional(HookCommandsSchema)
|
|
33
|
+
onBeforeRemove: Optional(HookCommandsSchema),
|
|
34
|
+
branchNameGenerator: Optional(TypeString())
|
|
34
35
|
}, {
|
|
35
36
|
$id: "WorktreeSettingsConfig",
|
|
36
37
|
additionalProperties: false
|
|
@@ -960,21 +961,251 @@ var DefaultWorktreeSettings = {
|
|
|
960
961
|
};
|
|
961
962
|
var DefaultLogfileTemplate = DEFAULT_LOGFILE_TEMPLATE;
|
|
962
963
|
|
|
964
|
+
// src/cmds/createArgs.ts
|
|
965
|
+
var SIMPLE_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
966
|
+
var BRANCH_FIRST_USAGE = "/worktree create <branch> [--name <worktree-name>]";
|
|
967
|
+
var GENERATE_USAGE = "/worktree create --generate [--name <worktree-name>] <prompt-or-name>";
|
|
968
|
+
var CREATE_USAGE = `Usage: ${BRANCH_FIRST_USAGE} OR ${GENERATE_USAGE}`;
|
|
969
|
+
function slugifyBranch(branch) {
|
|
970
|
+
return branch.toLowerCase().replace(/[\s/_.]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
971
|
+
}
|
|
972
|
+
function isValidWorktreeName(name) {
|
|
973
|
+
return SIMPLE_NAME_PATTERN.test(name);
|
|
974
|
+
}
|
|
975
|
+
function isLegacyStyleToken(token) {
|
|
976
|
+
if (!token) {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
if (token.includes("/")) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
return SIMPLE_NAME_PATTERN.test(token);
|
|
983
|
+
}
|
|
984
|
+
function parseCreateCommandArgs(args) {
|
|
985
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
986
|
+
if (tokens.length === 0) {
|
|
987
|
+
return { error: CREATE_USAGE };
|
|
988
|
+
}
|
|
989
|
+
let explicitName;
|
|
990
|
+
let useGenerator = false;
|
|
991
|
+
const positional = [];
|
|
992
|
+
for (let index = 0;index < tokens.length; index += 1) {
|
|
993
|
+
const token = tokens[index];
|
|
994
|
+
if (token === "--name") {
|
|
995
|
+
if (explicitName) {
|
|
996
|
+
return { error: "Duplicate --name option. Provide it only once." };
|
|
997
|
+
}
|
|
998
|
+
const value = tokens[index + 1];
|
|
999
|
+
if (!value || value.startsWith("--")) {
|
|
1000
|
+
return {
|
|
1001
|
+
error: `Missing value for --name. ${CREATE_USAGE}`
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
explicitName = value;
|
|
1005
|
+
index += 1;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (token === "--generate") {
|
|
1009
|
+
if (useGenerator) {
|
|
1010
|
+
return { error: "Duplicate --generate option. Provide it only once." };
|
|
1011
|
+
}
|
|
1012
|
+
useGenerator = true;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (token.startsWith("--")) {
|
|
1016
|
+
return {
|
|
1017
|
+
error: `Unknown argument: ${token}. ${CREATE_USAGE}`
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
positional.push(token);
|
|
1021
|
+
}
|
|
1022
|
+
if (positional.length !== 1) {
|
|
1023
|
+
return {
|
|
1024
|
+
error: `Expected exactly one ${useGenerator ? "<prompt-or-name>" : "<branch>"}. ${CREATE_USAGE}`
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
if (explicitName && !isValidWorktreeName(explicitName)) {
|
|
1028
|
+
return {
|
|
1029
|
+
error: "Invalid worktree name for --name. Use only letters, numbers, '.', '_' or '-' (no '/')."
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
const sourceToken = positional[0];
|
|
1033
|
+
const derivedName = slugifyBranch(sourceToken);
|
|
1034
|
+
if (!explicitName && !derivedName) {
|
|
1035
|
+
return {
|
|
1036
|
+
error: "Derived worktree name is empty after slugify. Use a source with letters/numbers or pass --name <worktree-name>."
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
if (useGenerator) {
|
|
1040
|
+
return {
|
|
1041
|
+
generate: true,
|
|
1042
|
+
generatorInput: sourceToken,
|
|
1043
|
+
worktreeName: explicitName ?? derivedName,
|
|
1044
|
+
explicitName: Boolean(explicitName),
|
|
1045
|
+
showLegacyWarning: false
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
generate: false,
|
|
1050
|
+
branch: sourceToken,
|
|
1051
|
+
worktreeName: explicitName ?? derivedName,
|
|
1052
|
+
explicitName: Boolean(explicitName),
|
|
1053
|
+
showLegacyWarning: !explicitName && isLegacyStyleToken(sourceToken)
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/services/branchNameGenerator.ts
|
|
1058
|
+
import { spawn as spawn2 } from "child_process";
|
|
1059
|
+
var BRANCH_NAME_GENERATOR_TIMEOUT_MS = 5000;
|
|
1060
|
+
function shellQuote(value) {
|
|
1061
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1062
|
+
}
|
|
1063
|
+
function renderCommand(template, input) {
|
|
1064
|
+
const quotedInput = shellQuote(input);
|
|
1065
|
+
return template.replace(/\{\{prompt\}\}|\{prompt\}/g, quotedInput);
|
|
1066
|
+
}
|
|
1067
|
+
async function validateBranchName(branchName, cwd) {
|
|
1068
|
+
const checker = spawn2("git", ["check-ref-format", "--branch", branchName], {
|
|
1069
|
+
cwd,
|
|
1070
|
+
shell: false,
|
|
1071
|
+
stdio: "ignore"
|
|
1072
|
+
});
|
|
1073
|
+
return new Promise((resolve2) => {
|
|
1074
|
+
checker.on("close", (code) => resolve2(code === 0));
|
|
1075
|
+
checker.on("error", () => resolve2(false));
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
async function generateBranchName(params) {
|
|
1079
|
+
const timeoutMs = params.timeoutMs ?? BRANCH_NAME_GENERATOR_TIMEOUT_MS;
|
|
1080
|
+
if (!params.commandTemplate?.trim()) {
|
|
1081
|
+
return {
|
|
1082
|
+
ok: false,
|
|
1083
|
+
code: "missing-config",
|
|
1084
|
+
message: "No branchNameGenerator configured for this repository. Set worktrees.<pattern>.branchNameGenerator or run '/worktree create <branch>' without --generate."
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
const command = renderCommand(params.commandTemplate, params.input);
|
|
1088
|
+
const result = await new Promise((resolve2) => {
|
|
1089
|
+
const child = spawn2(command, {
|
|
1090
|
+
cwd: params.cwd,
|
|
1091
|
+
shell: true,
|
|
1092
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1093
|
+
env: {
|
|
1094
|
+
...process.env,
|
|
1095
|
+
PI_WORKTREE_PROMPT: params.input
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
let stdout = "";
|
|
1099
|
+
let stderr = "";
|
|
1100
|
+
let done = false;
|
|
1101
|
+
const timer = setTimeout(() => {
|
|
1102
|
+
if (done) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
done = true;
|
|
1106
|
+
child.kill("SIGKILL");
|
|
1107
|
+
resolve2({ kind: "timeout" });
|
|
1108
|
+
}, timeoutMs);
|
|
1109
|
+
child.stdout?.on("data", (chunk) => {
|
|
1110
|
+
stdout += chunk.toString();
|
|
1111
|
+
});
|
|
1112
|
+
child.stderr?.on("data", (chunk) => {
|
|
1113
|
+
stderr += chunk.toString();
|
|
1114
|
+
});
|
|
1115
|
+
child.on("error", (error) => {
|
|
1116
|
+
if (done) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
done = true;
|
|
1120
|
+
clearTimeout(timer);
|
|
1121
|
+
resolve2({ kind: "spawn-error", error: error.message });
|
|
1122
|
+
});
|
|
1123
|
+
child.on("close", (code) => {
|
|
1124
|
+
if (done) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
done = true;
|
|
1128
|
+
clearTimeout(timer);
|
|
1129
|
+
resolve2({ kind: "success", stdout, stderr, code: code ?? 1 });
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
if (result.kind === "timeout") {
|
|
1133
|
+
return {
|
|
1134
|
+
ok: false,
|
|
1135
|
+
code: "timeout",
|
|
1136
|
+
message: `branchNameGenerator timed out after ${timeoutMs}ms. Make it faster or run '/worktree create <branch>' manually.`
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
if (result.kind === "spawn-error") {
|
|
1140
|
+
return {
|
|
1141
|
+
ok: false,
|
|
1142
|
+
code: "spawn-error",
|
|
1143
|
+
message: `Failed to run branchNameGenerator command: ${result.error}`
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
if (result.code !== 0) {
|
|
1147
|
+
const stderr = result.stderr.trim();
|
|
1148
|
+
return {
|
|
1149
|
+
ok: false,
|
|
1150
|
+
code: "non-zero-exit",
|
|
1151
|
+
message: `branchNameGenerator exited with code ${result.code}.${stderr ? ` stderr: ${stderr}` : ""}`
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
const branchName = result.stdout.trim();
|
|
1155
|
+
if (!branchName) {
|
|
1156
|
+
return {
|
|
1157
|
+
ok: false,
|
|
1158
|
+
code: "empty-output",
|
|
1159
|
+
message: "branchNameGenerator produced empty output. Ensure the command prints exactly one branch name to stdout."
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
const valid = await validateBranchName(branchName, params.cwd);
|
|
1163
|
+
if (!valid) {
|
|
1164
|
+
return {
|
|
1165
|
+
ok: false,
|
|
1166
|
+
code: "invalid-output",
|
|
1167
|
+
message: `branchNameGenerator output is not a valid branch name: '${branchName}'. Fix the command output or run '/worktree create <branch>' manually.`
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
return {
|
|
1171
|
+
ok: true,
|
|
1172
|
+
branchName,
|
|
1173
|
+
command: params.commandTemplate
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
963
1177
|
// src/cmds/cmdCreate.ts
|
|
964
1178
|
async function cmdCreate(args, ctx, deps) {
|
|
965
|
-
const
|
|
966
|
-
if (
|
|
967
|
-
ctx.ui.notify(
|
|
1179
|
+
const parsed = parseCreateCommandArgs(args);
|
|
1180
|
+
if ("error" in parsed) {
|
|
1181
|
+
ctx.ui.notify(parsed.error, "error");
|
|
968
1182
|
return;
|
|
969
1183
|
}
|
|
1184
|
+
const worktreeName = parsed.worktreeName;
|
|
970
1185
|
if (!isGitRepo(ctx.cwd)) {
|
|
971
1186
|
ctx.ui.notify("Not in a git repository", "error");
|
|
972
1187
|
return;
|
|
973
1188
|
}
|
|
974
1189
|
const current = deps.configService.current(ctx);
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1190
|
+
let branchName = parsed.generate ? "" : parsed.branch;
|
|
1191
|
+
if (parsed.generate) {
|
|
1192
|
+
const generated = await generateBranchName({
|
|
1193
|
+
commandTemplate: current.branchNameGenerator,
|
|
1194
|
+
input: parsed.generatorInput,
|
|
1195
|
+
cwd: ctx.cwd
|
|
1196
|
+
});
|
|
1197
|
+
if (!generated.ok) {
|
|
1198
|
+
ctx.ui.notify(generated.message, "error");
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
branchName = generated.branchName;
|
|
1202
|
+
ctx.ui.notify(`Using generated branch '${branchName}' from branchNameGenerator (input: '${parsed.generatorInput}').`, "info");
|
|
1203
|
+
}
|
|
1204
|
+
if (!parsed.generate && parsed.showLegacyWarning) {
|
|
1205
|
+
ctx.ui.notify(`Legacy create style detected: '/worktree create <feature-name>' is deprecated. '${branchName}' is now treated as the branch name. If you want old semantics, run '/worktree create feature/${branchName}' (optionally '--name ${branchName}').`, "warning");
|
|
1206
|
+
}
|
|
1207
|
+
const worktreePath = join3(current.parentDir, worktreeName);
|
|
1208
|
+
const existingWorktree = listWorktrees(ctx.cwd).find((worktree) => worktree.path === worktreePath || basename3(worktree.path) === worktreeName || worktree.branch === branchName);
|
|
978
1209
|
if (existingWorktree) {
|
|
979
1210
|
if (!ctx.hasUI) {
|
|
980
1211
|
ctx.ui.notify(`Worktree already exists at: ${worktreePath}`, "error");
|
|
@@ -1029,11 +1260,11 @@ Switch to this worktree?`;
|
|
|
1029
1260
|
return;
|
|
1030
1261
|
} catch {}
|
|
1031
1262
|
ensureExcluded(ctx.cwd, current.parentDir);
|
|
1032
|
-
const stopBusy = deps.statusService.busy(ctx, `Creating worktree: ${
|
|
1263
|
+
const stopBusy = deps.statusService.busy(ctx, `Creating worktree: ${worktreeName}...`);
|
|
1033
1264
|
try {
|
|
1034
1265
|
git(["worktree", "add", "-b", branchName, worktreePath], current.mainWorktree);
|
|
1035
1266
|
stopBusy();
|
|
1036
|
-
deps.statusService.positive(ctx, `Created: ${
|
|
1267
|
+
deps.statusService.positive(ctx, `Created: ${worktreeName}`);
|
|
1037
1268
|
} catch (err) {
|
|
1038
1269
|
stopBusy();
|
|
1039
1270
|
deps.statusService.critical(ctx, `Failed to create worktree`);
|
|
@@ -1042,12 +1273,12 @@ Switch to this worktree?`;
|
|
|
1042
1273
|
}
|
|
1043
1274
|
const createdCtx = {
|
|
1044
1275
|
path: worktreePath,
|
|
1045
|
-
name:
|
|
1276
|
+
name: worktreeName,
|
|
1046
1277
|
branch: branchName,
|
|
1047
1278
|
...current
|
|
1048
1279
|
};
|
|
1049
1280
|
const sessionId = sanitizePathPart(ctx.sessionManager?.getSessionId?.() || "session");
|
|
1050
|
-
const safeName = sanitizePathPart(
|
|
1281
|
+
const safeName = sanitizePathPart(worktreeName);
|
|
1051
1282
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1052
1283
|
const logPath = resolveLogfilePath(current.logfile ?? DefaultLogfileTemplate, {
|
|
1053
1284
|
sessionId,
|
|
@@ -1689,7 +1920,8 @@ var HELP_TEXT = `
|
|
|
1689
1920
|
Commands:
|
|
1690
1921
|
/worktree init Configure worktree settings interactively
|
|
1691
1922
|
/worktree settings [key] [val] Get/set individual settings
|
|
1692
|
-
/worktree create <
|
|
1923
|
+
/worktree create <branch> [--name <worktree-name>] Create new worktree from branch
|
|
1924
|
+
/worktree create --generate [--name <worktree-name>] <prompt-or-name> Generate branch via config command
|
|
1693
1925
|
/worktree list List worktrees and run onSwitch for a selection
|
|
1694
1926
|
/worktree remove <name> Remove a worktree (runs onBeforeRemove if set)
|
|
1695
1927
|
/worktree status Show current worktree info
|
|
@@ -1704,7 +1936,8 @@ Configuration (~/.pi/agent/pi-worktrees.config.json):
|
|
|
1704
1936
|
"worktreeRoot": "~/work/org",
|
|
1705
1937
|
"onCreate": ["mise install", "bun install"],
|
|
1706
1938
|
"onSwitch": "mise run dev:resume",
|
|
1707
|
-
"onBeforeRemove": "bun test"
|
|
1939
|
+
"onBeforeRemove": "bun test",
|
|
1940
|
+
"branchNameGenerator": "pi -p "branch name for $PI_WORKTREE_PROMPT" --model local/model",
|
|
1708
1941
|
},
|
|
1709
1942
|
"github.com/org/*": {
|
|
1710
1943
|
"worktreeRoot": "~/work/org-other",
|
|
@@ -1730,6 +1963,11 @@ Pattern matching: exact URL > most-specific glob > fallback (worktree)
|
|
|
1730
1963
|
Matching strategies: fail-on-tie | first-wins | last-wins
|
|
1731
1964
|
|
|
1732
1965
|
Config note: parentDir is deprecated and supported as an alias for worktreeRoot.
|
|
1966
|
+
Naming note: default worktree name is slugify(branch); explicit '--name' takes precedence.
|
|
1967
|
+
Generator note: '--generate' is explicit opt-in and requires branchNameGenerator config.
|
|
1968
|
+
Generated branch output must be valid and is never used unless --generate is present.
|
|
1969
|
+
Migration note: legacy '/worktree create <feature-name>' is deprecated and now treats token as branch.
|
|
1970
|
+
Use '/worktree create feature/<name> --name <name>' to preserve old semantics.
|
|
1733
1971
|
Hook vars: {{path}}, {{name}}, {{branch}}, {{project}}, {{mainWorktree}}
|
|
1734
1972
|
Hooks: onCreate (new), onSwitch (existing), onBeforeRemove (pre-delete, non-zero blocks)
|
|
1735
1973
|
Logfile vars: {sessionId} / {{sessionId}}, {name} / {{name}}, {timestamp} / {{timestamp}}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const BRANCH_NAME_GENERATOR_TIMEOUT_MS = 5000;
|
|
2
|
+
export type BranchNameGeneratorErrorCode = 'missing-config' | 'timeout' | 'non-zero-exit' | 'empty-output' | 'invalid-output' | 'spawn-error';
|
|
3
|
+
export interface GenerateBranchNameParams {
|
|
4
|
+
commandTemplate: string | undefined;
|
|
5
|
+
input: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GenerateBranchNameSuccess {
|
|
10
|
+
ok: true;
|
|
11
|
+
branchName: string;
|
|
12
|
+
command: string;
|
|
13
|
+
}
|
|
14
|
+
export interface GenerateBranchNameFailure {
|
|
15
|
+
ok: false;
|
|
16
|
+
code: BranchNameGeneratorErrorCode;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
export type GenerateBranchNameResult = GenerateBranchNameSuccess | GenerateBranchNameFailure;
|
|
20
|
+
export declare function generateBranchName(params: GenerateBranchNameParams): Promise<GenerateBranchNameResult>;
|
|
@@ -6,6 +6,7 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
6
6
|
worktreeRoot?: string | undefined;
|
|
7
7
|
onSwitch?: string | string[] | undefined;
|
|
8
8
|
onBeforeRemove?: string | string[] | undefined;
|
|
9
|
+
branchNameGenerator?: string | undefined;
|
|
9
10
|
}>;
|
|
10
11
|
current: (ctx: {
|
|
11
12
|
cwd: string;
|
|
@@ -27,6 +28,7 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
27
28
|
worktreeRoot?: string | undefined;
|
|
28
29
|
onSwitch?: string | string[] | undefined;
|
|
29
30
|
onBeforeRemove?: string | string[] | undefined;
|
|
31
|
+
branchNameGenerator?: string | undefined;
|
|
30
32
|
};
|
|
31
33
|
save: (data: PiWorktreeConfig) => Promise<void>;
|
|
32
34
|
config: {
|
|
@@ -37,6 +39,7 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
37
39
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
38
40
|
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
39
41
|
onBeforeRemove: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
42
|
+
branchNameGenerator: import("typebox").TOptional<import("typebox").TString>;
|
|
40
43
|
}>>>;
|
|
41
44
|
matchingStrategy: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<"fail-on-tie">, import("typebox").TLiteral<"first-wins">, import("typebox").TLiteral<"last-wins">]>>;
|
|
42
45
|
logfile: import("typebox").TOptional<import("typebox").TString>;
|
|
@@ -53,6 +56,7 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
53
56
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
54
57
|
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
55
58
|
onBeforeRemove: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
59
|
+
branchNameGenerator: import("typebox").TOptional<import("typebox").TString>;
|
|
56
60
|
}>> | undefined;
|
|
57
61
|
logfile?: string | undefined;
|
|
58
62
|
onCreateDisplayOutputMaxLines?: number | undefined;
|
|
@@ -75,6 +79,7 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
75
79
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
76
80
|
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
77
81
|
onBeforeRemove: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
82
|
+
branchNameGenerator: import("typebox").TOptional<import("typebox").TString>;
|
|
78
83
|
}>>>;
|
|
79
84
|
matchingStrategy: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<"fail-on-tie">, import("typebox").TLiteral<"first-wins">, import("typebox").TLiteral<"last-wins">]>>;
|
|
80
85
|
logfile: import("typebox").TOptional<import("typebox").TString>;
|
|
@@ -91,6 +96,7 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
91
96
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
92
97
|
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
93
98
|
onBeforeRemove: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
99
|
+
branchNameGenerator: import("typebox").TOptional<import("typebox").TString>;
|
|
94
100
|
}>> | undefined;
|
|
95
101
|
logfile?: string | undefined;
|
|
96
102
|
onCreateDisplayOutputMaxLines?: number | undefined;
|
|
@@ -5,6 +5,7 @@ declare const WorktreeSettingsSchema: import("typebox").TObject<{
|
|
|
5
5
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
6
6
|
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
7
7
|
onBeforeRemove: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
8
|
+
branchNameGenerator: import("typebox").TOptional<import("typebox").TString>;
|
|
8
9
|
}>;
|
|
9
10
|
declare const MatchingStrategySchema: import("typebox").TUnion<[import("typebox").TLiteral<"fail-on-tie">, import("typebox").TLiteral<"first-wins">, import("typebox").TLiteral<"last-wins">]>;
|
|
10
11
|
declare const MatchStrategyResultSchema: import("typebox").TUnion<[import("typebox").TLiteral<"exact">, import("typebox").TLiteral<"unmatched">]>;
|
|
@@ -15,6 +16,7 @@ export declare const PiWorktreeConfigSchema: import("typebox").TObject<{
|
|
|
15
16
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
16
17
|
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
17
18
|
onBeforeRemove: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
19
|
+
branchNameGenerator: import("typebox").TOptional<import("typebox").TString>;
|
|
18
20
|
}>>>;
|
|
19
21
|
matchingStrategy: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<"fail-on-tie">, import("typebox").TLiteral<"first-wins">, import("typebox").TLiteral<"last-wins">]>>;
|
|
20
22
|
logfile: import("typebox").TOptional<import("typebox").TString>;
|