@aku11i/phantom 0.6.0 → 0.8.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
  }
@@ -407,7 +409,8 @@ async function attachHandler(args2) {
407
409
  const execResult = await execInWorktree(
408
410
  gitRoot,
409
411
  branchName,
410
- values.exec.split(" ")
412
+ values.exec.split(" "),
413
+ { interactive: true }
411
414
  );
412
415
  if (isErr(execResult)) {
413
416
  exitWithError(execResult.error.message, exitCodes.generalError);
@@ -458,6 +461,20 @@ function validateConfig(config) {
458
461
  );
459
462
  }
460
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
+ }
461
478
  }
462
479
  return ok(config);
463
480
  }
@@ -741,17 +758,42 @@ async function createHandler(args2) {
741
758
  Warning: Failed to copy some files: ${result.value.copyError}`
742
759
  );
743
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
+ }
744
785
  if (execCommand && isOk(result)) {
745
786
  output.log(
746
787
  `
747
788
  Executing command in worktree '${worktreeName}': ${execCommand}`
748
789
  );
749
790
  const shell = process.env.SHELL || "/bin/sh";
750
- const execResult = await execInWorktree(gitRoot, worktreeName, [
751
- shell,
752
- "-c",
753
- execCommand
754
- ]);
791
+ const execResult = await execInWorktree(
792
+ gitRoot,
793
+ worktreeName,
794
+ [shell, "-c", execCommand],
795
+ { interactive: true }
796
+ );
755
797
  if (isErr(execResult)) {
756
798
  output.error(execResult.error.message);
757
799
  const exitCode = "exitCode" in execResult.error ? execResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
@@ -809,14 +851,14 @@ import { parseArgs as parseArgs3 } from "node:util";
809
851
 
810
852
  // src/core/git/libs/list-worktrees.ts
811
853
  async function listWorktrees(gitRoot) {
812
- const { stdout } = await executeGitCommand([
854
+ const { stdout: stdout2 } = await executeGitCommand([
813
855
  "worktree",
814
856
  "list",
815
857
  "--porcelain"
816
858
  ]);
817
859
  const worktrees = [];
818
860
  let currentWorktree = {};
819
- const lines = stdout.split("\n").filter((line) => line.length > 0);
861
+ const lines = stdout2.split("\n").filter((line) => line.length > 0);
820
862
  for (const line of lines) {
821
863
  if (line.startsWith("worktree ")) {
822
864
  if (currentWorktree.path) {
@@ -870,14 +912,14 @@ async function getCurrentWorktree(gitRoot) {
870
912
  // src/core/worktree/delete.ts
871
913
  async function getWorktreeStatus(worktreePath) {
872
914
  try {
873
- const { stdout } = await executeGitCommandInDirectory(worktreePath, [
915
+ const { stdout: stdout2 } = await executeGitCommandInDirectory(worktreePath, [
874
916
  "status",
875
917
  "--porcelain"
876
918
  ]);
877
- if (stdout) {
919
+ if (stdout2) {
878
920
  return {
879
921
  hasUncommittedChanges: true,
880
- changedFiles: stdout.split("\n").length
922
+ changedFiles: stdout2.split("\n").length
881
923
  };
882
924
  }
883
925
  } catch {
@@ -953,6 +995,154 @@ ${message}`;
953
995
  }
954
996
  }
955
997
 
998
+ // src/core/utils/fzf.ts
999
+ import { spawn } from "node:child_process";
1000
+ async function selectWithFzf(items, options = {}) {
1001
+ return new Promise((resolve2) => {
1002
+ const args2 = [];
1003
+ if (options.prompt) {
1004
+ args2.push("--prompt", options.prompt);
1005
+ }
1006
+ if (options.header) {
1007
+ args2.push("--header", options.header);
1008
+ }
1009
+ if (options.previewCommand) {
1010
+ args2.push("--preview", options.previewCommand);
1011
+ }
1012
+ const fzf = spawn("fzf", args2, {
1013
+ stdio: ["pipe", "pipe", "pipe"]
1014
+ });
1015
+ let result = "";
1016
+ let errorOutput = "";
1017
+ fzf.stdout.on("data", (data) => {
1018
+ result += data.toString();
1019
+ });
1020
+ if (fzf.stderr) {
1021
+ fzf.stderr.on("data", (data) => {
1022
+ errorOutput += data.toString();
1023
+ });
1024
+ }
1025
+ fzf.on("error", (error) => {
1026
+ if (error.message.includes("ENOENT")) {
1027
+ resolve2(
1028
+ err(new Error("fzf command not found. Please install fzf first."))
1029
+ );
1030
+ } else {
1031
+ resolve2(err(error));
1032
+ }
1033
+ });
1034
+ fzf.on("close", (code) => {
1035
+ if (code === 0) {
1036
+ const selected = result.trim();
1037
+ resolve2(ok(selected || null));
1038
+ } else if (code === 1) {
1039
+ resolve2(ok(null));
1040
+ } else if (code === 130) {
1041
+ resolve2(ok(null));
1042
+ } else {
1043
+ resolve2(err(new Error(`fzf exited with code ${code}: ${errorOutput}`)));
1044
+ }
1045
+ });
1046
+ fzf.stdin.write(items.join("\n"));
1047
+ fzf.stdin.end();
1048
+ });
1049
+ }
1050
+
1051
+ // src/core/worktree/list.ts
1052
+ async function getWorktreeStatus2(worktreePath) {
1053
+ try {
1054
+ const { stdout: stdout2 } = await executeGitCommandInDirectory(worktreePath, [
1055
+ "status",
1056
+ "--porcelain"
1057
+ ]);
1058
+ return !stdout2;
1059
+ } catch {
1060
+ return true;
1061
+ }
1062
+ }
1063
+ async function listWorktrees2(gitRoot) {
1064
+ try {
1065
+ const gitWorktrees = await listWorktrees(gitRoot);
1066
+ const phantomDir = getPhantomDirectory(gitRoot);
1067
+ const phantomWorktrees = gitWorktrees.filter(
1068
+ (worktree) => worktree.path.startsWith(phantomDir)
1069
+ );
1070
+ if (phantomWorktrees.length === 0) {
1071
+ return ok({
1072
+ worktrees: [],
1073
+ message: "No worktrees found"
1074
+ });
1075
+ }
1076
+ const worktrees = await Promise.all(
1077
+ phantomWorktrees.map(async (gitWorktree) => {
1078
+ const name = gitWorktree.path.substring(phantomDir.length + 1);
1079
+ const isClean = await getWorktreeStatus2(gitWorktree.path);
1080
+ return {
1081
+ name,
1082
+ path: gitWorktree.path,
1083
+ branch: gitWorktree.branch || "(detached HEAD)",
1084
+ isClean
1085
+ };
1086
+ })
1087
+ );
1088
+ return ok({
1089
+ worktrees
1090
+ });
1091
+ } catch (error) {
1092
+ const errorMessage = error instanceof Error ? error.message : String(error);
1093
+ throw new Error(`Failed to list worktrees: ${errorMessage}`);
1094
+ }
1095
+ }
1096
+
1097
+ // src/core/worktree/select.ts
1098
+ async function selectWorktreeWithFzf(gitRoot) {
1099
+ const listResult = await listWorktrees2(gitRoot);
1100
+ if (isErr(listResult)) {
1101
+ return listResult;
1102
+ }
1103
+ const { worktrees } = listResult.value;
1104
+ if (worktrees.length === 0) {
1105
+ return {
1106
+ ok: true,
1107
+ value: null
1108
+ };
1109
+ }
1110
+ const list = worktrees.map((wt) => {
1111
+ const branchInfo = wt.branch ? `(${wt.branch})` : "";
1112
+ const status = !wt.isClean ? " [dirty]" : "";
1113
+ return `${wt.name} ${branchInfo}${status}`;
1114
+ });
1115
+ const fzfResult = await selectWithFzf(list, {
1116
+ prompt: "Select worktree> ",
1117
+ header: "Git Worktrees (Phantoms)"
1118
+ });
1119
+ if (isErr(fzfResult)) {
1120
+ return fzfResult;
1121
+ }
1122
+ if (!fzfResult.value) {
1123
+ return {
1124
+ ok: true,
1125
+ value: null
1126
+ };
1127
+ }
1128
+ const selectedName = fzfResult.value.split(" ")[0];
1129
+ const selectedWorktree = worktrees.find((wt) => wt.name === selectedName);
1130
+ if (!selectedWorktree) {
1131
+ return {
1132
+ ok: false,
1133
+ error: new Error("Selected worktree not found")
1134
+ };
1135
+ }
1136
+ return {
1137
+ ok: true,
1138
+ value: {
1139
+ name: selectedWorktree.name,
1140
+ branch: selectedWorktree.branch,
1141
+ isClean: selectedWorktree.isClean
1142
+ }
1143
+ };
1144
+ }
1145
+
956
1146
  // src/cli/handlers/delete.ts
957
1147
  async function deleteHandler(args2) {
958
1148
  const { values, positionals } = parseArgs3({
@@ -964,21 +1154,32 @@ async function deleteHandler(args2) {
964
1154
  },
965
1155
  current: {
966
1156
  type: "boolean"
1157
+ },
1158
+ fzf: {
1159
+ type: "boolean",
1160
+ default: false
967
1161
  }
968
1162
  },
969
1163
  strict: true,
970
1164
  allowPositionals: true
971
1165
  });
972
1166
  const deleteCurrent = values.current ?? false;
973
- if (positionals.length === 0 && !deleteCurrent) {
1167
+ const useFzf = values.fzf ?? false;
1168
+ if (positionals.length === 0 && !deleteCurrent && !useFzf) {
974
1169
  exitWithError(
975
- "Please provide a worktree name to delete or use --current to delete the current worktree",
1170
+ "Please provide a worktree name to delete, use --current to delete the current worktree, or use --fzf for interactive selection",
976
1171
  exitCodes.validationError
977
1172
  );
978
1173
  }
979
- if (positionals.length > 0 && deleteCurrent) {
1174
+ if ((positionals.length > 0 || useFzf) && deleteCurrent) {
980
1175
  exitWithError(
981
- "Cannot specify both a worktree name and --current option",
1176
+ "Cannot specify --current with a worktree name or --fzf option",
1177
+ exitCodes.validationError
1178
+ );
1179
+ }
1180
+ if (positionals.length > 0 && useFzf) {
1181
+ exitWithError(
1182
+ "Cannot specify both a worktree name and --fzf option",
982
1183
  exitCodes.validationError
983
1184
  );
984
1185
  }
@@ -995,6 +1196,15 @@ async function deleteHandler(args2) {
995
1196
  );
996
1197
  }
997
1198
  worktreeName = currentWorktree;
1199
+ } else if (useFzf) {
1200
+ const selectResult = await selectWorktreeWithFzf(gitRoot);
1201
+ if (isErr(selectResult)) {
1202
+ exitWithError(selectResult.error.message, exitCodes.generalError);
1203
+ }
1204
+ if (!selectResult.value) {
1205
+ exitWithSuccess();
1206
+ }
1207
+ worktreeName = selectResult.value.name;
998
1208
  } else {
999
1209
  worktreeName = positionals[0];
1000
1210
  }
@@ -1033,7 +1243,12 @@ async function execHandler(args2) {
1033
1243
  const [worktreeName, ...commandArgs] = positionals;
1034
1244
  try {
1035
1245
  const gitRoot = await getGitRoot();
1036
- const result = await execInWorktree(gitRoot, worktreeName, commandArgs);
1246
+ const result = await execInWorktree(
1247
+ gitRoot,
1248
+ worktreeName,
1249
+ commandArgs,
1250
+ { interactive: true }
1251
+ );
1037
1252
  if (isErr(result)) {
1038
1253
  const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.notFound : result.error.exitCode || exitCodes.generalError;
1039
1254
  exitWithError(result.error.message, exitCode);
@@ -1049,78 +1264,45 @@ async function execHandler(args2) {
1049
1264
 
1050
1265
  // src/cli/handlers/list.ts
1051
1266
  import { parseArgs as parseArgs5 } from "node:util";
1052
-
1053
- // src/core/worktree/list.ts
1054
- async function getWorktreeStatus2(worktreePath) {
1055
- try {
1056
- const { stdout } = await executeGitCommandInDirectory(worktreePath, [
1057
- "status",
1058
- "--porcelain"
1059
- ]);
1060
- return !stdout;
1061
- } catch {
1062
- return true;
1063
- }
1064
- }
1065
- async function listWorktrees2(gitRoot) {
1066
- try {
1067
- const gitWorktrees = await listWorktrees(gitRoot);
1068
- const phantomDir = getPhantomDirectory(gitRoot);
1069
- const phantomWorktrees = gitWorktrees.filter(
1070
- (worktree) => worktree.path.startsWith(phantomDir)
1071
- );
1072
- if (phantomWorktrees.length === 0) {
1073
- return ok({
1074
- worktrees: [],
1075
- message: "No worktrees found"
1076
- });
1077
- }
1078
- const worktrees = await Promise.all(
1079
- phantomWorktrees.map(async (gitWorktree) => {
1080
- const name = gitWorktree.path.substring(phantomDir.length + 1);
1081
- const isClean = await getWorktreeStatus2(gitWorktree.path);
1082
- return {
1083
- name,
1084
- path: gitWorktree.path,
1085
- branch: gitWorktree.branch || "(detached HEAD)",
1086
- isClean
1087
- };
1088
- })
1089
- );
1090
- return ok({
1091
- worktrees
1092
- });
1093
- } catch (error) {
1094
- const errorMessage = error instanceof Error ? error.message : String(error);
1095
- throw new Error(`Failed to list worktrees: ${errorMessage}`);
1096
- }
1097
- }
1098
-
1099
- // src/cli/handlers/list.ts
1100
1267
  async function listHandler(args2 = []) {
1101
- parseArgs5({
1268
+ const { values } = parseArgs5({
1102
1269
  args: args2,
1103
- options: {},
1270
+ options: {
1271
+ fzf: {
1272
+ type: "boolean",
1273
+ default: false
1274
+ }
1275
+ },
1104
1276
  strict: true,
1105
1277
  allowPositionals: false
1106
1278
  });
1107
1279
  try {
1108
1280
  const gitRoot = await getGitRoot();
1109
- const result = await listWorktrees2(gitRoot);
1110
- if (isErr(result)) {
1111
- exitWithError("Failed to list worktrees", exitCodes.generalError);
1112
- }
1113
- const { worktrees, message } = result.value;
1114
- if (worktrees.length === 0) {
1115
- output.log(message || "No worktrees found.");
1116
- process.exit(exitCodes.success);
1117
- }
1118
- const maxNameLength = Math.max(...worktrees.map((wt) => wt.name.length));
1119
- for (const worktree of worktrees) {
1120
- const paddedName = worktree.name.padEnd(maxNameLength + 2);
1121
- const branchInfo = worktree.branch ? `(${worktree.branch})` : "";
1122
- const status = !worktree.isClean ? " [dirty]" : "";
1123
- output.log(`${paddedName} ${branchInfo}${status}`);
1281
+ if (values.fzf) {
1282
+ const selectResult = await selectWorktreeWithFzf(gitRoot);
1283
+ if (isErr(selectResult)) {
1284
+ exitWithError(selectResult.error.message, exitCodes.generalError);
1285
+ }
1286
+ if (selectResult.value) {
1287
+ output.log(selectResult.value.name);
1288
+ }
1289
+ } else {
1290
+ const result = await listWorktrees2(gitRoot);
1291
+ if (isErr(result)) {
1292
+ exitWithError("Failed to list worktrees", exitCodes.generalError);
1293
+ }
1294
+ const { worktrees, message } = result.value;
1295
+ if (worktrees.length === 0) {
1296
+ output.log(message || "No worktrees found.");
1297
+ process.exit(exitCodes.success);
1298
+ }
1299
+ const maxNameLength = Math.max(...worktrees.map((wt) => wt.name.length));
1300
+ for (const worktree of worktrees) {
1301
+ const paddedName = worktree.name.padEnd(maxNameLength + 2);
1302
+ const branchInfo = worktree.branch ? `(${worktree.branch})` : "";
1303
+ const status = !worktree.isClean ? " [dirty]" : "";
1304
+ output.log(`${paddedName} ${branchInfo}${status}`);
1305
+ }
1124
1306
  }
1125
1307
  process.exit(exitCodes.success);
1126
1308
  } catch (error) {
@@ -1134,21 +1316,45 @@ async function listHandler(args2 = []) {
1134
1316
  // src/cli/handlers/shell.ts
1135
1317
  import { parseArgs as parseArgs6 } from "node:util";
1136
1318
  async function shellHandler(args2) {
1137
- const { positionals } = parseArgs6({
1319
+ const { positionals, values } = parseArgs6({
1138
1320
  args: args2,
1139
- options: {},
1321
+ options: {
1322
+ fzf: {
1323
+ type: "boolean",
1324
+ default: false
1325
+ }
1326
+ },
1140
1327
  strict: true,
1141
1328
  allowPositionals: true
1142
1329
  });
1143
- if (positionals.length === 0) {
1330
+ const useFzf = values.fzf ?? false;
1331
+ if (positionals.length === 0 && !useFzf) {
1144
1332
  exitWithError(
1145
- "Usage: phantom shell <worktree-name>",
1333
+ "Usage: phantom shell <worktree-name> or phantom shell --fzf",
1146
1334
  exitCodes.validationError
1147
1335
  );
1148
1336
  }
1149
- const worktreeName = positionals[0];
1337
+ if (positionals.length > 0 && useFzf) {
1338
+ exitWithError(
1339
+ "Cannot specify both a worktree name and --fzf option",
1340
+ exitCodes.validationError
1341
+ );
1342
+ }
1343
+ let worktreeName;
1150
1344
  try {
1151
1345
  const gitRoot = await getGitRoot();
1346
+ if (useFzf) {
1347
+ const selectResult = await selectWorktreeWithFzf(gitRoot);
1348
+ if (isErr(selectResult)) {
1349
+ exitWithError(selectResult.error.message, exitCodes.generalError);
1350
+ }
1351
+ if (!selectResult.value) {
1352
+ exitWithSuccess();
1353
+ }
1354
+ worktreeName = selectResult.value.name;
1355
+ } else {
1356
+ worktreeName = positionals[0];
1357
+ }
1152
1358
  const validation = await validateWorktreeExists(gitRoot, worktreeName);
1153
1359
  if (!validation.exists) {
1154
1360
  exitWithError(
@@ -1179,7 +1385,7 @@ import { parseArgs as parseArgs7 } from "node:util";
1179
1385
  var package_default = {
1180
1386
  name: "@aku11i/phantom",
1181
1387
  packageManager: "pnpm@10.8.1",
1182
- version: "0.6.0",
1388
+ version: "0.8.0",
1183
1389
  description: "A powerful CLI tool for managing Git worktrees for parallel development",
1184
1390
  keywords: [
1185
1391
  "git",
@@ -1269,81 +1475,559 @@ async function whereWorktree(gitRoot, name) {
1269
1475
 
1270
1476
  // src/cli/handlers/where.ts
1271
1477
  async function whereHandler(args2) {
1272
- const { positionals } = parseArgs8({
1478
+ const { positionals, values } = parseArgs8({
1273
1479
  args: args2,
1274
- options: {},
1480
+ options: {
1481
+ fzf: {
1482
+ type: "boolean",
1483
+ default: false
1484
+ }
1485
+ },
1275
1486
  strict: true,
1276
1487
  allowPositionals: true
1277
1488
  });
1278
- if (positionals.length === 0) {
1279
- exitWithError("Please provide a worktree name", exitCodes.validationError);
1489
+ const useFzf = values.fzf ?? false;
1490
+ if (positionals.length === 0 && !useFzf) {
1491
+ exitWithError(
1492
+ "Usage: phantom where <worktree-name> or phantom where --fzf",
1493
+ exitCodes.validationError
1494
+ );
1280
1495
  }
1281
- const worktreeName = positionals[0];
1496
+ if (positionals.length > 0 && useFzf) {
1497
+ exitWithError(
1498
+ "Cannot specify both a worktree name and --fzf option",
1499
+ exitCodes.validationError
1500
+ );
1501
+ }
1502
+ let worktreeName;
1503
+ let gitRoot;
1282
1504
  try {
1283
- const gitRoot = await getGitRoot();
1284
- const result = await whereWorktree(gitRoot, worktreeName);
1285
- if (isErr(result)) {
1286
- exitWithError(result.error.message, exitCodes.notFound);
1287
- }
1288
- output.log(result.value.path);
1289
- exitWithSuccess();
1505
+ gitRoot = await getGitRoot();
1290
1506
  } catch (error) {
1291
1507
  exitWithError(
1292
1508
  error instanceof Error ? error.message : String(error),
1293
1509
  exitCodes.generalError
1294
1510
  );
1295
1511
  }
1512
+ if (useFzf) {
1513
+ const selectResult = await selectWorktreeWithFzf(gitRoot);
1514
+ if (isErr(selectResult)) {
1515
+ exitWithError(selectResult.error.message, exitCodes.generalError);
1516
+ }
1517
+ if (!selectResult.value) {
1518
+ exitWithSuccess();
1519
+ }
1520
+ worktreeName = selectResult.value.name;
1521
+ } else {
1522
+ worktreeName = positionals[0];
1523
+ }
1524
+ const result = await whereWorktree(gitRoot, worktreeName);
1525
+ if (isErr(result)) {
1526
+ exitWithError(result.error.message, exitCodes.notFound);
1527
+ }
1528
+ output.log(result.value.path);
1529
+ exitWithSuccess();
1296
1530
  }
1297
1531
 
1532
+ // src/cli/help.ts
1533
+ import { stdout } from "node:process";
1534
+ var HelpFormatter = class {
1535
+ width;
1536
+ indent = " ";
1537
+ constructor() {
1538
+ this.width = stdout.columns || 80;
1539
+ }
1540
+ formatMainHelp(commands2) {
1541
+ const lines = [];
1542
+ lines.push(this.bold("Phantom - Git Worktree Manager"));
1543
+ lines.push("");
1544
+ lines.push(
1545
+ this.dim(
1546
+ "A CLI tool for managing Git worktrees with enhanced functionality"
1547
+ )
1548
+ );
1549
+ lines.push("");
1550
+ lines.push(this.section("USAGE"));
1551
+ lines.push(`${this.indent}phantom <command> [options]`);
1552
+ lines.push("");
1553
+ lines.push(this.section("COMMANDS"));
1554
+ const maxNameLength = Math.max(...commands2.map((cmd) => cmd.name.length));
1555
+ for (const cmd of commands2) {
1556
+ const paddedName = cmd.name.padEnd(maxNameLength + 2);
1557
+ lines.push(`${this.indent}${this.cyan(paddedName)}${cmd.description}`);
1558
+ }
1559
+ lines.push("");
1560
+ lines.push(this.section("GLOBAL OPTIONS"));
1561
+ const helpOption = "-h, --help";
1562
+ const versionOption = "-v, --version";
1563
+ const globalOptionWidth = Math.max(helpOption.length, versionOption.length) + 2;
1564
+ lines.push(
1565
+ `${this.indent}${this.cyan(helpOption.padEnd(globalOptionWidth))}Show help`
1566
+ );
1567
+ lines.push(
1568
+ `${this.indent}${this.cyan(versionOption.padEnd(globalOptionWidth))}Show version`
1569
+ );
1570
+ lines.push("");
1571
+ lines.push(
1572
+ this.dim(
1573
+ "Run 'phantom <command> --help' for more information on a command."
1574
+ )
1575
+ );
1576
+ return lines.join("\n");
1577
+ }
1578
+ formatCommandHelp(help) {
1579
+ const lines = [];
1580
+ lines.push(this.bold(`phantom ${help.name}`));
1581
+ lines.push(this.dim(help.description));
1582
+ lines.push("");
1583
+ lines.push(this.section("USAGE"));
1584
+ lines.push(`${this.indent}${help.usage}`);
1585
+ lines.push("");
1586
+ if (help.options && help.options.length > 0) {
1587
+ lines.push(this.section("OPTIONS"));
1588
+ const maxOptionLength = Math.max(
1589
+ ...help.options.map((opt) => this.formatOptionName(opt).length)
1590
+ );
1591
+ for (const option of help.options) {
1592
+ const optionName = this.formatOptionName(option);
1593
+ const paddedName = optionName.padEnd(maxOptionLength + 2);
1594
+ const description = this.wrapText(
1595
+ option.description,
1596
+ maxOptionLength + 4
1597
+ );
1598
+ lines.push(`${this.indent}${this.cyan(paddedName)}${description[0]}`);
1599
+ for (let i = 1; i < description.length; i++) {
1600
+ lines.push(
1601
+ `${this.indent}${" ".repeat(maxOptionLength + 2)}${description[i]}`
1602
+ );
1603
+ }
1604
+ if (option.example) {
1605
+ const exampleIndent = " ".repeat(maxOptionLength + 4);
1606
+ lines.push(
1607
+ `${this.indent}${exampleIndent}${this.dim(`Example: ${option.example}`)}`
1608
+ );
1609
+ }
1610
+ }
1611
+ lines.push("");
1612
+ }
1613
+ if (help.examples && help.examples.length > 0) {
1614
+ lines.push(this.section("EXAMPLES"));
1615
+ for (const example of help.examples) {
1616
+ lines.push(`${this.indent}${this.dim(example.description)}`);
1617
+ lines.push(`${this.indent}${this.indent}$ ${example.command}`);
1618
+ lines.push("");
1619
+ }
1620
+ }
1621
+ if (help.notes && help.notes.length > 0) {
1622
+ lines.push(this.section("NOTES"));
1623
+ for (const note of help.notes) {
1624
+ const wrappedNote = this.wrapText(note, 2);
1625
+ for (const line of wrappedNote) {
1626
+ lines.push(`${this.indent}${line}`);
1627
+ }
1628
+ }
1629
+ lines.push("");
1630
+ }
1631
+ return lines.join("\n");
1632
+ }
1633
+ formatOptionName(option) {
1634
+ const parts = [];
1635
+ if (option.short) {
1636
+ parts.push(`-${option.short},`);
1637
+ }
1638
+ parts.push(`--${option.name}`);
1639
+ if (option.type === "string") {
1640
+ parts.push(option.multiple ? "<value>..." : "<value>");
1641
+ }
1642
+ return parts.join(" ");
1643
+ }
1644
+ wrapText(text, indent) {
1645
+ const maxWidth = this.width - indent - 2;
1646
+ const words = text.split(" ");
1647
+ const lines = [];
1648
+ let currentLine = "";
1649
+ for (const word of words) {
1650
+ if (currentLine.length + word.length + 1 > maxWidth) {
1651
+ lines.push(currentLine);
1652
+ currentLine = word;
1653
+ } else {
1654
+ currentLine = currentLine ? `${currentLine} ${word}` : word;
1655
+ }
1656
+ }
1657
+ if (currentLine) {
1658
+ lines.push(currentLine);
1659
+ }
1660
+ return lines;
1661
+ }
1662
+ section(text) {
1663
+ return this.bold(text);
1664
+ }
1665
+ bold(text) {
1666
+ return `\x1B[1m${text}\x1B[0m`;
1667
+ }
1668
+ dim(text) {
1669
+ return `\x1B[2m${text}\x1B[0m`;
1670
+ }
1671
+ cyan(text) {
1672
+ return `\x1B[36m${text}\x1B[0m`;
1673
+ }
1674
+ };
1675
+ var helpFormatter = new HelpFormatter();
1676
+
1677
+ // src/cli/help/attach.ts
1678
+ var attachHelp = {
1679
+ name: "attach",
1680
+ description: "Attach to an existing branch by creating a new worktree",
1681
+ usage: "phantom attach <worktree-name> <branch-name> [options]",
1682
+ options: [
1683
+ {
1684
+ name: "shell",
1685
+ short: "s",
1686
+ type: "boolean",
1687
+ description: "Open an interactive shell in the worktree after attaching"
1688
+ },
1689
+ {
1690
+ name: "exec",
1691
+ short: "x",
1692
+ type: "string",
1693
+ description: "Execute a command in the worktree after attaching",
1694
+ example: "--exec 'git pull'"
1695
+ }
1696
+ ],
1697
+ examples: [
1698
+ {
1699
+ description: "Attach to an existing branch",
1700
+ command: "phantom attach review-pr main"
1701
+ },
1702
+ {
1703
+ description: "Attach to a remote branch and open a shell",
1704
+ command: "phantom attach hotfix origin/hotfix-v1.2 --shell"
1705
+ },
1706
+ {
1707
+ description: "Attach to a branch and pull latest changes",
1708
+ command: "phantom attach staging origin/staging --exec 'git pull'"
1709
+ }
1710
+ ],
1711
+ notes: [
1712
+ "The branch must already exist (locally or remotely)",
1713
+ "If attaching to a remote branch, it will be checked out locally",
1714
+ "Only one of --shell or --exec options can be used at a time"
1715
+ ]
1716
+ };
1717
+
1718
+ // src/cli/help/create.ts
1719
+ var createHelp = {
1720
+ name: "create",
1721
+ description: "Create a new Git worktree (phantom)",
1722
+ usage: "phantom create <name> [options]",
1723
+ options: [
1724
+ {
1725
+ name: "shell",
1726
+ short: "s",
1727
+ type: "boolean",
1728
+ description: "Open an interactive shell in the new worktree after creation"
1729
+ },
1730
+ {
1731
+ name: "exec",
1732
+ short: "x",
1733
+ type: "string",
1734
+ description: "Execute a command in the new worktree after creation",
1735
+ example: "--exec 'npm install'"
1736
+ },
1737
+ {
1738
+ name: "tmux",
1739
+ short: "t",
1740
+ type: "boolean",
1741
+ description: "Open the worktree in a new tmux window (requires being inside tmux)"
1742
+ },
1743
+ {
1744
+ name: "tmux-vertical",
1745
+ type: "boolean",
1746
+ description: "Open the worktree in a vertical tmux pane (requires being inside tmux)"
1747
+ },
1748
+ {
1749
+ name: "tmux-horizontal",
1750
+ type: "boolean",
1751
+ description: "Open the worktree in a horizontal tmux pane (requires being inside tmux)"
1752
+ },
1753
+ {
1754
+ name: "copy-file",
1755
+ type: "string",
1756
+ multiple: true,
1757
+ description: "Copy specified files from the current worktree to the new one. Can be used multiple times",
1758
+ example: "--copy-file .env --copy-file config.local.json"
1759
+ }
1760
+ ],
1761
+ examples: [
1762
+ {
1763
+ description: "Create a new worktree named 'feature-auth'",
1764
+ command: "phantom create feature-auth"
1765
+ },
1766
+ {
1767
+ description: "Create a worktree and open a shell in it",
1768
+ command: "phantom create bugfix-123 --shell"
1769
+ },
1770
+ {
1771
+ description: "Create a worktree and run npm install",
1772
+ command: "phantom create new-feature --exec 'npm install'"
1773
+ },
1774
+ {
1775
+ description: "Create a worktree in a new tmux window",
1776
+ command: "phantom create experiment --tmux"
1777
+ },
1778
+ {
1779
+ description: "Create a worktree and copy environment files",
1780
+ command: "phantom create staging --copy-file .env --copy-file database.yml"
1781
+ }
1782
+ ],
1783
+ notes: [
1784
+ "The worktree name will be used as the branch name",
1785
+ "Only one of --shell, --exec, or --tmux options can be used at a time",
1786
+ "File copying can also be configured in phantom.config.json"
1787
+ ]
1788
+ };
1789
+
1790
+ // src/cli/help/delete.ts
1791
+ var deleteHelp = {
1792
+ name: "delete",
1793
+ description: "Delete a Git worktree (phantom)",
1794
+ usage: "phantom delete <name> [options]",
1795
+ options: [
1796
+ {
1797
+ name: "force",
1798
+ short: "f",
1799
+ type: "boolean",
1800
+ description: "Force deletion even if the worktree has uncommitted or unpushed changes"
1801
+ },
1802
+ {
1803
+ name: "--current",
1804
+ type: "boolean",
1805
+ description: "Delete the current worktree"
1806
+ },
1807
+ {
1808
+ name: "--fzf",
1809
+ type: "boolean",
1810
+ description: "Use fzf for interactive selection"
1811
+ }
1812
+ ],
1813
+ examples: [
1814
+ {
1815
+ description: "Delete a worktree",
1816
+ command: "phantom delete feature-auth"
1817
+ },
1818
+ {
1819
+ description: "Force delete a worktree with uncommitted changes",
1820
+ command: "phantom delete experimental --force"
1821
+ },
1822
+ {
1823
+ description: "Delete the current worktree",
1824
+ command: "phantom delete --current"
1825
+ },
1826
+ {
1827
+ description: "Delete a worktree with interactive fzf selection",
1828
+ command: "phantom delete --fzf"
1829
+ }
1830
+ ],
1831
+ notes: [
1832
+ "By default, deletion will fail if the worktree has uncommitted changes",
1833
+ "The associated branch will also be deleted if it's not checked out elsewhere",
1834
+ "With --fzf, you can interactively select the worktree to delete"
1835
+ ]
1836
+ };
1837
+
1838
+ // src/cli/help/exec.ts
1839
+ var execHelp = {
1840
+ name: "exec",
1841
+ description: "Execute a command in a worktree directory",
1842
+ usage: "phantom exec <worktree-name> <command> [args...]",
1843
+ examples: [
1844
+ {
1845
+ description: "Run npm test in a worktree",
1846
+ command: "phantom exec feature-auth npm test"
1847
+ },
1848
+ {
1849
+ description: "Check git status in a worktree",
1850
+ command: "phantom exec bugfix-123 git status"
1851
+ },
1852
+ {
1853
+ description: "Run a complex command with arguments",
1854
+ command: "phantom exec staging npm run build -- --production"
1855
+ }
1856
+ ],
1857
+ notes: [
1858
+ "The command is executed with the worktree directory as the working directory",
1859
+ "All arguments after the worktree name are passed to the command",
1860
+ "The exit code of the executed command is preserved"
1861
+ ]
1862
+ };
1863
+
1864
+ // src/cli/help/list.ts
1865
+ var listHelp = {
1866
+ name: "list",
1867
+ description: "List all Git worktrees (phantoms)",
1868
+ usage: "phantom list [options]",
1869
+ options: [
1870
+ {
1871
+ name: "--fzf",
1872
+ type: "boolean",
1873
+ description: "Use fzf for interactive selection"
1874
+ }
1875
+ ],
1876
+ examples: [
1877
+ {
1878
+ description: "List all worktrees",
1879
+ command: "phantom list"
1880
+ },
1881
+ {
1882
+ description: "List worktrees with interactive fzf selection",
1883
+ command: "phantom list --fzf"
1884
+ }
1885
+ ],
1886
+ notes: [
1887
+ "Shows all worktrees with their paths and associated branches",
1888
+ "The main worktree is marked as '(bare)' if using a bare repository",
1889
+ "With --fzf, outputs only the selected worktree name"
1890
+ ]
1891
+ };
1892
+
1893
+ // src/cli/help/shell.ts
1894
+ var shellHelp = {
1895
+ name: "shell",
1896
+ description: "Open an interactive shell in a worktree directory",
1897
+ usage: "phantom shell <worktree-name> [options]",
1898
+ options: [
1899
+ {
1900
+ name: "--fzf",
1901
+ type: "boolean",
1902
+ description: "Use fzf for interactive selection"
1903
+ }
1904
+ ],
1905
+ examples: [
1906
+ {
1907
+ description: "Open a shell in a worktree",
1908
+ command: "phantom shell feature-auth"
1909
+ },
1910
+ {
1911
+ description: "Open a shell with interactive fzf selection",
1912
+ command: "phantom shell --fzf"
1913
+ }
1914
+ ],
1915
+ notes: [
1916
+ "Uses your default shell from the SHELL environment variable",
1917
+ "The shell starts with the worktree directory as the working directory",
1918
+ "Type 'exit' to return to your original directory",
1919
+ "With --fzf, you can interactively select the worktree to enter"
1920
+ ]
1921
+ };
1922
+
1923
+ // src/cli/help/version.ts
1924
+ var versionHelp = {
1925
+ name: "version",
1926
+ description: "Display phantom version information",
1927
+ usage: "phantom version",
1928
+ examples: [
1929
+ {
1930
+ description: "Show version",
1931
+ command: "phantom version"
1932
+ }
1933
+ ],
1934
+ notes: ["Also accessible via 'phantom --version' or 'phantom -v'"]
1935
+ };
1936
+
1937
+ // src/cli/help/where.ts
1938
+ var whereHelp = {
1939
+ name: "where",
1940
+ description: "Output the filesystem path of a specific worktree",
1941
+ usage: "phantom where <worktree-name> [options]",
1942
+ options: [
1943
+ {
1944
+ name: "--fzf",
1945
+ type: "boolean",
1946
+ description: "Use fzf for interactive selection"
1947
+ }
1948
+ ],
1949
+ examples: [
1950
+ {
1951
+ description: "Get the path of a worktree",
1952
+ command: "phantom where feature-auth"
1953
+ },
1954
+ {
1955
+ description: "Change directory to a worktree",
1956
+ command: "cd $(phantom where staging)"
1957
+ },
1958
+ {
1959
+ description: "Get path with interactive fzf selection",
1960
+ command: "phantom where --fzf"
1961
+ },
1962
+ {
1963
+ description: "Change directory using fzf selection",
1964
+ command: "cd $(phantom where --fzf)"
1965
+ }
1966
+ ],
1967
+ notes: [
1968
+ "Outputs only the path, making it suitable for use in scripts",
1969
+ "Exits with an error code if the worktree doesn't exist",
1970
+ "With --fzf, you can interactively select the worktree"
1971
+ ]
1972
+ };
1973
+
1298
1974
  // src/bin/phantom.ts
1299
1975
  var commands = [
1300
1976
  {
1301
1977
  name: "create",
1302
- description: "Create a new worktree [--shell | --exec <command> | --tmux | --tmux-vertical | --tmux-horizontal] [--copy-file <file>]...",
1303
- handler: createHandler
1978
+ description: "Create a new Git worktree (phantom)",
1979
+ handler: createHandler,
1980
+ help: createHelp
1304
1981
  },
1305
1982
  {
1306
1983
  name: "attach",
1307
- description: "Attach to an existing branch [--shell | --exec <command>]",
1308
- handler: attachHandler
1984
+ description: "Attach to an existing branch by creating a new worktree",
1985
+ handler: attachHandler,
1986
+ help: attachHelp
1309
1987
  },
1310
1988
  {
1311
1989
  name: "list",
1312
- description: "List all worktrees",
1313
- handler: listHandler
1990
+ description: "List all Git worktrees (phantoms)",
1991
+ handler: listHandler,
1992
+ help: listHelp
1314
1993
  },
1315
1994
  {
1316
1995
  name: "where",
1317
- description: "Output the path of a specific worktree",
1318
- handler: whereHandler
1996
+ description: "Output the filesystem path of a specific worktree",
1997
+ handler: whereHandler,
1998
+ help: whereHelp
1319
1999
  },
1320
2000
  {
1321
2001
  name: "delete",
1322
- description: "Delete a worktree (use --force for uncommitted changes)",
1323
- handler: deleteHandler
2002
+ description: "Delete a Git worktree (phantom)",
2003
+ handler: deleteHandler,
2004
+ help: deleteHelp
1324
2005
  },
1325
2006
  {
1326
2007
  name: "exec",
1327
2008
  description: "Execute a command in a worktree directory",
1328
- handler: execHandler
2009
+ handler: execHandler,
2010
+ help: execHelp
1329
2011
  },
1330
2012
  {
1331
2013
  name: "shell",
1332
- description: "Open interactive shell in a worktree directory",
1333
- handler: shellHandler
2014
+ description: "Open an interactive shell in a worktree directory",
2015
+ handler: shellHandler,
2016
+ help: shellHelp
1334
2017
  },
1335
2018
  {
1336
2019
  name: "version",
1337
- description: "Display phantom version",
1338
- handler: versionHandler
2020
+ description: "Display phantom version information",
2021
+ handler: versionHandler,
2022
+ help: versionHelp
1339
2023
  }
1340
2024
  ];
1341
2025
  function printHelp(commands2) {
1342
- console.log("Usage: phantom <command> [options]\n");
1343
- console.log("Commands:");
1344
- for (const cmd of commands2) {
1345
- console.log(` ${cmd.name.padEnd(12)} ${cmd.description}`);
1346
- }
2026
+ const simpleCommands = commands2.map((cmd) => ({
2027
+ name: cmd.name,
2028
+ description: cmd.description
2029
+ }));
2030
+ console.log(helpFormatter.formatMainHelp(simpleCommands));
1347
2031
  }
1348
2032
  function findCommand(args2, commands2) {
1349
2033
  if (args2.length === 0) {
@@ -1381,6 +2065,14 @@ if (!command || !command.handler) {
1381
2065
  printHelp(commands);
1382
2066
  exit(1);
1383
2067
  }
2068
+ if (remainingArgs.includes("--help") || remainingArgs.includes("-h")) {
2069
+ if (command.help) {
2070
+ console.log(helpFormatter.formatCommandHelp(command.help));
2071
+ } else {
2072
+ console.log(`Help not available for command '${command.name}'`);
2073
+ }
2074
+ exit(0);
2075
+ }
1384
2076
  try {
1385
2077
  await command.handler(remainingArgs);
1386
2078
  } catch (error) {