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