@aku11i/phantom 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/garden.js +394 -0
- package/dist/garden.js.map +7 -0
- package/dist/phantom.js +566 -0
- package/dist/phantom.js.map +7 -0
- package/package.json +6 -4
- package/src/bin/garden.ts +0 -74
- package/src/bin/phantom.ts +0 -121
- package/src/commands/exec.test.ts +0 -256
- package/src/commands/exec.ts +0 -81
- package/src/commands/shell.test.ts +0 -248
- package/src/commands/shell.ts +0 -91
- package/src/gardens/commands/create.test.ts +0 -176
- package/src/gardens/commands/create.ts +0 -67
- package/src/gardens/commands/delete.test.ts +0 -316
- package/src/gardens/commands/delete.ts +0 -134
- package/src/gardens/commands/list.test.ts +0 -243
- package/src/gardens/commands/list.ts +0 -152
- package/src/gardens/commands/where.test.ts +0 -149
- package/src/gardens/commands/where.ts +0 -53
- package/src/git/libs/add-worktree.ts +0 -16
- package/src/git/libs/get-current-branch.ts +0 -9
- package/src/git/libs/get-git-root.ts +0 -9
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import { strictEqual } from "node:assert";
|
|
2
|
-
import { before, describe, it, mock } from "node:test";
|
|
3
|
-
|
|
4
|
-
describe("shellInGarden", () => {
|
|
5
|
-
let spawnMock: ReturnType<typeof mock.fn>;
|
|
6
|
-
let whereGardenMock: ReturnType<typeof mock.fn>;
|
|
7
|
-
let shellInGarden: typeof import("./shell.ts").shellInGarden;
|
|
8
|
-
const originalEnv = process.env;
|
|
9
|
-
|
|
10
|
-
before(async () => {
|
|
11
|
-
spawnMock = mock.fn();
|
|
12
|
-
whereGardenMock = mock.fn();
|
|
13
|
-
|
|
14
|
-
mock.module("node:child_process", {
|
|
15
|
-
namedExports: {
|
|
16
|
-
spawn: spawnMock,
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
mock.module("../gardens/commands/where.ts", {
|
|
21
|
-
namedExports: {
|
|
22
|
-
whereGarden: whereGardenMock,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
({ shellInGarden } = await import("./shell.ts"));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("should return error when garden name is not provided", async () => {
|
|
30
|
-
const result = await shellInGarden("");
|
|
31
|
-
strictEqual(result.success, false);
|
|
32
|
-
strictEqual(result.message, "Error: garden name required");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("should return error when garden does not exist", async () => {
|
|
36
|
-
whereGardenMock.mock.resetCalls();
|
|
37
|
-
spawnMock.mock.resetCalls();
|
|
38
|
-
|
|
39
|
-
whereGardenMock.mock.mockImplementation(() =>
|
|
40
|
-
Promise.resolve({
|
|
41
|
-
success: false,
|
|
42
|
-
message: "Error: Garden 'nonexistent' does not exist",
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const result = await shellInGarden("nonexistent");
|
|
47
|
-
|
|
48
|
-
strictEqual(result.success, false);
|
|
49
|
-
strictEqual(result.message, "Error: Garden 'nonexistent' does not exist");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("should start shell successfully with exit code 0", async () => {
|
|
53
|
-
whereGardenMock.mock.resetCalls();
|
|
54
|
-
spawnMock.mock.resetCalls();
|
|
55
|
-
|
|
56
|
-
// Mock successful garden location
|
|
57
|
-
whereGardenMock.mock.mockImplementation(() =>
|
|
58
|
-
Promise.resolve({
|
|
59
|
-
success: true,
|
|
60
|
-
path: "/test/repo/.git/phantom/gardens/test-garden",
|
|
61
|
-
}),
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
// Mock successful shell session
|
|
65
|
-
const mockChildProcess = {
|
|
66
|
-
on: mock.fn(
|
|
67
|
-
(
|
|
68
|
-
event: string,
|
|
69
|
-
callback: (code: number | null, signal: string | null) => void,
|
|
70
|
-
) => {
|
|
71
|
-
if (event === "exit") {
|
|
72
|
-
// Simulate successful shell exit
|
|
73
|
-
setTimeout(() => callback(0, null), 0);
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
),
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
spawnMock.mock.mockImplementation(() => mockChildProcess);
|
|
80
|
-
|
|
81
|
-
const result = await shellInGarden("test-garden");
|
|
82
|
-
|
|
83
|
-
strictEqual(result.success, true);
|
|
84
|
-
strictEqual(result.exitCode, 0);
|
|
85
|
-
|
|
86
|
-
// Verify spawn was called with correct arguments
|
|
87
|
-
strictEqual(spawnMock.mock.calls.length, 1);
|
|
88
|
-
const [shell, args, options] = spawnMock.mock.calls[0].arguments as [
|
|
89
|
-
string,
|
|
90
|
-
string[],
|
|
91
|
-
{ cwd: string; stdio: string; env: NodeJS.ProcessEnv },
|
|
92
|
-
];
|
|
93
|
-
strictEqual(shell, process.env.SHELL || "/bin/sh");
|
|
94
|
-
strictEqual(args.length, 0);
|
|
95
|
-
strictEqual(options.cwd, "/test/repo/.git/phantom/gardens/test-garden");
|
|
96
|
-
strictEqual(options.stdio, "inherit");
|
|
97
|
-
strictEqual(options.env.PHANTOM_GARDEN, "test-garden");
|
|
98
|
-
strictEqual(
|
|
99
|
-
options.env.PHANTOM_GARDEN_PATH,
|
|
100
|
-
"/test/repo/.git/phantom/gardens/test-garden",
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("should use /bin/sh when SHELL is not set", async () => {
|
|
105
|
-
whereGardenMock.mock.resetCalls();
|
|
106
|
-
spawnMock.mock.resetCalls();
|
|
107
|
-
|
|
108
|
-
// Temporarily remove SHELL env var
|
|
109
|
-
const originalShell = process.env.SHELL;
|
|
110
|
-
// biome-ignore lint/performance/noDelete: Need to actually delete for test
|
|
111
|
-
delete process.env.SHELL;
|
|
112
|
-
|
|
113
|
-
// Mock successful garden location
|
|
114
|
-
whereGardenMock.mock.mockImplementation(() =>
|
|
115
|
-
Promise.resolve({
|
|
116
|
-
success: true,
|
|
117
|
-
path: "/test/repo/.git/phantom/gardens/test-garden",
|
|
118
|
-
}),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// Mock successful shell session
|
|
122
|
-
const mockChildProcess = {
|
|
123
|
-
on: mock.fn(
|
|
124
|
-
(
|
|
125
|
-
event: string,
|
|
126
|
-
callback: (code: number | null, signal: string | null) => void,
|
|
127
|
-
) => {
|
|
128
|
-
if (event === "exit") {
|
|
129
|
-
setTimeout(() => callback(0, null), 0);
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
),
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
spawnMock.mock.mockImplementation(() => mockChildProcess);
|
|
136
|
-
|
|
137
|
-
await shellInGarden("test-garden");
|
|
138
|
-
|
|
139
|
-
// Verify /bin/sh was used
|
|
140
|
-
const [shell] = spawnMock.mock.calls[0].arguments as [string, unknown];
|
|
141
|
-
strictEqual(shell, "/bin/sh");
|
|
142
|
-
|
|
143
|
-
// Restore SHELL env var
|
|
144
|
-
if (originalShell !== undefined) {
|
|
145
|
-
process.env.SHELL = originalShell;
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("should handle shell execution failure with non-zero exit code", async () => {
|
|
150
|
-
whereGardenMock.mock.resetCalls();
|
|
151
|
-
spawnMock.mock.resetCalls();
|
|
152
|
-
|
|
153
|
-
// Mock successful garden location
|
|
154
|
-
whereGardenMock.mock.mockImplementation(() =>
|
|
155
|
-
Promise.resolve({
|
|
156
|
-
success: true,
|
|
157
|
-
path: "/test/repo/.git/phantom/gardens/test-garden",
|
|
158
|
-
}),
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
// Mock failed shell session
|
|
162
|
-
const mockChildProcess = {
|
|
163
|
-
on: mock.fn(
|
|
164
|
-
(
|
|
165
|
-
event: string,
|
|
166
|
-
callback: (code: number | null, signal: string | null) => void,
|
|
167
|
-
) => {
|
|
168
|
-
if (event === "exit") {
|
|
169
|
-
// Simulate failed shell exit
|
|
170
|
-
setTimeout(() => callback(1, null), 0);
|
|
171
|
-
}
|
|
172
|
-
},
|
|
173
|
-
),
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
spawnMock.mock.mockImplementation(() => mockChildProcess);
|
|
177
|
-
|
|
178
|
-
const result = await shellInGarden("test-garden");
|
|
179
|
-
|
|
180
|
-
strictEqual(result.success, false);
|
|
181
|
-
strictEqual(result.exitCode, 1);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("should handle shell startup error", async () => {
|
|
185
|
-
whereGardenMock.mock.resetCalls();
|
|
186
|
-
spawnMock.mock.resetCalls();
|
|
187
|
-
|
|
188
|
-
// Mock successful garden location
|
|
189
|
-
whereGardenMock.mock.mockImplementation(() =>
|
|
190
|
-
Promise.resolve({
|
|
191
|
-
success: true,
|
|
192
|
-
path: "/test/repo/.git/phantom/gardens/test-garden",
|
|
193
|
-
}),
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
// Mock shell startup error
|
|
197
|
-
const mockChildProcess = {
|
|
198
|
-
on: mock.fn((event: string, callback: (error: Error) => void) => {
|
|
199
|
-
if (event === "error") {
|
|
200
|
-
setTimeout(() => callback(new Error("Shell not found")), 0);
|
|
201
|
-
}
|
|
202
|
-
}),
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
spawnMock.mock.mockImplementation(() => mockChildProcess);
|
|
206
|
-
|
|
207
|
-
const result = await shellInGarden("test-garden");
|
|
208
|
-
|
|
209
|
-
strictEqual(result.success, false);
|
|
210
|
-
strictEqual(result.message, "Error starting shell: Shell not found");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("should handle signal termination", async () => {
|
|
214
|
-
whereGardenMock.mock.resetCalls();
|
|
215
|
-
spawnMock.mock.resetCalls();
|
|
216
|
-
|
|
217
|
-
// Mock successful garden location
|
|
218
|
-
whereGardenMock.mock.mockImplementation(() =>
|
|
219
|
-
Promise.resolve({
|
|
220
|
-
success: true,
|
|
221
|
-
path: "/test/repo/.git/phantom/gardens/test-garden",
|
|
222
|
-
}),
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
// Mock signal termination
|
|
226
|
-
const mockChildProcess = {
|
|
227
|
-
on: mock.fn(
|
|
228
|
-
(
|
|
229
|
-
event: string,
|
|
230
|
-
callback: (code: number | null, signal: string | null) => void,
|
|
231
|
-
) => {
|
|
232
|
-
if (event === "exit") {
|
|
233
|
-
// Simulate signal termination
|
|
234
|
-
setTimeout(() => callback(null, "SIGTERM"), 0);
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
),
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
spawnMock.mock.mockImplementation(() => mockChildProcess);
|
|
241
|
-
|
|
242
|
-
const result = await shellInGarden("test-garden");
|
|
243
|
-
|
|
244
|
-
strictEqual(result.success, false);
|
|
245
|
-
strictEqual(result.message, "Shell terminated by signal: SIGTERM");
|
|
246
|
-
strictEqual(result.exitCode, 143); // 128 + 15 (SIGTERM)
|
|
247
|
-
});
|
|
248
|
-
});
|
package/src/commands/shell.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { exit } from "node:process";
|
|
3
|
-
import { whereGarden } from "../gardens/commands/where.ts";
|
|
4
|
-
|
|
5
|
-
export async function shellInGarden(gardenName: string): Promise<{
|
|
6
|
-
success: boolean;
|
|
7
|
-
message?: string;
|
|
8
|
-
exitCode?: number;
|
|
9
|
-
}> {
|
|
10
|
-
if (!gardenName) {
|
|
11
|
-
return { success: false, message: "Error: garden name required" };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Validate garden exists and get its path
|
|
15
|
-
const gardenResult = await whereGarden(gardenName);
|
|
16
|
-
if (!gardenResult.success) {
|
|
17
|
-
return { success: false, message: gardenResult.message };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const gardenPath = gardenResult.path as string;
|
|
21
|
-
// Use user's preferred shell or fallback to /bin/sh
|
|
22
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
23
|
-
|
|
24
|
-
return new Promise((resolve) => {
|
|
25
|
-
const childProcess = spawn(shell, [], {
|
|
26
|
-
cwd: gardenPath,
|
|
27
|
-
stdio: "inherit",
|
|
28
|
-
env: {
|
|
29
|
-
...process.env,
|
|
30
|
-
// Add environment variable to indicate we're in a phantom garden
|
|
31
|
-
PHANTOM_GARDEN: gardenName,
|
|
32
|
-
PHANTOM_GARDEN_PATH: gardenPath,
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
childProcess.on("error", (error) => {
|
|
37
|
-
resolve({
|
|
38
|
-
success: false,
|
|
39
|
-
message: `Error starting shell: ${error.message}`,
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
childProcess.on("exit", (code, signal) => {
|
|
44
|
-
if (signal) {
|
|
45
|
-
resolve({
|
|
46
|
-
success: false,
|
|
47
|
-
message: `Shell terminated by signal: ${signal}`,
|
|
48
|
-
exitCode: 128 + (signal === "SIGTERM" ? 15 : 1),
|
|
49
|
-
});
|
|
50
|
-
} else {
|
|
51
|
-
const exitCode = code ?? 0;
|
|
52
|
-
resolve({
|
|
53
|
-
success: exitCode === 0,
|
|
54
|
-
exitCode,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function shellHandler(args: string[]): Promise<void> {
|
|
62
|
-
if (args.length < 1) {
|
|
63
|
-
console.error("Usage: phantom shell <garden-name>");
|
|
64
|
-
exit(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const gardenName = args[0];
|
|
68
|
-
|
|
69
|
-
// Get garden path for display
|
|
70
|
-
const gardenResult = await whereGarden(gardenName);
|
|
71
|
-
if (!gardenResult.success) {
|
|
72
|
-
console.error(gardenResult.message);
|
|
73
|
-
exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Display entering message
|
|
77
|
-
console.log(`Entering garden '${gardenName}' at ${gardenResult.path}`);
|
|
78
|
-
console.log("Type 'exit' to return to your original directory\n");
|
|
79
|
-
|
|
80
|
-
const result = await shellInGarden(gardenName);
|
|
81
|
-
|
|
82
|
-
if (!result.success) {
|
|
83
|
-
if (result.message) {
|
|
84
|
-
console.error(result.message);
|
|
85
|
-
}
|
|
86
|
-
exit(result.exitCode ?? 1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Exit with the same code as the shell
|
|
90
|
-
exit(result.exitCode ?? 0);
|
|
91
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { deepStrictEqual, strictEqual } from "node:assert";
|
|
2
|
-
import { before, describe, it, mock } from "node:test";
|
|
3
|
-
|
|
4
|
-
describe("createGarden", () => {
|
|
5
|
-
let accessMock: ReturnType<typeof mock.fn>;
|
|
6
|
-
let mkdirMock: ReturnType<typeof mock.fn>;
|
|
7
|
-
let execMock: ReturnType<typeof mock.fn>;
|
|
8
|
-
let createGarden: typeof import("./create.ts").createGarden;
|
|
9
|
-
|
|
10
|
-
before(async () => {
|
|
11
|
-
accessMock = mock.fn();
|
|
12
|
-
mkdirMock = mock.fn();
|
|
13
|
-
execMock = mock.fn((cmd: string) => {
|
|
14
|
-
if (cmd === "git rev-parse --show-toplevel") {
|
|
15
|
-
return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
|
|
16
|
-
}
|
|
17
|
-
if (cmd.startsWith("git worktree add")) {
|
|
18
|
-
return Promise.resolve({ stdout: "", stderr: "" });
|
|
19
|
-
}
|
|
20
|
-
return Promise.resolve({ stdout: "", stderr: "" });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
mock.module("node:fs/promises", {
|
|
24
|
-
namedExports: {
|
|
25
|
-
access: accessMock,
|
|
26
|
-
mkdir: mkdirMock,
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
mock.module("node:child_process", {
|
|
31
|
-
namedExports: {
|
|
32
|
-
exec: execMock,
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
mock.module("node:util", {
|
|
37
|
-
namedExports: {
|
|
38
|
-
promisify: (fn: unknown) => fn,
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
({ createGarden } = await import("./create.ts"));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("should return error when name is not provided", async () => {
|
|
46
|
-
const result = await createGarden("");
|
|
47
|
-
strictEqual(result.success, false);
|
|
48
|
-
strictEqual(result.message, "Error: garden name required");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("should create garden directory when it does not exist", async () => {
|
|
52
|
-
accessMock.mock.resetCalls();
|
|
53
|
-
mkdirMock.mock.resetCalls();
|
|
54
|
-
execMock.mock.resetCalls();
|
|
55
|
-
|
|
56
|
-
accessMock.mock.mockImplementation((path: string) => {
|
|
57
|
-
if (path === "/test/repo/.git/phantom/gardens") {
|
|
58
|
-
return Promise.reject(new Error("ENOENT"));
|
|
59
|
-
}
|
|
60
|
-
if (path === "/test/repo/.git/phantom/gardens/test-garden") {
|
|
61
|
-
return Promise.reject(new Error("ENOENT"));
|
|
62
|
-
}
|
|
63
|
-
return Promise.resolve();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
execMock.mock.mockImplementation((cmd: string) => {
|
|
67
|
-
if (cmd === "git rev-parse --show-toplevel") {
|
|
68
|
-
return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
|
|
69
|
-
}
|
|
70
|
-
if (cmd.startsWith("git worktree add")) {
|
|
71
|
-
return Promise.resolve({ stdout: "", stderr: "" });
|
|
72
|
-
}
|
|
73
|
-
return Promise.resolve({ stdout: "", stderr: "" });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const result = await createGarden("test-garden");
|
|
77
|
-
|
|
78
|
-
strictEqual(result.success, true);
|
|
79
|
-
strictEqual(
|
|
80
|
-
result.message,
|
|
81
|
-
"Created garden 'test-garden' at /test/repo/.git/phantom/gardens/test-garden",
|
|
82
|
-
);
|
|
83
|
-
strictEqual(result.path, "/test/repo/.git/phantom/gardens/test-garden");
|
|
84
|
-
|
|
85
|
-
strictEqual(mkdirMock.mock.calls.length, 1);
|
|
86
|
-
deepStrictEqual(mkdirMock.mock.calls[0].arguments, [
|
|
87
|
-
"/test/repo/.git/phantom/gardens",
|
|
88
|
-
{ recursive: true },
|
|
89
|
-
]);
|
|
90
|
-
|
|
91
|
-
strictEqual(execMock.mock.calls.length, 2);
|
|
92
|
-
strictEqual(
|
|
93
|
-
execMock.mock.calls[0].arguments[0],
|
|
94
|
-
"git rev-parse --show-toplevel",
|
|
95
|
-
);
|
|
96
|
-
strictEqual(
|
|
97
|
-
execMock.mock.calls[1].arguments[0],
|
|
98
|
-
'git worktree add "/test/repo/.git/phantom/gardens/test-garden" -b "phantom/gardens/test-garden" HEAD',
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("should return error when garden already exists", async () => {
|
|
103
|
-
accessMock.mock.resetCalls();
|
|
104
|
-
mkdirMock.mock.resetCalls();
|
|
105
|
-
execMock.mock.resetCalls();
|
|
106
|
-
|
|
107
|
-
accessMock.mock.mockImplementation((path: string) => {
|
|
108
|
-
if (path === "/test/repo/.git/phantom/gardens") {
|
|
109
|
-
return Promise.resolve();
|
|
110
|
-
}
|
|
111
|
-
if (path === "/test/repo/.git/phantom/gardens/existing-garden") {
|
|
112
|
-
return Promise.resolve();
|
|
113
|
-
}
|
|
114
|
-
return Promise.reject(new Error("ENOENT"));
|
|
115
|
-
});
|
|
116
|
-
execMock.mock.mockImplementation((cmd: string) => {
|
|
117
|
-
if (cmd === "git rev-parse --show-toplevel") {
|
|
118
|
-
return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
|
|
119
|
-
}
|
|
120
|
-
return Promise.resolve({ stdout: "", stderr: "" });
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const result = await createGarden("existing-garden");
|
|
124
|
-
|
|
125
|
-
strictEqual(result.success, false);
|
|
126
|
-
strictEqual(
|
|
127
|
-
result.message,
|
|
128
|
-
"Error: garden 'existing-garden' already exists",
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("should handle git command errors", async () => {
|
|
133
|
-
accessMock.mock.resetCalls();
|
|
134
|
-
mkdirMock.mock.resetCalls();
|
|
135
|
-
execMock.mock.resetCalls();
|
|
136
|
-
|
|
137
|
-
execMock.mock.mockImplementation(() => {
|
|
138
|
-
return Promise.reject(new Error("Not a git repository"));
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const result = await createGarden("test-garden");
|
|
142
|
-
|
|
143
|
-
strictEqual(result.success, false);
|
|
144
|
-
strictEqual(result.message, "Error creating garden: Not a git repository");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("should not create gardens directory if it already exists", async () => {
|
|
148
|
-
accessMock.mock.resetCalls();
|
|
149
|
-
mkdirMock.mock.resetCalls();
|
|
150
|
-
execMock.mock.resetCalls();
|
|
151
|
-
|
|
152
|
-
accessMock.mock.mockImplementation((path: string) => {
|
|
153
|
-
if (path === "/test/repo/.git/phantom/gardens") {
|
|
154
|
-
return Promise.resolve();
|
|
155
|
-
}
|
|
156
|
-
if (path === "/test/repo/.git/phantom/gardens/test-garden") {
|
|
157
|
-
return Promise.reject(new Error("ENOENT"));
|
|
158
|
-
}
|
|
159
|
-
return Promise.reject(new Error("ENOENT"));
|
|
160
|
-
});
|
|
161
|
-
execMock.mock.mockImplementation((cmd: string) => {
|
|
162
|
-
if (cmd === "git rev-parse --show-toplevel") {
|
|
163
|
-
return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
|
|
164
|
-
}
|
|
165
|
-
if (cmd.startsWith("git worktree add")) {
|
|
166
|
-
return Promise.resolve({ stdout: "", stderr: "" });
|
|
167
|
-
}
|
|
168
|
-
return Promise.resolve({ stdout: "", stderr: "" });
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const result = await createGarden("test-garden");
|
|
172
|
-
|
|
173
|
-
strictEqual(result.success, true);
|
|
174
|
-
strictEqual(mkdirMock.mock.calls.length, 0);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { access, mkdir } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { exit } from "node:process";
|
|
4
|
-
import { addWorktree } from "../../git/libs/add-worktree.ts";
|
|
5
|
-
import { getGitRoot } from "../../git/libs/get-git-root.ts";
|
|
6
|
-
|
|
7
|
-
export async function createGarden(name: string): Promise<{
|
|
8
|
-
success: boolean;
|
|
9
|
-
message: string;
|
|
10
|
-
path?: string;
|
|
11
|
-
}> {
|
|
12
|
-
if (!name) {
|
|
13
|
-
return { success: false, message: "Error: garden name required" };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const gitRoot = await getGitRoot();
|
|
18
|
-
const gardensPath = join(gitRoot, ".git", "phantom", "gardens");
|
|
19
|
-
const worktreePath = join(gardensPath, name);
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
await access(gardensPath);
|
|
23
|
-
} catch {
|
|
24
|
-
await mkdir(gardensPath, { recursive: true });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
await access(worktreePath);
|
|
29
|
-
return {
|
|
30
|
-
success: false,
|
|
31
|
-
message: `Error: garden '${name}' already exists`,
|
|
32
|
-
};
|
|
33
|
-
} catch {
|
|
34
|
-
// Path doesn't exist, which is what we want
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
await addWorktree({
|
|
38
|
-
path: worktreePath,
|
|
39
|
-
branch: `phantom/gardens/${name}`,
|
|
40
|
-
commitish: "HEAD",
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
success: true,
|
|
45
|
-
message: `Created garden '${name}' at ${worktreePath}`,
|
|
46
|
-
path: worktreePath,
|
|
47
|
-
};
|
|
48
|
-
} catch (error) {
|
|
49
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
50
|
-
return {
|
|
51
|
-
success: false,
|
|
52
|
-
message: `Error creating garden: ${errorMessage}`,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function gardensCreateHandler(args: string[]): Promise<void> {
|
|
58
|
-
const name = args[0];
|
|
59
|
-
const result = await createGarden(name);
|
|
60
|
-
|
|
61
|
-
if (!result.success) {
|
|
62
|
-
console.error(result.message);
|
|
63
|
-
exit(1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
console.log(result.message);
|
|
67
|
-
}
|