@aku11i/phantom 0.9.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -417
- package/package.json +6 -27
- package/phantom.js +10210 -0
- package/README.ja.md +0 -427
- package/dist/garden.js +0 -458
- package/dist/garden.js.map +0 -7
- package/dist/phantom.js +0 -2338
- package/dist/phantom.js.map +0 -7
package/dist/phantom.js
DELETED
|
@@ -1,2338 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/bin/phantom.ts
|
|
4
|
-
import { argv, exit as exit2 } from "node:process";
|
|
5
|
-
|
|
6
|
-
// src/cli/handlers/attach.ts
|
|
7
|
-
import { parseArgs } from "node:util";
|
|
8
|
-
|
|
9
|
-
// src/core/git/libs/get-git-root.ts
|
|
10
|
-
import { dirname, resolve } from "node:path";
|
|
11
|
-
|
|
12
|
-
// src/core/git/executor.ts
|
|
13
|
-
import { execFile as execFileCallback } from "node:child_process";
|
|
14
|
-
import { promisify } from "node:util";
|
|
15
|
-
var execFile = promisify(execFileCallback);
|
|
16
|
-
async function executeGitCommand(args2, options = {}) {
|
|
17
|
-
try {
|
|
18
|
-
const result = await execFile("git", args2, {
|
|
19
|
-
cwd: options.cwd,
|
|
20
|
-
env: options.env || process.env,
|
|
21
|
-
encoding: "utf8"
|
|
22
|
-
});
|
|
23
|
-
return {
|
|
24
|
-
stdout: result.stdout.trim(),
|
|
25
|
-
stderr: result.stderr.trim()
|
|
26
|
-
};
|
|
27
|
-
} catch (error) {
|
|
28
|
-
if (error && typeof error === "object" && "stdout" in error && "stderr" in error) {
|
|
29
|
-
const execError = error;
|
|
30
|
-
if (execError.stderr?.trim()) {
|
|
31
|
-
throw new Error(execError.stderr.trim());
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
stdout: execError.stdout?.trim() || "",
|
|
35
|
-
stderr: execError.stderr?.trim() || ""
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
async function executeGitCommandInDirectory(directory, args2) {
|
|
42
|
-
return executeGitCommand(["-C", directory, ...args2], {});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// src/core/git/libs/get-git-root.ts
|
|
46
|
-
async function getGitRoot() {
|
|
47
|
-
const { stdout: stdout2 } = await executeGitCommand(["rev-parse", "--git-common-dir"]);
|
|
48
|
-
if (stdout2.endsWith("/.git") || stdout2 === ".git") {
|
|
49
|
-
return resolve(process.cwd(), dirname(stdout2));
|
|
50
|
-
}
|
|
51
|
-
const { stdout: toplevel } = await executeGitCommand([
|
|
52
|
-
"rev-parse",
|
|
53
|
-
"--show-toplevel"
|
|
54
|
-
]);
|
|
55
|
-
return toplevel;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// src/core/types/result.ts
|
|
59
|
-
var ok = (value) => ({
|
|
60
|
-
ok: true,
|
|
61
|
-
value
|
|
62
|
-
});
|
|
63
|
-
var err = (error) => ({
|
|
64
|
-
ok: false,
|
|
65
|
-
error
|
|
66
|
-
});
|
|
67
|
-
var isOk = (result) => result.ok;
|
|
68
|
-
var isErr = (result) => !result.ok;
|
|
69
|
-
|
|
70
|
-
// src/core/worktree/errors.ts
|
|
71
|
-
var WorktreeError = class extends Error {
|
|
72
|
-
constructor(message) {
|
|
73
|
-
super(message);
|
|
74
|
-
this.name = "WorktreeError";
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
var WorktreeNotFoundError = class extends WorktreeError {
|
|
78
|
-
constructor(name) {
|
|
79
|
-
super(`Worktree '${name}' not found`);
|
|
80
|
-
this.name = "WorktreeNotFoundError";
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
var WorktreeAlreadyExistsError = class extends WorktreeError {
|
|
84
|
-
constructor(name) {
|
|
85
|
-
super(`Worktree '${name}' already exists`);
|
|
86
|
-
this.name = "WorktreeAlreadyExistsError";
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
var GitOperationError = class extends WorktreeError {
|
|
90
|
-
constructor(operation, details) {
|
|
91
|
-
super(`Git ${operation} failed: ${details}`);
|
|
92
|
-
this.name = "GitOperationError";
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
var BranchNotFoundError = class extends WorktreeError {
|
|
96
|
-
constructor(branchName) {
|
|
97
|
-
super(`Branch '${branchName}' not found`);
|
|
98
|
-
this.name = "BranchNotFoundError";
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// src/core/worktree/validate.ts
|
|
103
|
-
import fs from "node:fs/promises";
|
|
104
|
-
|
|
105
|
-
// src/core/paths.ts
|
|
106
|
-
import { join } from "node:path";
|
|
107
|
-
function getPhantomDirectory(gitRoot) {
|
|
108
|
-
return join(gitRoot, ".git", "phantom", "worktrees");
|
|
109
|
-
}
|
|
110
|
-
function getWorktreePath(gitRoot, name) {
|
|
111
|
-
return join(getPhantomDirectory(gitRoot), name);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// src/core/worktree/validate.ts
|
|
115
|
-
async function validateWorktreeExists(gitRoot, name) {
|
|
116
|
-
const worktreePath = getWorktreePath(gitRoot, name);
|
|
117
|
-
try {
|
|
118
|
-
await fs.access(worktreePath);
|
|
119
|
-
return {
|
|
120
|
-
exists: true,
|
|
121
|
-
path: worktreePath
|
|
122
|
-
};
|
|
123
|
-
} catch {
|
|
124
|
-
return {
|
|
125
|
-
exists: false,
|
|
126
|
-
message: `Worktree '${name}' does not exist`
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
async function validateWorktreeDoesNotExist(gitRoot, name) {
|
|
131
|
-
const worktreePath = getWorktreePath(gitRoot, name);
|
|
132
|
-
try {
|
|
133
|
-
await fs.access(worktreePath);
|
|
134
|
-
return {
|
|
135
|
-
exists: true,
|
|
136
|
-
message: `Worktree '${name}' already exists`
|
|
137
|
-
};
|
|
138
|
-
} catch {
|
|
139
|
-
return {
|
|
140
|
-
exists: false,
|
|
141
|
-
path: worktreePath
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function validateWorktreeName(name) {
|
|
146
|
-
if (!name || name.trim() === "") {
|
|
147
|
-
return err(new Error("Phantom name cannot be empty"));
|
|
148
|
-
}
|
|
149
|
-
if (name.includes("/")) {
|
|
150
|
-
return err(new Error("Phantom name cannot contain slashes"));
|
|
151
|
-
}
|
|
152
|
-
if (name.startsWith(".")) {
|
|
153
|
-
return err(new Error("Phantom name cannot start with a dot"));
|
|
154
|
-
}
|
|
155
|
-
return ok(void 0);
|
|
156
|
-
}
|
|
157
|
-
|
|
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;
|
|
170
|
-
}
|
|
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";
|
|
176
|
-
}
|
|
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) {
|
|
194
|
-
return new Promise((resolve2) => {
|
|
195
|
-
const { command: command2, args: args2 = [], options = {} } = config;
|
|
196
|
-
const childProcess = nodeSpawn(command2, args2, {
|
|
197
|
-
stdio: "inherit",
|
|
198
|
-
...options
|
|
199
|
-
});
|
|
200
|
-
childProcess.on("error", (error) => {
|
|
201
|
-
resolve2(err(new ProcessSpawnError(command2, error.message)));
|
|
202
|
-
});
|
|
203
|
-
childProcess.on("exit", (code, signal) => {
|
|
204
|
-
if (signal) {
|
|
205
|
-
resolve2(err(new ProcessSignalError(signal)));
|
|
206
|
-
} else {
|
|
207
|
-
const exitCode = code ?? 0;
|
|
208
|
-
if (exitCode === 0) {
|
|
209
|
-
resolve2(ok({ exitCode }));
|
|
210
|
-
} else {
|
|
211
|
-
resolve2(err(new ProcessExecutionError(command2, exitCode)));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// src/core/process/exec.ts
|
|
219
|
-
async function execInWorktree(gitRoot, worktreeName, command2, options = {}) {
|
|
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
|
-
const stdio = options.interactive ? "inherit" : ["ignore", "inherit", "inherit"];
|
|
227
|
-
return spawnProcess({
|
|
228
|
-
command: cmd,
|
|
229
|
-
args: args2,
|
|
230
|
-
options: {
|
|
231
|
-
cwd: worktreePath,
|
|
232
|
-
stdio
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// src/core/process/shell.ts
|
|
238
|
-
async function shellInWorktree(gitRoot, worktreeName) {
|
|
239
|
-
const validation = await validateWorktreeExists(gitRoot, worktreeName);
|
|
240
|
-
if (!validation.exists) {
|
|
241
|
-
return err(new WorktreeNotFoundError(worktreeName));
|
|
242
|
-
}
|
|
243
|
-
const worktreePath = validation.path;
|
|
244
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
245
|
-
return spawnProcess({
|
|
246
|
-
command: shell,
|
|
247
|
-
args: [],
|
|
248
|
-
options: {
|
|
249
|
-
cwd: worktreePath,
|
|
250
|
-
env: {
|
|
251
|
-
...process.env,
|
|
252
|
-
PHANTOM: "1",
|
|
253
|
-
PHANTOM_NAME: worktreeName,
|
|
254
|
-
PHANTOM_PATH: worktreePath
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// src/core/worktree/attach.ts
|
|
261
|
-
import { existsSync } from "node:fs";
|
|
262
|
-
|
|
263
|
-
// src/core/git/libs/attach-worktree.ts
|
|
264
|
-
async function attachWorktree(gitRoot, worktreePath, branchName) {
|
|
265
|
-
try {
|
|
266
|
-
await executeGitCommand(["worktree", "add", worktreePath, branchName], {
|
|
267
|
-
cwd: gitRoot
|
|
268
|
-
});
|
|
269
|
-
return ok(void 0);
|
|
270
|
-
} catch (error) {
|
|
271
|
-
return err(
|
|
272
|
-
error instanceof Error ? error : new Error(`Failed to attach worktree: ${String(error)}`)
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// src/core/git/libs/branch-exists.ts
|
|
278
|
-
async function branchExists(gitRoot, branchName) {
|
|
279
|
-
try {
|
|
280
|
-
await executeGitCommand(
|
|
281
|
-
["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
|
|
282
|
-
{ cwd: gitRoot }
|
|
283
|
-
);
|
|
284
|
-
return ok(true);
|
|
285
|
-
} catch (error) {
|
|
286
|
-
if (error && typeof error === "object" && "code" in error) {
|
|
287
|
-
const execError = error;
|
|
288
|
-
if (execError.code === 1) {
|
|
289
|
-
return ok(false);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return err(
|
|
293
|
-
new Error(
|
|
294
|
-
`Failed to check branch existence: ${error instanceof Error ? error.message : String(error)}`
|
|
295
|
-
)
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// src/core/worktree/attach.ts
|
|
301
|
-
async function attachWorktreeCore(gitRoot, name) {
|
|
302
|
-
const validation = validateWorktreeName(name);
|
|
303
|
-
if (isErr(validation)) {
|
|
304
|
-
return validation;
|
|
305
|
-
}
|
|
306
|
-
const worktreePath = getWorktreePath(gitRoot, name);
|
|
307
|
-
if (existsSync(worktreePath)) {
|
|
308
|
-
return err(new WorktreeAlreadyExistsError(name));
|
|
309
|
-
}
|
|
310
|
-
const branchCheckResult = await branchExists(gitRoot, name);
|
|
311
|
-
if (isErr(branchCheckResult)) {
|
|
312
|
-
return err(branchCheckResult.error);
|
|
313
|
-
}
|
|
314
|
-
if (!branchCheckResult.value) {
|
|
315
|
-
return err(new BranchNotFoundError(name));
|
|
316
|
-
}
|
|
317
|
-
const attachResult = await attachWorktree(gitRoot, worktreePath, name);
|
|
318
|
-
if (isErr(attachResult)) {
|
|
319
|
-
return err(attachResult.error);
|
|
320
|
-
}
|
|
321
|
-
return ok(worktreePath);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// src/cli/output.ts
|
|
325
|
-
var output = {
|
|
326
|
-
log: (message) => {
|
|
327
|
-
console.log(message);
|
|
328
|
-
},
|
|
329
|
-
error: (message) => {
|
|
330
|
-
console.error(message);
|
|
331
|
-
},
|
|
332
|
-
warn: (message) => {
|
|
333
|
-
console.warn(message);
|
|
334
|
-
},
|
|
335
|
-
table: (data) => {
|
|
336
|
-
console.table(data);
|
|
337
|
-
},
|
|
338
|
-
processOutput: (proc) => {
|
|
339
|
-
proc.stdout?.pipe(process.stdout);
|
|
340
|
-
proc.stderr?.pipe(process.stderr);
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
// src/cli/errors.ts
|
|
345
|
-
var exitCodes = {
|
|
346
|
-
success: 0,
|
|
347
|
-
generalError: 1,
|
|
348
|
-
notFound: 2,
|
|
349
|
-
validationError: 3
|
|
350
|
-
};
|
|
351
|
-
function exitWithSuccess() {
|
|
352
|
-
process.exit(exitCodes.success);
|
|
353
|
-
}
|
|
354
|
-
function exitWithError(message, exitCode = exitCodes.generalError) {
|
|
355
|
-
output.error(message);
|
|
356
|
-
process.exit(exitCode);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// src/cli/handlers/attach.ts
|
|
360
|
-
async function attachHandler(args2) {
|
|
361
|
-
const { positionals, values } = parseArgs({
|
|
362
|
-
args: args2,
|
|
363
|
-
strict: true,
|
|
364
|
-
allowPositionals: true,
|
|
365
|
-
options: {
|
|
366
|
-
shell: {
|
|
367
|
-
type: "boolean",
|
|
368
|
-
short: "s"
|
|
369
|
-
},
|
|
370
|
-
exec: {
|
|
371
|
-
type: "string",
|
|
372
|
-
short: "e"
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
if (positionals.length === 0) {
|
|
377
|
-
exitWithError(
|
|
378
|
-
"Missing required argument: branch name",
|
|
379
|
-
exitCodes.validationError
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
const [branchName] = positionals;
|
|
383
|
-
if (values.shell && values.exec) {
|
|
384
|
-
exitWithError(
|
|
385
|
-
"Cannot use both --shell and --exec options",
|
|
386
|
-
exitCodes.validationError
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
const gitRoot = await getGitRoot();
|
|
390
|
-
const result = await attachWorktreeCore(gitRoot, branchName);
|
|
391
|
-
if (isErr(result)) {
|
|
392
|
-
const error = result.error;
|
|
393
|
-
if (error instanceof WorktreeAlreadyExistsError) {
|
|
394
|
-
exitWithError(error.message, exitCodes.validationError);
|
|
395
|
-
}
|
|
396
|
-
if (error instanceof BranchNotFoundError) {
|
|
397
|
-
exitWithError(error.message, exitCodes.notFound);
|
|
398
|
-
}
|
|
399
|
-
exitWithError(error.message, exitCodes.generalError);
|
|
400
|
-
}
|
|
401
|
-
const worktreePath = result.value;
|
|
402
|
-
output.log(`Attached phantom: ${branchName}`);
|
|
403
|
-
if (values.shell) {
|
|
404
|
-
const shellResult = await shellInWorktree(gitRoot, branchName);
|
|
405
|
-
if (isErr(shellResult)) {
|
|
406
|
-
exitWithError(shellResult.error.message, exitCodes.generalError);
|
|
407
|
-
}
|
|
408
|
-
} else if (values.exec) {
|
|
409
|
-
const execResult = await execInWorktree(
|
|
410
|
-
gitRoot,
|
|
411
|
-
branchName,
|
|
412
|
-
values.exec.split(" "),
|
|
413
|
-
{ interactive: true }
|
|
414
|
-
);
|
|
415
|
-
if (isErr(execResult)) {
|
|
416
|
-
exitWithError(execResult.error.message, exitCodes.generalError);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// src/cli/handlers/completion.ts
|
|
422
|
-
import { exit } from "node:process";
|
|
423
|
-
var FISH_COMPLETION_SCRIPT = `# Fish completion for phantom
|
|
424
|
-
# Place this in ~/.config/fish/completions/phantom.fish
|
|
425
|
-
|
|
426
|
-
function __phantom_list_worktrees
|
|
427
|
-
phantom list --names 2>/dev/null
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
function __phantom_using_command
|
|
431
|
-
set -l cmd (commandline -opc)
|
|
432
|
-
set -l cmd_count (count $cmd)
|
|
433
|
-
if test $cmd_count -eq 1
|
|
434
|
-
# No subcommand yet, so any command can be used
|
|
435
|
-
if test (count $argv) -eq 0
|
|
436
|
-
return 0
|
|
437
|
-
else
|
|
438
|
-
return 1
|
|
439
|
-
end
|
|
440
|
-
else if test $cmd_count -ge 2
|
|
441
|
-
# Check if we're in the context of a specific command
|
|
442
|
-
if test (count $argv) -gt 0 -a "$argv[1]" = "$cmd[2]"
|
|
443
|
-
return 0
|
|
444
|
-
end
|
|
445
|
-
end
|
|
446
|
-
return 1
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
# Disable file completion for phantom
|
|
450
|
-
complete -c phantom -f
|
|
451
|
-
|
|
452
|
-
# Main commands
|
|
453
|
-
complete -c phantom -n "__phantom_using_command" -a "create" -d "Create a new Git worktree (phantom)"
|
|
454
|
-
complete -c phantom -n "__phantom_using_command" -a "attach" -d "Attach to an existing branch by creating a new worktree"
|
|
455
|
-
complete -c phantom -n "__phantom_using_command" -a "list" -d "List all Git worktrees (phantoms)"
|
|
456
|
-
complete -c phantom -n "__phantom_using_command" -a "where" -d "Output the filesystem path of a specific worktree"
|
|
457
|
-
complete -c phantom -n "__phantom_using_command" -a "delete" -d "Delete a Git worktree (phantom)"
|
|
458
|
-
complete -c phantom -n "__phantom_using_command" -a "exec" -d "Execute a command in a worktree directory"
|
|
459
|
-
complete -c phantom -n "__phantom_using_command" -a "shell" -d "Open an interactive shell in a worktree directory"
|
|
460
|
-
complete -c phantom -n "__phantom_using_command" -a "version" -d "Display phantom version information"
|
|
461
|
-
complete -c phantom -n "__phantom_using_command" -a "completion" -d "Generate shell completion scripts"
|
|
462
|
-
|
|
463
|
-
# Global options
|
|
464
|
-
complete -c phantom -l help -d "Show help (-h)"
|
|
465
|
-
complete -c phantom -l version -d "Show version (-v)"
|
|
466
|
-
|
|
467
|
-
# create command options
|
|
468
|
-
complete -c phantom -n "__phantom_using_command create" -l shell -d "Open an interactive shell in the new worktree after creation (-s)"
|
|
469
|
-
complete -c phantom -n "__phantom_using_command create" -l exec -d "Execute a command in the new worktree after creation (-x)" -x
|
|
470
|
-
complete -c phantom -n "__phantom_using_command create" -l tmux -d "Open the worktree in a new tmux window (-t)"
|
|
471
|
-
complete -c phantom -n "__phantom_using_command create" -l tmux-vertical -d "Open the worktree in a vertical tmux pane"
|
|
472
|
-
complete -c phantom -n "__phantom_using_command create" -l tmux-horizontal -d "Open the worktree in a horizontal tmux pane"
|
|
473
|
-
complete -c phantom -n "__phantom_using_command create" -l copy-file -d "Copy specified files from the current worktree" -r
|
|
474
|
-
|
|
475
|
-
# attach command options
|
|
476
|
-
complete -c phantom -n "__phantom_using_command attach" -l shell -d "Open an interactive shell in the worktree after attaching (-s)"
|
|
477
|
-
complete -c phantom -n "__phantom_using_command attach" -l exec -d "Execute a command in the worktree after attaching (-x)" -x
|
|
478
|
-
|
|
479
|
-
# list command options
|
|
480
|
-
complete -c phantom -n "__phantom_using_command list" -l fzf -d "Use fzf for interactive selection"
|
|
481
|
-
complete -c phantom -n "__phantom_using_command list" -l names -d "Output only phantom names (for scripts and completion)"
|
|
482
|
-
|
|
483
|
-
# where command options
|
|
484
|
-
complete -c phantom -n "__phantom_using_command where" -l fzf -d "Use fzf for interactive selection"
|
|
485
|
-
complete -c phantom -n "__phantom_using_command where" -a "(__phantom_list_worktrees)"
|
|
486
|
-
|
|
487
|
-
# delete command options
|
|
488
|
-
complete -c phantom -n "__phantom_using_command delete" -l force -d "Force deletion even if worktree has uncommitted changes (-f)"
|
|
489
|
-
complete -c phantom -n "__phantom_using_command delete" -l current -d "Delete the current worktree"
|
|
490
|
-
complete -c phantom -n "__phantom_using_command delete" -l fzf -d "Use fzf for interactive selection"
|
|
491
|
-
complete -c phantom -n "__phantom_using_command delete" -a "(__phantom_list_worktrees)"
|
|
492
|
-
|
|
493
|
-
# exec command - accept worktree names and then any command
|
|
494
|
-
complete -c phantom -n "__phantom_using_command exec" -a "(__phantom_list_worktrees)"
|
|
495
|
-
|
|
496
|
-
# shell command options
|
|
497
|
-
complete -c phantom -n "__phantom_using_command shell" -l fzf -d "Use fzf for interactive selection"
|
|
498
|
-
complete -c phantom -n "__phantom_using_command shell" -a "(__phantom_list_worktrees)"
|
|
499
|
-
|
|
500
|
-
# completion command - shell names
|
|
501
|
-
complete -c phantom -n "__phantom_using_command completion" -a "fish zsh" -d "Shell type"`;
|
|
502
|
-
var ZSH_COMPLETION_SCRIPT = `#compdef phantom
|
|
503
|
-
# Zsh completion for phantom
|
|
504
|
-
# Place this in a directory in your $fpath (e.g., ~/.zsh/completions/)
|
|
505
|
-
# Or load dynamically with: eval "$(phantom completion zsh)"
|
|
506
|
-
|
|
507
|
-
# Only define the function, don't execute it
|
|
508
|
-
_phantom() {
|
|
509
|
-
local -a commands
|
|
510
|
-
commands=(
|
|
511
|
-
'create:Create a new Git worktree (phantom)'
|
|
512
|
-
'attach:Attach to an existing branch by creating a new worktree'
|
|
513
|
-
'list:List all Git worktrees (phantoms)'
|
|
514
|
-
'where:Output the filesystem path of a specific worktree'
|
|
515
|
-
'delete:Delete a Git worktree (phantom)'
|
|
516
|
-
'exec:Execute a command in a worktree directory'
|
|
517
|
-
'shell:Open an interactive shell in a worktree directory'
|
|
518
|
-
'version:Display phantom version information'
|
|
519
|
-
'completion:Generate shell completion scripts'
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
_arguments -C \\
|
|
523
|
-
'--help[Show help (-h)]' \\
|
|
524
|
-
'--version[Show version (-v)]' \\
|
|
525
|
-
'1:command:->command' \\
|
|
526
|
-
'*::arg:->args'
|
|
527
|
-
|
|
528
|
-
case \${state} in
|
|
529
|
-
command)
|
|
530
|
-
_describe 'phantom command' commands
|
|
531
|
-
;;
|
|
532
|
-
args)
|
|
533
|
-
case \${line[1]} in
|
|
534
|
-
create)
|
|
535
|
-
_arguments \\
|
|
536
|
-
'--shell[Open an interactive shell in the new worktree after creation (-s)]' \\
|
|
537
|
-
'--exec[Execute a command in the new worktree after creation (-x)]:command:' \\
|
|
538
|
-
'--tmux[Open the worktree in a new tmux window (-t)]' \\
|
|
539
|
-
'--tmux-vertical[Open the worktree in a vertical tmux pane]' \\
|
|
540
|
-
'--tmux-horizontal[Open the worktree in a horizontal tmux pane]' \\
|
|
541
|
-
'*--copy-file[Copy specified files from the current worktree]:file:_files' \\
|
|
542
|
-
'1:name:'
|
|
543
|
-
;;
|
|
544
|
-
attach)
|
|
545
|
-
_arguments \\
|
|
546
|
-
'--shell[Open an interactive shell in the worktree after attaching (-s)]' \\
|
|
547
|
-
'--exec[Execute a command in the worktree after attaching (-x)]:command:' \\
|
|
548
|
-
'1:worktree-name:' \\
|
|
549
|
-
'2:branch-name:'
|
|
550
|
-
;;
|
|
551
|
-
list)
|
|
552
|
-
_arguments \\
|
|
553
|
-
'--fzf[Use fzf for interactive selection]' \\
|
|
554
|
-
'--names[Output only phantom names (for scripts and completion)]'
|
|
555
|
-
;;
|
|
556
|
-
where|delete|shell)
|
|
557
|
-
local worktrees
|
|
558
|
-
worktrees=(\${(f)"$(phantom list --names 2>/dev/null)"})
|
|
559
|
-
if [[ \${line[1]} == "where" || \${line[1]} == "shell" ]]; then
|
|
560
|
-
_arguments \\
|
|
561
|
-
'--fzf[Use fzf for interactive selection]' \\
|
|
562
|
-
'1:worktree:(\${(q)worktrees[@]})'
|
|
563
|
-
elif [[ \${line[1]} == "delete" ]]; then
|
|
564
|
-
_arguments \\
|
|
565
|
-
'--force[Force deletion even if worktree has uncommitted changes (-f)]' \\
|
|
566
|
-
'--current[Delete the current worktree]' \\
|
|
567
|
-
'--fzf[Use fzf for interactive selection]' \\
|
|
568
|
-
'1:worktree:(\${(q)worktrees[@]})'
|
|
569
|
-
fi
|
|
570
|
-
;;
|
|
571
|
-
exec)
|
|
572
|
-
local worktrees
|
|
573
|
-
worktrees=(\${(f)"$(phantom list --names 2>/dev/null)"})
|
|
574
|
-
_arguments \\
|
|
575
|
-
'1:worktree:(\${(q)worktrees[@]})' \\
|
|
576
|
-
'*:command:_command_names'
|
|
577
|
-
;;
|
|
578
|
-
completion)
|
|
579
|
-
_arguments \\
|
|
580
|
-
'1:shell:(fish zsh)'
|
|
581
|
-
;;
|
|
582
|
-
esac
|
|
583
|
-
;;
|
|
584
|
-
esac
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
# Register the completion function if loading dynamically
|
|
588
|
-
if [[ -n \${ZSH_VERSION} ]]; then
|
|
589
|
-
autoload -Uz compinit && compinit -C
|
|
590
|
-
compdef _phantom phantom
|
|
591
|
-
fi`;
|
|
592
|
-
function completionHandler(args2) {
|
|
593
|
-
const shell = args2[0];
|
|
594
|
-
if (!shell) {
|
|
595
|
-
output.error("Usage: phantom completion <shell>");
|
|
596
|
-
output.error("Supported shells: fish, zsh");
|
|
597
|
-
exit(1);
|
|
598
|
-
}
|
|
599
|
-
switch (shell.toLowerCase()) {
|
|
600
|
-
case "fish":
|
|
601
|
-
console.log(FISH_COMPLETION_SCRIPT);
|
|
602
|
-
break;
|
|
603
|
-
case "zsh":
|
|
604
|
-
console.log(ZSH_COMPLETION_SCRIPT);
|
|
605
|
-
break;
|
|
606
|
-
default:
|
|
607
|
-
output.error(`Unsupported shell: ${shell}`);
|
|
608
|
-
output.error("Supported shells: fish, zsh");
|
|
609
|
-
exit(1);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// src/cli/handlers/create.ts
|
|
614
|
-
import { parseArgs as parseArgs2 } from "node:util";
|
|
615
|
-
|
|
616
|
-
// src/core/config/loader.ts
|
|
617
|
-
import fs2 from "node:fs/promises";
|
|
618
|
-
import path from "node:path";
|
|
619
|
-
|
|
620
|
-
// src/core/utils/type-guards.ts
|
|
621
|
-
function isObject(value) {
|
|
622
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// src/core/config/validate.ts
|
|
626
|
-
var ConfigValidationError = class extends Error {
|
|
627
|
-
constructor(message) {
|
|
628
|
-
super(`Invalid phantom.config.json: ${message}`);
|
|
629
|
-
this.name = "ConfigValidationError";
|
|
630
|
-
}
|
|
631
|
-
};
|
|
632
|
-
function validateConfig(config) {
|
|
633
|
-
if (!isObject(config)) {
|
|
634
|
-
return err(new ConfigValidationError("Configuration must be an object"));
|
|
635
|
-
}
|
|
636
|
-
const cfg = config;
|
|
637
|
-
if (cfg.postCreate !== void 0) {
|
|
638
|
-
if (!isObject(cfg.postCreate)) {
|
|
639
|
-
return err(new ConfigValidationError("postCreate must be an object"));
|
|
640
|
-
}
|
|
641
|
-
const postCreate = cfg.postCreate;
|
|
642
|
-
if (postCreate.copyFiles !== void 0) {
|
|
643
|
-
if (!Array.isArray(postCreate.copyFiles)) {
|
|
644
|
-
return err(
|
|
645
|
-
new ConfigValidationError("postCreate.copyFiles must be an array")
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
if (!postCreate.copyFiles.every((f) => typeof f === "string")) {
|
|
649
|
-
return err(
|
|
650
|
-
new ConfigValidationError(
|
|
651
|
-
"postCreate.copyFiles must contain only strings"
|
|
652
|
-
)
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (postCreate.commands !== void 0) {
|
|
657
|
-
if (!Array.isArray(postCreate.commands)) {
|
|
658
|
-
return err(
|
|
659
|
-
new ConfigValidationError("postCreate.commands must be an array")
|
|
660
|
-
);
|
|
661
|
-
}
|
|
662
|
-
if (!postCreate.commands.every((c) => typeof c === "string")) {
|
|
663
|
-
return err(
|
|
664
|
-
new ConfigValidationError(
|
|
665
|
-
"postCreate.commands must contain only strings"
|
|
666
|
-
)
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return ok(config);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// src/core/config/loader.ts
|
|
675
|
-
var ConfigNotFoundError = class extends Error {
|
|
676
|
-
constructor() {
|
|
677
|
-
super("phantom.config.json not found");
|
|
678
|
-
this.name = "ConfigNotFoundError";
|
|
679
|
-
}
|
|
680
|
-
};
|
|
681
|
-
var ConfigParseError = class extends Error {
|
|
682
|
-
constructor(message) {
|
|
683
|
-
super(`Failed to parse phantom.config.json: ${message}`);
|
|
684
|
-
this.name = "ConfigParseError";
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
async function loadConfig(gitRoot) {
|
|
688
|
-
const configPath = path.join(gitRoot, "phantom.config.json");
|
|
689
|
-
try {
|
|
690
|
-
const content = await fs2.readFile(configPath, "utf-8");
|
|
691
|
-
try {
|
|
692
|
-
const parsed = JSON.parse(content);
|
|
693
|
-
const validationResult = validateConfig(parsed);
|
|
694
|
-
if (!validationResult.ok) {
|
|
695
|
-
return err(validationResult.error);
|
|
696
|
-
}
|
|
697
|
-
return ok(validationResult.value);
|
|
698
|
-
} catch (error) {
|
|
699
|
-
return err(
|
|
700
|
-
new ConfigParseError(
|
|
701
|
-
error instanceof Error ? error.message : String(error)
|
|
702
|
-
)
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
} catch (error) {
|
|
706
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
707
|
-
return err(new ConfigNotFoundError());
|
|
708
|
-
}
|
|
709
|
-
throw error;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// src/core/process/tmux.ts
|
|
714
|
-
async function isInsideTmux() {
|
|
715
|
-
return process.env.TMUX !== void 0;
|
|
716
|
-
}
|
|
717
|
-
async function executeTmuxCommand(options) {
|
|
718
|
-
const { direction, command: command2, cwd, env } = options;
|
|
719
|
-
const tmuxArgs = [];
|
|
720
|
-
switch (direction) {
|
|
721
|
-
case "new":
|
|
722
|
-
tmuxArgs.push("new-window");
|
|
723
|
-
break;
|
|
724
|
-
case "vertical":
|
|
725
|
-
tmuxArgs.push("split-window", "-v");
|
|
726
|
-
break;
|
|
727
|
-
case "horizontal":
|
|
728
|
-
tmuxArgs.push("split-window", "-h");
|
|
729
|
-
break;
|
|
730
|
-
}
|
|
731
|
-
if (cwd) {
|
|
732
|
-
tmuxArgs.push("-c", cwd);
|
|
733
|
-
}
|
|
734
|
-
if (env) {
|
|
735
|
-
for (const [key, value] of Object.entries(env)) {
|
|
736
|
-
tmuxArgs.push("-e", `${key}=${value}`);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
tmuxArgs.push(command2);
|
|
740
|
-
const result = await spawnProcess({
|
|
741
|
-
command: "tmux",
|
|
742
|
-
args: tmuxArgs
|
|
743
|
-
});
|
|
744
|
-
return result;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// src/core/worktree/create.ts
|
|
748
|
-
import fs3 from "node:fs/promises";
|
|
749
|
-
|
|
750
|
-
// src/core/git/libs/add-worktree.ts
|
|
751
|
-
async function addWorktree(options) {
|
|
752
|
-
const { path: path3, branch, commitish = "HEAD" } = options;
|
|
753
|
-
await executeGitCommand(["worktree", "add", path3, "-b", branch, commitish]);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// src/core/worktree/file-copier.ts
|
|
757
|
-
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
758
|
-
import path2 from "node:path";
|
|
759
|
-
var FileCopyError = class extends Error {
|
|
760
|
-
file;
|
|
761
|
-
constructor(file, message) {
|
|
762
|
-
super(`Failed to copy ${file}: ${message}`);
|
|
763
|
-
this.name = "FileCopyError";
|
|
764
|
-
this.file = file;
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
async function copyFiles(sourceDir, targetDir, files) {
|
|
768
|
-
const copiedFiles = [];
|
|
769
|
-
const skippedFiles = [];
|
|
770
|
-
for (const file of files) {
|
|
771
|
-
const sourcePath = path2.join(sourceDir, file);
|
|
772
|
-
const targetPath = path2.join(targetDir, file);
|
|
773
|
-
try {
|
|
774
|
-
const stats = await stat(sourcePath);
|
|
775
|
-
if (!stats.isFile()) {
|
|
776
|
-
skippedFiles.push(file);
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
const targetDirPath = path2.dirname(targetPath);
|
|
780
|
-
await mkdir(targetDirPath, { recursive: true });
|
|
781
|
-
await copyFile(sourcePath, targetPath);
|
|
782
|
-
copiedFiles.push(file);
|
|
783
|
-
} catch (error) {
|
|
784
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
785
|
-
skippedFiles.push(file);
|
|
786
|
-
} else {
|
|
787
|
-
return err(
|
|
788
|
-
new FileCopyError(
|
|
789
|
-
file,
|
|
790
|
-
error instanceof Error ? error.message : String(error)
|
|
791
|
-
)
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
return ok({ copiedFiles, skippedFiles });
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// src/core/worktree/create.ts
|
|
800
|
-
async function createWorktree(gitRoot, name, options = {}) {
|
|
801
|
-
const nameValidation = validateWorktreeName(name);
|
|
802
|
-
if (isErr(nameValidation)) {
|
|
803
|
-
return nameValidation;
|
|
804
|
-
}
|
|
805
|
-
const { branch = name, commitish = "HEAD" } = options;
|
|
806
|
-
const worktreesPath = getPhantomDirectory(gitRoot);
|
|
807
|
-
const worktreePath = getWorktreePath(gitRoot, name);
|
|
808
|
-
try {
|
|
809
|
-
await fs3.access(worktreesPath);
|
|
810
|
-
} catch {
|
|
811
|
-
await fs3.mkdir(worktreesPath, { recursive: true });
|
|
812
|
-
}
|
|
813
|
-
const validation = await validateWorktreeDoesNotExist(gitRoot, name);
|
|
814
|
-
if (validation.exists) {
|
|
815
|
-
return err(new WorktreeAlreadyExistsError(name));
|
|
816
|
-
}
|
|
817
|
-
try {
|
|
818
|
-
await addWorktree({
|
|
819
|
-
path: worktreePath,
|
|
820
|
-
branch,
|
|
821
|
-
commitish
|
|
822
|
-
});
|
|
823
|
-
let copiedFiles;
|
|
824
|
-
let skippedFiles;
|
|
825
|
-
let copyError;
|
|
826
|
-
if (options.copyFiles && options.copyFiles.length > 0) {
|
|
827
|
-
const copyResult = await copyFiles(
|
|
828
|
-
gitRoot,
|
|
829
|
-
worktreePath,
|
|
830
|
-
options.copyFiles
|
|
831
|
-
);
|
|
832
|
-
if (isOk(copyResult)) {
|
|
833
|
-
copiedFiles = copyResult.value.copiedFiles;
|
|
834
|
-
skippedFiles = copyResult.value.skippedFiles;
|
|
835
|
-
} else {
|
|
836
|
-
copyError = copyResult.error.message;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
return ok({
|
|
840
|
-
message: `Created worktree '${name}' at ${worktreePath}`,
|
|
841
|
-
path: worktreePath,
|
|
842
|
-
copiedFiles,
|
|
843
|
-
skippedFiles,
|
|
844
|
-
copyError
|
|
845
|
-
});
|
|
846
|
-
} catch (error) {
|
|
847
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
848
|
-
return err(new GitOperationError("worktree add", errorMessage));
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// src/cli/handlers/create.ts
|
|
853
|
-
async function createHandler(args2) {
|
|
854
|
-
const { values, positionals } = parseArgs2({
|
|
855
|
-
args: args2,
|
|
856
|
-
options: {
|
|
857
|
-
shell: {
|
|
858
|
-
type: "boolean",
|
|
859
|
-
short: "s"
|
|
860
|
-
},
|
|
861
|
-
exec: {
|
|
862
|
-
type: "string",
|
|
863
|
-
short: "x"
|
|
864
|
-
},
|
|
865
|
-
tmux: {
|
|
866
|
-
type: "boolean",
|
|
867
|
-
short: "t"
|
|
868
|
-
},
|
|
869
|
-
"tmux-vertical": {
|
|
870
|
-
type: "boolean"
|
|
871
|
-
},
|
|
872
|
-
"tmux-v": {
|
|
873
|
-
type: "boolean"
|
|
874
|
-
},
|
|
875
|
-
"tmux-horizontal": {
|
|
876
|
-
type: "boolean"
|
|
877
|
-
},
|
|
878
|
-
"tmux-h": {
|
|
879
|
-
type: "boolean"
|
|
880
|
-
},
|
|
881
|
-
"copy-file": {
|
|
882
|
-
type: "string",
|
|
883
|
-
multiple: true
|
|
884
|
-
}
|
|
885
|
-
},
|
|
886
|
-
strict: true,
|
|
887
|
-
allowPositionals: true
|
|
888
|
-
});
|
|
889
|
-
if (positionals.length === 0) {
|
|
890
|
-
exitWithError(
|
|
891
|
-
"Please provide a name for the new worktree",
|
|
892
|
-
exitCodes.validationError
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
const worktreeName = positionals[0];
|
|
896
|
-
const openShell = values.shell ?? false;
|
|
897
|
-
const execCommand = values.exec;
|
|
898
|
-
const copyFileOptions = values["copy-file"];
|
|
899
|
-
const tmuxOption = values.tmux || values["tmux-vertical"] || values["tmux-v"] || values["tmux-horizontal"] || values["tmux-h"];
|
|
900
|
-
let tmuxDirection;
|
|
901
|
-
if (values.tmux) {
|
|
902
|
-
tmuxDirection = "new";
|
|
903
|
-
} else if (values["tmux-vertical"] || values["tmux-v"]) {
|
|
904
|
-
tmuxDirection = "vertical";
|
|
905
|
-
} else if (values["tmux-horizontal"] || values["tmux-h"]) {
|
|
906
|
-
tmuxDirection = "horizontal";
|
|
907
|
-
}
|
|
908
|
-
if ([openShell, execCommand !== void 0, tmuxOption].filter(Boolean).length > 1) {
|
|
909
|
-
exitWithError(
|
|
910
|
-
"Cannot use --shell, --exec, and --tmux options together",
|
|
911
|
-
exitCodes.validationError
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
if (tmuxOption && !await isInsideTmux()) {
|
|
915
|
-
exitWithError(
|
|
916
|
-
"The --tmux option can only be used inside a tmux session",
|
|
917
|
-
exitCodes.validationError
|
|
918
|
-
);
|
|
919
|
-
}
|
|
920
|
-
try {
|
|
921
|
-
const gitRoot = await getGitRoot();
|
|
922
|
-
let filesToCopy = [];
|
|
923
|
-
const configResult = await loadConfig(gitRoot);
|
|
924
|
-
if (isOk(configResult)) {
|
|
925
|
-
if (configResult.value.postCreate?.copyFiles) {
|
|
926
|
-
filesToCopy = [...configResult.value.postCreate.copyFiles];
|
|
927
|
-
}
|
|
928
|
-
} else {
|
|
929
|
-
if (configResult.error instanceof ConfigValidationError) {
|
|
930
|
-
output.warn(`Configuration warning: ${configResult.error.message}`);
|
|
931
|
-
} else if (configResult.error instanceof ConfigParseError) {
|
|
932
|
-
output.warn(`Configuration warning: ${configResult.error.message}`);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
if (copyFileOptions && copyFileOptions.length > 0) {
|
|
936
|
-
const cliFiles = Array.isArray(copyFileOptions) ? copyFileOptions : [copyFileOptions];
|
|
937
|
-
filesToCopy = [.../* @__PURE__ */ new Set([...filesToCopy, ...cliFiles])];
|
|
938
|
-
}
|
|
939
|
-
const result = await createWorktree(gitRoot, worktreeName, {
|
|
940
|
-
copyFiles: filesToCopy.length > 0 ? filesToCopy : void 0
|
|
941
|
-
});
|
|
942
|
-
if (isErr(result)) {
|
|
943
|
-
const exitCode = result.error instanceof WorktreeAlreadyExistsError ? exitCodes.validationError : exitCodes.generalError;
|
|
944
|
-
exitWithError(result.error.message, exitCode);
|
|
945
|
-
}
|
|
946
|
-
output.log(result.value.message);
|
|
947
|
-
if (result.value.copyError) {
|
|
948
|
-
output.error(
|
|
949
|
-
`
|
|
950
|
-
Warning: Failed to copy some files: ${result.value.copyError}`
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
if (isOk(configResult) && configResult.value.postCreate?.commands) {
|
|
954
|
-
const commands2 = configResult.value.postCreate.commands;
|
|
955
|
-
output.log("\nRunning post-create commands...");
|
|
956
|
-
for (const command2 of commands2) {
|
|
957
|
-
output.log(`Executing: ${command2}`);
|
|
958
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
959
|
-
const cmdResult = await execInWorktree(gitRoot, worktreeName, [
|
|
960
|
-
shell,
|
|
961
|
-
"-c",
|
|
962
|
-
command2
|
|
963
|
-
]);
|
|
964
|
-
if (isErr(cmdResult)) {
|
|
965
|
-
output.error(`Failed to execute command: ${cmdResult.error.message}`);
|
|
966
|
-
const exitCode = "exitCode" in cmdResult.error ? cmdResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
|
|
967
|
-
exitWithError(`Post-create command failed: ${command2}`, exitCode);
|
|
968
|
-
}
|
|
969
|
-
if (cmdResult.value.exitCode !== 0) {
|
|
970
|
-
exitWithError(
|
|
971
|
-
`Post-create command failed: ${command2}`,
|
|
972
|
-
cmdResult.value.exitCode
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
if (execCommand && isOk(result)) {
|
|
978
|
-
output.log(
|
|
979
|
-
`
|
|
980
|
-
Executing command in worktree '${worktreeName}': ${execCommand}`
|
|
981
|
-
);
|
|
982
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
983
|
-
const execResult = await execInWorktree(
|
|
984
|
-
gitRoot,
|
|
985
|
-
worktreeName,
|
|
986
|
-
[shell, "-c", execCommand],
|
|
987
|
-
{ interactive: true }
|
|
988
|
-
);
|
|
989
|
-
if (isErr(execResult)) {
|
|
990
|
-
output.error(execResult.error.message);
|
|
991
|
-
const exitCode = "exitCode" in execResult.error ? execResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
|
|
992
|
-
exitWithError("", exitCode);
|
|
993
|
-
}
|
|
994
|
-
process.exit(execResult.value.exitCode ?? 0);
|
|
995
|
-
}
|
|
996
|
-
if (openShell && isOk(result)) {
|
|
997
|
-
output.log(
|
|
998
|
-
`
|
|
999
|
-
Entering worktree '${worktreeName}' at ${result.value.path}`
|
|
1000
|
-
);
|
|
1001
|
-
output.log("Type 'exit' to return to your original directory\n");
|
|
1002
|
-
const shellResult = await shellInWorktree(gitRoot, worktreeName);
|
|
1003
|
-
if (isErr(shellResult)) {
|
|
1004
|
-
output.error(shellResult.error.message);
|
|
1005
|
-
const exitCode = "exitCode" in shellResult.error ? shellResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
|
|
1006
|
-
exitWithError("", exitCode);
|
|
1007
|
-
}
|
|
1008
|
-
process.exit(shellResult.value.exitCode ?? 0);
|
|
1009
|
-
}
|
|
1010
|
-
if (tmuxDirection && isOk(result)) {
|
|
1011
|
-
output.log(
|
|
1012
|
-
`
|
|
1013
|
-
Opening worktree '${worktreeName}' in tmux ${tmuxDirection === "new" ? "window" : "pane"}...`
|
|
1014
|
-
);
|
|
1015
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
1016
|
-
const tmuxResult = await executeTmuxCommand({
|
|
1017
|
-
direction: tmuxDirection,
|
|
1018
|
-
command: shell,
|
|
1019
|
-
cwd: result.value.path,
|
|
1020
|
-
env: {
|
|
1021
|
-
PHANTOM: "1",
|
|
1022
|
-
PHANTOM_NAME: worktreeName,
|
|
1023
|
-
PHANTOM_PATH: result.value.path
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1026
|
-
if (isErr(tmuxResult)) {
|
|
1027
|
-
output.error(tmuxResult.error.message);
|
|
1028
|
-
const exitCode = "exitCode" in tmuxResult.error ? tmuxResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
|
|
1029
|
-
exitWithError("", exitCode);
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
exitWithSuccess();
|
|
1033
|
-
} catch (error) {
|
|
1034
|
-
exitWithError(
|
|
1035
|
-
error instanceof Error ? error.message : String(error),
|
|
1036
|
-
exitCodes.generalError
|
|
1037
|
-
);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// src/cli/handlers/delete.ts
|
|
1042
|
-
import { parseArgs as parseArgs3 } from "node:util";
|
|
1043
|
-
|
|
1044
|
-
// src/core/git/libs/list-worktrees.ts
|
|
1045
|
-
async function listWorktrees(gitRoot) {
|
|
1046
|
-
const { stdout: stdout2 } = await executeGitCommand([
|
|
1047
|
-
"worktree",
|
|
1048
|
-
"list",
|
|
1049
|
-
"--porcelain"
|
|
1050
|
-
]);
|
|
1051
|
-
const worktrees = [];
|
|
1052
|
-
let currentWorktree = {};
|
|
1053
|
-
const lines = stdout2.split("\n").filter((line) => line.length > 0);
|
|
1054
|
-
for (const line of lines) {
|
|
1055
|
-
if (line.startsWith("worktree ")) {
|
|
1056
|
-
if (currentWorktree.path) {
|
|
1057
|
-
worktrees.push(currentWorktree);
|
|
1058
|
-
}
|
|
1059
|
-
currentWorktree = {
|
|
1060
|
-
path: line.substring("worktree ".length),
|
|
1061
|
-
isLocked: false,
|
|
1062
|
-
isPrunable: false
|
|
1063
|
-
};
|
|
1064
|
-
} else if (line.startsWith("HEAD ")) {
|
|
1065
|
-
currentWorktree.head = line.substring("HEAD ".length);
|
|
1066
|
-
} else if (line.startsWith("branch ")) {
|
|
1067
|
-
const fullBranch = line.substring("branch ".length);
|
|
1068
|
-
currentWorktree.branch = fullBranch.startsWith("refs/heads/") ? fullBranch.substring("refs/heads/".length) : fullBranch;
|
|
1069
|
-
} else if (line === "detached") {
|
|
1070
|
-
currentWorktree.branch = "(detached HEAD)";
|
|
1071
|
-
} else if (line === "locked") {
|
|
1072
|
-
currentWorktree.isLocked = true;
|
|
1073
|
-
} else if (line === "prunable") {
|
|
1074
|
-
currentWorktree.isPrunable = true;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
if (currentWorktree.path) {
|
|
1078
|
-
worktrees.push(currentWorktree);
|
|
1079
|
-
}
|
|
1080
|
-
return worktrees;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// src/core/git/libs/get-current-worktree.ts
|
|
1084
|
-
async function getCurrentWorktree(gitRoot) {
|
|
1085
|
-
try {
|
|
1086
|
-
const { stdout: currentPath } = await executeGitCommand([
|
|
1087
|
-
"rev-parse",
|
|
1088
|
-
"--show-toplevel"
|
|
1089
|
-
]);
|
|
1090
|
-
const currentPathTrimmed = currentPath.trim();
|
|
1091
|
-
const worktrees = await listWorktrees(gitRoot);
|
|
1092
|
-
const currentWorktree = worktrees.find(
|
|
1093
|
-
(wt) => wt.path === currentPathTrimmed
|
|
1094
|
-
);
|
|
1095
|
-
if (!currentWorktree || currentWorktree.path === gitRoot) {
|
|
1096
|
-
return null;
|
|
1097
|
-
}
|
|
1098
|
-
return currentWorktree.branch;
|
|
1099
|
-
} catch {
|
|
1100
|
-
return null;
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// src/core/worktree/delete.ts
|
|
1105
|
-
async function getWorktreeStatus(worktreePath) {
|
|
1106
|
-
try {
|
|
1107
|
-
const { stdout: stdout2 } = await executeGitCommandInDirectory(worktreePath, [
|
|
1108
|
-
"status",
|
|
1109
|
-
"--porcelain"
|
|
1110
|
-
]);
|
|
1111
|
-
if (stdout2) {
|
|
1112
|
-
return {
|
|
1113
|
-
hasUncommittedChanges: true,
|
|
1114
|
-
changedFiles: stdout2.split("\n").length
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
} catch {
|
|
1118
|
-
}
|
|
1119
|
-
return {
|
|
1120
|
-
hasUncommittedChanges: false,
|
|
1121
|
-
changedFiles: 0
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
async function removeWorktree(gitRoot, worktreePath, force = false) {
|
|
1125
|
-
try {
|
|
1126
|
-
await executeGitCommand(["worktree", "remove", worktreePath], {
|
|
1127
|
-
cwd: gitRoot
|
|
1128
|
-
});
|
|
1129
|
-
} catch (error) {
|
|
1130
|
-
try {
|
|
1131
|
-
await executeGitCommand(["worktree", "remove", "--force", worktreePath], {
|
|
1132
|
-
cwd: gitRoot
|
|
1133
|
-
});
|
|
1134
|
-
} catch {
|
|
1135
|
-
throw new Error("Failed to remove worktree");
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
async function deleteBranch(gitRoot, branchName) {
|
|
1140
|
-
try {
|
|
1141
|
-
await executeGitCommand(["branch", "-D", branchName], { cwd: gitRoot });
|
|
1142
|
-
return ok(true);
|
|
1143
|
-
} catch (error) {
|
|
1144
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1145
|
-
return err(new GitOperationError("branch delete", errorMessage));
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
async function deleteWorktree(gitRoot, name, options = {}) {
|
|
1149
|
-
const { force = false } = options;
|
|
1150
|
-
const validation = await validateWorktreeExists(gitRoot, name);
|
|
1151
|
-
if (!validation.exists) {
|
|
1152
|
-
return err(new WorktreeNotFoundError(name));
|
|
1153
|
-
}
|
|
1154
|
-
const worktreePath = validation.path;
|
|
1155
|
-
const status = await getWorktreeStatus(worktreePath);
|
|
1156
|
-
if (status.hasUncommittedChanges && !force) {
|
|
1157
|
-
return err(
|
|
1158
|
-
new WorktreeError(
|
|
1159
|
-
`Worktree '${name}' has uncommitted changes (${status.changedFiles} files). Use --force to delete anyway.`
|
|
1160
|
-
)
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
|
-
try {
|
|
1164
|
-
await removeWorktree(gitRoot, worktreePath, force);
|
|
1165
|
-
const branchName = name;
|
|
1166
|
-
const branchResult = await deleteBranch(gitRoot, branchName);
|
|
1167
|
-
let message;
|
|
1168
|
-
if (isOk(branchResult)) {
|
|
1169
|
-
message = `Deleted worktree '${name}' and its branch '${branchName}'`;
|
|
1170
|
-
} else {
|
|
1171
|
-
message = `Deleted worktree '${name}'`;
|
|
1172
|
-
message += `
|
|
1173
|
-
Note: Branch '${branchName}' could not be deleted: ${branchResult.error.message}`;
|
|
1174
|
-
}
|
|
1175
|
-
if (status.hasUncommittedChanges) {
|
|
1176
|
-
message = `Warning: Worktree '${name}' had uncommitted changes (${status.changedFiles} files)
|
|
1177
|
-
${message}`;
|
|
1178
|
-
}
|
|
1179
|
-
return ok({
|
|
1180
|
-
message,
|
|
1181
|
-
hasUncommittedChanges: status.hasUncommittedChanges,
|
|
1182
|
-
changedFiles: status.hasUncommittedChanges ? status.changedFiles : void 0
|
|
1183
|
-
});
|
|
1184
|
-
} catch (error) {
|
|
1185
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1186
|
-
return err(new GitOperationError("worktree remove", errorMessage));
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// src/core/utils/fzf.ts
|
|
1191
|
-
import { spawn } from "node:child_process";
|
|
1192
|
-
async function selectWithFzf(items, options = {}) {
|
|
1193
|
-
return new Promise((resolve2) => {
|
|
1194
|
-
const args2 = [];
|
|
1195
|
-
if (options.prompt) {
|
|
1196
|
-
args2.push("--prompt", options.prompt);
|
|
1197
|
-
}
|
|
1198
|
-
if (options.header) {
|
|
1199
|
-
args2.push("--header", options.header);
|
|
1200
|
-
}
|
|
1201
|
-
if (options.previewCommand) {
|
|
1202
|
-
args2.push("--preview", options.previewCommand);
|
|
1203
|
-
}
|
|
1204
|
-
const fzf = spawn("fzf", args2, {
|
|
1205
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1206
|
-
});
|
|
1207
|
-
let result = "";
|
|
1208
|
-
let errorOutput = "";
|
|
1209
|
-
fzf.stdout.on("data", (data) => {
|
|
1210
|
-
result += data.toString();
|
|
1211
|
-
});
|
|
1212
|
-
if (fzf.stderr) {
|
|
1213
|
-
fzf.stderr.on("data", (data) => {
|
|
1214
|
-
errorOutput += data.toString();
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1217
|
-
fzf.on("error", (error) => {
|
|
1218
|
-
if (error.message.includes("ENOENT")) {
|
|
1219
|
-
resolve2(
|
|
1220
|
-
err(new Error("fzf command not found. Please install fzf first."))
|
|
1221
|
-
);
|
|
1222
|
-
} else {
|
|
1223
|
-
resolve2(err(error));
|
|
1224
|
-
}
|
|
1225
|
-
});
|
|
1226
|
-
fzf.on("close", (code) => {
|
|
1227
|
-
if (code === 0) {
|
|
1228
|
-
const selected = result.trim();
|
|
1229
|
-
resolve2(ok(selected || null));
|
|
1230
|
-
} else if (code === 1) {
|
|
1231
|
-
resolve2(ok(null));
|
|
1232
|
-
} else if (code === 130) {
|
|
1233
|
-
resolve2(ok(null));
|
|
1234
|
-
} else {
|
|
1235
|
-
resolve2(err(new Error(`fzf exited with code ${code}: ${errorOutput}`)));
|
|
1236
|
-
}
|
|
1237
|
-
});
|
|
1238
|
-
fzf.stdin.write(items.join("\n"));
|
|
1239
|
-
fzf.stdin.end();
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
// src/core/worktree/list.ts
|
|
1244
|
-
async function getWorktreeStatus2(worktreePath) {
|
|
1245
|
-
try {
|
|
1246
|
-
const { stdout: stdout2 } = await executeGitCommandInDirectory(worktreePath, [
|
|
1247
|
-
"status",
|
|
1248
|
-
"--porcelain"
|
|
1249
|
-
]);
|
|
1250
|
-
return !stdout2;
|
|
1251
|
-
} catch {
|
|
1252
|
-
return true;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
async function listWorktrees2(gitRoot) {
|
|
1256
|
-
try {
|
|
1257
|
-
const gitWorktrees = await listWorktrees(gitRoot);
|
|
1258
|
-
const phantomDir = getPhantomDirectory(gitRoot);
|
|
1259
|
-
const phantomWorktrees = gitWorktrees.filter(
|
|
1260
|
-
(worktree) => worktree.path.startsWith(phantomDir)
|
|
1261
|
-
);
|
|
1262
|
-
if (phantomWorktrees.length === 0) {
|
|
1263
|
-
return ok({
|
|
1264
|
-
worktrees: [],
|
|
1265
|
-
message: "No worktrees found"
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1268
|
-
const worktrees = await Promise.all(
|
|
1269
|
-
phantomWorktrees.map(async (gitWorktree) => {
|
|
1270
|
-
const name = gitWorktree.path.substring(phantomDir.length + 1);
|
|
1271
|
-
const isClean = await getWorktreeStatus2(gitWorktree.path);
|
|
1272
|
-
return {
|
|
1273
|
-
name,
|
|
1274
|
-
path: gitWorktree.path,
|
|
1275
|
-
branch: gitWorktree.branch || "(detached HEAD)",
|
|
1276
|
-
isClean
|
|
1277
|
-
};
|
|
1278
|
-
})
|
|
1279
|
-
);
|
|
1280
|
-
return ok({
|
|
1281
|
-
worktrees
|
|
1282
|
-
});
|
|
1283
|
-
} catch (error) {
|
|
1284
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1285
|
-
throw new Error(`Failed to list worktrees: ${errorMessage}`);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// src/core/worktree/select.ts
|
|
1290
|
-
async function selectWorktreeWithFzf(gitRoot) {
|
|
1291
|
-
const listResult = await listWorktrees2(gitRoot);
|
|
1292
|
-
if (isErr(listResult)) {
|
|
1293
|
-
return listResult;
|
|
1294
|
-
}
|
|
1295
|
-
const { worktrees } = listResult.value;
|
|
1296
|
-
if (worktrees.length === 0) {
|
|
1297
|
-
return {
|
|
1298
|
-
ok: true,
|
|
1299
|
-
value: null
|
|
1300
|
-
};
|
|
1301
|
-
}
|
|
1302
|
-
const list = worktrees.map((wt) => {
|
|
1303
|
-
const branchInfo = wt.branch ? `(${wt.branch})` : "";
|
|
1304
|
-
const status = !wt.isClean ? " [dirty]" : "";
|
|
1305
|
-
return `${wt.name} ${branchInfo}${status}`;
|
|
1306
|
-
});
|
|
1307
|
-
const fzfResult = await selectWithFzf(list, {
|
|
1308
|
-
prompt: "Select worktree> ",
|
|
1309
|
-
header: "Git Worktrees (Phantoms)"
|
|
1310
|
-
});
|
|
1311
|
-
if (isErr(fzfResult)) {
|
|
1312
|
-
return fzfResult;
|
|
1313
|
-
}
|
|
1314
|
-
if (!fzfResult.value) {
|
|
1315
|
-
return {
|
|
1316
|
-
ok: true,
|
|
1317
|
-
value: null
|
|
1318
|
-
};
|
|
1319
|
-
}
|
|
1320
|
-
const selectedName = fzfResult.value.split(" ")[0];
|
|
1321
|
-
const selectedWorktree = worktrees.find((wt) => wt.name === selectedName);
|
|
1322
|
-
if (!selectedWorktree) {
|
|
1323
|
-
return {
|
|
1324
|
-
ok: false,
|
|
1325
|
-
error: new Error("Selected worktree not found")
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
return {
|
|
1329
|
-
ok: true,
|
|
1330
|
-
value: {
|
|
1331
|
-
name: selectedWorktree.name,
|
|
1332
|
-
branch: selectedWorktree.branch,
|
|
1333
|
-
isClean: selectedWorktree.isClean
|
|
1334
|
-
}
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// src/cli/handlers/delete.ts
|
|
1339
|
-
async function deleteHandler(args2) {
|
|
1340
|
-
const { values, positionals } = parseArgs3({
|
|
1341
|
-
args: args2,
|
|
1342
|
-
options: {
|
|
1343
|
-
force: {
|
|
1344
|
-
type: "boolean",
|
|
1345
|
-
short: "f"
|
|
1346
|
-
},
|
|
1347
|
-
current: {
|
|
1348
|
-
type: "boolean"
|
|
1349
|
-
},
|
|
1350
|
-
fzf: {
|
|
1351
|
-
type: "boolean",
|
|
1352
|
-
default: false
|
|
1353
|
-
}
|
|
1354
|
-
},
|
|
1355
|
-
strict: true,
|
|
1356
|
-
allowPositionals: true
|
|
1357
|
-
});
|
|
1358
|
-
const deleteCurrent = values.current ?? false;
|
|
1359
|
-
const useFzf = values.fzf ?? false;
|
|
1360
|
-
if (positionals.length === 0 && !deleteCurrent && !useFzf) {
|
|
1361
|
-
exitWithError(
|
|
1362
|
-
"Please provide a worktree name to delete, use --current to delete the current worktree, or use --fzf for interactive selection",
|
|
1363
|
-
exitCodes.validationError
|
|
1364
|
-
);
|
|
1365
|
-
}
|
|
1366
|
-
if ((positionals.length > 0 || useFzf) && deleteCurrent) {
|
|
1367
|
-
exitWithError(
|
|
1368
|
-
"Cannot specify --current with a worktree name or --fzf option",
|
|
1369
|
-
exitCodes.validationError
|
|
1370
|
-
);
|
|
1371
|
-
}
|
|
1372
|
-
if (positionals.length > 0 && useFzf) {
|
|
1373
|
-
exitWithError(
|
|
1374
|
-
"Cannot specify both a worktree name and --fzf option",
|
|
1375
|
-
exitCodes.validationError
|
|
1376
|
-
);
|
|
1377
|
-
}
|
|
1378
|
-
const forceDelete = values.force ?? false;
|
|
1379
|
-
try {
|
|
1380
|
-
const gitRoot = await getGitRoot();
|
|
1381
|
-
let worktreeName;
|
|
1382
|
-
if (deleteCurrent) {
|
|
1383
|
-
const currentWorktree = await getCurrentWorktree(gitRoot);
|
|
1384
|
-
if (!currentWorktree) {
|
|
1385
|
-
exitWithError(
|
|
1386
|
-
"Not in a worktree directory. The --current option can only be used from within a worktree.",
|
|
1387
|
-
exitCodes.validationError
|
|
1388
|
-
);
|
|
1389
|
-
}
|
|
1390
|
-
worktreeName = currentWorktree;
|
|
1391
|
-
} else if (useFzf) {
|
|
1392
|
-
const selectResult = await selectWorktreeWithFzf(gitRoot);
|
|
1393
|
-
if (isErr(selectResult)) {
|
|
1394
|
-
exitWithError(selectResult.error.message, exitCodes.generalError);
|
|
1395
|
-
}
|
|
1396
|
-
if (!selectResult.value) {
|
|
1397
|
-
exitWithSuccess();
|
|
1398
|
-
}
|
|
1399
|
-
worktreeName = selectResult.value.name;
|
|
1400
|
-
} else {
|
|
1401
|
-
worktreeName = positionals[0];
|
|
1402
|
-
}
|
|
1403
|
-
const result = await deleteWorktree(gitRoot, worktreeName, {
|
|
1404
|
-
force: forceDelete
|
|
1405
|
-
});
|
|
1406
|
-
if (isErr(result)) {
|
|
1407
|
-
const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.validationError : result.error instanceof WorktreeError && result.error.message.includes("uncommitted changes") ? exitCodes.validationError : exitCodes.generalError;
|
|
1408
|
-
exitWithError(result.error.message, exitCode);
|
|
1409
|
-
}
|
|
1410
|
-
output.log(result.value.message);
|
|
1411
|
-
exitWithSuccess();
|
|
1412
|
-
} catch (error) {
|
|
1413
|
-
exitWithError(
|
|
1414
|
-
error instanceof Error ? error.message : String(error),
|
|
1415
|
-
exitCodes.generalError
|
|
1416
|
-
);
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
// src/cli/handlers/exec.ts
|
|
1421
|
-
import { parseArgs as parseArgs4 } from "node:util";
|
|
1422
|
-
async function execHandler(args2) {
|
|
1423
|
-
const { positionals } = parseArgs4({
|
|
1424
|
-
args: args2,
|
|
1425
|
-
options: {},
|
|
1426
|
-
strict: true,
|
|
1427
|
-
allowPositionals: true
|
|
1428
|
-
});
|
|
1429
|
-
if (positionals.length < 2) {
|
|
1430
|
-
exitWithError(
|
|
1431
|
-
"Usage: phantom exec <worktree-name> <command> [args...]",
|
|
1432
|
-
exitCodes.validationError
|
|
1433
|
-
);
|
|
1434
|
-
}
|
|
1435
|
-
const [worktreeName, ...commandArgs] = positionals;
|
|
1436
|
-
try {
|
|
1437
|
-
const gitRoot = await getGitRoot();
|
|
1438
|
-
const result = await execInWorktree(
|
|
1439
|
-
gitRoot,
|
|
1440
|
-
worktreeName,
|
|
1441
|
-
commandArgs,
|
|
1442
|
-
{ interactive: true }
|
|
1443
|
-
);
|
|
1444
|
-
if (isErr(result)) {
|
|
1445
|
-
const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.notFound : result.error.exitCode || exitCodes.generalError;
|
|
1446
|
-
exitWithError(result.error.message, exitCode);
|
|
1447
|
-
}
|
|
1448
|
-
process.exit(result.value.exitCode);
|
|
1449
|
-
} catch (error) {
|
|
1450
|
-
exitWithError(
|
|
1451
|
-
error instanceof Error ? error.message : String(error),
|
|
1452
|
-
exitCodes.generalError
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
// src/cli/handlers/list.ts
|
|
1458
|
-
import { parseArgs as parseArgs5 } from "node:util";
|
|
1459
|
-
async function listHandler(args2 = []) {
|
|
1460
|
-
const { values } = parseArgs5({
|
|
1461
|
-
args: args2,
|
|
1462
|
-
options: {
|
|
1463
|
-
fzf: {
|
|
1464
|
-
type: "boolean",
|
|
1465
|
-
default: false
|
|
1466
|
-
},
|
|
1467
|
-
names: {
|
|
1468
|
-
type: "boolean",
|
|
1469
|
-
default: false
|
|
1470
|
-
}
|
|
1471
|
-
},
|
|
1472
|
-
strict: true,
|
|
1473
|
-
allowPositionals: false
|
|
1474
|
-
});
|
|
1475
|
-
try {
|
|
1476
|
-
const gitRoot = await getGitRoot();
|
|
1477
|
-
if (values.fzf) {
|
|
1478
|
-
const selectResult = await selectWorktreeWithFzf(gitRoot);
|
|
1479
|
-
if (isErr(selectResult)) {
|
|
1480
|
-
exitWithError(selectResult.error.message, exitCodes.generalError);
|
|
1481
|
-
}
|
|
1482
|
-
if (selectResult.value) {
|
|
1483
|
-
output.log(selectResult.value.name);
|
|
1484
|
-
}
|
|
1485
|
-
} else {
|
|
1486
|
-
const result = await listWorktrees2(gitRoot);
|
|
1487
|
-
if (isErr(result)) {
|
|
1488
|
-
exitWithError("Failed to list worktrees", exitCodes.generalError);
|
|
1489
|
-
}
|
|
1490
|
-
const { worktrees, message } = result.value;
|
|
1491
|
-
if (worktrees.length === 0) {
|
|
1492
|
-
if (!values.names) {
|
|
1493
|
-
output.log(message || "No worktrees found.");
|
|
1494
|
-
}
|
|
1495
|
-
process.exit(exitCodes.success);
|
|
1496
|
-
}
|
|
1497
|
-
if (values.names) {
|
|
1498
|
-
for (const worktree of worktrees) {
|
|
1499
|
-
output.log(worktree.name);
|
|
1500
|
-
}
|
|
1501
|
-
} else {
|
|
1502
|
-
const maxNameLength = Math.max(
|
|
1503
|
-
...worktrees.map((wt) => wt.name.length)
|
|
1504
|
-
);
|
|
1505
|
-
for (const worktree of worktrees) {
|
|
1506
|
-
const paddedName = worktree.name.padEnd(maxNameLength + 2);
|
|
1507
|
-
const branchInfo = worktree.branch ? `(${worktree.branch})` : "";
|
|
1508
|
-
const status = !worktree.isClean ? " [dirty]" : "";
|
|
1509
|
-
output.log(`${paddedName} ${branchInfo}${status}`);
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
process.exit(exitCodes.success);
|
|
1514
|
-
} catch (error) {
|
|
1515
|
-
exitWithError(
|
|
1516
|
-
error instanceof Error ? error.message : String(error),
|
|
1517
|
-
exitCodes.generalError
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
// src/cli/handlers/shell.ts
|
|
1523
|
-
import { parseArgs as parseArgs6 } from "node:util";
|
|
1524
|
-
async function shellHandler(args2) {
|
|
1525
|
-
const { positionals, values } = parseArgs6({
|
|
1526
|
-
args: args2,
|
|
1527
|
-
options: {
|
|
1528
|
-
fzf: {
|
|
1529
|
-
type: "boolean",
|
|
1530
|
-
default: false
|
|
1531
|
-
}
|
|
1532
|
-
},
|
|
1533
|
-
strict: true,
|
|
1534
|
-
allowPositionals: true
|
|
1535
|
-
});
|
|
1536
|
-
const useFzf = values.fzf ?? false;
|
|
1537
|
-
if (positionals.length === 0 && !useFzf) {
|
|
1538
|
-
exitWithError(
|
|
1539
|
-
"Usage: phantom shell <worktree-name> or phantom shell --fzf",
|
|
1540
|
-
exitCodes.validationError
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1543
|
-
if (positionals.length > 0 && useFzf) {
|
|
1544
|
-
exitWithError(
|
|
1545
|
-
"Cannot specify both a worktree name and --fzf option",
|
|
1546
|
-
exitCodes.validationError
|
|
1547
|
-
);
|
|
1548
|
-
}
|
|
1549
|
-
let worktreeName;
|
|
1550
|
-
try {
|
|
1551
|
-
const gitRoot = await getGitRoot();
|
|
1552
|
-
if (useFzf) {
|
|
1553
|
-
const selectResult = await selectWorktreeWithFzf(gitRoot);
|
|
1554
|
-
if (isErr(selectResult)) {
|
|
1555
|
-
exitWithError(selectResult.error.message, exitCodes.generalError);
|
|
1556
|
-
}
|
|
1557
|
-
if (!selectResult.value) {
|
|
1558
|
-
exitWithSuccess();
|
|
1559
|
-
}
|
|
1560
|
-
worktreeName = selectResult.value.name;
|
|
1561
|
-
} else {
|
|
1562
|
-
worktreeName = positionals[0];
|
|
1563
|
-
}
|
|
1564
|
-
const validation = await validateWorktreeExists(gitRoot, worktreeName);
|
|
1565
|
-
if (!validation.exists) {
|
|
1566
|
-
exitWithError(
|
|
1567
|
-
validation.message || `Worktree '${worktreeName}' not found`,
|
|
1568
|
-
exitCodes.generalError
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
1571
|
-
output.log(`Entering worktree '${worktreeName}' at ${validation.path}`);
|
|
1572
|
-
output.log("Type 'exit' to return to your original directory\n");
|
|
1573
|
-
const result = await shellInWorktree(gitRoot, worktreeName);
|
|
1574
|
-
if (isErr(result)) {
|
|
1575
|
-
const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.notFound : result.error.exitCode || exitCodes.generalError;
|
|
1576
|
-
exitWithError(result.error.message, exitCode);
|
|
1577
|
-
}
|
|
1578
|
-
process.exit(result.value.exitCode);
|
|
1579
|
-
} catch (error) {
|
|
1580
|
-
exitWithError(
|
|
1581
|
-
error instanceof Error ? error.message : String(error),
|
|
1582
|
-
exitCodes.generalError
|
|
1583
|
-
);
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
// src/cli/handlers/version.ts
|
|
1588
|
-
import { parseArgs as parseArgs7 } from "node:util";
|
|
1589
|
-
|
|
1590
|
-
// package.json
|
|
1591
|
-
var package_default = {
|
|
1592
|
-
name: "@aku11i/phantom",
|
|
1593
|
-
packageManager: "pnpm@10.8.1",
|
|
1594
|
-
version: "0.9.0",
|
|
1595
|
-
description: "A powerful CLI tool for managing Git worktrees for parallel development",
|
|
1596
|
-
keywords: [
|
|
1597
|
-
"git",
|
|
1598
|
-
"worktree",
|
|
1599
|
-
"cli",
|
|
1600
|
-
"phantom",
|
|
1601
|
-
"workspace",
|
|
1602
|
-
"development",
|
|
1603
|
-
"parallel"
|
|
1604
|
-
],
|
|
1605
|
-
homepage: "https://github.com/aku11i/phantom#readme",
|
|
1606
|
-
bugs: {
|
|
1607
|
-
url: "https://github.com/aku11i/phantom/issues"
|
|
1608
|
-
},
|
|
1609
|
-
repository: {
|
|
1610
|
-
type: "git",
|
|
1611
|
-
url: "git+https://github.com/aku11i/phantom.git"
|
|
1612
|
-
},
|
|
1613
|
-
license: "MIT",
|
|
1614
|
-
author: "aku11i",
|
|
1615
|
-
type: "module",
|
|
1616
|
-
bin: {
|
|
1617
|
-
phantom: "./dist/phantom.js"
|
|
1618
|
-
},
|
|
1619
|
-
scripts: {
|
|
1620
|
-
start: "node ./src/bin/phantom.ts",
|
|
1621
|
-
phantom: "node ./src/bin/phantom.ts",
|
|
1622
|
-
build: "node build.ts",
|
|
1623
|
-
typecheck: "tsgo --noEmit",
|
|
1624
|
-
test: 'node --test --experimental-strip-types --experimental-test-module-mocks "src/**/*.test.js"',
|
|
1625
|
-
"test:coverage": 'node --experimental-test-coverage --test --experimental-strip-types --experimental-test-module-mocks "src/**/*.test.js"',
|
|
1626
|
-
"test:file": "node --test --experimental-strip-types --experimental-test-module-mocks",
|
|
1627
|
-
lint: "biome check .",
|
|
1628
|
-
fix: "biome check --write .",
|
|
1629
|
-
ready: "pnpm fix && pnpm typecheck && pnpm test",
|
|
1630
|
-
"ready:check": "pnpm lint && pnpm typecheck && pnpm test",
|
|
1631
|
-
prepublishOnly: "pnpm ready:check && pnpm build"
|
|
1632
|
-
},
|
|
1633
|
-
engines: {
|
|
1634
|
-
node: ">=22.0.0"
|
|
1635
|
-
},
|
|
1636
|
-
files: [
|
|
1637
|
-
"dist/",
|
|
1638
|
-
"README.md",
|
|
1639
|
-
"LICENSE"
|
|
1640
|
-
],
|
|
1641
|
-
devDependencies: {
|
|
1642
|
-
"@biomejs/biome": "^1.9.4",
|
|
1643
|
-
"@types/node": "^22.15.29",
|
|
1644
|
-
"@typescript/native-preview": "7.0.0-dev.20250602.1",
|
|
1645
|
-
esbuild: "^0.25.5",
|
|
1646
|
-
typescript: "^5.8.3"
|
|
1647
|
-
}
|
|
1648
|
-
};
|
|
1649
|
-
|
|
1650
|
-
// src/core/version.ts
|
|
1651
|
-
function getVersion() {
|
|
1652
|
-
return package_default.version;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// src/cli/handlers/version.ts
|
|
1656
|
-
function versionHandler(args2 = []) {
|
|
1657
|
-
parseArgs7({
|
|
1658
|
-
args: args2,
|
|
1659
|
-
options: {},
|
|
1660
|
-
strict: true,
|
|
1661
|
-
allowPositionals: false
|
|
1662
|
-
});
|
|
1663
|
-
const version = getVersion();
|
|
1664
|
-
output.log(`Phantom v${version}`);
|
|
1665
|
-
exitWithSuccess();
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
// src/cli/handlers/where.ts
|
|
1669
|
-
import { parseArgs as parseArgs8 } from "node:util";
|
|
1670
|
-
|
|
1671
|
-
// src/core/worktree/where.ts
|
|
1672
|
-
async function whereWorktree(gitRoot, name) {
|
|
1673
|
-
const validation = await validateWorktreeExists(gitRoot, name);
|
|
1674
|
-
if (!validation.exists) {
|
|
1675
|
-
return err(new WorktreeNotFoundError(name));
|
|
1676
|
-
}
|
|
1677
|
-
return ok({
|
|
1678
|
-
path: validation.path
|
|
1679
|
-
});
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
// src/cli/handlers/where.ts
|
|
1683
|
-
async function whereHandler(args2) {
|
|
1684
|
-
const { positionals, values } = parseArgs8({
|
|
1685
|
-
args: args2,
|
|
1686
|
-
options: {
|
|
1687
|
-
fzf: {
|
|
1688
|
-
type: "boolean",
|
|
1689
|
-
default: false
|
|
1690
|
-
}
|
|
1691
|
-
},
|
|
1692
|
-
strict: true,
|
|
1693
|
-
allowPositionals: true
|
|
1694
|
-
});
|
|
1695
|
-
const useFzf = values.fzf ?? false;
|
|
1696
|
-
if (positionals.length === 0 && !useFzf) {
|
|
1697
|
-
exitWithError(
|
|
1698
|
-
"Usage: phantom where <worktree-name> or phantom where --fzf",
|
|
1699
|
-
exitCodes.validationError
|
|
1700
|
-
);
|
|
1701
|
-
}
|
|
1702
|
-
if (positionals.length > 0 && useFzf) {
|
|
1703
|
-
exitWithError(
|
|
1704
|
-
"Cannot specify both a worktree name and --fzf option",
|
|
1705
|
-
exitCodes.validationError
|
|
1706
|
-
);
|
|
1707
|
-
}
|
|
1708
|
-
let worktreeName;
|
|
1709
|
-
let gitRoot;
|
|
1710
|
-
try {
|
|
1711
|
-
gitRoot = await getGitRoot();
|
|
1712
|
-
} catch (error) {
|
|
1713
|
-
exitWithError(
|
|
1714
|
-
error instanceof Error ? error.message : String(error),
|
|
1715
|
-
exitCodes.generalError
|
|
1716
|
-
);
|
|
1717
|
-
}
|
|
1718
|
-
if (useFzf) {
|
|
1719
|
-
const selectResult = await selectWorktreeWithFzf(gitRoot);
|
|
1720
|
-
if (isErr(selectResult)) {
|
|
1721
|
-
exitWithError(selectResult.error.message, exitCodes.generalError);
|
|
1722
|
-
}
|
|
1723
|
-
if (!selectResult.value) {
|
|
1724
|
-
exitWithSuccess();
|
|
1725
|
-
}
|
|
1726
|
-
worktreeName = selectResult.value.name;
|
|
1727
|
-
} else {
|
|
1728
|
-
worktreeName = positionals[0];
|
|
1729
|
-
}
|
|
1730
|
-
const result = await whereWorktree(gitRoot, worktreeName);
|
|
1731
|
-
if (isErr(result)) {
|
|
1732
|
-
exitWithError(result.error.message, exitCodes.notFound);
|
|
1733
|
-
}
|
|
1734
|
-
output.log(result.value.path);
|
|
1735
|
-
exitWithSuccess();
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// src/cli/help.ts
|
|
1739
|
-
import { stdout } from "node:process";
|
|
1740
|
-
var HelpFormatter = class {
|
|
1741
|
-
width;
|
|
1742
|
-
indent = " ";
|
|
1743
|
-
constructor() {
|
|
1744
|
-
this.width = stdout.columns || 80;
|
|
1745
|
-
}
|
|
1746
|
-
formatMainHelp(commands2) {
|
|
1747
|
-
const lines = [];
|
|
1748
|
-
lines.push(this.bold("Phantom - Git Worktree Manager"));
|
|
1749
|
-
lines.push("");
|
|
1750
|
-
lines.push(
|
|
1751
|
-
this.dim(
|
|
1752
|
-
"A CLI tool for managing Git worktrees with enhanced functionality"
|
|
1753
|
-
)
|
|
1754
|
-
);
|
|
1755
|
-
lines.push("");
|
|
1756
|
-
lines.push(this.section("USAGE"));
|
|
1757
|
-
lines.push(`${this.indent}phantom <command> [options]`);
|
|
1758
|
-
lines.push("");
|
|
1759
|
-
lines.push(this.section("COMMANDS"));
|
|
1760
|
-
const maxNameLength = Math.max(...commands2.map((cmd) => cmd.name.length));
|
|
1761
|
-
for (const cmd of commands2) {
|
|
1762
|
-
const paddedName = cmd.name.padEnd(maxNameLength + 2);
|
|
1763
|
-
lines.push(`${this.indent}${this.cyan(paddedName)}${cmd.description}`);
|
|
1764
|
-
}
|
|
1765
|
-
lines.push("");
|
|
1766
|
-
lines.push(this.section("GLOBAL OPTIONS"));
|
|
1767
|
-
const helpOption = "-h, --help";
|
|
1768
|
-
const versionOption = "-v, --version";
|
|
1769
|
-
const globalOptionWidth = Math.max(helpOption.length, versionOption.length) + 2;
|
|
1770
|
-
lines.push(
|
|
1771
|
-
`${this.indent}${this.cyan(helpOption.padEnd(globalOptionWidth))}Show help`
|
|
1772
|
-
);
|
|
1773
|
-
lines.push(
|
|
1774
|
-
`${this.indent}${this.cyan(versionOption.padEnd(globalOptionWidth))}Show version`
|
|
1775
|
-
);
|
|
1776
|
-
lines.push("");
|
|
1777
|
-
lines.push(
|
|
1778
|
-
this.dim(
|
|
1779
|
-
"Run 'phantom <command> --help' for more information on a command."
|
|
1780
|
-
)
|
|
1781
|
-
);
|
|
1782
|
-
return lines.join("\n");
|
|
1783
|
-
}
|
|
1784
|
-
formatCommandHelp(help) {
|
|
1785
|
-
const lines = [];
|
|
1786
|
-
lines.push(this.bold(`phantom ${help.name}`));
|
|
1787
|
-
lines.push(this.dim(help.description));
|
|
1788
|
-
lines.push("");
|
|
1789
|
-
lines.push(this.section("USAGE"));
|
|
1790
|
-
lines.push(`${this.indent}${help.usage}`);
|
|
1791
|
-
lines.push("");
|
|
1792
|
-
if (help.options && help.options.length > 0) {
|
|
1793
|
-
lines.push(this.section("OPTIONS"));
|
|
1794
|
-
const maxOptionLength = Math.max(
|
|
1795
|
-
...help.options.map((opt) => this.formatOptionName(opt).length)
|
|
1796
|
-
);
|
|
1797
|
-
for (const option of help.options) {
|
|
1798
|
-
const optionName = this.formatOptionName(option);
|
|
1799
|
-
const paddedName = optionName.padEnd(maxOptionLength + 2);
|
|
1800
|
-
const description = this.wrapText(
|
|
1801
|
-
option.description,
|
|
1802
|
-
maxOptionLength + 4
|
|
1803
|
-
);
|
|
1804
|
-
lines.push(`${this.indent}${this.cyan(paddedName)}${description[0]}`);
|
|
1805
|
-
for (let i = 1; i < description.length; i++) {
|
|
1806
|
-
lines.push(
|
|
1807
|
-
`${this.indent}${" ".repeat(maxOptionLength + 2)}${description[i]}`
|
|
1808
|
-
);
|
|
1809
|
-
}
|
|
1810
|
-
if (option.example) {
|
|
1811
|
-
const exampleIndent = " ".repeat(maxOptionLength + 4);
|
|
1812
|
-
lines.push(
|
|
1813
|
-
`${this.indent}${exampleIndent}${this.dim(`Example: ${option.example}`)}`
|
|
1814
|
-
);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
lines.push("");
|
|
1818
|
-
}
|
|
1819
|
-
if (help.examples && help.examples.length > 0) {
|
|
1820
|
-
lines.push(this.section("EXAMPLES"));
|
|
1821
|
-
for (const example of help.examples) {
|
|
1822
|
-
lines.push(`${this.indent}${this.dim(example.description)}`);
|
|
1823
|
-
lines.push(`${this.indent}${this.indent}$ ${example.command}`);
|
|
1824
|
-
lines.push("");
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
if (help.notes && help.notes.length > 0) {
|
|
1828
|
-
lines.push(this.section("NOTES"));
|
|
1829
|
-
for (const note of help.notes) {
|
|
1830
|
-
const wrappedNote = this.wrapText(note, 2);
|
|
1831
|
-
for (const line of wrappedNote) {
|
|
1832
|
-
lines.push(`${this.indent}${line}`);
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
lines.push("");
|
|
1836
|
-
}
|
|
1837
|
-
return lines.join("\n");
|
|
1838
|
-
}
|
|
1839
|
-
formatOptionName(option) {
|
|
1840
|
-
const parts = [];
|
|
1841
|
-
if (option.short) {
|
|
1842
|
-
parts.push(`-${option.short},`);
|
|
1843
|
-
}
|
|
1844
|
-
parts.push(`--${option.name}`);
|
|
1845
|
-
if (option.type === "string") {
|
|
1846
|
-
parts.push(option.multiple ? "<value>..." : "<value>");
|
|
1847
|
-
}
|
|
1848
|
-
return parts.join(" ");
|
|
1849
|
-
}
|
|
1850
|
-
wrapText(text, indent) {
|
|
1851
|
-
const maxWidth = this.width - indent - 2;
|
|
1852
|
-
const words = text.split(" ");
|
|
1853
|
-
const lines = [];
|
|
1854
|
-
let currentLine = "";
|
|
1855
|
-
for (const word of words) {
|
|
1856
|
-
if (currentLine.length + word.length + 1 > maxWidth) {
|
|
1857
|
-
lines.push(currentLine);
|
|
1858
|
-
currentLine = word;
|
|
1859
|
-
} else {
|
|
1860
|
-
currentLine = currentLine ? `${currentLine} ${word}` : word;
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
if (currentLine) {
|
|
1864
|
-
lines.push(currentLine);
|
|
1865
|
-
}
|
|
1866
|
-
return lines;
|
|
1867
|
-
}
|
|
1868
|
-
section(text) {
|
|
1869
|
-
return this.bold(text);
|
|
1870
|
-
}
|
|
1871
|
-
bold(text) {
|
|
1872
|
-
return `\x1B[1m${text}\x1B[0m`;
|
|
1873
|
-
}
|
|
1874
|
-
dim(text) {
|
|
1875
|
-
return `\x1B[2m${text}\x1B[0m`;
|
|
1876
|
-
}
|
|
1877
|
-
cyan(text) {
|
|
1878
|
-
return `\x1B[36m${text}\x1B[0m`;
|
|
1879
|
-
}
|
|
1880
|
-
};
|
|
1881
|
-
var helpFormatter = new HelpFormatter();
|
|
1882
|
-
|
|
1883
|
-
// src/cli/help/attach.ts
|
|
1884
|
-
var attachHelp = {
|
|
1885
|
-
name: "attach",
|
|
1886
|
-
description: "Attach to an existing branch by creating a new worktree",
|
|
1887
|
-
usage: "phantom attach <worktree-name> <branch-name> [options]",
|
|
1888
|
-
options: [
|
|
1889
|
-
{
|
|
1890
|
-
name: "shell",
|
|
1891
|
-
short: "s",
|
|
1892
|
-
type: "boolean",
|
|
1893
|
-
description: "Open an interactive shell in the worktree after attaching"
|
|
1894
|
-
},
|
|
1895
|
-
{
|
|
1896
|
-
name: "exec",
|
|
1897
|
-
short: "x",
|
|
1898
|
-
type: "string",
|
|
1899
|
-
description: "Execute a command in the worktree after attaching",
|
|
1900
|
-
example: "--exec 'git pull'"
|
|
1901
|
-
}
|
|
1902
|
-
],
|
|
1903
|
-
examples: [
|
|
1904
|
-
{
|
|
1905
|
-
description: "Attach to an existing branch",
|
|
1906
|
-
command: "phantom attach review-pr main"
|
|
1907
|
-
},
|
|
1908
|
-
{
|
|
1909
|
-
description: "Attach to a remote branch and open a shell",
|
|
1910
|
-
command: "phantom attach hotfix origin/hotfix-v1.2 --shell"
|
|
1911
|
-
},
|
|
1912
|
-
{
|
|
1913
|
-
description: "Attach to a branch and pull latest changes",
|
|
1914
|
-
command: "phantom attach staging origin/staging --exec 'git pull'"
|
|
1915
|
-
}
|
|
1916
|
-
],
|
|
1917
|
-
notes: [
|
|
1918
|
-
"The branch must already exist (locally or remotely)",
|
|
1919
|
-
"If attaching to a remote branch, it will be checked out locally",
|
|
1920
|
-
"Only one of --shell or --exec options can be used at a time"
|
|
1921
|
-
]
|
|
1922
|
-
};
|
|
1923
|
-
|
|
1924
|
-
// src/cli/help/completion.ts
|
|
1925
|
-
var completionHelp = {
|
|
1926
|
-
name: "completion",
|
|
1927
|
-
usage: "phantom completion <shell>",
|
|
1928
|
-
description: "Generate shell completion scripts for fish or zsh",
|
|
1929
|
-
examples: [
|
|
1930
|
-
{
|
|
1931
|
-
command: "phantom completion fish > ~/.config/fish/completions/phantom.fish",
|
|
1932
|
-
description: "Generate and install Fish completion"
|
|
1933
|
-
},
|
|
1934
|
-
{
|
|
1935
|
-
command: "phantom completion fish | source",
|
|
1936
|
-
description: "Load Fish completion in current session"
|
|
1937
|
-
},
|
|
1938
|
-
{
|
|
1939
|
-
command: "phantom completion zsh > ~/.zsh/completions/_phantom",
|
|
1940
|
-
description: "Generate and install Zsh completion"
|
|
1941
|
-
},
|
|
1942
|
-
{
|
|
1943
|
-
command: 'eval "$(phantom completion zsh)"',
|
|
1944
|
-
description: "Load Zsh completion in current session"
|
|
1945
|
-
}
|
|
1946
|
-
],
|
|
1947
|
-
notes: [
|
|
1948
|
-
"Supported shells: fish, zsh",
|
|
1949
|
-
"After installing completions, you may need to restart your shell or source the completion file",
|
|
1950
|
-
"For Fish: completions are loaded automatically from ~/.config/fish/completions/",
|
|
1951
|
-
"For Zsh: ensure the completion file is in a directory in your $fpath"
|
|
1952
|
-
]
|
|
1953
|
-
};
|
|
1954
|
-
|
|
1955
|
-
// src/cli/help/create.ts
|
|
1956
|
-
var createHelp = {
|
|
1957
|
-
name: "create",
|
|
1958
|
-
description: "Create a new Git worktree (phantom)",
|
|
1959
|
-
usage: "phantom create <name> [options]",
|
|
1960
|
-
options: [
|
|
1961
|
-
{
|
|
1962
|
-
name: "shell",
|
|
1963
|
-
short: "s",
|
|
1964
|
-
type: "boolean",
|
|
1965
|
-
description: "Open an interactive shell in the new worktree after creation"
|
|
1966
|
-
},
|
|
1967
|
-
{
|
|
1968
|
-
name: "exec",
|
|
1969
|
-
short: "x",
|
|
1970
|
-
type: "string",
|
|
1971
|
-
description: "Execute a command in the new worktree after creation",
|
|
1972
|
-
example: "--exec 'npm install'"
|
|
1973
|
-
},
|
|
1974
|
-
{
|
|
1975
|
-
name: "tmux",
|
|
1976
|
-
short: "t",
|
|
1977
|
-
type: "boolean",
|
|
1978
|
-
description: "Open the worktree in a new tmux window (requires being inside tmux)"
|
|
1979
|
-
},
|
|
1980
|
-
{
|
|
1981
|
-
name: "tmux-vertical",
|
|
1982
|
-
type: "boolean",
|
|
1983
|
-
description: "Open the worktree in a vertical tmux pane (requires being inside tmux)"
|
|
1984
|
-
},
|
|
1985
|
-
{
|
|
1986
|
-
name: "tmux-horizontal",
|
|
1987
|
-
type: "boolean",
|
|
1988
|
-
description: "Open the worktree in a horizontal tmux pane (requires being inside tmux)"
|
|
1989
|
-
},
|
|
1990
|
-
{
|
|
1991
|
-
name: "copy-file",
|
|
1992
|
-
type: "string",
|
|
1993
|
-
multiple: true,
|
|
1994
|
-
description: "Copy specified files from the current worktree to the new one. Can be used multiple times",
|
|
1995
|
-
example: "--copy-file .env --copy-file config.local.json"
|
|
1996
|
-
}
|
|
1997
|
-
],
|
|
1998
|
-
examples: [
|
|
1999
|
-
{
|
|
2000
|
-
description: "Create a new worktree named 'feature-auth'",
|
|
2001
|
-
command: "phantom create feature-auth"
|
|
2002
|
-
},
|
|
2003
|
-
{
|
|
2004
|
-
description: "Create a worktree and open a shell in it",
|
|
2005
|
-
command: "phantom create bugfix-123 --shell"
|
|
2006
|
-
},
|
|
2007
|
-
{
|
|
2008
|
-
description: "Create a worktree and run npm install",
|
|
2009
|
-
command: "phantom create new-feature --exec 'npm install'"
|
|
2010
|
-
},
|
|
2011
|
-
{
|
|
2012
|
-
description: "Create a worktree in a new tmux window",
|
|
2013
|
-
command: "phantom create experiment --tmux"
|
|
2014
|
-
},
|
|
2015
|
-
{
|
|
2016
|
-
description: "Create a worktree and copy environment files",
|
|
2017
|
-
command: "phantom create staging --copy-file .env --copy-file database.yml"
|
|
2018
|
-
}
|
|
2019
|
-
],
|
|
2020
|
-
notes: [
|
|
2021
|
-
"The worktree name will be used as the branch name",
|
|
2022
|
-
"Only one of --shell, --exec, or --tmux options can be used at a time",
|
|
2023
|
-
"File copying can also be configured in phantom.config.json"
|
|
2024
|
-
]
|
|
2025
|
-
};
|
|
2026
|
-
|
|
2027
|
-
// src/cli/help/delete.ts
|
|
2028
|
-
var deleteHelp = {
|
|
2029
|
-
name: "delete",
|
|
2030
|
-
description: "Delete a Git worktree (phantom)",
|
|
2031
|
-
usage: "phantom delete <name> [options]",
|
|
2032
|
-
options: [
|
|
2033
|
-
{
|
|
2034
|
-
name: "force",
|
|
2035
|
-
short: "f",
|
|
2036
|
-
type: "boolean",
|
|
2037
|
-
description: "Force deletion even if the worktree has uncommitted or unpushed changes"
|
|
2038
|
-
},
|
|
2039
|
-
{
|
|
2040
|
-
name: "--current",
|
|
2041
|
-
type: "boolean",
|
|
2042
|
-
description: "Delete the current worktree"
|
|
2043
|
-
},
|
|
2044
|
-
{
|
|
2045
|
-
name: "--fzf",
|
|
2046
|
-
type: "boolean",
|
|
2047
|
-
description: "Use fzf for interactive selection"
|
|
2048
|
-
}
|
|
2049
|
-
],
|
|
2050
|
-
examples: [
|
|
2051
|
-
{
|
|
2052
|
-
description: "Delete a worktree",
|
|
2053
|
-
command: "phantom delete feature-auth"
|
|
2054
|
-
},
|
|
2055
|
-
{
|
|
2056
|
-
description: "Force delete a worktree with uncommitted changes",
|
|
2057
|
-
command: "phantom delete experimental --force"
|
|
2058
|
-
},
|
|
2059
|
-
{
|
|
2060
|
-
description: "Delete the current worktree",
|
|
2061
|
-
command: "phantom delete --current"
|
|
2062
|
-
},
|
|
2063
|
-
{
|
|
2064
|
-
description: "Delete a worktree with interactive fzf selection",
|
|
2065
|
-
command: "phantom delete --fzf"
|
|
2066
|
-
}
|
|
2067
|
-
],
|
|
2068
|
-
notes: [
|
|
2069
|
-
"By default, deletion will fail if the worktree has uncommitted changes",
|
|
2070
|
-
"The associated branch will also be deleted if it's not checked out elsewhere",
|
|
2071
|
-
"With --fzf, you can interactively select the worktree to delete"
|
|
2072
|
-
]
|
|
2073
|
-
};
|
|
2074
|
-
|
|
2075
|
-
// src/cli/help/exec.ts
|
|
2076
|
-
var execHelp = {
|
|
2077
|
-
name: "exec",
|
|
2078
|
-
description: "Execute a command in a worktree directory",
|
|
2079
|
-
usage: "phantom exec <worktree-name> <command> [args...]",
|
|
2080
|
-
examples: [
|
|
2081
|
-
{
|
|
2082
|
-
description: "Run npm test in a worktree",
|
|
2083
|
-
command: "phantom exec feature-auth npm test"
|
|
2084
|
-
},
|
|
2085
|
-
{
|
|
2086
|
-
description: "Check git status in a worktree",
|
|
2087
|
-
command: "phantom exec bugfix-123 git status"
|
|
2088
|
-
},
|
|
2089
|
-
{
|
|
2090
|
-
description: "Run a complex command with arguments",
|
|
2091
|
-
command: "phantom exec staging npm run build -- --production"
|
|
2092
|
-
}
|
|
2093
|
-
],
|
|
2094
|
-
notes: [
|
|
2095
|
-
"The command is executed with the worktree directory as the working directory",
|
|
2096
|
-
"All arguments after the worktree name are passed to the command",
|
|
2097
|
-
"The exit code of the executed command is preserved"
|
|
2098
|
-
]
|
|
2099
|
-
};
|
|
2100
|
-
|
|
2101
|
-
// src/cli/help/list.ts
|
|
2102
|
-
var listHelp = {
|
|
2103
|
-
name: "list",
|
|
2104
|
-
description: "List all Git worktrees (phantoms)",
|
|
2105
|
-
usage: "phantom list [options]",
|
|
2106
|
-
options: [
|
|
2107
|
-
{
|
|
2108
|
-
name: "--fzf",
|
|
2109
|
-
type: "boolean",
|
|
2110
|
-
description: "Use fzf for interactive selection"
|
|
2111
|
-
},
|
|
2112
|
-
{
|
|
2113
|
-
name: "--names",
|
|
2114
|
-
type: "boolean",
|
|
2115
|
-
description: "Output only phantom names (for scripts and completion)"
|
|
2116
|
-
}
|
|
2117
|
-
],
|
|
2118
|
-
examples: [
|
|
2119
|
-
{
|
|
2120
|
-
description: "List all worktrees",
|
|
2121
|
-
command: "phantom list"
|
|
2122
|
-
},
|
|
2123
|
-
{
|
|
2124
|
-
description: "List worktrees with interactive fzf selection",
|
|
2125
|
-
command: "phantom list --fzf"
|
|
2126
|
-
},
|
|
2127
|
-
{
|
|
2128
|
-
description: "List only worktree names",
|
|
2129
|
-
command: "phantom list --names"
|
|
2130
|
-
}
|
|
2131
|
-
],
|
|
2132
|
-
notes: [
|
|
2133
|
-
"Shows all worktrees with their paths and associated branches",
|
|
2134
|
-
"The main worktree is marked as '(bare)' if using a bare repository",
|
|
2135
|
-
"With --fzf, outputs only the selected worktree name",
|
|
2136
|
-
"Use --names for shell completion scripts and automation"
|
|
2137
|
-
]
|
|
2138
|
-
};
|
|
2139
|
-
|
|
2140
|
-
// src/cli/help/shell.ts
|
|
2141
|
-
var shellHelp = {
|
|
2142
|
-
name: "shell",
|
|
2143
|
-
description: "Open an interactive shell in a worktree directory",
|
|
2144
|
-
usage: "phantom shell <worktree-name> [options]",
|
|
2145
|
-
options: [
|
|
2146
|
-
{
|
|
2147
|
-
name: "--fzf",
|
|
2148
|
-
type: "boolean",
|
|
2149
|
-
description: "Use fzf for interactive selection"
|
|
2150
|
-
}
|
|
2151
|
-
],
|
|
2152
|
-
examples: [
|
|
2153
|
-
{
|
|
2154
|
-
description: "Open a shell in a worktree",
|
|
2155
|
-
command: "phantom shell feature-auth"
|
|
2156
|
-
},
|
|
2157
|
-
{
|
|
2158
|
-
description: "Open a shell with interactive fzf selection",
|
|
2159
|
-
command: "phantom shell --fzf"
|
|
2160
|
-
}
|
|
2161
|
-
],
|
|
2162
|
-
notes: [
|
|
2163
|
-
"Uses your default shell from the SHELL environment variable",
|
|
2164
|
-
"The shell starts with the worktree directory as the working directory",
|
|
2165
|
-
"Type 'exit' to return to your original directory",
|
|
2166
|
-
"With --fzf, you can interactively select the worktree to enter"
|
|
2167
|
-
]
|
|
2168
|
-
};
|
|
2169
|
-
|
|
2170
|
-
// src/cli/help/version.ts
|
|
2171
|
-
var versionHelp = {
|
|
2172
|
-
name: "version",
|
|
2173
|
-
description: "Display phantom version information",
|
|
2174
|
-
usage: "phantom version",
|
|
2175
|
-
examples: [
|
|
2176
|
-
{
|
|
2177
|
-
description: "Show version",
|
|
2178
|
-
command: "phantom version"
|
|
2179
|
-
}
|
|
2180
|
-
],
|
|
2181
|
-
notes: ["Also accessible via 'phantom --version' or 'phantom -v'"]
|
|
2182
|
-
};
|
|
2183
|
-
|
|
2184
|
-
// src/cli/help/where.ts
|
|
2185
|
-
var whereHelp = {
|
|
2186
|
-
name: "where",
|
|
2187
|
-
description: "Output the filesystem path of a specific worktree",
|
|
2188
|
-
usage: "phantom where <worktree-name> [options]",
|
|
2189
|
-
options: [
|
|
2190
|
-
{
|
|
2191
|
-
name: "--fzf",
|
|
2192
|
-
type: "boolean",
|
|
2193
|
-
description: "Use fzf for interactive selection"
|
|
2194
|
-
}
|
|
2195
|
-
],
|
|
2196
|
-
examples: [
|
|
2197
|
-
{
|
|
2198
|
-
description: "Get the path of a worktree",
|
|
2199
|
-
command: "phantom where feature-auth"
|
|
2200
|
-
},
|
|
2201
|
-
{
|
|
2202
|
-
description: "Change directory to a worktree",
|
|
2203
|
-
command: "cd $(phantom where staging)"
|
|
2204
|
-
},
|
|
2205
|
-
{
|
|
2206
|
-
description: "Get path with interactive fzf selection",
|
|
2207
|
-
command: "phantom where --fzf"
|
|
2208
|
-
},
|
|
2209
|
-
{
|
|
2210
|
-
description: "Change directory using fzf selection",
|
|
2211
|
-
command: "cd $(phantom where --fzf)"
|
|
2212
|
-
}
|
|
2213
|
-
],
|
|
2214
|
-
notes: [
|
|
2215
|
-
"Outputs only the path, making it suitable for use in scripts",
|
|
2216
|
-
"Exits with an error code if the worktree doesn't exist",
|
|
2217
|
-
"With --fzf, you can interactively select the worktree"
|
|
2218
|
-
]
|
|
2219
|
-
};
|
|
2220
|
-
|
|
2221
|
-
// src/bin/phantom.ts
|
|
2222
|
-
var commands = [
|
|
2223
|
-
{
|
|
2224
|
-
name: "create",
|
|
2225
|
-
description: "Create a new Git worktree (phantom)",
|
|
2226
|
-
handler: createHandler,
|
|
2227
|
-
help: createHelp
|
|
2228
|
-
},
|
|
2229
|
-
{
|
|
2230
|
-
name: "attach",
|
|
2231
|
-
description: "Attach to an existing branch by creating a new worktree",
|
|
2232
|
-
handler: attachHandler,
|
|
2233
|
-
help: attachHelp
|
|
2234
|
-
},
|
|
2235
|
-
{
|
|
2236
|
-
name: "list",
|
|
2237
|
-
description: "List all Git worktrees (phantoms)",
|
|
2238
|
-
handler: listHandler,
|
|
2239
|
-
help: listHelp
|
|
2240
|
-
},
|
|
2241
|
-
{
|
|
2242
|
-
name: "where",
|
|
2243
|
-
description: "Output the filesystem path of a specific worktree",
|
|
2244
|
-
handler: whereHandler,
|
|
2245
|
-
help: whereHelp
|
|
2246
|
-
},
|
|
2247
|
-
{
|
|
2248
|
-
name: "delete",
|
|
2249
|
-
description: "Delete a Git worktree (phantom)",
|
|
2250
|
-
handler: deleteHandler,
|
|
2251
|
-
help: deleteHelp
|
|
2252
|
-
},
|
|
2253
|
-
{
|
|
2254
|
-
name: "exec",
|
|
2255
|
-
description: "Execute a command in a worktree directory",
|
|
2256
|
-
handler: execHandler,
|
|
2257
|
-
help: execHelp
|
|
2258
|
-
},
|
|
2259
|
-
{
|
|
2260
|
-
name: "shell",
|
|
2261
|
-
description: "Open an interactive shell in a worktree directory",
|
|
2262
|
-
handler: shellHandler,
|
|
2263
|
-
help: shellHelp
|
|
2264
|
-
},
|
|
2265
|
-
{
|
|
2266
|
-
name: "version",
|
|
2267
|
-
description: "Display phantom version information",
|
|
2268
|
-
handler: versionHandler,
|
|
2269
|
-
help: versionHelp
|
|
2270
|
-
},
|
|
2271
|
-
{
|
|
2272
|
-
name: "completion",
|
|
2273
|
-
description: "Generate shell completion scripts",
|
|
2274
|
-
handler: completionHandler,
|
|
2275
|
-
help: completionHelp
|
|
2276
|
-
}
|
|
2277
|
-
];
|
|
2278
|
-
function printHelp(commands2) {
|
|
2279
|
-
const simpleCommands = commands2.map((cmd) => ({
|
|
2280
|
-
name: cmd.name,
|
|
2281
|
-
description: cmd.description
|
|
2282
|
-
}));
|
|
2283
|
-
console.log(helpFormatter.formatMainHelp(simpleCommands));
|
|
2284
|
-
}
|
|
2285
|
-
function findCommand(args2, commands2) {
|
|
2286
|
-
if (args2.length === 0) {
|
|
2287
|
-
return { command: null, remainingArgs: [] };
|
|
2288
|
-
}
|
|
2289
|
-
const [cmdName, ...rest] = args2;
|
|
2290
|
-
const command2 = commands2.find((cmd) => cmd.name === cmdName);
|
|
2291
|
-
if (!command2) {
|
|
2292
|
-
return { command: null, remainingArgs: args2 };
|
|
2293
|
-
}
|
|
2294
|
-
if (command2.subcommands && rest.length > 0) {
|
|
2295
|
-
const { command: subcommand, remainingArgs: remainingArgs2 } = findCommand(
|
|
2296
|
-
rest,
|
|
2297
|
-
command2.subcommands
|
|
2298
|
-
);
|
|
2299
|
-
if (subcommand) {
|
|
2300
|
-
return { command: subcommand, remainingArgs: remainingArgs2 };
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
return { command: command2, remainingArgs: rest };
|
|
2304
|
-
}
|
|
2305
|
-
var args = argv.slice(2);
|
|
2306
|
-
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
|
|
2307
|
-
printHelp(commands);
|
|
2308
|
-
exit2(0);
|
|
2309
|
-
}
|
|
2310
|
-
if (args[0] === "--version" || args[0] === "-v") {
|
|
2311
|
-
versionHandler();
|
|
2312
|
-
exit2(0);
|
|
2313
|
-
}
|
|
2314
|
-
var { command, remainingArgs } = findCommand(args, commands);
|
|
2315
|
-
if (!command || !command.handler) {
|
|
2316
|
-
console.error(`Error: Unknown command '${args.join(" ")}'
|
|
2317
|
-
`);
|
|
2318
|
-
printHelp(commands);
|
|
2319
|
-
exit2(1);
|
|
2320
|
-
}
|
|
2321
|
-
if (remainingArgs.includes("--help") || remainingArgs.includes("-h")) {
|
|
2322
|
-
if (command.help) {
|
|
2323
|
-
console.log(helpFormatter.formatCommandHelp(command.help));
|
|
2324
|
-
} else {
|
|
2325
|
-
console.log(`Help not available for command '${command.name}'`);
|
|
2326
|
-
}
|
|
2327
|
-
exit2(0);
|
|
2328
|
-
}
|
|
2329
|
-
try {
|
|
2330
|
-
await command.handler(remainingArgs);
|
|
2331
|
-
} catch (error) {
|
|
2332
|
-
console.error(
|
|
2333
|
-
"Error:",
|
|
2334
|
-
error instanceof Error ? error.message : String(error)
|
|
2335
|
-
);
|
|
2336
|
-
exit2(1);
|
|
2337
|
-
}
|
|
2338
|
-
//# sourceMappingURL=phantom.js.map
|