@aku11i/phantom 0.3.0 → 0.5.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.ja.md +109 -25
- package/README.md +107 -23
- package/dist/garden.js +458 -0
- package/dist/garden.js.map +7 -0
- package/dist/phantom.js +826 -385
- package/dist/phantom.js.map +4 -4
- package/package.json +5 -4
package/dist/phantom.js
CHANGED
|
@@ -1,493 +1,877 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin/phantom.ts
|
|
4
|
-
import { argv, exit
|
|
4
|
+
import { argv, exit } from "node:process";
|
|
5
5
|
|
|
6
|
-
// src/
|
|
7
|
-
import {
|
|
8
|
-
import { join as join2 } from "node:path";
|
|
9
|
-
import { exit as exit3 } from "node:process";
|
|
6
|
+
// src/cli/handlers/attach.ts
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
10
8
|
|
|
11
|
-
// src/git/libs/
|
|
12
|
-
import {
|
|
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";
|
|
13
14
|
import { promisify } from "node:util";
|
|
14
|
-
var
|
|
15
|
-
async function
|
|
16
|
-
|
|
17
|
-
|
|
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], {});
|
|
18
43
|
}
|
|
19
44
|
|
|
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);
|
|
45
|
+
// src/core/git/libs/get-git-root.ts
|
|
24
46
|
async function getGitRoot() {
|
|
25
|
-
const { stdout } = await
|
|
26
|
-
|
|
47
|
+
const { stdout } = await executeGitCommand(["rev-parse", "--git-common-dir"]);
|
|
48
|
+
if (stdout.endsWith("/.git") || stdout === ".git") {
|
|
49
|
+
return resolve(process.cwd(), dirname(stdout));
|
|
50
|
+
}
|
|
51
|
+
const { stdout: toplevel } = await executeGitCommand([
|
|
52
|
+
"rev-parse",
|
|
53
|
+
"--show-toplevel"
|
|
54
|
+
]);
|
|
55
|
+
return toplevel;
|
|
27
56
|
}
|
|
28
57
|
|
|
29
|
-
// src/
|
|
30
|
-
|
|
31
|
-
|
|
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;
|
|
32
69
|
|
|
33
|
-
// src/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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";
|
|
40
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);
|
|
41
117
|
try {
|
|
42
|
-
|
|
43
|
-
const worktreesPath = join(gitRoot, ".git", "phantom", "worktrees");
|
|
44
|
-
const worktreePath = join(worktreesPath, name);
|
|
45
|
-
try {
|
|
46
|
-
await access(worktreePath);
|
|
47
|
-
} catch {
|
|
48
|
-
return {
|
|
49
|
-
success: false,
|
|
50
|
-
message: `Error: Worktree '${name}' does not exist`
|
|
51
|
-
};
|
|
52
|
-
}
|
|
118
|
+
await fs.access(worktreePath);
|
|
53
119
|
return {
|
|
54
|
-
|
|
120
|
+
exists: true,
|
|
55
121
|
path: worktreePath
|
|
56
122
|
};
|
|
57
|
-
} catch
|
|
58
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
123
|
+
} catch {
|
|
59
124
|
return {
|
|
60
|
-
|
|
61
|
-
message: `
|
|
125
|
+
exists: false,
|
|
126
|
+
message: `Worktree '${name}' does not exist`
|
|
62
127
|
};
|
|
63
128
|
}
|
|
64
129
|
}
|
|
65
|
-
async function
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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"));
|
|
71
154
|
}
|
|
72
|
-
|
|
155
|
+
return ok(void 0);
|
|
73
156
|
}
|
|
74
157
|
|
|
75
|
-
// src/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
158
|
+
// src/core/process/spawn.ts
|
|
159
|
+
import {
|
|
160
|
+
spawn as nodeSpawn
|
|
161
|
+
} from "node:child_process";
|
|
162
|
+
|
|
163
|
+
// src/core/process/errors.ts
|
|
164
|
+
var ProcessError = class extends Error {
|
|
165
|
+
exitCode;
|
|
166
|
+
constructor(message, exitCode) {
|
|
167
|
+
super(message);
|
|
168
|
+
this.name = "ProcessError";
|
|
169
|
+
this.exitCode = exitCode;
|
|
79
170
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
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, {
|
|
89
197
|
stdio: "inherit",
|
|
90
|
-
|
|
91
|
-
...process.env,
|
|
92
|
-
// Add environment variable to indicate we're in a worktree
|
|
93
|
-
WORKTREE_NAME: worktreeName,
|
|
94
|
-
WORKTREE_PATH: worktreePath
|
|
95
|
-
}
|
|
198
|
+
...options
|
|
96
199
|
});
|
|
97
200
|
childProcess.on("error", (error) => {
|
|
98
|
-
|
|
99
|
-
success: false,
|
|
100
|
-
message: `Error starting shell: ${error.message}`
|
|
101
|
-
});
|
|
201
|
+
resolve2(err(new ProcessSpawnError(command2, error.message)));
|
|
102
202
|
});
|
|
103
203
|
childProcess.on("exit", (code, signal) => {
|
|
104
204
|
if (signal) {
|
|
105
|
-
|
|
106
|
-
success: false,
|
|
107
|
-
message: `Shell terminated by signal: ${signal}`,
|
|
108
|
-
exitCode: 128 + (signal === "SIGTERM" ? 15 : 1)
|
|
109
|
-
});
|
|
205
|
+
resolve2(err(new ProcessSignalError(signal)));
|
|
110
206
|
} else {
|
|
111
207
|
const exitCode = code ?? 0;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
208
|
+
if (exitCode === 0) {
|
|
209
|
+
resolve2(ok({ exitCode }));
|
|
210
|
+
} else {
|
|
211
|
+
resolve2(err(new ProcessExecutionError(command2, exitCode)));
|
|
212
|
+
}
|
|
116
213
|
}
|
|
117
214
|
});
|
|
118
215
|
});
|
|
119
216
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
258
|
+
// src/core/worktree/attach.ts
|
|
259
|
+
import { existsSync } from "node:fs";
|
|
260
|
+
|
|
261
|
+
// src/core/git/libs/attach-worktree.ts
|
|
262
|
+
async function attachWorktree(gitRoot, worktreePath, branchName) {
|
|
263
|
+
try {
|
|
264
|
+
await executeGitCommand(["worktree", "add", worktreePath, branchName], {
|
|
265
|
+
cwd: gitRoot
|
|
266
|
+
});
|
|
267
|
+
return ok(void 0);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
return err(
|
|
270
|
+
error instanceof Error ? error : new Error(`Failed to attach worktree: ${String(error)}`)
|
|
271
|
+
);
|
|
147
272
|
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/core/git/libs/branch-exists.ts
|
|
276
|
+
async function branchExists(gitRoot, branchName) {
|
|
148
277
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
278
|
+
await executeGitCommand(
|
|
279
|
+
["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
|
|
280
|
+
{ cwd: gitRoot }
|
|
281
|
+
);
|
|
282
|
+
return ok(true);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
285
|
+
const execError = error;
|
|
286
|
+
if (execError.code === 1) {
|
|
287
|
+
return ok(false);
|
|
288
|
+
}
|
|
156
289
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
290
|
+
return err(
|
|
291
|
+
new Error(
|
|
292
|
+
`Failed to check branch existence: ${error instanceof Error ? error.message : String(error)}`
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/core/worktree/attach.ts
|
|
299
|
+
async function attachWorktreeCore(gitRoot, name) {
|
|
300
|
+
const validation = validateWorktreeName(name);
|
|
301
|
+
if (isErr(validation)) {
|
|
302
|
+
return validation;
|
|
303
|
+
}
|
|
304
|
+
const worktreePath = getWorktreePath(gitRoot, name);
|
|
305
|
+
if (existsSync(worktreePath)) {
|
|
306
|
+
return err(new WorktreeAlreadyExistsError(name));
|
|
307
|
+
}
|
|
308
|
+
const branchCheckResult = await branchExists(gitRoot, name);
|
|
309
|
+
if (isErr(branchCheckResult)) {
|
|
310
|
+
return err(branchCheckResult.error);
|
|
311
|
+
}
|
|
312
|
+
if (!branchCheckResult.value) {
|
|
313
|
+
return err(new BranchNotFoundError(name));
|
|
314
|
+
}
|
|
315
|
+
const attachResult = await attachWorktree(gitRoot, worktreePath, name);
|
|
316
|
+
if (isErr(attachResult)) {
|
|
317
|
+
return err(attachResult.error);
|
|
318
|
+
}
|
|
319
|
+
return ok(worktreePath);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/cli/output.ts
|
|
323
|
+
var output = {
|
|
324
|
+
log: (message) => {
|
|
325
|
+
console.log(message);
|
|
326
|
+
},
|
|
327
|
+
error: (message) => {
|
|
328
|
+
console.error(message);
|
|
329
|
+
},
|
|
330
|
+
table: (data) => {
|
|
331
|
+
console.table(data);
|
|
332
|
+
},
|
|
333
|
+
processOutput: (proc) => {
|
|
334
|
+
proc.stdout?.pipe(process.stdout);
|
|
335
|
+
proc.stderr?.pipe(process.stderr);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// src/cli/errors.ts
|
|
340
|
+
var exitCodes = {
|
|
341
|
+
success: 0,
|
|
342
|
+
generalError: 1,
|
|
343
|
+
notFound: 2,
|
|
344
|
+
validationError: 3
|
|
345
|
+
};
|
|
346
|
+
function exitWithSuccess() {
|
|
347
|
+
process.exit(exitCodes.success);
|
|
348
|
+
}
|
|
349
|
+
function exitWithError(message, exitCode = exitCodes.generalError) {
|
|
350
|
+
output.error(message);
|
|
351
|
+
process.exit(exitCode);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/cli/handlers/attach.ts
|
|
355
|
+
async function attachHandler(args2) {
|
|
356
|
+
const { positionals, values } = parseArgs({
|
|
357
|
+
args: args2,
|
|
358
|
+
strict: true,
|
|
359
|
+
allowPositionals: true,
|
|
360
|
+
options: {
|
|
361
|
+
shell: {
|
|
362
|
+
type: "boolean",
|
|
363
|
+
short: "s"
|
|
364
|
+
},
|
|
365
|
+
exec: {
|
|
366
|
+
type: "string",
|
|
367
|
+
short: "e"
|
|
368
|
+
}
|
|
164
369
|
}
|
|
370
|
+
});
|
|
371
|
+
if (positionals.length === 0) {
|
|
372
|
+
exitWithError(
|
|
373
|
+
"Missing required argument: branch name",
|
|
374
|
+
exitCodes.validationError
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const [branchName] = positionals;
|
|
378
|
+
if (values.shell && values.exec) {
|
|
379
|
+
exitWithError(
|
|
380
|
+
"Cannot use both --shell and --exec options",
|
|
381
|
+
exitCodes.validationError
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const gitRoot = await getGitRoot();
|
|
385
|
+
const result = await attachWorktreeCore(gitRoot, branchName);
|
|
386
|
+
if (isErr(result)) {
|
|
387
|
+
const error = result.error;
|
|
388
|
+
if (error instanceof WorktreeAlreadyExistsError) {
|
|
389
|
+
exitWithError(error.message, exitCodes.validationError);
|
|
390
|
+
}
|
|
391
|
+
if (error instanceof BranchNotFoundError) {
|
|
392
|
+
exitWithError(error.message, exitCodes.notFound);
|
|
393
|
+
}
|
|
394
|
+
exitWithError(error.message, exitCodes.generalError);
|
|
395
|
+
}
|
|
396
|
+
const worktreePath = result.value;
|
|
397
|
+
output.log(`Attached phantom: ${branchName}`);
|
|
398
|
+
if (values.shell) {
|
|
399
|
+
const shellResult = await shellInWorktree(gitRoot, branchName);
|
|
400
|
+
if (isErr(shellResult)) {
|
|
401
|
+
exitWithError(shellResult.error.message, exitCodes.generalError);
|
|
402
|
+
}
|
|
403
|
+
} else if (values.exec) {
|
|
404
|
+
const execResult = await execInWorktree(
|
|
405
|
+
gitRoot,
|
|
406
|
+
branchName,
|
|
407
|
+
values.exec.split(" ")
|
|
408
|
+
);
|
|
409
|
+
if (isErr(execResult)) {
|
|
410
|
+
exitWithError(execResult.error.message, exitCodes.generalError);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/cli/handlers/create.ts
|
|
416
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
417
|
+
|
|
418
|
+
// src/core/worktree/create.ts
|
|
419
|
+
import fs2 from "node:fs/promises";
|
|
420
|
+
|
|
421
|
+
// src/core/git/libs/add-worktree.ts
|
|
422
|
+
async function addWorktree(options) {
|
|
423
|
+
const { path, branch, commitish = "HEAD" } = options;
|
|
424
|
+
await executeGitCommand(["worktree", "add", path, "-b", branch, commitish]);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/core/worktree/create.ts
|
|
428
|
+
async function createWorktree(gitRoot, name, options = {}) {
|
|
429
|
+
const nameValidation = validateWorktreeName(name);
|
|
430
|
+
if (isErr(nameValidation)) {
|
|
431
|
+
return nameValidation;
|
|
432
|
+
}
|
|
433
|
+
const { branch = name, commitish = "HEAD" } = options;
|
|
434
|
+
const worktreesPath = getPhantomDirectory(gitRoot);
|
|
435
|
+
const worktreePath = getWorktreePath(gitRoot, name);
|
|
436
|
+
try {
|
|
437
|
+
await fs2.access(worktreesPath);
|
|
438
|
+
} catch {
|
|
439
|
+
await fs2.mkdir(worktreesPath, { recursive: true });
|
|
440
|
+
}
|
|
441
|
+
const validation = await validateWorktreeDoesNotExist(gitRoot, name);
|
|
442
|
+
if (validation.exists) {
|
|
443
|
+
return err(new WorktreeAlreadyExistsError(name));
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
165
446
|
await addWorktree({
|
|
166
447
|
path: worktreePath,
|
|
167
|
-
branch
|
|
168
|
-
commitish
|
|
448
|
+
branch,
|
|
449
|
+
commitish
|
|
169
450
|
});
|
|
170
|
-
return {
|
|
171
|
-
success: true,
|
|
451
|
+
return ok({
|
|
172
452
|
message: `Created worktree '${name}' at ${worktreePath}`,
|
|
173
453
|
path: worktreePath
|
|
174
|
-
};
|
|
454
|
+
});
|
|
175
455
|
} catch (error) {
|
|
176
456
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
177
|
-
return
|
|
178
|
-
success: false,
|
|
179
|
-
message: `Error creating worktree: ${errorMessage}`
|
|
180
|
-
};
|
|
457
|
+
return err(new GitOperationError("worktree add", errorMessage));
|
|
181
458
|
}
|
|
182
459
|
}
|
|
460
|
+
|
|
461
|
+
// src/cli/handlers/create.ts
|
|
183
462
|
async function createHandler(args2) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
Entering worktree '${name}' at ${result.path}`);
|
|
195
|
-
console.log("Type 'exit' to return to your original directory\n");
|
|
196
|
-
const shellResult = await shellInWorktree(name);
|
|
197
|
-
if (!shellResult.success) {
|
|
198
|
-
if (shellResult.message) {
|
|
199
|
-
console.error(shellResult.message);
|
|
463
|
+
const { values, positionals } = parseArgs2({
|
|
464
|
+
args: args2,
|
|
465
|
+
options: {
|
|
466
|
+
shell: {
|
|
467
|
+
type: "boolean",
|
|
468
|
+
short: "s"
|
|
469
|
+
},
|
|
470
|
+
exec: {
|
|
471
|
+
type: "string",
|
|
472
|
+
short: "x"
|
|
200
473
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
474
|
+
},
|
|
475
|
+
strict: true,
|
|
476
|
+
allowPositionals: true
|
|
477
|
+
});
|
|
478
|
+
if (positionals.length === 0) {
|
|
479
|
+
exitWithError(
|
|
480
|
+
"Please provide a name for the new worktree",
|
|
481
|
+
exitCodes.validationError
|
|
482
|
+
);
|
|
204
483
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
var execAsync3 = promisify3(exec3);
|
|
214
|
-
async function deleteWorktree(name, options = {}) {
|
|
215
|
-
if (!name) {
|
|
216
|
-
return { success: false, message: "Error: worktree name required" };
|
|
484
|
+
const worktreeName = positionals[0];
|
|
485
|
+
const openShell = values.shell ?? false;
|
|
486
|
+
const execCommand = values.exec;
|
|
487
|
+
if (openShell && execCommand) {
|
|
488
|
+
exitWithError(
|
|
489
|
+
"Cannot use --shell and --exec together",
|
|
490
|
+
exitCodes.validationError
|
|
491
|
+
);
|
|
217
492
|
}
|
|
218
|
-
const { force = false } = options;
|
|
219
493
|
try {
|
|
220
494
|
const gitRoot = await getGitRoot();
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} catch {
|
|
226
|
-
return {
|
|
227
|
-
success: false,
|
|
228
|
-
message: `Error: Worktree '${name}' does not exist`
|
|
229
|
-
};
|
|
495
|
+
const result = await createWorktree(gitRoot, worktreeName);
|
|
496
|
+
if (isErr(result)) {
|
|
497
|
+
const exitCode = result.error instanceof WorktreeAlreadyExistsError ? exitCodes.validationError : exitCodes.generalError;
|
|
498
|
+
exitWithError(result.error.message, exitCode);
|
|
230
499
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
500
|
+
output.log(result.value.message);
|
|
501
|
+
if (execCommand && isOk(result)) {
|
|
502
|
+
output.log(
|
|
503
|
+
`
|
|
504
|
+
Executing command in worktree '${worktreeName}': ${execCommand}`
|
|
505
|
+
);
|
|
506
|
+
const shell = process.env.SHELL || "/bin/sh";
|
|
507
|
+
const execResult = await execInWorktree(gitRoot, worktreeName, [
|
|
508
|
+
shell,
|
|
509
|
+
"-c",
|
|
510
|
+
execCommand
|
|
511
|
+
]);
|
|
512
|
+
if (isErr(execResult)) {
|
|
513
|
+
output.error(execResult.error.message);
|
|
514
|
+
const exitCode = "exitCode" in execResult.error ? execResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
|
|
515
|
+
exitWithError("", exitCode);
|
|
241
516
|
}
|
|
242
|
-
|
|
243
|
-
|
|
517
|
+
process.exit(execResult.value.exitCode ?? 0);
|
|
518
|
+
}
|
|
519
|
+
if (openShell && isOk(result)) {
|
|
520
|
+
output.log(
|
|
521
|
+
`
|
|
522
|
+
Entering worktree '${worktreeName}' at ${result.value.path}`
|
|
523
|
+
);
|
|
524
|
+
output.log("Type 'exit' to return to your original directory\n");
|
|
525
|
+
const shellResult = await shellInWorktree(gitRoot, worktreeName);
|
|
526
|
+
if (isErr(shellResult)) {
|
|
527
|
+
output.error(shellResult.error.message);
|
|
528
|
+
const exitCode = "exitCode" in shellResult.error ? shellResult.error.exitCode ?? exitCodes.generalError : exitCodes.generalError;
|
|
529
|
+
exitWithError("", exitCode);
|
|
530
|
+
}
|
|
531
|
+
process.exit(shellResult.value.exitCode ?? 0);
|
|
244
532
|
}
|
|
245
|
-
|
|
533
|
+
exitWithSuccess();
|
|
534
|
+
} catch (error) {
|
|
535
|
+
exitWithError(
|
|
536
|
+
error instanceof Error ? error.message : String(error),
|
|
537
|
+
exitCodes.generalError
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/cli/handlers/delete.ts
|
|
543
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
544
|
+
|
|
545
|
+
// src/core/worktree/delete.ts
|
|
546
|
+
async function getWorktreeStatus(worktreePath) {
|
|
547
|
+
try {
|
|
548
|
+
const { stdout } = await executeGitCommandInDirectory(worktreePath, [
|
|
549
|
+
"status",
|
|
550
|
+
"--porcelain"
|
|
551
|
+
]);
|
|
552
|
+
if (stdout) {
|
|
246
553
|
return {
|
|
247
|
-
success: false,
|
|
248
|
-
message: `Error: Worktree '${name}' has uncommitted changes (${changedFiles} files). Use --force to delete anyway.`,
|
|
249
554
|
hasUncommittedChanges: true,
|
|
250
|
-
changedFiles
|
|
555
|
+
changedFiles: stdout.split("\n").length
|
|
251
556
|
};
|
|
252
557
|
}
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
hasUncommittedChanges: false,
|
|
562
|
+
changedFiles: 0
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
async function removeWorktree(gitRoot, worktreePath, force = false) {
|
|
566
|
+
try {
|
|
567
|
+
await executeGitCommand(["worktree", "remove", worktreePath], {
|
|
568
|
+
cwd: gitRoot
|
|
569
|
+
});
|
|
570
|
+
} catch (error) {
|
|
253
571
|
try {
|
|
254
|
-
await
|
|
255
|
-
cwd: gitRoot
|
|
256
|
-
});
|
|
257
|
-
} catch (error) {
|
|
258
|
-
try {
|
|
259
|
-
await execAsync3(`git worktree remove --force "${worktreePath}"`, {
|
|
260
|
-
cwd: gitRoot
|
|
261
|
-
});
|
|
262
|
-
} catch {
|
|
263
|
-
return {
|
|
264
|
-
success: false,
|
|
265
|
-
message: `Error: Failed to remove worktree '${name}'`
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const branchName = `phantom/worktrees/${name}`;
|
|
270
|
-
try {
|
|
271
|
-
await execAsync3(`git branch -D "${branchName}"`, {
|
|
572
|
+
await executeGitCommand(["worktree", "remove", "--force", worktreePath], {
|
|
272
573
|
cwd: gitRoot
|
|
273
574
|
});
|
|
274
575
|
} catch {
|
|
576
|
+
throw new Error("Failed to remove worktree");
|
|
275
577
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function deleteBranch(gitRoot, branchName) {
|
|
581
|
+
try {
|
|
582
|
+
await executeGitCommand(["branch", "-D", branchName], { cwd: gitRoot });
|
|
583
|
+
return ok(true);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
586
|
+
return err(new GitOperationError("branch delete", errorMessage));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function deleteWorktree(gitRoot, name, options = {}) {
|
|
590
|
+
const { force = false } = options;
|
|
591
|
+
const validation = await validateWorktreeExists(gitRoot, name);
|
|
592
|
+
if (!validation.exists) {
|
|
593
|
+
return err(new WorktreeNotFoundError(name));
|
|
594
|
+
}
|
|
595
|
+
const worktreePath = validation.path;
|
|
596
|
+
const status = await getWorktreeStatus(worktreePath);
|
|
597
|
+
if (status.hasUncommittedChanges && !force) {
|
|
598
|
+
return err(
|
|
599
|
+
new WorktreeError(
|
|
600
|
+
`Worktree '${name}' has uncommitted changes (${status.changedFiles} files). Use --force to delete anyway.`
|
|
601
|
+
)
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
await removeWorktree(gitRoot, worktreePath, force);
|
|
606
|
+
const branchName = name;
|
|
607
|
+
const branchResult = await deleteBranch(gitRoot, branchName);
|
|
608
|
+
let message;
|
|
609
|
+
if (isOk(branchResult)) {
|
|
610
|
+
message = `Deleted worktree '${name}' and its branch '${branchName}'`;
|
|
611
|
+
} else {
|
|
612
|
+
message = `Deleted worktree '${name}'`;
|
|
613
|
+
message += `
|
|
614
|
+
Note: Branch '${branchName}' could not be deleted: ${branchResult.error.message}`;
|
|
615
|
+
}
|
|
616
|
+
if (status.hasUncommittedChanges) {
|
|
617
|
+
message = `Warning: Worktree '${name}' had uncommitted changes (${status.changedFiles} files)
|
|
279
618
|
${message}`;
|
|
280
619
|
}
|
|
281
|
-
return {
|
|
282
|
-
success: true,
|
|
620
|
+
return ok({
|
|
283
621
|
message,
|
|
284
|
-
hasUncommittedChanges,
|
|
285
|
-
changedFiles: hasUncommittedChanges ? changedFiles : void 0
|
|
286
|
-
};
|
|
622
|
+
hasUncommittedChanges: status.hasUncommittedChanges,
|
|
623
|
+
changedFiles: status.hasUncommittedChanges ? status.changedFiles : void 0
|
|
624
|
+
});
|
|
287
625
|
} catch (error) {
|
|
288
626
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
289
|
-
return
|
|
290
|
-
success: false,
|
|
291
|
-
message: `Error deleting worktree: ${errorMessage}`
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
async function deleteHandler(args2) {
|
|
296
|
-
const forceIndex = args2.indexOf("--force");
|
|
297
|
-
const force = forceIndex !== -1;
|
|
298
|
-
const filteredArgs = args2.filter((arg) => arg !== "--force");
|
|
299
|
-
const name = filteredArgs[0];
|
|
300
|
-
const result = await deleteWorktree(name, { force });
|
|
301
|
-
if (!result.success) {
|
|
302
|
-
console.error(result.message);
|
|
303
|
-
exit4(1);
|
|
627
|
+
return err(new GitOperationError("worktree remove", errorMessage));
|
|
304
628
|
}
|
|
305
|
-
console.log(result.message);
|
|
306
629
|
}
|
|
307
630
|
|
|
308
|
-
// src/
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return { success: false, message: "Error: command required" };
|
|
317
|
-
}
|
|
318
|
-
const worktreeResult = await whereWorktree(worktreeName);
|
|
319
|
-
if (!worktreeResult.success) {
|
|
320
|
-
return { success: false, message: worktreeResult.message };
|
|
321
|
-
}
|
|
322
|
-
const worktreePath = worktreeResult.path;
|
|
323
|
-
const [cmd, ...args2] = command2;
|
|
324
|
-
return new Promise((resolve) => {
|
|
325
|
-
const childProcess = spawn2(cmd, args2, {
|
|
326
|
-
cwd: worktreePath,
|
|
327
|
-
stdio: "inherit"
|
|
328
|
-
});
|
|
329
|
-
childProcess.on("error", (error) => {
|
|
330
|
-
resolve({
|
|
331
|
-
success: false,
|
|
332
|
-
message: `Error executing command: ${error.message}`
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
childProcess.on("exit", (code, signal) => {
|
|
336
|
-
if (signal) {
|
|
337
|
-
resolve({
|
|
338
|
-
success: false,
|
|
339
|
-
message: `Command terminated by signal: ${signal}`,
|
|
340
|
-
exitCode: 128 + (signal === "SIGTERM" ? 15 : 1)
|
|
341
|
-
});
|
|
342
|
-
} else {
|
|
343
|
-
const exitCode = code ?? 0;
|
|
344
|
-
resolve({
|
|
345
|
-
success: exitCode === 0,
|
|
346
|
-
exitCode
|
|
347
|
-
});
|
|
631
|
+
// src/cli/handlers/delete.ts
|
|
632
|
+
async function deleteHandler(args2) {
|
|
633
|
+
const { values, positionals } = parseArgs3({
|
|
634
|
+
args: args2,
|
|
635
|
+
options: {
|
|
636
|
+
force: {
|
|
637
|
+
type: "boolean",
|
|
638
|
+
short: "f"
|
|
348
639
|
}
|
|
349
|
-
}
|
|
640
|
+
},
|
|
641
|
+
strict: true,
|
|
642
|
+
allowPositionals: true
|
|
350
643
|
});
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
const worktreeName =
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
644
|
+
if (positionals.length === 0) {
|
|
645
|
+
exitWithError(
|
|
646
|
+
"Please provide a worktree name to delete",
|
|
647
|
+
exitCodes.validationError
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
const worktreeName = positionals[0];
|
|
651
|
+
const forceDelete = values.force ?? false;
|
|
652
|
+
try {
|
|
653
|
+
const gitRoot = await getGitRoot();
|
|
654
|
+
const result = await deleteWorktree(gitRoot, worktreeName, {
|
|
655
|
+
force: forceDelete
|
|
656
|
+
});
|
|
657
|
+
if (isErr(result)) {
|
|
658
|
+
const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.validationError : result.error instanceof WorktreeError && result.error.message.includes("uncommitted changes") ? exitCodes.validationError : exitCodes.generalError;
|
|
659
|
+
exitWithError(result.error.message, exitCode);
|
|
363
660
|
}
|
|
364
|
-
|
|
661
|
+
output.log(result.value.message);
|
|
662
|
+
exitWithSuccess();
|
|
663
|
+
} catch (error) {
|
|
664
|
+
exitWithError(
|
|
665
|
+
error instanceof Error ? error.message : String(error),
|
|
666
|
+
exitCodes.generalError
|
|
667
|
+
);
|
|
365
668
|
}
|
|
366
|
-
exit5(result.exitCode ?? 0);
|
|
367
669
|
}
|
|
368
670
|
|
|
369
|
-
// src/
|
|
370
|
-
import {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
671
|
+
// src/cli/handlers/exec.ts
|
|
672
|
+
import { parseArgs as parseArgs4 } from "node:util";
|
|
673
|
+
async function execHandler(args2) {
|
|
674
|
+
const { positionals } = parseArgs4({
|
|
675
|
+
args: args2,
|
|
676
|
+
options: {},
|
|
677
|
+
strict: true,
|
|
678
|
+
allowPositionals: true
|
|
679
|
+
});
|
|
680
|
+
if (positionals.length < 2) {
|
|
681
|
+
exitWithError(
|
|
682
|
+
"Usage: phantom exec <worktree-name> <command> [args...]",
|
|
683
|
+
exitCodes.validationError
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const [worktreeName, ...commandArgs] = positionals;
|
|
376
687
|
try {
|
|
377
688
|
const gitRoot = await getGitRoot();
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return {
|
|
383
|
-
success: true,
|
|
384
|
-
worktrees: [],
|
|
385
|
-
message: "No worktrees found (worktrees directory doesn't exist)"
|
|
386
|
-
};
|
|
689
|
+
const result = await execInWorktree(gitRoot, worktreeName, commandArgs);
|
|
690
|
+
if (isErr(result)) {
|
|
691
|
+
const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.notFound : result.error.exitCode || exitCodes.generalError;
|
|
692
|
+
exitWithError(result.error.message, exitCode);
|
|
387
693
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
694
|
+
process.exit(result.value.exitCode);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
exitWithError(
|
|
697
|
+
error instanceof Error ? error.message : String(error),
|
|
698
|
+
exitCodes.generalError
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/cli/handlers/list.ts
|
|
704
|
+
import { parseArgs as parseArgs5 } from "node:util";
|
|
705
|
+
|
|
706
|
+
// src/core/git/libs/list-worktrees.ts
|
|
707
|
+
async function listWorktrees(gitRoot) {
|
|
708
|
+
const { stdout } = await executeGitCommand([
|
|
709
|
+
"worktree",
|
|
710
|
+
"list",
|
|
711
|
+
"--porcelain"
|
|
712
|
+
]);
|
|
713
|
+
const worktrees = [];
|
|
714
|
+
let currentWorktree = {};
|
|
715
|
+
const lines = stdout.split("\n").filter((line) => line.length > 0);
|
|
716
|
+
for (const line of lines) {
|
|
717
|
+
if (line.startsWith("worktree ")) {
|
|
718
|
+
if (currentWorktree.path) {
|
|
719
|
+
worktrees.push(currentWorktree);
|
|
720
|
+
}
|
|
721
|
+
currentWorktree = {
|
|
722
|
+
path: line.substring("worktree ".length),
|
|
723
|
+
isLocked: false,
|
|
724
|
+
isPrunable: false
|
|
410
725
|
};
|
|
726
|
+
} else if (line.startsWith("HEAD ")) {
|
|
727
|
+
currentWorktree.head = line.substring("HEAD ".length);
|
|
728
|
+
} else if (line.startsWith("branch ")) {
|
|
729
|
+
const fullBranch = line.substring("branch ".length);
|
|
730
|
+
currentWorktree.branch = fullBranch.startsWith("refs/heads/") ? fullBranch.substring("refs/heads/".length) : fullBranch;
|
|
731
|
+
} else if (line === "detached") {
|
|
732
|
+
currentWorktree.branch = "(detached HEAD)";
|
|
733
|
+
} else if (line === "locked") {
|
|
734
|
+
currentWorktree.isLocked = true;
|
|
735
|
+
} else if (line === "prunable") {
|
|
736
|
+
currentWorktree.isPrunable = true;
|
|
411
737
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
738
|
+
}
|
|
739
|
+
if (currentWorktree.path) {
|
|
740
|
+
worktrees.push(currentWorktree);
|
|
741
|
+
}
|
|
742
|
+
return worktrees;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/core/worktree/list.ts
|
|
746
|
+
async function getWorktreeStatus2(worktreePath) {
|
|
747
|
+
try {
|
|
748
|
+
const { stdout } = await executeGitCommandInDirectory(worktreePath, [
|
|
749
|
+
"status",
|
|
750
|
+
"--porcelain"
|
|
751
|
+
]);
|
|
752
|
+
return !stdout;
|
|
753
|
+
} catch {
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async function listWorktrees2(gitRoot) {
|
|
758
|
+
try {
|
|
759
|
+
const gitWorktrees = await listWorktrees(gitRoot);
|
|
760
|
+
const phantomDir = getPhantomDirectory(gitRoot);
|
|
761
|
+
const phantomWorktrees = gitWorktrees.filter(
|
|
762
|
+
(worktree) => worktree.path.startsWith(phantomDir)
|
|
763
|
+
);
|
|
764
|
+
if (phantomWorktrees.length === 0) {
|
|
765
|
+
return ok({
|
|
415
766
|
worktrees: [],
|
|
416
767
|
message: "No worktrees found"
|
|
417
|
-
};
|
|
768
|
+
});
|
|
418
769
|
}
|
|
419
770
|
const worktrees = await Promise.all(
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
try {
|
|
424
|
-
const { stdout } = await execAsync4("git branch --show-current", {
|
|
425
|
-
cwd: worktreePath
|
|
426
|
-
});
|
|
427
|
-
branch = stdout.trim() || "detached HEAD";
|
|
428
|
-
} catch {
|
|
429
|
-
branch = "unknown";
|
|
430
|
-
}
|
|
431
|
-
let status = "clean";
|
|
432
|
-
let changedFiles;
|
|
433
|
-
try {
|
|
434
|
-
const { stdout } = await execAsync4("git status --porcelain", {
|
|
435
|
-
cwd: worktreePath
|
|
436
|
-
});
|
|
437
|
-
const changes = stdout.trim();
|
|
438
|
-
if (changes) {
|
|
439
|
-
status = "dirty";
|
|
440
|
-
changedFiles = changes.split("\n").length;
|
|
441
|
-
}
|
|
442
|
-
} catch {
|
|
443
|
-
status = "clean";
|
|
444
|
-
}
|
|
771
|
+
phantomWorktrees.map(async (gitWorktree) => {
|
|
772
|
+
const name = gitWorktree.path.substring(phantomDir.length + 1);
|
|
773
|
+
const isClean = await getWorktreeStatus2(gitWorktree.path);
|
|
445
774
|
return {
|
|
446
775
|
name,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
776
|
+
path: gitWorktree.path,
|
|
777
|
+
branch: gitWorktree.branch || "(detached HEAD)",
|
|
778
|
+
isClean
|
|
450
779
|
};
|
|
451
780
|
})
|
|
452
781
|
);
|
|
453
|
-
return {
|
|
454
|
-
success: true,
|
|
782
|
+
return ok({
|
|
455
783
|
worktrees
|
|
456
|
-
};
|
|
784
|
+
});
|
|
457
785
|
} catch (error) {
|
|
458
786
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
459
|
-
|
|
460
|
-
success: false,
|
|
461
|
-
message: `Error listing worktrees: ${errorMessage}`
|
|
462
|
-
};
|
|
787
|
+
throw new Error(`Failed to list worktrees: ${errorMessage}`);
|
|
463
788
|
}
|
|
464
789
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
790
|
+
|
|
791
|
+
// src/cli/handlers/list.ts
|
|
792
|
+
async function listHandler(args2 = []) {
|
|
793
|
+
parseArgs5({
|
|
794
|
+
args: args2,
|
|
795
|
+
options: {},
|
|
796
|
+
strict: true,
|
|
797
|
+
allowPositionals: false
|
|
798
|
+
});
|
|
799
|
+
try {
|
|
800
|
+
const gitRoot = await getGitRoot();
|
|
801
|
+
const result = await listWorktrees2(gitRoot);
|
|
802
|
+
if (isErr(result)) {
|
|
803
|
+
exitWithError("Failed to list worktrees", exitCodes.generalError);
|
|
804
|
+
}
|
|
805
|
+
const { worktrees, message } = result.value;
|
|
806
|
+
if (worktrees.length === 0) {
|
|
807
|
+
output.log(message || "No worktrees found.");
|
|
808
|
+
process.exit(exitCodes.success);
|
|
809
|
+
}
|
|
810
|
+
const maxNameLength = Math.max(...worktrees.map((wt) => wt.name.length));
|
|
811
|
+
for (const worktree of worktrees) {
|
|
812
|
+
const paddedName = worktree.name.padEnd(maxNameLength + 2);
|
|
813
|
+
const branchInfo = worktree.branch ? `(${worktree.branch})` : "";
|
|
814
|
+
const status = !worktree.isClean ? " [dirty]" : "";
|
|
815
|
+
output.log(`${paddedName} ${branchInfo}${status}`);
|
|
816
|
+
}
|
|
817
|
+
process.exit(exitCodes.success);
|
|
818
|
+
} catch (error) {
|
|
819
|
+
exitWithError(
|
|
820
|
+
error instanceof Error ? error.message : String(error),
|
|
821
|
+
exitCodes.generalError
|
|
822
|
+
);
|
|
470
823
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/cli/handlers/shell.ts
|
|
827
|
+
import { parseArgs as parseArgs6 } from "node:util";
|
|
828
|
+
async function shellHandler(args2) {
|
|
829
|
+
const { positionals } = parseArgs6({
|
|
830
|
+
args: args2,
|
|
831
|
+
options: {},
|
|
832
|
+
strict: true,
|
|
833
|
+
allowPositionals: true
|
|
834
|
+
});
|
|
835
|
+
if (positionals.length === 0) {
|
|
836
|
+
exitWithError(
|
|
837
|
+
"Usage: phantom shell <worktree-name>",
|
|
838
|
+
exitCodes.validationError
|
|
839
|
+
);
|
|
474
840
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
841
|
+
const worktreeName = positionals[0];
|
|
842
|
+
try {
|
|
843
|
+
const gitRoot = await getGitRoot();
|
|
844
|
+
const validation = await validateWorktreeExists(gitRoot, worktreeName);
|
|
845
|
+
if (!validation.exists) {
|
|
846
|
+
exitWithError(
|
|
847
|
+
validation.message || `Worktree '${worktreeName}' not found`,
|
|
848
|
+
exitCodes.generalError
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
output.log(`Entering worktree '${worktreeName}' at ${validation.path}`);
|
|
852
|
+
output.log("Type 'exit' to return to your original directory\n");
|
|
853
|
+
const result = await shellInWorktree(gitRoot, worktreeName);
|
|
854
|
+
if (isErr(result)) {
|
|
855
|
+
const exitCode = result.error instanceof WorktreeNotFoundError ? exitCodes.notFound : result.error.exitCode || exitCodes.generalError;
|
|
856
|
+
exitWithError(result.error.message, exitCode);
|
|
857
|
+
}
|
|
858
|
+
process.exit(result.value.exitCode);
|
|
859
|
+
} catch (error) {
|
|
860
|
+
exitWithError(
|
|
861
|
+
error instanceof Error ? error.message : String(error),
|
|
862
|
+
exitCodes.generalError
|
|
480
863
|
);
|
|
481
864
|
}
|
|
482
|
-
console.log(`
|
|
483
|
-
Total: ${result.worktrees.length} worktrees`);
|
|
484
865
|
}
|
|
485
866
|
|
|
867
|
+
// src/cli/handlers/version.ts
|
|
868
|
+
import { parseArgs as parseArgs7 } from "node:util";
|
|
869
|
+
|
|
486
870
|
// package.json
|
|
487
871
|
var package_default = {
|
|
488
872
|
name: "@aku11i/phantom",
|
|
489
873
|
packageManager: "pnpm@10.8.1",
|
|
490
|
-
version: "0.
|
|
874
|
+
version: "0.5.0",
|
|
491
875
|
description: "A powerful CLI tool for managing Git worktrees for parallel development",
|
|
492
876
|
keywords: [
|
|
493
877
|
"git",
|
|
@@ -516,12 +900,12 @@ var package_default = {
|
|
|
516
900
|
start: "node ./src/bin/phantom.ts",
|
|
517
901
|
phantom: "node ./src/bin/phantom.ts",
|
|
518
902
|
build: "node build.ts",
|
|
519
|
-
|
|
903
|
+
typecheck: "tsgo --noEmit",
|
|
520
904
|
test: "node --test --experimental-strip-types --experimental-test-module-mocks src/**/*.test.ts",
|
|
521
905
|
lint: "biome check .",
|
|
522
906
|
fix: "biome check --write .",
|
|
523
|
-
ready: "pnpm fix && pnpm
|
|
524
|
-
"ready:check": "pnpm lint && pnpm
|
|
907
|
+
ready: "pnpm fix && pnpm typecheck && pnpm test",
|
|
908
|
+
"ready:check": "pnpm lint && pnpm typecheck && pnpm test",
|
|
525
909
|
prepublishOnly: "pnpm ready:check && pnpm build"
|
|
526
910
|
},
|
|
527
911
|
engines: {
|
|
@@ -535,27 +919,84 @@ var package_default = {
|
|
|
535
919
|
devDependencies: {
|
|
536
920
|
"@biomejs/biome": "^1.9.4",
|
|
537
921
|
"@types/node": "^22.15.29",
|
|
922
|
+
"@typescript/native-preview": "7.0.0-dev.20250602.1",
|
|
538
923
|
esbuild: "^0.25.5",
|
|
539
924
|
typescript: "^5.8.3"
|
|
540
925
|
}
|
|
541
926
|
};
|
|
542
927
|
|
|
543
|
-
// src/
|
|
928
|
+
// src/core/version.ts
|
|
544
929
|
function getVersion() {
|
|
545
930
|
return package_default.version;
|
|
546
931
|
}
|
|
547
|
-
|
|
932
|
+
|
|
933
|
+
// src/cli/handlers/version.ts
|
|
934
|
+
function versionHandler(args2 = []) {
|
|
935
|
+
parseArgs7({
|
|
936
|
+
args: args2,
|
|
937
|
+
options: {},
|
|
938
|
+
strict: true,
|
|
939
|
+
allowPositionals: false
|
|
940
|
+
});
|
|
548
941
|
const version = getVersion();
|
|
549
|
-
|
|
942
|
+
output.log(`Phantom v${version}`);
|
|
943
|
+
exitWithSuccess();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/cli/handlers/where.ts
|
|
947
|
+
import { parseArgs as parseArgs8 } from "node:util";
|
|
948
|
+
|
|
949
|
+
// src/core/worktree/where.ts
|
|
950
|
+
async function whereWorktree(gitRoot, name) {
|
|
951
|
+
const validation = await validateWorktreeExists(gitRoot, name);
|
|
952
|
+
if (!validation.exists) {
|
|
953
|
+
return err(new WorktreeNotFoundError(name));
|
|
954
|
+
}
|
|
955
|
+
return ok({
|
|
956
|
+
path: validation.path
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/cli/handlers/where.ts
|
|
961
|
+
async function whereHandler(args2) {
|
|
962
|
+
const { positionals } = parseArgs8({
|
|
963
|
+
args: args2,
|
|
964
|
+
options: {},
|
|
965
|
+
strict: true,
|
|
966
|
+
allowPositionals: true
|
|
967
|
+
});
|
|
968
|
+
if (positionals.length === 0) {
|
|
969
|
+
exitWithError("Please provide a worktree name", exitCodes.validationError);
|
|
970
|
+
}
|
|
971
|
+
const worktreeName = positionals[0];
|
|
972
|
+
try {
|
|
973
|
+
const gitRoot = await getGitRoot();
|
|
974
|
+
const result = await whereWorktree(gitRoot, worktreeName);
|
|
975
|
+
if (isErr(result)) {
|
|
976
|
+
exitWithError(result.error.message, exitCodes.notFound);
|
|
977
|
+
}
|
|
978
|
+
output.log(result.value.path);
|
|
979
|
+
exitWithSuccess();
|
|
980
|
+
} catch (error) {
|
|
981
|
+
exitWithError(
|
|
982
|
+
error instanceof Error ? error.message : String(error),
|
|
983
|
+
exitCodes.generalError
|
|
984
|
+
);
|
|
985
|
+
}
|
|
550
986
|
}
|
|
551
987
|
|
|
552
988
|
// src/bin/phantom.ts
|
|
553
989
|
var commands = [
|
|
554
990
|
{
|
|
555
991
|
name: "create",
|
|
556
|
-
description: "Create a new worktree [--shell
|
|
992
|
+
description: "Create a new worktree [--shell | --exec <command>]",
|
|
557
993
|
handler: createHandler
|
|
558
994
|
},
|
|
995
|
+
{
|
|
996
|
+
name: "attach",
|
|
997
|
+
description: "Attach to an existing branch [--shell | --exec <command>]",
|
|
998
|
+
handler: attachHandler
|
|
999
|
+
},
|
|
559
1000
|
{
|
|
560
1001
|
name: "list",
|
|
561
1002
|
description: "List all worktrees",
|
|
@@ -617,18 +1058,18 @@ function findCommand(args2, commands2) {
|
|
|
617
1058
|
var args = argv.slice(2);
|
|
618
1059
|
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
|
|
619
1060
|
printHelp(commands);
|
|
620
|
-
|
|
1061
|
+
exit(0);
|
|
621
1062
|
}
|
|
622
1063
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
623
1064
|
versionHandler();
|
|
624
|
-
|
|
1065
|
+
exit(0);
|
|
625
1066
|
}
|
|
626
1067
|
var { command, remainingArgs } = findCommand(args, commands);
|
|
627
1068
|
if (!command || !command.handler) {
|
|
628
1069
|
console.error(`Error: Unknown command '${args.join(" ")}'
|
|
629
1070
|
`);
|
|
630
1071
|
printHelp(commands);
|
|
631
|
-
|
|
1072
|
+
exit(1);
|
|
632
1073
|
}
|
|
633
1074
|
try {
|
|
634
1075
|
await command.handler(remainingArgs);
|
|
@@ -637,6 +1078,6 @@ try {
|
|
|
637
1078
|
"Error:",
|
|
638
1079
|
error instanceof Error ? error.message : String(error)
|
|
639
1080
|
);
|
|
640
|
-
|
|
1081
|
+
exit(1);
|
|
641
1082
|
}
|
|
642
1083
|
//# sourceMappingURL=phantom.js.map
|