@aku11i/phantom 0.8.1 → 1.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/dist/phantom.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin/phantom.ts
4
- import { argv, exit } from "node:process";
4
+ import { argv, exit as exit2 } from "node:process";
5
5
 
6
6
  // src/cli/handlers/attach.ts
7
7
  import { parseArgs } from "node:util";
@@ -67,6 +67,18 @@ var err = (error) => ({
67
67
  var isOk = (result) => result.ok;
68
68
  var isErr = (result) => !result.ok;
69
69
 
70
+ // src/core/worktree/validate.ts
71
+ import fs from "node:fs/promises";
72
+
73
+ // src/core/paths.ts
74
+ import { join } from "node:path";
75
+ function getPhantomDirectory(gitRoot) {
76
+ return join(gitRoot, ".git", "phantom", "worktrees");
77
+ }
78
+ function getWorktreePath(gitRoot, name) {
79
+ return join(getPhantomDirectory(gitRoot), name);
80
+ }
81
+
70
82
  // src/core/worktree/errors.ts
71
83
  var WorktreeError = class extends Error {
72
84
  constructor(message) {
@@ -99,58 +111,39 @@ var BranchNotFoundError = class extends WorktreeError {
99
111
  }
100
112
  };
101
113
 
102
- // src/core/worktree/validate.ts
103
- import fs from "node:fs/promises";
104
-
105
- // src/core/paths.ts
106
- import { join } from "node:path";
107
- function getPhantomDirectory(gitRoot) {
108
- return join(gitRoot, ".git", "phantom", "worktrees");
109
- }
110
- function getWorktreePath(gitRoot, name) {
111
- return join(getPhantomDirectory(gitRoot), name);
112
- }
113
-
114
114
  // src/core/worktree/validate.ts
115
115
  async function validateWorktreeExists(gitRoot, name) {
116
116
  const worktreePath = getWorktreePath(gitRoot, name);
117
117
  try {
118
118
  await fs.access(worktreePath);
119
- return {
120
- exists: true,
121
- path: worktreePath
122
- };
119
+ return ok({ path: worktreePath });
123
120
  } catch {
124
- return {
125
- exists: false,
126
- message: `Worktree '${name}' does not exist`
127
- };
121
+ return err(new WorktreeNotFoundError(name));
128
122
  }
129
123
  }
130
124
  async function validateWorktreeDoesNotExist(gitRoot, name) {
131
125
  const worktreePath = getWorktreePath(gitRoot, name);
132
126
  try {
133
127
  await fs.access(worktreePath);
134
- return {
135
- exists: true,
136
- message: `Worktree '${name}' already exists`
137
- };
128
+ return err(new WorktreeAlreadyExistsError(name));
138
129
  } catch {
139
- return {
140
- exists: false,
141
- path: worktreePath
142
- };
130
+ return ok({ path: worktreePath });
143
131
  }
144
132
  }
145
133
  function validateWorktreeName(name) {
146
134
  if (!name || name.trim() === "") {
147
135
  return err(new Error("Phantom name cannot be empty"));
148
136
  }
149
- if (name.includes("/")) {
150
- return err(new Error("Phantom name cannot contain slashes"));
137
+ const validNamePattern = /^[a-zA-Z0-9\-_.\/]+$/;
138
+ if (!validNamePattern.test(name)) {
139
+ return err(
140
+ new Error(
141
+ "Phantom name can only contain letters, numbers, hyphens, underscores, dots, and slashes"
142
+ )
143
+ );
151
144
  }
152
- if (name.startsWith(".")) {
153
- return err(new Error("Phantom name cannot start with a dot"));
145
+ if (name.includes("..")) {
146
+ return err(new Error("Phantom name cannot contain consecutive dots"));
154
147
  }
155
148
  return ok(void 0);
156
149
  }
@@ -218,10 +211,10 @@ async function spawnProcess(config) {
218
211
  // src/core/process/exec.ts
219
212
  async function execInWorktree(gitRoot, worktreeName, command2, options = {}) {
220
213
  const validation = await validateWorktreeExists(gitRoot, worktreeName);
221
- if (!validation.exists) {
222
- return err(new WorktreeNotFoundError(worktreeName));
214
+ if (isErr(validation)) {
215
+ return err(validation.error);
223
216
  }
224
- const worktreePath = validation.path;
217
+ const worktreePath = validation.value.path;
225
218
  const [cmd, ...args2] = command2;
226
219
  const stdio = options.interactive ? "inherit" : ["ignore", "inherit", "inherit"];
227
220
  return spawnProcess({
@@ -234,13 +227,22 @@ async function execInWorktree(gitRoot, worktreeName, command2, options = {}) {
234
227
  });
235
228
  }
236
229
 
230
+ // src/core/process/env.ts
231
+ function getPhantomEnv(worktreeName, worktreePath) {
232
+ return {
233
+ PHANTOM: "1",
234
+ PHANTOM_NAME: worktreeName,
235
+ PHANTOM_PATH: worktreePath
236
+ };
237
+ }
238
+
237
239
  // src/core/process/shell.ts
238
240
  async function shellInWorktree(gitRoot, worktreeName) {
239
241
  const validation = await validateWorktreeExists(gitRoot, worktreeName);
240
- if (!validation.exists) {
241
- return err(new WorktreeNotFoundError(worktreeName));
242
+ if (isErr(validation)) {
243
+ return err(validation.error);
242
244
  }
243
- const worktreePath = validation.path;
245
+ const worktreePath = validation.value.path;
244
246
  const shell = process.env.SHELL || "/bin/sh";
245
247
  return spawnProcess({
246
248
  command: shell,
@@ -249,9 +251,7 @@ async function shellInWorktree(gitRoot, worktreeName) {
249
251
  cwd: worktreePath,
250
252
  env: {
251
253
  ...process.env,
252
- PHANTOM: "1",
253
- PHANTOM_NAME: worktreeName,
254
- PHANTOM_PATH: worktreePath
254
+ ...getPhantomEnv(worktreeName, worktreePath)
255
255
  }
256
256
  }
257
257
  });
@@ -406,10 +406,11 @@ async function attachHandler(args2) {
406
406
  exitWithError(shellResult.error.message, exitCodes.generalError);
407
407
  }
408
408
  } else if (values.exec) {
409
+ const shell = process.env.SHELL || "/bin/sh";
409
410
  const execResult = await execInWorktree(
410
411
  gitRoot,
411
412
  branchName,
412
- values.exec.split(" "),
413
+ [shell, "-c", values.exec],
413
414
  { interactive: true }
414
415
  );
415
416
  if (isErr(execResult)) {
@@ -418,6 +419,198 @@ async function attachHandler(args2) {
418
419
  }
419
420
  }
420
421
 
422
+ // src/cli/handlers/completion.ts
423
+ import { exit } from "node:process";
424
+ var FISH_COMPLETION_SCRIPT = `# Fish completion for phantom
425
+ # Place this in ~/.config/fish/completions/phantom.fish
426
+
427
+ function __phantom_list_worktrees
428
+ phantom list --names 2>/dev/null
429
+ end
430
+
431
+ function __phantom_using_command
432
+ set -l cmd (commandline -opc)
433
+ set -l cmd_count (count $cmd)
434
+ if test $cmd_count -eq 1
435
+ # No subcommand yet, so any command can be used
436
+ if test (count $argv) -eq 0
437
+ return 0
438
+ else
439
+ return 1
440
+ end
441
+ else if test $cmd_count -ge 2
442
+ # Check if we're in the context of a specific command
443
+ if test (count $argv) -gt 0 -a "$argv[1]" = "$cmd[2]"
444
+ return 0
445
+ end
446
+ end
447
+ return 1
448
+ end
449
+
450
+ # Disable file completion for phantom
451
+ complete -c phantom -f
452
+
453
+ # Main commands
454
+ complete -c phantom -n "__phantom_using_command" -a "create" -d "Create a new Git worktree (phantom)"
455
+ complete -c phantom -n "__phantom_using_command" -a "attach" -d "Attach to an existing branch by creating a new worktree"
456
+ complete -c phantom -n "__phantom_using_command" -a "list" -d "List all Git worktrees (phantoms)"
457
+ complete -c phantom -n "__phantom_using_command" -a "where" -d "Output the filesystem path of a specific worktree"
458
+ complete -c phantom -n "__phantom_using_command" -a "delete" -d "Delete a Git worktree (phantom)"
459
+ complete -c phantom -n "__phantom_using_command" -a "exec" -d "Execute a command in a worktree directory"
460
+ complete -c phantom -n "__phantom_using_command" -a "shell" -d "Open an interactive shell in a worktree directory"
461
+ complete -c phantom -n "__phantom_using_command" -a "version" -d "Display phantom version information"
462
+ complete -c phantom -n "__phantom_using_command" -a "completion" -d "Generate shell completion scripts"
463
+
464
+ # Global options
465
+ complete -c phantom -l help -d "Show help (-h)"
466
+ complete -c phantom -l version -d "Show version (-v)"
467
+
468
+ # create command options
469
+ complete -c phantom -n "__phantom_using_command create" -l shell -d "Open an interactive shell in the new worktree after creation (-s)"
470
+ complete -c phantom -n "__phantom_using_command create" -l exec -d "Execute a command in the new worktree after creation (-x)" -x
471
+ complete -c phantom -n "__phantom_using_command create" -l tmux -d "Open the worktree in a new tmux window (-t)"
472
+ complete -c phantom -n "__phantom_using_command create" -l tmux-vertical -d "Open the worktree in a vertical tmux pane"
473
+ complete -c phantom -n "__phantom_using_command create" -l tmux-horizontal -d "Open the worktree in a horizontal tmux pane"
474
+ complete -c phantom -n "__phantom_using_command create" -l copy-file -d "Copy specified files from the current worktree" -r
475
+
476
+ # attach command options
477
+ complete -c phantom -n "__phantom_using_command attach" -l shell -d "Open an interactive shell in the worktree after attaching (-s)"
478
+ complete -c phantom -n "__phantom_using_command attach" -l exec -d "Execute a command in the worktree after attaching (-x)" -x
479
+
480
+ # list command options
481
+ complete -c phantom -n "__phantom_using_command list" -l fzf -d "Use fzf for interactive selection"
482
+ complete -c phantom -n "__phantom_using_command list" -l names -d "Output only phantom names (for scripts and completion)"
483
+
484
+ # where command options
485
+ complete -c phantom -n "__phantom_using_command where" -l fzf -d "Use fzf for interactive selection"
486
+ complete -c phantom -n "__phantom_using_command where" -a "(__phantom_list_worktrees)"
487
+
488
+ # delete command options
489
+ complete -c phantom -n "__phantom_using_command delete" -l force -d "Force deletion even if worktree has uncommitted changes (-f)"
490
+ complete -c phantom -n "__phantom_using_command delete" -l current -d "Delete the current worktree"
491
+ complete -c phantom -n "__phantom_using_command delete" -l fzf -d "Use fzf for interactive selection"
492
+ complete -c phantom -n "__phantom_using_command delete" -a "(__phantom_list_worktrees)"
493
+
494
+ # exec command - accept worktree names and then any command
495
+ complete -c phantom -n "__phantom_using_command exec" -a "(__phantom_list_worktrees)"
496
+
497
+ # shell command options
498
+ complete -c phantom -n "__phantom_using_command shell" -l fzf -d "Use fzf for interactive selection"
499
+ complete -c phantom -n "__phantom_using_command shell" -a "(__phantom_list_worktrees)"
500
+
501
+ # completion command - shell names
502
+ complete -c phantom -n "__phantom_using_command completion" -a "fish zsh" -d "Shell type"`;
503
+ var ZSH_COMPLETION_SCRIPT = `#compdef phantom
504
+ # Zsh completion for phantom
505
+ # Place this in a directory in your $fpath (e.g., ~/.zsh/completions/)
506
+ # Or load dynamically with: eval "$(phantom completion zsh)"
507
+
508
+ # Only define the function, don't execute it
509
+ _phantom() {
510
+ local -a commands
511
+ commands=(
512
+ 'create:Create a new Git worktree (phantom)'
513
+ 'attach:Attach to an existing branch by creating a new worktree'
514
+ 'list:List all Git worktrees (phantoms)'
515
+ 'where:Output the filesystem path of a specific worktree'
516
+ 'delete:Delete a Git worktree (phantom)'
517
+ 'exec:Execute a command in a worktree directory'
518
+ 'shell:Open an interactive shell in a worktree directory'
519
+ 'version:Display phantom version information'
520
+ 'completion:Generate shell completion scripts'
521
+ )
522
+
523
+ _arguments -C \\
524
+ '--help[Show help (-h)]' \\
525
+ '--version[Show version (-v)]' \\
526
+ '1:command:->command' \\
527
+ '*::arg:->args'
528
+
529
+ case \${state} in
530
+ command)
531
+ _describe 'phantom command' commands
532
+ ;;
533
+ args)
534
+ case \${line[1]} in
535
+ create)
536
+ _arguments \\
537
+ '--shell[Open an interactive shell in the new worktree after creation (-s)]' \\
538
+ '--exec[Execute a command in the new worktree after creation (-x)]:command:' \\
539
+ '--tmux[Open the worktree in a new tmux window (-t)]' \\
540
+ '--tmux-vertical[Open the worktree in a vertical tmux pane]' \\
541
+ '--tmux-horizontal[Open the worktree in a horizontal tmux pane]' \\
542
+ '*--copy-file[Copy specified files from the current worktree]:file:_files' \\
543
+ '1:name:'
544
+ ;;
545
+ attach)
546
+ _arguments \\
547
+ '--shell[Open an interactive shell in the worktree after attaching (-s)]' \\
548
+ '--exec[Execute a command in the worktree after attaching (-x)]:command:' \\
549
+ '1:worktree-name:' \\
550
+ '2:branch-name:'
551
+ ;;
552
+ list)
553
+ _arguments \\
554
+ '--fzf[Use fzf for interactive selection]' \\
555
+ '--names[Output only phantom names (for scripts and completion)]'
556
+ ;;
557
+ where|delete|shell)
558
+ local worktrees
559
+ worktrees=(\${(f)"$(phantom list --names 2>/dev/null)"})
560
+ if [[ \${line[1]} == "where" || \${line[1]} == "shell" ]]; then
561
+ _arguments \\
562
+ '--fzf[Use fzf for interactive selection]' \\
563
+ '1:worktree:(\${(q)worktrees[@]})'
564
+ elif [[ \${line[1]} == "delete" ]]; then
565
+ _arguments \\
566
+ '--force[Force deletion even if worktree has uncommitted changes (-f)]' \\
567
+ '--current[Delete the current worktree]' \\
568
+ '--fzf[Use fzf for interactive selection]' \\
569
+ '1:worktree:(\${(q)worktrees[@]})'
570
+ fi
571
+ ;;
572
+ exec)
573
+ local worktrees
574
+ worktrees=(\${(f)"$(phantom list --names 2>/dev/null)"})
575
+ _arguments \\
576
+ '1:worktree:(\${(q)worktrees[@]})' \\
577
+ '*:command:_command_names'
578
+ ;;
579
+ completion)
580
+ _arguments \\
581
+ '1:shell:(fish zsh)'
582
+ ;;
583
+ esac
584
+ ;;
585
+ esac
586
+ }
587
+
588
+ # Register the completion function if loading dynamically
589
+ if [[ -n \${ZSH_VERSION} ]]; then
590
+ autoload -Uz compinit && compinit -C
591
+ compdef _phantom phantom
592
+ fi`;
593
+ function completionHandler(args2) {
594
+ const shell = args2[0];
595
+ if (!shell) {
596
+ output.error("Usage: phantom completion <shell>");
597
+ output.error("Supported shells: fish, zsh");
598
+ exit(1);
599
+ }
600
+ switch (shell.toLowerCase()) {
601
+ case "fish":
602
+ console.log(FISH_COMPLETION_SCRIPT);
603
+ break;
604
+ case "zsh":
605
+ console.log(ZSH_COMPLETION_SCRIPT);
606
+ break;
607
+ default:
608
+ output.error(`Unsupported shell: ${shell}`);
609
+ output.error("Supported shells: fish, zsh");
610
+ exit(1);
611
+ }
612
+ }
613
+
421
614
  // src/cli/handlers/create.ts
422
615
  import { parseArgs as parseArgs2 } from "node:util";
423
616
 
@@ -523,11 +716,14 @@ async function isInsideTmux() {
523
716
  return process.env.TMUX !== void 0;
524
717
  }
525
718
  async function executeTmuxCommand(options) {
526
- const { direction, command: command2, cwd, env } = options;
719
+ const { direction, command: command2, args: args2, cwd, env, windowName } = options;
527
720
  const tmuxArgs = [];
528
721
  switch (direction) {
529
722
  case "new":
530
723
  tmuxArgs.push("new-window");
724
+ if (windowName) {
725
+ tmuxArgs.push("-n", windowName);
726
+ }
531
727
  break;
532
728
  case "vertical":
533
729
  tmuxArgs.push("split-window", "-v");
@@ -545,6 +741,9 @@ async function executeTmuxCommand(options) {
545
741
  }
546
742
  }
547
743
  tmuxArgs.push(command2);
744
+ if (args2 && args2.length > 0) {
745
+ tmuxArgs.push(...args2);
746
+ }
548
747
  const result = await spawnProcess({
549
748
  command: "tmux",
550
749
  args: tmuxArgs
@@ -619,8 +818,8 @@ async function createWorktree(gitRoot, name, options = {}) {
619
818
  await fs3.mkdir(worktreesPath, { recursive: true });
620
819
  }
621
820
  const validation = await validateWorktreeDoesNotExist(gitRoot, name);
622
- if (validation.exists) {
623
- return err(new WorktreeAlreadyExistsError(name));
821
+ if (isErr(validation)) {
822
+ return err(validation.error);
624
823
  }
625
824
  try {
626
825
  await addWorktree({
@@ -825,11 +1024,8 @@ Opening worktree '${worktreeName}' in tmux ${tmuxDirection === "new" ? "window"
825
1024
  direction: tmuxDirection,
826
1025
  command: shell,
827
1026
  cwd: result.value.path,
828
- env: {
829
- PHANTOM: "1",
830
- PHANTOM_NAME: worktreeName,
831
- PHANTOM_PATH: result.value.path
832
- }
1027
+ env: getPhantomEnv(worktreeName, result.value.path),
1028
+ windowName: tmuxDirection === "new" ? worktreeName : void 0
833
1029
  });
834
1030
  if (isErr(tmuxResult)) {
835
1031
  output.error(tmuxResult.error.message);
@@ -956,10 +1152,10 @@ async function deleteBranch(gitRoot, branchName) {
956
1152
  async function deleteWorktree(gitRoot, name, options = {}) {
957
1153
  const { force = false } = options;
958
1154
  const validation = await validateWorktreeExists(gitRoot, name);
959
- if (!validation.exists) {
960
- return err(new WorktreeNotFoundError(name));
1155
+ if (isErr(validation)) {
1156
+ return err(validation.error);
961
1157
  }
962
- const worktreePath = validation.path;
1158
+ const worktreePath = validation.value.path;
963
1159
  const status = await getWorktreeStatus(worktreePath);
964
1160
  if (status.hasUncommittedChanges && !force) {
965
1161
  return err(
@@ -1114,7 +1310,7 @@ async function selectWorktreeWithFzf(gitRoot) {
1114
1310
  });
1115
1311
  const fzfResult = await selectWithFzf(list, {
1116
1312
  prompt: "Select worktree> ",
1117
- header: "Git Worktrees (Phantoms)"
1313
+ header: "Git Worktrees"
1118
1314
  });
1119
1315
  if (isErr(fzfResult)) {
1120
1316
  return fzfResult;
@@ -1228,21 +1424,106 @@ async function deleteHandler(args2) {
1228
1424
  // src/cli/handlers/exec.ts
1229
1425
  import { parseArgs as parseArgs4 } from "node:util";
1230
1426
  async function execHandler(args2) {
1231
- const { positionals } = parseArgs4({
1427
+ const { positionals, values } = parseArgs4({
1232
1428
  args: args2,
1233
- options: {},
1429
+ options: {
1430
+ fzf: {
1431
+ type: "boolean",
1432
+ default: false
1433
+ },
1434
+ tmux: {
1435
+ type: "boolean",
1436
+ short: "t"
1437
+ },
1438
+ "tmux-vertical": {
1439
+ type: "boolean"
1440
+ },
1441
+ "tmux-v": {
1442
+ type: "boolean"
1443
+ },
1444
+ "tmux-horizontal": {
1445
+ type: "boolean"
1446
+ },
1447
+ "tmux-h": {
1448
+ type: "boolean"
1449
+ }
1450
+ },
1234
1451
  strict: true,
1235
1452
  allowPositionals: true
1236
1453
  });
1237
- if (positionals.length < 2) {
1238
- exitWithError(
1239
- "Usage: phantom exec <worktree-name> <command> [args...]",
1240
- exitCodes.validationError
1241
- );
1454
+ const useFzf = values.fzf ?? false;
1455
+ const tmuxOption = values.tmux || values["tmux-vertical"] || values["tmux-v"] || values["tmux-horizontal"] || values["tmux-h"];
1456
+ let tmuxDirection;
1457
+ if (values.tmux) {
1458
+ tmuxDirection = "new";
1459
+ } else if (values["tmux-vertical"] || values["tmux-v"]) {
1460
+ tmuxDirection = "vertical";
1461
+ } else if (values["tmux-horizontal"] || values["tmux-h"]) {
1462
+ tmuxDirection = "horizontal";
1463
+ }
1464
+ let commandArgs;
1465
+ if (useFzf) {
1466
+ if (positionals.length < 1) {
1467
+ exitWithError(
1468
+ "Usage: phantom exec --fzf <command> [args...]",
1469
+ exitCodes.validationError
1470
+ );
1471
+ }
1472
+ commandArgs = positionals;
1473
+ } else {
1474
+ if (positionals.length < 2) {
1475
+ exitWithError(
1476
+ "Usage: phantom exec <worktree-name> <command> [args...]",
1477
+ exitCodes.validationError
1478
+ );
1479
+ }
1480
+ commandArgs = positionals.slice(1);
1242
1481
  }
1243
- const [worktreeName, ...commandArgs] = positionals;
1244
1482
  try {
1245
1483
  const gitRoot = await getGitRoot();
1484
+ if (tmuxOption && !await isInsideTmux()) {
1485
+ exitWithError(
1486
+ "The --tmux option can only be used inside a tmux session",
1487
+ exitCodes.validationError
1488
+ );
1489
+ }
1490
+ let worktreeName;
1491
+ if (useFzf) {
1492
+ const selectResult = await selectWorktreeWithFzf(gitRoot);
1493
+ if (isErr(selectResult)) {
1494
+ exitWithError(selectResult.error.message, exitCodes.generalError);
1495
+ }
1496
+ if (!selectResult.value) {
1497
+ exitWithSuccess();
1498
+ }
1499
+ worktreeName = selectResult.value.name;
1500
+ } else {
1501
+ worktreeName = positionals[0];
1502
+ }
1503
+ const validation = await validateWorktreeExists(gitRoot, worktreeName);
1504
+ if (isErr(validation)) {
1505
+ exitWithError(validation.error.message, exitCodes.generalError);
1506
+ }
1507
+ if (tmuxDirection) {
1508
+ output.log(
1509
+ `Executing command in worktree '${worktreeName}' in tmux ${tmuxDirection === "new" ? "window" : "pane"}...`
1510
+ );
1511
+ const [command2, ...args3] = commandArgs;
1512
+ const tmuxResult = await executeTmuxCommand({
1513
+ direction: tmuxDirection,
1514
+ command: command2,
1515
+ args: args3,
1516
+ cwd: validation.value.path,
1517
+ env: getPhantomEnv(worktreeName, validation.value.path),
1518
+ windowName: tmuxDirection === "new" ? worktreeName : void 0
1519
+ });
1520
+ if (isErr(tmuxResult)) {
1521
+ output.error(tmuxResult.error.message);
1522
+ const exitCode = "exitCode" in tmuxResult.error ? tmuxResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
1523
+ exitWithError("", exitCode);
1524
+ }
1525
+ exitWithSuccess();
1526
+ }
1246
1527
  const result = await execInWorktree(
1247
1528
  gitRoot,
1248
1529
  worktreeName,
@@ -1336,12 +1617,37 @@ async function shellHandler(args2) {
1336
1617
  fzf: {
1337
1618
  type: "boolean",
1338
1619
  default: false
1620
+ },
1621
+ tmux: {
1622
+ type: "boolean",
1623
+ short: "t"
1624
+ },
1625
+ "tmux-vertical": {
1626
+ type: "boolean"
1627
+ },
1628
+ "tmux-v": {
1629
+ type: "boolean"
1630
+ },
1631
+ "tmux-horizontal": {
1632
+ type: "boolean"
1633
+ },
1634
+ "tmux-h": {
1635
+ type: "boolean"
1339
1636
  }
1340
1637
  },
1341
1638
  strict: true,
1342
1639
  allowPositionals: true
1343
1640
  });
1344
1641
  const useFzf = values.fzf ?? false;
1642
+ const tmuxOption = values.tmux || values["tmux-vertical"] || values["tmux-v"] || values["tmux-horizontal"] || values["tmux-h"];
1643
+ let tmuxDirection;
1644
+ if (values.tmux) {
1645
+ tmuxDirection = "new";
1646
+ } else if (values["tmux-vertical"] || values["tmux-v"]) {
1647
+ tmuxDirection = "vertical";
1648
+ } else if (values["tmux-horizontal"] || values["tmux-h"]) {
1649
+ tmuxDirection = "horizontal";
1650
+ }
1345
1651
  if (positionals.length === 0 && !useFzf) {
1346
1652
  exitWithError(
1347
1653
  "Usage: phantom shell <worktree-name> or phantom shell --fzf",
@@ -1357,6 +1663,12 @@ async function shellHandler(args2) {
1357
1663
  let worktreeName;
1358
1664
  try {
1359
1665
  const gitRoot = await getGitRoot();
1666
+ if (tmuxOption && !await isInsideTmux()) {
1667
+ exitWithError(
1668
+ "The --tmux option can only be used inside a tmux session",
1669
+ exitCodes.validationError
1670
+ );
1671
+ }
1360
1672
  if (useFzf) {
1361
1673
  const selectResult = await selectWorktreeWithFzf(gitRoot);
1362
1674
  if (isErr(selectResult)) {
@@ -1370,13 +1682,31 @@ async function shellHandler(args2) {
1370
1682
  worktreeName = positionals[0];
1371
1683
  }
1372
1684
  const validation = await validateWorktreeExists(gitRoot, worktreeName);
1373
- if (!validation.exists) {
1374
- exitWithError(
1375
- validation.message || `Worktree '${worktreeName}' not found`,
1376
- exitCodes.generalError
1685
+ if (isErr(validation)) {
1686
+ exitWithError(validation.error.message, exitCodes.generalError);
1687
+ }
1688
+ if (tmuxDirection) {
1689
+ output.log(
1690
+ `Opening worktree '${worktreeName}' in tmux ${tmuxDirection === "new" ? "window" : "pane"}...`
1377
1691
  );
1692
+ const shell = process.env.SHELL || "/bin/sh";
1693
+ const tmuxResult = await executeTmuxCommand({
1694
+ direction: tmuxDirection,
1695
+ command: shell,
1696
+ cwd: validation.value.path,
1697
+ env: getPhantomEnv(worktreeName, validation.value.path),
1698
+ windowName: tmuxDirection === "new" ? worktreeName : void 0
1699
+ });
1700
+ if (isErr(tmuxResult)) {
1701
+ output.error(tmuxResult.error.message);
1702
+ const exitCode = "exitCode" in tmuxResult.error ? tmuxResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
1703
+ exitWithError("", exitCode);
1704
+ }
1705
+ exitWithSuccess();
1378
1706
  }
1379
- output.log(`Entering worktree '${worktreeName}' at ${validation.path}`);
1707
+ output.log(
1708
+ `Entering worktree '${worktreeName}' at ${validation.value.path}`
1709
+ );
1380
1710
  output.log("Type 'exit' to return to your original directory\n");
1381
1711
  const result = await shellInWorktree(gitRoot, worktreeName);
1382
1712
  if (isErr(result)) {
@@ -1399,7 +1729,7 @@ import { parseArgs as parseArgs7 } from "node:util";
1399
1729
  var package_default = {
1400
1730
  name: "@aku11i/phantom",
1401
1731
  packageManager: "pnpm@10.8.1",
1402
- version: "0.8.1",
1732
+ version: "1.0.0",
1403
1733
  description: "A powerful CLI tool for managing Git worktrees for parallel development",
1404
1734
  keywords: [
1405
1735
  "git",
@@ -1479,11 +1809,11 @@ import { parseArgs as parseArgs8 } from "node:util";
1479
1809
  // src/core/worktree/where.ts
1480
1810
  async function whereWorktree(gitRoot, name) {
1481
1811
  const validation = await validateWorktreeExists(gitRoot, name);
1482
- if (!validation.exists) {
1483
- return err(new WorktreeNotFoundError(name));
1812
+ if (isErr(validation)) {
1813
+ return err(validation.error);
1484
1814
  }
1485
1815
  return ok({
1486
- path: validation.path
1816
+ path: validation.value.path
1487
1817
  });
1488
1818
  }
1489
1819
 
@@ -1729,10 +2059,41 @@ var attachHelp = {
1729
2059
  ]
1730
2060
  };
1731
2061
 
2062
+ // src/cli/help/completion.ts
2063
+ var completionHelp = {
2064
+ name: "completion",
2065
+ usage: "phantom completion <shell>",
2066
+ description: "Generate shell completion scripts for fish or zsh",
2067
+ examples: [
2068
+ {
2069
+ command: "phantom completion fish > ~/.config/fish/completions/phantom.fish",
2070
+ description: "Generate and install Fish completion"
2071
+ },
2072
+ {
2073
+ command: "phantom completion fish | source",
2074
+ description: "Load Fish completion in current session"
2075
+ },
2076
+ {
2077
+ command: "phantom completion zsh > ~/.zsh/completions/_phantom",
2078
+ description: "Generate and install Zsh completion"
2079
+ },
2080
+ {
2081
+ command: 'eval "$(phantom completion zsh)"',
2082
+ description: "Load Zsh completion in current session"
2083
+ }
2084
+ ],
2085
+ notes: [
2086
+ "Supported shells: fish, zsh",
2087
+ "After installing completions, you may need to restart your shell or source the completion file",
2088
+ "For Fish: completions are loaded automatically from ~/.config/fish/completions/",
2089
+ "For Zsh: ensure the completion file is in a directory in your $fpath"
2090
+ ]
2091
+ };
2092
+
1732
2093
  // src/cli/help/create.ts
1733
2094
  var createHelp = {
1734
2095
  name: "create",
1735
- description: "Create a new Git worktree (phantom)",
2096
+ description: "Create a new Git worktree",
1736
2097
  usage: "phantom create <name> [options]",
1737
2098
  options: [
1738
2099
  {
@@ -1804,7 +2165,7 @@ var createHelp = {
1804
2165
  // src/cli/help/delete.ts
1805
2166
  var deleteHelp = {
1806
2167
  name: "delete",
1807
- description: "Delete a Git worktree (phantom)",
2168
+ description: "Delete a Git worktree",
1808
2169
  usage: "phantom delete <name> [options]",
1809
2170
  options: [
1810
2171
  {
@@ -1853,7 +2214,29 @@ var deleteHelp = {
1853
2214
  var execHelp = {
1854
2215
  name: "exec",
1855
2216
  description: "Execute a command in a worktree directory",
1856
- usage: "phantom exec <worktree-name> <command> [args...]",
2217
+ usage: "phantom exec [options] <worktree-name> <command> [args...]",
2218
+ options: [
2219
+ {
2220
+ name: "--fzf",
2221
+ type: "boolean",
2222
+ description: "Use fzf for interactive worktree selection"
2223
+ },
2224
+ {
2225
+ name: "--tmux, -t",
2226
+ type: "boolean",
2227
+ description: "Execute command in new tmux window"
2228
+ },
2229
+ {
2230
+ name: "--tmux-vertical, --tmux-v",
2231
+ type: "boolean",
2232
+ description: "Execute command in vertical split pane"
2233
+ },
2234
+ {
2235
+ name: "--tmux-horizontal, --tmux-h",
2236
+ type: "boolean",
2237
+ description: "Execute command in horizontal split pane"
2238
+ }
2239
+ ],
1857
2240
  examples: [
1858
2241
  {
1859
2242
  description: "Run npm test in a worktree",
@@ -1866,19 +2249,37 @@ var execHelp = {
1866
2249
  {
1867
2250
  description: "Run a complex command with arguments",
1868
2251
  command: "phantom exec staging npm run build -- --production"
2252
+ },
2253
+ {
2254
+ description: "Execute with interactive selection",
2255
+ command: "phantom exec --fzf npm run dev"
2256
+ },
2257
+ {
2258
+ description: "Run dev server in new tmux window",
2259
+ command: "phantom exec --tmux feature-auth npm run dev"
2260
+ },
2261
+ {
2262
+ description: "Run tests in vertical split pane",
2263
+ command: "phantom exec --tmux-v feature-auth npm test"
2264
+ },
2265
+ {
2266
+ description: "Interactive selection with tmux",
2267
+ command: "phantom exec --fzf --tmux npm run dev"
1869
2268
  }
1870
2269
  ],
1871
2270
  notes: [
1872
2271
  "The command is executed with the worktree directory as the working directory",
1873
2272
  "All arguments after the worktree name are passed to the command",
1874
- "The exit code of the executed command is preserved"
2273
+ "The exit code of the executed command is preserved",
2274
+ "With --fzf, select the worktree interactively before executing the command",
2275
+ "Tmux options require being inside a tmux session"
1875
2276
  ]
1876
2277
  };
1877
2278
 
1878
2279
  // src/cli/help/list.ts
1879
2280
  var listHelp = {
1880
2281
  name: "list",
1881
- description: "List all Git worktrees (phantoms)",
2282
+ description: "List all Git worktrees",
1882
2283
  usage: "phantom list [options]",
1883
2284
  options: [
1884
2285
  {
@@ -1889,7 +2290,7 @@ var listHelp = {
1889
2290
  {
1890
2291
  name: "--names",
1891
2292
  type: "boolean",
1892
- description: "Output only phantom names (for scripts and completion)"
2293
+ description: "Output only worktree names (for scripts and completion)"
1893
2294
  }
1894
2295
  ],
1895
2296
  examples: [
@@ -1924,6 +2325,21 @@ var shellHelp = {
1924
2325
  name: "--fzf",
1925
2326
  type: "boolean",
1926
2327
  description: "Use fzf for interactive selection"
2328
+ },
2329
+ {
2330
+ name: "--tmux, -t",
2331
+ type: "boolean",
2332
+ description: "Open shell in new tmux window"
2333
+ },
2334
+ {
2335
+ name: "--tmux-vertical, --tmux-v",
2336
+ type: "boolean",
2337
+ description: "Open shell in vertical split pane"
2338
+ },
2339
+ {
2340
+ name: "--tmux-horizontal, --tmux-h",
2341
+ type: "boolean",
2342
+ description: "Open shell in horizontal split pane"
1927
2343
  }
1928
2344
  ],
1929
2345
  examples: [
@@ -1934,13 +2350,26 @@ var shellHelp = {
1934
2350
  {
1935
2351
  description: "Open a shell with interactive fzf selection",
1936
2352
  command: "phantom shell --fzf"
2353
+ },
2354
+ {
2355
+ description: "Open a shell in a new tmux window",
2356
+ command: "phantom shell feature-auth --tmux"
2357
+ },
2358
+ {
2359
+ description: "Open a shell in a vertical tmux pane",
2360
+ command: "phantom shell feature-auth --tmux-v"
2361
+ },
2362
+ {
2363
+ description: "Interactive selection with tmux",
2364
+ command: "phantom shell --fzf --tmux"
1937
2365
  }
1938
2366
  ],
1939
2367
  notes: [
1940
2368
  "Uses your default shell from the SHELL environment variable",
1941
2369
  "The shell starts with the worktree directory as the working directory",
1942
2370
  "Type 'exit' to return to your original directory",
1943
- "With --fzf, you can interactively select the worktree to enter"
2371
+ "With --fzf, you can interactively select the worktree to enter",
2372
+ "Tmux options require being inside a tmux session"
1944
2373
  ]
1945
2374
  };
1946
2375
 
@@ -2044,6 +2473,12 @@ var commands = [
2044
2473
  description: "Display phantom version information",
2045
2474
  handler: versionHandler,
2046
2475
  help: versionHelp
2476
+ },
2477
+ {
2478
+ name: "completion",
2479
+ description: "Generate shell completion scripts",
2480
+ handler: completionHandler,
2481
+ help: completionHelp
2047
2482
  }
2048
2483
  ];
2049
2484
  function printHelp(commands2) {
@@ -2076,18 +2511,18 @@ function findCommand(args2, commands2) {
2076
2511
  var args = argv.slice(2);
2077
2512
  if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
2078
2513
  printHelp(commands);
2079
- exit(0);
2514
+ exit2(0);
2080
2515
  }
2081
2516
  if (args[0] === "--version" || args[0] === "-v") {
2082
2517
  versionHandler();
2083
- exit(0);
2518
+ exit2(0);
2084
2519
  }
2085
2520
  var { command, remainingArgs } = findCommand(args, commands);
2086
2521
  if (!command || !command.handler) {
2087
2522
  console.error(`Error: Unknown command '${args.join(" ")}'
2088
2523
  `);
2089
2524
  printHelp(commands);
2090
- exit(1);
2525
+ exit2(1);
2091
2526
  }
2092
2527
  if (remainingArgs.includes("--help") || remainingArgs.includes("-h")) {
2093
2528
  if (command.help) {
@@ -2095,7 +2530,7 @@ if (remainingArgs.includes("--help") || remainingArgs.includes("-h")) {
2095
2530
  } else {
2096
2531
  console.log(`Help not available for command '${command.name}'`);
2097
2532
  }
2098
- exit(0);
2533
+ exit2(0);
2099
2534
  }
2100
2535
  try {
2101
2536
  await command.handler(remainingArgs);
@@ -2104,6 +2539,6 @@ try {
2104
2539
  "Error:",
2105
2540
  error instanceof Error ? error.message : String(error)
2106
2541
  );
2107
- exit(1);
2542
+ exit2(1);
2108
2543
  }
2109
2544
  //# sourceMappingURL=phantom.js.map