@aku11i/phantom 0.4.0 → 0.6.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
@@ -3,9 +3,12 @@
3
3
  // src/bin/phantom.ts
4
4
  import { argv, exit } from "node:process";
5
5
 
6
- // src/cli/handlers/create.ts
6
+ // src/cli/handlers/attach.ts
7
7
  import { parseArgs } from "node:util";
8
8
 
9
+ // src/core/git/libs/get-git-root.ts
10
+ import { dirname, resolve } from "node:path";
11
+
9
12
  // src/core/git/executor.ts
10
13
  import { execFile as execFileCallback } from "node:child_process";
11
14
  import { promisify } from "node:util";
@@ -41,8 +44,15 @@ async function executeGitCommandInDirectory(directory, args2) {
41
44
 
42
45
  // src/core/git/libs/get-git-root.ts
43
46
  async function getGitRoot() {
44
- const { stdout } = await executeGitCommand(["rev-parse", "--show-toplevel"]);
45
- return stdout;
47
+ const { stdout } = await executeGitCommand(["rev-parse", "--git-common-dir"]);
48
+ if (stdout.endsWith("/.git") || stdout === ".git") {
49
+ return resolve(process.cwd(), dirname(stdout));
50
+ }
51
+ const { stdout: toplevel } = await executeGitCommand([
52
+ "rev-parse",
53
+ "--show-toplevel"
54
+ ]);
55
+ return toplevel;
46
56
  }
47
57
 
48
58
  // src/core/types/result.ts
@@ -82,6 +92,12 @@ var GitOperationError = class extends WorktreeError {
82
92
  this.name = "GitOperationError";
83
93
  }
84
94
  };
95
+ var BranchNotFoundError = class extends WorktreeError {
96
+ constructor(branchName) {
97
+ super(`Branch '${branchName}' not found`);
98
+ this.name = "BranchNotFoundError";
99
+ }
100
+ };
85
101
 
86
102
  // src/core/worktree/validate.ts
87
103
  import fs from "node:fs/promises";
@@ -126,33 +142,17 @@ async function validateWorktreeDoesNotExist(gitRoot, name) {
126
142
  };
127
143
  }
128
144
  }
129
- async function validatePhantomDirectoryExists(gitRoot) {
130
- const phantomDir = getPhantomDirectory(gitRoot);
131
- try {
132
- await fs.access(phantomDir);
133
- return true;
134
- } catch {
135
- return false;
145
+ function validateWorktreeName(name) {
146
+ if (!name || name.trim() === "") {
147
+ return err(new Error("Phantom name cannot be empty"));
136
148
  }
137
- }
138
- async function listValidWorktrees(gitRoot) {
139
- const phantomDir = getPhantomDirectory(gitRoot);
140
- if (!await validatePhantomDirectoryExists(gitRoot)) {
141
- return [];
149
+ if (name.includes("/")) {
150
+ return err(new Error("Phantom name cannot contain slashes"));
142
151
  }
143
- try {
144
- const entries = await fs.readdir(phantomDir);
145
- const validWorktrees = [];
146
- for (const entry of entries) {
147
- const result = await validateWorktreeExists(gitRoot, entry);
148
- if (result.exists) {
149
- validWorktrees.push(entry);
150
- }
151
- }
152
- return validWorktrees;
153
- } catch {
154
- return [];
152
+ if (name.startsWith(".")) {
153
+ return err(new Error("Phantom name cannot start with a dot"));
155
154
  }
155
+ return ok(void 0);
156
156
  }
157
157
 
158
158
  // src/core/process/spawn.ts
@@ -191,24 +191,24 @@ var ProcessSpawnError = class extends ProcessError {
191
191
 
192
192
  // src/core/process/spawn.ts
193
193
  async function spawnProcess(config) {
194
- return new Promise((resolve) => {
194
+ return new Promise((resolve2) => {
195
195
  const { command: command2, args: args2 = [], options = {} } = config;
196
196
  const childProcess = nodeSpawn(command2, args2, {
197
197
  stdio: "inherit",
198
198
  ...options
199
199
  });
200
200
  childProcess.on("error", (error) => {
201
- resolve(err(new ProcessSpawnError(command2, error.message)));
201
+ resolve2(err(new ProcessSpawnError(command2, error.message)));
202
202
  });
203
203
  childProcess.on("exit", (code, signal) => {
204
204
  if (signal) {
205
- resolve(err(new ProcessSignalError(signal)));
205
+ resolve2(err(new ProcessSignalError(signal)));
206
206
  } else {
207
207
  const exitCode = code ?? 0;
208
208
  if (exitCode === 0) {
209
- resolve(ok({ exitCode }));
209
+ resolve2(ok({ exitCode }));
210
210
  } else {
211
- resolve(err(new ProcessExecutionError(command2, exitCode)));
211
+ resolve2(err(new ProcessExecutionError(command2, exitCode)));
212
212
  }
213
213
  }
214
214
  });
@@ -255,43 +255,68 @@ async function shellInWorktree(gitRoot, worktreeName) {
255
255
  });
256
256
  }
257
257
 
258
- // src/core/worktree/create.ts
259
- import fs2 from "node:fs/promises";
258
+ // src/core/worktree/attach.ts
259
+ import { existsSync } from "node:fs";
260
260
 
261
- // src/core/git/libs/add-worktree.ts
262
- async function addWorktree(options) {
263
- const { path, branch, commitish = "HEAD" } = options;
264
- await executeGitCommand(["worktree", "add", path, "-b", branch, commitish]);
261
+ // src/core/git/libs/attach-worktree.ts
262
+ async function attachWorktree(gitRoot, worktreePath, branchName) {
263
+ try {
264
+ await executeGitCommand(["worktree", "add", worktreePath, branchName], {
265
+ cwd: gitRoot
266
+ });
267
+ return ok(void 0);
268
+ } catch (error) {
269
+ return err(
270
+ error instanceof Error ? error : new Error(`Failed to attach worktree: ${String(error)}`)
271
+ );
272
+ }
265
273
  }
266
274
 
267
- // src/core/worktree/create.ts
268
- async function createWorktree(gitRoot, name, options = {}) {
269
- const { branch = name, commitish = "HEAD" } = options;
270
- const worktreesPath = getPhantomDirectory(gitRoot);
271
- const worktreePath = getWorktreePath(gitRoot, name);
275
+ // src/core/git/libs/branch-exists.ts
276
+ async function branchExists(gitRoot, branchName) {
272
277
  try {
273
- await fs2.access(worktreesPath);
274
- } catch {
275
- await fs2.mkdir(worktreesPath, { recursive: true });
278
+ await executeGitCommand(
279
+ ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
280
+ { cwd: gitRoot }
281
+ );
282
+ return ok(true);
283
+ } catch (error) {
284
+ if (error && typeof error === "object" && "code" in error) {
285
+ const execError = error;
286
+ if (execError.code === 1) {
287
+ return ok(false);
288
+ }
289
+ }
290
+ return err(
291
+ new Error(
292
+ `Failed to check branch existence: ${error instanceof Error ? error.message : String(error)}`
293
+ )
294
+ );
276
295
  }
277
- const validation = await validateWorktreeDoesNotExist(gitRoot, name);
278
- if (validation.exists) {
296
+ }
297
+
298
+ // src/core/worktree/attach.ts
299
+ async function attachWorktreeCore(gitRoot, name) {
300
+ const validation = validateWorktreeName(name);
301
+ if (isErr(validation)) {
302
+ return validation;
303
+ }
304
+ const worktreePath = getWorktreePath(gitRoot, name);
305
+ if (existsSync(worktreePath)) {
279
306
  return err(new WorktreeAlreadyExistsError(name));
280
307
  }
281
- try {
282
- await addWorktree({
283
- path: worktreePath,
284
- branch,
285
- commitish
286
- });
287
- return ok({
288
- message: `Created worktree '${name}' at ${worktreePath}`,
289
- path: worktreePath
290
- });
291
- } catch (error) {
292
- const errorMessage = error instanceof Error ? error.message : String(error);
293
- return err(new GitOperationError("worktree add", errorMessage));
308
+ const branchCheckResult = await branchExists(gitRoot, name);
309
+ if (isErr(branchCheckResult)) {
310
+ return err(branchCheckResult.error);
311
+ }
312
+ if (!branchCheckResult.value) {
313
+ return err(new BranchNotFoundError(name));
314
+ }
315
+ const attachResult = await attachWorktree(gitRoot, worktreePath, name);
316
+ if (isErr(attachResult)) {
317
+ return err(attachResult.error);
294
318
  }
319
+ return ok(worktreePath);
295
320
  }
296
321
 
297
322
  // src/cli/output.ts
@@ -302,6 +327,9 @@ var output = {
302
327
  error: (message) => {
303
328
  console.error(message);
304
329
  },
330
+ warn: (message) => {
331
+ console.warn(message);
332
+ },
305
333
  table: (data) => {
306
334
  console.table(data);
307
335
  },
@@ -326,9 +354,295 @@ function exitWithError(message, exitCode = exitCodes.generalError) {
326
354
  process.exit(exitCode);
327
355
  }
328
356
 
357
+ // src/cli/handlers/attach.ts
358
+ async function attachHandler(args2) {
359
+ const { positionals, values } = parseArgs({
360
+ args: args2,
361
+ strict: true,
362
+ allowPositionals: true,
363
+ options: {
364
+ shell: {
365
+ type: "boolean",
366
+ short: "s"
367
+ },
368
+ exec: {
369
+ type: "string",
370
+ short: "e"
371
+ }
372
+ }
373
+ });
374
+ if (positionals.length === 0) {
375
+ exitWithError(
376
+ "Missing required argument: branch name",
377
+ exitCodes.validationError
378
+ );
379
+ }
380
+ const [branchName] = positionals;
381
+ if (values.shell && values.exec) {
382
+ exitWithError(
383
+ "Cannot use both --shell and --exec options",
384
+ exitCodes.validationError
385
+ );
386
+ }
387
+ const gitRoot = await getGitRoot();
388
+ const result = await attachWorktreeCore(gitRoot, branchName);
389
+ if (isErr(result)) {
390
+ const error = result.error;
391
+ if (error instanceof WorktreeAlreadyExistsError) {
392
+ exitWithError(error.message, exitCodes.validationError);
393
+ }
394
+ if (error instanceof BranchNotFoundError) {
395
+ exitWithError(error.message, exitCodes.notFound);
396
+ }
397
+ exitWithError(error.message, exitCodes.generalError);
398
+ }
399
+ const worktreePath = result.value;
400
+ output.log(`Attached phantom: ${branchName}`);
401
+ if (values.shell) {
402
+ const shellResult = await shellInWorktree(gitRoot, branchName);
403
+ if (isErr(shellResult)) {
404
+ exitWithError(shellResult.error.message, exitCodes.generalError);
405
+ }
406
+ } else if (values.exec) {
407
+ const execResult = await execInWorktree(
408
+ gitRoot,
409
+ branchName,
410
+ values.exec.split(" ")
411
+ );
412
+ if (isErr(execResult)) {
413
+ exitWithError(execResult.error.message, exitCodes.generalError);
414
+ }
415
+ }
416
+ }
417
+
418
+ // src/cli/handlers/create.ts
419
+ import { parseArgs as parseArgs2 } from "node:util";
420
+
421
+ // src/core/config/loader.ts
422
+ import fs2 from "node:fs/promises";
423
+ import path from "node:path";
424
+
425
+ // src/core/utils/type-guards.ts
426
+ function isObject(value) {
427
+ return typeof value === "object" && value !== null && !Array.isArray(value);
428
+ }
429
+
430
+ // src/core/config/validate.ts
431
+ var ConfigValidationError = class extends Error {
432
+ constructor(message) {
433
+ super(`Invalid phantom.config.json: ${message}`);
434
+ this.name = "ConfigValidationError";
435
+ }
436
+ };
437
+ function validateConfig(config) {
438
+ if (!isObject(config)) {
439
+ return err(new ConfigValidationError("Configuration must be an object"));
440
+ }
441
+ const cfg = config;
442
+ if (cfg.postCreate !== void 0) {
443
+ if (!isObject(cfg.postCreate)) {
444
+ return err(new ConfigValidationError("postCreate must be an object"));
445
+ }
446
+ const postCreate = cfg.postCreate;
447
+ if (postCreate.copyFiles !== void 0) {
448
+ if (!Array.isArray(postCreate.copyFiles)) {
449
+ return err(
450
+ new ConfigValidationError("postCreate.copyFiles must be an array")
451
+ );
452
+ }
453
+ if (!postCreate.copyFiles.every((f) => typeof f === "string")) {
454
+ return err(
455
+ new ConfigValidationError(
456
+ "postCreate.copyFiles must contain only strings"
457
+ )
458
+ );
459
+ }
460
+ }
461
+ }
462
+ return ok(config);
463
+ }
464
+
465
+ // src/core/config/loader.ts
466
+ var ConfigNotFoundError = class extends Error {
467
+ constructor() {
468
+ super("phantom.config.json not found");
469
+ this.name = "ConfigNotFoundError";
470
+ }
471
+ };
472
+ var ConfigParseError = class extends Error {
473
+ constructor(message) {
474
+ super(`Failed to parse phantom.config.json: ${message}`);
475
+ this.name = "ConfigParseError";
476
+ }
477
+ };
478
+ async function loadConfig(gitRoot) {
479
+ const configPath = path.join(gitRoot, "phantom.config.json");
480
+ try {
481
+ const content = await fs2.readFile(configPath, "utf-8");
482
+ try {
483
+ const parsed = JSON.parse(content);
484
+ const validationResult = validateConfig(parsed);
485
+ if (!validationResult.ok) {
486
+ return err(validationResult.error);
487
+ }
488
+ return ok(validationResult.value);
489
+ } catch (error) {
490
+ return err(
491
+ new ConfigParseError(
492
+ error instanceof Error ? error.message : String(error)
493
+ )
494
+ );
495
+ }
496
+ } catch (error) {
497
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
498
+ return err(new ConfigNotFoundError());
499
+ }
500
+ throw error;
501
+ }
502
+ }
503
+
504
+ // src/core/process/tmux.ts
505
+ async function isInsideTmux() {
506
+ return process.env.TMUX !== void 0;
507
+ }
508
+ async function executeTmuxCommand(options) {
509
+ const { direction, command: command2, cwd, env } = options;
510
+ const tmuxArgs = [];
511
+ switch (direction) {
512
+ case "new":
513
+ tmuxArgs.push("new-window");
514
+ break;
515
+ case "vertical":
516
+ tmuxArgs.push("split-window", "-v");
517
+ break;
518
+ case "horizontal":
519
+ tmuxArgs.push("split-window", "-h");
520
+ break;
521
+ }
522
+ if (cwd) {
523
+ tmuxArgs.push("-c", cwd);
524
+ }
525
+ if (env) {
526
+ for (const [key, value] of Object.entries(env)) {
527
+ tmuxArgs.push("-e", `${key}=${value}`);
528
+ }
529
+ }
530
+ tmuxArgs.push(command2);
531
+ const result = await spawnProcess({
532
+ command: "tmux",
533
+ args: tmuxArgs
534
+ });
535
+ return result;
536
+ }
537
+
538
+ // src/core/worktree/create.ts
539
+ import fs3 from "node:fs/promises";
540
+
541
+ // src/core/git/libs/add-worktree.ts
542
+ async function addWorktree(options) {
543
+ const { path: path3, branch, commitish = "HEAD" } = options;
544
+ await executeGitCommand(["worktree", "add", path3, "-b", branch, commitish]);
545
+ }
546
+
547
+ // src/core/worktree/file-copier.ts
548
+ import { copyFile, mkdir, stat } from "node:fs/promises";
549
+ import path2 from "node:path";
550
+ var FileCopyError = class extends Error {
551
+ file;
552
+ constructor(file, message) {
553
+ super(`Failed to copy ${file}: ${message}`);
554
+ this.name = "FileCopyError";
555
+ this.file = file;
556
+ }
557
+ };
558
+ async function copyFiles(sourceDir, targetDir, files) {
559
+ const copiedFiles = [];
560
+ const skippedFiles = [];
561
+ for (const file of files) {
562
+ const sourcePath = path2.join(sourceDir, file);
563
+ const targetPath = path2.join(targetDir, file);
564
+ try {
565
+ const stats = await stat(sourcePath);
566
+ if (!stats.isFile()) {
567
+ skippedFiles.push(file);
568
+ continue;
569
+ }
570
+ const targetDirPath = path2.dirname(targetPath);
571
+ await mkdir(targetDirPath, { recursive: true });
572
+ await copyFile(sourcePath, targetPath);
573
+ copiedFiles.push(file);
574
+ } catch (error) {
575
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
576
+ skippedFiles.push(file);
577
+ } else {
578
+ return err(
579
+ new FileCopyError(
580
+ file,
581
+ error instanceof Error ? error.message : String(error)
582
+ )
583
+ );
584
+ }
585
+ }
586
+ }
587
+ return ok({ copiedFiles, skippedFiles });
588
+ }
589
+
590
+ // src/core/worktree/create.ts
591
+ async function createWorktree(gitRoot, name, options = {}) {
592
+ const nameValidation = validateWorktreeName(name);
593
+ if (isErr(nameValidation)) {
594
+ return nameValidation;
595
+ }
596
+ const { branch = name, commitish = "HEAD" } = options;
597
+ const worktreesPath = getPhantomDirectory(gitRoot);
598
+ const worktreePath = getWorktreePath(gitRoot, name);
599
+ try {
600
+ await fs3.access(worktreesPath);
601
+ } catch {
602
+ await fs3.mkdir(worktreesPath, { recursive: true });
603
+ }
604
+ const validation = await validateWorktreeDoesNotExist(gitRoot, name);
605
+ if (validation.exists) {
606
+ return err(new WorktreeAlreadyExistsError(name));
607
+ }
608
+ try {
609
+ await addWorktree({
610
+ path: worktreePath,
611
+ branch,
612
+ commitish
613
+ });
614
+ let copiedFiles;
615
+ let skippedFiles;
616
+ let copyError;
617
+ if (options.copyFiles && options.copyFiles.length > 0) {
618
+ const copyResult = await copyFiles(
619
+ gitRoot,
620
+ worktreePath,
621
+ options.copyFiles
622
+ );
623
+ if (isOk(copyResult)) {
624
+ copiedFiles = copyResult.value.copiedFiles;
625
+ skippedFiles = copyResult.value.skippedFiles;
626
+ } else {
627
+ copyError = copyResult.error.message;
628
+ }
629
+ }
630
+ return ok({
631
+ message: `Created worktree '${name}' at ${worktreePath}`,
632
+ path: worktreePath,
633
+ copiedFiles,
634
+ skippedFiles,
635
+ copyError
636
+ });
637
+ } catch (error) {
638
+ const errorMessage = error instanceof Error ? error.message : String(error);
639
+ return err(new GitOperationError("worktree add", errorMessage));
640
+ }
641
+ }
642
+
329
643
  // src/cli/handlers/create.ts
330
644
  async function createHandler(args2) {
331
- const { values, positionals } = parseArgs({
645
+ const { values, positionals } = parseArgs2({
332
646
  args: args2,
333
647
  options: {
334
648
  shell: {
@@ -338,6 +652,26 @@ async function createHandler(args2) {
338
652
  exec: {
339
653
  type: "string",
340
654
  short: "x"
655
+ },
656
+ tmux: {
657
+ type: "boolean",
658
+ short: "t"
659
+ },
660
+ "tmux-vertical": {
661
+ type: "boolean"
662
+ },
663
+ "tmux-v": {
664
+ type: "boolean"
665
+ },
666
+ "tmux-horizontal": {
667
+ type: "boolean"
668
+ },
669
+ "tmux-h": {
670
+ type: "boolean"
671
+ },
672
+ "copy-file": {
673
+ type: "string",
674
+ multiple: true
341
675
  }
342
676
  },
343
677
  strict: true,
@@ -352,20 +686,61 @@ async function createHandler(args2) {
352
686
  const worktreeName = positionals[0];
353
687
  const openShell = values.shell ?? false;
354
688
  const execCommand = values.exec;
355
- if (openShell && execCommand) {
689
+ const copyFileOptions = values["copy-file"];
690
+ const tmuxOption = values.tmux || values["tmux-vertical"] || values["tmux-v"] || values["tmux-horizontal"] || values["tmux-h"];
691
+ let tmuxDirection;
692
+ if (values.tmux) {
693
+ tmuxDirection = "new";
694
+ } else if (values["tmux-vertical"] || values["tmux-v"]) {
695
+ tmuxDirection = "vertical";
696
+ } else if (values["tmux-horizontal"] || values["tmux-h"]) {
697
+ tmuxDirection = "horizontal";
698
+ }
699
+ if ([openShell, execCommand !== void 0, tmuxOption].filter(Boolean).length > 1) {
700
+ exitWithError(
701
+ "Cannot use --shell, --exec, and --tmux options together",
702
+ exitCodes.validationError
703
+ );
704
+ }
705
+ if (tmuxOption && !await isInsideTmux()) {
356
706
  exitWithError(
357
- "Cannot use --shell and --exec together",
707
+ "The --tmux option can only be used inside a tmux session",
358
708
  exitCodes.validationError
359
709
  );
360
710
  }
361
711
  try {
362
712
  const gitRoot = await getGitRoot();
363
- const result = await createWorktree(gitRoot, worktreeName);
713
+ let filesToCopy = [];
714
+ const configResult = await loadConfig(gitRoot);
715
+ if (isOk(configResult)) {
716
+ if (configResult.value.postCreate?.copyFiles) {
717
+ filesToCopy = [...configResult.value.postCreate.copyFiles];
718
+ }
719
+ } else {
720
+ if (configResult.error instanceof ConfigValidationError) {
721
+ output.warn(`Configuration warning: ${configResult.error.message}`);
722
+ } else if (configResult.error instanceof ConfigParseError) {
723
+ output.warn(`Configuration warning: ${configResult.error.message}`);
724
+ }
725
+ }
726
+ if (copyFileOptions && copyFileOptions.length > 0) {
727
+ const cliFiles = Array.isArray(copyFileOptions) ? copyFileOptions : [copyFileOptions];
728
+ filesToCopy = [.../* @__PURE__ */ new Set([...filesToCopy, ...cliFiles])];
729
+ }
730
+ const result = await createWorktree(gitRoot, worktreeName, {
731
+ copyFiles: filesToCopy.length > 0 ? filesToCopy : void 0
732
+ });
364
733
  if (isErr(result)) {
365
734
  const exitCode = result.error instanceof WorktreeAlreadyExistsError ? exitCodes.validationError : exitCodes.generalError;
366
735
  exitWithError(result.error.message, exitCode);
367
736
  }
368
737
  output.log(result.value.message);
738
+ if (result.value.copyError) {
739
+ output.error(
740
+ `
741
+ Warning: Failed to copy some files: ${result.value.copyError}`
742
+ );
743
+ }
369
744
  if (execCommand && isOk(result)) {
370
745
  output.log(
371
746
  `
@@ -398,6 +773,28 @@ Entering worktree '${worktreeName}' at ${result.value.path}`
398
773
  }
399
774
  process.exit(shellResult.value.exitCode ?? 0);
400
775
  }
776
+ if (tmuxDirection && isOk(result)) {
777
+ output.log(
778
+ `
779
+ Opening worktree '${worktreeName}' in tmux ${tmuxDirection === "new" ? "window" : "pane"}...`
780
+ );
781
+ const shell = process.env.SHELL || "/bin/sh";
782
+ const tmuxResult = await executeTmuxCommand({
783
+ direction: tmuxDirection,
784
+ command: shell,
785
+ cwd: result.value.path,
786
+ env: {
787
+ PHANTOM: "1",
788
+ PHANTOM_NAME: worktreeName,
789
+ PHANTOM_PATH: result.value.path
790
+ }
791
+ });
792
+ if (isErr(tmuxResult)) {
793
+ output.error(tmuxResult.error.message);
794
+ const exitCode = "exitCode" in tmuxResult.error ? tmuxResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
795
+ exitWithError("", exitCode);
796
+ }
797
+ }
401
798
  exitWithSuccess();
402
799
  } catch (error) {
403
800
  exitWithError(
@@ -408,7 +805,67 @@ Entering worktree '${worktreeName}' at ${result.value.path}`
408
805
  }
409
806
 
410
807
  // src/cli/handlers/delete.ts
411
- import { parseArgs as parseArgs2 } from "node:util";
808
+ import { parseArgs as parseArgs3 } from "node:util";
809
+
810
+ // src/core/git/libs/list-worktrees.ts
811
+ async function listWorktrees(gitRoot) {
812
+ const { stdout } = await executeGitCommand([
813
+ "worktree",
814
+ "list",
815
+ "--porcelain"
816
+ ]);
817
+ const worktrees = [];
818
+ let currentWorktree = {};
819
+ const lines = stdout.split("\n").filter((line) => line.length > 0);
820
+ for (const line of lines) {
821
+ if (line.startsWith("worktree ")) {
822
+ if (currentWorktree.path) {
823
+ worktrees.push(currentWorktree);
824
+ }
825
+ currentWorktree = {
826
+ path: line.substring("worktree ".length),
827
+ isLocked: false,
828
+ isPrunable: false
829
+ };
830
+ } else if (line.startsWith("HEAD ")) {
831
+ currentWorktree.head = line.substring("HEAD ".length);
832
+ } else if (line.startsWith("branch ")) {
833
+ const fullBranch = line.substring("branch ".length);
834
+ currentWorktree.branch = fullBranch.startsWith("refs/heads/") ? fullBranch.substring("refs/heads/".length) : fullBranch;
835
+ } else if (line === "detached") {
836
+ currentWorktree.branch = "(detached HEAD)";
837
+ } else if (line === "locked") {
838
+ currentWorktree.isLocked = true;
839
+ } else if (line === "prunable") {
840
+ currentWorktree.isPrunable = true;
841
+ }
842
+ }
843
+ if (currentWorktree.path) {
844
+ worktrees.push(currentWorktree);
845
+ }
846
+ return worktrees;
847
+ }
848
+
849
+ // src/core/git/libs/get-current-worktree.ts
850
+ async function getCurrentWorktree(gitRoot) {
851
+ try {
852
+ const { stdout: currentPath } = await executeGitCommand([
853
+ "rev-parse",
854
+ "--show-toplevel"
855
+ ]);
856
+ const currentPathTrimmed = currentPath.trim();
857
+ const worktrees = await listWorktrees(gitRoot);
858
+ const currentWorktree = worktrees.find(
859
+ (wt) => wt.path === currentPathTrimmed
860
+ );
861
+ if (!currentWorktree || currentWorktree.path === gitRoot) {
862
+ return null;
863
+ }
864
+ return currentWorktree.branch;
865
+ } catch {
866
+ return null;
867
+ }
868
+ }
412
869
 
413
870
  // src/core/worktree/delete.ts
414
871
  async function getWorktreeStatus(worktreePath) {
@@ -498,27 +955,49 @@ ${message}`;
498
955
 
499
956
  // src/cli/handlers/delete.ts
500
957
  async function deleteHandler(args2) {
501
- const { values, positionals } = parseArgs2({
958
+ const { values, positionals } = parseArgs3({
502
959
  args: args2,
503
960
  options: {
504
961
  force: {
505
962
  type: "boolean",
506
963
  short: "f"
964
+ },
965
+ current: {
966
+ type: "boolean"
507
967
  }
508
968
  },
509
969
  strict: true,
510
970
  allowPositionals: true
511
971
  });
512
- if (positionals.length === 0) {
972
+ const deleteCurrent = values.current ?? false;
973
+ if (positionals.length === 0 && !deleteCurrent) {
513
974
  exitWithError(
514
- "Please provide a worktree name to delete",
975
+ "Please provide a worktree name to delete or use --current to delete the current worktree",
976
+ exitCodes.validationError
977
+ );
978
+ }
979
+ if (positionals.length > 0 && deleteCurrent) {
980
+ exitWithError(
981
+ "Cannot specify both a worktree name and --current option",
515
982
  exitCodes.validationError
516
983
  );
517
984
  }
518
- const worktreeName = positionals[0];
519
985
  const forceDelete = values.force ?? false;
520
986
  try {
521
987
  const gitRoot = await getGitRoot();
988
+ let worktreeName;
989
+ if (deleteCurrent) {
990
+ const currentWorktree = await getCurrentWorktree(gitRoot);
991
+ if (!currentWorktree) {
992
+ exitWithError(
993
+ "Not in a worktree directory. The --current option can only be used from within a worktree.",
994
+ exitCodes.validationError
995
+ );
996
+ }
997
+ worktreeName = currentWorktree;
998
+ } else {
999
+ worktreeName = positionals[0];
1000
+ }
522
1001
  const result = await deleteWorktree(gitRoot, worktreeName, {
523
1002
  force: forceDelete
524
1003
  });
@@ -537,9 +1016,9 @@ async function deleteHandler(args2) {
537
1016
  }
538
1017
 
539
1018
  // src/cli/handlers/exec.ts
540
- import { parseArgs as parseArgs3 } from "node:util";
1019
+ import { parseArgs as parseArgs4 } from "node:util";
541
1020
  async function execHandler(args2) {
542
- const { positionals } = parseArgs3({
1021
+ const { positionals } = parseArgs4({
543
1022
  args: args2,
544
1023
  options: {},
545
1024
  strict: true,
@@ -569,20 +1048,9 @@ async function execHandler(args2) {
569
1048
  }
570
1049
 
571
1050
  // src/cli/handlers/list.ts
572
- import { parseArgs as parseArgs4 } from "node:util";
1051
+ import { parseArgs as parseArgs5 } from "node:util";
573
1052
 
574
1053
  // src/core/worktree/list.ts
575
- async function getWorktreeBranch(worktreePath) {
576
- try {
577
- const { stdout } = await executeGitCommandInDirectory(worktreePath, [
578
- "branch",
579
- "--show-current"
580
- ]);
581
- return stdout || "(detached HEAD)";
582
- } catch {
583
- return "unknown";
584
- }
585
- }
586
1054
  async function getWorktreeStatus2(worktreePath) {
587
1055
  try {
588
1056
  const { stdout } = await executeGitCommandInDirectory(worktreePath, [
@@ -594,36 +1062,30 @@ async function getWorktreeStatus2(worktreePath) {
594
1062
  return true;
595
1063
  }
596
1064
  }
597
- async function getWorktreeInfo(gitRoot, name) {
598
- const worktreePath = getWorktreePath(gitRoot, name);
599
- const [branch, isClean] = await Promise.all([
600
- getWorktreeBranch(worktreePath),
601
- getWorktreeStatus2(worktreePath)
602
- ]);
603
- return {
604
- name,
605
- path: worktreePath,
606
- branch,
607
- isClean
608
- };
609
- }
610
- async function listWorktrees(gitRoot) {
611
- if (!await validatePhantomDirectoryExists(gitRoot)) {
612
- return ok({
613
- worktrees: [],
614
- message: "No worktrees found (worktrees directory doesn't exist)"
615
- });
616
- }
617
- const worktreeNames = await listValidWorktrees(gitRoot);
618
- if (worktreeNames.length === 0) {
619
- return ok({
620
- worktrees: [],
621
- message: "No worktrees found"
622
- });
623
- }
1065
+ async function listWorktrees2(gitRoot) {
624
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
+ }
625
1078
  const worktrees = await Promise.all(
626
- worktreeNames.map((name) => getWorktreeInfo(gitRoot, name))
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
+ })
627
1089
  );
628
1090
  return ok({
629
1091
  worktrees
@@ -636,7 +1098,7 @@ async function listWorktrees(gitRoot) {
636
1098
 
637
1099
  // src/cli/handlers/list.ts
638
1100
  async function listHandler(args2 = []) {
639
- parseArgs4({
1101
+ parseArgs5({
640
1102
  args: args2,
641
1103
  options: {},
642
1104
  strict: true,
@@ -644,7 +1106,7 @@ async function listHandler(args2 = []) {
644
1106
  });
645
1107
  try {
646
1108
  const gitRoot = await getGitRoot();
647
- const result = await listWorktrees(gitRoot);
1109
+ const result = await listWorktrees2(gitRoot);
648
1110
  if (isErr(result)) {
649
1111
  exitWithError("Failed to list worktrees", exitCodes.generalError);
650
1112
  }
@@ -670,9 +1132,9 @@ async function listHandler(args2 = []) {
670
1132
  }
671
1133
 
672
1134
  // src/cli/handlers/shell.ts
673
- import { parseArgs as parseArgs5 } from "node:util";
1135
+ import { parseArgs as parseArgs6 } from "node:util";
674
1136
  async function shellHandler(args2) {
675
- const { positionals } = parseArgs5({
1137
+ const { positionals } = parseArgs6({
676
1138
  args: args2,
677
1139
  options: {},
678
1140
  strict: true,
@@ -711,13 +1173,13 @@ async function shellHandler(args2) {
711
1173
  }
712
1174
 
713
1175
  // src/cli/handlers/version.ts
714
- import { parseArgs as parseArgs6 } from "node:util";
1176
+ import { parseArgs as parseArgs7 } from "node:util";
715
1177
 
716
1178
  // package.json
717
1179
  var package_default = {
718
1180
  name: "@aku11i/phantom",
719
1181
  packageManager: "pnpm@10.8.1",
720
- version: "0.4.0",
1182
+ version: "0.6.0",
721
1183
  description: "A powerful CLI tool for managing Git worktrees for parallel development",
722
1184
  keywords: [
723
1185
  "git",
@@ -746,12 +1208,14 @@ var package_default = {
746
1208
  start: "node ./src/bin/phantom.ts",
747
1209
  phantom: "node ./src/bin/phantom.ts",
748
1210
  build: "node build.ts",
749
- "type-check": "tsgo --noEmit",
750
- test: "node --test --experimental-strip-types --experimental-test-module-mocks src/**/*.test.ts",
1211
+ typecheck: "tsgo --noEmit",
1212
+ test: 'node --test --experimental-strip-types --experimental-test-module-mocks "src/**/*.test.js"',
1213
+ "test:coverage": 'node --experimental-test-coverage --test --experimental-strip-types --experimental-test-module-mocks "src/**/*.test.js"',
1214
+ "test:file": "node --test --experimental-strip-types --experimental-test-module-mocks",
751
1215
  lint: "biome check .",
752
1216
  fix: "biome check --write .",
753
- ready: "pnpm fix && pnpm type-check && pnpm test",
754
- "ready:check": "pnpm lint && pnpm type-check && pnpm test",
1217
+ ready: "pnpm fix && pnpm typecheck && pnpm test",
1218
+ "ready:check": "pnpm lint && pnpm typecheck && pnpm test",
755
1219
  prepublishOnly: "pnpm ready:check && pnpm build"
756
1220
  },
757
1221
  engines: {
@@ -778,7 +1242,7 @@ function getVersion() {
778
1242
 
779
1243
  // src/cli/handlers/version.ts
780
1244
  function versionHandler(args2 = []) {
781
- parseArgs6({
1245
+ parseArgs7({
782
1246
  args: args2,
783
1247
  options: {},
784
1248
  strict: true,
@@ -790,7 +1254,7 @@ function versionHandler(args2 = []) {
790
1254
  }
791
1255
 
792
1256
  // src/cli/handlers/where.ts
793
- import { parseArgs as parseArgs7 } from "node:util";
1257
+ import { parseArgs as parseArgs8 } from "node:util";
794
1258
 
795
1259
  // src/core/worktree/where.ts
796
1260
  async function whereWorktree(gitRoot, name) {
@@ -805,7 +1269,7 @@ async function whereWorktree(gitRoot, name) {
805
1269
 
806
1270
  // src/cli/handlers/where.ts
807
1271
  async function whereHandler(args2) {
808
- const { positionals } = parseArgs7({
1272
+ const { positionals } = parseArgs8({
809
1273
  args: args2,
810
1274
  options: {},
811
1275
  strict: true,
@@ -835,9 +1299,14 @@ async function whereHandler(args2) {
835
1299
  var commands = [
836
1300
  {
837
1301
  name: "create",
838
- description: "Create a new worktree [--shell | --exec <command>]",
1302
+ description: "Create a new worktree [--shell | --exec <command> | --tmux | --tmux-vertical | --tmux-horizontal] [--copy-file <file>]...",
839
1303
  handler: createHandler
840
1304
  },
1305
+ {
1306
+ name: "attach",
1307
+ description: "Attach to an existing branch [--shell | --exec <command>]",
1308
+ handler: attachHandler
1309
+ },
841
1310
  {
842
1311
  name: "list",
843
1312
  description: "List all worktrees",