@simonfestl/husky-cli 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -0
- package/dist/commands/agent.d.ts +2 -0
- package/dist/commands/agent.js +279 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +73 -0
- package/dist/commands/roadmap.d.ts +2 -0
- package/dist/commands/roadmap.js +325 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +635 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +16 -0
- package/dist/lib/streaming.d.ts +44 -0
- package/dist/lib/streaming.js +157 -0
- package/package.json +30 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
export const taskCommand = new Command("task")
|
|
5
|
+
.description("Manage tasks");
|
|
6
|
+
// Helper: Get task ID from --id flag or HUSKY_TASK_ID env var
|
|
7
|
+
function getTaskId(options) {
|
|
8
|
+
const id = options.id || process.env.HUSKY_TASK_ID;
|
|
9
|
+
if (!id) {
|
|
10
|
+
console.error("Error: Task ID required. Use --id or set HUSKY_TASK_ID environment variable.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return id;
|
|
14
|
+
}
|
|
15
|
+
// Helper: Ensure API is configured
|
|
16
|
+
function ensureConfig() {
|
|
17
|
+
const config = getConfig();
|
|
18
|
+
if (!config.apiUrl) {
|
|
19
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return config;
|
|
23
|
+
}
|
|
24
|
+
// husky task list
|
|
25
|
+
taskCommand
|
|
26
|
+
.command("list")
|
|
27
|
+
.description("List all tasks")
|
|
28
|
+
.option("-s, --status <status>", "Filter by status")
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
const config = getConfig();
|
|
31
|
+
if (!config.apiUrl) {
|
|
32
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL("/api/tasks", config.apiUrl);
|
|
37
|
+
if (options.status) {
|
|
38
|
+
url.searchParams.set("status", options.status);
|
|
39
|
+
}
|
|
40
|
+
const res = await fetch(url.toString(), {
|
|
41
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
throw new Error(`API error: ${res.status}`);
|
|
45
|
+
}
|
|
46
|
+
const tasks = await res.json();
|
|
47
|
+
printTasks(tasks);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error("Error fetching tasks:", error);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// husky task start <id>
|
|
55
|
+
taskCommand
|
|
56
|
+
.command("start <id>")
|
|
57
|
+
.description("Start working on a task")
|
|
58
|
+
.action(async (id) => {
|
|
59
|
+
const config = getConfig();
|
|
60
|
+
if (!config.apiUrl) {
|
|
61
|
+
console.error("Error: API URL not configured.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${id}/start`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({ agent: "claude-code" }),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
throw new Error(`API error: ${res.status}`);
|
|
75
|
+
}
|
|
76
|
+
const task = await res.json();
|
|
77
|
+
console.log(`✓ Started: ${task.title}`);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error("Error starting task:", error);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// husky task done <id>
|
|
85
|
+
taskCommand
|
|
86
|
+
.command("done <id>")
|
|
87
|
+
.description("Mark task as done")
|
|
88
|
+
.option("--pr <url>", "Link to PR")
|
|
89
|
+
.action(async (id, options) => {
|
|
90
|
+
const config = getConfig();
|
|
91
|
+
if (!config.apiUrl) {
|
|
92
|
+
console.error("Error: API URL not configured.");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${id}/done`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({ prUrl: options.pr }),
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw new Error(`API error: ${res.status}`);
|
|
106
|
+
}
|
|
107
|
+
const task = await res.json();
|
|
108
|
+
console.log(`✓ Completed: ${task.title}`);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error("Error completing task:", error);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// husky task create <title>
|
|
116
|
+
taskCommand
|
|
117
|
+
.command("create <title>")
|
|
118
|
+
.description("Create a new task")
|
|
119
|
+
.option("-d, --description <desc>", "Task description")
|
|
120
|
+
.option("--project <projectId>", "Project ID")
|
|
121
|
+
.option("--path <path>", "Path in project")
|
|
122
|
+
.option("-p, --priority <priority>", "Priority (low, medium, high)", "medium")
|
|
123
|
+
.action(async (title, options) => {
|
|
124
|
+
const config = getConfig();
|
|
125
|
+
if (!config.apiUrl) {
|
|
126
|
+
console.error("Error: API URL not configured.");
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(`${config.apiUrl}/api/tasks`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
title,
|
|
138
|
+
description: options.description,
|
|
139
|
+
projectId: options.project,
|
|
140
|
+
linkedPath: options.path,
|
|
141
|
+
priority: options.priority,
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
throw new Error(`API error: ${res.status}`);
|
|
146
|
+
}
|
|
147
|
+
const task = await res.json();
|
|
148
|
+
console.log(`✓ Created: #${task.id} ${task.title}`);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error("Error creating task:", error);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
// husky task get [--id <id>] [--json]
|
|
156
|
+
taskCommand
|
|
157
|
+
.command("get")
|
|
158
|
+
.description("Get task details")
|
|
159
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
160
|
+
.option("--json", "Output as JSON")
|
|
161
|
+
.action(async (options) => {
|
|
162
|
+
const config = ensureConfig();
|
|
163
|
+
const taskId = getTaskId(options);
|
|
164
|
+
try {
|
|
165
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}`, {
|
|
166
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
167
|
+
});
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
if (res.status === 404) {
|
|
170
|
+
console.error(`Error: Task ${taskId} not found`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.error(`Error: API returned ${res.status}`);
|
|
174
|
+
}
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
const task = await res.json();
|
|
178
|
+
if (options.json) {
|
|
179
|
+
console.log(JSON.stringify(task, null, 2));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log(`\n Task: ${task.title}`);
|
|
183
|
+
console.log(" " + "─".repeat(50));
|
|
184
|
+
console.log(` ID: ${task.id}`);
|
|
185
|
+
console.log(` Status: ${task.status}`);
|
|
186
|
+
console.log(` Priority: ${task.priority}`);
|
|
187
|
+
if (task.description)
|
|
188
|
+
console.log(` Description: ${task.description}`);
|
|
189
|
+
if (task.agent)
|
|
190
|
+
console.log(` Agent: ${task.agent}`);
|
|
191
|
+
if (task.projectId)
|
|
192
|
+
console.log(` Project: ${task.projectId}`);
|
|
193
|
+
console.log("");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error("Error fetching task:", error);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// husky task status <message> [--id <id>]
|
|
202
|
+
taskCommand
|
|
203
|
+
.command("status <message>")
|
|
204
|
+
.description("Report task progress status")
|
|
205
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
206
|
+
.action(async (message, options) => {
|
|
207
|
+
const config = ensureConfig();
|
|
208
|
+
const taskId = getTaskId(options);
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/status`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: {
|
|
213
|
+
"Content-Type": "application/json",
|
|
214
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
message,
|
|
218
|
+
timestamp: new Date().toISOString(),
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
if (!res.ok) {
|
|
222
|
+
throw new Error(`API error: ${res.status}`);
|
|
223
|
+
}
|
|
224
|
+
console.log(`✓ Status updated: ${message}`);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
console.error("Error updating status:", error);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
// husky task plan [--summary <text>] [--file <path>] [--stdin] [--id <id>]
|
|
232
|
+
taskCommand
|
|
233
|
+
.command("plan")
|
|
234
|
+
.description("Submit execution plan for approval")
|
|
235
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
236
|
+
.option("--summary <text>", "Plan summary")
|
|
237
|
+
.option("--file <path>", "Read plan from file")
|
|
238
|
+
.option("--stdin", "Read plan from stdin")
|
|
239
|
+
.option("--steps <steps>", "Comma-separated steps")
|
|
240
|
+
.action(async (options) => {
|
|
241
|
+
const config = ensureConfig();
|
|
242
|
+
const taskId = getTaskId(options);
|
|
243
|
+
let content;
|
|
244
|
+
let summary = options.summary;
|
|
245
|
+
// Read content from file or stdin
|
|
246
|
+
if (options.file) {
|
|
247
|
+
try {
|
|
248
|
+
content = fs.readFileSync(options.file, "utf-8");
|
|
249
|
+
if (!summary) {
|
|
250
|
+
// Use first line as summary if not provided
|
|
251
|
+
summary = content.split("\n")[0].replace(/^#\s*/, "").slice(0, 100);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
console.error(`Error reading file ${options.file}:`, error);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (options.stdin) {
|
|
260
|
+
content = fs.readFileSync(0, "utf-8"); // Read from stdin
|
|
261
|
+
if (!summary) {
|
|
262
|
+
summary = content.split("\n")[0].replace(/^#\s*/, "").slice(0, 100);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!summary && !content) {
|
|
266
|
+
console.error("Error: Provide --summary, --file, or --stdin");
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
const steps = options.steps ? options.steps.split(",").map((s) => s.trim()) : undefined;
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/plan`, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: {
|
|
274
|
+
"Content-Type": "application/json",
|
|
275
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
276
|
+
},
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
summary: summary || "Execution plan",
|
|
279
|
+
steps,
|
|
280
|
+
content,
|
|
281
|
+
}),
|
|
282
|
+
});
|
|
283
|
+
if (!res.ok) {
|
|
284
|
+
throw new Error(`API error: ${res.status}`);
|
|
285
|
+
}
|
|
286
|
+
console.log(`✓ Plan submitted for task ${taskId}`);
|
|
287
|
+
console.log(" Waiting for approval...");
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
console.error("Error submitting plan:", error);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
// husky task wait-approval [--timeout <seconds>] [--id <id>]
|
|
295
|
+
taskCommand
|
|
296
|
+
.command("wait-approval")
|
|
297
|
+
.description("Wait for plan approval")
|
|
298
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
299
|
+
.option("--timeout <seconds>", "Timeout in seconds", "1800")
|
|
300
|
+
.action(async (options) => {
|
|
301
|
+
const config = ensureConfig();
|
|
302
|
+
const taskId = getTaskId(options);
|
|
303
|
+
const timeout = parseInt(options.timeout, 10) * 1000;
|
|
304
|
+
const pollInterval = 5000; // 5 seconds
|
|
305
|
+
const startTime = Date.now();
|
|
306
|
+
console.log(`Waiting for approval on task ${taskId}...`);
|
|
307
|
+
while (Date.now() - startTime < timeout) {
|
|
308
|
+
try {
|
|
309
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/approval`, {
|
|
310
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
311
|
+
});
|
|
312
|
+
if (!res.ok) {
|
|
313
|
+
throw new Error(`API error: ${res.status}`);
|
|
314
|
+
}
|
|
315
|
+
const data = await res.json();
|
|
316
|
+
if (data.status === "approved") {
|
|
317
|
+
console.log("✓ Plan approved!");
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
else if (data.status === "rejected") {
|
|
321
|
+
console.log("✗ Plan rejected");
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
// Still pending, wait and poll again
|
|
325
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
326
|
+
process.stdout.write(".");
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
console.error("\nError checking approval:", error);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
console.log("\n✗ Timeout waiting for approval");
|
|
334
|
+
process.exit(2);
|
|
335
|
+
});
|
|
336
|
+
// husky task complete [--output <text>] [--pr <url>] [--error <text>] [--id <id>]
|
|
337
|
+
taskCommand
|
|
338
|
+
.command("complete")
|
|
339
|
+
.description("Mark task as complete with result")
|
|
340
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
341
|
+
.option("--output <text>", "Completion output/summary")
|
|
342
|
+
.option("--pr <url>", "Pull request URL")
|
|
343
|
+
.option("--error <text>", "Error message (marks task as failed)")
|
|
344
|
+
.action(async (options) => {
|
|
345
|
+
const config = ensureConfig();
|
|
346
|
+
const taskId = getTaskId(options);
|
|
347
|
+
try {
|
|
348
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/complete`, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: {
|
|
351
|
+
"Content-Type": "application/json",
|
|
352
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
353
|
+
},
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
output: options.output,
|
|
356
|
+
prUrl: options.pr,
|
|
357
|
+
error: options.error,
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
if (!res.ok) {
|
|
361
|
+
throw new Error(`API error: ${res.status}`);
|
|
362
|
+
}
|
|
363
|
+
if (options.error) {
|
|
364
|
+
console.log(`✗ Task ${taskId} marked as failed`);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
console.log(`✓ Task ${taskId} completed`);
|
|
368
|
+
if (options.pr) {
|
|
369
|
+
console.log(` PR: ${options.pr}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
console.error("Error completing task:", error);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
// ============================================
|
|
379
|
+
// QA VALIDATION COMMANDS
|
|
380
|
+
// ============================================
|
|
381
|
+
// husky task qa-start [--id <id>] [--max-iterations <n>]
|
|
382
|
+
taskCommand
|
|
383
|
+
.command("qa-start")
|
|
384
|
+
.description("Start QA validation for a task")
|
|
385
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
386
|
+
.option("--max-iterations <n>", "Max QA iterations", "5")
|
|
387
|
+
.option("--no-auto-fix", "Disable automatic fix attempts")
|
|
388
|
+
.action(async (options) => {
|
|
389
|
+
const config = ensureConfig();
|
|
390
|
+
const taskId = getTaskId(options);
|
|
391
|
+
try {
|
|
392
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/qa/start`, {
|
|
393
|
+
method: "POST",
|
|
394
|
+
headers: {
|
|
395
|
+
"Content-Type": "application/json",
|
|
396
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
397
|
+
},
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
maxIterations: parseInt(options.maxIterations, 10),
|
|
400
|
+
autoFix: options.autoFix !== false,
|
|
401
|
+
}),
|
|
402
|
+
});
|
|
403
|
+
if (!res.ok) {
|
|
404
|
+
throw new Error(`API error: ${res.status}`);
|
|
405
|
+
}
|
|
406
|
+
const data = await res.json();
|
|
407
|
+
console.log(`✓ QA validation started for task ${taskId}`);
|
|
408
|
+
console.log(` Max iterations: ${data.maxIterations}`);
|
|
409
|
+
console.log(` Status: ${data.qaStatus}`);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
console.error("Error starting QA:", error);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
// husky task qa-status [--id <id>] [--json]
|
|
417
|
+
taskCommand
|
|
418
|
+
.command("qa-status")
|
|
419
|
+
.description("Get QA validation status for a task")
|
|
420
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
421
|
+
.option("--json", "Output as JSON")
|
|
422
|
+
.action(async (options) => {
|
|
423
|
+
const config = ensureConfig();
|
|
424
|
+
const taskId = getTaskId(options);
|
|
425
|
+
try {
|
|
426
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/qa/status`, {
|
|
427
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
428
|
+
});
|
|
429
|
+
if (!res.ok) {
|
|
430
|
+
throw new Error(`API error: ${res.status}`);
|
|
431
|
+
}
|
|
432
|
+
const data = await res.json();
|
|
433
|
+
if (options.json) {
|
|
434
|
+
console.log(JSON.stringify(data, null, 2));
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
console.log(`\n QA Status: ${data.taskTitle}`);
|
|
438
|
+
console.log(" " + "─".repeat(50));
|
|
439
|
+
console.log(` Status: ${data.qaStatus}`);
|
|
440
|
+
console.log(` Iterations: ${data.iterations.total}/${data.qaMaxIterations}`);
|
|
441
|
+
console.log(` Approved: ${data.iterations.approved}`);
|
|
442
|
+
console.log(` Rejected: ${data.iterations.rejected}`);
|
|
443
|
+
console.log(` Errors: ${data.iterations.errors}`);
|
|
444
|
+
if (data.latestIssues && data.latestIssues.length > 0) {
|
|
445
|
+
console.log(`\n Latest Issues:`);
|
|
446
|
+
for (const issue of data.latestIssues.slice(0, 5)) {
|
|
447
|
+
const icon = issue.type === "critical" ? "🔴" : issue.type === "major" ? "🟠" : "🟡";
|
|
448
|
+
console.log(` ${icon} [${issue.type}] ${issue.title}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (data.isComplete) {
|
|
452
|
+
console.log(`\n ✓ QA Complete: ${data.qaStatus}`);
|
|
453
|
+
}
|
|
454
|
+
else if (data.requiresHumanReview) {
|
|
455
|
+
console.log(`\n ⚠ Human review required`);
|
|
456
|
+
}
|
|
457
|
+
console.log("");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
console.error("Error getting QA status:", error);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
// husky task qa-approve [--id <id>] [--notes <text>]
|
|
466
|
+
taskCommand
|
|
467
|
+
.command("qa-approve")
|
|
468
|
+
.description("Manually approve QA for a task")
|
|
469
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
470
|
+
.option("--notes <text>", "Approval notes")
|
|
471
|
+
.action(async (options) => {
|
|
472
|
+
const config = ensureConfig();
|
|
473
|
+
const taskId = getTaskId(options);
|
|
474
|
+
try {
|
|
475
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/qa/approve`, {
|
|
476
|
+
method: "POST",
|
|
477
|
+
headers: {
|
|
478
|
+
"Content-Type": "application/json",
|
|
479
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
480
|
+
},
|
|
481
|
+
body: JSON.stringify({
|
|
482
|
+
approved: true,
|
|
483
|
+
notes: options.notes,
|
|
484
|
+
}),
|
|
485
|
+
});
|
|
486
|
+
if (!res.ok) {
|
|
487
|
+
throw new Error(`API error: ${res.status}`);
|
|
488
|
+
}
|
|
489
|
+
console.log(`✓ QA manually approved for task ${taskId}`);
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
console.error("Error approving QA:", error);
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
// husky task qa-reject [--id <id>] [--notes <text>]
|
|
497
|
+
taskCommand
|
|
498
|
+
.command("qa-reject")
|
|
499
|
+
.description("Manually reject QA for a task")
|
|
500
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
501
|
+
.option("--notes <text>", "Rejection notes")
|
|
502
|
+
.action(async (options) => {
|
|
503
|
+
const config = ensureConfig();
|
|
504
|
+
const taskId = getTaskId(options);
|
|
505
|
+
try {
|
|
506
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/qa/approve`, {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: {
|
|
509
|
+
"Content-Type": "application/json",
|
|
510
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
511
|
+
},
|
|
512
|
+
body: JSON.stringify({
|
|
513
|
+
approved: false,
|
|
514
|
+
notes: options.notes,
|
|
515
|
+
}),
|
|
516
|
+
});
|
|
517
|
+
if (!res.ok) {
|
|
518
|
+
throw new Error(`API error: ${res.status}`);
|
|
519
|
+
}
|
|
520
|
+
console.log(`✗ QA manually rejected for task ${taskId}`);
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
console.error("Error rejecting QA:", error);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
// husky task qa-iteration [--id <id>] --iteration <n> --status <status> [--issues <json>] [--duration <seconds>]
|
|
528
|
+
taskCommand
|
|
529
|
+
.command("qa-iteration")
|
|
530
|
+
.description("Add a QA iteration result (for agents)")
|
|
531
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
532
|
+
.requiredOption("--iteration <n>", "Iteration number")
|
|
533
|
+
.requiredOption("--status <status>", "Status (approved, rejected, error)")
|
|
534
|
+
.option("--issues <json>", "Issues as JSON array")
|
|
535
|
+
.option("--duration <seconds>", "Duration in seconds", "0")
|
|
536
|
+
.action(async (options) => {
|
|
537
|
+
const config = ensureConfig();
|
|
538
|
+
const taskId = getTaskId(options);
|
|
539
|
+
// Parse issues
|
|
540
|
+
let issues = [];
|
|
541
|
+
if (options.issues) {
|
|
542
|
+
try {
|
|
543
|
+
issues = JSON.parse(options.issues);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
console.error("Error: --issues must be valid JSON");
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/qa/iteration`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: {
|
|
554
|
+
"Content-Type": "application/json",
|
|
555
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
556
|
+
},
|
|
557
|
+
body: JSON.stringify({
|
|
558
|
+
iteration: parseInt(options.iteration, 10),
|
|
559
|
+
status: options.status,
|
|
560
|
+
issues,
|
|
561
|
+
duration: parseFloat(options.duration),
|
|
562
|
+
}),
|
|
563
|
+
});
|
|
564
|
+
if (!res.ok) {
|
|
565
|
+
throw new Error(`API error: ${res.status}`);
|
|
566
|
+
}
|
|
567
|
+
const data = await res.json();
|
|
568
|
+
console.log(`✓ QA iteration ${options.iteration} recorded`);
|
|
569
|
+
console.log(` Status: ${data.qaStatus}`);
|
|
570
|
+
console.log(` Issues: ${data.issuesCount}`);
|
|
571
|
+
console.log(` ${data.message}`);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
console.error("Error adding QA iteration:", error);
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
// husky task qa-escalate [--id <id>]
|
|
579
|
+
taskCommand
|
|
580
|
+
.command("qa-escalate")
|
|
581
|
+
.description("Escalate QA to human review")
|
|
582
|
+
.option("--id <id>", "Task ID (or set HUSKY_TASK_ID)")
|
|
583
|
+
.action(async (options) => {
|
|
584
|
+
const config = ensureConfig();
|
|
585
|
+
const taskId = getTaskId(options);
|
|
586
|
+
try {
|
|
587
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/qa/approve`, {
|
|
588
|
+
method: "PUT", // PUT for escalation
|
|
589
|
+
headers: {
|
|
590
|
+
"Content-Type": "application/json",
|
|
591
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
if (!res.ok) {
|
|
595
|
+
throw new Error(`API error: ${res.status}`);
|
|
596
|
+
}
|
|
597
|
+
console.log(`⚠ QA escalated to human review for task ${taskId}`);
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
console.error("Error escalating QA:", error);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
function printTasks(tasks) {
|
|
605
|
+
const byStatus = {
|
|
606
|
+
backlog: [],
|
|
607
|
+
in_progress: [],
|
|
608
|
+
review: [],
|
|
609
|
+
done: [],
|
|
610
|
+
};
|
|
611
|
+
for (const task of tasks) {
|
|
612
|
+
if (byStatus[task.status]) {
|
|
613
|
+
byStatus[task.status].push(task);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const statusLabels = {
|
|
617
|
+
backlog: "BACKLOG",
|
|
618
|
+
in_progress: "IN PROGRESS",
|
|
619
|
+
review: "REVIEW",
|
|
620
|
+
done: "DONE",
|
|
621
|
+
};
|
|
622
|
+
for (const [status, label] of Object.entries(statusLabels)) {
|
|
623
|
+
const statusTasks = byStatus[status];
|
|
624
|
+
if (statusTasks.length === 0)
|
|
625
|
+
continue;
|
|
626
|
+
console.log(`\n ${label}`);
|
|
627
|
+
console.log(" " + "─".repeat(50));
|
|
628
|
+
for (const task of statusTasks) {
|
|
629
|
+
const agentStr = task.agent ? ` (${task.agent})` : "";
|
|
630
|
+
const doneStr = status === "done" ? " ✓" : "";
|
|
631
|
+
console.log(` #${task.id.slice(0, 6)} ${task.title.padEnd(30)} ${task.priority}${agentStr}${doneStr}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
console.log("");
|
|
635
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { taskCommand } from "./commands/task.js";
|
|
4
|
+
import { configCommand } from "./commands/config.js";
|
|
5
|
+
import { agentCommand } from "./commands/agent.js";
|
|
6
|
+
import { roadmapCommand } from "./commands/roadmap.js";
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("husky")
|
|
10
|
+
.description("CLI for Huskyv0 Task Orchestration with Claude Agent")
|
|
11
|
+
.version("0.3.0");
|
|
12
|
+
program.addCommand(taskCommand);
|
|
13
|
+
program.addCommand(configCommand);
|
|
14
|
+
program.addCommand(agentCommand);
|
|
15
|
+
program.addCommand(roadmapCommand);
|
|
16
|
+
program.parse();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamClient - Sends output to Husky Dashboard via SSE
|
|
3
|
+
* Uses batching to reduce API calls
|
|
4
|
+
*/
|
|
5
|
+
export declare class StreamClient {
|
|
6
|
+
private apiUrl;
|
|
7
|
+
private sessionId;
|
|
8
|
+
private apiKey;
|
|
9
|
+
private buffer;
|
|
10
|
+
private flushTimeout;
|
|
11
|
+
private flushIntervalMs;
|
|
12
|
+
private maxBufferSize;
|
|
13
|
+
constructor(apiUrl: string, sessionId: string, apiKey: string);
|
|
14
|
+
private flushBuffer;
|
|
15
|
+
private scheduleFlush;
|
|
16
|
+
send(content: string, type: "stdout" | "stderr" | "system" | "plan"): Promise<void>;
|
|
17
|
+
sendImmediate(content: string, type: "stdout" | "stderr" | "system" | "plan"): Promise<void>;
|
|
18
|
+
stdout(content: string): Promise<void>;
|
|
19
|
+
stderr(content: string): Promise<void>;
|
|
20
|
+
system(content: string): Promise<void>;
|
|
21
|
+
plan(content: string): Promise<void>;
|
|
22
|
+
flush(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Update session status in Husky Dashboard
|
|
26
|
+
*/
|
|
27
|
+
export declare function updateSessionStatus(apiUrl: string, sessionId: string, apiKey: string, status: string, data?: Record<string, unknown>): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Submit plan for approval
|
|
30
|
+
*/
|
|
31
|
+
export declare function submitPlan(apiUrl: string, sessionId: string, apiKey: string, plan: {
|
|
32
|
+
steps: Array<{
|
|
33
|
+
order: number;
|
|
34
|
+
description: string;
|
|
35
|
+
files: string[];
|
|
36
|
+
risk: "low" | "medium" | "high";
|
|
37
|
+
}>;
|
|
38
|
+
estimatedCost: number;
|
|
39
|
+
estimatedRuntime: number;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Wait for plan approval from user
|
|
43
|
+
*/
|
|
44
|
+
export declare function waitForApproval(apiUrl: string, sessionId: string, apiKey: string, timeoutMs?: number): Promise<"approved" | "rejected" | "timeout">;
|