@zenobius/pi-worktrees 0.4.0-next.8 → 0.5.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 +75 -13
- package/dist/cmds/cmdList.d.ts +1 -1
- package/dist/cmds/cmdPrune.d.ts +2 -1
- package/dist/cmds/createArgs.d.ts +21 -0
- package/dist/cmds/shared.d.ts +4 -1
- package/dist/index.js +535 -77
- package/dist/services/branchNameGenerator.d.ts +20 -0
- package/dist/services/config/config.d.ts +18 -0
- package/dist/services/config/schema.d.ts +6 -0
- package/dist/types.d.ts +2 -0
- package/dist/ui/status.d.ts +27 -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.
|
package/dist/cmds/cmdList.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { CmdHandler } from '../types.ts';
|
|
1
|
+
import type { CmdHandler } from '../types.ts';
|
|
2
2
|
export declare const cmdList: CmdHandler;
|
package/dist/cmds/cmdPrune.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from '@mariozechner/pi-coding-agent';
|
|
2
|
-
|
|
2
|
+
import type { CommandDeps } from '../types.ts';
|
|
3
|
+
export declare function cmdPrune(_args: string, ctx: ExtensionCommandContext, deps: CommandDeps): Promise<void>;
|
|
@@ -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/cmds/shared.d.ts
CHANGED
|
@@ -19,8 +19,11 @@ export interface OnCreateHookOptions {
|
|
|
19
19
|
cmdDisplaySuccessColor?: string;
|
|
20
20
|
cmdDisplayErrorColor?: string;
|
|
21
21
|
}
|
|
22
|
+
export declare function sanitizePathPart(value: string): string;
|
|
23
|
+
export declare function resolveLogfilePath(template: string, values: Record<'sessionId' | 'name' | 'timestamp', string>): string;
|
|
22
24
|
/**
|
|
23
|
-
* Runs
|
|
25
|
+
* Runs hook commands sequentially.
|
|
24
26
|
* Stops at first failure and reports the failing command.
|
|
25
27
|
*/
|
|
28
|
+
export declare function runHook(createdCtx: WorktreeCreatedContext, hookValue: WorktreeSettingsConfig['onCreate'] | undefined, hookName: 'onCreate' | 'onSwitch' | 'onBeforeRemove', notify: (msg: string, type: 'info' | 'error' | 'warning') => void, options?: OnCreateHookOptions): Promise<OnCreateResult>;
|
|
26
29
|
export declare function runOnCreateHook(createdCtx: WorktreeCreatedContext, settings: WorktreeSettingsConfig, notify: (msg: string, type: 'info' | 'error' | 'warning') => void, options?: OnCreateHookOptions): Promise<OnCreateResult>;
|
package/dist/index.js
CHANGED
|
@@ -24,11 +24,14 @@ import {
|
|
|
24
24
|
Union,
|
|
25
25
|
Integer as TypeInteger
|
|
26
26
|
} from "typebox";
|
|
27
|
-
var
|
|
27
|
+
var HookCommandsSchema = Union([TypeString(), TypeArray(TypeString())]);
|
|
28
28
|
var WorktreeSettingsSchema = TypeObject({
|
|
29
29
|
worktreeRoot: Optional(TypeString()),
|
|
30
30
|
parentDir: Optional(TypeString()),
|
|
31
|
-
onCreate: Optional(
|
|
31
|
+
onCreate: Optional(HookCommandsSchema),
|
|
32
|
+
onSwitch: Optional(HookCommandsSchema),
|
|
33
|
+
onBeforeRemove: Optional(HookCommandsSchema),
|
|
34
|
+
branchNameGenerator: Optional(TypeString())
|
|
32
35
|
}, {
|
|
33
36
|
$id: "WorktreeSettingsConfig",
|
|
34
37
|
additionalProperties: false
|
|
@@ -73,7 +76,7 @@ function globToRegExp(pattern) {
|
|
|
73
76
|
const doubleStarReplaced = escaped.replace(/\*\*/g, "::DOUBLE_STAR::");
|
|
74
77
|
const singleStarReplaced = doubleStarReplaced.replace(/\*/g, "[^/]*");
|
|
75
78
|
const regexBody = singleStarReplaced.replace(/::DOUBLE_STAR::/g, ".*");
|
|
76
|
-
return new RegExp(
|
|
79
|
+
return new RegExp(regexBody, "i");
|
|
77
80
|
}
|
|
78
81
|
function globMatch(input, pattern) {
|
|
79
82
|
return globToRegExp(pattern).test(input);
|
|
@@ -340,11 +343,17 @@ async function cmdCd(args, ctx, deps) {
|
|
|
340
343
|
}
|
|
341
344
|
|
|
342
345
|
// src/cmds/cmdCreate.ts
|
|
343
|
-
import { join as join3 } from "path";
|
|
346
|
+
import { basename as basename3, join as join3 } from "path";
|
|
344
347
|
|
|
345
348
|
// src/cmds/shared.ts
|
|
346
349
|
import { appendFileSync as appendFileSync2, writeFileSync } from "fs";
|
|
347
350
|
import { spawn } from "child_process";
|
|
351
|
+
function sanitizePathPart(value) {
|
|
352
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
353
|
+
}
|
|
354
|
+
function resolveLogfilePath(template, values) {
|
|
355
|
+
return template.replace(/\{\{sessionId\}\}|\{sessionId\}/g, values.sessionId).replace(/\{\{name\}\}|\{name\}/g, values.name).replace(/\{\{timestamp\}\}|\{timestamp\}/g, values.timestamp);
|
|
356
|
+
}
|
|
348
357
|
var ANSI = {
|
|
349
358
|
reset: "\x1B[0m",
|
|
350
359
|
gray: "\x1B[90m",
|
|
@@ -411,8 +420,8 @@ function getDisplayLines(text, maxLines) {
|
|
|
411
420
|
}
|
|
412
421
|
return lines.slice(-maxLines);
|
|
413
422
|
}
|
|
414
|
-
function formatCommandList(commands, states, outputs, commandDisplay, logPath, displayOutputMaxLines = 5) {
|
|
415
|
-
const lines = [
|
|
423
|
+
function formatCommandList(commands, states, outputs, commandDisplay, hookName, logPath, displayOutputMaxLines = 5) {
|
|
424
|
+
const lines = [`${hookName} steps:`];
|
|
416
425
|
for (const [index, command] of commands.entries()) {
|
|
417
426
|
const state = states[index];
|
|
418
427
|
lines.push(formatCommandLine(command, state, commandDisplay));
|
|
@@ -483,18 +492,18 @@ function runCommand(command, cwd, onOutput) {
|
|
|
483
492
|
});
|
|
484
493
|
});
|
|
485
494
|
}
|
|
486
|
-
async function
|
|
487
|
-
if (!
|
|
495
|
+
async function runHook(createdCtx, hookValue, hookName, notify, options) {
|
|
496
|
+
if (!hookValue) {
|
|
488
497
|
return { success: true, executed: [] };
|
|
489
498
|
}
|
|
490
|
-
const commandTemplates = Array.isArray(
|
|
499
|
+
const commandTemplates = Array.isArray(hookValue) ? hookValue : [hookValue];
|
|
491
500
|
const commands = commandTemplates.map((template) => expandTemplate(template, createdCtx));
|
|
492
501
|
const executed = [];
|
|
493
502
|
const commandStates = commands.map(() => "pending");
|
|
494
503
|
const commandOutputs = commands.map(() => ({ stdout: "", stderr: "" }));
|
|
495
504
|
if (options?.logPath) {
|
|
496
505
|
writeFileSync(options.logPath, [
|
|
497
|
-
`# pi-worktree
|
|
506
|
+
`# pi-worktree ${hookName} log`,
|
|
498
507
|
`# worktree: ${createdCtx.path}`,
|
|
499
508
|
`# branch: ${createdCtx.branch}`,
|
|
500
509
|
""
|
|
@@ -510,13 +519,13 @@ async function runOnCreateHook(createdCtx, settings, notify, options) {
|
|
|
510
519
|
successColor: options?.cmdDisplaySuccessColor ?? "success",
|
|
511
520
|
errorColor: options?.cmdDisplayErrorColor ?? "error"
|
|
512
521
|
};
|
|
513
|
-
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, undefined, displayOutputMaxLines), "info");
|
|
522
|
+
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, hookName, undefined, displayOutputMaxLines), "info");
|
|
514
523
|
for (const [index, command] of commands.entries()) {
|
|
515
524
|
commandStates[index] = "running";
|
|
516
|
-
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, undefined, displayOutputMaxLines), "info");
|
|
525
|
+
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, hookName, undefined, displayOutputMaxLines), "info");
|
|
517
526
|
const result = await runCommand(command, createdCtx.path, (stream, chunk) => {
|
|
518
527
|
commandOutputs[index][stream] += chunk;
|
|
519
|
-
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, undefined, displayOutputMaxLines), "info");
|
|
528
|
+
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, hookName, undefined, displayOutputMaxLines), "info");
|
|
520
529
|
});
|
|
521
530
|
if (options?.logPath) {
|
|
522
531
|
appendCommandLog(options.logPath, command, result);
|
|
@@ -524,8 +533,8 @@ async function runOnCreateHook(createdCtx, settings, notify, options) {
|
|
|
524
533
|
executed.push(command);
|
|
525
534
|
if (!result.success) {
|
|
526
535
|
commandStates[index] = "failed";
|
|
527
|
-
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, options?.logPath, displayOutputMaxLines), "error");
|
|
528
|
-
notify(
|
|
536
|
+
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, hookName, options?.logPath, displayOutputMaxLines), "error");
|
|
537
|
+
notify(`${hookName} failed (exit ${result.code}): ${result.stderr.slice(0, 200)}${options?.logPath ? `
|
|
529
538
|
log: ${options.logPath}` : ""}`, "error");
|
|
530
539
|
return {
|
|
531
540
|
success: false,
|
|
@@ -538,11 +547,14 @@ log: ${options.logPath}` : ""}`, "error");
|
|
|
538
547
|
};
|
|
539
548
|
}
|
|
540
549
|
commandStates[index] = "success";
|
|
541
|
-
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, undefined, displayOutputMaxLines), "info");
|
|
550
|
+
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, hookName, undefined, displayOutputMaxLines), "info");
|
|
542
551
|
}
|
|
543
|
-
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, options?.logPath, displayOutputMaxLines), "info");
|
|
552
|
+
notify(formatCommandList(commands, commandStates, commandOutputs, commandDisplay, hookName, options?.logPath, displayOutputMaxLines), "info");
|
|
544
553
|
return { success: true, executed };
|
|
545
554
|
}
|
|
555
|
+
async function runOnCreateHook(createdCtx, settings, notify, options) {
|
|
556
|
+
return runHook(createdCtx, settings.onCreate, "onCreate", notify, options);
|
|
557
|
+
}
|
|
546
558
|
|
|
547
559
|
// src/services/config/config.ts
|
|
548
560
|
import { createConfigService } from "@zenobius/pi-extension-config";
|
|
@@ -949,29 +961,297 @@ var DefaultWorktreeSettings = {
|
|
|
949
961
|
};
|
|
950
962
|
var DefaultLogfileTemplate = DEFAULT_LOGFILE_TEMPLATE;
|
|
951
963
|
|
|
952
|
-
// src/cmds/
|
|
953
|
-
|
|
954
|
-
|
|
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, "");
|
|
955
971
|
}
|
|
956
|
-
function
|
|
957
|
-
return
|
|
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, `'\\''`)}'`;
|
|
958
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 = globalThis.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
|
+
globalThis.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
|
+
globalThis.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
|
+
|
|
1177
|
+
// src/cmds/cmdCreate.ts
|
|
959
1178
|
async function cmdCreate(args, ctx, deps) {
|
|
960
|
-
const
|
|
961
|
-
if (
|
|
962
|
-
ctx.ui.notify(
|
|
1179
|
+
const parsed = parseCreateCommandArgs(args);
|
|
1180
|
+
if ("error" in parsed) {
|
|
1181
|
+
ctx.ui.notify(parsed.error, "error");
|
|
963
1182
|
return;
|
|
964
1183
|
}
|
|
1184
|
+
const worktreeName = parsed.worktreeName;
|
|
965
1185
|
if (!isGitRepo(ctx.cwd)) {
|
|
966
1186
|
ctx.ui.notify("Not in a git repository", "error");
|
|
967
1187
|
return;
|
|
968
1188
|
}
|
|
969
1189
|
const current = deps.configService.current(ctx);
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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);
|
|
1209
|
+
if (existingWorktree) {
|
|
1210
|
+
if (!ctx.hasUI) {
|
|
1211
|
+
ctx.ui.notify(`Worktree already exists at: ${worktreePath}`, "error");
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const confirmMessage = current.onSwitch ? `Path: ${existingWorktree.path}
|
|
1215
|
+
Branch: ${existingWorktree.branch}
|
|
1216
|
+
|
|
1217
|
+
Switch to this worktree and run onSwitch?` : `Path: ${existingWorktree.path}
|
|
1218
|
+
Branch: ${existingWorktree.branch}
|
|
1219
|
+
|
|
1220
|
+
Switch to this worktree?`;
|
|
1221
|
+
const shouldSwitch = await ctx.ui.confirm("Worktree already exists", confirmMessage);
|
|
1222
|
+
if (!shouldSwitch) {
|
|
1223
|
+
ctx.ui.notify("Cancelled", "info");
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const existingCtx = {
|
|
1227
|
+
path: existingWorktree.path,
|
|
1228
|
+
name: basename3(existingWorktree.path),
|
|
1229
|
+
branch: existingWorktree.branch,
|
|
1230
|
+
...current
|
|
1231
|
+
};
|
|
1232
|
+
const sessionId2 = sanitizePathPart(ctx.sessionManager?.getSessionId?.() || "session");
|
|
1233
|
+
const safeName2 = sanitizePathPart(existingCtx.name);
|
|
1234
|
+
const timestamp2 = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1235
|
+
const logPath2 = resolveLogfilePath(current.logfile ?? DefaultLogfileTemplate, {
|
|
1236
|
+
sessionId: sessionId2,
|
|
1237
|
+
name: safeName2,
|
|
1238
|
+
timestamp: timestamp2
|
|
1239
|
+
});
|
|
1240
|
+
const result = await runHook(existingCtx, current.onSwitch, "onSwitch", ctx.ui.notify.bind(ctx.ui), {
|
|
1241
|
+
logPath: logPath2,
|
|
1242
|
+
displayOutputMaxLines: current.onCreateDisplayOutputMaxLines,
|
|
1243
|
+
cmdDisplayPending: current.onCreateCmdDisplayPending,
|
|
1244
|
+
cmdDisplaySuccess: current.onCreateCmdDisplaySuccess,
|
|
1245
|
+
cmdDisplayError: current.onCreateCmdDisplayError,
|
|
1246
|
+
cmdDisplayPendingColor: current.onCreateCmdDisplayPendingColor,
|
|
1247
|
+
cmdDisplaySuccessColor: current.onCreateCmdDisplaySuccessColor,
|
|
1248
|
+
cmdDisplayErrorColor: current.onCreateCmdDisplayErrorColor
|
|
1249
|
+
});
|
|
1250
|
+
if (!result.success) {
|
|
1251
|
+
ctx.ui.notify("onSwitch failed", "error");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
ctx.ui.notify(`Worktree path: ${existingWorktree.path}`, "info");
|
|
975
1255
|
return;
|
|
976
1256
|
}
|
|
977
1257
|
try {
|
|
@@ -980,21 +1260,25 @@ async function cmdCreate(args, ctx, deps) {
|
|
|
980
1260
|
return;
|
|
981
1261
|
} catch {}
|
|
982
1262
|
ensureExcluded(ctx.cwd, current.parentDir);
|
|
983
|
-
|
|
1263
|
+
const stopBusy = deps.statusService.busy(ctx, `Creating worktree: ${worktreeName}...`);
|
|
984
1264
|
try {
|
|
985
1265
|
git(["worktree", "add", "-b", branchName, worktreePath], current.mainWorktree);
|
|
1266
|
+
stopBusy();
|
|
1267
|
+
deps.statusService.positive(ctx, `Created: ${worktreeName}`);
|
|
986
1268
|
} catch (err) {
|
|
1269
|
+
stopBusy();
|
|
1270
|
+
deps.statusService.critical(ctx, `Failed to create worktree`);
|
|
987
1271
|
ctx.ui.notify(`Failed to create worktree: ${err.message}`, "error");
|
|
988
1272
|
return;
|
|
989
1273
|
}
|
|
990
1274
|
const createdCtx = {
|
|
991
1275
|
path: worktreePath,
|
|
992
|
-
name:
|
|
1276
|
+
name: worktreeName,
|
|
993
1277
|
branch: branchName,
|
|
994
1278
|
...current
|
|
995
1279
|
};
|
|
996
1280
|
const sessionId = sanitizePathPart(ctx.sessionManager?.getSessionId?.() || "session");
|
|
997
|
-
const safeName = sanitizePathPart(
|
|
1281
|
+
const safeName = sanitizePathPart(worktreeName);
|
|
998
1282
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
999
1283
|
const logPath = resolveLogfilePath(current.logfile ?? DefaultLogfileTemplate, {
|
|
1000
1284
|
sessionId,
|
|
@@ -1100,6 +1384,12 @@ ${finalConfig}`, "info");
|
|
|
1100
1384
|
}
|
|
1101
1385
|
|
|
1102
1386
|
// src/cmds/cmdList.ts
|
|
1387
|
+
import { basename as basename4 } from "path";
|
|
1388
|
+
function formatWorktreeOption(worktree) {
|
|
1389
|
+
const markers = [worktree.isMain ? "[main]" : "", worktree.isCurrent ? "[current]" : ""].filter(Boolean).join(" ");
|
|
1390
|
+
return `${worktree.branch}${markers ? " " + markers : ""}
|
|
1391
|
+
${worktree.path}`;
|
|
1392
|
+
}
|
|
1103
1393
|
var cmdList = async (_args, ctx, deps) => {
|
|
1104
1394
|
if (!isGitRepo(ctx.cwd)) {
|
|
1105
1395
|
ctx.ui.notify("Not in a git repository", "error");
|
|
@@ -1110,17 +1400,18 @@ var cmdList = async (_args, ctx, deps) => {
|
|
|
1110
1400
|
ctx.ui.notify("No worktrees found", "info");
|
|
1111
1401
|
return;
|
|
1112
1402
|
}
|
|
1113
|
-
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1403
|
+
if (!ctx.hasUI) {
|
|
1404
|
+
const lines = worktrees.map((worktree) => {
|
|
1405
|
+
const markers = [worktree.isMain ? "[main]" : "", worktree.isCurrent ? "[current]" : ""].filter(Boolean).join(" ");
|
|
1406
|
+
return `${worktree.branch}${markers ? " " + markers : ""}
|
|
1116
1407
|
${worktree.path}`;
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1408
|
+
});
|
|
1409
|
+
const configured = Array.from(deps.configService.worktrees.entries()).map(([pattern, settings]) => {
|
|
1410
|
+
return `${pattern}
|
|
1120
1411
|
${settings.worktreeRoot ?? settings.parentDir}
|
|
1121
1412
|
${settings.onCreate}`;
|
|
1122
|
-
|
|
1123
|
-
|
|
1413
|
+
});
|
|
1414
|
+
ctx.ui.notify(`Worktrees:
|
|
1124
1415
|
|
|
1125
1416
|
${lines.join(`
|
|
1126
1417
|
|
|
@@ -1131,10 +1422,69 @@ Configured:
|
|
|
1131
1422
|
${configured.join(`
|
|
1132
1423
|
|
|
1133
1424
|
`)}`, "info");
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
const options = worktrees.map(formatWorktreeOption);
|
|
1428
|
+
const byOption = new Map(options.map((option, index) => [option, worktrees[index]]));
|
|
1429
|
+
const selected = await ctx.ui.select("Select worktree to switch to", options);
|
|
1430
|
+
if (selected === undefined) {
|
|
1431
|
+
ctx.ui.notify("Cancelled", "info");
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const target = byOption.get(selected);
|
|
1435
|
+
if (!target) {
|
|
1436
|
+
ctx.ui.notify("Invalid selection", "error");
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const current = deps.configService.current({ cwd: target.path });
|
|
1440
|
+
if (!current.onSwitch) {
|
|
1441
|
+
ctx.ui.notify(`No onSwitch configured for: ${target.path}`, "info");
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const sessionId = sanitizePathPart(ctx.sessionManager?.getSessionId?.() || "session");
|
|
1445
|
+
const safeName = sanitizePathPart(basename4(target.path));
|
|
1446
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1447
|
+
const logPath = resolveLogfilePath(current.logfile ?? DefaultLogfileTemplate, {
|
|
1448
|
+
sessionId,
|
|
1449
|
+
name: safeName,
|
|
1450
|
+
timestamp
|
|
1451
|
+
});
|
|
1452
|
+
const createdCtx = {
|
|
1453
|
+
path: target.path,
|
|
1454
|
+
name: basename4(target.path),
|
|
1455
|
+
branch: target.branch,
|
|
1456
|
+
project: current.project,
|
|
1457
|
+
mainWorktree: current.mainWorktree
|
|
1458
|
+
};
|
|
1459
|
+
const stopBusy = deps.statusService.busy(ctx, `Running onSwitch for ${target.branch}...`);
|
|
1460
|
+
try {
|
|
1461
|
+
const result = await runHook(createdCtx, current.onSwitch, "onSwitch", ctx.ui.notify.bind(ctx.ui), {
|
|
1462
|
+
logPath,
|
|
1463
|
+
displayOutputMaxLines: current.onCreateDisplayOutputMaxLines,
|
|
1464
|
+
cmdDisplayPending: current.onCreateCmdDisplayPending,
|
|
1465
|
+
cmdDisplaySuccess: current.onCreateCmdDisplaySuccess,
|
|
1466
|
+
cmdDisplayError: current.onCreateCmdDisplayError,
|
|
1467
|
+
cmdDisplayPendingColor: current.onCreateCmdDisplayPendingColor,
|
|
1468
|
+
cmdDisplaySuccessColor: current.onCreateCmdDisplaySuccessColor,
|
|
1469
|
+
cmdDisplayErrorColor: current.onCreateCmdDisplayErrorColor
|
|
1470
|
+
});
|
|
1471
|
+
if (!result.success) {
|
|
1472
|
+
stopBusy();
|
|
1473
|
+
deps.statusService.critical(ctx, `onSwitch failed`);
|
|
1474
|
+
ctx.ui.notify(`onSwitch failed`, "error");
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
stopBusy();
|
|
1478
|
+
deps.statusService.positive(ctx, `onSwitch complete: ${target.branch}`);
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
stopBusy();
|
|
1481
|
+
deps.statusService.critical(ctx, `onSwitch failed`);
|
|
1482
|
+
ctx.ui.notify(`onSwitch failed: ${err.message}`, "error");
|
|
1483
|
+
}
|
|
1134
1484
|
};
|
|
1135
1485
|
|
|
1136
1486
|
// src/cmds/cmdPrune.ts
|
|
1137
|
-
async function cmdPrune(_args, ctx) {
|
|
1487
|
+
async function cmdPrune(_args, ctx, deps) {
|
|
1138
1488
|
if (!isGitRepo(ctx.cwd)) {
|
|
1139
1489
|
ctx.ui.notify("Not in a git repository", "error");
|
|
1140
1490
|
return;
|
|
@@ -1157,18 +1507,23 @@ ${dryRun}`);
|
|
|
1157
1507
|
ctx.ui.notify("Cancelled", "info");
|
|
1158
1508
|
return;
|
|
1159
1509
|
}
|
|
1510
|
+
const stopBusy = deps.statusService.busy(ctx, "Pruning stale worktrees...");
|
|
1160
1511
|
try {
|
|
1161
1512
|
git(["worktree", "prune"], ctx.cwd);
|
|
1513
|
+
stopBusy();
|
|
1514
|
+
deps.statusService.positive(ctx, "Pruned stale references");
|
|
1162
1515
|
ctx.ui.notify("\u2713 Stale worktree references pruned", "info");
|
|
1163
1516
|
} catch (err) {
|
|
1517
|
+
stopBusy();
|
|
1518
|
+
deps.statusService.critical(ctx, "Failed to prune");
|
|
1164
1519
|
ctx.ui.notify(`Failed to prune: ${err.message}`, "error");
|
|
1165
1520
|
}
|
|
1166
1521
|
}
|
|
1167
1522
|
|
|
1168
1523
|
// src/cmds/cmdRemove.ts
|
|
1169
|
-
import { basename as
|
|
1524
|
+
import { basename as basename5, join as join4 } from "path";
|
|
1170
1525
|
function findTarget(worktrees, worktreeName, parentDir) {
|
|
1171
|
-
return worktrees.find((worktree) =>
|
|
1526
|
+
return worktrees.find((worktree) => basename5(worktree.path) === worktreeName || worktree.path === worktreeName || worktree.path === join4(parentDir, worktreeName));
|
|
1172
1527
|
}
|
|
1173
1528
|
function isProtectedWorktree(worktree) {
|
|
1174
1529
|
return worktree.isMain || worktree.isCurrent;
|
|
@@ -1179,7 +1534,7 @@ async function pickWorktreeInteractively(ctx, worktrees) {
|
|
|
1179
1534
|
ctx.ui.notify("No removable worktrees found", "info");
|
|
1180
1535
|
return;
|
|
1181
1536
|
}
|
|
1182
|
-
const options = candidates.map((worktree) => `${
|
|
1537
|
+
const options = candidates.map((worktree) => `${basename5(worktree.path)} (${worktree.branch})
|
|
1183
1538
|
${worktree.path}`);
|
|
1184
1539
|
const byOption = new Map(options.map((option, index) => [option, candidates[index]]));
|
|
1185
1540
|
const selected = await ctx.ui.select("Select worktree to remove", options);
|
|
@@ -1189,7 +1544,7 @@ async function pickWorktreeInteractively(ctx, worktrees) {
|
|
|
1189
1544
|
}
|
|
1190
1545
|
return byOption.get(selected);
|
|
1191
1546
|
}
|
|
1192
|
-
async function removeWorktreeWithConfirm(ctx, cwd, target) {
|
|
1547
|
+
async function removeWorktreeWithConfirm(ctx, cwd, target, status, runBeforeRemove) {
|
|
1193
1548
|
const confirmed = await ctx.ui.confirm("Remove worktree?", `This will remove:
|
|
1194
1549
|
Path: ${target.path}
|
|
1195
1550
|
Branch: ${target.branch}
|
|
@@ -1199,19 +1554,34 @@ The branch will NOT be deleted.`);
|
|
|
1199
1554
|
ctx.ui.notify("Cancelled", "info");
|
|
1200
1555
|
return;
|
|
1201
1556
|
}
|
|
1557
|
+
if (runBeforeRemove) {
|
|
1558
|
+
const canContinue = await runBeforeRemove();
|
|
1559
|
+
if (!canContinue) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
const stopBusy = status.busy(ctx, "Removing worktree...");
|
|
1202
1564
|
try {
|
|
1203
1565
|
git(["worktree", "remove", target.path], cwd);
|
|
1566
|
+
stopBusy();
|
|
1567
|
+
status.positive(ctx, `Removed: ${target.path}`);
|
|
1204
1568
|
ctx.ui.notify(`\u2713 Worktree removed: ${target.path}`, "info");
|
|
1205
1569
|
} catch {
|
|
1570
|
+
stopBusy();
|
|
1206
1571
|
const forceConfirmed = await ctx.ui.confirm("Force remove?", "Worktree has uncommitted changes. Force remove anyway?");
|
|
1207
1572
|
if (!forceConfirmed) {
|
|
1208
1573
|
ctx.ui.notify("Cancelled", "info");
|
|
1209
1574
|
return;
|
|
1210
1575
|
}
|
|
1576
|
+
const stopForceBusy = status.busy(ctx, "Force removing worktree...");
|
|
1211
1577
|
try {
|
|
1212
1578
|
git(["worktree", "remove", "--force", target.path], cwd);
|
|
1579
|
+
stopForceBusy();
|
|
1580
|
+
status.positive(ctx, `Force removed: ${target.path}`);
|
|
1213
1581
|
ctx.ui.notify(`\u2713 Worktree force removed: ${target.path}`, "info");
|
|
1214
1582
|
} catch (forceErr) {
|
|
1583
|
+
stopForceBusy();
|
|
1584
|
+
status.critical(ctx, `Failed to remove`);
|
|
1215
1585
|
ctx.ui.notify(`Failed to remove: ${forceErr.message}`, "error");
|
|
1216
1586
|
}
|
|
1217
1587
|
}
|
|
@@ -1249,7 +1619,38 @@ async function cmdRemove(args, ctx, deps) {
|
|
|
1249
1619
|
return;
|
|
1250
1620
|
}
|
|
1251
1621
|
}
|
|
1252
|
-
|
|
1622
|
+
const current = deps.configService.current({ cwd: target.path });
|
|
1623
|
+
await removeWorktreeWithConfirm(ctx, ctx.cwd, target, deps.statusService, async () => {
|
|
1624
|
+
if (!current.onBeforeRemove) {
|
|
1625
|
+
return true;
|
|
1626
|
+
}
|
|
1627
|
+
const hookCtx = {
|
|
1628
|
+
path: target.path,
|
|
1629
|
+
name: basename5(target.path),
|
|
1630
|
+
branch: target.branch,
|
|
1631
|
+
project: current.project,
|
|
1632
|
+
mainWorktree: current.mainWorktree
|
|
1633
|
+
};
|
|
1634
|
+
const sessionId = sanitizePathPart(ctx.sessionManager?.getSessionId?.() || "session");
|
|
1635
|
+
const safeName = sanitizePathPart(hookCtx.name);
|
|
1636
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1637
|
+
const logPath = resolveLogfilePath(current.logfile ?? DefaultLogfileTemplate, {
|
|
1638
|
+
sessionId,
|
|
1639
|
+
name: safeName,
|
|
1640
|
+
timestamp
|
|
1641
|
+
});
|
|
1642
|
+
const result = await runHook(hookCtx, current.onBeforeRemove, "onBeforeRemove", ctx.ui.notify.bind(ctx.ui), {
|
|
1643
|
+
logPath,
|
|
1644
|
+
displayOutputMaxLines: current.onCreateDisplayOutputMaxLines,
|
|
1645
|
+
cmdDisplayPending: current.onCreateCmdDisplayPending,
|
|
1646
|
+
cmdDisplaySuccess: current.onCreateCmdDisplaySuccess,
|
|
1647
|
+
cmdDisplayError: current.onCreateCmdDisplayError,
|
|
1648
|
+
cmdDisplayPendingColor: current.onCreateCmdDisplayPendingColor,
|
|
1649
|
+
cmdDisplaySuccessColor: current.onCreateCmdDisplaySuccessColor,
|
|
1650
|
+
cmdDisplayErrorColor: current.onCreateCmdDisplayErrorColor
|
|
1651
|
+
});
|
|
1652
|
+
return result.success;
|
|
1653
|
+
});
|
|
1253
1654
|
}
|
|
1254
1655
|
|
|
1255
1656
|
// src/cmds/cmdSettings.ts
|
|
@@ -1444,6 +1845,74 @@ function createCompletionFactory(commands) {
|
|
|
1444
1845
|
};
|
|
1445
1846
|
}
|
|
1446
1847
|
|
|
1848
|
+
// src/ui/status.ts
|
|
1849
|
+
class StatusIndicator {
|
|
1850
|
+
statusKey;
|
|
1851
|
+
busyStyle;
|
|
1852
|
+
busyFrames;
|
|
1853
|
+
progressStyle = "bars";
|
|
1854
|
+
progressFrames;
|
|
1855
|
+
constructor(statusKey, options = {
|
|
1856
|
+
busy: "dots",
|
|
1857
|
+
progress: "bars"
|
|
1858
|
+
}) {
|
|
1859
|
+
this.statusKey = statusKey;
|
|
1860
|
+
this.busyStyle = options.busy || "dots";
|
|
1861
|
+
this.busyFrames = StatusIndicator.busyStyles[this.busyStyle];
|
|
1862
|
+
this.progressStyle = options.progress || "bars";
|
|
1863
|
+
this.progressFrames = StatusIndicator.progressStyles[this.progressStyle];
|
|
1864
|
+
}
|
|
1865
|
+
busy(ctx, message) {
|
|
1866
|
+
if (typeof ctx.ui.setStatus !== "function") {
|
|
1867
|
+
return () => {};
|
|
1868
|
+
}
|
|
1869
|
+
let i = 0;
|
|
1870
|
+
ctx.ui.setStatus(this.statusKey, `${this.busyFrames[i]} ${message}`);
|
|
1871
|
+
const timer = globalThis.setInterval(() => {
|
|
1872
|
+
i = (i + 1) % this.busyFrames.length;
|
|
1873
|
+
ctx.ui.setStatus?.(this.statusKey, `${this.busyFrames[i]} ${message}`);
|
|
1874
|
+
}, 100);
|
|
1875
|
+
return () => {
|
|
1876
|
+
globalThis.clearInterval(timer);
|
|
1877
|
+
ctx.ui.setStatus?.(this.statusKey, undefined);
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
cautious(ctx, message) {
|
|
1881
|
+
ctx.ui.setStatus?.(this.statusKey, `\u26A0\uFE0F ${message}`);
|
|
1882
|
+
}
|
|
1883
|
+
critical(ctx, message) {
|
|
1884
|
+
ctx.ui.setStatus?.(this.statusKey, `\u274C ${message}`);
|
|
1885
|
+
}
|
|
1886
|
+
positive(ctx, message) {
|
|
1887
|
+
ctx.ui.setStatus?.(this.statusKey, `\u2705 ${message}`);
|
|
1888
|
+
}
|
|
1889
|
+
informative(ctx, message) {
|
|
1890
|
+
ctx.ui.setStatus?.(this.statusKey, `\u2139\uFE0F ${message}`);
|
|
1891
|
+
}
|
|
1892
|
+
progress(ctx, message, percent) {
|
|
1893
|
+
const progressBar = this.progressFrames(percent);
|
|
1894
|
+
ctx.ui.setStatus?.(this.statusKey, `${progressBar} ${message}`);
|
|
1895
|
+
}
|
|
1896
|
+
static busyStyles = {
|
|
1897
|
+
dots: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
|
|
1898
|
+
};
|
|
1899
|
+
static progressStyles = {
|
|
1900
|
+
bars: (percent) => {
|
|
1901
|
+
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
1902
|
+
const progressBarLength = 20;
|
|
1903
|
+
const filledLength = Math.round(clampedPercent / 100 * progressBarLength);
|
|
1904
|
+
const emptyLength = progressBarLength - filledLength;
|
|
1905
|
+
return "\u2588".repeat(filledLength) + "\u2591".repeat(emptyLength);
|
|
1906
|
+
},
|
|
1907
|
+
pie: (percent) => {
|
|
1908
|
+
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
1909
|
+
const pieFrames = ["\u25CB", "\u25D4", "\u25D1", "\u25D5", "\u25CF"];
|
|
1910
|
+
const frameIndex = Math.floor(clampedPercent / 100 * (pieFrames.length - 1));
|
|
1911
|
+
return pieFrames[frameIndex];
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1447
1916
|
// src/index.ts
|
|
1448
1917
|
var HELP_TEXT = `
|
|
1449
1918
|
/worktree - Git worktree management
|
|
@@ -1451,20 +1920,24 @@ var HELP_TEXT = `
|
|
|
1451
1920
|
Commands:
|
|
1452
1921
|
/worktree init Configure worktree settings interactively
|
|
1453
1922
|
/worktree settings [key] [val] Get/set individual settings
|
|
1454
|
-
/worktree create <
|
|
1455
|
-
/worktree
|
|
1456
|
-
/worktree
|
|
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
|
|
1925
|
+
/worktree list List worktrees and run onSwitch for a selection
|
|
1926
|
+
/worktree remove <name> Remove a worktree (runs onBeforeRemove if set)
|
|
1457
1927
|
/worktree status Show current worktree info
|
|
1458
1928
|
/worktree cd <name> Print path to worktree
|
|
1459
1929
|
/worktree prune Clean up stale references
|
|
1460
1930
|
/worktree templates Show template variables preview
|
|
1461
1931
|
|
|
1462
|
-
Configuration (~/.pi/agent/pi-worktrees
|
|
1932
|
+
Configuration (~/.pi/agent/pi-worktrees.config.json):
|
|
1463
1933
|
{
|
|
1464
1934
|
"worktrees": {
|
|
1465
1935
|
"github.com/org/repo": {
|
|
1466
1936
|
"worktreeRoot": "~/work/org",
|
|
1467
|
-
"onCreate": ["mise install", "bun install"]
|
|
1937
|
+
"onCreate": ["mise install", "bun install"],
|
|
1938
|
+
"onSwitch": "mise run dev:resume",
|
|
1939
|
+
"onBeforeRemove": "bun test",
|
|
1940
|
+
"branchNameGenerator": "pi -p 'branch name for $PI_WORKTREE_PROMPT' --model local/model",
|
|
1468
1941
|
},
|
|
1469
1942
|
"github.com/org/*": {
|
|
1470
1943
|
"worktreeRoot": "~/work/org-other",
|
|
@@ -1490,7 +1963,13 @@ Pattern matching: exact URL > most-specific glob > fallback (worktree)
|
|
|
1490
1963
|
Matching strategies: fail-on-tie | first-wins | last-wins
|
|
1491
1964
|
|
|
1492
1965
|
Config note: parentDir is deprecated and supported as an alias for worktreeRoot.
|
|
1493
|
-
|
|
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.
|
|
1971
|
+
Hook vars: {{path}}, {{name}}, {{branch}}, {{project}}, {{mainWorktree}}
|
|
1972
|
+
Hooks: onCreate (new), onSwitch (existing), onBeforeRemove (pre-delete, non-zero blocks)
|
|
1494
1973
|
Logfile vars: {sessionId} / {{sessionId}}, {name} / {{name}}, {timestamp} / {{timestamp}}
|
|
1495
1974
|
`.trim();
|
|
1496
1975
|
var commands = {
|
|
@@ -1511,29 +1990,7 @@ var commands = {
|
|
|
1511
1990
|
};
|
|
1512
1991
|
var PiWorktreeExtension = async function(pi) {
|
|
1513
1992
|
const configService = await createPiWorktreeConfigService();
|
|
1514
|
-
const
|
|
1515
|
-
configService.events.on("MigrationFailed", () => {
|
|
1516
|
-
queue.push({ type: "error", msg: "MigrationFailed" });
|
|
1517
|
-
});
|
|
1518
|
-
configService.events.on("MigrationApplied", () => {
|
|
1519
|
-
queue.push({ type: "info", msg: "MigrationApplied" });
|
|
1520
|
-
});
|
|
1521
|
-
configService.events.on("ConfigLoading", () => {
|
|
1522
|
-
queue.push({ type: "info", msg: "ConfigLoading" });
|
|
1523
|
-
});
|
|
1524
|
-
configService.events.on("ConfigLoaded", () => {
|
|
1525
|
-
queue.push({ type: "info", msg: "ConfigLoaded" });
|
|
1526
|
-
});
|
|
1527
|
-
pi.on("session_start", async (event, ctx) => {
|
|
1528
|
-
await configService.ready;
|
|
1529
|
-
while (queue.length > 0) {
|
|
1530
|
-
const notification = queue.shift();
|
|
1531
|
-
if (!notification) {
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
ctx.ui.setStatus(`Worktrees`, notification.msg);
|
|
1535
|
-
}
|
|
1536
|
-
});
|
|
1993
|
+
const statusService = new StatusIndicator("pi-worktree");
|
|
1537
1994
|
const getSubcommandCompletions = createCompletionFactory(commands);
|
|
1538
1995
|
pi.registerCommand("worktree", {
|
|
1539
1996
|
description: "Git worktree management for isolated workspaces",
|
|
@@ -1552,7 +2009,8 @@ var PiWorktreeExtension = async function(pi) {
|
|
|
1552
2009
|
const settings = configService.current(ctx);
|
|
1553
2010
|
await command(rest.join(" "), ctx, {
|
|
1554
2011
|
settings,
|
|
1555
|
-
configService
|
|
2012
|
+
configService,
|
|
2013
|
+
statusService
|
|
1556
2014
|
});
|
|
1557
2015
|
} catch (error) {
|
|
1558
2016
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -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>;
|
|
@@ -4,6 +4,9 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
4
4
|
parentDir?: string | undefined;
|
|
5
5
|
onCreate?: string | string[] | undefined;
|
|
6
6
|
worktreeRoot?: string | undefined;
|
|
7
|
+
onSwitch?: string | string[] | undefined;
|
|
8
|
+
onBeforeRemove?: string | string[] | undefined;
|
|
9
|
+
branchNameGenerator?: string | undefined;
|
|
7
10
|
}>;
|
|
8
11
|
current: (ctx: {
|
|
9
12
|
cwd: string;
|
|
@@ -23,6 +26,9 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
23
26
|
matchedPattern: string | null;
|
|
24
27
|
onCreate?: string | string[] | undefined;
|
|
25
28
|
worktreeRoot?: string | undefined;
|
|
29
|
+
onSwitch?: string | string[] | undefined;
|
|
30
|
+
onBeforeRemove?: string | string[] | undefined;
|
|
31
|
+
branchNameGenerator?: string | undefined;
|
|
26
32
|
};
|
|
27
33
|
save: (data: PiWorktreeConfig) => Promise<void>;
|
|
28
34
|
config: {
|
|
@@ -31,6 +37,9 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
31
37
|
worktreeRoot: import("typebox").TOptional<import("typebox").TString>;
|
|
32
38
|
parentDir: import("typebox").TOptional<import("typebox").TString>;
|
|
33
39
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
40
|
+
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
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>;
|
|
34
43
|
}>>>;
|
|
35
44
|
matchingStrategy: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<"fail-on-tie">, import("typebox").TLiteral<"first-wins">, import("typebox").TLiteral<"last-wins">]>>;
|
|
36
45
|
logfile: import("typebox").TOptional<import("typebox").TString>;
|
|
@@ -45,6 +54,9 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
45
54
|
worktreeRoot: import("typebox").TOptional<import("typebox").TString>;
|
|
46
55
|
parentDir: import("typebox").TOptional<import("typebox").TString>;
|
|
47
56
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
57
|
+
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
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>;
|
|
48
60
|
}>> | undefined;
|
|
49
61
|
logfile?: string | undefined;
|
|
50
62
|
onCreateDisplayOutputMaxLines?: number | undefined;
|
|
@@ -65,6 +77,9 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
65
77
|
worktreeRoot: import("typebox").TOptional<import("typebox").TString>;
|
|
66
78
|
parentDir: import("typebox").TOptional<import("typebox").TString>;
|
|
67
79
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
80
|
+
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
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>;
|
|
68
83
|
}>>>;
|
|
69
84
|
matchingStrategy: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<"fail-on-tie">, import("typebox").TLiteral<"first-wins">, import("typebox").TLiteral<"last-wins">]>>;
|
|
70
85
|
logfile: import("typebox").TOptional<import("typebox").TString>;
|
|
@@ -79,6 +94,9 @@ export declare function createPiWorktreeConfigService(): Promise<{
|
|
|
79
94
|
worktreeRoot: import("typebox").TOptional<import("typebox").TString>;
|
|
80
95
|
parentDir: import("typebox").TOptional<import("typebox").TString>;
|
|
81
96
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
97
|
+
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
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>;
|
|
82
100
|
}>> | undefined;
|
|
83
101
|
logfile?: string | undefined;
|
|
84
102
|
onCreateDisplayOutputMaxLines?: number | undefined;
|
|
@@ -3,6 +3,9 @@ declare const WorktreeSettingsSchema: import("typebox").TObject<{
|
|
|
3
3
|
worktreeRoot: import("typebox").TOptional<import("typebox").TString>;
|
|
4
4
|
parentDir: import("typebox").TOptional<import("typebox").TString>;
|
|
5
5
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
6
|
+
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
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>;
|
|
6
9
|
}>;
|
|
7
10
|
declare const MatchingStrategySchema: import("typebox").TUnion<[import("typebox").TLiteral<"fail-on-tie">, import("typebox").TLiteral<"first-wins">, import("typebox").TLiteral<"last-wins">]>;
|
|
8
11
|
declare const MatchStrategyResultSchema: import("typebox").TUnion<[import("typebox").TLiteral<"exact">, import("typebox").TLiteral<"unmatched">]>;
|
|
@@ -11,6 +14,9 @@ export declare const PiWorktreeConfigSchema: import("typebox").TObject<{
|
|
|
11
14
|
worktreeRoot: import("typebox").TOptional<import("typebox").TString>;
|
|
12
15
|
parentDir: import("typebox").TOptional<import("typebox").TString>;
|
|
13
16
|
onCreate: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
17
|
+
onSwitch: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TArray<import("typebox").TString>]>>;
|
|
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>;
|
|
14
20
|
}>>>;
|
|
15
21
|
matchingStrategy: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<"fail-on-tie">, import("typebox").TLiteral<"first-wins">, import("typebox").TLiteral<"last-wins">]>>;
|
|
16
22
|
logfile: import("typebox").TOptional<import("typebox").TString>;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from '@mariozechner/pi-coding-agent';
|
|
2
2
|
import type { PiWorktreeConfigService } from './services/config/config.ts';
|
|
3
3
|
import { WorktreeSettingsConfig } from './services/config/schema.ts';
|
|
4
|
+
import { StatusIndicator } from './ui/status.ts';
|
|
4
5
|
export interface WorktreeCreatedContext {
|
|
5
6
|
path: string;
|
|
6
7
|
name: string;
|
|
@@ -11,5 +12,6 @@ export interface WorktreeCreatedContext {
|
|
|
11
12
|
export interface CommandDeps {
|
|
12
13
|
settings: WorktreeSettingsConfig;
|
|
13
14
|
configService: PiWorktreeConfigService;
|
|
15
|
+
statusService: StatusIndicator;
|
|
14
16
|
}
|
|
15
17
|
export type CmdHandler = (...args: [string, ExtensionCommandContext, CommandDeps]) => Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ExtensionCommandContext } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
type StatusOptions = {
|
|
3
|
+
busy?: keyof typeof StatusIndicator.busyStyles;
|
|
4
|
+
progress?: keyof typeof StatusIndicator.progressStyles;
|
|
5
|
+
};
|
|
6
|
+
export declare class StatusIndicator {
|
|
7
|
+
statusKey: string;
|
|
8
|
+
busyStyle: keyof typeof StatusIndicator.busyStyles;
|
|
9
|
+
private busyFrames;
|
|
10
|
+
private progressStyle;
|
|
11
|
+
private progressFrames;
|
|
12
|
+
constructor(statusKey: string, options?: StatusOptions);
|
|
13
|
+
busy(ctx: ExtensionCommandContext, message: string): () => void;
|
|
14
|
+
cautious(ctx: ExtensionCommandContext, message: string): void;
|
|
15
|
+
critical(ctx: ExtensionCommandContext, message: string): void;
|
|
16
|
+
positive(ctx: ExtensionCommandContext, message: string): void;
|
|
17
|
+
informative(ctx: ExtensionCommandContext, message: string): void;
|
|
18
|
+
progress(ctx: ExtensionCommandContext, message: string, percent: number): void;
|
|
19
|
+
static busyStyles: {
|
|
20
|
+
dots: string[];
|
|
21
|
+
};
|
|
22
|
+
static progressStyles: {
|
|
23
|
+
bars: (percent: number) => string;
|
|
24
|
+
pie: (percent: number) => string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export {};
|