@zenobius/pi-worktrees 0.0.1

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -0
  3. package/dist/index.js +615 -0
  4. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 zenobi.us
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # pi-worktrees
2
+
3
+ Worktrees extension for Pi Coding Agent
4
+
5
+ > A Bun module created from the [bun-module](https://github.com/zenobi-us/bun-module) template
6
+
7
+ ## Features
8
+
9
+ - Git worktree management commands for Pi
10
+ - Interactive setup for worktree settings
11
+ - Automatic worktree listing, creation, removal, and pruning
12
+
13
+ ## Usage
14
+
15
+ Run in Pi:
16
+
17
+ ```
18
+ /worktree init
19
+ /worktree create <feature-name>
20
+ /worktree list
21
+ /worktree status
22
+ /worktree cd <name>
23
+ /worktree remove <name>
24
+ /worktree prune
25
+ ```
26
+
27
+ ## Development
28
+
29
+ - `mise run build` - Build the module
30
+ - `mise run test` - Run tests
31
+ - `mise run lint` - Lint code
32
+ - `mise run lint:fix` - Fix linting issues
33
+ - `mise run format` - Format code with Prettier
34
+
35
+ ## License
36
+
37
+ MIT License. See the [LICENSE](LICENSE) file for details.
package/dist/index.js ADDED
@@ -0,0 +1,615 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import { execSync, spawn } from "child_process";
4
+ import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync, statSync } from "fs";
5
+ import { dirname, basename, join, resolve, relative } from "path";
6
+ import { homedir } from "os";
7
+ var HELP_TEXT = `
8
+ /worktree - Git worktree management
9
+
10
+ Commands:
11
+ /worktree init Configure worktree settings interactively
12
+ /worktree settings [key] [val] Get/set individual settings
13
+ /worktree create <feature-name> Create new worktree with branch
14
+ /worktree list List all worktrees
15
+ /worktree remove <name> Remove a worktree
16
+ /worktree status Show current worktree info
17
+ /worktree cd <name> Print path to worktree
18
+ /worktree prune Clean up stale references
19
+
20
+ Settings:
21
+ /worktree settings Show all settings
22
+ /worktree settings parentDir Get parentDir value
23
+ /worktree settings parentDir ~ Set parentDir value
24
+ /worktree settings onCreate Get onCreate value
25
+ /worktree settings onCreate "" Clear onCreate value
26
+
27
+ Configuration (~/.pi/settings.json):
28
+ {
29
+ "worktree": {
30
+ "parentDir": "...", // Override default parent directory
31
+ "onCreate": "mise setup" // Command to run after creation
32
+ }
33
+ }
34
+
35
+ Template vars: {{path}}, {{name}}, {{branch}}, {{project}}
36
+
37
+ Examples:
38
+ /worktree init
39
+ /worktree settings parentDir "~/.worktrees/{{project}}"
40
+ /worktree create auth-feature
41
+ /worktree list
42
+ /worktree cd auth-feature
43
+ /worktree remove auth-feature
44
+ `.trim();
45
+ function git(args, cwd) {
46
+ try {
47
+ return execSync(`git ${args.join(" ")}`, {
48
+ cwd,
49
+ encoding: "utf-8",
50
+ stdio: ["pipe", "pipe", "pipe"]
51
+ }).trim();
52
+ } catch (error) {
53
+ throw new Error(`git ${args[0]} failed: ${error.message}`);
54
+ }
55
+ }
56
+ function isGitRepo(cwd) {
57
+ try {
58
+ git(["rev-parse", "--git-dir"], cwd);
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ function getMainWorktreePath(cwd) {
65
+ const gitCommonDir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], cwd);
66
+ return dirname(gitCommonDir);
67
+ }
68
+ function getProjectName(cwd) {
69
+ return basename(getMainWorktreePath(cwd));
70
+ }
71
+ function isWorktree(cwd) {
72
+ try {
73
+ const gitDir = git(["rev-parse", "--git-dir"], cwd);
74
+ const gitPath = join(cwd, ".git");
75
+ if (existsSync(gitPath)) {
76
+ const stat = statSync(gitPath);
77
+ return stat.isFile();
78
+ }
79
+ return false;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+ function getCurrentBranch(cwd) {
85
+ try {
86
+ return git(["branch", "--show-current"], cwd) || "HEAD (detached)";
87
+ } catch {
88
+ return "unknown";
89
+ }
90
+ }
91
+ function listWorktrees(cwd) {
92
+ const output = git(["worktree", "list", "--porcelain"], cwd);
93
+ const worktrees = [];
94
+ const currentPath = resolve(cwd);
95
+ const mainPath = getMainWorktreePath(cwd);
96
+ let current = {};
97
+ for (const line of output.split(`
98
+ `)) {
99
+ if (line.startsWith("worktree ")) {
100
+ current.path = line.slice(9);
101
+ } else if (line.startsWith("HEAD ")) {
102
+ current.head = line.slice(5);
103
+ } else if (line.startsWith("branch ")) {
104
+ current.branch = line.slice(7).replace("refs/heads/", "");
105
+ } else if (line === "detached") {
106
+ current.branch = "HEAD (detached)";
107
+ } else if (line === "") {
108
+ if (current.path) {
109
+ worktrees.push({
110
+ path: current.path,
111
+ branch: current.branch || "unknown",
112
+ head: current.head || "unknown",
113
+ isMain: current.path === mainPath,
114
+ isCurrent: current.path === currentPath
115
+ });
116
+ }
117
+ current = {};
118
+ }
119
+ }
120
+ if (current.path) {
121
+ worktrees.push({
122
+ path: current.path,
123
+ branch: current.branch || "unknown",
124
+ head: current.head || "unknown",
125
+ isMain: current.path === mainPath,
126
+ isCurrent: current.path === currentPath
127
+ });
128
+ }
129
+ return worktrees;
130
+ }
131
+ function getSettingsPath() {
132
+ return join(homedir(), ".pi", "settings.json");
133
+ }
134
+ function loadFullSettings() {
135
+ const settingsPath = getSettingsPath();
136
+ try {
137
+ if (existsSync(settingsPath)) {
138
+ const content = readFileSync(settingsPath, "utf-8");
139
+ return JSON.parse(content);
140
+ }
141
+ } catch {}
142
+ return {};
143
+ }
144
+ function loadSettings() {
145
+ const settings = loadFullSettings();
146
+ return settings.worktree || {};
147
+ }
148
+ function saveSettings(worktreeSettings) {
149
+ const settingsPath = getSettingsPath();
150
+ const settingsDir = dirname(settingsPath);
151
+ if (!existsSync(settingsDir)) {
152
+ mkdirSync(settingsDir, { recursive: true });
153
+ }
154
+ const fullSettings = loadFullSettings();
155
+ fullSettings.worktree = worktreeSettings;
156
+ writeFileSync(settingsPath, JSON.stringify(fullSettings, null, 2) + `
157
+ `, "utf-8");
158
+ }
159
+ function expandTemplate(template, ctx) {
160
+ return template.replace(/\{\{path\}\}/g, ctx.path).replace(/\{\{name\}\}/g, ctx.name).replace(/\{\{branch\}\}/g, ctx.branch).replace(/\{\{project\}\}/g, ctx.project).replace(/^~/, homedir());
161
+ }
162
+ function getWorktreeParentDir(cwd, settings) {
163
+ const project = getProjectName(cwd);
164
+ const mainWorktree = getMainWorktreePath(cwd);
165
+ if (settings.parentDir) {
166
+ return expandTemplate(settings.parentDir, {
167
+ path: "",
168
+ name: "",
169
+ branch: "",
170
+ project,
171
+ mainWorktree
172
+ });
173
+ }
174
+ return join(dirname(mainWorktree), `${project}.worktrees`);
175
+ }
176
+ function isPathInsideRepo(repoPath, targetPath) {
177
+ const relPath = relative(repoPath, targetPath);
178
+ return !relPath.startsWith("..") && !relPath.startsWith("/");
179
+ }
180
+ function ensureExcluded(cwd, worktreeParentDir) {
181
+ const mainWorktree = getMainWorktreePath(cwd);
182
+ if (!isPathInsideRepo(mainWorktree, worktreeParentDir)) {
183
+ return;
184
+ }
185
+ const excludePath = join(mainWorktree, ".git", "info", "exclude");
186
+ const relPath = relative(mainWorktree, worktreeParentDir);
187
+ const excludePattern = `/${relPath}/`;
188
+ try {
189
+ let content = "";
190
+ if (existsSync(excludePath)) {
191
+ content = readFileSync(excludePath, "utf-8");
192
+ }
193
+ if (content.includes(excludePattern) || content.includes(relPath)) {
194
+ return;
195
+ }
196
+ const newEntry = `
197
+ # Worktree directory (added by worktree extension)
198
+ ${excludePattern}
199
+ `;
200
+ appendFileSync(excludePath, newEntry);
201
+ } catch {}
202
+ }
203
+ async function runOnCreateHook(ctx, settings, notify) {
204
+ const { onCreate } = settings;
205
+ if (!onCreate) {
206
+ return;
207
+ }
208
+ if (typeof onCreate === "string") {
209
+ const command = expandTemplate(onCreate, ctx);
210
+ notify(`Running: ${command}`, "info");
211
+ return new Promise((resolve2, reject) => {
212
+ const child = spawn(command, {
213
+ cwd: ctx.path,
214
+ shell: true,
215
+ stdio: ["ignore", "pipe", "pipe"]
216
+ });
217
+ let stdout = "";
218
+ let stderr = "";
219
+ child.stdout?.on("data", (data) => {
220
+ stdout += data.toString();
221
+ });
222
+ child.stderr?.on("data", (data) => {
223
+ stderr += data.toString();
224
+ });
225
+ child.on("close", (code) => {
226
+ if (code === 0) {
227
+ if (stdout.trim()) {
228
+ notify(stdout.trim().slice(0, 200), "info");
229
+ }
230
+ resolve2();
231
+ } else {
232
+ notify(`onCreate failed (exit ${code}): ${stderr.slice(0, 200)}`, "error");
233
+ resolve2();
234
+ }
235
+ });
236
+ child.on("error", (err) => {
237
+ notify(`onCreate error: ${err.message}`, "error");
238
+ resolve2();
239
+ });
240
+ });
241
+ } else if (typeof onCreate === "function") {
242
+ try {
243
+ await onCreate(ctx);
244
+ } catch (err) {
245
+ notify(`onCreate error: ${err.message}`, "error");
246
+ }
247
+ }
248
+ }
249
+ async function handleInit(_args, ctx) {
250
+ if (!ctx.hasUI) {
251
+ ctx.ui.notify("init requires interactive mode", "error");
252
+ return;
253
+ }
254
+ const currentSettings = loadSettings();
255
+ const settingsPath = getSettingsPath();
256
+ ctx.ui.notify(`Worktree Extension Setup
257
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`, "info");
258
+ if (currentSettings.parentDir || currentSettings.onCreate) {
259
+ const current = [
260
+ "Current settings:",
261
+ currentSettings.parentDir ? ` parentDir: ${currentSettings.parentDir}` : null,
262
+ currentSettings.onCreate ? ` onCreate: ${currentSettings.onCreate}` : null
263
+ ].filter(Boolean).join(`
264
+ `);
265
+ ctx.ui.notify(current, "info");
266
+ }
267
+ const PARENT_DIR_DEFAULT = "Default (../{{project}}.worktrees/)";
268
+ const PARENT_DIR_GLOBAL = "Global (~/.local/share/worktrees/{{project}})";
269
+ const PARENT_DIR_CUSTOM = "Custom path...";
270
+ const PARENT_DIR_KEEP = "Keep current";
271
+ const parentDirOptions = [
272
+ PARENT_DIR_DEFAULT,
273
+ PARENT_DIR_GLOBAL,
274
+ PARENT_DIR_CUSTOM,
275
+ currentSettings.parentDir ? PARENT_DIR_KEEP : null
276
+ ].filter(Boolean);
277
+ const parentDirChoice = await ctx.ui.select("Where should worktrees be created?", parentDirOptions);
278
+ if (parentDirChoice === undefined) {
279
+ ctx.ui.notify("Setup cancelled", "info");
280
+ return;
281
+ }
282
+ let parentDir;
283
+ if (parentDirChoice === PARENT_DIR_DEFAULT) {
284
+ parentDir = undefined;
285
+ } else if (parentDirChoice === PARENT_DIR_GLOBAL) {
286
+ parentDir = "~/.local/share/worktrees/{{project}}";
287
+ } else if (parentDirChoice === PARENT_DIR_CUSTOM) {
288
+ const customPath = await ctx.ui.input("Enter custom path (supports {{project}}, {{name}}):", currentSettings.parentDir || "../{{project}}.worktrees");
289
+ if (customPath === undefined) {
290
+ ctx.ui.notify("Setup cancelled", "info");
291
+ return;
292
+ }
293
+ parentDir = customPath || undefined;
294
+ } else if (parentDirChoice === PARENT_DIR_KEEP) {
295
+ parentDir = currentSettings.parentDir;
296
+ }
297
+ const onCreate = await ctx.ui.input(`Enter command to run after creating worktree (or leave empty):
298
+ Supports: {{path}}, {{name}}, {{branch}}, {{project}}`, currentSettings.onCreate && typeof currentSettings.onCreate === "string" ? currentSettings.onCreate : "mise setup");
299
+ if (onCreate === undefined) {
300
+ ctx.ui.notify("Setup cancelled", "info");
301
+ return;
302
+ }
303
+ const newSettings = {};
304
+ if (parentDir) {
305
+ newSettings.parentDir = parentDir;
306
+ }
307
+ if (onCreate && onCreate.trim()) {
308
+ newSettings.onCreate = onCreate.trim();
309
+ }
310
+ const preview = [
311
+ "Settings to save:",
312
+ "",
313
+ newSettings.parentDir ? ` parentDir: "${newSettings.parentDir}"` : " parentDir: (default)",
314
+ newSettings.onCreate ? ` onCreate: "${newSettings.onCreate}"` : " onCreate: (none)",
315
+ "",
316
+ `File: ${settingsPath}`
317
+ ].join(`
318
+ `);
319
+ const confirmed = await ctx.ui.confirm("Save settings?", preview);
320
+ if (!confirmed) {
321
+ ctx.ui.notify("Setup cancelled", "info");
322
+ return;
323
+ }
324
+ try {
325
+ saveSettings(newSettings);
326
+ ctx.ui.notify(`\u2713 Settings saved to ${settingsPath}`, "info");
327
+ const finalConfig = JSON.stringify({ worktree: newSettings }, null, 2);
328
+ ctx.ui.notify(`Configuration:
329
+ ${finalConfig}`, "info");
330
+ } catch (err) {
331
+ ctx.ui.notify(`Failed to save settings: ${err.message}`, "error");
332
+ }
333
+ }
334
+ var VALID_SETTING_KEYS = ["parentDir", "onCreate"];
335
+ async function handleSettings(args, ctx) {
336
+ const currentSettings = loadSettings();
337
+ const settingsPath = getSettingsPath();
338
+ const parts = args.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
339
+ const key = parts[0]?.trim();
340
+ const value = parts.slice(1).join(" ").replace(/^"(.*)"$/, "$1");
341
+ if (!key) {
342
+ const lines = [
343
+ "Worktree Settings:",
344
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
345
+ "",
346
+ `parentDir: ${currentSettings.parentDir || "(default: ../<project>.worktrees/)"}`,
347
+ `onCreate: ${currentSettings.onCreate || "(none)"}`,
348
+ "",
349
+ `File: ${settingsPath}`
350
+ ];
351
+ ctx.ui.notify(lines.join(`
352
+ `), "info");
353
+ return;
354
+ }
355
+ if (!VALID_SETTING_KEYS.includes(key)) {
356
+ ctx.ui.notify(`Invalid setting key: "${key}"
357
+ Valid keys: ${VALID_SETTING_KEYS.join(", ")}`, "error");
358
+ return;
359
+ }
360
+ if (!value && parts.length === 1) {
361
+ const currentValue = currentSettings[key];
362
+ if (currentValue) {
363
+ ctx.ui.notify(`${key}: ${currentValue}`, "info");
364
+ } else {
365
+ const defaults = {
366
+ parentDir: "(default: ../<project>.worktrees/)",
367
+ onCreate: "(none)"
368
+ };
369
+ ctx.ui.notify(`${key}: ${defaults[key]}`, "info");
370
+ }
371
+ return;
372
+ }
373
+ const newSettings = { ...currentSettings };
374
+ if (value === "" || value === '""' || value === "null" || value === "clear") {
375
+ delete newSettings[key];
376
+ ctx.ui.notify(`\u2713 Cleared ${key}`, "info");
377
+ } else {
378
+ newSettings[key] = value;
379
+ ctx.ui.notify(`\u2713 Set ${key} = "${value}"`, "info");
380
+ }
381
+ try {
382
+ saveSettings(newSettings);
383
+ } catch (err) {
384
+ ctx.ui.notify(`Failed to save settings: ${err.message}`, "error");
385
+ }
386
+ }
387
+ async function handleCreate(args, ctx) {
388
+ const featureName = args.trim();
389
+ if (!featureName) {
390
+ ctx.ui.notify("Usage: /worktree create <feature-name>", "error");
391
+ return;
392
+ }
393
+ if (!isGitRepo(ctx.cwd)) {
394
+ ctx.ui.notify("Not in a git repository", "error");
395
+ return;
396
+ }
397
+ const settings = loadSettings();
398
+ const project = getProjectName(ctx.cwd);
399
+ const mainWorktree = getMainWorktreePath(ctx.cwd);
400
+ const parentDir = getWorktreeParentDir(ctx.cwd, settings);
401
+ const worktreePath = join(parentDir, featureName);
402
+ const branchName = `feature/${featureName}`;
403
+ const existing = listWorktrees(ctx.cwd);
404
+ if (existing.some((w) => w.path === worktreePath)) {
405
+ ctx.ui.notify(`Worktree already exists at: ${worktreePath}`, "error");
406
+ return;
407
+ }
408
+ try {
409
+ git(["rev-parse", "--verify", branchName], ctx.cwd);
410
+ ctx.ui.notify(`Branch '${branchName}' already exists. Use a different name.`, "error");
411
+ return;
412
+ } catch {}
413
+ ensureExcluded(ctx.cwd, parentDir);
414
+ ctx.ui.notify(`Creating worktree: ${featureName}`, "info");
415
+ try {
416
+ git(["worktree", "add", "-b", branchName, worktreePath], mainWorktree);
417
+ } catch (err) {
418
+ ctx.ui.notify(`Failed to create worktree: ${err.message}`, "error");
419
+ return;
420
+ }
421
+ const createdCtx = {
422
+ path: worktreePath,
423
+ name: featureName,
424
+ branch: branchName,
425
+ project,
426
+ mainWorktree
427
+ };
428
+ await runOnCreateHook(createdCtx, settings, ctx.ui.notify.bind(ctx.ui));
429
+ ctx.ui.notify(`\u2713 Worktree created!
430
+ Path: ${worktreePath}
431
+ Branch: ${branchName}`, "info");
432
+ }
433
+ async function handleList(_args, ctx) {
434
+ if (!isGitRepo(ctx.cwd)) {
435
+ ctx.ui.notify("Not in a git repository", "error");
436
+ return;
437
+ }
438
+ const worktrees = listWorktrees(ctx.cwd);
439
+ if (worktrees.length === 0) {
440
+ ctx.ui.notify("No worktrees found", "info");
441
+ return;
442
+ }
443
+ const lines = worktrees.map((w) => {
444
+ const markers = [
445
+ w.isMain ? "[main]" : "",
446
+ w.isCurrent ? "[current]" : ""
447
+ ].filter(Boolean).join(" ");
448
+ return `${w.branch}${markers ? " " + markers : ""}
449
+ ${w.path}`;
450
+ });
451
+ ctx.ui.notify(`Worktrees:
452
+
453
+ ${lines.join(`
454
+
455
+ `)}`, "info");
456
+ }
457
+ async function handleRemove(args, ctx) {
458
+ const worktreeName = args.trim();
459
+ if (!worktreeName) {
460
+ ctx.ui.notify("Usage: /worktree remove <name>", "error");
461
+ return;
462
+ }
463
+ if (!isGitRepo(ctx.cwd)) {
464
+ ctx.ui.notify("Not in a git repository", "error");
465
+ return;
466
+ }
467
+ const worktrees = listWorktrees(ctx.cwd);
468
+ const settings = loadSettings();
469
+ const parentDir = getWorktreeParentDir(ctx.cwd, settings);
470
+ const target = worktrees.find((w) => basename(w.path) === worktreeName || w.path === worktreeName || w.path === join(parentDir, worktreeName));
471
+ if (!target) {
472
+ ctx.ui.notify(`Worktree not found: ${worktreeName}`, "error");
473
+ return;
474
+ }
475
+ if (target.isMain) {
476
+ ctx.ui.notify("Cannot remove the main worktree", "error");
477
+ return;
478
+ }
479
+ if (target.isCurrent) {
480
+ ctx.ui.notify("Cannot remove the current worktree. Switch to another first.", "error");
481
+ return;
482
+ }
483
+ const confirmed = await ctx.ui.confirm(`Remove worktree?`, `This will remove:
484
+ Path: ${target.path}
485
+ Branch: ${target.branch}
486
+
487
+ The branch will NOT be deleted.`);
488
+ if (!confirmed) {
489
+ ctx.ui.notify("Cancelled", "info");
490
+ return;
491
+ }
492
+ try {
493
+ git(["worktree", "remove", target.path], ctx.cwd);
494
+ ctx.ui.notify(`\u2713 Worktree removed: ${target.path}`, "info");
495
+ } catch (err) {
496
+ const forceConfirmed = await ctx.ui.confirm("Force remove?", `Worktree has uncommitted changes. Force remove anyway?`);
497
+ if (forceConfirmed) {
498
+ try {
499
+ git(["worktree", "remove", "--force", target.path], ctx.cwd);
500
+ ctx.ui.notify(`\u2713 Worktree force removed: ${target.path}`, "info");
501
+ } catch (forceErr) {
502
+ ctx.ui.notify(`Failed to remove: ${forceErr.message}`, "error");
503
+ }
504
+ } else {
505
+ ctx.ui.notify("Cancelled", "info");
506
+ }
507
+ }
508
+ }
509
+ async function handleStatus(_args, ctx) {
510
+ if (!isGitRepo(ctx.cwd)) {
511
+ ctx.ui.notify("Not in a git repository", "error");
512
+ return;
513
+ }
514
+ const isWt = isWorktree(ctx.cwd);
515
+ const mainPath = getMainWorktreePath(ctx.cwd);
516
+ const project = getProjectName(ctx.cwd);
517
+ const branch = getCurrentBranch(ctx.cwd);
518
+ const worktrees = listWorktrees(ctx.cwd);
519
+ const current = worktrees.find((w) => w.isCurrent);
520
+ const status = [
521
+ `Project: ${project}`,
522
+ `Current path: ${ctx.cwd}`,
523
+ `Branch: ${branch}`,
524
+ `Is worktree: ${isWt ? "Yes" : "No (main repository)"}`,
525
+ `Main worktree: ${mainPath}`,
526
+ `Total worktrees: ${worktrees.length}`
527
+ ];
528
+ ctx.ui.notify(status.join(`
529
+ `), "info");
530
+ }
531
+ async function handleCd(args, ctx) {
532
+ const worktreeName = args.trim();
533
+ if (!isGitRepo(ctx.cwd)) {
534
+ ctx.ui.notify("Not in a git repository", "error");
535
+ return;
536
+ }
537
+ const worktrees = listWorktrees(ctx.cwd);
538
+ const settings = loadSettings();
539
+ const parentDir = getWorktreeParentDir(ctx.cwd, settings);
540
+ if (!worktreeName) {
541
+ const main = worktrees.find((w) => w.isMain);
542
+ if (main) {
543
+ console.log(main.path);
544
+ ctx.ui.notify(`Main worktree: ${main.path}`, "info");
545
+ }
546
+ return;
547
+ }
548
+ const target = worktrees.find((w) => basename(w.path) === worktreeName || w.path === worktreeName || w.path === join(parentDir, worktreeName));
549
+ if (!target) {
550
+ ctx.ui.notify(`Worktree not found: ${worktreeName}`, "error");
551
+ return;
552
+ }
553
+ console.log(target.path);
554
+ ctx.ui.notify(`Worktree path: ${target.path}`, "info");
555
+ }
556
+ async function handlePrune(_args, ctx) {
557
+ if (!isGitRepo(ctx.cwd)) {
558
+ ctx.ui.notify("Not in a git repository", "error");
559
+ return;
560
+ }
561
+ let dryRun;
562
+ try {
563
+ dryRun = git(["worktree", "prune", "--dry-run"], ctx.cwd);
564
+ } catch (err) {
565
+ ctx.ui.notify(`Failed to check stale worktrees: ${err.message}`, "error");
566
+ return;
567
+ }
568
+ if (!dryRun.trim()) {
569
+ ctx.ui.notify("No stale worktree references to prune", "info");
570
+ return;
571
+ }
572
+ const confirmed = await ctx.ui.confirm("Prune stale worktrees?", `The following stale references will be removed:
573
+
574
+ ${dryRun}`);
575
+ if (!confirmed) {
576
+ ctx.ui.notify("Cancelled", "info");
577
+ return;
578
+ }
579
+ try {
580
+ git(["worktree", "prune"], ctx.cwd);
581
+ ctx.ui.notify("\u2713 Stale worktree references pruned", "info");
582
+ } catch (err) {
583
+ ctx.ui.notify(`Failed to prune: ${err.message}`, "error");
584
+ }
585
+ }
586
+ var commands = {
587
+ init: handleInit,
588
+ settings: handleSettings,
589
+ config: handleSettings,
590
+ create: handleCreate,
591
+ list: handleList,
592
+ ls: handleList,
593
+ remove: handleRemove,
594
+ rm: handleRemove,
595
+ status: handleStatus,
596
+ cd: handleCd,
597
+ prune: handlePrune
598
+ };
599
+ function src_default(pi) {
600
+ pi.registerCommand("worktree", {
601
+ description: "Git worktree management for isolated workspaces",
602
+ handler: async (args, ctx) => {
603
+ const [cmd, ...rest] = args.trim().split(/\s+/);
604
+ const handler = commands[cmd];
605
+ if (handler) {
606
+ await handler(rest.join(" "), ctx);
607
+ } else {
608
+ ctx.ui.notify(HELP_TEXT, "info");
609
+ }
610
+ }
611
+ });
612
+ }
613
+ export {
614
+ src_default as default
615
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@zenobius/pi-worktrees",
3
+ "version": "0.0.1",
4
+ "description": "Worktrees extension for Pi Coding Agent",
5
+ "author": {
6
+ "name": "Zenobius",
7
+ "email": "airtonix@users.noreploy.github.com"
8
+ },
9
+ "type": "module",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git@github.com:zenobi-us/pi-worktrees.git"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "keywords": [
24
+ "pi-package"
25
+ ],
26
+ "files": [
27
+ "dist",
28
+ "src/version.ts"
29
+ ],
30
+ "devDependencies": {
31
+ "@eslint/js": "^9.39.1",
32
+ "@types/node": "^20.11.5",
33
+ "@typescript-eslint/eslint-plugin": "8.47.0",
34
+ "@typescript-eslint/parser": "8.47.0",
35
+ "bun-types": "latest",
36
+ "eslint": "^9.39.1",
37
+ "eslint-config-prettier": "10.1.8",
38
+ "eslint-plugin-prettier": "^5.1.3",
39
+ "prettier": "^3.2.4",
40
+ "typescript-eslint": "^8.47.0",
41
+ "vitest": "^3.2.4"
42
+ }
43
+ }