@simonfestl/husky-cli 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -56
- 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.js +95 -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 +1287 -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
package/dist/commands/task.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { getConfig } from "./config.js";
|
|
3
3
|
import * as fs from "fs";
|
|
4
|
+
import * as readline from "readline";
|
|
4
5
|
export const taskCommand = new Command("task")
|
|
5
6
|
.description("Manage tasks");
|
|
6
7
|
// Helper: Get task ID from --id flag or HUSKY_TASK_ID env var
|
|
@@ -21,6 +22,19 @@ function ensureConfig() {
|
|
|
21
22
|
}
|
|
22
23
|
return config;
|
|
23
24
|
}
|
|
25
|
+
// Helper: Prompt for confirmation
|
|
26
|
+
async function confirm(message) {
|
|
27
|
+
const rl = readline.createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stdout,
|
|
30
|
+
});
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
rl.question(`${message} (y/N): `, (answer) => {
|
|
33
|
+
rl.close();
|
|
34
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
24
38
|
// husky task list
|
|
25
39
|
taskCommand
|
|
26
40
|
.command("list")
|
|
@@ -152,6 +166,132 @@ taskCommand
|
|
|
152
166
|
process.exit(1);
|
|
153
167
|
}
|
|
154
168
|
});
|
|
169
|
+
// husky task update <id>
|
|
170
|
+
taskCommand
|
|
171
|
+
.command("update <id>")
|
|
172
|
+
.description("Update task properties")
|
|
173
|
+
.option("-t, --title <title>", "New title")
|
|
174
|
+
.option("-d, --description <desc>", "New description")
|
|
175
|
+
.option("--status <status>", "New status (backlog, in_progress, review, done)")
|
|
176
|
+
.option("--priority <priority>", "New priority (low, medium, high, urgent)")
|
|
177
|
+
.option("--assignee <assignee>", "New assignee (human, llm, unassigned)")
|
|
178
|
+
.option("--project <projectId>", "Link to project")
|
|
179
|
+
.option("--json", "Output as JSON")
|
|
180
|
+
.action(async (id, options) => {
|
|
181
|
+
const config = ensureConfig();
|
|
182
|
+
// Build update payload with only changed fields
|
|
183
|
+
const updates = {};
|
|
184
|
+
if (options.title)
|
|
185
|
+
updates.title = options.title;
|
|
186
|
+
if (options.description)
|
|
187
|
+
updates.description = options.description;
|
|
188
|
+
if (options.status)
|
|
189
|
+
updates.status = options.status;
|
|
190
|
+
if (options.priority)
|
|
191
|
+
updates.priority = options.priority;
|
|
192
|
+
if (options.assignee)
|
|
193
|
+
updates.assignee = options.assignee;
|
|
194
|
+
if (options.project)
|
|
195
|
+
updates.projectId = options.project;
|
|
196
|
+
if (Object.keys(updates).length === 0) {
|
|
197
|
+
console.error("Error: No update options provided. Use --help for available options.");
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${id}`, {
|
|
202
|
+
method: "PATCH",
|
|
203
|
+
headers: {
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
206
|
+
},
|
|
207
|
+
body: JSON.stringify(updates),
|
|
208
|
+
});
|
|
209
|
+
if (!res.ok) {
|
|
210
|
+
if (res.status === 404) {
|
|
211
|
+
console.error(`Error: Task ${id} not found`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.error(`Error: API returned ${res.status}`);
|
|
215
|
+
}
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const task = await res.json();
|
|
219
|
+
if (options.json) {
|
|
220
|
+
console.log(JSON.stringify(task, null, 2));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
console.log(`✓ Updated: ${task.title}`);
|
|
224
|
+
const changedFields = Object.keys(updates).join(", ");
|
|
225
|
+
console.log(` Changed: ${changedFields}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.error("Error updating task:", error);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
// husky task delete <id>
|
|
234
|
+
taskCommand
|
|
235
|
+
.command("delete <id>")
|
|
236
|
+
.description("Delete a task")
|
|
237
|
+
.option("--force", "Skip confirmation prompt")
|
|
238
|
+
.option("--json", "Output as JSON")
|
|
239
|
+
.action(async (id, options) => {
|
|
240
|
+
const config = ensureConfig();
|
|
241
|
+
// Confirm deletion unless --force is provided
|
|
242
|
+
if (!options.force) {
|
|
243
|
+
// First fetch task details to show what will be deleted
|
|
244
|
+
try {
|
|
245
|
+
const getRes = await fetch(`${config.apiUrl}/api/tasks/${id}`, {
|
|
246
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
247
|
+
});
|
|
248
|
+
if (!getRes.ok) {
|
|
249
|
+
if (getRes.status === 404) {
|
|
250
|
+
console.error(`Error: Task ${id} not found`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
console.error(`Error: API returned ${getRes.status}`);
|
|
254
|
+
}
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
const task = await getRes.json();
|
|
258
|
+
const confirmed = await confirm(`Delete task "${task.title}" (${id})?`);
|
|
259
|
+
if (!confirmed) {
|
|
260
|
+
console.log("Deletion cancelled.");
|
|
261
|
+
process.exit(0);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
console.error("Error fetching task:", error);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${id}`, {
|
|
271
|
+
method: "DELETE",
|
|
272
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
273
|
+
});
|
|
274
|
+
if (!res.ok) {
|
|
275
|
+
if (res.status === 404) {
|
|
276
|
+
console.error(`Error: Task ${id} not found`);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
console.error(`Error: API returned ${res.status}`);
|
|
280
|
+
}
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
if (options.json) {
|
|
284
|
+
console.log(JSON.stringify({ deleted: true, id }, null, 2));
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
console.log(`✓ Deleted task ${id}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
console.error("Error deleting task:", error);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
155
295
|
// husky task get [--id <id>] [--json]
|
|
156
296
|
taskCommand
|
|
157
297
|
.command("get")
|
|
@@ -601,6 +741,109 @@ taskCommand
|
|
|
601
741
|
process.exit(1);
|
|
602
742
|
}
|
|
603
743
|
});
|
|
744
|
+
// ============================================
|
|
745
|
+
// MERGE CONFLICT RESOLUTION
|
|
746
|
+
// ============================================
|
|
747
|
+
// husky task merge-conflict --file <path> --ours <content> --theirs <content> [--base <content>] [--context <text>]
|
|
748
|
+
taskCommand
|
|
749
|
+
.command("merge-conflict")
|
|
750
|
+
.description("Resolve a Git merge conflict using AI")
|
|
751
|
+
.requiredOption("--file <path>", "Path to the conflicted file")
|
|
752
|
+
.requiredOption("--ours <content>", "Content from current branch (ours)")
|
|
753
|
+
.requiredOption("--theirs <content>", "Content from incoming branch (theirs)")
|
|
754
|
+
.option("--base <content>", "Content from common ancestor (for 3-way merge)")
|
|
755
|
+
.option("--context <text>", "Additional context about the merge")
|
|
756
|
+
.option("--json", "Output as JSON")
|
|
757
|
+
.option("--ours-file <path>", "Read ours content from file")
|
|
758
|
+
.option("--theirs-file <path>", "Read theirs content from file")
|
|
759
|
+
.option("--base-file <path>", "Read base content from file")
|
|
760
|
+
.action(async (options) => {
|
|
761
|
+
const config = ensureConfig();
|
|
762
|
+
// Read content from files if specified
|
|
763
|
+
let oursContent = options.ours;
|
|
764
|
+
let theirsContent = options.theirs;
|
|
765
|
+
let baseContent = options.base;
|
|
766
|
+
if (options.oursFile) {
|
|
767
|
+
try {
|
|
768
|
+
oursContent = fs.readFileSync(options.oursFile, "utf-8");
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
console.error(`Error reading ours file ${options.oursFile}:`, error);
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (options.theirsFile) {
|
|
776
|
+
try {
|
|
777
|
+
theirsContent = fs.readFileSync(options.theirsFile, "utf-8");
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
console.error(`Error reading theirs file ${options.theirsFile}:`, error);
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (options.baseFile) {
|
|
785
|
+
try {
|
|
786
|
+
baseContent = fs.readFileSync(options.baseFile, "utf-8");
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
console.error(`Error reading base file ${options.baseFile}:`, error);
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
const res = await fetch(`${config.apiUrl}/api/merge-conflict/resolve`, {
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: {
|
|
797
|
+
"Content-Type": "application/json",
|
|
798
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
799
|
+
},
|
|
800
|
+
body: JSON.stringify({
|
|
801
|
+
filePath: options.file,
|
|
802
|
+
oursContent,
|
|
803
|
+
theirsContent,
|
|
804
|
+
baseContent,
|
|
805
|
+
context: options.context,
|
|
806
|
+
}),
|
|
807
|
+
});
|
|
808
|
+
if (!res.ok) {
|
|
809
|
+
const errorData = await res.json().catch(() => ({ error: `API error: ${res.status}` }));
|
|
810
|
+
throw new Error(errorData.error || `API error: ${res.status}`);
|
|
811
|
+
}
|
|
812
|
+
const result = await res.json();
|
|
813
|
+
if (options.json) {
|
|
814
|
+
console.log(JSON.stringify(result, null, 2));
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
console.log(`\n Merge Conflict Resolution: ${options.file}`);
|
|
818
|
+
console.log(" " + "=".repeat(50));
|
|
819
|
+
console.log(`\n Confidence: ${result.confidence}%`);
|
|
820
|
+
// Confidence indicator
|
|
821
|
+
if (result.confidence >= 90) {
|
|
822
|
+
console.log(" Status: High confidence - safe to use");
|
|
823
|
+
}
|
|
824
|
+
else if (result.confidence >= 70) {
|
|
825
|
+
console.log(" Status: Good confidence - review recommended");
|
|
826
|
+
}
|
|
827
|
+
else if (result.confidence >= 50) {
|
|
828
|
+
console.log(" Status: Medium confidence - careful review needed");
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
console.log(" Status: Low confidence - manual resolution recommended");
|
|
832
|
+
}
|
|
833
|
+
console.log(`\n Explanation:`);
|
|
834
|
+
console.log(` ${result.explanation}`);
|
|
835
|
+
console.log("\n " + "-".repeat(50));
|
|
836
|
+
console.log(" Resolved Content:");
|
|
837
|
+
console.log(" " + "-".repeat(50));
|
|
838
|
+
console.log(result.resolvedContent);
|
|
839
|
+
console.log("");
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
console.error("Error resolving merge conflict:", error);
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
604
847
|
function printTasks(tasks) {
|
|
605
848
|
const byStatus = {
|
|
606
849
|
backlog: [],
|
|
@@ -628,7 +871,7 @@ function printTasks(tasks) {
|
|
|
628
871
|
for (const task of statusTasks) {
|
|
629
872
|
const agentStr = task.agent ? ` (${task.agent})` : "";
|
|
630
873
|
const doneStr = status === "done" ? " ✓" : "";
|
|
631
|
-
console.log(`
|
|
874
|
+
console.log(` ${task.id} ${task.title.slice(0, 30).padEnd(30)} ${task.priority}${agentStr}${doneStr}`);
|
|
632
875
|
}
|
|
633
876
|
}
|
|
634
877
|
console.log("");
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
import * as readline from "readline";
|
|
4
|
+
export const vmConfigCommand = new Command("vm-config")
|
|
5
|
+
.description("Manage VM configurations");
|
|
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
|
+
// husky vm-config list
|
|
29
|
+
vmConfigCommand
|
|
30
|
+
.command("list")
|
|
31
|
+
.description("List all VM configurations")
|
|
32
|
+
.option("--json", "Output as JSON")
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
const config = ensureConfig();
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${config.apiUrl}/api/vm-configs`, {
|
|
37
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(`API error: ${res.status}`);
|
|
41
|
+
}
|
|
42
|
+
const configs = await res.json();
|
|
43
|
+
if (options.json) {
|
|
44
|
+
console.log(JSON.stringify(configs, null, 2));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
printVMConfigs(configs);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error("Error fetching VM configs:", error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// husky vm-config get <id>
|
|
56
|
+
vmConfigCommand
|
|
57
|
+
.command("get <id>")
|
|
58
|
+
.description("Get VM configuration details")
|
|
59
|
+
.option("--json", "Output as JSON")
|
|
60
|
+
.action(async (id, options) => {
|
|
61
|
+
const config = ensureConfig();
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${config.apiUrl}/api/vm-configs/${id}`, {
|
|
64
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
if (res.status === 404) {
|
|
68
|
+
console.error(`Error: VM config ${id} not found`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.error(`Error: API returned ${res.status}`);
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const vmConfig = await res.json();
|
|
76
|
+
if (options.json) {
|
|
77
|
+
console.log(JSON.stringify(vmConfig, null, 2));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
printVMConfigDetail(vmConfig);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error("Error fetching VM config:", error);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// husky vm-config create <name>
|
|
89
|
+
vmConfigCommand
|
|
90
|
+
.command("create <name>")
|
|
91
|
+
.description("Create a new VM configuration")
|
|
92
|
+
.option("--machine-type <type>", "GCP machine type (e.g., e2-medium)", "e2-medium")
|
|
93
|
+
.option("--zone <zone>", "GCP zone", "europe-west1-b")
|
|
94
|
+
.option("--disk-size <size>", "Disk size in GB", "20")
|
|
95
|
+
.option("--image <image>", "Base image")
|
|
96
|
+
.option("--max-runtime <minutes>", "Max runtime in minutes", "60")
|
|
97
|
+
.option("--max-concurrent <n>", "Max concurrent VMs", "3")
|
|
98
|
+
.option("--daily-budget <usd>", "Daily budget in USD", "10")
|
|
99
|
+
.option("--json", "Output as JSON")
|
|
100
|
+
.action(async (name, options) => {
|
|
101
|
+
const config = ensureConfig();
|
|
102
|
+
try {
|
|
103
|
+
const createPayload = {
|
|
104
|
+
name,
|
|
105
|
+
machineType: options.machineType,
|
|
106
|
+
zone: options.zone,
|
|
107
|
+
diskSizeGb: parseInt(options.diskSize, 10),
|
|
108
|
+
maxRuntimeMinutes: parseInt(options.maxRuntime, 10),
|
|
109
|
+
maxConcurrentVMs: parseInt(options.maxConcurrent, 10),
|
|
110
|
+
dailyBudgetUsd: parseFloat(options.dailyBudget),
|
|
111
|
+
};
|
|
112
|
+
if (options.image) {
|
|
113
|
+
createPayload.baseImage = options.image;
|
|
114
|
+
}
|
|
115
|
+
const res = await fetch(`${config.apiUrl}/api/vm-configs`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify(createPayload),
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const errorData = await res.json().catch(() => ({}));
|
|
125
|
+
throw new Error(errorData.error || `API error: ${res.status}`);
|
|
126
|
+
}
|
|
127
|
+
const vmConfig = await res.json();
|
|
128
|
+
if (options.json) {
|
|
129
|
+
console.log(JSON.stringify(vmConfig, null, 2));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.log(`Created VM config: ${vmConfig.name}`);
|
|
133
|
+
console.log(` ID: ${vmConfig.id}`);
|
|
134
|
+
console.log(` Machine Type: ${vmConfig.machineType}`);
|
|
135
|
+
console.log(` Zone: ${vmConfig.zone}`);
|
|
136
|
+
console.log(` Disk Size: ${vmConfig.diskSizeGb} GB`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error("Error creating VM config:", error);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// husky vm-config update <id>
|
|
145
|
+
vmConfigCommand
|
|
146
|
+
.command("update <id>")
|
|
147
|
+
.description("Update a VM configuration")
|
|
148
|
+
.option("-n, --name <name>", "New name")
|
|
149
|
+
.option("--machine-type <type>", "GCP machine type (e.g., e2-medium)")
|
|
150
|
+
.option("--zone <zone>", "GCP zone")
|
|
151
|
+
.option("--disk-size <size>", "Disk size in GB")
|
|
152
|
+
.option("--image <image>", "Base image")
|
|
153
|
+
.option("--max-runtime <minutes>", "Max runtime in minutes")
|
|
154
|
+
.option("--max-concurrent <n>", "Max concurrent VMs")
|
|
155
|
+
.option("--daily-budget <usd>", "Daily budget in USD")
|
|
156
|
+
.option("--json", "Output as JSON")
|
|
157
|
+
.action(async (id, options) => {
|
|
158
|
+
const config = ensureConfig();
|
|
159
|
+
// Build update payload
|
|
160
|
+
const updateData = {};
|
|
161
|
+
if (options.name)
|
|
162
|
+
updateData.name = options.name;
|
|
163
|
+
if (options.machineType)
|
|
164
|
+
updateData.machineType = options.machineType;
|
|
165
|
+
if (options.zone)
|
|
166
|
+
updateData.zone = options.zone;
|
|
167
|
+
if (options.diskSize)
|
|
168
|
+
updateData.diskSizeGb = parseInt(options.diskSize, 10);
|
|
169
|
+
if (options.image)
|
|
170
|
+
updateData.baseImage = options.image;
|
|
171
|
+
if (options.maxRuntime)
|
|
172
|
+
updateData.maxRuntimeMinutes = parseInt(options.maxRuntime, 10);
|
|
173
|
+
if (options.maxConcurrent)
|
|
174
|
+
updateData.maxConcurrentVMs = parseInt(options.maxConcurrent, 10);
|
|
175
|
+
if (options.dailyBudget)
|
|
176
|
+
updateData.dailyBudgetUsd = parseFloat(options.dailyBudget);
|
|
177
|
+
if (Object.keys(updateData).length === 0) {
|
|
178
|
+
console.error("Error: No update options provided. Use --help for available options.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const res = await fetch(`${config.apiUrl}/api/vm-configs/${id}`, {
|
|
183
|
+
method: "PATCH",
|
|
184
|
+
headers: {
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
187
|
+
},
|
|
188
|
+
body: JSON.stringify(updateData),
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
if (res.status === 404) {
|
|
192
|
+
console.error(`Error: VM config ${id} not found`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const errorData = await res.json().catch(() => ({}));
|
|
196
|
+
console.error(`Error: ${errorData.error || `API returned ${res.status}`}`);
|
|
197
|
+
}
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
const vmConfig = await res.json();
|
|
201
|
+
if (options.json) {
|
|
202
|
+
console.log(JSON.stringify(vmConfig, null, 2));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log(`VM config updated successfully`);
|
|
206
|
+
console.log(` Name: ${vmConfig.name}`);
|
|
207
|
+
console.log(` Machine Type: ${vmConfig.machineType}`);
|
|
208
|
+
console.log(` Zone: ${vmConfig.zone}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
console.error("Error updating VM config:", error);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// husky vm-config delete <id>
|
|
217
|
+
vmConfigCommand
|
|
218
|
+
.command("delete <id>")
|
|
219
|
+
.description("Delete a VM configuration")
|
|
220
|
+
.option("--force", "Skip confirmation")
|
|
221
|
+
.action(async (id, options) => {
|
|
222
|
+
const config = ensureConfig();
|
|
223
|
+
// Prevent deletion of default config
|
|
224
|
+
if (id === "default") {
|
|
225
|
+
console.error("Error: Cannot delete the default VM config");
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
// Confirm deletion unless --force is provided
|
|
229
|
+
if (!options.force) {
|
|
230
|
+
try {
|
|
231
|
+
const getRes = await fetch(`${config.apiUrl}/api/vm-configs/${id}`, {
|
|
232
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
233
|
+
});
|
|
234
|
+
if (!getRes.ok) {
|
|
235
|
+
if (getRes.status === 404) {
|
|
236
|
+
console.error(`Error: VM config ${id} not found`);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.error(`Error: API returned ${getRes.status}`);
|
|
240
|
+
}
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
const vmConfig = await getRes.json();
|
|
244
|
+
const confirmed = await confirm(`Delete VM config "${vmConfig.name}" (${id})?`);
|
|
245
|
+
if (!confirmed) {
|
|
246
|
+
console.log("Deletion cancelled.");
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
console.error("Error fetching VM config:", error);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`${config.apiUrl}/api/vm-configs/${id}`, {
|
|
257
|
+
method: "DELETE",
|
|
258
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
259
|
+
});
|
|
260
|
+
if (!res.ok) {
|
|
261
|
+
if (res.status === 404) {
|
|
262
|
+
console.error(`Error: VM config ${id} not found`);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const errorData = await res.json().catch(() => ({}));
|
|
266
|
+
console.error(`Error: ${errorData.error || `API returned ${res.status}`}`);
|
|
267
|
+
}
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
console.log(`VM config deleted`);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
console.error("Error deleting VM config:", error);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// Print helpers
|
|
278
|
+
function printVMConfigs(configs) {
|
|
279
|
+
if (configs.length === 0) {
|
|
280
|
+
console.log("\n No VM configs found.");
|
|
281
|
+
console.log(" Create one with: husky vm-config create <name>\n");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
console.log("\n VM CONFIGURATIONS");
|
|
285
|
+
console.log(" " + "-".repeat(90));
|
|
286
|
+
console.log(` ${"ID".padEnd(24)} ${"NAME".padEnd(20)} ${"MACHINE TYPE".padEnd(14)} ${"ZONE".padEnd(18)} DISK`);
|
|
287
|
+
console.log(" " + "-".repeat(90));
|
|
288
|
+
for (const cfg of configs) {
|
|
289
|
+
const truncatedName = cfg.name.length > 18 ? cfg.name.substring(0, 15) + "..." : cfg.name;
|
|
290
|
+
console.log(` ${cfg.id.padEnd(24)} ${truncatedName.padEnd(20)} ${cfg.machineType.padEnd(14)} ${cfg.zone.padEnd(18)} ${cfg.diskSizeGb}GB`);
|
|
291
|
+
}
|
|
292
|
+
console.log("");
|
|
293
|
+
}
|
|
294
|
+
function printVMConfigDetail(vmConfig) {
|
|
295
|
+
console.log(`\n VM Config: ${vmConfig.name}`);
|
|
296
|
+
console.log(" " + "-".repeat(60));
|
|
297
|
+
console.log(` ID: ${vmConfig.id}`);
|
|
298
|
+
console.log(` Name: ${vmConfig.name}`);
|
|
299
|
+
console.log("\n GCP Settings:");
|
|
300
|
+
console.log(` Machine Type: ${vmConfig.machineType}`);
|
|
301
|
+
console.log(` Zone: ${vmConfig.zone}`);
|
|
302
|
+
console.log(` Disk Size: ${vmConfig.diskSizeGb} GB`);
|
|
303
|
+
console.log("\n Limits:");
|
|
304
|
+
console.log(` Max Runtime: ${vmConfig.maxRuntimeMinutes} minutes`);
|
|
305
|
+
console.log(` Max Concurrent: ${vmConfig.maxConcurrentVMs}`);
|
|
306
|
+
console.log(` Daily Budget: $${vmConfig.dailyBudgetUsd.toFixed(2)}`);
|
|
307
|
+
console.log("\n Auto-Start:");
|
|
308
|
+
console.log(` Enabled: ${vmConfig.autoStartEnabled ? "Yes" : "No"}`);
|
|
309
|
+
console.log("\n Review Settings:");
|
|
310
|
+
console.log(` AI Review: ${vmConfig.aiReviewEnabled ? "Enabled" : "Disabled"}`);
|
|
311
|
+
console.log(` Human Review: ${vmConfig.humanReviewRequired ? "Required" : "Optional"}`);
|
|
312
|
+
console.log(` Auto-Approve: ${vmConfig.autoApproveThreshold}% confidence threshold`);
|
|
313
|
+
console.log("\n Retry Settings:");
|
|
314
|
+
console.log(` Max Retries: ${vmConfig.maxRetries}`);
|
|
315
|
+
console.log(`\n Created: ${new Date(vmConfig.createdAt).toLocaleString()}`);
|
|
316
|
+
console.log(` Updated: ${new Date(vmConfig.updatedAt).toLocaleString()}`);
|
|
317
|
+
console.log("");
|
|
318
|
+
}
|