@aku11i/phantom 6.1.1-1 → 6.2.0-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/package.json +1 -1
- package/phantom.js +574 -424
package/phantom.js
CHANGED
|
@@ -28,15 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
enumerable: true
|
|
29
29
|
}) : target, mod));
|
|
30
30
|
//#endregion
|
|
31
|
-
//#region ../
|
|
32
|
-
const exitCodes = {
|
|
33
|
-
success: 0,
|
|
34
|
-
generalError: 1,
|
|
35
|
-
notFound: 2,
|
|
36
|
-
validationError: 3
|
|
37
|
-
};
|
|
38
|
-
//#endregion
|
|
39
|
-
//#region ../shared/src/types/result.ts
|
|
31
|
+
//#region ../utils/src/types/result.ts
|
|
40
32
|
/**
|
|
41
33
|
* Creates a successful Result containing the given value.
|
|
42
34
|
*
|
|
@@ -4375,6 +4367,8 @@ const execFile$1 = promisify(execFile);
|
|
|
4375
4367
|
* Execute a git command with consistent error handling
|
|
4376
4368
|
*/
|
|
4377
4369
|
async function executeGitCommand(args, options = {}) {
|
|
4370
|
+
const trimStdout = options.trimStdout ?? true;
|
|
4371
|
+
const trimStderr = options.trimStderr ?? true;
|
|
4378
4372
|
try {
|
|
4379
4373
|
const result = await execFile$1("git", args, {
|
|
4380
4374
|
cwd: options.cwd,
|
|
@@ -4382,58 +4376,33 @@ async function executeGitCommand(args, options = {}) {
|
|
|
4382
4376
|
encoding: "utf8"
|
|
4383
4377
|
});
|
|
4384
4378
|
return {
|
|
4385
|
-
stdout: result.stdout.trim(),
|
|
4386
|
-
stderr: result.stderr.trim()
|
|
4379
|
+
stdout: trimStdout ? result.stdout.trim() : result.stdout,
|
|
4380
|
+
stderr: trimStderr ? result.stderr.trim() : result.stderr
|
|
4387
4381
|
};
|
|
4388
4382
|
} catch (error) {
|
|
4389
4383
|
if (error && typeof error === "object" && "stdout" in error && "stderr" in error) {
|
|
4390
4384
|
const execError = error;
|
|
4391
4385
|
if (execError.stderr?.trim()) throw new Error(execError.stderr.trim());
|
|
4392
4386
|
return {
|
|
4393
|
-
stdout: execError.stdout?.trim()
|
|
4394
|
-
stderr: execError.stderr?.trim()
|
|
4387
|
+
stdout: trimStdout ? execError.stdout?.trim() ?? "" : execError.stdout ?? "",
|
|
4388
|
+
stderr: trimStderr ? execError.stderr?.trim() ?? "" : execError.stderr ?? ""
|
|
4395
4389
|
};
|
|
4396
4390
|
}
|
|
4397
4391
|
throw error;
|
|
4398
4392
|
}
|
|
4399
4393
|
}
|
|
4400
|
-
/**
|
|
4401
|
-
* Execute a git command in a specific directory
|
|
4402
|
-
*/
|
|
4403
|
-
async function executeGitCommandInDirectory(directory, args) {
|
|
4404
|
-
return executeGitCommand([
|
|
4405
|
-
"-C",
|
|
4406
|
-
directory,
|
|
4407
|
-
...args
|
|
4408
|
-
], {});
|
|
4409
|
-
}
|
|
4410
4394
|
//#endregion
|
|
4411
4395
|
//#region ../git/src/libs/add-worktree.ts
|
|
4412
4396
|
async function addWorktree(options) {
|
|
4413
|
-
const { path, branch, base = "HEAD" } = options;
|
|
4414
|
-
|
|
4397
|
+
const { path, branch, base = "HEAD", createBranch = true, cwd } = options;
|
|
4398
|
+
const args = [
|
|
4415
4399
|
"worktree",
|
|
4416
4400
|
"add",
|
|
4417
|
-
path
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
}
|
|
4423
|
-
//#endregion
|
|
4424
|
-
//#region ../git/src/libs/attach-worktree.ts
|
|
4425
|
-
async function attachWorktree(gitRoot, worktreePath, branchName) {
|
|
4426
|
-
try {
|
|
4427
|
-
await executeGitCommand([
|
|
4428
|
-
"worktree",
|
|
4429
|
-
"add",
|
|
4430
|
-
worktreePath,
|
|
4431
|
-
branchName
|
|
4432
|
-
], { cwd: gitRoot });
|
|
4433
|
-
return ok(void 0);
|
|
4434
|
-
} catch (error) {
|
|
4435
|
-
return err(error instanceof Error ? error : /* @__PURE__ */ new Error(`Failed to attach worktree: ${String(error)}`));
|
|
4436
|
-
}
|
|
4401
|
+
path
|
|
4402
|
+
];
|
|
4403
|
+
if (createBranch) args.push("-b", branch, base);
|
|
4404
|
+
else args.push(branch);
|
|
4405
|
+
await executeGitCommand(args, { cwd });
|
|
4437
4406
|
}
|
|
4438
4407
|
//#endregion
|
|
4439
4408
|
//#region ../git/src/libs/branch-exists.ts
|
|
@@ -4449,6 +4418,45 @@ async function branchExists(gitRoot, branchName) {
|
|
|
4449
4418
|
}
|
|
4450
4419
|
}
|
|
4451
4420
|
//#endregion
|
|
4421
|
+
//#region ../git/src/libs/config-get-regexp.ts
|
|
4422
|
+
async function configGetRegexp(options) {
|
|
4423
|
+
const { pattern, global = false, nullSeparated = false, cwd } = options;
|
|
4424
|
+
const args = ["config"];
|
|
4425
|
+
if (global) args.push("--global");
|
|
4426
|
+
if (nullSeparated) args.push("--null");
|
|
4427
|
+
args.push("--get-regexp", pattern);
|
|
4428
|
+
const { stdout } = await executeGitCommand(args, { cwd });
|
|
4429
|
+
return stdout;
|
|
4430
|
+
}
|
|
4431
|
+
//#endregion
|
|
4432
|
+
//#region ../git/src/libs/config-set.ts
|
|
4433
|
+
async function configSet(options) {
|
|
4434
|
+
const { key, value, global = false, cwd } = options;
|
|
4435
|
+
const args = ["config"];
|
|
4436
|
+
if (global) args.push("--global");
|
|
4437
|
+
args.push(key, value);
|
|
4438
|
+
await executeGitCommand(args, { cwd });
|
|
4439
|
+
}
|
|
4440
|
+
//#endregion
|
|
4441
|
+
//#region ../git/src/libs/config-unset.ts
|
|
4442
|
+
async function configUnset(options) {
|
|
4443
|
+
const { key, global = false, cwd } = options;
|
|
4444
|
+
const args = ["config"];
|
|
4445
|
+
if (global) args.push("--global");
|
|
4446
|
+
args.push("--unset", key);
|
|
4447
|
+
await executeGitCommand(args, { cwd });
|
|
4448
|
+
}
|
|
4449
|
+
//#endregion
|
|
4450
|
+
//#region ../git/src/libs/delete-branch.ts
|
|
4451
|
+
async function deleteBranch$1(options) {
|
|
4452
|
+
const { gitRoot, branch, force = true } = options;
|
|
4453
|
+
await executeGitCommand([
|
|
4454
|
+
"branch",
|
|
4455
|
+
force ? "-D" : "-d",
|
|
4456
|
+
branch
|
|
4457
|
+
], { cwd: gitRoot });
|
|
4458
|
+
}
|
|
4459
|
+
//#endregion
|
|
4452
4460
|
//#region ../git/src/libs/fetch.ts
|
|
4453
4461
|
async function fetch(options = {}) {
|
|
4454
4462
|
const { remote = "origin", refspec, cwd } = options;
|
|
@@ -4463,6 +4471,51 @@ async function fetch(options = {}) {
|
|
|
4463
4471
|
}
|
|
4464
4472
|
}
|
|
4465
4473
|
//#endregion
|
|
4474
|
+
//#region ../git/src/libs/get-current-branch.ts
|
|
4475
|
+
async function getCurrentBranch(options = {}) {
|
|
4476
|
+
const { stdout } = await executeGitCommand(["branch", "--show-current"], { cwd: options.cwd });
|
|
4477
|
+
return stdout;
|
|
4478
|
+
}
|
|
4479
|
+
//#endregion
|
|
4480
|
+
//#region ../git/src/libs/get-git-root.ts
|
|
4481
|
+
async function getGitRoot() {
|
|
4482
|
+
const { stdout } = await executeGitCommand(["rev-parse", "--git-common-dir"]);
|
|
4483
|
+
if (stdout.endsWith("/.git") || stdout === ".git") return resolve(process.cwd(), dirname(stdout));
|
|
4484
|
+
const { stdout: toplevel } = await executeGitCommand(["rev-parse", "--show-toplevel"]);
|
|
4485
|
+
return toplevel;
|
|
4486
|
+
}
|
|
4487
|
+
//#endregion
|
|
4488
|
+
//#region ../git/src/libs/get-status.ts
|
|
4489
|
+
function parseStatusLine(line) {
|
|
4490
|
+
const indexStatus = line[0] ?? " ";
|
|
4491
|
+
const workingTreeStatus = line[1] ?? " ";
|
|
4492
|
+
const pathPart = line.slice(3);
|
|
4493
|
+
const renamedPaths = indexStatus === "R" || indexStatus === "C" || workingTreeStatus === "R" || workingTreeStatus === "C" ? pathPart.match(/^(?<originalPath>.+) -> (?<path>.+)$/) : null;
|
|
4494
|
+
return {
|
|
4495
|
+
indexStatus,
|
|
4496
|
+
workingTreeStatus,
|
|
4497
|
+
path: renamedPaths?.groups?.path ?? pathPart,
|
|
4498
|
+
originalPath: renamedPaths?.groups?.originalPath
|
|
4499
|
+
};
|
|
4500
|
+
}
|
|
4501
|
+
async function getStatus(options) {
|
|
4502
|
+
const { stdout } = await executeGitCommand(["status", "--porcelain"], {
|
|
4503
|
+
cwd: options.cwd,
|
|
4504
|
+
trimStdout: false
|
|
4505
|
+
});
|
|
4506
|
+
const entries = stdout.split("\n").filter((line) => line.length > 0).map(parseStatusLine);
|
|
4507
|
+
return {
|
|
4508
|
+
entries,
|
|
4509
|
+
isClean: entries.length === 0
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
//#endregion
|
|
4513
|
+
//#region ../git/src/libs/get-top-level.ts
|
|
4514
|
+
async function getTopLevel(options = {}) {
|
|
4515
|
+
const { stdout } = await executeGitCommand(["rev-parse", "--show-toplevel"], { cwd: options.cwd });
|
|
4516
|
+
return stdout;
|
|
4517
|
+
}
|
|
4518
|
+
//#endregion
|
|
4466
4519
|
//#region ../git/src/libs/list-worktrees.ts
|
|
4467
4520
|
async function listWorktrees$1(_gitRoot) {
|
|
4468
4521
|
const { stdout } = await executeGitCommand([
|
|
@@ -4491,25 +4544,13 @@ async function listWorktrees$1(_gitRoot) {
|
|
|
4491
4544
|
return worktrees;
|
|
4492
4545
|
}
|
|
4493
4546
|
//#endregion
|
|
4494
|
-
//#region ../git/src/libs/
|
|
4495
|
-
async function
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
return currentWorktree.branch;
|
|
4502
|
-
} catch {
|
|
4503
|
-
return null;
|
|
4504
|
-
}
|
|
4505
|
-
}
|
|
4506
|
-
//#endregion
|
|
4507
|
-
//#region ../git/src/libs/get-git-root.ts
|
|
4508
|
-
async function getGitRoot() {
|
|
4509
|
-
const { stdout } = await executeGitCommand(["rev-parse", "--git-common-dir"]);
|
|
4510
|
-
if (stdout.endsWith("/.git") || stdout === ".git") return resolve(process.cwd(), dirname(stdout));
|
|
4511
|
-
const { stdout: toplevel } = await executeGitCommand(["rev-parse", "--show-toplevel"]);
|
|
4512
|
-
return toplevel;
|
|
4547
|
+
//#region ../git/src/libs/remove-worktree.ts
|
|
4548
|
+
async function removeWorktree$1(options) {
|
|
4549
|
+
const { gitRoot, path, force = false } = options;
|
|
4550
|
+
const args = ["worktree", "remove"];
|
|
4551
|
+
if (force) args.push("--force");
|
|
4552
|
+
args.push(path);
|
|
4553
|
+
await executeGitCommand(args, { cwd: gitRoot });
|
|
4513
4554
|
}
|
|
4514
4555
|
//#endregion
|
|
4515
4556
|
//#region ../git/src/libs/set-upstream-branch.ts
|
|
@@ -4538,7 +4579,8 @@ const preferencesSchema = object$1({
|
|
|
4538
4579
|
editor: string().optional(),
|
|
4539
4580
|
ai: string().optional(),
|
|
4540
4581
|
worktreesDirectory: string().optional(),
|
|
4541
|
-
directoryNameSeparator: string().optional()
|
|
4582
|
+
directoryNameSeparator: string().optional(),
|
|
4583
|
+
keepBranch: boolean().optional()
|
|
4542
4584
|
}).passthrough();
|
|
4543
4585
|
function parsePreferences(output) {
|
|
4544
4586
|
if (!output) return {};
|
|
@@ -4556,20 +4598,18 @@ function parsePreferences(output) {
|
|
|
4556
4598
|
else if (strippedKey === "ai") preferences.ai = value;
|
|
4557
4599
|
else if (strippedKey === "worktreesdirectory") preferences.worktreesDirectory = value;
|
|
4558
4600
|
else if (strippedKey === "directorynameseparator") preferences.directoryNameSeparator = value;
|
|
4601
|
+
else if (strippedKey === "keepbranch") preferences.keepBranch = value === "true" ? true : value === "false" ? false : void 0;
|
|
4559
4602
|
}
|
|
4560
4603
|
const parsed = preferencesSchema.safeParse(preferences);
|
|
4561
4604
|
if (!parsed.success) throw new PreferencesValidationError(parsed.error.issues[0]?.message ?? parsed.error.message);
|
|
4562
4605
|
return parsed.data;
|
|
4563
4606
|
}
|
|
4564
4607
|
async function loadPreferences() {
|
|
4565
|
-
|
|
4566
|
-
"
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
"^phantom\\."
|
|
4571
|
-
]);
|
|
4572
|
-
return parsePreferences(stdout);
|
|
4608
|
+
return parsePreferences(await configGetRegexp({
|
|
4609
|
+
pattern: "^phantom\\.",
|
|
4610
|
+
global: true,
|
|
4611
|
+
nullSeparated: true
|
|
4612
|
+
}));
|
|
4573
4613
|
}
|
|
4574
4614
|
//#endregion
|
|
4575
4615
|
//#region ../core/src/context.ts
|
|
@@ -4703,35 +4743,6 @@ async function spawnProcess(config) {
|
|
|
4703
4743
|
});
|
|
4704
4744
|
}
|
|
4705
4745
|
//#endregion
|
|
4706
|
-
//#region ../process/src/tmux.ts
|
|
4707
|
-
async function isInsideTmux() {
|
|
4708
|
-
return process.env.TMUX !== void 0;
|
|
4709
|
-
}
|
|
4710
|
-
async function executeTmuxCommand(options) {
|
|
4711
|
-
const { direction, command, args, cwd, env, windowName } = options;
|
|
4712
|
-
const tmuxArgs = [];
|
|
4713
|
-
switch (direction) {
|
|
4714
|
-
case "new":
|
|
4715
|
-
tmuxArgs.push("new-window");
|
|
4716
|
-
if (windowName) tmuxArgs.push("-n", windowName);
|
|
4717
|
-
break;
|
|
4718
|
-
case "vertical":
|
|
4719
|
-
tmuxArgs.push("split-window", "-v");
|
|
4720
|
-
break;
|
|
4721
|
-
case "horizontal":
|
|
4722
|
-
tmuxArgs.push("split-window", "-h");
|
|
4723
|
-
break;
|
|
4724
|
-
}
|
|
4725
|
-
if (cwd) tmuxArgs.push("-c", cwd);
|
|
4726
|
-
if (env) for (const [key, value] of Object.entries(env)) tmuxArgs.push("-e", `${key}=${value}`);
|
|
4727
|
-
tmuxArgs.push(command);
|
|
4728
|
-
if (args && args.length > 0) tmuxArgs.push(...args);
|
|
4729
|
-
return await spawnProcess({
|
|
4730
|
-
command: "tmux",
|
|
4731
|
-
args: tmuxArgs
|
|
4732
|
-
});
|
|
4733
|
-
}
|
|
4734
|
-
//#endregion
|
|
4735
4746
|
//#region ../core/src/worktree/errors.ts
|
|
4736
4747
|
var WorktreeError = class extends Error {
|
|
4737
4748
|
constructor(message) {
|
|
@@ -4757,12 +4768,23 @@ var BranchNotFoundError = class extends WorktreeError {
|
|
|
4757
4768
|
this.name = "BranchNotFoundError";
|
|
4758
4769
|
}
|
|
4759
4770
|
};
|
|
4771
|
+
var WorktreeActionConflictError = class extends WorktreeError {
|
|
4772
|
+
constructor() {
|
|
4773
|
+
super("Cannot use --shell, --exec, and --tmux options together");
|
|
4774
|
+
this.name = "WorktreeActionConflictError";
|
|
4775
|
+
}
|
|
4776
|
+
};
|
|
4777
|
+
var TmuxSessionRequiredError = class extends WorktreeError {
|
|
4778
|
+
constructor() {
|
|
4779
|
+
super("The --tmux option can only be used inside a tmux session");
|
|
4780
|
+
this.name = "TmuxSessionRequiredError";
|
|
4781
|
+
}
|
|
4782
|
+
};
|
|
4760
4783
|
//#endregion
|
|
4761
4784
|
//#region ../core/src/worktree/list.ts
|
|
4762
4785
|
async function getWorktreeStatus(worktreePath) {
|
|
4763
4786
|
try {
|
|
4764
|
-
|
|
4765
|
-
return !stdout;
|
|
4787
|
+
return (await getStatus({ cwd: worktreePath })).isClean;
|
|
4766
4788
|
} catch {
|
|
4767
4789
|
return true;
|
|
4768
4790
|
}
|
|
@@ -4862,6 +4884,91 @@ async function shellInWorktree(gitRoot, worktreeDirectory, worktreeName) {
|
|
|
4862
4884
|
});
|
|
4863
4885
|
}
|
|
4864
4886
|
//#endregion
|
|
4887
|
+
//#region ../tmux/src/tmux.ts
|
|
4888
|
+
async function isInsideTmux() {
|
|
4889
|
+
return process.env.TMUX !== void 0;
|
|
4890
|
+
}
|
|
4891
|
+
async function executeTmuxCommand(options) {
|
|
4892
|
+
const { direction, command, args, cwd, env, windowName } = options;
|
|
4893
|
+
const tmuxArgs = [];
|
|
4894
|
+
switch (direction) {
|
|
4895
|
+
case "new":
|
|
4896
|
+
tmuxArgs.push("new-window");
|
|
4897
|
+
if (windowName) tmuxArgs.push("-n", windowName);
|
|
4898
|
+
break;
|
|
4899
|
+
case "vertical":
|
|
4900
|
+
tmuxArgs.push("split-window", "-v");
|
|
4901
|
+
break;
|
|
4902
|
+
case "horizontal":
|
|
4903
|
+
tmuxArgs.push("split-window", "-h");
|
|
4904
|
+
break;
|
|
4905
|
+
}
|
|
4906
|
+
if (cwd) tmuxArgs.push("-c", cwd);
|
|
4907
|
+
if (env) for (const [key, value] of Object.entries(env)) tmuxArgs.push("-e", `${key}=${value}`);
|
|
4908
|
+
tmuxArgs.push(command);
|
|
4909
|
+
if (args && args.length > 0) tmuxArgs.push(...args);
|
|
4910
|
+
return spawnProcess({
|
|
4911
|
+
command: "tmux",
|
|
4912
|
+
args: tmuxArgs
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
//#endregion
|
|
4916
|
+
//#region ../core/src/worktree/action.ts
|
|
4917
|
+
function mergeWorktreeCopyFiles(configuredFiles, requestedFiles) {
|
|
4918
|
+
const files = [...new Set([...configuredFiles ?? [], ...requestedFiles ?? []])];
|
|
4919
|
+
return files.length > 0 ? files : void 0;
|
|
4920
|
+
}
|
|
4921
|
+
function resolveWorktreeAction(options) {
|
|
4922
|
+
const actions = [];
|
|
4923
|
+
if (options?.shell) actions.push({ kind: "shell" });
|
|
4924
|
+
if (options?.exec !== void 0) actions.push({
|
|
4925
|
+
kind: "exec",
|
|
4926
|
+
command: options.exec
|
|
4927
|
+
});
|
|
4928
|
+
if (options?.tmuxDirection) actions.push({
|
|
4929
|
+
kind: "tmux",
|
|
4930
|
+
direction: options.tmuxDirection
|
|
4931
|
+
});
|
|
4932
|
+
if (actions.length > 1) return err(new WorktreeActionConflictError());
|
|
4933
|
+
return ok(actions[0]);
|
|
4934
|
+
}
|
|
4935
|
+
async function validateWorktreeAction(action) {
|
|
4936
|
+
if (action?.kind === "tmux" && !await isInsideTmux()) return err(new TmuxSessionRequiredError());
|
|
4937
|
+
return ok(void 0);
|
|
4938
|
+
}
|
|
4939
|
+
async function runWorktreeAction(options) {
|
|
4940
|
+
const { gitRoot, worktreeDirectory, worktreeName, worktreePath, action, logger, exitWithProcessCode = false } = options;
|
|
4941
|
+
if (!action) return ok({});
|
|
4942
|
+
if (action.kind === "shell") {
|
|
4943
|
+
logger?.log(`\nEntering worktree '${worktreeName}' at ${worktreePath}`);
|
|
4944
|
+
logger?.log("Type 'exit' to return to your original directory\n");
|
|
4945
|
+
const shellResult = await shellInWorktree(gitRoot, worktreeDirectory, worktreeName);
|
|
4946
|
+
if (isErr(shellResult)) return err(shellResult.error);
|
|
4947
|
+
return ok({ exitProcessCode: exitWithProcessCode ? shellResult.value.exitCode ?? 0 : void 0 });
|
|
4948
|
+
}
|
|
4949
|
+
if (action.kind === "exec") {
|
|
4950
|
+
logger?.log(`\nExecuting command in worktree '${worktreeName}': ${action.command}`);
|
|
4951
|
+
const execResult = await execInWorktree(gitRoot, worktreeDirectory, worktreeName, [
|
|
4952
|
+
process.env.SHELL || "/bin/sh",
|
|
4953
|
+
"-c",
|
|
4954
|
+
action.command
|
|
4955
|
+
], { interactive: true });
|
|
4956
|
+
if (isErr(execResult)) return err(execResult.error);
|
|
4957
|
+
return ok({ exitProcessCode: exitWithProcessCode ? execResult.value.exitCode ?? 0 : void 0 });
|
|
4958
|
+
}
|
|
4959
|
+
logger?.log(`\nOpening worktree '${worktreeName}' in tmux ${action.direction === "new" ? "window" : "pane"}...`);
|
|
4960
|
+
const shell = process.env.SHELL || "/bin/sh";
|
|
4961
|
+
const tmuxResult = await executeTmuxCommand({
|
|
4962
|
+
direction: action.direction,
|
|
4963
|
+
command: shell,
|
|
4964
|
+
cwd: worktreePath,
|
|
4965
|
+
env: getPhantomEnv(worktreeName, worktreePath),
|
|
4966
|
+
windowName: action.direction === "new" ? worktreeName : void 0
|
|
4967
|
+
});
|
|
4968
|
+
if (isErr(tmuxResult)) return err(tmuxResult.error);
|
|
4969
|
+
return ok({});
|
|
4970
|
+
}
|
|
4971
|
+
//#endregion
|
|
4865
4972
|
//#region ../core/src/worktree/file-copier.ts
|
|
4866
4973
|
var FileCopyError = class extends Error {
|
|
4867
4974
|
file;
|
|
@@ -4898,10 +5005,10 @@ async function copyFiles(sourceDir, targetDir, files) {
|
|
|
4898
5005
|
//#endregion
|
|
4899
5006
|
//#region ../core/src/worktree/post-create.ts
|
|
4900
5007
|
async function executePostCreateCommands(options) {
|
|
4901
|
-
const { gitRoot, worktreesDirectory, worktreeName, commands } = options;
|
|
5008
|
+
const { gitRoot, worktreesDirectory, worktreeName, commands, logger } = options;
|
|
4902
5009
|
const executedCommands = [];
|
|
4903
5010
|
for (const command of commands) {
|
|
4904
|
-
|
|
5011
|
+
logger?.log(`Executing: ${command}`);
|
|
4905
5012
|
const cmdResult = await execInWorktree(gitRoot, worktreesDirectory, worktreeName, [
|
|
4906
5013
|
process.env.SHELL || "/bin/sh",
|
|
4907
5014
|
"-c",
|
|
@@ -4916,14 +5023,9 @@ async function executePostCreateCommands(options) {
|
|
|
4916
5023
|
}
|
|
4917
5024
|
return ok({ executedCommands });
|
|
4918
5025
|
}
|
|
4919
|
-
async function copyFilesToWorktree(gitRoot, worktreesDirectory, worktreeName, filesToCopy, directoryNameSeparator) {
|
|
4920
|
-
const copyResult = await copyFiles(gitRoot, getWorktreePathFromDirectory(worktreesDirectory, worktreeName, directoryNameSeparator), filesToCopy);
|
|
4921
|
-
if (isErr(copyResult)) return err(copyResult.error);
|
|
4922
|
-
return ok(void 0);
|
|
4923
|
-
}
|
|
4924
5026
|
//#endregion
|
|
4925
5027
|
//#region ../core/src/worktree/attach.ts
|
|
4926
|
-
async function attachWorktreeCore(gitRoot, worktreeDirectory, name, postCreateCopyFiles, postCreateCommands, directoryNameSeparator) {
|
|
5028
|
+
async function attachWorktreeCore(gitRoot, worktreeDirectory, name, postCreateCopyFiles, postCreateCommands, directoryNameSeparator, logger) {
|
|
4927
5029
|
const validation = validateWorktreeName(name);
|
|
4928
5030
|
if (isErr(validation)) return validation;
|
|
4929
5031
|
const worktreePath = getWorktreePathFromDirectory(worktreeDirectory, name, directoryNameSeparator);
|
|
@@ -4931,170 +5033,59 @@ async function attachWorktreeCore(gitRoot, worktreeDirectory, name, postCreateCo
|
|
|
4931
5033
|
const branchCheckResult = await branchExists(gitRoot, name);
|
|
4932
5034
|
if (isErr(branchCheckResult)) return err(branchCheckResult.error);
|
|
4933
5035
|
if (!branchCheckResult.value) return err(new BranchNotFoundError(name));
|
|
4934
|
-
|
|
4935
|
-
|
|
5036
|
+
try {
|
|
5037
|
+
await addWorktree({
|
|
5038
|
+
path: worktreePath,
|
|
5039
|
+
branch: name,
|
|
5040
|
+
createBranch: false,
|
|
5041
|
+
cwd: gitRoot
|
|
5042
|
+
});
|
|
5043
|
+
} catch (error) {
|
|
5044
|
+
return err(error instanceof Error ? error : /* @__PURE__ */ new Error(`Failed to attach worktree: ${String(error)}`));
|
|
5045
|
+
}
|
|
4936
5046
|
if (postCreateCopyFiles && postCreateCopyFiles.length > 0) {
|
|
4937
|
-
const copyResult = await
|
|
4938
|
-
if (isErr(copyResult))
|
|
5047
|
+
const copyResult = await copyFiles(gitRoot, worktreePath, postCreateCopyFiles);
|
|
5048
|
+
if (isErr(copyResult)) logger?.warn?.(`Warning: Failed to copy some files: ${copyResult.error.message}`);
|
|
4939
5049
|
}
|
|
4940
5050
|
if (postCreateCommands && postCreateCommands.length > 0) {
|
|
4941
|
-
|
|
5051
|
+
logger?.log?.("\nRunning post-create commands...");
|
|
4942
5052
|
const commandsResult = await executePostCreateCommands({
|
|
4943
5053
|
gitRoot,
|
|
4944
5054
|
worktreesDirectory: worktreeDirectory,
|
|
4945
5055
|
worktreeName: name,
|
|
4946
|
-
commands: postCreateCommands
|
|
5056
|
+
commands: postCreateCommands,
|
|
5057
|
+
logger
|
|
4947
5058
|
});
|
|
4948
5059
|
if (isErr(commandsResult)) return err(new WorktreeError(commandsResult.error.message));
|
|
4949
5060
|
}
|
|
4950
5061
|
return ok(worktreePath);
|
|
4951
5062
|
}
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
const
|
|
4956
|
-
if (isErr(
|
|
4957
|
-
const { branch = name, base = "HEAD" } = options;
|
|
4958
|
-
const worktreePath = getWorktreePathFromDirectory(worktreeDirectory, name, directoryNameSeparator);
|
|
4959
|
-
try {
|
|
4960
|
-
await fs.access(worktreeDirectory);
|
|
4961
|
-
} catch {
|
|
4962
|
-
await fs.mkdir(worktreeDirectory, { recursive: true });
|
|
4963
|
-
}
|
|
4964
|
-
const validation = await validateWorktreeDoesNotExist(gitRoot, worktreeDirectory, name);
|
|
4965
|
-
if (isErr(validation)) return err(validation.error);
|
|
4966
|
-
try {
|
|
4967
|
-
await addWorktree({
|
|
4968
|
-
path: worktreePath,
|
|
4969
|
-
branch,
|
|
4970
|
-
base
|
|
4971
|
-
});
|
|
4972
|
-
let copiedFiles;
|
|
4973
|
-
let skippedFiles;
|
|
4974
|
-
let copyError;
|
|
4975
|
-
if (options.copyFiles && options.copyFiles.length > 0) {
|
|
4976
|
-
const copyResult = await copyFiles(gitRoot, worktreePath, options.copyFiles);
|
|
4977
|
-
if (isOk(copyResult)) {
|
|
4978
|
-
copiedFiles = copyResult.value.copiedFiles;
|
|
4979
|
-
skippedFiles = copyResult.value.skippedFiles;
|
|
4980
|
-
} else copyError = copyResult.error.message;
|
|
4981
|
-
}
|
|
4982
|
-
if (postCreateCopyFiles && postCreateCopyFiles.length > 0) {
|
|
4983
|
-
const copyResult = await copyFilesToWorktree(gitRoot, worktreeDirectory, name, postCreateCopyFiles, directoryNameSeparator);
|
|
4984
|
-
if (isErr(copyResult)) {
|
|
4985
|
-
if (!copyError) copyError = copyResult.error.message;
|
|
4986
|
-
}
|
|
4987
|
-
}
|
|
4988
|
-
if (postCreateCommands && postCreateCommands.length > 0) {
|
|
4989
|
-
console.log("\nRunning post-create commands...");
|
|
4990
|
-
const commandsResult = await executePostCreateCommands({
|
|
4991
|
-
gitRoot,
|
|
4992
|
-
worktreesDirectory: worktreeDirectory,
|
|
4993
|
-
worktreeName: name,
|
|
4994
|
-
commands: postCreateCommands
|
|
4995
|
-
});
|
|
4996
|
-
if (isErr(commandsResult)) return err(new WorktreeError(commandsResult.error.message));
|
|
4997
|
-
}
|
|
4998
|
-
return ok({
|
|
4999
|
-
message: `Created worktree '${name}' at ${worktreePath}`,
|
|
5000
|
-
path: worktreePath,
|
|
5001
|
-
copiedFiles,
|
|
5002
|
-
skippedFiles,
|
|
5003
|
-
copyError
|
|
5004
|
-
});
|
|
5005
|
-
} catch (error) {
|
|
5006
|
-
return err(new WorktreeError(`worktree add failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
5007
|
-
}
|
|
5008
|
-
}
|
|
5009
|
-
//#endregion
|
|
5010
|
-
//#region ../core/src/worktree/pre-delete.ts
|
|
5011
|
-
async function executePreDeleteCommands(options) {
|
|
5012
|
-
const { gitRoot, worktreesDirectory, worktreeName, commands } = options;
|
|
5013
|
-
const executedCommands = [];
|
|
5014
|
-
for (const command of commands) {
|
|
5015
|
-
console.log(`Executing pre-delete command: ${command}`);
|
|
5016
|
-
const cmdResult = await execInWorktree(gitRoot, worktreesDirectory, worktreeName, [
|
|
5017
|
-
process.env.SHELL || "/bin/sh",
|
|
5018
|
-
"-c",
|
|
5019
|
-
command
|
|
5020
|
-
]);
|
|
5021
|
-
if (isErr(cmdResult)) {
|
|
5022
|
-
const errorMessage = cmdResult.error instanceof Error ? cmdResult.error.message : String(cmdResult.error);
|
|
5023
|
-
return err(/* @__PURE__ */ new Error(`Failed to execute pre-delete command "${command}": ${errorMessage}`));
|
|
5024
|
-
}
|
|
5025
|
-
if (cmdResult.value.exitCode !== 0) return err(/* @__PURE__ */ new Error(`Pre-delete command failed with exit code ${cmdResult.value.exitCode}: ${command}`));
|
|
5026
|
-
executedCommands.push(command);
|
|
5027
|
-
}
|
|
5028
|
-
return ok({ executedCommands });
|
|
5029
|
-
}
|
|
5030
|
-
//#endregion
|
|
5031
|
-
//#region ../core/src/worktree/delete.ts
|
|
5032
|
-
async function getWorktreeChangesStatus(worktreePath) {
|
|
5063
|
+
async function runAttachWorktree(options) {
|
|
5064
|
+
const actionResult = resolveWorktreeAction(options.action);
|
|
5065
|
+
if (isErr(actionResult)) return actionResult;
|
|
5066
|
+
const actionValidation = await validateWorktreeAction(actionResult.value);
|
|
5067
|
+
if (isErr(actionValidation)) return actionValidation;
|
|
5033
5068
|
try {
|
|
5034
|
-
const
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
};
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
const args = ["worktree", "remove"];
|
|
5047
|
-
if (force) args.push("--force");
|
|
5048
|
-
args.push(worktreePath);
|
|
5049
|
-
await executeGitCommand(args, { cwd: gitRoot });
|
|
5050
|
-
}
|
|
5051
|
-
async function deleteBranch(gitRoot, branchName) {
|
|
5052
|
-
try {
|
|
5053
|
-
await executeGitCommand([
|
|
5054
|
-
"branch",
|
|
5055
|
-
"-D",
|
|
5056
|
-
branchName
|
|
5057
|
-
], { cwd: gitRoot });
|
|
5058
|
-
return ok(true);
|
|
5059
|
-
} catch (error) {
|
|
5060
|
-
return err(new WorktreeError(`branch delete failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
5061
|
-
}
|
|
5062
|
-
}
|
|
5063
|
-
async function deleteWorktree(gitRoot, worktreeDirectory, name, options, preDeleteCommands) {
|
|
5064
|
-
const { force = false } = options || {};
|
|
5065
|
-
const validation = await validateWorktreeExists(gitRoot, worktreeDirectory, name, { excludeDefault: true });
|
|
5066
|
-
if (isErr(validation)) return err(validation.error);
|
|
5067
|
-
const worktreePath = validation.value.path;
|
|
5068
|
-
const status = await getWorktreeChangesStatus(worktreePath);
|
|
5069
|
-
if (status.hasUncommittedChanges && !force) return err(new WorktreeError(`Worktree '${name}' has uncommitted changes (${status.changedFiles} files). Use --force to delete anyway.`));
|
|
5070
|
-
if (preDeleteCommands && preDeleteCommands.length > 0) {
|
|
5071
|
-
console.log("\nRunning pre-delete commands...");
|
|
5072
|
-
const preDeleteResult = await executePreDeleteCommands({
|
|
5073
|
-
gitRoot,
|
|
5074
|
-
worktreesDirectory: worktreeDirectory,
|
|
5075
|
-
worktreeName: name,
|
|
5076
|
-
commands: preDeleteCommands
|
|
5069
|
+
const context = await createContext(await getGitRoot());
|
|
5070
|
+
const filesToCopy = mergeWorktreeCopyFiles(context.config?.postCreate?.copyFiles, options.copyFiles);
|
|
5071
|
+
const attachResult = await attachWorktreeCore(context.gitRoot, context.worktreesDirectory, options.name, filesToCopy, context.config?.postCreate?.commands, context.directoryNameSeparator, options.logger);
|
|
5072
|
+
if (isErr(attachResult)) return err(attachResult.error);
|
|
5073
|
+
options.logger?.log(`Attached phantom: ${options.name}`);
|
|
5074
|
+
const worktreeActionResult = await runWorktreeAction({
|
|
5075
|
+
gitRoot: context.gitRoot,
|
|
5076
|
+
worktreeDirectory: context.worktreesDirectory,
|
|
5077
|
+
worktreeName: options.name,
|
|
5078
|
+
worktreePath: attachResult.value,
|
|
5079
|
+
action: actionResult.value,
|
|
5080
|
+
logger: options.logger
|
|
5077
5081
|
});
|
|
5078
|
-
if (isErr(
|
|
5079
|
-
}
|
|
5080
|
-
try {
|
|
5081
|
-
await removeWorktree(gitRoot, worktreePath, force);
|
|
5082
|
-
const branchName = name;
|
|
5083
|
-
const branchResult = await deleteBranch(gitRoot, branchName);
|
|
5084
|
-
let message;
|
|
5085
|
-
if (isOk(branchResult)) message = `Deleted worktree '${name}' and its branch '${branchName}'`;
|
|
5086
|
-
else {
|
|
5087
|
-
message = `Deleted worktree '${name}'`;
|
|
5088
|
-
message += `\nNote: Branch '${branchName}' could not be deleted: ${branchResult.error.message}`;
|
|
5089
|
-
}
|
|
5090
|
-
if (status.hasUncommittedChanges) message = `Warning: Worktree '${name}' had uncommitted changes (${status.changedFiles} files)\n${message}`;
|
|
5082
|
+
if (isErr(worktreeActionResult)) return err(worktreeActionResult.error);
|
|
5091
5083
|
return ok({
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
changedFiles: status.hasUncommittedChanges ? status.changedFiles : void 0
|
|
5084
|
+
name: options.name,
|
|
5085
|
+
path: attachResult.value
|
|
5095
5086
|
});
|
|
5096
5087
|
} catch (error) {
|
|
5097
|
-
return err(
|
|
5088
|
+
return err(error instanceof Error ? error : new Error(String(error)));
|
|
5098
5089
|
}
|
|
5099
5090
|
}
|
|
5100
5091
|
//#endregion
|
|
@@ -5956,6 +5947,208 @@ async function generateUniqueName(gitRoot, worktreesDirectory, directoryNameSepa
|
|
|
5956
5947
|
return err(/* @__PURE__ */ new Error("Failed to generate a unique worktree name after maximum retries"));
|
|
5957
5948
|
}
|
|
5958
5949
|
//#endregion
|
|
5950
|
+
//#region ../core/src/worktree/create.ts
|
|
5951
|
+
async function createWorktree(gitRoot, worktreeDirectory, name, options, postCreateCopyFiles, postCreateCommands, directoryNameSeparator) {
|
|
5952
|
+
const nameValidation = validateWorktreeName(name);
|
|
5953
|
+
if (isErr(nameValidation)) return nameValidation;
|
|
5954
|
+
const { branch = name, base = "HEAD", copyFiles: requestedCopyFiles, logger } = options;
|
|
5955
|
+
const worktreePath = getWorktreePathFromDirectory(worktreeDirectory, name, directoryNameSeparator);
|
|
5956
|
+
try {
|
|
5957
|
+
await fs.access(worktreeDirectory);
|
|
5958
|
+
} catch {
|
|
5959
|
+
await fs.mkdir(worktreeDirectory, { recursive: true });
|
|
5960
|
+
}
|
|
5961
|
+
const validation = await validateWorktreeDoesNotExist(gitRoot, worktreeDirectory, name);
|
|
5962
|
+
if (isErr(validation)) return err(validation.error);
|
|
5963
|
+
try {
|
|
5964
|
+
await addWorktree({
|
|
5965
|
+
path: worktreePath,
|
|
5966
|
+
branch,
|
|
5967
|
+
base
|
|
5968
|
+
});
|
|
5969
|
+
let copiedFiles;
|
|
5970
|
+
let skippedFiles;
|
|
5971
|
+
let copyError;
|
|
5972
|
+
const filesToCopy = mergeWorktreeCopyFiles(requestedCopyFiles, postCreateCopyFiles);
|
|
5973
|
+
if (filesToCopy) {
|
|
5974
|
+
const copyResult = await copyFiles(gitRoot, worktreePath, filesToCopy);
|
|
5975
|
+
if (isOk(copyResult)) {
|
|
5976
|
+
copiedFiles = copyResult.value.copiedFiles;
|
|
5977
|
+
skippedFiles = copyResult.value.skippedFiles;
|
|
5978
|
+
} else copyError = copyResult.error.message;
|
|
5979
|
+
}
|
|
5980
|
+
if (postCreateCommands && postCreateCommands.length > 0) {
|
|
5981
|
+
logger?.log?.("\nRunning post-create commands...");
|
|
5982
|
+
const commandsResult = await executePostCreateCommands({
|
|
5983
|
+
gitRoot,
|
|
5984
|
+
worktreesDirectory: worktreeDirectory,
|
|
5985
|
+
worktreeName: name,
|
|
5986
|
+
commands: postCreateCommands,
|
|
5987
|
+
logger
|
|
5988
|
+
});
|
|
5989
|
+
if (isErr(commandsResult)) return err(new WorktreeError(commandsResult.error.message));
|
|
5990
|
+
}
|
|
5991
|
+
return ok({
|
|
5992
|
+
message: `Created worktree '${name}' at ${worktreePath}`,
|
|
5993
|
+
path: worktreePath,
|
|
5994
|
+
copiedFiles,
|
|
5995
|
+
skippedFiles,
|
|
5996
|
+
copyError
|
|
5997
|
+
});
|
|
5998
|
+
} catch (error) {
|
|
5999
|
+
return err(new WorktreeError(`worktree add failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
async function runCreateWorktree(options) {
|
|
6003
|
+
const actionResult = resolveWorktreeAction(options.action);
|
|
6004
|
+
if (isErr(actionResult)) return actionResult;
|
|
6005
|
+
const actionValidation = await validateWorktreeAction(actionResult.value);
|
|
6006
|
+
if (isErr(actionValidation)) return actionValidation;
|
|
6007
|
+
try {
|
|
6008
|
+
const context = await createContext(await getGitRoot());
|
|
6009
|
+
let worktreeName = options.name;
|
|
6010
|
+
if (!worktreeName) {
|
|
6011
|
+
const nameResult = await generateUniqueName(context.gitRoot, context.worktreesDirectory, context.directoryNameSeparator);
|
|
6012
|
+
if (isErr(nameResult)) return err(nameResult.error);
|
|
6013
|
+
worktreeName = nameResult.value;
|
|
6014
|
+
}
|
|
6015
|
+
const filesToCopy = mergeWorktreeCopyFiles(context.config?.postCreate?.copyFiles, options.copyFiles);
|
|
6016
|
+
const createResult = await createWorktree(context.gitRoot, context.worktreesDirectory, worktreeName, {
|
|
6017
|
+
base: options.base,
|
|
6018
|
+
copyFiles: filesToCopy,
|
|
6019
|
+
logger: options.logger
|
|
6020
|
+
}, void 0, context.config?.postCreate?.commands, context.directoryNameSeparator);
|
|
6021
|
+
if (isErr(createResult)) return err(createResult.error);
|
|
6022
|
+
options.logger?.log(createResult.value.message);
|
|
6023
|
+
if (createResult.value.copyError) options.logger?.warn?.(`\nWarning: Failed to copy some files: ${createResult.value.copyError}`);
|
|
6024
|
+
const worktreeActionResult = await runWorktreeAction({
|
|
6025
|
+
gitRoot: context.gitRoot,
|
|
6026
|
+
worktreeDirectory: context.worktreesDirectory,
|
|
6027
|
+
worktreeName,
|
|
6028
|
+
worktreePath: createResult.value.path,
|
|
6029
|
+
action: actionResult.value,
|
|
6030
|
+
logger: options.logger,
|
|
6031
|
+
exitWithProcessCode: true
|
|
6032
|
+
});
|
|
6033
|
+
if (isErr(worktreeActionResult)) return err(worktreeActionResult.error);
|
|
6034
|
+
return ok({
|
|
6035
|
+
name: worktreeName,
|
|
6036
|
+
path: createResult.value.path,
|
|
6037
|
+
message: createResult.value.message,
|
|
6038
|
+
copyError: createResult.value.copyError,
|
|
6039
|
+
exitProcessCode: worktreeActionResult.value.exitProcessCode
|
|
6040
|
+
});
|
|
6041
|
+
} catch (error) {
|
|
6042
|
+
return err(error instanceof Error ? error : new Error(String(error)));
|
|
6043
|
+
}
|
|
6044
|
+
}
|
|
6045
|
+
//#endregion
|
|
6046
|
+
//#region ../core/src/worktree/current.ts
|
|
6047
|
+
async function getCurrentWorktreeName(gitRoot) {
|
|
6048
|
+
try {
|
|
6049
|
+
const currentTopLevel = await getTopLevel();
|
|
6050
|
+
if (currentTopLevel === gitRoot) return null;
|
|
6051
|
+
return await getCurrentBranch({ cwd: currentTopLevel }) || null;
|
|
6052
|
+
} catch {
|
|
6053
|
+
return null;
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
//#endregion
|
|
6057
|
+
//#region ../core/src/worktree/pre-delete.ts
|
|
6058
|
+
async function executePreDeleteCommands(options) {
|
|
6059
|
+
const { gitRoot, worktreesDirectory, worktreeName, commands } = options;
|
|
6060
|
+
const executedCommands = [];
|
|
6061
|
+
for (const command of commands) {
|
|
6062
|
+
console.log(`Executing pre-delete command: ${command}`);
|
|
6063
|
+
const cmdResult = await execInWorktree(gitRoot, worktreesDirectory, worktreeName, [
|
|
6064
|
+
process.env.SHELL || "/bin/sh",
|
|
6065
|
+
"-c",
|
|
6066
|
+
command
|
|
6067
|
+
]);
|
|
6068
|
+
if (isErr(cmdResult)) {
|
|
6069
|
+
const errorMessage = cmdResult.error instanceof Error ? cmdResult.error.message : String(cmdResult.error);
|
|
6070
|
+
return err(/* @__PURE__ */ new Error(`Failed to execute pre-delete command "${command}": ${errorMessage}`));
|
|
6071
|
+
}
|
|
6072
|
+
if (cmdResult.value.exitCode !== 0) return err(/* @__PURE__ */ new Error(`Pre-delete command failed with exit code ${cmdResult.value.exitCode}: ${command}`));
|
|
6073
|
+
executedCommands.push(command);
|
|
6074
|
+
}
|
|
6075
|
+
return ok({ executedCommands });
|
|
6076
|
+
}
|
|
6077
|
+
//#endregion
|
|
6078
|
+
//#region ../core/src/worktree/delete.ts
|
|
6079
|
+
async function getWorktreeChangesStatus(worktreePath) {
|
|
6080
|
+
try {
|
|
6081
|
+
const status = await getStatus({ cwd: worktreePath });
|
|
6082
|
+
if (!status.isClean) return {
|
|
6083
|
+
hasUncommittedChanges: true,
|
|
6084
|
+
changedFiles: status.entries.length
|
|
6085
|
+
};
|
|
6086
|
+
} catch {}
|
|
6087
|
+
return {
|
|
6088
|
+
hasUncommittedChanges: false,
|
|
6089
|
+
changedFiles: 0
|
|
6090
|
+
};
|
|
6091
|
+
}
|
|
6092
|
+
async function removeWorktree(gitRoot, worktreePath, force = false) {
|
|
6093
|
+
await removeWorktree$1({
|
|
6094
|
+
gitRoot,
|
|
6095
|
+
path: worktreePath,
|
|
6096
|
+
force
|
|
6097
|
+
});
|
|
6098
|
+
}
|
|
6099
|
+
async function deleteBranch(gitRoot, branchName) {
|
|
6100
|
+
try {
|
|
6101
|
+
await deleteBranch$1({
|
|
6102
|
+
gitRoot,
|
|
6103
|
+
branch: branchName
|
|
6104
|
+
});
|
|
6105
|
+
return ok(true);
|
|
6106
|
+
} catch (error) {
|
|
6107
|
+
return err(new WorktreeError(`branch delete failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
async function deleteWorktree(gitRoot, worktreeDirectory, name, options, preDeleteCommands) {
|
|
6111
|
+
const { force = false } = options || {};
|
|
6112
|
+
const keepBranch = options?.keepBranch ?? false;
|
|
6113
|
+
const validation = await validateWorktreeExists(gitRoot, worktreeDirectory, name, { excludeDefault: true });
|
|
6114
|
+
if (isErr(validation)) return err(validation.error);
|
|
6115
|
+
const worktreePath = validation.value.path;
|
|
6116
|
+
const status = await getWorktreeChangesStatus(worktreePath);
|
|
6117
|
+
if (status.hasUncommittedChanges && !force) return err(new WorktreeError(`Worktree '${name}' has uncommitted changes (${status.changedFiles} files). Use --force to delete anyway.`));
|
|
6118
|
+
if (preDeleteCommands && preDeleteCommands.length > 0) {
|
|
6119
|
+
console.log("\nRunning pre-delete commands...");
|
|
6120
|
+
const preDeleteResult = await executePreDeleteCommands({
|
|
6121
|
+
gitRoot,
|
|
6122
|
+
worktreesDirectory: worktreeDirectory,
|
|
6123
|
+
worktreeName: name,
|
|
6124
|
+
commands: preDeleteCommands
|
|
6125
|
+
});
|
|
6126
|
+
if (isErr(preDeleteResult)) return err(new WorktreeError(preDeleteResult.error.message));
|
|
6127
|
+
}
|
|
6128
|
+
try {
|
|
6129
|
+
await removeWorktree(gitRoot, worktreePath, force);
|
|
6130
|
+
const branchName = name;
|
|
6131
|
+
let message;
|
|
6132
|
+
if (keepBranch) message = `Deleted worktree '${name}' and kept its branch '${branchName}'`;
|
|
6133
|
+
else {
|
|
6134
|
+
const branchResult = await deleteBranch(gitRoot, branchName);
|
|
6135
|
+
if (isOk(branchResult)) message = `Deleted worktree '${name}' and its branch '${branchName}'`;
|
|
6136
|
+
else {
|
|
6137
|
+
message = `Deleted worktree '${name}'`;
|
|
6138
|
+
message += `\nNote: Branch '${branchName}' could not be deleted: ${branchResult.error.message}`;
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
if (status.hasUncommittedChanges) message = `Warning: Worktree '${name}' had uncommitted changes (${status.changedFiles} files)\n${message}`;
|
|
6142
|
+
return ok({
|
|
6143
|
+
message,
|
|
6144
|
+
hasUncommittedChanges: status.hasUncommittedChanges,
|
|
6145
|
+
changedFiles: status.hasUncommittedChanges ? status.changedFiles : void 0
|
|
6146
|
+
});
|
|
6147
|
+
} catch (error) {
|
|
6148
|
+
return err(new WorktreeError(`worktree remove failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
//#endregion
|
|
5959
6152
|
//#region ../core/src/worktree/select.ts
|
|
5960
6153
|
async function selectWorktreeWithFzf(gitRoot, options = {}) {
|
|
5961
6154
|
const listResult = await listWorktrees(gitRoot, { excludeDefault: options.excludeDefault });
|
|
@@ -6000,6 +6193,14 @@ async function whereWorktree(gitRoot, worktreeDirectory, name) {
|
|
|
6000
6193
|
return ok({ path: validation.value.path });
|
|
6001
6194
|
}
|
|
6002
6195
|
//#endregion
|
|
6196
|
+
//#region src/constants/exit-codes.ts
|
|
6197
|
+
const exitCodes = {
|
|
6198
|
+
success: 0,
|
|
6199
|
+
generalError: 1,
|
|
6200
|
+
notFound: 2,
|
|
6201
|
+
validationError: 3
|
|
6202
|
+
};
|
|
6203
|
+
//#endregion
|
|
6003
6204
|
//#region src/output.ts
|
|
6004
6205
|
const output = {
|
|
6005
6206
|
log: (message) => {
|
|
@@ -6021,6 +6222,10 @@ const output = {
|
|
|
6021
6222
|
};
|
|
6022
6223
|
//#endregion
|
|
6023
6224
|
//#region src/errors.ts
|
|
6225
|
+
function getProcessExitCode(error) {
|
|
6226
|
+
if (!error || typeof error !== "object" || !("exitCode" in error) || typeof error.exitCode !== "number") return;
|
|
6227
|
+
return error.exitCode;
|
|
6228
|
+
}
|
|
6024
6229
|
function exitWithSuccess() {
|
|
6025
6230
|
process.exit(exitCodes.success);
|
|
6026
6231
|
}
|
|
@@ -6105,54 +6310,23 @@ async function attachHandler(args) {
|
|
|
6105
6310
|
}
|
|
6106
6311
|
}
|
|
6107
6312
|
});
|
|
6108
|
-
if (positionals.length === 0) exitWithError("Missing required argument: branch name", exitCodes.validationError);
|
|
6109
6313
|
const [branchName] = positionals;
|
|
6110
|
-
|
|
6111
|
-
const copyFileOptions = values["copy-file"];
|
|
6314
|
+
if (!branchName) exitWithError("Missing required argument: branch name", exitCodes.validationError);
|
|
6112
6315
|
const tmuxDirection = values.tmux ? "new" : values["tmux-vertical"] || values["tmux-v"] ? "vertical" : values["tmux-horizontal"] || values["tmux-h"] ? "horizontal" : void 0;
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
values
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
}
|
|
6124
|
-
const postCreateCopyFiles = copyFiles.length > 0 ? copyFiles : void 0;
|
|
6125
|
-
if (tmuxOption && !await isInsideTmux()) exitWithError("The --tmux option can only be used inside a tmux session", exitCodes.validationError);
|
|
6126
|
-
const result = await attachWorktreeCore(context.gitRoot, context.worktreesDirectory, branchName, postCreateCopyFiles, context.config?.postCreate?.commands, context.directoryNameSeparator);
|
|
6316
|
+
const result = await runAttachWorktree({
|
|
6317
|
+
name: branchName,
|
|
6318
|
+
copyFiles: values["copy-file"],
|
|
6319
|
+
action: {
|
|
6320
|
+
shell: values.shell ?? false,
|
|
6321
|
+
exec: values.exec,
|
|
6322
|
+
tmuxDirection
|
|
6323
|
+
},
|
|
6324
|
+
logger: output
|
|
6325
|
+
});
|
|
6127
6326
|
if (isErr(result)) {
|
|
6128
|
-
|
|
6129
|
-
if (error instanceof
|
|
6130
|
-
|
|
6131
|
-
exitWithError(error.message, exitCodes.generalError);
|
|
6132
|
-
}
|
|
6133
|
-
output.log(`Attached phantom: ${branchName}`);
|
|
6134
|
-
const worktreePath = result.value;
|
|
6135
|
-
if (values.shell) {
|
|
6136
|
-
const shellResult = await shellInWorktree(context.gitRoot, context.worktreesDirectory, branchName);
|
|
6137
|
-
if (isErr(shellResult)) exitWithError(shellResult.error.message, exitCodes.generalError);
|
|
6138
|
-
} else if (values.exec) {
|
|
6139
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
6140
|
-
const execResult = await execInWorktree(context.gitRoot, context.worktreesDirectory, branchName, [
|
|
6141
|
-
shell,
|
|
6142
|
-
"-c",
|
|
6143
|
-
values.exec
|
|
6144
|
-
], { interactive: true });
|
|
6145
|
-
if (isErr(execResult)) exitWithError(execResult.error.message, exitCodes.generalError);
|
|
6146
|
-
} else if (tmuxDirection) {
|
|
6147
|
-
output.log(`Opening worktree '${branchName}' in tmux ${tmuxDirection === "new" ? "window" : "pane"}...`);
|
|
6148
|
-
const tmuxResult = await executeTmuxCommand({
|
|
6149
|
-
direction: tmuxDirection,
|
|
6150
|
-
command: process.env.SHELL || "/bin/sh",
|
|
6151
|
-
cwd: worktreePath,
|
|
6152
|
-
env: getPhantomEnv(branchName, worktreePath),
|
|
6153
|
-
windowName: tmuxDirection === "new" ? branchName : void 0
|
|
6154
|
-
});
|
|
6155
|
-
if (isErr(tmuxResult)) exitWithError(tmuxResult.error.message, exitCodes.generalError);
|
|
6327
|
+
if (result.error instanceof WorktreeAlreadyExistsError || result.error instanceof WorktreeActionConflictError || result.error instanceof TmuxSessionRequiredError) exitWithError(result.error.message, exitCodes.validationError);
|
|
6328
|
+
if (result.error instanceof BranchNotFoundError) exitWithError(result.error.message, exitCodes.notFound);
|
|
6329
|
+
exitWithError(result.error.message, getProcessExitCode(result.error) ?? exitCodes.generalError);
|
|
6156
6330
|
}
|
|
6157
6331
|
}
|
|
6158
6332
|
//#endregion
|
|
@@ -6305,7 +6479,7 @@ _phantom_completion() {
|
|
|
6305
6479
|
;;
|
|
6306
6480
|
delete)
|
|
6307
6481
|
if [[ "\${cur}" == -* ]]; then
|
|
6308
|
-
local opts="--force --current --fzf"
|
|
6482
|
+
local opts="--force --keep-branch --current --fzf"
|
|
6309
6483
|
COMPREPLY=( $(compgen -W "\${opts}" -- "\${cur}") )
|
|
6310
6484
|
else
|
|
6311
6485
|
local worktrees=$(_phantom_list_worktrees_no_default)
|
|
@@ -6431,19 +6605,19 @@ _phantom_completion() {
|
|
|
6431
6605
|
return 0
|
|
6432
6606
|
elif [[ \${words[2]} == "get" ]]; then
|
|
6433
6607
|
if [[ \${cword} -eq 3 ]]; then
|
|
6434
|
-
local keys="editor ai worktreesDirectory directoryNameSeparator"
|
|
6608
|
+
local keys="editor ai worktreesDirectory directoryNameSeparator keepBranch"
|
|
6435
6609
|
COMPREPLY=( $(compgen -W "\${keys}" -- "\${cur}") )
|
|
6436
6610
|
return 0
|
|
6437
6611
|
fi
|
|
6438
6612
|
elif [[ \${words[2]} == "set" ]]; then
|
|
6439
6613
|
if [[ \${cword} -eq 3 ]]; then
|
|
6440
|
-
local keys="editor ai worktreesDirectory directoryNameSeparator"
|
|
6614
|
+
local keys="editor ai worktreesDirectory directoryNameSeparator keepBranch"
|
|
6441
6615
|
COMPREPLY=( $(compgen -W "\${keys}" -- "\${cur}") )
|
|
6442
6616
|
return 0
|
|
6443
6617
|
fi
|
|
6444
6618
|
elif [[ \${words[2]} == "remove" ]]; then
|
|
6445
6619
|
if [[ \${cword} -eq 3 ]]; then
|
|
6446
|
-
local keys="editor ai worktreesDirectory directoryNameSeparator"
|
|
6620
|
+
local keys="editor ai worktreesDirectory directoryNameSeparator keepBranch"
|
|
6447
6621
|
COMPREPLY=( $(compgen -W "\${keys}" -- "\${cur}") )
|
|
6448
6622
|
return 0
|
|
6449
6623
|
fi
|
|
@@ -6639,6 +6813,7 @@ complete -c phantom -n "__phantom_using_command where" -a "(__phantom_list_workt
|
|
|
6639
6813
|
|
|
6640
6814
|
# delete command options
|
|
6641
6815
|
complete -c phantom -n "__phantom_using_command delete" -l force -d "Force deletion even if worktree has uncommitted changes (-f)"
|
|
6816
|
+
complete -c phantom -n "__phantom_using_command delete" -l keep-branch -d "Delete the worktree but keep its branch"
|
|
6642
6817
|
complete -c phantom -n "__phantom_using_command delete" -l current -d "Delete the current worktree"
|
|
6643
6818
|
complete -c phantom -n "__phantom_using_command delete" -l fzf -d "Use fzf for interactive selection"
|
|
6644
6819
|
complete -c phantom -n "__phantom_using_command delete" -a "(__phantom_list_worktrees_no_default)"
|
|
@@ -6663,9 +6838,9 @@ complete -c phantom -n "__phantom_using_command ai" -a "(__phantom_list_worktree
|
|
|
6663
6838
|
|
|
6664
6839
|
# preferences command
|
|
6665
6840
|
complete -c phantom -n "__phantom_using_command preferences" -a "get set remove" -d "Manage preferences"
|
|
6666
|
-
complete -c phantom -n "__phantom_using_command preferences get" -a "editor ai worktreesDirectory directoryNameSeparator" -d "Preference key"
|
|
6667
|
-
complete -c phantom -n "__phantom_using_command preferences set" -a "editor ai worktreesDirectory directoryNameSeparator" -d "Preference key"
|
|
6668
|
-
complete -c phantom -n "__phantom_using_command preferences remove" -a "editor ai worktreesDirectory directoryNameSeparator" -d "Preference key"
|
|
6841
|
+
complete -c phantom -n "__phantom_using_command preferences get" -a "editor ai worktreesDirectory directoryNameSeparator keepBranch" -d "Preference key"
|
|
6842
|
+
complete -c phantom -n "__phantom_using_command preferences set" -a "editor ai worktreesDirectory directoryNameSeparator keepBranch" -d "Preference key"
|
|
6843
|
+
complete -c phantom -n "__phantom_using_command preferences remove" -a "editor ai worktreesDirectory directoryNameSeparator keepBranch" -d "Preference key"
|
|
6669
6844
|
|
|
6670
6845
|
# shell command options
|
|
6671
6846
|
complete -c phantom -n "__phantom_using_command shell" -l fzf -d "Use fzf for interactive selection"
|
|
@@ -6787,6 +6962,7 @@ _phantom() {
|
|
|
6787
6962
|
worktrees=(\${(f)"$(phantom list --names --no-default 2>/dev/null)"})
|
|
6788
6963
|
_arguments \
|
|
6789
6964
|
'--force[Force deletion even if worktree has uncommitted changes (-f)]' \
|
|
6965
|
+
'--keep-branch[Delete the worktree but keep its branch]' \
|
|
6790
6966
|
'--current[Delete the current worktree]' \
|
|
6791
6967
|
'--fzf[Use fzf for interactive selection]' \
|
|
6792
6968
|
'*:worktree:(\${(q)worktrees[@]})'
|
|
@@ -6819,7 +6995,7 @@ _phantom() {
|
|
|
6819
6995
|
preferences)
|
|
6820
6996
|
_arguments \
|
|
6821
6997
|
'1:subcommand:(get set remove)' \
|
|
6822
|
-
'2:key:(editor ai worktreesDirectory directoryNameSeparator)'
|
|
6998
|
+
'2:key:(editor ai worktreesDirectory directoryNameSeparator keepBranch)'
|
|
6823
6999
|
;;
|
|
6824
7000
|
completion)
|
|
6825
7001
|
_arguments \
|
|
@@ -6910,89 +7086,24 @@ async function createHandler(args) {
|
|
|
6910
7086
|
strict: true,
|
|
6911
7087
|
allowPositionals: true
|
|
6912
7088
|
});
|
|
6913
|
-
|
|
6914
|
-
const
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
if (tmuxOption && !await isInsideTmux()) exitWithError("The --tmux option can only be used inside a tmux session", exitCodes.validationError);
|
|
6929
|
-
try {
|
|
6930
|
-
const gitRoot = await getGitRoot();
|
|
6931
|
-
const context = await createContext(gitRoot);
|
|
6932
|
-
if (!worktreeName) {
|
|
6933
|
-
const nameResult = await generateUniqueName(gitRoot, context.worktreesDirectory, context.directoryNameSeparator);
|
|
6934
|
-
if (isErr(nameResult)) exitWithError(nameResult.error.message, exitCodes.generalError);
|
|
6935
|
-
worktreeName = nameResult.value;
|
|
6936
|
-
}
|
|
6937
|
-
let filesToCopy = [];
|
|
6938
|
-
if (context.config?.postCreate?.copyFiles) filesToCopy = [...context.config.postCreate.copyFiles];
|
|
6939
|
-
if (copyFileOptions && copyFileOptions.length > 0) {
|
|
6940
|
-
const cliFiles = Array.isArray(copyFileOptions) ? copyFileOptions : [copyFileOptions];
|
|
6941
|
-
filesToCopy = [...new Set([...filesToCopy, ...cliFiles])];
|
|
6942
|
-
}
|
|
6943
|
-
const result = await createWorktree(context.gitRoot, context.worktreesDirectory, worktreeName, {
|
|
6944
|
-
copyFiles: filesToCopy.length > 0 ? filesToCopy : void 0,
|
|
6945
|
-
base: baseOption
|
|
6946
|
-
}, filesToCopy.length > 0 ? filesToCopy : void 0, context.config?.postCreate?.commands, context.directoryNameSeparator);
|
|
6947
|
-
if (isErr(result)) {
|
|
6948
|
-
const exitCode = result.error instanceof WorktreeAlreadyExistsError ? exitCodes.validationError : exitCodes.generalError;
|
|
6949
|
-
exitWithError(result.error.message, exitCode);
|
|
6950
|
-
}
|
|
6951
|
-
output.log(result.value.message);
|
|
6952
|
-
if (result.value.copyError) output.error(`\nWarning: Failed to copy some files: ${result.value.copyError}`);
|
|
6953
|
-
if (execCommand && isOk(result)) {
|
|
6954
|
-
output.log(`\nExecuting command in worktree '${worktreeName}': ${execCommand}`);
|
|
6955
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
6956
|
-
const execResult = await execInWorktree(context.gitRoot, context.worktreesDirectory, worktreeName, [
|
|
6957
|
-
shell,
|
|
6958
|
-
"-c",
|
|
6959
|
-
execCommand
|
|
6960
|
-
], { interactive: true });
|
|
6961
|
-
if (isErr(execResult)) {
|
|
6962
|
-
output.error(execResult.error.message);
|
|
6963
|
-
exitWithError("", "exitCode" in execResult.error ? execResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError);
|
|
6964
|
-
}
|
|
6965
|
-
return process.exit(execResult.value.exitCode ?? 0);
|
|
6966
|
-
}
|
|
6967
|
-
if (openShell && isOk(result)) {
|
|
6968
|
-
output.log(`\nEntering worktree '${worktreeName}' at ${result.value.path}`);
|
|
6969
|
-
output.log("Type 'exit' to return to your original directory\n");
|
|
6970
|
-
const shellResult = await shellInWorktree(context.gitRoot, context.worktreesDirectory, worktreeName);
|
|
6971
|
-
if (isErr(shellResult)) {
|
|
6972
|
-
output.error(shellResult.error.message);
|
|
6973
|
-
exitWithError("", "exitCode" in shellResult.error ? shellResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError);
|
|
6974
|
-
}
|
|
6975
|
-
return process.exit(shellResult.value.exitCode ?? 0);
|
|
6976
|
-
}
|
|
6977
|
-
if (tmuxDirection && isOk(result)) {
|
|
6978
|
-
output.log(`\nOpening worktree '${worktreeName}' in tmux ${tmuxDirection === "new" ? "window" : "pane"}...`);
|
|
6979
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
6980
|
-
const tmuxResult = await executeTmuxCommand({
|
|
6981
|
-
direction: tmuxDirection,
|
|
6982
|
-
command: shell,
|
|
6983
|
-
cwd: result.value.path,
|
|
6984
|
-
env: getPhantomEnv(worktreeName, result.value.path),
|
|
6985
|
-
windowName: tmuxDirection === "new" ? worktreeName : void 0
|
|
6986
|
-
});
|
|
6987
|
-
if (isErr(tmuxResult)) {
|
|
6988
|
-
output.error(tmuxResult.error.message);
|
|
6989
|
-
exitWithError("", "exitCode" in tmuxResult.error ? tmuxResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError);
|
|
6990
|
-
}
|
|
6991
|
-
}
|
|
6992
|
-
exitWithSuccess();
|
|
6993
|
-
} catch (error) {
|
|
6994
|
-
exitWithError(error instanceof Error ? error.message : String(error), exitCodes.generalError);
|
|
7089
|
+
const tmuxDirection = values.tmux ? "new" : values["tmux-vertical"] || values["tmux-v"] ? "vertical" : values["tmux-horizontal"] || values["tmux-h"] ? "horizontal" : void 0;
|
|
7090
|
+
const result = await runCreateWorktree({
|
|
7091
|
+
name: positionals[0],
|
|
7092
|
+
base: values.base,
|
|
7093
|
+
copyFiles: values["copy-file"],
|
|
7094
|
+
action: {
|
|
7095
|
+
shell: values.shell ?? false,
|
|
7096
|
+
exec: values.exec,
|
|
7097
|
+
tmuxDirection
|
|
7098
|
+
},
|
|
7099
|
+
logger: output
|
|
7100
|
+
});
|
|
7101
|
+
if (isErr(result)) {
|
|
7102
|
+
const exitCode = result.error instanceof WorktreeAlreadyExistsError || result.error instanceof WorktreeActionConflictError || result.error instanceof TmuxSessionRequiredError ? exitCodes.validationError : getProcessExitCode(result.error) ?? exitCodes.generalError;
|
|
7103
|
+
exitWithError(result.error.message, exitCode);
|
|
6995
7104
|
}
|
|
7105
|
+
if (result.value.exitProcessCode !== void 0) return process.exit(result.value.exitProcessCode);
|
|
7106
|
+
exitWithSuccess();
|
|
6996
7107
|
}
|
|
6997
7108
|
//#endregion
|
|
6998
7109
|
//#region src/handlers/delete.ts
|
|
@@ -7004,6 +7115,7 @@ async function deleteHandler(args) {
|
|
|
7004
7115
|
type: "boolean",
|
|
7005
7116
|
short: "f"
|
|
7006
7117
|
},
|
|
7118
|
+
"keep-branch": { type: "boolean" },
|
|
7007
7119
|
current: { type: "boolean" },
|
|
7008
7120
|
fzf: {
|
|
7009
7121
|
type: "boolean",
|
|
@@ -7022,9 +7134,10 @@ async function deleteHandler(args) {
|
|
|
7022
7134
|
try {
|
|
7023
7135
|
const gitRoot = await getGitRoot();
|
|
7024
7136
|
const context = await createContext(gitRoot);
|
|
7137
|
+
const keepBranch = values["keep-branch"] ?? context.preferences.keepBranch ?? false;
|
|
7025
7138
|
const worktreeNames = [];
|
|
7026
7139
|
if (deleteCurrent) {
|
|
7027
|
-
const currentWorktree = await
|
|
7140
|
+
const currentWorktree = await getCurrentWorktreeName(gitRoot);
|
|
7028
7141
|
if (!currentWorktree) exitWithError("Not in a worktree directory. The --current option can only be used from within a worktree.", exitCodes.validationError);
|
|
7029
7142
|
worktreeNames.push(currentWorktree);
|
|
7030
7143
|
} else if (useFzf) {
|
|
@@ -7034,7 +7147,10 @@ async function deleteHandler(args) {
|
|
|
7034
7147
|
worktreeNames.push(selectResult.value.name);
|
|
7035
7148
|
} else worktreeNames.push(...positionals);
|
|
7036
7149
|
for (const worktreeName of worktreeNames) {
|
|
7037
|
-
const result = await deleteWorktree(context.gitRoot, context.worktreesDirectory, worktreeName, {
|
|
7150
|
+
const result = await deleteWorktree(context.gitRoot, context.worktreesDirectory, worktreeName, {
|
|
7151
|
+
force: forceDelete,
|
|
7152
|
+
keepBranch
|
|
7153
|
+
}, context.config?.preDelete?.commands);
|
|
7038
7154
|
if (isErr(result)) {
|
|
7039
7155
|
const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.validationError : result.error instanceof WorktreeError && result.error.message.includes("uncommitted changes") ? exitCodes.validationError : exitCodes.generalError;
|
|
7040
7156
|
exitWithError(result.error.message, exitCode);
|
|
@@ -24381,7 +24497,7 @@ var StdioServerTransport = class {
|
|
|
24381
24497
|
};
|
|
24382
24498
|
//#endregion
|
|
24383
24499
|
//#region ../mcp/package.json
|
|
24384
|
-
var version$1 = "6.
|
|
24500
|
+
var version$1 = "6.2.0-0";
|
|
24385
24501
|
const createWorktreeTool = {
|
|
24386
24502
|
name: "phantom_create_worktree",
|
|
24387
24503
|
description: "Create a new Git worktree (phantom)",
|
|
@@ -24393,8 +24509,9 @@ const createWorktreeTool = {
|
|
|
24393
24509
|
const context = await createContext(await getGitRoot());
|
|
24394
24510
|
const result = await createWorktree(context.gitRoot, context.worktreesDirectory, name, {
|
|
24395
24511
|
branch: name,
|
|
24396
|
-
base: baseBranch
|
|
24397
|
-
|
|
24512
|
+
base: baseBranch,
|
|
24513
|
+
copyFiles: context.config?.postCreate?.copyFiles
|
|
24514
|
+
}, void 0, context.config?.postCreate?.commands, context.directoryNameSeparator);
|
|
24398
24515
|
if (!isOk(result)) throw new Error(result.error.message);
|
|
24399
24516
|
return { content: [{
|
|
24400
24517
|
type: "text",
|
|
@@ -24412,11 +24529,15 @@ const deleteWorktreeTool = {
|
|
|
24412
24529
|
description: "Delete a Git worktree (phantom)",
|
|
24413
24530
|
inputSchema: object$1({
|
|
24414
24531
|
name: string().describe("Name of the worktree to delete"),
|
|
24415
|
-
force: boolean().optional().describe("Force deletion even if there are uncommitted changes")
|
|
24532
|
+
force: boolean().optional().describe("Force deletion even if there are uncommitted changes"),
|
|
24533
|
+
keepBranch: boolean().optional().describe("Keep the branch after deleting the worktree")
|
|
24416
24534
|
}),
|
|
24417
|
-
handler: async ({ name, force }) => {
|
|
24535
|
+
handler: async ({ name, force, keepBranch }) => {
|
|
24418
24536
|
const context = await createContext(await getGitRoot());
|
|
24419
|
-
const result = await deleteWorktree(context.gitRoot, context.worktreesDirectory, name, {
|
|
24537
|
+
const result = await deleteWorktree(context.gitRoot, context.worktreesDirectory, name, {
|
|
24538
|
+
force,
|
|
24539
|
+
keepBranch: keepBranch ?? context.preferences.keepBranch
|
|
24540
|
+
}, context.config?.preDelete?.commands);
|
|
24420
24541
|
if (!isOk(result)) throw new Error(result.error.message);
|
|
24421
24542
|
return { content: [{
|
|
24422
24543
|
type: "text",
|
|
@@ -24576,6 +24697,10 @@ const preferencesHelp = {
|
|
|
24576
24697
|
command: "phantom preferences set directoryNameSeparator \"-\"",
|
|
24577
24698
|
description: "Store a separator for flattening worktree directory names while keeping branch names unchanged"
|
|
24578
24699
|
},
|
|
24700
|
+
{
|
|
24701
|
+
command: "phantom preferences set keepBranch true",
|
|
24702
|
+
description: "Keep branches by default when deleting worktrees with phantom delete or MCP"
|
|
24703
|
+
},
|
|
24579
24704
|
{
|
|
24580
24705
|
command: "phantom preferences remove editor",
|
|
24581
24706
|
description: "Remove the editor preference (fallback to env/default)"
|
|
@@ -24592,7 +24717,8 @@ const preferencesHelp = {
|
|
|
24592
24717
|
" editor - used by 'phantom edit', preferred over $EDITOR",
|
|
24593
24718
|
" ai - used by 'phantom ai'",
|
|
24594
24719
|
" worktreesDirectory - path relative to the Git repo root for storing worktrees (defaults to .git/phantom/worktrees)",
|
|
24595
|
-
" directoryNameSeparator - replaces '/' in worktree directory names only (defaults to / for nested directories)"
|
|
24720
|
+
" directoryNameSeparator - replaces '/' in worktree directory names only (defaults to / for nested directories)",
|
|
24721
|
+
" keepBranch - keeps the branch when deleting a worktree (defaults to false)"
|
|
24596
24722
|
]
|
|
24597
24723
|
};
|
|
24598
24724
|
const preferencesGetHelp = {
|
|
@@ -24615,9 +24741,13 @@ const preferencesGetHelp = {
|
|
|
24615
24741
|
{
|
|
24616
24742
|
command: "phantom preferences get directoryNameSeparator",
|
|
24617
24743
|
description: "Show the preferred worktree directory name separator"
|
|
24744
|
+
},
|
|
24745
|
+
{
|
|
24746
|
+
command: "phantom preferences get keepBranch",
|
|
24747
|
+
description: "Show whether delete keeps branches by default"
|
|
24618
24748
|
}
|
|
24619
24749
|
],
|
|
24620
|
-
notes: ["Supported keys: editor, ai, worktreesDirectory, directoryNameSeparator"]
|
|
24750
|
+
notes: ["Supported keys: editor, ai, worktreesDirectory, directoryNameSeparator, keepBranch"]
|
|
24621
24751
|
};
|
|
24622
24752
|
const preferencesSetHelp = {
|
|
24623
24753
|
name: "preferences set",
|
|
@@ -24639,12 +24769,17 @@ const preferencesSetHelp = {
|
|
|
24639
24769
|
{
|
|
24640
24770
|
command: "phantom preferences set directoryNameSeparator \"-\"",
|
|
24641
24771
|
description: "Flatten worktree directory names such as feature/test to feature-test"
|
|
24772
|
+
},
|
|
24773
|
+
{
|
|
24774
|
+
command: "phantom preferences set keepBranch true",
|
|
24775
|
+
description: "Keep branches when deleting worktrees by default"
|
|
24642
24776
|
}
|
|
24643
24777
|
],
|
|
24644
24778
|
notes: [
|
|
24645
|
-
"Supported keys: editor, ai, worktreesDirectory, directoryNameSeparator",
|
|
24779
|
+
"Supported keys: editor, ai, worktreesDirectory, directoryNameSeparator, keepBranch",
|
|
24646
24780
|
"For worktreesDirectory, provide a path relative to the Git repository root; defaults to .git/phantom/worktrees when unset",
|
|
24647
|
-
"For directoryNameSeparator, '/' keeps nested directories; any other string replaces '/' only in the directory path"
|
|
24781
|
+
"For directoryNameSeparator, '/' keeps nested directories; any other string replaces '/' only in the directory path",
|
|
24782
|
+
"For keepBranch, use true to preserve branches on delete or false to keep the default branch deletion behavior"
|
|
24648
24783
|
]
|
|
24649
24784
|
};
|
|
24650
24785
|
const preferencesRemoveHelp = {
|
|
@@ -24667,9 +24802,13 @@ const preferencesRemoveHelp = {
|
|
|
24667
24802
|
{
|
|
24668
24803
|
command: "phantom preferences remove directoryNameSeparator",
|
|
24669
24804
|
description: "Restore nested worktree directories that follow branch slashes"
|
|
24805
|
+
},
|
|
24806
|
+
{
|
|
24807
|
+
command: "phantom preferences remove keepBranch",
|
|
24808
|
+
description: "Restore the default behavior of deleting branches with worktrees"
|
|
24670
24809
|
}
|
|
24671
24810
|
],
|
|
24672
|
-
notes: ["Supported keys: editor, ai, worktreesDirectory, directoryNameSeparator"]
|
|
24811
|
+
notes: ["Supported keys: editor, ai, worktreesDirectory, directoryNameSeparator, keepBranch"]
|
|
24673
24812
|
};
|
|
24674
24813
|
//#endregion
|
|
24675
24814
|
//#region src/handlers/preferences.ts
|
|
@@ -24686,7 +24825,8 @@ const supportedKeys$2 = [
|
|
|
24686
24825
|
"editor",
|
|
24687
24826
|
"ai",
|
|
24688
24827
|
"worktreesDirectory",
|
|
24689
|
-
"directoryNameSeparator"
|
|
24828
|
+
"directoryNameSeparator",
|
|
24829
|
+
"keepBranch"
|
|
24690
24830
|
];
|
|
24691
24831
|
async function preferencesGetHandler(args) {
|
|
24692
24832
|
const { positionals } = parseArgs({
|
|
@@ -24700,7 +24840,7 @@ async function preferencesGetHandler(args) {
|
|
|
24700
24840
|
if (!supportedKeys$2.includes(inputKey)) exitWithError(`Unknown preference '${inputKey}'. Supported keys: ${supportedKeys$2.join(", ")}`, exitCodes.validationError);
|
|
24701
24841
|
try {
|
|
24702
24842
|
const preferences = await loadPreferences();
|
|
24703
|
-
const value = inputKey === "editor" ? preferences.editor : inputKey === "ai" ? preferences.ai : inputKey === "worktreesDirectory" ? preferences.worktreesDirectory : inputKey === "directoryNameSeparator" ? preferences.directoryNameSeparator : void 0;
|
|
24843
|
+
const value = inputKey === "editor" ? preferences.editor : inputKey === "ai" ? preferences.ai : inputKey === "worktreesDirectory" ? preferences.worktreesDirectory : inputKey === "directoryNameSeparator" ? preferences.directoryNameSeparator : inputKey === "keepBranch" ? preferences.keepBranch?.toString() : void 0;
|
|
24704
24844
|
if (value === void 0) output.log(`Preference '${inputKey}' is not set (git config --global phantom.${inputKey})`);
|
|
24705
24845
|
else output.log(value);
|
|
24706
24846
|
exitWithSuccess();
|
|
@@ -24714,7 +24854,8 @@ const supportedKeys$1 = [
|
|
|
24714
24854
|
"editor",
|
|
24715
24855
|
"ai",
|
|
24716
24856
|
"worktreesDirectory",
|
|
24717
|
-
"directoryNameSeparator"
|
|
24857
|
+
"directoryNameSeparator",
|
|
24858
|
+
"keepBranch"
|
|
24718
24859
|
];
|
|
24719
24860
|
async function preferencesRemoveHandler(args) {
|
|
24720
24861
|
const { positionals } = parseArgs({
|
|
@@ -24727,12 +24868,10 @@ async function preferencesRemoveHandler(args) {
|
|
|
24727
24868
|
const inputKey = positionals[0];
|
|
24728
24869
|
if (!supportedKeys$1.includes(inputKey)) exitWithError(`Unknown preference '${inputKey}'. Supported keys: ${supportedKeys$1.join(", ")}`, exitCodes.validationError);
|
|
24729
24870
|
try {
|
|
24730
|
-
await
|
|
24731
|
-
|
|
24732
|
-
|
|
24733
|
-
|
|
24734
|
-
`phantom.${inputKey}`
|
|
24735
|
-
]);
|
|
24871
|
+
await configUnset({
|
|
24872
|
+
key: `phantom.${inputKey}`,
|
|
24873
|
+
global: true
|
|
24874
|
+
});
|
|
24736
24875
|
output.log(`Removed phantom.${inputKey} from global git config`);
|
|
24737
24876
|
exitWithSuccess();
|
|
24738
24877
|
} catch (error) {
|
|
@@ -24745,7 +24884,8 @@ const supportedKeys = [
|
|
|
24745
24884
|
"editor",
|
|
24746
24885
|
"ai",
|
|
24747
24886
|
"worktreesDirectory",
|
|
24748
|
-
"directoryNameSeparator"
|
|
24887
|
+
"directoryNameSeparator",
|
|
24888
|
+
"keepBranch"
|
|
24749
24889
|
];
|
|
24750
24890
|
async function preferencesSetHandler(args) {
|
|
24751
24891
|
if (args.length < 2) exitWithError("Usage: phantom preferences set <key> <value>", exitCodes.validationError);
|
|
@@ -24753,13 +24893,13 @@ async function preferencesSetHandler(args) {
|
|
|
24753
24893
|
if (!supportedKeys.includes(inputKey)) exitWithError(`Unknown preference '${inputKey}'. Supported keys: ${supportedKeys.join(", ")}`, exitCodes.validationError);
|
|
24754
24894
|
const value = valueParts.join(" ");
|
|
24755
24895
|
if (!value) exitWithError(`Preference '${inputKey}' requires a value`, exitCodes.validationError);
|
|
24896
|
+
if (inputKey === "keepBranch" && value !== "true" && value !== "false") exitWithError("Preference 'keepBranch' must be 'true' or 'false'", exitCodes.validationError);
|
|
24756
24897
|
try {
|
|
24757
|
-
await
|
|
24758
|
-
|
|
24759
|
-
|
|
24760
|
-
|
|
24761
|
-
|
|
24762
|
-
]);
|
|
24898
|
+
await configSet({
|
|
24899
|
+
key: `phantom.${inputKey}`,
|
|
24900
|
+
value,
|
|
24901
|
+
global: true
|
|
24902
|
+
});
|
|
24763
24903
|
output.log(`Set phantom.${inputKey} (global) to '${value}'`);
|
|
24764
24904
|
exitWithSuccess();
|
|
24765
24905
|
} catch (error) {
|
|
@@ -24838,7 +24978,7 @@ async function shellHandler(args) {
|
|
|
24838
24978
|
}
|
|
24839
24979
|
//#endregion
|
|
24840
24980
|
//#region package.json
|
|
24841
|
-
var version = "6.
|
|
24981
|
+
var version = "6.2.0-0";
|
|
24842
24982
|
//#endregion
|
|
24843
24983
|
//#region src/version.ts
|
|
24844
24984
|
function getVersion() {
|
|
@@ -25184,6 +25324,11 @@ const commands = [
|
|
|
25184
25324
|
type: "boolean",
|
|
25185
25325
|
description: "Delete the current worktree"
|
|
25186
25326
|
},
|
|
25327
|
+
{
|
|
25328
|
+
name: "--keep-branch",
|
|
25329
|
+
type: "boolean",
|
|
25330
|
+
description: "Delete the worktree but keep its branch"
|
|
25331
|
+
},
|
|
25187
25332
|
{
|
|
25188
25333
|
name: "--fzf",
|
|
25189
25334
|
type: "boolean",
|
|
@@ -25207,6 +25352,10 @@ const commands = [
|
|
|
25207
25352
|
description: "Delete the current worktree",
|
|
25208
25353
|
command: "phantom delete --current"
|
|
25209
25354
|
},
|
|
25355
|
+
{
|
|
25356
|
+
description: "Delete a worktree while keeping its branch",
|
|
25357
|
+
command: "phantom delete feature-auth --keep-branch"
|
|
25358
|
+
},
|
|
25210
25359
|
{
|
|
25211
25360
|
description: "Delete a worktree with interactive fzf selection",
|
|
25212
25361
|
command: "phantom delete --fzf"
|
|
@@ -25216,6 +25365,7 @@ const commands = [
|
|
|
25216
25365
|
"By default, deletion will fail if the worktree has uncommitted changes",
|
|
25217
25366
|
"You can pass multiple worktree names to delete them at once",
|
|
25218
25367
|
"The associated branch will also be deleted if it's not checked out elsewhere",
|
|
25368
|
+
"Use --keep-branch to delete only the worktree and preserve the branch",
|
|
25219
25369
|
"With --fzf, you can interactively select the worktree to delete"
|
|
25220
25370
|
]
|
|
25221
25371
|
}
|