@aku11i/phantom 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/phantom.js CHANGED
@@ -1,493 +1,723 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin/phantom.ts
4
- import { argv, exit as exit6 } from "node:process";
4
+ import { argv, exit } from "node:process";
5
5
 
6
- // src/commands/create.ts
7
- import { access as access2, mkdir } from "node:fs/promises";
8
- import { join as join2 } from "node:path";
9
- import { exit as exit3 } from "node:process";
6
+ // src/cli/handlers/create.ts
7
+ import { parseArgs } from "node:util";
10
8
 
11
- // src/git/libs/add-worktree.ts
12
- import { exec } from "node:child_process";
9
+ // src/core/git/executor.ts
10
+ import { execFile as execFileCallback } from "node:child_process";
13
11
  import { promisify } from "node:util";
14
- var execAsync = promisify(exec);
15
- async function addWorktree(options) {
16
- const { path, branch, commitish = "HEAD" } = options;
17
- await execAsync(`git worktree add "${path}" -b "${branch}" ${commitish}`);
12
+ var execFile = promisify(execFileCallback);
13
+ async function executeGitCommand(args2, options = {}) {
14
+ try {
15
+ const result = await execFile("git", args2, {
16
+ cwd: options.cwd,
17
+ env: options.env || process.env,
18
+ encoding: "utf8"
19
+ });
20
+ return {
21
+ stdout: result.stdout.trim(),
22
+ stderr: result.stderr.trim()
23
+ };
24
+ } catch (error) {
25
+ if (error && typeof error === "object" && "stdout" in error && "stderr" in error) {
26
+ const execError = error;
27
+ if (execError.stderr?.trim()) {
28
+ throw new Error(execError.stderr.trim());
29
+ }
30
+ return {
31
+ stdout: execError.stdout?.trim() || "",
32
+ stderr: execError.stderr?.trim() || ""
33
+ };
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+ async function executeGitCommandInDirectory(directory, args2) {
39
+ return executeGitCommand(["-C", directory, ...args2], {});
18
40
  }
19
41
 
20
- // src/git/libs/get-git-root.ts
21
- import { exec as exec2 } from "node:child_process";
22
- import { promisify as promisify2 } from "node:util";
23
- var execAsync2 = promisify2(exec2);
42
+ // src/core/git/libs/get-git-root.ts
24
43
  async function getGitRoot() {
25
- const { stdout } = await execAsync2("git rev-parse --show-toplevel");
26
- return stdout.trim();
44
+ const { stdout } = await executeGitCommand(["rev-parse", "--show-toplevel"]);
45
+ return stdout;
27
46
  }
28
47
 
29
- // src/commands/shell.ts
30
- import { spawn } from "node:child_process";
31
- import { exit as exit2 } from "node:process";
48
+ // src/core/types/result.ts
49
+ var ok = (value) => ({
50
+ ok: true,
51
+ value
52
+ });
53
+ var err = (error) => ({
54
+ ok: false,
55
+ error
56
+ });
57
+ var isOk = (result) => result.ok;
58
+ var isErr = (result) => !result.ok;
32
59
 
33
- // src/commands/where.ts
34
- import { access } from "node:fs/promises";
35
- import { join } from "node:path";
36
- import { exit } from "node:process";
37
- async function whereWorktree(name) {
38
- if (!name) {
39
- return { success: false, message: "Error: worktree name required" };
60
+ // src/core/worktree/errors.ts
61
+ var WorktreeError = class extends Error {
62
+ constructor(message) {
63
+ super(message);
64
+ this.name = "WorktreeError";
65
+ }
66
+ };
67
+ var WorktreeNotFoundError = class extends WorktreeError {
68
+ constructor(name) {
69
+ super(`Worktree '${name}' not found`);
70
+ this.name = "WorktreeNotFoundError";
71
+ }
72
+ };
73
+ var WorktreeAlreadyExistsError = class extends WorktreeError {
74
+ constructor(name) {
75
+ super(`Worktree '${name}' already exists`);
76
+ this.name = "WorktreeAlreadyExistsError";
77
+ }
78
+ };
79
+ var GitOperationError = class extends WorktreeError {
80
+ constructor(operation, details) {
81
+ super(`Git ${operation} failed: ${details}`);
82
+ this.name = "GitOperationError";
40
83
  }
84
+ };
85
+
86
+ // src/core/worktree/validate.ts
87
+ import fs from "node:fs/promises";
88
+
89
+ // src/core/paths.ts
90
+ import { join } from "node:path";
91
+ function getPhantomDirectory(gitRoot) {
92
+ return join(gitRoot, ".git", "phantom", "worktrees");
93
+ }
94
+ function getWorktreePath(gitRoot, name) {
95
+ return join(getPhantomDirectory(gitRoot), name);
96
+ }
97
+
98
+ // src/core/worktree/validate.ts
99
+ async function validateWorktreeExists(gitRoot, name) {
100
+ const worktreePath = getWorktreePath(gitRoot, name);
41
101
  try {
42
- const gitRoot = await getGitRoot();
43
- const worktreesPath = join(gitRoot, ".git", "phantom", "worktrees");
44
- const worktreePath = join(worktreesPath, name);
45
- try {
46
- await access(worktreePath);
47
- } catch {
48
- return {
49
- success: false,
50
- message: `Error: Worktree '${name}' does not exist`
51
- };
52
- }
102
+ await fs.access(worktreePath);
53
103
  return {
54
- success: true,
104
+ exists: true,
55
105
  path: worktreePath
56
106
  };
57
- } catch (error) {
58
- const errorMessage = error instanceof Error ? error.message : String(error);
107
+ } catch {
59
108
  return {
60
- success: false,
61
- message: `Error locating worktree: ${errorMessage}`
109
+ exists: false,
110
+ message: `Worktree '${name}' does not exist`
62
111
  };
63
112
  }
64
113
  }
65
- async function whereHandler(args2) {
66
- const name = args2[0];
67
- const result = await whereWorktree(name);
68
- if (!result.success) {
69
- console.error(result.message);
70
- exit(1);
114
+ async function validateWorktreeDoesNotExist(gitRoot, name) {
115
+ const worktreePath = getWorktreePath(gitRoot, name);
116
+ try {
117
+ await fs.access(worktreePath);
118
+ return {
119
+ exists: true,
120
+ message: `Worktree '${name}' already exists`
121
+ };
122
+ } catch {
123
+ return {
124
+ exists: false,
125
+ path: worktreePath
126
+ };
127
+ }
128
+ }
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;
136
+ }
137
+ }
138
+ async function listValidWorktrees(gitRoot) {
139
+ const phantomDir = getPhantomDirectory(gitRoot);
140
+ if (!await validatePhantomDirectoryExists(gitRoot)) {
141
+ return [];
142
+ }
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 [];
71
155
  }
72
- console.log(result.path);
73
156
  }
74
157
 
75
- // src/commands/shell.ts
76
- async function shellInWorktree(worktreeName) {
77
- if (!worktreeName) {
78
- return { success: false, message: "Error: worktree name required" };
158
+ // src/core/process/spawn.ts
159
+ import {
160
+ spawn as nodeSpawn
161
+ } from "node:child_process";
162
+
163
+ // src/core/process/errors.ts
164
+ var ProcessError = class extends Error {
165
+ exitCode;
166
+ constructor(message, exitCode) {
167
+ super(message);
168
+ this.name = "ProcessError";
169
+ this.exitCode = exitCode;
79
170
  }
80
- const worktreeResult = await whereWorktree(worktreeName);
81
- if (!worktreeResult.success) {
82
- return { success: false, message: worktreeResult.message };
171
+ };
172
+ var ProcessExecutionError = class extends ProcessError {
173
+ constructor(command2, exitCode) {
174
+ super(`Command '${command2}' failed with exit code ${exitCode}`, exitCode);
175
+ this.name = "ProcessExecutionError";
83
176
  }
84
- const worktreePath = worktreeResult.path;
85
- const shell = process.env.SHELL || "/bin/sh";
177
+ };
178
+ var ProcessSignalError = class extends ProcessError {
179
+ constructor(signal) {
180
+ const exitCode = 128 + (signal === "SIGTERM" ? 15 : 1);
181
+ super(`Command terminated by signal: ${signal}`, exitCode);
182
+ this.name = "ProcessSignalError";
183
+ }
184
+ };
185
+ var ProcessSpawnError = class extends ProcessError {
186
+ constructor(command2, details) {
187
+ super(`Error executing command '${command2}': ${details}`);
188
+ this.name = "ProcessSpawnError";
189
+ }
190
+ };
191
+
192
+ // src/core/process/spawn.ts
193
+ async function spawnProcess(config) {
86
194
  return new Promise((resolve) => {
87
- const childProcess = spawn(shell, [], {
88
- cwd: worktreePath,
195
+ const { command: command2, args: args2 = [], options = {} } = config;
196
+ const childProcess = nodeSpawn(command2, args2, {
89
197
  stdio: "inherit",
90
- env: {
91
- ...process.env,
92
- // Add environment variable to indicate we're in a worktree
93
- WORKTREE_NAME: worktreeName,
94
- WORKTREE_PATH: worktreePath
95
- }
198
+ ...options
96
199
  });
97
200
  childProcess.on("error", (error) => {
98
- resolve({
99
- success: false,
100
- message: `Error starting shell: ${error.message}`
101
- });
201
+ resolve(err(new ProcessSpawnError(command2, error.message)));
102
202
  });
103
203
  childProcess.on("exit", (code, signal) => {
104
204
  if (signal) {
105
- resolve({
106
- success: false,
107
- message: `Shell terminated by signal: ${signal}`,
108
- exitCode: 128 + (signal === "SIGTERM" ? 15 : 1)
109
- });
205
+ resolve(err(new ProcessSignalError(signal)));
110
206
  } else {
111
207
  const exitCode = code ?? 0;
112
- resolve({
113
- success: exitCode === 0,
114
- exitCode
115
- });
208
+ if (exitCode === 0) {
209
+ resolve(ok({ exitCode }));
210
+ } else {
211
+ resolve(err(new ProcessExecutionError(command2, exitCode)));
212
+ }
116
213
  }
117
214
  });
118
215
  });
119
216
  }
120
- async function shellHandler(args2) {
121
- if (args2.length < 1) {
122
- console.error("Usage: phantom shell <worktree-name>");
123
- exit2(1);
124
- }
125
- const worktreeName = args2[0];
126
- const worktreeResult = await whereWorktree(worktreeName);
127
- if (!worktreeResult.success) {
128
- console.error(worktreeResult.message);
129
- exit2(1);
130
- }
131
- console.log(`Entering worktree '${worktreeName}' at ${worktreeResult.path}`);
132
- console.log("Type 'exit' to return to your original directory\n");
133
- const result = await shellInWorktree(worktreeName);
134
- if (!result.success) {
135
- if (result.message) {
136
- console.error(result.message);
217
+
218
+ // src/core/process/exec.ts
219
+ async function execInWorktree(gitRoot, worktreeName, command2) {
220
+ const validation = await validateWorktreeExists(gitRoot, worktreeName);
221
+ if (!validation.exists) {
222
+ return err(new WorktreeNotFoundError(worktreeName));
223
+ }
224
+ const worktreePath = validation.path;
225
+ const [cmd, ...args2] = command2;
226
+ return spawnProcess({
227
+ command: cmd,
228
+ args: args2,
229
+ options: {
230
+ cwd: worktreePath
137
231
  }
138
- exit2(result.exitCode ?? 1);
232
+ });
233
+ }
234
+
235
+ // src/core/process/shell.ts
236
+ async function shellInWorktree(gitRoot, worktreeName) {
237
+ const validation = await validateWorktreeExists(gitRoot, worktreeName);
238
+ if (!validation.exists) {
239
+ return err(new WorktreeNotFoundError(worktreeName));
139
240
  }
140
- exit2(result.exitCode ?? 0);
241
+ const worktreePath = validation.path;
242
+ const shell = process.env.SHELL || "/bin/sh";
243
+ return spawnProcess({
244
+ command: shell,
245
+ args: [],
246
+ options: {
247
+ cwd: worktreePath,
248
+ env: {
249
+ ...process.env,
250
+ PHANTOM: "1",
251
+ PHANTOM_NAME: worktreeName,
252
+ PHANTOM_PATH: worktreePath
253
+ }
254
+ }
255
+ });
256
+ }
257
+
258
+ // src/core/worktree/create.ts
259
+ import fs2 from "node:fs/promises";
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]);
141
265
  }
142
266
 
143
- // src/commands/create.ts
144
- async function createWorktree(name) {
145
- if (!name) {
146
- return { success: false, message: "Error: worktree name required" };
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);
272
+ try {
273
+ await fs2.access(worktreesPath);
274
+ } catch {
275
+ await fs2.mkdir(worktreesPath, { recursive: true });
276
+ }
277
+ const validation = await validateWorktreeDoesNotExist(gitRoot, name);
278
+ if (validation.exists) {
279
+ return err(new WorktreeAlreadyExistsError(name));
147
280
  }
148
281
  try {
149
- const gitRoot = await getGitRoot();
150
- const worktreesPath = join2(gitRoot, ".git", "phantom", "worktrees");
151
- const worktreePath = join2(worktreesPath, name);
152
- try {
153
- await access2(worktreesPath);
154
- } catch {
155
- await mkdir(worktreesPath, { recursive: true });
156
- }
157
- try {
158
- await access2(worktreePath);
159
- return {
160
- success: false,
161
- message: `Error: worktree '${name}' already exists`
162
- };
163
- } catch {
164
- }
165
282
  await addWorktree({
166
283
  path: worktreePath,
167
- branch: name,
168
- commitish: "HEAD"
284
+ branch,
285
+ commitish
169
286
  });
170
- return {
171
- success: true,
287
+ return ok({
172
288
  message: `Created worktree '${name}' at ${worktreePath}`,
173
289
  path: worktreePath
174
- };
290
+ });
175
291
  } catch (error) {
176
292
  const errorMessage = error instanceof Error ? error.message : String(error);
177
- return {
178
- success: false,
179
- message: `Error creating worktree: ${errorMessage}`
180
- };
293
+ return err(new GitOperationError("worktree add", errorMessage));
181
294
  }
182
295
  }
183
- async function createHandler(args2) {
184
- const name = args2[0];
185
- const openShell = args2.includes("--shell");
186
- const result = await createWorktree(name);
187
- if (!result.success) {
188
- console.error(result.message);
189
- exit3(1);
190
- }
191
- console.log(result.message);
192
- if (openShell && result.path) {
193
- console.log(`
194
- Entering worktree '${name}' at ${result.path}`);
195
- console.log("Type 'exit' to return to your original directory\n");
196
- const shellResult = await shellInWorktree(name);
197
- if (!shellResult.success) {
198
- if (shellResult.message) {
199
- console.error(shellResult.message);
200
- }
201
- exit3(shellResult.exitCode ?? 1);
202
- }
203
- exit3(shellResult.exitCode ?? 0);
296
+
297
+ // src/cli/output.ts
298
+ var output = {
299
+ log: (message) => {
300
+ console.log(message);
301
+ },
302
+ error: (message) => {
303
+ console.error(message);
304
+ },
305
+ table: (data) => {
306
+ console.table(data);
307
+ },
308
+ processOutput: (proc) => {
309
+ proc.stdout?.pipe(process.stdout);
310
+ proc.stderr?.pipe(process.stderr);
204
311
  }
312
+ };
313
+
314
+ // src/cli/errors.ts
315
+ var exitCodes = {
316
+ success: 0,
317
+ generalError: 1,
318
+ notFound: 2,
319
+ validationError: 3
320
+ };
321
+ function exitWithSuccess() {
322
+ process.exit(exitCodes.success);
323
+ }
324
+ function exitWithError(message, exitCode = exitCodes.generalError) {
325
+ output.error(message);
326
+ process.exit(exitCode);
205
327
  }
206
328
 
207
- // src/commands/delete.ts
208
- import { exec as exec3 } from "node:child_process";
209
- import { access as access3 } from "node:fs/promises";
210
- import { join as join3 } from "node:path";
211
- import { exit as exit4 } from "node:process";
212
- import { promisify as promisify3 } from "node:util";
213
- var execAsync3 = promisify3(exec3);
214
- async function deleteWorktree(name, options = {}) {
215
- if (!name) {
216
- return { success: false, message: "Error: worktree name required" };
329
+ // src/cli/handlers/create.ts
330
+ async function createHandler(args2) {
331
+ const { values, positionals } = parseArgs({
332
+ args: args2,
333
+ options: {
334
+ shell: {
335
+ type: "boolean",
336
+ short: "s"
337
+ },
338
+ exec: {
339
+ type: "string",
340
+ short: "x"
341
+ }
342
+ },
343
+ strict: true,
344
+ allowPositionals: true
345
+ });
346
+ if (positionals.length === 0) {
347
+ exitWithError(
348
+ "Please provide a name for the new worktree",
349
+ exitCodes.validationError
350
+ );
351
+ }
352
+ const worktreeName = positionals[0];
353
+ const openShell = values.shell ?? false;
354
+ const execCommand = values.exec;
355
+ if (openShell && execCommand) {
356
+ exitWithError(
357
+ "Cannot use --shell and --exec together",
358
+ exitCodes.validationError
359
+ );
217
360
  }
218
- const { force = false } = options;
219
361
  try {
220
362
  const gitRoot = await getGitRoot();
221
- const worktreesPath = join3(gitRoot, ".git", "phantom", "worktrees");
222
- const worktreePath = join3(worktreesPath, name);
223
- try {
224
- await access3(worktreePath);
225
- } catch {
226
- return {
227
- success: false,
228
- message: `Error: Worktree '${name}' does not exist`
229
- };
363
+ const result = await createWorktree(gitRoot, worktreeName);
364
+ if (isErr(result)) {
365
+ const exitCode = result.error instanceof WorktreeAlreadyExistsError ? exitCodes.validationError : exitCodes.generalError;
366
+ exitWithError(result.error.message, exitCode);
230
367
  }
231
- let hasUncommittedChanges = false;
232
- let changedFiles = 0;
233
- try {
234
- const { stdout } = await execAsync3("git status --porcelain", {
235
- cwd: worktreePath
236
- });
237
- const changes = stdout.trim();
238
- if (changes) {
239
- hasUncommittedChanges = true;
240
- changedFiles = changes.split("\n").length;
368
+ output.log(result.value.message);
369
+ if (execCommand && isOk(result)) {
370
+ output.log(
371
+ `
372
+ Executing command in worktree '${worktreeName}': ${execCommand}`
373
+ );
374
+ const shell = process.env.SHELL || "/bin/sh";
375
+ const execResult = await execInWorktree(gitRoot, worktreeName, [
376
+ shell,
377
+ "-c",
378
+ execCommand
379
+ ]);
380
+ if (isErr(execResult)) {
381
+ output.error(execResult.error.message);
382
+ const exitCode = "exitCode" in execResult.error ? execResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
383
+ exitWithError("", exitCode);
241
384
  }
242
- } catch {
243
- hasUncommittedChanges = false;
385
+ process.exit(execResult.value.exitCode ?? 0);
244
386
  }
245
- if (hasUncommittedChanges && !force) {
387
+ if (openShell && isOk(result)) {
388
+ output.log(
389
+ `
390
+ Entering worktree '${worktreeName}' at ${result.value.path}`
391
+ );
392
+ output.log("Type 'exit' to return to your original directory\n");
393
+ const shellResult = await shellInWorktree(gitRoot, worktreeName);
394
+ if (isErr(shellResult)) {
395
+ output.error(shellResult.error.message);
396
+ const exitCode = "exitCode" in shellResult.error ? shellResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
397
+ exitWithError("", exitCode);
398
+ }
399
+ process.exit(shellResult.value.exitCode ?? 0);
400
+ }
401
+ exitWithSuccess();
402
+ } catch (error) {
403
+ exitWithError(
404
+ error instanceof Error ? error.message : String(error),
405
+ exitCodes.generalError
406
+ );
407
+ }
408
+ }
409
+
410
+ // src/cli/handlers/delete.ts
411
+ import { parseArgs as parseArgs2 } from "node:util";
412
+
413
+ // src/core/worktree/delete.ts
414
+ async function getWorktreeStatus(worktreePath) {
415
+ try {
416
+ const { stdout } = await executeGitCommandInDirectory(worktreePath, [
417
+ "status",
418
+ "--porcelain"
419
+ ]);
420
+ if (stdout) {
246
421
  return {
247
- success: false,
248
- message: `Error: Worktree '${name}' has uncommitted changes (${changedFiles} files). Use --force to delete anyway.`,
249
422
  hasUncommittedChanges: true,
250
- changedFiles
423
+ changedFiles: stdout.split("\n").length
251
424
  };
252
425
  }
426
+ } catch {
427
+ }
428
+ return {
429
+ hasUncommittedChanges: false,
430
+ changedFiles: 0
431
+ };
432
+ }
433
+ async function removeWorktree(gitRoot, worktreePath, force = false) {
434
+ try {
435
+ await executeGitCommand(["worktree", "remove", worktreePath], {
436
+ cwd: gitRoot
437
+ });
438
+ } catch (error) {
253
439
  try {
254
- await execAsync3(`git worktree remove "${worktreePath}"`, {
255
- cwd: gitRoot
256
- });
257
- } catch (error) {
258
- try {
259
- await execAsync3(`git worktree remove --force "${worktreePath}"`, {
260
- cwd: gitRoot
261
- });
262
- } catch {
263
- return {
264
- success: false,
265
- message: `Error: Failed to remove worktree '${name}'`
266
- };
267
- }
268
- }
269
- const branchName = `phantom/worktrees/${name}`;
270
- try {
271
- await execAsync3(`git branch -D "${branchName}"`, {
440
+ await executeGitCommand(["worktree", "remove", "--force", worktreePath], {
272
441
  cwd: gitRoot
273
442
  });
274
443
  } catch {
444
+ throw new Error("Failed to remove worktree");
445
+ }
446
+ }
447
+ }
448
+ async function deleteBranch(gitRoot, branchName) {
449
+ try {
450
+ await executeGitCommand(["branch", "-D", branchName], { cwd: gitRoot });
451
+ return ok(true);
452
+ } catch (error) {
453
+ const errorMessage = error instanceof Error ? error.message : String(error);
454
+ return err(new GitOperationError("branch delete", errorMessage));
455
+ }
456
+ }
457
+ async function deleteWorktree(gitRoot, name, options = {}) {
458
+ const { force = false } = options;
459
+ const validation = await validateWorktreeExists(gitRoot, name);
460
+ if (!validation.exists) {
461
+ return err(new WorktreeNotFoundError(name));
462
+ }
463
+ const worktreePath = validation.path;
464
+ const status = await getWorktreeStatus(worktreePath);
465
+ if (status.hasUncommittedChanges && !force) {
466
+ return err(
467
+ new WorktreeError(
468
+ `Worktree '${name}' has uncommitted changes (${status.changedFiles} files). Use --force to delete anyway.`
469
+ )
470
+ );
471
+ }
472
+ try {
473
+ await removeWorktree(gitRoot, worktreePath, force);
474
+ const branchName = name;
475
+ const branchResult = await deleteBranch(gitRoot, branchName);
476
+ let message;
477
+ if (isOk(branchResult)) {
478
+ message = `Deleted worktree '${name}' and its branch '${branchName}'`;
479
+ } else {
480
+ message = `Deleted worktree '${name}'`;
481
+ message += `
482
+ Note: Branch '${branchName}' could not be deleted: ${branchResult.error.message}`;
275
483
  }
276
- let message = `Deleted worktree '${name}' and its branch '${branchName}'`;
277
- if (hasUncommittedChanges) {
278
- message = `Warning: Worktree '${name}' had uncommitted changes (${changedFiles} files)
484
+ if (status.hasUncommittedChanges) {
485
+ message = `Warning: Worktree '${name}' had uncommitted changes (${status.changedFiles} files)
279
486
  ${message}`;
280
487
  }
281
- return {
282
- success: true,
488
+ return ok({
283
489
  message,
284
- hasUncommittedChanges,
285
- changedFiles: hasUncommittedChanges ? changedFiles : void 0
286
- };
490
+ hasUncommittedChanges: status.hasUncommittedChanges,
491
+ changedFiles: status.hasUncommittedChanges ? status.changedFiles : void 0
492
+ });
287
493
  } catch (error) {
288
494
  const errorMessage = error instanceof Error ? error.message : String(error);
289
- return {
290
- success: false,
291
- message: `Error deleting worktree: ${errorMessage}`
292
- };
495
+ return err(new GitOperationError("worktree remove", errorMessage));
293
496
  }
294
497
  }
498
+
499
+ // src/cli/handlers/delete.ts
295
500
  async function deleteHandler(args2) {
296
- const forceIndex = args2.indexOf("--force");
297
- const force = forceIndex !== -1;
298
- const filteredArgs = args2.filter((arg) => arg !== "--force");
299
- const name = filteredArgs[0];
300
- const result = await deleteWorktree(name, { force });
301
- if (!result.success) {
302
- console.error(result.message);
303
- exit4(1);
501
+ const { values, positionals } = parseArgs2({
502
+ args: args2,
503
+ options: {
504
+ force: {
505
+ type: "boolean",
506
+ short: "f"
507
+ }
508
+ },
509
+ strict: true,
510
+ allowPositionals: true
511
+ });
512
+ if (positionals.length === 0) {
513
+ exitWithError(
514
+ "Please provide a worktree name to delete",
515
+ exitCodes.validationError
516
+ );
517
+ }
518
+ const worktreeName = positionals[0];
519
+ const forceDelete = values.force ?? false;
520
+ try {
521
+ const gitRoot = await getGitRoot();
522
+ const result = await deleteWorktree(gitRoot, worktreeName, {
523
+ force: forceDelete
524
+ });
525
+ if (isErr(result)) {
526
+ const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.validationError : result.error instanceof WorktreeError && result.error.message.includes("uncommitted changes") ? exitCodes.validationError : exitCodes.generalError;
527
+ exitWithError(result.error.message, exitCode);
528
+ }
529
+ output.log(result.value.message);
530
+ exitWithSuccess();
531
+ } catch (error) {
532
+ exitWithError(
533
+ error instanceof Error ? error.message : String(error),
534
+ exitCodes.generalError
535
+ );
304
536
  }
305
- console.log(result.message);
306
537
  }
307
538
 
308
- // src/commands/exec.ts
309
- import { spawn as spawn2 } from "node:child_process";
310
- import { exit as exit5 } from "node:process";
311
- async function execInWorktree(worktreeName, command2) {
312
- if (!worktreeName) {
313
- return { success: false, message: "Error: worktree name required" };
539
+ // src/cli/handlers/exec.ts
540
+ import { parseArgs as parseArgs3 } from "node:util";
541
+ async function execHandler(args2) {
542
+ const { positionals } = parseArgs3({
543
+ args: args2,
544
+ options: {},
545
+ strict: true,
546
+ allowPositionals: true
547
+ });
548
+ if (positionals.length < 2) {
549
+ exitWithError(
550
+ "Usage: phantom exec <worktree-name> <command> [args...]",
551
+ exitCodes.validationError
552
+ );
314
553
  }
315
- if (!command2 || command2.length === 0) {
316
- return { success: false, message: "Error: command required" };
554
+ const [worktreeName, ...commandArgs] = positionals;
555
+ try {
556
+ const gitRoot = await getGitRoot();
557
+ const result = await execInWorktree(gitRoot, worktreeName, commandArgs);
558
+ if (isErr(result)) {
559
+ const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.notFound : result.error.exitCode || exitCodes.generalError;
560
+ exitWithError(result.error.message, exitCode);
561
+ }
562
+ process.exit(result.value.exitCode);
563
+ } catch (error) {
564
+ exitWithError(
565
+ error instanceof Error ? error.message : String(error),
566
+ exitCodes.generalError
567
+ );
317
568
  }
318
- const worktreeResult = await whereWorktree(worktreeName);
319
- if (!worktreeResult.success) {
320
- return { success: false, message: worktreeResult.message };
569
+ }
570
+
571
+ // src/cli/handlers/list.ts
572
+ import { parseArgs as parseArgs4 } from "node:util";
573
+
574
+ // 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";
321
584
  }
322
- const worktreePath = worktreeResult.path;
323
- const [cmd, ...args2] = command2;
324
- return new Promise((resolve) => {
325
- const childProcess = spawn2(cmd, args2, {
326
- cwd: worktreePath,
327
- stdio: "inherit"
585
+ }
586
+ async function getWorktreeStatus2(worktreePath) {
587
+ try {
588
+ const { stdout } = await executeGitCommandInDirectory(worktreePath, [
589
+ "status",
590
+ "--porcelain"
591
+ ]);
592
+ return !stdout;
593
+ } catch {
594
+ return true;
595
+ }
596
+ }
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)"
328
615
  });
329
- childProcess.on("error", (error) => {
330
- resolve({
331
- success: false,
332
- message: `Error executing command: ${error.message}`
333
- });
616
+ }
617
+ const worktreeNames = await listValidWorktrees(gitRoot);
618
+ if (worktreeNames.length === 0) {
619
+ return ok({
620
+ worktrees: [],
621
+ message: "No worktrees found"
334
622
  });
335
- childProcess.on("exit", (code, signal) => {
336
- if (signal) {
337
- resolve({
338
- success: false,
339
- message: `Command terminated by signal: ${signal}`,
340
- exitCode: 128 + (signal === "SIGTERM" ? 15 : 1)
341
- });
342
- } else {
343
- const exitCode = code ?? 0;
344
- resolve({
345
- success: exitCode === 0,
346
- exitCode
347
- });
348
- }
623
+ }
624
+ try {
625
+ const worktrees = await Promise.all(
626
+ worktreeNames.map((name) => getWorktreeInfo(gitRoot, name))
627
+ );
628
+ return ok({
629
+ worktrees
349
630
  });
350
- });
351
- }
352
- async function execHandler(args2) {
353
- if (args2.length < 2) {
354
- console.error("Usage: phantom exec <worktree-name> <command> [args...]");
355
- exit5(1);
356
- }
357
- const worktreeName = args2[0];
358
- const command2 = args2.slice(1);
359
- const result = await execInWorktree(worktreeName, command2);
360
- if (!result.success) {
361
- if (result.message) {
362
- console.error(result.message);
363
- }
364
- exit5(result.exitCode ?? 1);
631
+ } catch (error) {
632
+ const errorMessage = error instanceof Error ? error.message : String(error);
633
+ throw new Error(`Failed to list worktrees: ${errorMessage}`);
365
634
  }
366
- exit5(result.exitCode ?? 0);
367
635
  }
368
636
 
369
- // src/commands/list.ts
370
- import { exec as exec4 } from "node:child_process";
371
- import { access as access4, readdir } from "node:fs/promises";
372
- import { join as join4 } from "node:path";
373
- import { promisify as promisify4 } from "node:util";
374
- var execAsync4 = promisify4(exec4);
375
- async function listWorktrees() {
637
+ // src/cli/handlers/list.ts
638
+ async function listHandler(args2 = []) {
639
+ parseArgs4({
640
+ args: args2,
641
+ options: {},
642
+ strict: true,
643
+ allowPositionals: false
644
+ });
376
645
  try {
377
646
  const gitRoot = await getGitRoot();
378
- const worktreesPath = join4(gitRoot, ".git", "phantom", "worktrees");
379
- try {
380
- await access4(worktreesPath);
381
- } catch {
382
- return {
383
- success: true,
384
- worktrees: [],
385
- message: "No worktrees found (worktrees directory doesn't exist)"
386
- };
647
+ const result = await listWorktrees(gitRoot);
648
+ if (isErr(result)) {
649
+ exitWithError("Failed to list worktrees", exitCodes.generalError);
387
650
  }
388
- let worktreeNames;
389
- try {
390
- const entries = await readdir(worktreesPath);
391
- const validEntries = await Promise.all(
392
- entries.map(async (entry) => {
393
- try {
394
- const entryPath = join4(worktreesPath, entry);
395
- await access4(entryPath);
396
- return entry;
397
- } catch {
398
- return null;
399
- }
400
- })
401
- );
402
- worktreeNames = validEntries.filter(
403
- (entry) => entry !== null
404
- );
405
- } catch {
406
- return {
407
- success: true,
408
- worktrees: [],
409
- message: "No worktrees found (unable to read worktrees directory)"
410
- };
651
+ const { worktrees, message } = result.value;
652
+ if (worktrees.length === 0) {
653
+ output.log(message || "No worktrees found.");
654
+ process.exit(exitCodes.success);
411
655
  }
412
- if (worktreeNames.length === 0) {
413
- return {
414
- success: true,
415
- worktrees: [],
416
- message: "No worktrees found"
417
- };
656
+ const maxNameLength = Math.max(...worktrees.map((wt) => wt.name.length));
657
+ for (const worktree of worktrees) {
658
+ const paddedName = worktree.name.padEnd(maxNameLength + 2);
659
+ const branchInfo = worktree.branch ? `(${worktree.branch})` : "";
660
+ const status = !worktree.isClean ? " [dirty]" : "";
661
+ output.log(`${paddedName} ${branchInfo}${status}`);
418
662
  }
419
- const worktrees = await Promise.all(
420
- worktreeNames.map(async (name) => {
421
- const worktreePath = join4(worktreesPath, name);
422
- let branch = "unknown";
423
- try {
424
- const { stdout } = await execAsync4("git branch --show-current", {
425
- cwd: worktreePath
426
- });
427
- branch = stdout.trim() || "detached HEAD";
428
- } catch {
429
- branch = "unknown";
430
- }
431
- let status = "clean";
432
- let changedFiles;
433
- try {
434
- const { stdout } = await execAsync4("git status --porcelain", {
435
- cwd: worktreePath
436
- });
437
- const changes = stdout.trim();
438
- if (changes) {
439
- status = "dirty";
440
- changedFiles = changes.split("\n").length;
441
- }
442
- } catch {
443
- status = "clean";
444
- }
445
- return {
446
- name,
447
- branch,
448
- status,
449
- changedFiles
450
- };
451
- })
452
- );
453
- return {
454
- success: true,
455
- worktrees
456
- };
663
+ process.exit(exitCodes.success);
457
664
  } catch (error) {
458
- const errorMessage = error instanceof Error ? error.message : String(error);
459
- return {
460
- success: false,
461
- message: `Error listing worktrees: ${errorMessage}`
462
- };
665
+ exitWithError(
666
+ error instanceof Error ? error.message : String(error),
667
+ exitCodes.generalError
668
+ );
463
669
  }
464
670
  }
465
- async function listHandler() {
466
- const result = await listWorktrees();
467
- if (!result.success) {
468
- console.error(result.message);
469
- return;
470
- }
471
- if (!result.worktrees || result.worktrees.length === 0) {
472
- console.log(result.message || "No worktrees found");
473
- return;
671
+
672
+ // src/cli/handlers/shell.ts
673
+ import { parseArgs as parseArgs5 } from "node:util";
674
+ async function shellHandler(args2) {
675
+ const { positionals } = parseArgs5({
676
+ args: args2,
677
+ options: {},
678
+ strict: true,
679
+ allowPositionals: true
680
+ });
681
+ if (positionals.length === 0) {
682
+ exitWithError(
683
+ "Usage: phantom shell <worktree-name>",
684
+ exitCodes.validationError
685
+ );
474
686
  }
475
- console.log("Worktrees:");
476
- for (const worktree of result.worktrees) {
477
- const statusText = worktree.status === "clean" ? "[clean]" : `[dirty: ${worktree.changedFiles} files]`;
478
- console.log(
479
- ` ${worktree.name.padEnd(20)} (branch: ${worktree.branch.padEnd(20)}) ${statusText}`
687
+ const worktreeName = positionals[0];
688
+ try {
689
+ const gitRoot = await getGitRoot();
690
+ const validation = await validateWorktreeExists(gitRoot, worktreeName);
691
+ if (!validation.exists) {
692
+ exitWithError(
693
+ validation.message || `Worktree '${worktreeName}' not found`,
694
+ exitCodes.generalError
695
+ );
696
+ }
697
+ output.log(`Entering worktree '${worktreeName}' at ${validation.path}`);
698
+ output.log("Type 'exit' to return to your original directory\n");
699
+ const result = await shellInWorktree(gitRoot, worktreeName);
700
+ if (isErr(result)) {
701
+ const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.notFound : result.error.exitCode || exitCodes.generalError;
702
+ exitWithError(result.error.message, exitCode);
703
+ }
704
+ process.exit(result.value.exitCode);
705
+ } catch (error) {
706
+ exitWithError(
707
+ error instanceof Error ? error.message : String(error),
708
+ exitCodes.generalError
480
709
  );
481
710
  }
482
- console.log(`
483
- Total: ${result.worktrees.length} worktrees`);
484
711
  }
485
712
 
713
+ // src/cli/handlers/version.ts
714
+ import { parseArgs as parseArgs6 } from "node:util";
715
+
486
716
  // package.json
487
717
  var package_default = {
488
718
  name: "@aku11i/phantom",
489
719
  packageManager: "pnpm@10.8.1",
490
- version: "0.3.0",
720
+ version: "0.4.0",
491
721
  description: "A powerful CLI tool for managing Git worktrees for parallel development",
492
722
  keywords: [
493
723
  "git",
@@ -516,7 +746,7 @@ var package_default = {
516
746
  start: "node ./src/bin/phantom.ts",
517
747
  phantom: "node ./src/bin/phantom.ts",
518
748
  build: "node build.ts",
519
- "type-check": "tsc --noEmit",
749
+ "type-check": "tsgo --noEmit",
520
750
  test: "node --test --experimental-strip-types --experimental-test-module-mocks src/**/*.test.ts",
521
751
  lint: "biome check .",
522
752
  fix: "biome check --write .",
@@ -535,25 +765,77 @@ var package_default = {
535
765
  devDependencies: {
536
766
  "@biomejs/biome": "^1.9.4",
537
767
  "@types/node": "^22.15.29",
768
+ "@typescript/native-preview": "7.0.0-dev.20250602.1",
538
769
  esbuild: "^0.25.5",
539
770
  typescript: "^5.8.3"
540
771
  }
541
772
  };
542
773
 
543
- // src/commands/version.ts
774
+ // src/core/version.ts
544
775
  function getVersion() {
545
776
  return package_default.version;
546
777
  }
547
- function versionHandler() {
778
+
779
+ // src/cli/handlers/version.ts
780
+ function versionHandler(args2 = []) {
781
+ parseArgs6({
782
+ args: args2,
783
+ options: {},
784
+ strict: true,
785
+ allowPositionals: false
786
+ });
548
787
  const version = getVersion();
549
- console.log(`Phantom v${version}`);
788
+ output.log(`Phantom v${version}`);
789
+ exitWithSuccess();
790
+ }
791
+
792
+ // src/cli/handlers/where.ts
793
+ import { parseArgs as parseArgs7 } from "node:util";
794
+
795
+ // src/core/worktree/where.ts
796
+ async function whereWorktree(gitRoot, name) {
797
+ const validation = await validateWorktreeExists(gitRoot, name);
798
+ if (!validation.exists) {
799
+ return err(new WorktreeNotFoundError(name));
800
+ }
801
+ return ok({
802
+ path: validation.path
803
+ });
804
+ }
805
+
806
+ // src/cli/handlers/where.ts
807
+ async function whereHandler(args2) {
808
+ const { positionals } = parseArgs7({
809
+ args: args2,
810
+ options: {},
811
+ strict: true,
812
+ allowPositionals: true
813
+ });
814
+ if (positionals.length === 0) {
815
+ exitWithError("Please provide a worktree name", exitCodes.validationError);
816
+ }
817
+ const worktreeName = positionals[0];
818
+ try {
819
+ const gitRoot = await getGitRoot();
820
+ const result = await whereWorktree(gitRoot, worktreeName);
821
+ if (isErr(result)) {
822
+ exitWithError(result.error.message, exitCodes.notFound);
823
+ }
824
+ output.log(result.value.path);
825
+ exitWithSuccess();
826
+ } catch (error) {
827
+ exitWithError(
828
+ error instanceof Error ? error.message : String(error),
829
+ exitCodes.generalError
830
+ );
831
+ }
550
832
  }
551
833
 
552
834
  // src/bin/phantom.ts
553
835
  var commands = [
554
836
  {
555
837
  name: "create",
556
- description: "Create a new worktree [--shell to open shell]",
838
+ description: "Create a new worktree [--shell | --exec <command>]",
557
839
  handler: createHandler
558
840
  },
559
841
  {
@@ -617,18 +899,18 @@ function findCommand(args2, commands2) {
617
899
  var args = argv.slice(2);
618
900
  if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
619
901
  printHelp(commands);
620
- exit6(0);
902
+ exit(0);
621
903
  }
622
904
  if (args[0] === "--version" || args[0] === "-v") {
623
905
  versionHandler();
624
- exit6(0);
906
+ exit(0);
625
907
  }
626
908
  var { command, remainingArgs } = findCommand(args, commands);
627
909
  if (!command || !command.handler) {
628
910
  console.error(`Error: Unknown command '${args.join(" ")}'
629
911
  `);
630
912
  printHelp(commands);
631
- exit6(1);
913
+ exit(1);
632
914
  }
633
915
  try {
634
916
  await command.handler(remainingArgs);
@@ -637,6 +919,6 @@ try {
637
919
  "Error:",
638
920
  error instanceof Error ? error.message : String(error)
639
921
  );
640
- exit6(1);
922
+ exit(1);
641
923
  }
642
924
  //# sourceMappingURL=phantom.js.map