@simonfestl/husky-cli 0.3.0 → 0.5.1
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 +228 -58
- package/dist/commands/changelog.d.ts +2 -0
- package/dist/commands/changelog.js +401 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +400 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +101 -1
- package/dist/commands/department.d.ts +2 -0
- package/dist/commands/department.js +240 -0
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +411 -0
- package/dist/commands/idea.d.ts +2 -0
- package/dist/commands/idea.js +340 -0
- package/dist/commands/interactive.d.ts +1 -0
- package/dist/commands/interactive.js +1397 -0
- package/dist/commands/jules.d.ts +2 -0
- package/dist/commands/jules.js +593 -0
- package/dist/commands/process.d.ts +2 -0
- package/dist/commands/process.js +289 -0
- package/dist/commands/project.d.ts +2 -0
- package/dist/commands/project.js +473 -0
- package/dist/commands/roadmap.js +318 -0
- package/dist/commands/settings.d.ts +2 -0
- package/dist/commands/settings.js +153 -0
- package/dist/commands/strategy.d.ts +2 -0
- package/dist/commands/strategy.js +706 -0
- package/dist/commands/task.js +244 -1
- package/dist/commands/vm-config.d.ts +2 -0
- package/dist/commands/vm-config.js +318 -0
- package/dist/commands/vm.d.ts +2 -0
- package/dist/commands/vm.js +621 -0
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +545 -0
- package/dist/index.js +35 -2
- package/package.json +8 -2
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
import * as readline from "readline";
|
|
4
|
+
export const vmCommand = new Command("vm")
|
|
5
|
+
.description("Manage VM sessions");
|
|
6
|
+
// Helper: Ensure API is configured
|
|
7
|
+
function ensureConfig() {
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
if (!config.apiUrl) {
|
|
10
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
// Helper: Prompt for confirmation
|
|
16
|
+
async function confirm(message) {
|
|
17
|
+
const rl = readline.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stdout,
|
|
20
|
+
});
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
rl.question(`${message} (y/N): `, (answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function formatStatus(status) {
|
|
29
|
+
return status.replace(/_/g, " ").toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
// husky vm list
|
|
32
|
+
vmCommand
|
|
33
|
+
.command("list")
|
|
34
|
+
.description("List all VM sessions")
|
|
35
|
+
.option("--json", "Output as JSON")
|
|
36
|
+
.option("--status <status>", "Filter by status (pending, starting, running, completed, failed, terminated)")
|
|
37
|
+
.option("--agent <agent>", "Filter by agent type (claude-code, gemini-cli, aider, custom)")
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
const config = ensureConfig();
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL("/api/vm-sessions", config.apiUrl);
|
|
42
|
+
if (options.status) {
|
|
43
|
+
url.searchParams.set("vmStatus", options.status);
|
|
44
|
+
}
|
|
45
|
+
const res = await fetch(url.toString(), {
|
|
46
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
throw new Error(`API error: ${res.status}`);
|
|
50
|
+
}
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
let sessions = data.sessions || [];
|
|
53
|
+
// Filter by agent type if specified
|
|
54
|
+
if (options.agent) {
|
|
55
|
+
sessions = sessions.filter((s) => s.agentType === options.agent);
|
|
56
|
+
}
|
|
57
|
+
if (options.json) {
|
|
58
|
+
console.log(JSON.stringify({ sessions, stats: data.stats }, null, 2));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
printVMSessions(sessions, data.stats);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error("Error fetching VM sessions:", error);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// husky vm create <name>
|
|
70
|
+
vmCommand
|
|
71
|
+
.command("create <name>")
|
|
72
|
+
.description("Create a new VM session")
|
|
73
|
+
.option("-p, --prompt <prompt>", "Initial prompt for the agent")
|
|
74
|
+
.option("--agent <agent>", "Agent type (claude-code, gemini-cli, aider, custom)", "claude-code")
|
|
75
|
+
.option("--config <configId>", "VM config to use")
|
|
76
|
+
.option("--project <projectId>", "Link to project")
|
|
77
|
+
.option("--task <taskId>", "Link to task")
|
|
78
|
+
.option("--repo <repoUrl>", "Git repository URL")
|
|
79
|
+
.option("--branch <branch>", "Git branch to use")
|
|
80
|
+
.option("--machine-type <machineType>", "GCP machine type", "e2-medium")
|
|
81
|
+
.option("--zone <zone>", "GCP zone", "us-central1-a")
|
|
82
|
+
.option("--json", "Output as JSON")
|
|
83
|
+
.action(async (name, options) => {
|
|
84
|
+
const config = ensureConfig();
|
|
85
|
+
if (!options.prompt) {
|
|
86
|
+
console.error("Error: --prompt is required");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(`${config.apiUrl}/api/vm-sessions`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
name,
|
|
98
|
+
prompt: options.prompt,
|
|
99
|
+
agentType: options.agent,
|
|
100
|
+
taskId: options.task,
|
|
101
|
+
workflowId: options.project,
|
|
102
|
+
repoUrl: options.repo,
|
|
103
|
+
branch: options.branch,
|
|
104
|
+
machineType: options.machineType,
|
|
105
|
+
zone: options.zone,
|
|
106
|
+
startTrigger: "manual",
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
const errorData = await res.json().catch(() => ({}));
|
|
111
|
+
throw new Error(errorData.error || `API error: ${res.status}`);
|
|
112
|
+
}
|
|
113
|
+
const session = await res.json();
|
|
114
|
+
if (options.json) {
|
|
115
|
+
console.log(JSON.stringify(session, null, 2));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.log(`Created VM session: ${session.name}`);
|
|
119
|
+
console.log(` ID: ${session.id}`);
|
|
120
|
+
console.log(` Agent: ${session.agentType}`);
|
|
121
|
+
console.log(` Status: ${formatStatus(session.vmStatus)}`);
|
|
122
|
+
console.log(` VM Name: ${session.vmName}`);
|
|
123
|
+
console.log(`\nTo start the VM, run: husky vm start ${session.id}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error("Error creating VM session:", error);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// husky vm get <id>
|
|
132
|
+
vmCommand
|
|
133
|
+
.command("get <id>")
|
|
134
|
+
.description("Get VM session details")
|
|
135
|
+
.option("--json", "Output as JSON")
|
|
136
|
+
.action(async (id, options) => {
|
|
137
|
+
const config = ensureConfig();
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(`${config.apiUrl}/api/vm-sessions/${id}`, {
|
|
140
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
if (res.status === 404) {
|
|
144
|
+
console.error(`Error: VM session ${id} not found`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
console.error(`Error: API returned ${res.status}`);
|
|
148
|
+
}
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const session = await res.json();
|
|
152
|
+
if (options.json) {
|
|
153
|
+
console.log(JSON.stringify(session, null, 2));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
printVMSessionDetail(session);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error("Error fetching VM session:", error);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// husky vm update <id>
|
|
165
|
+
vmCommand
|
|
166
|
+
.command("update <id>")
|
|
167
|
+
.description("Update VM session")
|
|
168
|
+
.option("-n, --name <name>", "New name")
|
|
169
|
+
.option("-p, --prompt <prompt>", "New prompt")
|
|
170
|
+
.option("--json", "Output as JSON")
|
|
171
|
+
.action(async (id, options) => {
|
|
172
|
+
const config = ensureConfig();
|
|
173
|
+
// Build update payload
|
|
174
|
+
const updateData = {};
|
|
175
|
+
if (options.name)
|
|
176
|
+
updateData.name = options.name;
|
|
177
|
+
if (options.prompt)
|
|
178
|
+
updateData.prompt = options.prompt;
|
|
179
|
+
if (Object.keys(updateData).length === 0) {
|
|
180
|
+
console.error("Error: No update options provided. Use -n/--name or -p/--prompt");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const res = await fetch(`${config.apiUrl}/api/vm-sessions/${id}`, {
|
|
185
|
+
method: "PATCH",
|
|
186
|
+
headers: {
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify(updateData),
|
|
191
|
+
});
|
|
192
|
+
if (!res.ok) {
|
|
193
|
+
if (res.status === 404) {
|
|
194
|
+
console.error(`Error: VM session ${id} not found`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const errorData = await res.json().catch(() => ({}));
|
|
198
|
+
console.error(`Error: ${errorData.error || `API returned ${res.status}`}`);
|
|
199
|
+
}
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const session = await res.json();
|
|
203
|
+
if (options.json) {
|
|
204
|
+
console.log(JSON.stringify(session, null, 2));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
console.log(`VM session updated successfully`);
|
|
208
|
+
console.log(` Name: ${session.name}`);
|
|
209
|
+
console.log(` Status: ${formatStatus(session.vmStatus)}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console.error("Error updating VM session:", error);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// husky vm delete <id>
|
|
218
|
+
vmCommand
|
|
219
|
+
.command("delete <id>")
|
|
220
|
+
.description("Delete VM session")
|
|
221
|
+
.option("--force", "Skip confirmation")
|
|
222
|
+
.option("--json", "Output as JSON")
|
|
223
|
+
.action(async (id, options) => {
|
|
224
|
+
const config = ensureConfig();
|
|
225
|
+
// Confirm deletion unless --force is provided
|
|
226
|
+
if (!options.force) {
|
|
227
|
+
try {
|
|
228
|
+
const getRes = await fetch(`${config.apiUrl}/api/vm-sessions/${id}`, {
|
|
229
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
230
|
+
});
|
|
231
|
+
if (!getRes.ok) {
|
|
232
|
+
if (getRes.status === 404) {
|
|
233
|
+
console.error(`Error: VM session ${id} not found`);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.error(`Error: API returned ${getRes.status}`);
|
|
237
|
+
}
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
const session = await getRes.json();
|
|
241
|
+
const confirmed = await confirm(`Delete VM session "${session.name}" (${id})?`);
|
|
242
|
+
if (!confirmed) {
|
|
243
|
+
console.log("Deletion cancelled.");
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error("Error fetching VM session:", error);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const res = await fetch(`${config.apiUrl}/api/vm-sessions/${id}`, {
|
|
254
|
+
method: "DELETE",
|
|
255
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
256
|
+
});
|
|
257
|
+
if (!res.ok) {
|
|
258
|
+
if (res.status === 404) {
|
|
259
|
+
console.error(`Error: VM session ${id} not found`);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.error(`Error: API returned ${res.status}`);
|
|
263
|
+
}
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
if (options.json) {
|
|
267
|
+
console.log(JSON.stringify({ deleted: true, id }, null, 2));
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
console.log(`VM session deleted`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
console.error("Error deleting VM session:", error);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
// husky vm start <id>
|
|
279
|
+
vmCommand
|
|
280
|
+
.command("start <id>")
|
|
281
|
+
.description("Start/provision the VM")
|
|
282
|
+
.option("--json", "Output as JSON")
|
|
283
|
+
.action(async (id, options) => {
|
|
284
|
+
const config = ensureConfig();
|
|
285
|
+
console.log("Starting VM provisioning...");
|
|
286
|
+
console.log("This may take a few minutes...\n");
|
|
287
|
+
try {
|
|
288
|
+
const res = await fetch(`${config.apiUrl}/api/vm-sessions/${id}/start`, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: {
|
|
291
|
+
"Content-Type": "application/json",
|
|
292
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
if (!res.ok) {
|
|
296
|
+
if (res.status === 404) {
|
|
297
|
+
console.error(`Error: VM session ${id} not found`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const errorData = await res.json().catch(() => ({}));
|
|
301
|
+
console.error(`Error: ${errorData.error || `API returned ${res.status}`}`);
|
|
302
|
+
}
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
const data = await res.json();
|
|
306
|
+
if (options.json) {
|
|
307
|
+
console.log(JSON.stringify(data, null, 2));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const session = data.session;
|
|
311
|
+
console.log(`VM started successfully!`);
|
|
312
|
+
console.log(` Name: ${session.name}`);
|
|
313
|
+
console.log(` Status: ${formatStatus(session.vmStatus)}`);
|
|
314
|
+
if (session.vmIpAddress) {
|
|
315
|
+
console.log(` IP: ${session.vmIpAddress}`);
|
|
316
|
+
}
|
|
317
|
+
console.log(`\nTo view logs, run: husky vm logs ${id}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
console.error("Error starting VM session:", error);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
// husky vm stop <id>
|
|
326
|
+
vmCommand
|
|
327
|
+
.command("stop <id>")
|
|
328
|
+
.description("Stop the VM")
|
|
329
|
+
.option("--json", "Output as JSON")
|
|
330
|
+
.action(async (id, options) => {
|
|
331
|
+
const config = ensureConfig();
|
|
332
|
+
console.log("Stopping VM...\n");
|
|
333
|
+
try {
|
|
334
|
+
const res = await fetch(`${config.apiUrl}/api/vm-sessions/${id}/stop`, {
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: {
|
|
337
|
+
"Content-Type": "application/json",
|
|
338
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
if (!res.ok) {
|
|
342
|
+
if (res.status === 404) {
|
|
343
|
+
console.error(`Error: VM session ${id} not found`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const errorData = await res.json().catch(() => ({}));
|
|
347
|
+
console.error(`Error: ${errorData.error || `API returned ${res.status}`}`);
|
|
348
|
+
}
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
const data = await res.json();
|
|
352
|
+
if (options.json) {
|
|
353
|
+
console.log(JSON.stringify(data, null, 2));
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
const session = data.session;
|
|
357
|
+
console.log(`VM stopped successfully`);
|
|
358
|
+
console.log(` Name: ${session.name}`);
|
|
359
|
+
console.log(` Status: ${formatStatus(session.vmStatus)}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
console.error("Error stopping VM session:", error);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
// husky vm logs <id>
|
|
368
|
+
vmCommand
|
|
369
|
+
.command("logs <id>")
|
|
370
|
+
.description("Get VM logs")
|
|
371
|
+
.option("--follow", "Stream logs (poll for updates)")
|
|
372
|
+
.option("--tail <n>", "Last n log entries", "50")
|
|
373
|
+
.option("--json", "Output as JSON")
|
|
374
|
+
.action(async (id, options) => {
|
|
375
|
+
const config = ensureConfig();
|
|
376
|
+
const limit = parseInt(options.tail, 10);
|
|
377
|
+
const fetchLogs = async () => {
|
|
378
|
+
const url = new URL(`/api/vm-sessions/${id}/logs`, config.apiUrl);
|
|
379
|
+
url.searchParams.set("limit", limit.toString());
|
|
380
|
+
const res = await fetch(url.toString(), {
|
|
381
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
382
|
+
});
|
|
383
|
+
if (!res.ok) {
|
|
384
|
+
if (res.status === 404) {
|
|
385
|
+
console.error(`Error: VM session ${id} not found`);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
throw new Error(`API error: ${res.status}`);
|
|
389
|
+
}
|
|
390
|
+
const data = await res.json();
|
|
391
|
+
return data.logs || [];
|
|
392
|
+
};
|
|
393
|
+
try {
|
|
394
|
+
const logs = await fetchLogs();
|
|
395
|
+
if (options.json) {
|
|
396
|
+
console.log(JSON.stringify(logs, null, 2));
|
|
397
|
+
if (options.follow) {
|
|
398
|
+
console.error("Note: --json mode does not support --follow streaming");
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
// Print initial logs
|
|
403
|
+
for (const log of logs) {
|
|
404
|
+
printLog(log);
|
|
405
|
+
}
|
|
406
|
+
if (options.follow) {
|
|
407
|
+
console.log("\n--- Following logs (Ctrl+C to stop) ---\n");
|
|
408
|
+
let lastLogCount = logs.length;
|
|
409
|
+
const pollInterval = 3000; // 3 seconds
|
|
410
|
+
// Poll for new logs
|
|
411
|
+
const interval = setInterval(async () => {
|
|
412
|
+
try {
|
|
413
|
+
const newLogs = await fetchLogs();
|
|
414
|
+
if (newLogs.length > lastLogCount) {
|
|
415
|
+
// Print only new logs
|
|
416
|
+
for (const log of newLogs.slice(lastLogCount)) {
|
|
417
|
+
printLog(log);
|
|
418
|
+
}
|
|
419
|
+
lastLogCount = newLogs.length;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
console.error("Error fetching logs:", error);
|
|
424
|
+
}
|
|
425
|
+
}, pollInterval);
|
|
426
|
+
// Handle graceful shutdown
|
|
427
|
+
process.on("SIGINT", () => {
|
|
428
|
+
clearInterval(interval);
|
|
429
|
+
console.log("\nStopped following logs.");
|
|
430
|
+
process.exit(0);
|
|
431
|
+
});
|
|
432
|
+
// Keep process alive
|
|
433
|
+
await new Promise(() => { });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
console.error("Error fetching logs:", error);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
// husky vm approve <id>
|
|
442
|
+
vmCommand
|
|
443
|
+
.command("approve <id>")
|
|
444
|
+
.description("Approve VM session plan")
|
|
445
|
+
.option("--json", "Output as JSON")
|
|
446
|
+
.action(async (id, options) => {
|
|
447
|
+
const config = ensureConfig();
|
|
448
|
+
try {
|
|
449
|
+
const res = await fetch(`${config.apiUrl}/api/vm-sessions/${id}/approve`, {
|
|
450
|
+
method: "POST",
|
|
451
|
+
headers: {
|
|
452
|
+
"Content-Type": "application/json",
|
|
453
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
if (!res.ok) {
|
|
457
|
+
if (res.status === 404) {
|
|
458
|
+
console.error(`Error: VM session ${id} not found`);
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
const errorData = await res.json().catch(() => ({}));
|
|
462
|
+
console.error(`Error: ${errorData.error || `API returned ${res.status}`}`);
|
|
463
|
+
}
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
const data = await res.json();
|
|
467
|
+
if (options.json) {
|
|
468
|
+
console.log(JSON.stringify(data, null, 2));
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
console.log(`Plan approved`);
|
|
472
|
+
console.log(` ${data.message}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
console.error("Error approving plan:", error);
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
// husky vm reject <id>
|
|
481
|
+
vmCommand
|
|
482
|
+
.command("reject <id>")
|
|
483
|
+
.description("Reject VM session plan")
|
|
484
|
+
.option("-r, --reason <reason>", "Rejection reason")
|
|
485
|
+
.option("--json", "Output as JSON")
|
|
486
|
+
.action(async (id, options) => {
|
|
487
|
+
const config = ensureConfig();
|
|
488
|
+
try {
|
|
489
|
+
const res = await fetch(`${config.apiUrl}/api/vm-sessions/${id}/reject`, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: {
|
|
492
|
+
"Content-Type": "application/json",
|
|
493
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
494
|
+
},
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
reason: options.reason,
|
|
497
|
+
}),
|
|
498
|
+
});
|
|
499
|
+
if (!res.ok) {
|
|
500
|
+
if (res.status === 404) {
|
|
501
|
+
console.error(`Error: VM session ${id} not found`);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
const errorData = await res.json().catch(() => ({}));
|
|
505
|
+
console.error(`Error: ${errorData.error || `API returned ${res.status}`}`);
|
|
506
|
+
}
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
const data = await res.json();
|
|
510
|
+
if (options.json) {
|
|
511
|
+
console.log(JSON.stringify(data, null, 2));
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
console.log(`Plan rejected`);
|
|
515
|
+
console.log(` ${data.message}`);
|
|
516
|
+
if (options.reason) {
|
|
517
|
+
console.log(` Reason: ${options.reason}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
console.error("Error rejecting plan:", error);
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
// Print helpers
|
|
527
|
+
function printVMSessions(sessions, stats) {
|
|
528
|
+
if (sessions.length === 0) {
|
|
529
|
+
console.log("\n No VM sessions found.");
|
|
530
|
+
console.log(" Create one with: husky vm create <name> --prompt <prompt>\n");
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (stats) {
|
|
534
|
+
console.log(`\n Running VMs: ${stats.runningCount} | Today's Cost: $${stats.todayCost.toFixed(2)}`);
|
|
535
|
+
}
|
|
536
|
+
console.log("\n VM SESSIONS");
|
|
537
|
+
console.log(" " + "-".repeat(90));
|
|
538
|
+
console.log(` ${"ID".padEnd(24)} ${"NAME".padEnd(20)} ${"STATUS".padEnd(16)} ${"AGENT".padEnd(14)} CREATED`);
|
|
539
|
+
console.log(" " + "-".repeat(90));
|
|
540
|
+
for (const session of sessions) {
|
|
541
|
+
const truncatedName = session.name.length > 18 ? session.name.substring(0, 15) + "..." : session.name;
|
|
542
|
+
const status = formatStatus(session.vmStatus).padEnd(16);
|
|
543
|
+
const createdAt = new Date(session.createdAt).toLocaleDateString();
|
|
544
|
+
console.log(` ${session.id.padEnd(24)} ${truncatedName.padEnd(20)} ${status} ${session.agentType.padEnd(14)} ${createdAt}`);
|
|
545
|
+
}
|
|
546
|
+
console.log("");
|
|
547
|
+
}
|
|
548
|
+
function printVMSessionDetail(session) {
|
|
549
|
+
console.log(`\n VM Session: ${session.name}`);
|
|
550
|
+
console.log(" " + "-".repeat(60));
|
|
551
|
+
console.log(` ID: ${session.id}`);
|
|
552
|
+
console.log(` Status: ${formatStatus(session.vmStatus)}`);
|
|
553
|
+
console.log(` Agent: ${session.agentType}`);
|
|
554
|
+
console.log(` VM Name: ${session.vmName}`);
|
|
555
|
+
console.log(` Zone: ${session.vmZone}`);
|
|
556
|
+
console.log(` Machine Type: ${session.machineType}`);
|
|
557
|
+
if (session.vmIpAddress) {
|
|
558
|
+
console.log(` IP Address: ${session.vmIpAddress}`);
|
|
559
|
+
}
|
|
560
|
+
if (session.repoUrl) {
|
|
561
|
+
console.log(` Repository: ${session.repoUrl}`);
|
|
562
|
+
if (session.branch) {
|
|
563
|
+
console.log(` Branch: ${session.branch}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (session.taskId) {
|
|
567
|
+
console.log(` Task ID: ${session.taskId}`);
|
|
568
|
+
}
|
|
569
|
+
if (session.workflowId) {
|
|
570
|
+
console.log(` Workflow ID: ${session.workflowId}`);
|
|
571
|
+
}
|
|
572
|
+
console.log(`\n Prompt:`);
|
|
573
|
+
console.log(` ${session.prompt}`);
|
|
574
|
+
if (session.costEstimate !== undefined) {
|
|
575
|
+
console.log(`\n Cost Estimate: $${session.costEstimate.toFixed(4)}`);
|
|
576
|
+
}
|
|
577
|
+
if (session.runtimeMinutes !== undefined) {
|
|
578
|
+
console.log(` Runtime: ${session.runtimeMinutes} minutes`);
|
|
579
|
+
}
|
|
580
|
+
if (session.prUrl) {
|
|
581
|
+
console.log(`\n Pull Request: ${session.prUrl}`);
|
|
582
|
+
}
|
|
583
|
+
if (session.output) {
|
|
584
|
+
console.log(`\n Output:`);
|
|
585
|
+
const outputLines = session.output.split("\n").slice(0, 10);
|
|
586
|
+
for (const line of outputLines) {
|
|
587
|
+
console.log(` ${line}`);
|
|
588
|
+
}
|
|
589
|
+
if (session.output.split("\n").length > 10) {
|
|
590
|
+
console.log(` ... (truncated)`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (session.lastError) {
|
|
594
|
+
console.log(`\n Last Error: ${session.lastError}`);
|
|
595
|
+
}
|
|
596
|
+
console.log(`\n Created: ${new Date(session.createdAt).toLocaleString()}`);
|
|
597
|
+
if (session.startedAt) {
|
|
598
|
+
console.log(` Started: ${new Date(session.startedAt).toLocaleString()}`);
|
|
599
|
+
}
|
|
600
|
+
if (session.completedAt) {
|
|
601
|
+
console.log(` Completed: ${new Date(session.completedAt).toLocaleString()}`);
|
|
602
|
+
}
|
|
603
|
+
console.log("");
|
|
604
|
+
}
|
|
605
|
+
function printLog(log) {
|
|
606
|
+
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
|
607
|
+
const level = log.level.toUpperCase().padEnd(5);
|
|
608
|
+
const source = `[${log.source}]`.padEnd(10);
|
|
609
|
+
// Color based on level
|
|
610
|
+
let prefix = "";
|
|
611
|
+
if (log.level === "error") {
|
|
612
|
+
prefix = "x ";
|
|
613
|
+
}
|
|
614
|
+
else if (log.level === "warn") {
|
|
615
|
+
prefix = "! ";
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
prefix = " ";
|
|
619
|
+
}
|
|
620
|
+
console.log(`${prefix}${timestamp} ${level} ${source} ${log.message}`);
|
|
621
|
+
}
|