@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/README.ja.md +91 -2
- package/README.md +94 -2
- package/dist/phantom.js +846 -90
- package/dist/phantom.js.map +4 -4
- package/package.json +4 -2
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 (
|
|
49
|
-
return resolve(process.cwd(), dirname(
|
|
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/
|
|
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",
|
|
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
|
|
617
|
+
await fs3.access(worktreesPath);
|
|
438
618
|
} catch {
|
|
439
|
-
await
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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(
|
|
508
|
-
|
|
509
|
-
|
|
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 (
|
|
919
|
+
if (stdout2) {
|
|
553
920
|
return {
|
|
554
921
|
hasUncommittedChanges: true,
|
|
555
|
-
changedFiles:
|
|
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
|
-
|
|
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(
|
|
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 !
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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) {
|