@aku11i/phantom 0.1.2 → 0.3.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 +254 -0
- package/README.md +90 -32
- package/dist/phantom.js +293 -217
- package/dist/phantom.js.map +4 -4
- package/package.json +7 -9
- package/dist/garden.js +0 -394
- package/dist/garden.js.map +0 -7
package/dist/phantom.js
CHANGED
|
@@ -3,56 +3,68 @@
|
|
|
3
3
|
// src/bin/phantom.ts
|
|
4
4
|
import { argv, exit as exit6 } from "node:process";
|
|
5
5
|
|
|
6
|
-
// src/commands/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
// src/gardens/commands/where.ts
|
|
11
|
-
import { access } from "node:fs/promises";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
import { exit } from "node:process";
|
|
6
|
+
// src/commands/create.ts
|
|
7
|
+
import { access as access2, mkdir } from "node:fs/promises";
|
|
8
|
+
import { join as join2 } from "node:path";
|
|
9
|
+
import { exit as exit3 } from "node:process";
|
|
14
10
|
|
|
15
|
-
// src/git/libs/
|
|
11
|
+
// src/git/libs/add-worktree.ts
|
|
16
12
|
import { exec } from "node:child_process";
|
|
17
13
|
import { promisify } from "node:util";
|
|
18
14
|
var execAsync = promisify(exec);
|
|
15
|
+
async function addWorktree(options) {
|
|
16
|
+
const { path, branch, commitish = "HEAD" } = options;
|
|
17
|
+
await execAsync(`git worktree add "${path}" -b "${branch}" ${commitish}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
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);
|
|
19
24
|
async function getGitRoot() {
|
|
20
|
-
const { stdout } = await
|
|
25
|
+
const { stdout } = await execAsync2("git rev-parse --show-toplevel");
|
|
21
26
|
return stdout.trim();
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
// src/
|
|
25
|
-
|
|
29
|
+
// src/commands/shell.ts
|
|
30
|
+
import { spawn } from "node:child_process";
|
|
31
|
+
import { exit as exit2 } from "node:process";
|
|
32
|
+
|
|
33
|
+
// src/commands/where.ts
|
|
34
|
+
import { access } from "node:fs/promises";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import { exit } from "node:process";
|
|
37
|
+
async function whereWorktree(name) {
|
|
26
38
|
if (!name) {
|
|
27
|
-
return { success: false, message: "Error:
|
|
39
|
+
return { success: false, message: "Error: worktree name required" };
|
|
28
40
|
}
|
|
29
41
|
try {
|
|
30
42
|
const gitRoot = await getGitRoot();
|
|
31
|
-
const
|
|
32
|
-
const
|
|
43
|
+
const worktreesPath = join(gitRoot, ".git", "phantom", "worktrees");
|
|
44
|
+
const worktreePath = join(worktreesPath, name);
|
|
33
45
|
try {
|
|
34
|
-
await access(
|
|
46
|
+
await access(worktreePath);
|
|
35
47
|
} catch {
|
|
36
48
|
return {
|
|
37
49
|
success: false,
|
|
38
|
-
message: `Error:
|
|
50
|
+
message: `Error: Worktree '${name}' does not exist`
|
|
39
51
|
};
|
|
40
52
|
}
|
|
41
53
|
return {
|
|
42
54
|
success: true,
|
|
43
|
-
path:
|
|
55
|
+
path: worktreePath
|
|
44
56
|
};
|
|
45
57
|
} catch (error) {
|
|
46
58
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
47
59
|
return {
|
|
48
60
|
success: false,
|
|
49
|
-
message: `Error locating
|
|
61
|
+
message: `Error locating worktree: ${errorMessage}`
|
|
50
62
|
};
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
|
-
async function
|
|
65
|
+
async function whereHandler(args2) {
|
|
54
66
|
const name = args2[0];
|
|
55
|
-
const result = await
|
|
67
|
+
const result = await whereWorktree(name);
|
|
56
68
|
if (!result.success) {
|
|
57
69
|
console.error(result.message);
|
|
58
70
|
exit(1);
|
|
@@ -60,87 +72,26 @@ async function gardensWhereHandler(args2) {
|
|
|
60
72
|
console.log(result.path);
|
|
61
73
|
}
|
|
62
74
|
|
|
63
|
-
// src/commands/exec.ts
|
|
64
|
-
async function execInGarden(gardenName, command2) {
|
|
65
|
-
if (!gardenName) {
|
|
66
|
-
return { success: false, message: "Error: garden name required" };
|
|
67
|
-
}
|
|
68
|
-
if (!command2 || command2.length === 0) {
|
|
69
|
-
return { success: false, message: "Error: command required" };
|
|
70
|
-
}
|
|
71
|
-
const gardenResult = await whereGarden(gardenName);
|
|
72
|
-
if (!gardenResult.success) {
|
|
73
|
-
return { success: false, message: gardenResult.message };
|
|
74
|
-
}
|
|
75
|
-
const gardenPath = gardenResult.path;
|
|
76
|
-
const [cmd, ...args2] = command2;
|
|
77
|
-
return new Promise((resolve) => {
|
|
78
|
-
const childProcess = spawn(cmd, args2, {
|
|
79
|
-
cwd: gardenPath,
|
|
80
|
-
stdio: "inherit"
|
|
81
|
-
});
|
|
82
|
-
childProcess.on("error", (error) => {
|
|
83
|
-
resolve({
|
|
84
|
-
success: false,
|
|
85
|
-
message: `Error executing command: ${error.message}`
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
childProcess.on("exit", (code, signal) => {
|
|
89
|
-
if (signal) {
|
|
90
|
-
resolve({
|
|
91
|
-
success: false,
|
|
92
|
-
message: `Command terminated by signal: ${signal}`,
|
|
93
|
-
exitCode: 128 + (signal === "SIGTERM" ? 15 : 1)
|
|
94
|
-
});
|
|
95
|
-
} else {
|
|
96
|
-
const exitCode = code ?? 0;
|
|
97
|
-
resolve({
|
|
98
|
-
success: exitCode === 0,
|
|
99
|
-
exitCode
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
async function execHandler(args2) {
|
|
106
|
-
if (args2.length < 2) {
|
|
107
|
-
console.error("Usage: phantom exec <garden-name> <command> [args...]");
|
|
108
|
-
exit2(1);
|
|
109
|
-
}
|
|
110
|
-
const gardenName = args2[0];
|
|
111
|
-
const command2 = args2.slice(1);
|
|
112
|
-
const result = await execInGarden(gardenName, command2);
|
|
113
|
-
if (!result.success) {
|
|
114
|
-
if (result.message) {
|
|
115
|
-
console.error(result.message);
|
|
116
|
-
}
|
|
117
|
-
exit2(result.exitCode ?? 1);
|
|
118
|
-
}
|
|
119
|
-
exit2(result.exitCode ?? 0);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
75
|
// src/commands/shell.ts
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!gardenName) {
|
|
127
|
-
return { success: false, message: "Error: garden name required" };
|
|
76
|
+
async function shellInWorktree(worktreeName) {
|
|
77
|
+
if (!worktreeName) {
|
|
78
|
+
return { success: false, message: "Error: worktree name required" };
|
|
128
79
|
}
|
|
129
|
-
const
|
|
130
|
-
if (!
|
|
131
|
-
return { success: false, message:
|
|
80
|
+
const worktreeResult = await whereWorktree(worktreeName);
|
|
81
|
+
if (!worktreeResult.success) {
|
|
82
|
+
return { success: false, message: worktreeResult.message };
|
|
132
83
|
}
|
|
133
|
-
const
|
|
84
|
+
const worktreePath = worktreeResult.path;
|
|
134
85
|
const shell = process.env.SHELL || "/bin/sh";
|
|
135
86
|
return new Promise((resolve) => {
|
|
136
|
-
const childProcess =
|
|
137
|
-
cwd:
|
|
87
|
+
const childProcess = spawn(shell, [], {
|
|
88
|
+
cwd: worktreePath,
|
|
138
89
|
stdio: "inherit",
|
|
139
90
|
env: {
|
|
140
91
|
...process.env,
|
|
141
|
-
// Add environment variable to indicate we're in a
|
|
142
|
-
|
|
143
|
-
|
|
92
|
+
// Add environment variable to indicate we're in a worktree
|
|
93
|
+
WORKTREE_NAME: worktreeName,
|
|
94
|
+
WORKTREE_PATH: worktreePath
|
|
144
95
|
}
|
|
145
96
|
});
|
|
146
97
|
childProcess.on("error", (error) => {
|
|
@@ -168,120 +119,120 @@ async function shellInGarden(gardenName) {
|
|
|
168
119
|
}
|
|
169
120
|
async function shellHandler(args2) {
|
|
170
121
|
if (args2.length < 1) {
|
|
171
|
-
console.error("Usage: phantom shell <
|
|
172
|
-
|
|
122
|
+
console.error("Usage: phantom shell <worktree-name>");
|
|
123
|
+
exit2(1);
|
|
173
124
|
}
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
if (!
|
|
177
|
-
console.error(
|
|
178
|
-
|
|
125
|
+
const worktreeName = args2[0];
|
|
126
|
+
const worktreeResult = await whereWorktree(worktreeName);
|
|
127
|
+
if (!worktreeResult.success) {
|
|
128
|
+
console.error(worktreeResult.message);
|
|
129
|
+
exit2(1);
|
|
179
130
|
}
|
|
180
|
-
console.log(`Entering
|
|
131
|
+
console.log(`Entering worktree '${worktreeName}' at ${worktreeResult.path}`);
|
|
181
132
|
console.log("Type 'exit' to return to your original directory\n");
|
|
182
|
-
const result = await
|
|
133
|
+
const result = await shellInWorktree(worktreeName);
|
|
183
134
|
if (!result.success) {
|
|
184
135
|
if (result.message) {
|
|
185
136
|
console.error(result.message);
|
|
186
137
|
}
|
|
187
|
-
|
|
138
|
+
exit2(result.exitCode ?? 1);
|
|
188
139
|
}
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// src/gardens/commands/create.ts
|
|
193
|
-
import { access as access2, mkdir } from "node:fs/promises";
|
|
194
|
-
import { join as join2 } from "node:path";
|
|
195
|
-
import { exit as exit4 } from "node:process";
|
|
196
|
-
|
|
197
|
-
// src/git/libs/add-worktree.ts
|
|
198
|
-
import { exec as exec2 } from "node:child_process";
|
|
199
|
-
import { promisify as promisify2 } from "node:util";
|
|
200
|
-
var execAsync2 = promisify2(exec2);
|
|
201
|
-
async function addWorktree(options) {
|
|
202
|
-
const { path, branch, commitish = "HEAD" } = options;
|
|
203
|
-
await execAsync2(`git worktree add "${path}" -b "${branch}" ${commitish}`);
|
|
140
|
+
exit2(result.exitCode ?? 0);
|
|
204
141
|
}
|
|
205
142
|
|
|
206
|
-
// src/
|
|
207
|
-
async function
|
|
143
|
+
// src/commands/create.ts
|
|
144
|
+
async function createWorktree(name) {
|
|
208
145
|
if (!name) {
|
|
209
|
-
return { success: false, message: "Error:
|
|
146
|
+
return { success: false, message: "Error: worktree name required" };
|
|
210
147
|
}
|
|
211
148
|
try {
|
|
212
149
|
const gitRoot = await getGitRoot();
|
|
213
|
-
const
|
|
214
|
-
const worktreePath = join2(
|
|
150
|
+
const worktreesPath = join2(gitRoot, ".git", "phantom", "worktrees");
|
|
151
|
+
const worktreePath = join2(worktreesPath, name);
|
|
215
152
|
try {
|
|
216
|
-
await access2(
|
|
153
|
+
await access2(worktreesPath);
|
|
217
154
|
} catch {
|
|
218
|
-
await mkdir(
|
|
155
|
+
await mkdir(worktreesPath, { recursive: true });
|
|
219
156
|
}
|
|
220
157
|
try {
|
|
221
158
|
await access2(worktreePath);
|
|
222
159
|
return {
|
|
223
160
|
success: false,
|
|
224
|
-
message: `Error:
|
|
161
|
+
message: `Error: worktree '${name}' already exists`
|
|
225
162
|
};
|
|
226
163
|
} catch {
|
|
227
164
|
}
|
|
228
165
|
await addWorktree({
|
|
229
166
|
path: worktreePath,
|
|
230
|
-
branch:
|
|
167
|
+
branch: name,
|
|
231
168
|
commitish: "HEAD"
|
|
232
169
|
});
|
|
233
170
|
return {
|
|
234
171
|
success: true,
|
|
235
|
-
message: `Created
|
|
172
|
+
message: `Created worktree '${name}' at ${worktreePath}`,
|
|
236
173
|
path: worktreePath
|
|
237
174
|
};
|
|
238
175
|
} catch (error) {
|
|
239
176
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
240
177
|
return {
|
|
241
178
|
success: false,
|
|
242
|
-
message: `Error creating
|
|
179
|
+
message: `Error creating worktree: ${errorMessage}`
|
|
243
180
|
};
|
|
244
181
|
}
|
|
245
182
|
}
|
|
246
|
-
async function
|
|
183
|
+
async function createHandler(args2) {
|
|
247
184
|
const name = args2[0];
|
|
248
|
-
const
|
|
185
|
+
const openShell = args2.includes("--shell");
|
|
186
|
+
const result = await createWorktree(name);
|
|
249
187
|
if (!result.success) {
|
|
250
188
|
console.error(result.message);
|
|
251
|
-
|
|
189
|
+
exit3(1);
|
|
252
190
|
}
|
|
253
191
|
console.log(result.message);
|
|
192
|
+
if (openShell && result.path) {
|
|
193
|
+
console.log(`
|
|
194
|
+
Entering worktree '${name}' at ${result.path}`);
|
|
195
|
+
console.log("Type 'exit' to return to your original directory\n");
|
|
196
|
+
const shellResult = await shellInWorktree(name);
|
|
197
|
+
if (!shellResult.success) {
|
|
198
|
+
if (shellResult.message) {
|
|
199
|
+
console.error(shellResult.message);
|
|
200
|
+
}
|
|
201
|
+
exit3(shellResult.exitCode ?? 1);
|
|
202
|
+
}
|
|
203
|
+
exit3(shellResult.exitCode ?? 0);
|
|
204
|
+
}
|
|
254
205
|
}
|
|
255
206
|
|
|
256
|
-
// src/
|
|
207
|
+
// src/commands/delete.ts
|
|
257
208
|
import { exec as exec3 } from "node:child_process";
|
|
258
209
|
import { access as access3 } from "node:fs/promises";
|
|
259
210
|
import { join as join3 } from "node:path";
|
|
260
|
-
import { exit as
|
|
211
|
+
import { exit as exit4 } from "node:process";
|
|
261
212
|
import { promisify as promisify3 } from "node:util";
|
|
262
213
|
var execAsync3 = promisify3(exec3);
|
|
263
|
-
async function
|
|
214
|
+
async function deleteWorktree(name, options = {}) {
|
|
264
215
|
if (!name) {
|
|
265
|
-
return { success: false, message: "Error:
|
|
216
|
+
return { success: false, message: "Error: worktree name required" };
|
|
266
217
|
}
|
|
267
218
|
const { force = false } = options;
|
|
268
219
|
try {
|
|
269
220
|
const gitRoot = await getGitRoot();
|
|
270
|
-
const
|
|
271
|
-
const
|
|
221
|
+
const worktreesPath = join3(gitRoot, ".git", "phantom", "worktrees");
|
|
222
|
+
const worktreePath = join3(worktreesPath, name);
|
|
272
223
|
try {
|
|
273
|
-
await access3(
|
|
224
|
+
await access3(worktreePath);
|
|
274
225
|
} catch {
|
|
275
226
|
return {
|
|
276
227
|
success: false,
|
|
277
|
-
message: `Error:
|
|
228
|
+
message: `Error: Worktree '${name}' does not exist`
|
|
278
229
|
};
|
|
279
230
|
}
|
|
280
231
|
let hasUncommittedChanges = false;
|
|
281
232
|
let changedFiles = 0;
|
|
282
233
|
try {
|
|
283
234
|
const { stdout } = await execAsync3("git status --porcelain", {
|
|
284
|
-
cwd:
|
|
235
|
+
cwd: worktreePath
|
|
285
236
|
});
|
|
286
237
|
const changes = stdout.trim();
|
|
287
238
|
if (changes) {
|
|
@@ -294,37 +245,37 @@ async function deleteGarden(name, options = {}) {
|
|
|
294
245
|
if (hasUncommittedChanges && !force) {
|
|
295
246
|
return {
|
|
296
247
|
success: false,
|
|
297
|
-
message: `Error:
|
|
248
|
+
message: `Error: Worktree '${name}' has uncommitted changes (${changedFiles} files). Use --force to delete anyway.`,
|
|
298
249
|
hasUncommittedChanges: true,
|
|
299
250
|
changedFiles
|
|
300
251
|
};
|
|
301
252
|
}
|
|
302
253
|
try {
|
|
303
|
-
await execAsync3(`git worktree remove "${
|
|
254
|
+
await execAsync3(`git worktree remove "${worktreePath}"`, {
|
|
304
255
|
cwd: gitRoot
|
|
305
256
|
});
|
|
306
257
|
} catch (error) {
|
|
307
258
|
try {
|
|
308
|
-
await execAsync3(`git worktree remove --force "${
|
|
259
|
+
await execAsync3(`git worktree remove --force "${worktreePath}"`, {
|
|
309
260
|
cwd: gitRoot
|
|
310
261
|
});
|
|
311
262
|
} catch {
|
|
312
263
|
return {
|
|
313
264
|
success: false,
|
|
314
|
-
message: `Error: Failed to remove worktree
|
|
265
|
+
message: `Error: Failed to remove worktree '${name}'`
|
|
315
266
|
};
|
|
316
267
|
}
|
|
317
268
|
}
|
|
318
|
-
const branchName = `phantom/
|
|
269
|
+
const branchName = `phantom/worktrees/${name}`;
|
|
319
270
|
try {
|
|
320
271
|
await execAsync3(`git branch -D "${branchName}"`, {
|
|
321
272
|
cwd: gitRoot
|
|
322
273
|
});
|
|
323
274
|
} catch {
|
|
324
275
|
}
|
|
325
|
-
let message = `Deleted
|
|
276
|
+
let message = `Deleted worktree '${name}' and its branch '${branchName}'`;
|
|
326
277
|
if (hasUncommittedChanges) {
|
|
327
|
-
message = `Warning:
|
|
278
|
+
message = `Warning: Worktree '${name}' had uncommitted changes (${changedFiles} files)
|
|
328
279
|
${message}`;
|
|
329
280
|
}
|
|
330
281
|
return {
|
|
@@ -337,49 +288,110 @@ ${message}`;
|
|
|
337
288
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
338
289
|
return {
|
|
339
290
|
success: false,
|
|
340
|
-
message: `Error deleting
|
|
291
|
+
message: `Error deleting worktree: ${errorMessage}`
|
|
341
292
|
};
|
|
342
293
|
}
|
|
343
294
|
}
|
|
344
|
-
async function
|
|
295
|
+
async function deleteHandler(args2) {
|
|
345
296
|
const forceIndex = args2.indexOf("--force");
|
|
346
297
|
const force = forceIndex !== -1;
|
|
347
298
|
const filteredArgs = args2.filter((arg) => arg !== "--force");
|
|
348
299
|
const name = filteredArgs[0];
|
|
349
|
-
const result = await
|
|
300
|
+
const result = await deleteWorktree(name, { force });
|
|
350
301
|
if (!result.success) {
|
|
351
302
|
console.error(result.message);
|
|
352
|
-
|
|
303
|
+
exit4(1);
|
|
353
304
|
}
|
|
354
305
|
console.log(result.message);
|
|
355
306
|
}
|
|
356
307
|
|
|
357
|
-
// src/
|
|
308
|
+
// src/commands/exec.ts
|
|
309
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
310
|
+
import { exit as exit5 } from "node:process";
|
|
311
|
+
async function execInWorktree(worktreeName, command2) {
|
|
312
|
+
if (!worktreeName) {
|
|
313
|
+
return { success: false, message: "Error: worktree name required" };
|
|
314
|
+
}
|
|
315
|
+
if (!command2 || command2.length === 0) {
|
|
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
|
+
});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async function execHandler(args2) {
|
|
353
|
+
if (args2.length < 2) {
|
|
354
|
+
console.error("Usage: phantom exec <worktree-name> <command> [args...]");
|
|
355
|
+
exit5(1);
|
|
356
|
+
}
|
|
357
|
+
const worktreeName = args2[0];
|
|
358
|
+
const command2 = args2.slice(1);
|
|
359
|
+
const result = await execInWorktree(worktreeName, command2);
|
|
360
|
+
if (!result.success) {
|
|
361
|
+
if (result.message) {
|
|
362
|
+
console.error(result.message);
|
|
363
|
+
}
|
|
364
|
+
exit5(result.exitCode ?? 1);
|
|
365
|
+
}
|
|
366
|
+
exit5(result.exitCode ?? 0);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/commands/list.ts
|
|
358
370
|
import { exec as exec4 } from "node:child_process";
|
|
359
371
|
import { access as access4, readdir } from "node:fs/promises";
|
|
360
372
|
import { join as join4 } from "node:path";
|
|
361
373
|
import { promisify as promisify4 } from "node:util";
|
|
362
374
|
var execAsync4 = promisify4(exec4);
|
|
363
|
-
async function
|
|
375
|
+
async function listWorktrees() {
|
|
364
376
|
try {
|
|
365
377
|
const gitRoot = await getGitRoot();
|
|
366
|
-
const
|
|
378
|
+
const worktreesPath = join4(gitRoot, ".git", "phantom", "worktrees");
|
|
367
379
|
try {
|
|
368
|
-
await access4(
|
|
380
|
+
await access4(worktreesPath);
|
|
369
381
|
} catch {
|
|
370
382
|
return {
|
|
371
383
|
success: true,
|
|
372
|
-
|
|
373
|
-
message: "No
|
|
384
|
+
worktrees: [],
|
|
385
|
+
message: "No worktrees found (worktrees directory doesn't exist)"
|
|
374
386
|
};
|
|
375
387
|
}
|
|
376
|
-
let
|
|
388
|
+
let worktreeNames;
|
|
377
389
|
try {
|
|
378
|
-
const entries = await readdir(
|
|
390
|
+
const entries = await readdir(worktreesPath);
|
|
379
391
|
const validEntries = await Promise.all(
|
|
380
392
|
entries.map(async (entry) => {
|
|
381
393
|
try {
|
|
382
|
-
const entryPath = join4(
|
|
394
|
+
const entryPath = join4(worktreesPath, entry);
|
|
383
395
|
await access4(entryPath);
|
|
384
396
|
return entry;
|
|
385
397
|
} catch {
|
|
@@ -387,30 +399,30 @@ async function listGardens() {
|
|
|
387
399
|
}
|
|
388
400
|
})
|
|
389
401
|
);
|
|
390
|
-
|
|
402
|
+
worktreeNames = validEntries.filter(
|
|
391
403
|
(entry) => entry !== null
|
|
392
404
|
);
|
|
393
405
|
} catch {
|
|
394
406
|
return {
|
|
395
407
|
success: true,
|
|
396
|
-
|
|
397
|
-
message: "No
|
|
408
|
+
worktrees: [],
|
|
409
|
+
message: "No worktrees found (unable to read worktrees directory)"
|
|
398
410
|
};
|
|
399
411
|
}
|
|
400
|
-
if (
|
|
412
|
+
if (worktreeNames.length === 0) {
|
|
401
413
|
return {
|
|
402
414
|
success: true,
|
|
403
|
-
|
|
404
|
-
message: "No
|
|
415
|
+
worktrees: [],
|
|
416
|
+
message: "No worktrees found"
|
|
405
417
|
};
|
|
406
418
|
}
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
const
|
|
419
|
+
const worktrees = await Promise.all(
|
|
420
|
+
worktreeNames.map(async (name) => {
|
|
421
|
+
const worktreePath = join4(worktreesPath, name);
|
|
410
422
|
let branch = "unknown";
|
|
411
423
|
try {
|
|
412
424
|
const { stdout } = await execAsync4("git branch --show-current", {
|
|
413
|
-
cwd:
|
|
425
|
+
cwd: worktreePath
|
|
414
426
|
});
|
|
415
427
|
branch = stdout.trim() || "detached HEAD";
|
|
416
428
|
} catch {
|
|
@@ -420,7 +432,7 @@ async function listGardens() {
|
|
|
420
432
|
let changedFiles;
|
|
421
433
|
try {
|
|
422
434
|
const { stdout } = await execAsync4("git status --porcelain", {
|
|
423
|
-
cwd:
|
|
435
|
+
cwd: worktreePath
|
|
424
436
|
});
|
|
425
437
|
const changes = stdout.trim();
|
|
426
438
|
if (changes) {
|
|
@@ -440,86 +452,146 @@ async function listGardens() {
|
|
|
440
452
|
);
|
|
441
453
|
return {
|
|
442
454
|
success: true,
|
|
443
|
-
|
|
455
|
+
worktrees
|
|
444
456
|
};
|
|
445
457
|
} catch (error) {
|
|
446
458
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
447
459
|
return {
|
|
448
460
|
success: false,
|
|
449
|
-
message: `Error listing
|
|
461
|
+
message: `Error listing worktrees: ${errorMessage}`
|
|
450
462
|
};
|
|
451
463
|
}
|
|
452
464
|
}
|
|
453
|
-
async function
|
|
454
|
-
const result = await
|
|
465
|
+
async function listHandler() {
|
|
466
|
+
const result = await listWorktrees();
|
|
455
467
|
if (!result.success) {
|
|
456
468
|
console.error(result.message);
|
|
457
469
|
return;
|
|
458
470
|
}
|
|
459
|
-
if (!result.
|
|
460
|
-
console.log(result.message || "No
|
|
471
|
+
if (!result.worktrees || result.worktrees.length === 0) {
|
|
472
|
+
console.log(result.message || "No worktrees found");
|
|
461
473
|
return;
|
|
462
474
|
}
|
|
463
|
-
console.log("
|
|
464
|
-
for (const
|
|
465
|
-
const statusText =
|
|
475
|
+
console.log("Worktrees:");
|
|
476
|
+
for (const worktree of result.worktrees) {
|
|
477
|
+
const statusText = worktree.status === "clean" ? "[clean]" : `[dirty: ${worktree.changedFiles} files]`;
|
|
466
478
|
console.log(
|
|
467
|
-
` ${
|
|
479
|
+
` ${worktree.name.padEnd(20)} (branch: ${worktree.branch.padEnd(20)}) ${statusText}`
|
|
468
480
|
);
|
|
469
481
|
}
|
|
470
482
|
console.log(`
|
|
471
|
-
Total: ${result.
|
|
483
|
+
Total: ${result.worktrees.length} worktrees`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// package.json
|
|
487
|
+
var package_default = {
|
|
488
|
+
name: "@aku11i/phantom",
|
|
489
|
+
packageManager: "pnpm@10.8.1",
|
|
490
|
+
version: "0.3.0",
|
|
491
|
+
description: "A powerful CLI tool for managing Git worktrees for parallel development",
|
|
492
|
+
keywords: [
|
|
493
|
+
"git",
|
|
494
|
+
"worktree",
|
|
495
|
+
"cli",
|
|
496
|
+
"phantom",
|
|
497
|
+
"workspace",
|
|
498
|
+
"development",
|
|
499
|
+
"parallel"
|
|
500
|
+
],
|
|
501
|
+
homepage: "https://github.com/aku11i/phantom#readme",
|
|
502
|
+
bugs: {
|
|
503
|
+
url: "https://github.com/aku11i/phantom/issues"
|
|
504
|
+
},
|
|
505
|
+
repository: {
|
|
506
|
+
type: "git",
|
|
507
|
+
url: "git+https://github.com/aku11i/phantom.git"
|
|
508
|
+
},
|
|
509
|
+
license: "MIT",
|
|
510
|
+
author: "aku11i",
|
|
511
|
+
type: "module",
|
|
512
|
+
bin: {
|
|
513
|
+
phantom: "./dist/phantom.js"
|
|
514
|
+
},
|
|
515
|
+
scripts: {
|
|
516
|
+
start: "node ./src/bin/phantom.ts",
|
|
517
|
+
phantom: "node ./src/bin/phantom.ts",
|
|
518
|
+
build: "node build.ts",
|
|
519
|
+
"type-check": "tsc --noEmit",
|
|
520
|
+
test: "node --test --experimental-strip-types --experimental-test-module-mocks src/**/*.test.ts",
|
|
521
|
+
lint: "biome check .",
|
|
522
|
+
fix: "biome check --write .",
|
|
523
|
+
ready: "pnpm fix && pnpm type-check && pnpm test",
|
|
524
|
+
"ready:check": "pnpm lint && pnpm type-check && pnpm test",
|
|
525
|
+
prepublishOnly: "pnpm ready:check && pnpm build"
|
|
526
|
+
},
|
|
527
|
+
engines: {
|
|
528
|
+
node: ">=22.0.0"
|
|
529
|
+
},
|
|
530
|
+
files: [
|
|
531
|
+
"dist/",
|
|
532
|
+
"README.md",
|
|
533
|
+
"LICENSE"
|
|
534
|
+
],
|
|
535
|
+
devDependencies: {
|
|
536
|
+
"@biomejs/biome": "^1.9.4",
|
|
537
|
+
"@types/node": "^22.15.29",
|
|
538
|
+
esbuild: "^0.25.5",
|
|
539
|
+
typescript: "^5.8.3"
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// src/commands/version.ts
|
|
544
|
+
function getVersion() {
|
|
545
|
+
return package_default.version;
|
|
546
|
+
}
|
|
547
|
+
function versionHandler() {
|
|
548
|
+
const version = getVersion();
|
|
549
|
+
console.log(`Phantom v${version}`);
|
|
472
550
|
}
|
|
473
551
|
|
|
474
552
|
// src/bin/phantom.ts
|
|
475
553
|
var commands = [
|
|
476
554
|
{
|
|
477
|
-
name: "
|
|
478
|
-
description: "
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
{
|
|
496
|
-
name: "delete",
|
|
497
|
-
description: "Delete a garden (use --force for dirty gardens)",
|
|
498
|
-
handler: gardensDeleteHandler
|
|
499
|
-
}
|
|
500
|
-
]
|
|
555
|
+
name: "create",
|
|
556
|
+
description: "Create a new worktree [--shell to open shell]",
|
|
557
|
+
handler: createHandler
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: "list",
|
|
561
|
+
description: "List all worktrees",
|
|
562
|
+
handler: listHandler
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: "where",
|
|
566
|
+
description: "Output the path of a specific worktree",
|
|
567
|
+
handler: whereHandler
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "delete",
|
|
571
|
+
description: "Delete a worktree (use --force for uncommitted changes)",
|
|
572
|
+
handler: deleteHandler
|
|
501
573
|
},
|
|
502
574
|
{
|
|
503
575
|
name: "exec",
|
|
504
|
-
description: "Execute a command in a
|
|
576
|
+
description: "Execute a command in a worktree directory",
|
|
505
577
|
handler: execHandler
|
|
506
578
|
},
|
|
507
579
|
{
|
|
508
580
|
name: "shell",
|
|
509
|
-
description: "Open interactive shell in a
|
|
581
|
+
description: "Open interactive shell in a worktree directory",
|
|
510
582
|
handler: shellHandler
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "version",
|
|
586
|
+
description: "Display phantom version",
|
|
587
|
+
handler: versionHandler
|
|
511
588
|
}
|
|
512
589
|
];
|
|
513
|
-
function printHelp(commands2
|
|
590
|
+
function printHelp(commands2) {
|
|
514
591
|
console.log("Usage: phantom <command> [options]\n");
|
|
515
592
|
console.log("Commands:");
|
|
516
593
|
for (const cmd of commands2) {
|
|
517
|
-
console.log(` ${
|
|
518
|
-
if (cmd.subcommands) {
|
|
519
|
-
for (const subcmd of cmd.subcommands) {
|
|
520
|
-
console.log(` ${subcmd.name.padEnd(18)} ${subcmd.description}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
594
|
+
console.log(` ${cmd.name.padEnd(12)} ${cmd.description}`);
|
|
523
595
|
}
|
|
524
596
|
}
|
|
525
597
|
function findCommand(args2, commands2) {
|
|
@@ -547,6 +619,10 @@ if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
|
|
|
547
619
|
printHelp(commands);
|
|
548
620
|
exit6(0);
|
|
549
621
|
}
|
|
622
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
623
|
+
versionHandler();
|
|
624
|
+
exit6(0);
|
|
625
|
+
}
|
|
550
626
|
var { command, remainingArgs } = findCommand(args, commands);
|
|
551
627
|
if (!command || !command.handler) {
|
|
552
628
|
console.error(`Error: Unknown command '${args.join(" ")}'
|