@aku11i/phantom 0.2.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,537 +1,879 @@
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/gardens/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/phantom/command/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;
59
+
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";
83
+ }
84
+ };
32
85
 
33
- // src/gardens/commands/where.ts
34
- import { access } from "node:fs/promises";
86
+ // src/core/worktree/validate.ts
87
+ import fs from "node:fs/promises";
88
+
89
+ // src/core/paths.ts
35
90
  import { join } from "node:path";
36
- import { exit } from "node:process";
37
- async function whereGarden(name) {
38
- if (!name) {
39
- return { success: false, message: "Error: garden name required" };
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);
101
+ try {
102
+ await fs.access(worktreePath);
103
+ return {
104
+ exists: true,
105
+ path: worktreePath
106
+ };
107
+ } catch {
108
+ return {
109
+ exists: false,
110
+ message: `Worktree '${name}' does not exist`
111
+ };
40
112
  }
113
+ }
114
+ async function validateWorktreeDoesNotExist(gitRoot, name) {
115
+ const worktreePath = getWorktreePath(gitRoot, name);
41
116
  try {
42
- const gitRoot = await getGitRoot();
43
- const gardensPath = join(gitRoot, ".git", "phantom", "gardens");
44
- const gardenPath = join(gardensPath, name);
45
- try {
46
- await access(gardenPath);
47
- } catch {
48
- return {
49
- success: false,
50
- message: `Error: Garden '${name}' does not exist`
51
- };
52
- }
117
+ await fs.access(worktreePath);
53
118
  return {
54
- success: true,
55
- path: gardenPath
119
+ exists: true,
120
+ message: `Worktree '${name}' already exists`
56
121
  };
57
- } catch (error) {
58
- const errorMessage = error instanceof Error ? error.message : String(error);
122
+ } catch {
59
123
  return {
60
- success: false,
61
- message: `Error locating garden: ${errorMessage}`
124
+ exists: false,
125
+ path: worktreePath
62
126
  };
63
127
  }
64
128
  }
65
- async function gardensWhereHandler(args2) {
66
- const name = args2[0];
67
- const result = await whereGarden(name);
68
- if (!result.success) {
69
- console.error(result.message);
70
- exit(1);
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;
71
136
  }
72
- console.log(result.path);
73
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 [];
155
+ }
156
+ }
157
+
158
+ // src/core/process/spawn.ts
159
+ import {
160
+ spawn as nodeSpawn
161
+ } from "node:child_process";
74
162
 
75
- // src/phantom/command/shell.ts
76
- async function shellInGarden(gardenName) {
77
- if (!gardenName) {
78
- return { success: false, message: "Error: garden name required" };
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 gardenResult = await whereGarden(gardenName);
81
- if (!gardenResult.success) {
82
- return { success: false, message: gardenResult.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 gardenPath = gardenResult.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: gardenPath,
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 phantom garden
93
- PHANTOM_GARDEN: gardenName,
94
- PHANTOM_GARDEN_PATH: gardenPath
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 <garden-name>");
123
- exit2(1);
124
- }
125
- const gardenName = args2[0];
126
- const gardenResult = await whereGarden(gardenName);
127
- if (!gardenResult.success) {
128
- console.error(gardenResult.message);
129
- exit2(1);
130
- }
131
- console.log(`Entering garden '${gardenName}' at ${gardenResult.path}`);
132
- console.log("Type 'exit' to return to your original directory\n");
133
- const result = await shellInGarden(gardenName);
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
+ });
141
256
  }
142
257
 
143
- // src/gardens/commands/create.ts
144
- async function createGarden(name) {
145
- if (!name) {
146
- return { success: false, message: "Error: garden name required" };
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]);
265
+ }
266
+
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 gardensPath = join2(gitRoot, ".git", "phantom", "gardens");
151
- const worktreePath = join2(gardensPath, name);
152
- try {
153
- await access2(gardensPath);
154
- } catch {
155
- await mkdir(gardensPath, { recursive: true });
156
- }
157
- try {
158
- await access2(worktreePath);
159
- return {
160
- success: false,
161
- message: `Error: garden '${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,
172
- message: `Created garden '${name}' at ${worktreePath}`,
287
+ return ok({
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 garden: ${errorMessage}`
180
- };
293
+ return err(new GitOperationError("worktree add", errorMessage));
181
294
  }
182
295
  }
183
- async function gardensCreateHandler(args2) {
184
- const name = args2[0];
185
- const openShell = args2.includes("--shell");
186
- const result = await createGarden(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 garden '${name}' at ${result.path}`);
195
- console.log("Type 'exit' to return to your original directory\n");
196
- const shellResult = await shellInGarden(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/gardens/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 deleteGarden(name, options = {}) {
215
- if (!name) {
216
- return { success: false, message: "Error: garden 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 gardensPath = join3(gitRoot, ".git", "phantom", "gardens");
222
- const gardenPath = join3(gardensPath, name);
223
- try {
224
- await access3(gardenPath);
225
- } catch {
226
- return {
227
- success: false,
228
- message: `Error: Garden '${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: gardenPath
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: Garden '${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 "${gardenPath}"`, {
255
- cwd: gitRoot
256
- });
257
- } catch (error) {
258
- try {
259
- await execAsync3(`git worktree remove --force "${gardenPath}"`, {
260
- cwd: gitRoot
261
- });
262
- } catch {
263
- return {
264
- success: false,
265
- message: `Error: Failed to remove worktree for garden '${name}'`
266
- };
267
- }
268
- }
269
- const branchName = `phantom/gardens/${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");
275
445
  }
276
- let message = `Deleted garden '${name}' and its branch '${branchName}'`;
277
- if (hasUncommittedChanges) {
278
- message = `Warning: Garden '${name}' had uncommitted changes (${changedFiles} files)
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}`;
483
+ }
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 garden: ${errorMessage}`
292
- };
495
+ return err(new GitOperationError("worktree remove", errorMessage));
293
496
  }
294
497
  }
295
- async function gardensDeleteHandler(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 deleteGarden(name, { force });
301
- if (!result.success) {
302
- console.error(result.message);
303
- exit4(1);
498
+
499
+ // src/cli/handlers/delete.ts
500
+ async function deleteHandler(args2) {
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/gardens/commands/list.ts
309
- import { exec as exec4 } from "node:child_process";
310
- import { access as access4, readdir } from "node:fs/promises";
311
- import { join as join4 } from "node:path";
312
- import { promisify as promisify4 } from "node:util";
313
- var execAsync4 = promisify4(exec4);
314
- async function listGardens() {
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
+ );
553
+ }
554
+ const [worktreeName, ...commandArgs] = positionals;
315
555
  try {
316
556
  const gitRoot = await getGitRoot();
317
- const gardensPath = join4(gitRoot, ".git", "phantom", "gardens");
318
- try {
319
- await access4(gardensPath);
320
- } catch {
321
- return {
322
- success: true,
323
- gardens: [],
324
- message: "No gardens found (gardens directory doesn't exist)"
325
- };
326
- }
327
- let gardenNames;
328
- try {
329
- const entries = await readdir(gardensPath);
330
- const validEntries = await Promise.all(
331
- entries.map(async (entry) => {
332
- try {
333
- const entryPath = join4(gardensPath, entry);
334
- await access4(entryPath);
335
- return entry;
336
- } catch {
337
- return null;
338
- }
339
- })
340
- );
341
- gardenNames = validEntries.filter(
342
- (entry) => entry !== null
343
- );
344
- } catch {
345
- return {
346
- success: true,
347
- gardens: [],
348
- message: "No gardens found (unable to read gardens directory)"
349
- };
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);
350
561
  }
351
- if (gardenNames.length === 0) {
352
- return {
353
- success: true,
354
- gardens: [],
355
- message: "No gardens found"
356
- };
357
- }
358
- const gardens = await Promise.all(
359
- gardenNames.map(async (name) => {
360
- const gardenPath = join4(gardensPath, name);
361
- let branch = "unknown";
362
- try {
363
- const { stdout } = await execAsync4("git branch --show-current", {
364
- cwd: gardenPath
365
- });
366
- branch = stdout.trim() || "detached HEAD";
367
- } catch {
368
- branch = "unknown";
369
- }
370
- let status = "clean";
371
- let changedFiles;
372
- try {
373
- const { stdout } = await execAsync4("git status --porcelain", {
374
- cwd: gardenPath
375
- });
376
- const changes = stdout.trim();
377
- if (changes) {
378
- status = "dirty";
379
- changedFiles = changes.split("\n").length;
380
- }
381
- } catch {
382
- status = "clean";
383
- }
384
- return {
385
- name,
386
- branch,
387
- status,
388
- changedFiles
389
- };
390
- })
562
+ process.exit(result.value.exitCode);
563
+ } catch (error) {
564
+ exitWithError(
565
+ error instanceof Error ? error.message : String(error),
566
+ exitCodes.generalError
391
567
  );
392
- return {
393
- success: true,
394
- gardens
395
- };
568
+ }
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";
584
+ }
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)"
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
+ }
624
+ try {
625
+ const worktrees = await Promise.all(
626
+ worktreeNames.map((name) => getWorktreeInfo(gitRoot, name))
627
+ );
628
+ return ok({
629
+ worktrees
630
+ });
396
631
  } catch (error) {
397
632
  const errorMessage = error instanceof Error ? error.message : String(error);
398
- return {
399
- success: false,
400
- message: `Error listing gardens: ${errorMessage}`
401
- };
633
+ throw new Error(`Failed to list worktrees: ${errorMessage}`);
402
634
  }
403
635
  }
404
- async function gardensListHandler() {
405
- const result = await listGardens();
406
- if (!result.success) {
407
- console.error(result.message);
408
- return;
636
+
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
+ });
645
+ try {
646
+ const gitRoot = await getGitRoot();
647
+ const result = await listWorktrees(gitRoot);
648
+ if (isErr(result)) {
649
+ exitWithError("Failed to list worktrees", exitCodes.generalError);
650
+ }
651
+ const { worktrees, message } = result.value;
652
+ if (worktrees.length === 0) {
653
+ output.log(message || "No worktrees found.");
654
+ process.exit(exitCodes.success);
655
+ }
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}`);
662
+ }
663
+ process.exit(exitCodes.success);
664
+ } catch (error) {
665
+ exitWithError(
666
+ error instanceof Error ? error.message : String(error),
667
+ exitCodes.generalError
668
+ );
409
669
  }
410
- if (!result.gardens || result.gardens.length === 0) {
411
- console.log(result.message || "No gardens found");
412
- return;
670
+ }
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
+ );
413
686
  }
414
- console.log("Gardens:");
415
- for (const garden of result.gardens) {
416
- const statusText = garden.status === "clean" ? "[clean]" : `[dirty: ${garden.changedFiles} files]`;
417
- console.log(
418
- ` ${garden.name.padEnd(20)} (branch: ${garden.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
419
709
  );
420
710
  }
421
- console.log(`
422
- Total: ${result.gardens.length} gardens`);
423
711
  }
424
712
 
425
- // src/phantom/command/exec.ts
426
- import { spawn as spawn2 } from "node:child_process";
427
- import { exit as exit5 } from "node:process";
428
- async function execInGarden(gardenName, command2) {
429
- if (!gardenName) {
430
- return { success: false, message: "Error: garden name required" };
431
- }
432
- if (!command2 || command2.length === 0) {
433
- return { success: false, message: "Error: command required" };
713
+ // src/cli/handlers/version.ts
714
+ import { parseArgs as parseArgs6 } from "node:util";
715
+
716
+ // package.json
717
+ var package_default = {
718
+ name: "@aku11i/phantom",
719
+ packageManager: "pnpm@10.8.1",
720
+ version: "0.4.0",
721
+ description: "A powerful CLI tool for managing Git worktrees for parallel development",
722
+ keywords: [
723
+ "git",
724
+ "worktree",
725
+ "cli",
726
+ "phantom",
727
+ "workspace",
728
+ "development",
729
+ "parallel"
730
+ ],
731
+ homepage: "https://github.com/aku11i/phantom#readme",
732
+ bugs: {
733
+ url: "https://github.com/aku11i/phantom/issues"
734
+ },
735
+ repository: {
736
+ type: "git",
737
+ url: "git+https://github.com/aku11i/phantom.git"
738
+ },
739
+ license: "MIT",
740
+ author: "aku11i",
741
+ type: "module",
742
+ bin: {
743
+ phantom: "./dist/phantom.js"
744
+ },
745
+ scripts: {
746
+ start: "node ./src/bin/phantom.ts",
747
+ phantom: "node ./src/bin/phantom.ts",
748
+ build: "node build.ts",
749
+ "type-check": "tsgo --noEmit",
750
+ test: "node --test --experimental-strip-types --experimental-test-module-mocks src/**/*.test.ts",
751
+ lint: "biome check .",
752
+ fix: "biome check --write .",
753
+ ready: "pnpm fix && pnpm type-check && pnpm test",
754
+ "ready:check": "pnpm lint && pnpm type-check && pnpm test",
755
+ prepublishOnly: "pnpm ready:check && pnpm build"
756
+ },
757
+ engines: {
758
+ node: ">=22.0.0"
759
+ },
760
+ files: [
761
+ "dist/",
762
+ "README.md",
763
+ "LICENSE"
764
+ ],
765
+ devDependencies: {
766
+ "@biomejs/biome": "^1.9.4",
767
+ "@types/node": "^22.15.29",
768
+ "@typescript/native-preview": "7.0.0-dev.20250602.1",
769
+ esbuild: "^0.25.5",
770
+ typescript: "^5.8.3"
434
771
  }
435
- const gardenResult = await whereGarden(gardenName);
436
- if (!gardenResult.success) {
437
- return { success: false, message: gardenResult.message };
772
+ };
773
+
774
+ // src/core/version.ts
775
+ function getVersion() {
776
+ return package_default.version;
777
+ }
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
+ });
787
+ const version = getVersion();
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));
438
800
  }
439
- const gardenPath = gardenResult.path;
440
- const [cmd, ...args2] = command2;
441
- return new Promise((resolve) => {
442
- const childProcess = spawn2(cmd, args2, {
443
- cwd: gardenPath,
444
- stdio: "inherit"
445
- });
446
- childProcess.on("error", (error) => {
447
- resolve({
448
- success: false,
449
- message: `Error executing command: ${error.message}`
450
- });
451
- });
452
- childProcess.on("exit", (code, signal) => {
453
- if (signal) {
454
- resolve({
455
- success: false,
456
- message: `Command terminated by signal: ${signal}`,
457
- exitCode: 128 + (signal === "SIGTERM" ? 15 : 1)
458
- });
459
- } else {
460
- const exitCode = code ?? 0;
461
- resolve({
462
- success: exitCode === 0,
463
- exitCode
464
- });
465
- }
466
- });
801
+ return ok({
802
+ path: validation.path
467
803
  });
468
804
  }
469
- async function execHandler(args2) {
470
- if (args2.length < 2) {
471
- console.error("Usage: phantom exec <garden-name> <command> [args...]");
472
- exit5(1);
473
- }
474
- const gardenName = args2[0];
475
- const command2 = args2.slice(1);
476
- const result = await execInGarden(gardenName, command2);
477
- if (!result.success) {
478
- if (result.message) {
479
- console.error(result.message);
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);
480
823
  }
481
- exit5(result.exitCode ?? 1);
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
+ );
482
831
  }
483
- exit5(result.exitCode ?? 0);
484
832
  }
485
833
 
486
834
  // src/bin/phantom.ts
487
835
  var commands = [
488
836
  {
489
- name: "garden",
490
- description: "Manage git worktrees (gardens)",
491
- subcommands: [
492
- {
493
- name: "create",
494
- description: "Create a new worktree (garden) [--shell to open shell]",
495
- handler: gardensCreateHandler
496
- },
497
- {
498
- name: "list",
499
- description: "List all gardens",
500
- handler: gardensListHandler
501
- },
502
- {
503
- name: "where",
504
- description: "Output the path of a specific garden",
505
- handler: gardensWhereHandler
506
- },
507
- {
508
- name: "delete",
509
- description: "Delete a garden (use --force for dirty gardens)",
510
- handler: gardensDeleteHandler
511
- }
512
- ]
837
+ name: "create",
838
+ description: "Create a new worktree [--shell | --exec <command>]",
839
+ handler: createHandler
840
+ },
841
+ {
842
+ name: "list",
843
+ description: "List all worktrees",
844
+ handler: listHandler
845
+ },
846
+ {
847
+ name: "where",
848
+ description: "Output the path of a specific worktree",
849
+ handler: whereHandler
850
+ },
851
+ {
852
+ name: "delete",
853
+ description: "Delete a worktree (use --force for uncommitted changes)",
854
+ handler: deleteHandler
513
855
  },
514
856
  {
515
857
  name: "exec",
516
- description: "Execute a command in a garden directory",
858
+ description: "Execute a command in a worktree directory",
517
859
  handler: execHandler
518
860
  },
519
861
  {
520
862
  name: "shell",
521
- description: "Open interactive shell in a garden directory",
863
+ description: "Open interactive shell in a worktree directory",
522
864
  handler: shellHandler
865
+ },
866
+ {
867
+ name: "version",
868
+ description: "Display phantom version",
869
+ handler: versionHandler
523
870
  }
524
871
  ];
525
- function printHelp(commands2, prefix = "") {
872
+ function printHelp(commands2) {
526
873
  console.log("Usage: phantom <command> [options]\n");
527
874
  console.log("Commands:");
528
875
  for (const cmd of commands2) {
529
- console.log(` ${prefix}${cmd.name.padEnd(20)} ${cmd.description}`);
530
- if (cmd.subcommands) {
531
- for (const subcmd of cmd.subcommands) {
532
- console.log(` ${subcmd.name.padEnd(18)} ${subcmd.description}`);
533
- }
534
- }
876
+ console.log(` ${cmd.name.padEnd(12)} ${cmd.description}`);
535
877
  }
536
878
  }
537
879
  function findCommand(args2, commands2) {
@@ -557,14 +899,18 @@ function findCommand(args2, commands2) {
557
899
  var args = argv.slice(2);
558
900
  if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
559
901
  printHelp(commands);
560
- exit6(0);
902
+ exit(0);
903
+ }
904
+ if (args[0] === "--version" || args[0] === "-v") {
905
+ versionHandler();
906
+ exit(0);
561
907
  }
562
908
  var { command, remainingArgs } = findCommand(args, commands);
563
909
  if (!command || !command.handler) {
564
910
  console.error(`Error: Unknown command '${args.join(" ")}'
565
911
  `);
566
912
  printHelp(commands);
567
- exit6(1);
913
+ exit(1);
568
914
  }
569
915
  try {
570
916
  await command.handler(remainingArgs);
@@ -573,6 +919,6 @@ try {
573
919
  "Error:",
574
920
  error instanceof Error ? error.message : String(error)
575
921
  );
576
- exit6(1);
922
+ exit(1);
577
923
  }
578
924
  //# sourceMappingURL=phantom.js.map