@aku11i/phantom 0.5.0 → 0.7.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
@@ -44,9 +44,9 @@ async function executeGitCommandInDirectory(directory, args2) {
44
44
 
45
45
  // src/core/git/libs/get-git-root.ts
46
46
  async function getGitRoot() {
47
- const { stdout } = await executeGitCommand(["rev-parse", "--git-common-dir"]);
48
- if (stdout.endsWith("/.git") || stdout === ".git") {
49
- return resolve(process.cwd(), dirname(stdout));
47
+ const { stdout: stdout2 } = await executeGitCommand(["rev-parse", "--git-common-dir"]);
48
+ if (stdout2.endsWith("/.git") || stdout2 === ".git") {
49
+ return resolve(process.cwd(), dirname(stdout2));
50
50
  }
51
51
  const { stdout: toplevel } = await executeGitCommand([
52
52
  "rev-parse",
@@ -216,18 +216,20 @@ async function spawnProcess(config) {
216
216
  }
217
217
 
218
218
  // src/core/process/exec.ts
219
- async function execInWorktree(gitRoot, worktreeName, command2) {
219
+ async function execInWorktree(gitRoot, worktreeName, command2, options = {}) {
220
220
  const validation = await validateWorktreeExists(gitRoot, worktreeName);
221
221
  if (!validation.exists) {
222
222
  return err(new WorktreeNotFoundError(worktreeName));
223
223
  }
224
224
  const worktreePath = validation.path;
225
225
  const [cmd, ...args2] = command2;
226
+ const stdio = options.interactive ? "inherit" : ["ignore", "inherit", "inherit"];
226
227
  return spawnProcess({
227
228
  command: cmd,
228
229
  args: args2,
229
230
  options: {
230
- cwd: worktreePath
231
+ cwd: worktreePath,
232
+ stdio
231
233
  }
232
234
  });
233
235
  }
@@ -327,6 +329,9 @@ var output = {
327
329
  error: (message) => {
328
330
  console.error(message);
329
331
  },
332
+ warn: (message) => {
333
+ console.warn(message);
334
+ },
330
335
  table: (data) => {
331
336
  console.table(data);
332
337
  },
@@ -404,7 +409,8 @@ async function attachHandler(args2) {
404
409
  const execResult = await execInWorktree(
405
410
  gitRoot,
406
411
  branchName,
407
- values.exec.split(" ")
412
+ values.exec.split(" "),
413
+ { interactive: true }
408
414
  );
409
415
  if (isErr(execResult)) {
410
416
  exitWithError(execResult.error.message, exitCodes.generalError);
@@ -415,13 +421,187 @@ async function attachHandler(args2) {
415
421
  // src/cli/handlers/create.ts
416
422
  import { parseArgs as parseArgs2 } from "node:util";
417
423
 
418
- // src/core/worktree/create.ts
424
+ // src/core/config/loader.ts
419
425
  import fs2 from "node:fs/promises";
426
+ import path from "node:path";
427
+
428
+ // src/core/utils/type-guards.ts
429
+ function isObject(value) {
430
+ return typeof value === "object" && value !== null && !Array.isArray(value);
431
+ }
432
+
433
+ // src/core/config/validate.ts
434
+ var ConfigValidationError = class extends Error {
435
+ constructor(message) {
436
+ super(`Invalid phantom.config.json: ${message}`);
437
+ this.name = "ConfigValidationError";
438
+ }
439
+ };
440
+ function validateConfig(config) {
441
+ if (!isObject(config)) {
442
+ return err(new ConfigValidationError("Configuration must be an object"));
443
+ }
444
+ const cfg = config;
445
+ if (cfg.postCreate !== void 0) {
446
+ if (!isObject(cfg.postCreate)) {
447
+ return err(new ConfigValidationError("postCreate must be an object"));
448
+ }
449
+ const postCreate = cfg.postCreate;
450
+ if (postCreate.copyFiles !== void 0) {
451
+ if (!Array.isArray(postCreate.copyFiles)) {
452
+ return err(
453
+ new ConfigValidationError("postCreate.copyFiles must be an array")
454
+ );
455
+ }
456
+ if (!postCreate.copyFiles.every((f) => typeof f === "string")) {
457
+ return err(
458
+ new ConfigValidationError(
459
+ "postCreate.copyFiles must contain only strings"
460
+ )
461
+ );
462
+ }
463
+ }
464
+ if (postCreate.commands !== void 0) {
465
+ if (!Array.isArray(postCreate.commands)) {
466
+ return err(
467
+ new ConfigValidationError("postCreate.commands must be an array")
468
+ );
469
+ }
470
+ if (!postCreate.commands.every((c) => typeof c === "string")) {
471
+ return err(
472
+ new ConfigValidationError(
473
+ "postCreate.commands must contain only strings"
474
+ )
475
+ );
476
+ }
477
+ }
478
+ }
479
+ return ok(config);
480
+ }
481
+
482
+ // src/core/config/loader.ts
483
+ var ConfigNotFoundError = class extends Error {
484
+ constructor() {
485
+ super("phantom.config.json not found");
486
+ this.name = "ConfigNotFoundError";
487
+ }
488
+ };
489
+ var ConfigParseError = class extends Error {
490
+ constructor(message) {
491
+ super(`Failed to parse phantom.config.json: ${message}`);
492
+ this.name = "ConfigParseError";
493
+ }
494
+ };
495
+ async function loadConfig(gitRoot) {
496
+ const configPath = path.join(gitRoot, "phantom.config.json");
497
+ try {
498
+ const content = await fs2.readFile(configPath, "utf-8");
499
+ try {
500
+ const parsed = JSON.parse(content);
501
+ const validationResult = validateConfig(parsed);
502
+ if (!validationResult.ok) {
503
+ return err(validationResult.error);
504
+ }
505
+ return ok(validationResult.value);
506
+ } catch (error) {
507
+ return err(
508
+ new ConfigParseError(
509
+ error instanceof Error ? error.message : String(error)
510
+ )
511
+ );
512
+ }
513
+ } catch (error) {
514
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
515
+ return err(new ConfigNotFoundError());
516
+ }
517
+ throw error;
518
+ }
519
+ }
520
+
521
+ // src/core/process/tmux.ts
522
+ async function isInsideTmux() {
523
+ return process.env.TMUX !== void 0;
524
+ }
525
+ async function executeTmuxCommand(options) {
526
+ const { direction, command: command2, cwd, env } = options;
527
+ const tmuxArgs = [];
528
+ switch (direction) {
529
+ case "new":
530
+ tmuxArgs.push("new-window");
531
+ break;
532
+ case "vertical":
533
+ tmuxArgs.push("split-window", "-v");
534
+ break;
535
+ case "horizontal":
536
+ tmuxArgs.push("split-window", "-h");
537
+ break;
538
+ }
539
+ if (cwd) {
540
+ tmuxArgs.push("-c", cwd);
541
+ }
542
+ if (env) {
543
+ for (const [key, value] of Object.entries(env)) {
544
+ tmuxArgs.push("-e", `${key}=${value}`);
545
+ }
546
+ }
547
+ tmuxArgs.push(command2);
548
+ const result = await spawnProcess({
549
+ command: "tmux",
550
+ args: tmuxArgs
551
+ });
552
+ return result;
553
+ }
554
+
555
+ // src/core/worktree/create.ts
556
+ import fs3 from "node:fs/promises";
420
557
 
421
558
  // src/core/git/libs/add-worktree.ts
422
559
  async function addWorktree(options) {
423
- const { path, branch, commitish = "HEAD" } = options;
424
- await executeGitCommand(["worktree", "add", path, "-b", branch, commitish]);
560
+ const { path: path3, branch, commitish = "HEAD" } = options;
561
+ await executeGitCommand(["worktree", "add", path3, "-b", branch, commitish]);
562
+ }
563
+
564
+ // src/core/worktree/file-copier.ts
565
+ import { copyFile, mkdir, stat } from "node:fs/promises";
566
+ import path2 from "node:path";
567
+ var FileCopyError = class extends Error {
568
+ file;
569
+ constructor(file, message) {
570
+ super(`Failed to copy ${file}: ${message}`);
571
+ this.name = "FileCopyError";
572
+ this.file = file;
573
+ }
574
+ };
575
+ async function copyFiles(sourceDir, targetDir, files) {
576
+ const copiedFiles = [];
577
+ const skippedFiles = [];
578
+ for (const file of files) {
579
+ const sourcePath = path2.join(sourceDir, file);
580
+ const targetPath = path2.join(targetDir, file);
581
+ try {
582
+ const stats = await stat(sourcePath);
583
+ if (!stats.isFile()) {
584
+ skippedFiles.push(file);
585
+ continue;
586
+ }
587
+ const targetDirPath = path2.dirname(targetPath);
588
+ await mkdir(targetDirPath, { recursive: true });
589
+ await copyFile(sourcePath, targetPath);
590
+ copiedFiles.push(file);
591
+ } catch (error) {
592
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
593
+ skippedFiles.push(file);
594
+ } else {
595
+ return err(
596
+ new FileCopyError(
597
+ file,
598
+ error instanceof Error ? error.message : String(error)
599
+ )
600
+ );
601
+ }
602
+ }
603
+ }
604
+ return ok({ copiedFiles, skippedFiles });
425
605
  }
426
606
 
427
607
  // src/core/worktree/create.ts
@@ -434,9 +614,9 @@ async function createWorktree(gitRoot, name, options = {}) {
434
614
  const worktreesPath = getPhantomDirectory(gitRoot);
435
615
  const worktreePath = getWorktreePath(gitRoot, name);
436
616
  try {
437
- await fs2.access(worktreesPath);
617
+ await fs3.access(worktreesPath);
438
618
  } catch {
439
- await fs2.mkdir(worktreesPath, { recursive: true });
619
+ await fs3.mkdir(worktreesPath, { recursive: true });
440
620
  }
441
621
  const validation = await validateWorktreeDoesNotExist(gitRoot, name);
442
622
  if (validation.exists) {
@@ -448,9 +628,28 @@ async function createWorktree(gitRoot, name, options = {}) {
448
628
  branch,
449
629
  commitish
450
630
  });
631
+ let copiedFiles;
632
+ let skippedFiles;
633
+ let copyError;
634
+ if (options.copyFiles && options.copyFiles.length > 0) {
635
+ const copyResult = await copyFiles(
636
+ gitRoot,
637
+ worktreePath,
638
+ options.copyFiles
639
+ );
640
+ if (isOk(copyResult)) {
641
+ copiedFiles = copyResult.value.copiedFiles;
642
+ skippedFiles = copyResult.value.skippedFiles;
643
+ } else {
644
+ copyError = copyResult.error.message;
645
+ }
646
+ }
451
647
  return ok({
452
648
  message: `Created worktree '${name}' at ${worktreePath}`,
453
- path: worktreePath
649
+ path: worktreePath,
650
+ copiedFiles,
651
+ skippedFiles,
652
+ copyError
454
653
  });
455
654
  } catch (error) {
456
655
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -470,6 +669,26 @@ async function createHandler(args2) {
470
669
  exec: {
471
670
  type: "string",
472
671
  short: "x"
672
+ },
673
+ tmux: {
674
+ type: "boolean",
675
+ short: "t"
676
+ },
677
+ "tmux-vertical": {
678
+ type: "boolean"
679
+ },
680
+ "tmux-v": {
681
+ type: "boolean"
682
+ },
683
+ "tmux-horizontal": {
684
+ type: "boolean"
685
+ },
686
+ "tmux-h": {
687
+ type: "boolean"
688
+ },
689
+ "copy-file": {
690
+ type: "string",
691
+ multiple: true
473
692
  }
474
693
  },
475
694
  strict: true,
@@ -484,31 +703,97 @@ async function createHandler(args2) {
484
703
  const worktreeName = positionals[0];
485
704
  const openShell = values.shell ?? false;
486
705
  const execCommand = values.exec;
487
- if (openShell && execCommand) {
706
+ const copyFileOptions = values["copy-file"];
707
+ const tmuxOption = values.tmux || values["tmux-vertical"] || values["tmux-v"] || values["tmux-horizontal"] || values["tmux-h"];
708
+ let tmuxDirection;
709
+ if (values.tmux) {
710
+ tmuxDirection = "new";
711
+ } else if (values["tmux-vertical"] || values["tmux-v"]) {
712
+ tmuxDirection = "vertical";
713
+ } else if (values["tmux-horizontal"] || values["tmux-h"]) {
714
+ tmuxDirection = "horizontal";
715
+ }
716
+ if ([openShell, execCommand !== void 0, tmuxOption].filter(Boolean).length > 1) {
717
+ exitWithError(
718
+ "Cannot use --shell, --exec, and --tmux options together",
719
+ exitCodes.validationError
720
+ );
721
+ }
722
+ if (tmuxOption && !await isInsideTmux()) {
488
723
  exitWithError(
489
- "Cannot use --shell and --exec together",
724
+ "The --tmux option can only be used inside a tmux session",
490
725
  exitCodes.validationError
491
726
  );
492
727
  }
493
728
  try {
494
729
  const gitRoot = await getGitRoot();
495
- const result = await createWorktree(gitRoot, worktreeName);
730
+ let filesToCopy = [];
731
+ const configResult = await loadConfig(gitRoot);
732
+ if (isOk(configResult)) {
733
+ if (configResult.value.postCreate?.copyFiles) {
734
+ filesToCopy = [...configResult.value.postCreate.copyFiles];
735
+ }
736
+ } else {
737
+ if (configResult.error instanceof ConfigValidationError) {
738
+ output.warn(`Configuration warning: ${configResult.error.message}`);
739
+ } else if (configResult.error instanceof ConfigParseError) {
740
+ output.warn(`Configuration warning: ${configResult.error.message}`);
741
+ }
742
+ }
743
+ if (copyFileOptions && copyFileOptions.length > 0) {
744
+ const cliFiles = Array.isArray(copyFileOptions) ? copyFileOptions : [copyFileOptions];
745
+ filesToCopy = [.../* @__PURE__ */ new Set([...filesToCopy, ...cliFiles])];
746
+ }
747
+ const result = await createWorktree(gitRoot, worktreeName, {
748
+ copyFiles: filesToCopy.length > 0 ? filesToCopy : void 0
749
+ });
496
750
  if (isErr(result)) {
497
751
  const exitCode = result.error instanceof WorktreeAlreadyExistsError ? exitCodes.validationError : exitCodes.generalError;
498
752
  exitWithError(result.error.message, exitCode);
499
753
  }
500
754
  output.log(result.value.message);
755
+ if (result.value.copyError) {
756
+ output.error(
757
+ `
758
+ Warning: Failed to copy some files: ${result.value.copyError}`
759
+ );
760
+ }
761
+ if (isOk(configResult) && configResult.value.postCreate?.commands) {
762
+ const commands2 = configResult.value.postCreate.commands;
763
+ output.log("\nRunning post-create commands...");
764
+ for (const command2 of commands2) {
765
+ output.log(`Executing: ${command2}`);
766
+ const shell = process.env.SHELL || "/bin/sh";
767
+ const cmdResult = await execInWorktree(gitRoot, worktreeName, [
768
+ shell,
769
+ "-c",
770
+ command2
771
+ ]);
772
+ if (isErr(cmdResult)) {
773
+ output.error(`Failed to execute command: ${cmdResult.error.message}`);
774
+ const exitCode = "exitCode" in cmdResult.error ? cmdResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
775
+ exitWithError(`Post-create command failed: ${command2}`, exitCode);
776
+ }
777
+ if (cmdResult.value.exitCode !== 0) {
778
+ exitWithError(
779
+ `Post-create command failed: ${command2}`,
780
+ cmdResult.value.exitCode
781
+ );
782
+ }
783
+ }
784
+ }
501
785
  if (execCommand && isOk(result)) {
502
786
  output.log(
503
787
  `
504
788
  Executing command in worktree '${worktreeName}': ${execCommand}`
505
789
  );
506
790
  const shell = process.env.SHELL || "/bin/sh";
507
- const execResult = await execInWorktree(gitRoot, worktreeName, [
508
- shell,
509
- "-c",
510
- execCommand
511
- ]);
791
+ const execResult = await execInWorktree(
792
+ gitRoot,
793
+ worktreeName,
794
+ [shell, "-c", execCommand],
795
+ { interactive: true }
796
+ );
512
797
  if (isErr(execResult)) {
513
798
  output.error(execResult.error.message);
514
799
  const exitCode = "exitCode" in execResult.error ? execResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
@@ -530,6 +815,28 @@ Entering worktree '${worktreeName}' at ${result.value.path}`
530
815
  }
531
816
  process.exit(shellResult.value.exitCode ?? 0);
532
817
  }
818
+ if (tmuxDirection && isOk(result)) {
819
+ output.log(
820
+ `
821
+ Opening worktree '${worktreeName}' in tmux ${tmuxDirection === "new" ? "window" : "pane"}...`
822
+ );
823
+ const shell = process.env.SHELL || "/bin/sh";
824
+ const tmuxResult = await executeTmuxCommand({
825
+ direction: tmuxDirection,
826
+ command: shell,
827
+ cwd: result.value.path,
828
+ env: {
829
+ PHANTOM: "1",
830
+ PHANTOM_NAME: worktreeName,
831
+ PHANTOM_PATH: result.value.path
832
+ }
833
+ });
834
+ if (isErr(tmuxResult)) {
835
+ output.error(tmuxResult.error.message);
836
+ const exitCode = "exitCode" in tmuxResult.error ? tmuxResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
837
+ exitWithError("", exitCode);
838
+ }
839
+ }
533
840
  exitWithSuccess();
534
841
  } catch (error) {
535
842
  exitWithError(
@@ -542,17 +849,77 @@ Entering worktree '${worktreeName}' at ${result.value.path}`
542
849
  // src/cli/handlers/delete.ts
543
850
  import { parseArgs as parseArgs3 } from "node:util";
544
851
 
852
+ // src/core/git/libs/list-worktrees.ts
853
+ async function listWorktrees(gitRoot) {
854
+ const { stdout: stdout2 } = await executeGitCommand([
855
+ "worktree",
856
+ "list",
857
+ "--porcelain"
858
+ ]);
859
+ const worktrees = [];
860
+ let currentWorktree = {};
861
+ const lines = stdout2.split("\n").filter((line) => line.length > 0);
862
+ for (const line of lines) {
863
+ if (line.startsWith("worktree ")) {
864
+ if (currentWorktree.path) {
865
+ worktrees.push(currentWorktree);
866
+ }
867
+ currentWorktree = {
868
+ path: line.substring("worktree ".length),
869
+ isLocked: false,
870
+ isPrunable: false
871
+ };
872
+ } else if (line.startsWith("HEAD ")) {
873
+ currentWorktree.head = line.substring("HEAD ".length);
874
+ } else if (line.startsWith("branch ")) {
875
+ const fullBranch = line.substring("branch ".length);
876
+ currentWorktree.branch = fullBranch.startsWith("refs/heads/") ? fullBranch.substring("refs/heads/".length) : fullBranch;
877
+ } else if (line === "detached") {
878
+ currentWorktree.branch = "(detached HEAD)";
879
+ } else if (line === "locked") {
880
+ currentWorktree.isLocked = true;
881
+ } else if (line === "prunable") {
882
+ currentWorktree.isPrunable = true;
883
+ }
884
+ }
885
+ if (currentWorktree.path) {
886
+ worktrees.push(currentWorktree);
887
+ }
888
+ return worktrees;
889
+ }
890
+
891
+ // src/core/git/libs/get-current-worktree.ts
892
+ async function getCurrentWorktree(gitRoot) {
893
+ try {
894
+ const { stdout: currentPath } = await executeGitCommand([
895
+ "rev-parse",
896
+ "--show-toplevel"
897
+ ]);
898
+ const currentPathTrimmed = currentPath.trim();
899
+ const worktrees = await listWorktrees(gitRoot);
900
+ const currentWorktree = worktrees.find(
901
+ (wt) => wt.path === currentPathTrimmed
902
+ );
903
+ if (!currentWorktree || currentWorktree.path === gitRoot) {
904
+ return null;
905
+ }
906
+ return currentWorktree.branch;
907
+ } catch {
908
+ return null;
909
+ }
910
+ }
911
+
545
912
  // src/core/worktree/delete.ts
546
913
  async function getWorktreeStatus(worktreePath) {
547
914
  try {
548
- const { stdout } = await executeGitCommandInDirectory(worktreePath, [
915
+ const { stdout: stdout2 } = await executeGitCommandInDirectory(worktreePath, [
549
916
  "status",
550
917
  "--porcelain"
551
918
  ]);
552
- if (stdout) {
919
+ if (stdout2) {
553
920
  return {
554
921
  hasUncommittedChanges: true,
555
- changedFiles: stdout.split("\n").length
922
+ changedFiles: stdout2.split("\n").length
556
923
  };
557
924
  }
558
925
  } catch {
@@ -636,21 +1003,43 @@ async function deleteHandler(args2) {
636
1003
  force: {
637
1004
  type: "boolean",
638
1005
  short: "f"
1006
+ },
1007
+ current: {
1008
+ type: "boolean"
639
1009
  }
640
1010
  },
641
1011
  strict: true,
642
1012
  allowPositionals: true
643
1013
  });
644
- if (positionals.length === 0) {
1014
+ const deleteCurrent = values.current ?? false;
1015
+ if (positionals.length === 0 && !deleteCurrent) {
645
1016
  exitWithError(
646
- "Please provide a worktree name to delete",
1017
+ "Please provide a worktree name to delete or use --current to delete the current worktree",
1018
+ exitCodes.validationError
1019
+ );
1020
+ }
1021
+ if (positionals.length > 0 && deleteCurrent) {
1022
+ exitWithError(
1023
+ "Cannot specify both a worktree name and --current option",
647
1024
  exitCodes.validationError
648
1025
  );
649
1026
  }
650
- const worktreeName = positionals[0];
651
1027
  const forceDelete = values.force ?? false;
652
1028
  try {
653
1029
  const gitRoot = await getGitRoot();
1030
+ let worktreeName;
1031
+ if (deleteCurrent) {
1032
+ const currentWorktree = await getCurrentWorktree(gitRoot);
1033
+ if (!currentWorktree) {
1034
+ exitWithError(
1035
+ "Not in a worktree directory. The --current option can only be used from within a worktree.",
1036
+ exitCodes.validationError
1037
+ );
1038
+ }
1039
+ worktreeName = currentWorktree;
1040
+ } else {
1041
+ worktreeName = positionals[0];
1042
+ }
654
1043
  const result = await deleteWorktree(gitRoot, worktreeName, {
655
1044
  force: forceDelete
656
1045
  });
@@ -686,7 +1075,12 @@ async function execHandler(args2) {
686
1075
  const [worktreeName, ...commandArgs] = positionals;
687
1076
  try {
688
1077
  const gitRoot = await getGitRoot();
689
- const result = await execInWorktree(gitRoot, worktreeName, commandArgs);
1078
+ const result = await execInWorktree(
1079
+ gitRoot,
1080
+ worktreeName,
1081
+ commandArgs,
1082
+ { interactive: true }
1083
+ );
690
1084
  if (isErr(result)) {
691
1085
  const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.notFound : result.error.exitCode || exitCodes.generalError;
692
1086
  exitWithError(result.error.message, exitCode);
@@ -703,53 +1097,14 @@ async function execHandler(args2) {
703
1097
  // src/cli/handlers/list.ts
704
1098
  import { parseArgs as parseArgs5 } from "node:util";
705
1099
 
706
- // src/core/git/libs/list-worktrees.ts
707
- async function listWorktrees(gitRoot) {
708
- const { stdout } = await executeGitCommand([
709
- "worktree",
710
- "list",
711
- "--porcelain"
712
- ]);
713
- const worktrees = [];
714
- let currentWorktree = {};
715
- const lines = stdout.split("\n").filter((line) => line.length > 0);
716
- for (const line of lines) {
717
- if (line.startsWith("worktree ")) {
718
- if (currentWorktree.path) {
719
- worktrees.push(currentWorktree);
720
- }
721
- currentWorktree = {
722
- path: line.substring("worktree ".length),
723
- isLocked: false,
724
- isPrunable: false
725
- };
726
- } else if (line.startsWith("HEAD ")) {
727
- currentWorktree.head = line.substring("HEAD ".length);
728
- } else if (line.startsWith("branch ")) {
729
- const fullBranch = line.substring("branch ".length);
730
- currentWorktree.branch = fullBranch.startsWith("refs/heads/") ? fullBranch.substring("refs/heads/".length) : fullBranch;
731
- } else if (line === "detached") {
732
- currentWorktree.branch = "(detached HEAD)";
733
- } else if (line === "locked") {
734
- currentWorktree.isLocked = true;
735
- } else if (line === "prunable") {
736
- currentWorktree.isPrunable = true;
737
- }
738
- }
739
- if (currentWorktree.path) {
740
- worktrees.push(currentWorktree);
741
- }
742
- return worktrees;
743
- }
744
-
745
1100
  // src/core/worktree/list.ts
746
1101
  async function getWorktreeStatus2(worktreePath) {
747
1102
  try {
748
- const { stdout } = await executeGitCommandInDirectory(worktreePath, [
1103
+ const { stdout: stdout2 } = await executeGitCommandInDirectory(worktreePath, [
749
1104
  "status",
750
1105
  "--porcelain"
751
1106
  ]);
752
- return !stdout;
1107
+ return !stdout2;
753
1108
  } catch {
754
1109
  return true;
755
1110
  }
@@ -871,7 +1226,7 @@ import { parseArgs as parseArgs7 } from "node:util";
871
1226
  var package_default = {
872
1227
  name: "@aku11i/phantom",
873
1228
  packageManager: "pnpm@10.8.1",
874
- version: "0.5.0",
1229
+ version: "0.7.0",
875
1230
  description: "A powerful CLI tool for managing Git worktrees for parallel development",
876
1231
  keywords: [
877
1232
  "git",
@@ -901,7 +1256,9 @@ var package_default = {
901
1256
  phantom: "node ./src/bin/phantom.ts",
902
1257
  build: "node build.ts",
903
1258
  typecheck: "tsgo --noEmit",
904
- test: "node --test --experimental-strip-types --experimental-test-module-mocks src/**/*.test.ts",
1259
+ test: 'node --test --experimental-strip-types --experimental-test-module-mocks "src/**/*.test.js"',
1260
+ "test:coverage": 'node --experimental-test-coverage --test --experimental-strip-types --experimental-test-module-mocks "src/**/*.test.js"',
1261
+ "test:file": "node --test --experimental-strip-types --experimental-test-module-mocks",
905
1262
  lint: "biome check .",
906
1263
  fix: "biome check --write .",
907
1264
  ready: "pnpm fix && pnpm typecheck && pnpm test",
@@ -985,55 +1342,446 @@ async function whereHandler(args2) {
985
1342
  }
986
1343
  }
987
1344
 
1345
+ // src/cli/help.ts
1346
+ import { stdout } from "node:process";
1347
+ var HelpFormatter = class {
1348
+ width;
1349
+ indent = " ";
1350
+ constructor() {
1351
+ this.width = stdout.columns || 80;
1352
+ }
1353
+ formatMainHelp(commands2) {
1354
+ const lines = [];
1355
+ lines.push(this.bold("Phantom - Git Worktree Manager"));
1356
+ lines.push("");
1357
+ lines.push(
1358
+ this.dim(
1359
+ "A CLI tool for managing Git worktrees with enhanced functionality"
1360
+ )
1361
+ );
1362
+ lines.push("");
1363
+ lines.push(this.section("USAGE"));
1364
+ lines.push(`${this.indent}phantom <command> [options]`);
1365
+ lines.push("");
1366
+ lines.push(this.section("COMMANDS"));
1367
+ const maxNameLength = Math.max(...commands2.map((cmd) => cmd.name.length));
1368
+ for (const cmd of commands2) {
1369
+ const paddedName = cmd.name.padEnd(maxNameLength + 2);
1370
+ lines.push(`${this.indent}${this.cyan(paddedName)}${cmd.description}`);
1371
+ }
1372
+ lines.push("");
1373
+ lines.push(this.section("GLOBAL OPTIONS"));
1374
+ const helpOption = "-h, --help";
1375
+ const versionOption = "-v, --version";
1376
+ const globalOptionWidth = Math.max(helpOption.length, versionOption.length) + 2;
1377
+ lines.push(
1378
+ `${this.indent}${this.cyan(helpOption.padEnd(globalOptionWidth))}Show help`
1379
+ );
1380
+ lines.push(
1381
+ `${this.indent}${this.cyan(versionOption.padEnd(globalOptionWidth))}Show version`
1382
+ );
1383
+ lines.push("");
1384
+ lines.push(
1385
+ this.dim(
1386
+ "Run 'phantom <command> --help' for more information on a command."
1387
+ )
1388
+ );
1389
+ return lines.join("\n");
1390
+ }
1391
+ formatCommandHelp(help) {
1392
+ const lines = [];
1393
+ lines.push(this.bold(`phantom ${help.name}`));
1394
+ lines.push(this.dim(help.description));
1395
+ lines.push("");
1396
+ lines.push(this.section("USAGE"));
1397
+ lines.push(`${this.indent}${help.usage}`);
1398
+ lines.push("");
1399
+ if (help.options && help.options.length > 0) {
1400
+ lines.push(this.section("OPTIONS"));
1401
+ const maxOptionLength = Math.max(
1402
+ ...help.options.map((opt) => this.formatOptionName(opt).length)
1403
+ );
1404
+ for (const option of help.options) {
1405
+ const optionName = this.formatOptionName(option);
1406
+ const paddedName = optionName.padEnd(maxOptionLength + 2);
1407
+ const description = this.wrapText(
1408
+ option.description,
1409
+ maxOptionLength + 4
1410
+ );
1411
+ lines.push(`${this.indent}${this.cyan(paddedName)}${description[0]}`);
1412
+ for (let i = 1; i < description.length; i++) {
1413
+ lines.push(
1414
+ `${this.indent}${" ".repeat(maxOptionLength + 2)}${description[i]}`
1415
+ );
1416
+ }
1417
+ if (option.example) {
1418
+ const exampleIndent = " ".repeat(maxOptionLength + 4);
1419
+ lines.push(
1420
+ `${this.indent}${exampleIndent}${this.dim(`Example: ${option.example}`)}`
1421
+ );
1422
+ }
1423
+ }
1424
+ lines.push("");
1425
+ }
1426
+ if (help.examples && help.examples.length > 0) {
1427
+ lines.push(this.section("EXAMPLES"));
1428
+ for (const example of help.examples) {
1429
+ lines.push(`${this.indent}${this.dim(example.description)}`);
1430
+ lines.push(`${this.indent}${this.indent}$ ${example.command}`);
1431
+ lines.push("");
1432
+ }
1433
+ }
1434
+ if (help.notes && help.notes.length > 0) {
1435
+ lines.push(this.section("NOTES"));
1436
+ for (const note of help.notes) {
1437
+ const wrappedNote = this.wrapText(note, 2);
1438
+ for (const line of wrappedNote) {
1439
+ lines.push(`${this.indent}${line}`);
1440
+ }
1441
+ }
1442
+ lines.push("");
1443
+ }
1444
+ return lines.join("\n");
1445
+ }
1446
+ formatOptionName(option) {
1447
+ const parts = [];
1448
+ if (option.short) {
1449
+ parts.push(`-${option.short},`);
1450
+ }
1451
+ parts.push(`--${option.name}`);
1452
+ if (option.type === "string") {
1453
+ parts.push(option.multiple ? "<value>..." : "<value>");
1454
+ }
1455
+ return parts.join(" ");
1456
+ }
1457
+ wrapText(text, indent) {
1458
+ const maxWidth = this.width - indent - 2;
1459
+ const words = text.split(" ");
1460
+ const lines = [];
1461
+ let currentLine = "";
1462
+ for (const word of words) {
1463
+ if (currentLine.length + word.length + 1 > maxWidth) {
1464
+ lines.push(currentLine);
1465
+ currentLine = word;
1466
+ } else {
1467
+ currentLine = currentLine ? `${currentLine} ${word}` : word;
1468
+ }
1469
+ }
1470
+ if (currentLine) {
1471
+ lines.push(currentLine);
1472
+ }
1473
+ return lines;
1474
+ }
1475
+ section(text) {
1476
+ return this.bold(text);
1477
+ }
1478
+ bold(text) {
1479
+ return `\x1B[1m${text}\x1B[0m`;
1480
+ }
1481
+ dim(text) {
1482
+ return `\x1B[2m${text}\x1B[0m`;
1483
+ }
1484
+ cyan(text) {
1485
+ return `\x1B[36m${text}\x1B[0m`;
1486
+ }
1487
+ };
1488
+ var helpFormatter = new HelpFormatter();
1489
+
1490
+ // src/cli/help/attach.ts
1491
+ var attachHelp = {
1492
+ name: "attach",
1493
+ description: "Attach to an existing branch by creating a new worktree",
1494
+ usage: "phantom attach <worktree-name> <branch-name> [options]",
1495
+ options: [
1496
+ {
1497
+ name: "shell",
1498
+ short: "s",
1499
+ type: "boolean",
1500
+ description: "Open an interactive shell in the worktree after attaching"
1501
+ },
1502
+ {
1503
+ name: "exec",
1504
+ short: "x",
1505
+ type: "string",
1506
+ description: "Execute a command in the worktree after attaching",
1507
+ example: "--exec 'git pull'"
1508
+ }
1509
+ ],
1510
+ examples: [
1511
+ {
1512
+ description: "Attach to an existing branch",
1513
+ command: "phantom attach review-pr main"
1514
+ },
1515
+ {
1516
+ description: "Attach to a remote branch and open a shell",
1517
+ command: "phantom attach hotfix origin/hotfix-v1.2 --shell"
1518
+ },
1519
+ {
1520
+ description: "Attach to a branch and pull latest changes",
1521
+ command: "phantom attach staging origin/staging --exec 'git pull'"
1522
+ }
1523
+ ],
1524
+ notes: [
1525
+ "The branch must already exist (locally or remotely)",
1526
+ "If attaching to a remote branch, it will be checked out locally",
1527
+ "Only one of --shell or --exec options can be used at a time"
1528
+ ]
1529
+ };
1530
+
1531
+ // src/cli/help/create.ts
1532
+ var createHelp = {
1533
+ name: "create",
1534
+ description: "Create a new Git worktree (phantom)",
1535
+ usage: "phantom create <name> [options]",
1536
+ options: [
1537
+ {
1538
+ name: "shell",
1539
+ short: "s",
1540
+ type: "boolean",
1541
+ description: "Open an interactive shell in the new worktree after creation"
1542
+ },
1543
+ {
1544
+ name: "exec",
1545
+ short: "x",
1546
+ type: "string",
1547
+ description: "Execute a command in the new worktree after creation",
1548
+ example: "--exec 'npm install'"
1549
+ },
1550
+ {
1551
+ name: "tmux",
1552
+ short: "t",
1553
+ type: "boolean",
1554
+ description: "Open the worktree in a new tmux window (requires being inside tmux)"
1555
+ },
1556
+ {
1557
+ name: "tmux-vertical",
1558
+ type: "boolean",
1559
+ description: "Open the worktree in a vertical tmux pane (requires being inside tmux)"
1560
+ },
1561
+ {
1562
+ name: "tmux-horizontal",
1563
+ type: "boolean",
1564
+ description: "Open the worktree in a horizontal tmux pane (requires being inside tmux)"
1565
+ },
1566
+ {
1567
+ name: "copy-file",
1568
+ type: "string",
1569
+ multiple: true,
1570
+ description: "Copy specified files from the current worktree to the new one. Can be used multiple times",
1571
+ example: "--copy-file .env --copy-file config.local.json"
1572
+ }
1573
+ ],
1574
+ examples: [
1575
+ {
1576
+ description: "Create a new worktree named 'feature-auth'",
1577
+ command: "phantom create feature-auth"
1578
+ },
1579
+ {
1580
+ description: "Create a worktree and open a shell in it",
1581
+ command: "phantom create bugfix-123 --shell"
1582
+ },
1583
+ {
1584
+ description: "Create a worktree and run npm install",
1585
+ command: "phantom create new-feature --exec 'npm install'"
1586
+ },
1587
+ {
1588
+ description: "Create a worktree in a new tmux window",
1589
+ command: "phantom create experiment --tmux"
1590
+ },
1591
+ {
1592
+ description: "Create a worktree and copy environment files",
1593
+ command: "phantom create staging --copy-file .env --copy-file database.yml"
1594
+ }
1595
+ ],
1596
+ notes: [
1597
+ "The worktree name will be used as the branch name",
1598
+ "Only one of --shell, --exec, or --tmux options can be used at a time",
1599
+ "File copying can also be configured in phantom.config.json"
1600
+ ]
1601
+ };
1602
+
1603
+ // src/cli/help/delete.ts
1604
+ var deleteHelp = {
1605
+ name: "delete",
1606
+ description: "Delete a Git worktree (phantom)",
1607
+ usage: "phantom delete <name> [options]",
1608
+ options: [
1609
+ {
1610
+ name: "force",
1611
+ short: "f",
1612
+ type: "boolean",
1613
+ description: "Force deletion even if the worktree has uncommitted or unpushed changes"
1614
+ }
1615
+ ],
1616
+ examples: [
1617
+ {
1618
+ description: "Delete a worktree",
1619
+ command: "phantom delete feature-auth"
1620
+ },
1621
+ {
1622
+ description: "Force delete a worktree with uncommitted changes",
1623
+ command: "phantom delete experimental --force"
1624
+ }
1625
+ ],
1626
+ notes: [
1627
+ "By default, deletion will fail if the worktree has uncommitted changes",
1628
+ "The associated branch will also be deleted if it's not checked out elsewhere"
1629
+ ]
1630
+ };
1631
+
1632
+ // src/cli/help/exec.ts
1633
+ var execHelp = {
1634
+ name: "exec",
1635
+ description: "Execute a command in a worktree directory",
1636
+ usage: "phantom exec <worktree-name> <command> [args...]",
1637
+ examples: [
1638
+ {
1639
+ description: "Run npm test in a worktree",
1640
+ command: "phantom exec feature-auth npm test"
1641
+ },
1642
+ {
1643
+ description: "Check git status in a worktree",
1644
+ command: "phantom exec bugfix-123 git status"
1645
+ },
1646
+ {
1647
+ description: "Run a complex command with arguments",
1648
+ command: "phantom exec staging npm run build -- --production"
1649
+ }
1650
+ ],
1651
+ notes: [
1652
+ "The command is executed with the worktree directory as the working directory",
1653
+ "All arguments after the worktree name are passed to the command",
1654
+ "The exit code of the executed command is preserved"
1655
+ ]
1656
+ };
1657
+
1658
+ // src/cli/help/list.ts
1659
+ var listHelp = {
1660
+ name: "list",
1661
+ description: "List all Git worktrees (phantoms)",
1662
+ usage: "phantom list",
1663
+ examples: [
1664
+ {
1665
+ description: "List all worktrees",
1666
+ command: "phantom list"
1667
+ }
1668
+ ],
1669
+ notes: [
1670
+ "Shows all worktrees with their paths and associated branches",
1671
+ "The main worktree is marked as '(bare)' if using a bare repository"
1672
+ ]
1673
+ };
1674
+
1675
+ // src/cli/help/shell.ts
1676
+ var shellHelp = {
1677
+ name: "shell",
1678
+ description: "Open an interactive shell in a worktree directory",
1679
+ usage: "phantom shell <worktree-name>",
1680
+ examples: [
1681
+ {
1682
+ description: "Open a shell in a worktree",
1683
+ command: "phantom shell feature-auth"
1684
+ }
1685
+ ],
1686
+ notes: [
1687
+ "Uses your default shell from the SHELL environment variable",
1688
+ "The shell starts with the worktree directory as the working directory",
1689
+ "Type 'exit' to return to your original directory"
1690
+ ]
1691
+ };
1692
+
1693
+ // src/cli/help/version.ts
1694
+ var versionHelp = {
1695
+ name: "version",
1696
+ description: "Display phantom version information",
1697
+ usage: "phantom version",
1698
+ examples: [
1699
+ {
1700
+ description: "Show version",
1701
+ command: "phantom version"
1702
+ }
1703
+ ],
1704
+ notes: ["Also accessible via 'phantom --version' or 'phantom -v'"]
1705
+ };
1706
+
1707
+ // src/cli/help/where.ts
1708
+ var whereHelp = {
1709
+ name: "where",
1710
+ description: "Output the filesystem path of a specific worktree",
1711
+ usage: "phantom where <worktree-name>",
1712
+ examples: [
1713
+ {
1714
+ description: "Get the path of a worktree",
1715
+ command: "phantom where feature-auth"
1716
+ },
1717
+ {
1718
+ description: "Change directory to a worktree",
1719
+ command: "cd $(phantom where staging)"
1720
+ }
1721
+ ],
1722
+ notes: [
1723
+ "Outputs only the path, making it suitable for use in scripts",
1724
+ "Exits with an error code if the worktree doesn't exist"
1725
+ ]
1726
+ };
1727
+
988
1728
  // src/bin/phantom.ts
989
1729
  var commands = [
990
1730
  {
991
1731
  name: "create",
992
- description: "Create a new worktree [--shell | --exec <command>]",
993
- handler: createHandler
1732
+ description: "Create a new Git worktree (phantom)",
1733
+ handler: createHandler,
1734
+ help: createHelp
994
1735
  },
995
1736
  {
996
1737
  name: "attach",
997
- description: "Attach to an existing branch [--shell | --exec <command>]",
998
- handler: attachHandler
1738
+ description: "Attach to an existing branch by creating a new worktree",
1739
+ handler: attachHandler,
1740
+ help: attachHelp
999
1741
  },
1000
1742
  {
1001
1743
  name: "list",
1002
- description: "List all worktrees",
1003
- handler: listHandler
1744
+ description: "List all Git worktrees (phantoms)",
1745
+ handler: listHandler,
1746
+ help: listHelp
1004
1747
  },
1005
1748
  {
1006
1749
  name: "where",
1007
- description: "Output the path of a specific worktree",
1008
- handler: whereHandler
1750
+ description: "Output the filesystem path of a specific worktree",
1751
+ handler: whereHandler,
1752
+ help: whereHelp
1009
1753
  },
1010
1754
  {
1011
1755
  name: "delete",
1012
- description: "Delete a worktree (use --force for uncommitted changes)",
1013
- handler: deleteHandler
1756
+ description: "Delete a Git worktree (phantom)",
1757
+ handler: deleteHandler,
1758
+ help: deleteHelp
1014
1759
  },
1015
1760
  {
1016
1761
  name: "exec",
1017
1762
  description: "Execute a command in a worktree directory",
1018
- handler: execHandler
1763
+ handler: execHandler,
1764
+ help: execHelp
1019
1765
  },
1020
1766
  {
1021
1767
  name: "shell",
1022
- description: "Open interactive shell in a worktree directory",
1023
- handler: shellHandler
1768
+ description: "Open an interactive shell in a worktree directory",
1769
+ handler: shellHandler,
1770
+ help: shellHelp
1024
1771
  },
1025
1772
  {
1026
1773
  name: "version",
1027
- description: "Display phantom version",
1028
- handler: versionHandler
1774
+ description: "Display phantom version information",
1775
+ handler: versionHandler,
1776
+ help: versionHelp
1029
1777
  }
1030
1778
  ];
1031
1779
  function printHelp(commands2) {
1032
- console.log("Usage: phantom <command> [options]\n");
1033
- console.log("Commands:");
1034
- for (const cmd of commands2) {
1035
- console.log(` ${cmd.name.padEnd(12)} ${cmd.description}`);
1036
- }
1780
+ const simpleCommands = commands2.map((cmd) => ({
1781
+ name: cmd.name,
1782
+ description: cmd.description
1783
+ }));
1784
+ console.log(helpFormatter.formatMainHelp(simpleCommands));
1037
1785
  }
1038
1786
  function findCommand(args2, commands2) {
1039
1787
  if (args2.length === 0) {
@@ -1071,6 +1819,14 @@ if (!command || !command.handler) {
1071
1819
  printHelp(commands);
1072
1820
  exit(1);
1073
1821
  }
1822
+ if (remainingArgs.includes("--help") || remainingArgs.includes("-h")) {
1823
+ if (command.help) {
1824
+ console.log(helpFormatter.formatCommandHelp(command.help));
1825
+ } else {
1826
+ console.log(`Help not available for command '${command.name}'`);
1827
+ }
1828
+ exit(0);
1829
+ }
1074
1830
  try {
1075
1831
  await command.handler(remainingArgs);
1076
1832
  } catch (error) {