@simonfestl/husky-cli 0.9.7 → 1.2.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 +56 -13
- package/dist/commands/chat.d.ts +2 -0
- package/dist/commands/chat.js +669 -0
- package/dist/commands/completion.js +0 -9
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +91 -0
- package/dist/commands/interactive/tasks.js +93 -9
- package/dist/commands/interactive.js +0 -5
- package/dist/commands/llm-context.d.ts +0 -4
- package/dist/commands/llm-context.js +4 -14
- package/dist/commands/preview.d.ts +2 -0
- package/dist/commands/preview.js +161 -0
- package/dist/commands/task.js +66 -11
- package/dist/index.js +11 -5
- package/dist/lib/project-resolver.d.ts +26 -0
- package/dist/lib/project-resolver.js +111 -0
- package/package.json +1 -1
- package/dist/commands/interactive/jules-sessions.d.ts +0 -1
- package/dist/commands/interactive/jules-sessions.js +0 -460
- package/dist/commands/jules.d.ts +0 -2
- package/dist/commands/jules.js +0 -593
- package/dist/commands/services.d.ts +0 -2
- package/dist/commands/services.js +0 -381
|
@@ -20,7 +20,6 @@ const COMMANDS = {
|
|
|
20
20
|
idea: ["list", "create", "get", "update", "delete", "convert"],
|
|
21
21
|
department: ["list", "create", "get", "update", "delete"],
|
|
22
22
|
vm: ["list", "create", "get", "update", "delete", "start", "stop", "logs", "approve", "reject"],
|
|
23
|
-
jules: ["list", "create", "get", "update", "delete", "message", "approve", "activities", "sources"],
|
|
24
23
|
process: ["list", "create", "get", "update", "delete"],
|
|
25
24
|
strategy: [
|
|
26
25
|
"show", "set-vision", "set-mission", "add-value", "update-value", "delete-value",
|
|
@@ -195,7 +194,6 @@ _husky() {
|
|
|
195
194
|
'idea:Manage ideas'
|
|
196
195
|
'department:Manage departments'
|
|
197
196
|
'vm:Manage VM sessions'
|
|
198
|
-
'jules:Manage Jules AI coding sessions'
|
|
199
197
|
'process:Manage processes'
|
|
200
198
|
'strategy:Manage business strategy'
|
|
201
199
|
'settings:Manage application settings'
|
|
@@ -286,7 +284,6 @@ function getCommandDescription(cmd) {
|
|
|
286
284
|
idea: "Manage ideas",
|
|
287
285
|
department: "Manage departments",
|
|
288
286
|
vm: "Manage VM sessions",
|
|
289
|
-
jules: "Manage Jules AI coding sessions",
|
|
290
287
|
process: "Manage processes",
|
|
291
288
|
strategy: "Manage business strategy",
|
|
292
289
|
settings: "Manage application settings",
|
|
@@ -356,12 +353,6 @@ function getSubcommandDescription(cmd, sub) {
|
|
|
356
353
|
approve: "Approve VM session plan",
|
|
357
354
|
reject: "Reject VM session plan",
|
|
358
355
|
},
|
|
359
|
-
jules: {
|
|
360
|
-
message: "Send a message to a Jules session",
|
|
361
|
-
approve: "Approve a Jules session plan",
|
|
362
|
-
activities: "Get session activities",
|
|
363
|
-
sources: "List available Jules sources",
|
|
364
|
-
},
|
|
365
356
|
strategy: {
|
|
366
357
|
"set-vision": "Set/update the company vision",
|
|
367
358
|
"set-mission": "Set/update the company mission",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { existsSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { generateLLMContext } from "./llm-context.js";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const packageJson = require("../../package.json");
|
|
8
|
+
const HUSKY_MD_FILENAME = "HUSKY.md";
|
|
9
|
+
function generateHuskyMdContent() {
|
|
10
|
+
const timestamp = new Date().toISOString();
|
|
11
|
+
const cliRef = generateLLMContext();
|
|
12
|
+
return `<!--
|
|
13
|
+
Auto-generated by husky init (v${packageJson.version})
|
|
14
|
+
Generated: ${timestamp}
|
|
15
|
+
|
|
16
|
+
This file instructs AI coding agents to use the Husky CLI.
|
|
17
|
+
Update with: husky init --force
|
|
18
|
+
-->
|
|
19
|
+
|
|
20
|
+
${cliRef}
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Standard Workflow for AI Agents
|
|
25
|
+
|
|
26
|
+
### On Session Start
|
|
27
|
+
\`\`\`bash
|
|
28
|
+
husky config test # Verify API connection
|
|
29
|
+
husky worker whoami # Confirm worker identity
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
### When Working on a Task
|
|
33
|
+
\`\`\`bash
|
|
34
|
+
# 1. Get task details
|
|
35
|
+
husky task get <id>
|
|
36
|
+
|
|
37
|
+
# 2. Start the task (creates isolated worktree)
|
|
38
|
+
husky task start <id>
|
|
39
|
+
|
|
40
|
+
# 3. CD into the worktree directory (MANDATORY)
|
|
41
|
+
cd <worktree-path> # Path shown in task start output
|
|
42
|
+
|
|
43
|
+
# 4. Report progress as you work
|
|
44
|
+
husky task message <id> "Analyzing codebase..."
|
|
45
|
+
husky task message <id> "Implementing feature X..."
|
|
46
|
+
|
|
47
|
+
# 5. When done, create PR and complete
|
|
48
|
+
husky worktree pr <worktree-name> -t "feat: description"
|
|
49
|
+
husky task done <id> --pr <pr-url>
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
**IMPORTANT:** After \`husky task start\`, you MUST \`cd\` into the worktree directory before making any code changes.
|
|
53
|
+
|
|
54
|
+
### When Handling Customer Support
|
|
55
|
+
\`\`\`bash
|
|
56
|
+
# 1. Get full customer context
|
|
57
|
+
husky biz customers 360 <email>
|
|
58
|
+
|
|
59
|
+
# 2. Check relevant tickets
|
|
60
|
+
husky biz tickets search "<customer-email>"
|
|
61
|
+
|
|
62
|
+
# 3. Check order history if needed
|
|
63
|
+
husky biz orders search "<order-id-or-email>"
|
|
64
|
+
|
|
65
|
+
# 4. Reply with context
|
|
66
|
+
husky biz tickets reply <ticket-id> "Your response..."
|
|
67
|
+
\`\`\`
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
export const initCommand = new Command("init")
|
|
71
|
+
.description("Initialize Husky in the current directory (creates HUSKY.md)")
|
|
72
|
+
.option("-f, --force", "Overwrite existing HUSKY.md")
|
|
73
|
+
.option("-q, --quiet", "Suppress output")
|
|
74
|
+
.action((options) => {
|
|
75
|
+
const targetPath = join(process.cwd(), HUSKY_MD_FILENAME);
|
|
76
|
+
if (existsSync(targetPath) && !options.force) {
|
|
77
|
+
console.error(`Error: ${HUSKY_MD_FILENAME} already exists.`);
|
|
78
|
+
console.error("Use --force to overwrite.");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const content = generateHuskyMdContent();
|
|
82
|
+
writeFileSync(targetPath, content);
|
|
83
|
+
if (!options.quiet) {
|
|
84
|
+
console.log(`✓ Created ${HUSKY_MD_FILENAME}`);
|
|
85
|
+
console.log("");
|
|
86
|
+
console.log(" This file instructs AI agents to use the Husky CLI.");
|
|
87
|
+
console.log(" Commit it to your repository so all agents see it.");
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log(" Update with: husky init --force");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { select, input, confirm } from "@inquirer/prompts";
|
|
2
2
|
import { ensureConfig, pressEnterToContinue, truncate } from "./utils.js";
|
|
3
|
+
import { resolveProject } from "../../lib/project-resolver.js";
|
|
3
4
|
export async function tasksMenu() {
|
|
4
5
|
const config = ensureConfig();
|
|
5
6
|
const menuItems = [
|
|
@@ -157,7 +158,6 @@ async function createTask(config) {
|
|
|
157
158
|
],
|
|
158
159
|
default: "medium",
|
|
159
160
|
});
|
|
160
|
-
// Optional: Link to project
|
|
161
161
|
const projects = await fetchProjects(config);
|
|
162
162
|
let projectId;
|
|
163
163
|
if (projects.length > 0) {
|
|
@@ -166,8 +166,49 @@ async function createTask(config) {
|
|
|
166
166
|
default: false,
|
|
167
167
|
});
|
|
168
168
|
if (linkToProject) {
|
|
169
|
-
const
|
|
170
|
-
|
|
169
|
+
const selectionMethod = await select({
|
|
170
|
+
message: "How to select project?",
|
|
171
|
+
choices: [
|
|
172
|
+
{ name: "Choose from list", value: "list" },
|
|
173
|
+
{ name: "Type project name", value: "type" },
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
if (selectionMethod === "list") {
|
|
177
|
+
const projectChoices = projects.map((p) => ({ name: p.name, value: p.id }));
|
|
178
|
+
projectId = await select({ message: "Select project:", choices: projectChoices });
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const projectInput = await input({
|
|
182
|
+
message: "Project name or ID:",
|
|
183
|
+
validate: (value) => (value.length > 0 ? true : "Project name required"),
|
|
184
|
+
});
|
|
185
|
+
const resolved = await resolveProject(projectInput, config);
|
|
186
|
+
if (!resolved) {
|
|
187
|
+
console.log(`\n ❌ Project "${projectInput}" not found.`);
|
|
188
|
+
console.log(" Available projects:");
|
|
189
|
+
for (const p of projects) {
|
|
190
|
+
console.log(` - ${p.name}`);
|
|
191
|
+
}
|
|
192
|
+
console.log("");
|
|
193
|
+
await pressEnterToContinue();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (resolved.resolvedBy === "fuzzy-match") {
|
|
197
|
+
const confirmFuzzy = await confirm({
|
|
198
|
+
message: `Did you mean "${resolved.projectName}"? (${Math.round(resolved.confidence * 100)}% match)`,
|
|
199
|
+
default: true,
|
|
200
|
+
});
|
|
201
|
+
if (!confirmFuzzy) {
|
|
202
|
+
console.log("\n Cancelled.\n");
|
|
203
|
+
await pressEnterToContinue();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (resolved.resolvedBy !== "exact-id") {
|
|
208
|
+
console.log(` ℹ️ Resolved to: ${resolved.projectName}`);
|
|
209
|
+
}
|
|
210
|
+
projectId = resolved.projectId;
|
|
211
|
+
}
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
const res = await fetch(`${config.apiUrl}/api/tasks`, {
|
|
@@ -254,16 +295,59 @@ async function updateTask(config) {
|
|
|
254
295
|
});
|
|
255
296
|
break;
|
|
256
297
|
case "project":
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
298
|
+
const projectsForUpdate = await fetchProjects(config);
|
|
299
|
+
if (projectsForUpdate.length === 0) {
|
|
259
300
|
console.log("\n No projects available.\n");
|
|
260
301
|
await pressEnterToContinue();
|
|
261
302
|
return;
|
|
262
303
|
}
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
304
|
+
const projectSelectionMethod = await select({
|
|
305
|
+
message: "How to select project?",
|
|
306
|
+
choices: [
|
|
307
|
+
{ name: "Choose from list", value: "list" },
|
|
308
|
+
{ name: "Type project name", value: "type" },
|
|
309
|
+
{ name: "Remove project link", value: "remove" },
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
if (projectSelectionMethod === "remove") {
|
|
313
|
+
updateData.projectId = "";
|
|
314
|
+
}
|
|
315
|
+
else if (projectSelectionMethod === "list") {
|
|
316
|
+
const projectChoicesForUpdate = projectsForUpdate.map((p) => ({ name: p.name, value: p.id }));
|
|
317
|
+
updateData.projectId = await select({ message: "Select project:", choices: projectChoicesForUpdate });
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const projectInputForUpdate = await input({
|
|
321
|
+
message: "Project name or ID:",
|
|
322
|
+
validate: (value) => (value.length > 0 ? true : "Project name required"),
|
|
323
|
+
});
|
|
324
|
+
const resolvedForUpdate = await resolveProject(projectInputForUpdate, config);
|
|
325
|
+
if (!resolvedForUpdate) {
|
|
326
|
+
console.log(`\n ❌ Project "${projectInputForUpdate}" not found.`);
|
|
327
|
+
console.log(" Available projects:");
|
|
328
|
+
for (const p of projectsForUpdate) {
|
|
329
|
+
console.log(` - ${p.name}`);
|
|
330
|
+
}
|
|
331
|
+
console.log("");
|
|
332
|
+
await pressEnterToContinue();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (resolvedForUpdate.resolvedBy === "fuzzy-match") {
|
|
336
|
+
const confirmFuzzyUpdate = await confirm({
|
|
337
|
+
message: `Did you mean "${resolvedForUpdate.projectName}"? (${Math.round(resolvedForUpdate.confidence * 100)}% match)`,
|
|
338
|
+
default: true,
|
|
339
|
+
});
|
|
340
|
+
if (!confirmFuzzyUpdate) {
|
|
341
|
+
console.log("\n Cancelled.\n");
|
|
342
|
+
await pressEnterToContinue();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (resolvedForUpdate.resolvedBy !== "exact-id") {
|
|
347
|
+
console.log(` ℹ️ Resolved to: ${resolvedForUpdate.projectName}`);
|
|
348
|
+
}
|
|
349
|
+
updateData.projectId = resolvedForUpdate.projectId;
|
|
350
|
+
}
|
|
267
351
|
break;
|
|
268
352
|
case "title":
|
|
269
353
|
updateData.title = await input({
|
|
@@ -8,7 +8,6 @@ import { departmentsMenu } from "./interactive/departments.js";
|
|
|
8
8
|
import { processesMenu } from "./interactive/processes.js";
|
|
9
9
|
import { workflowsMenu } from "./interactive/workflows.js";
|
|
10
10
|
import { vmSessionsMenu } from "./interactive/vm-sessions.js";
|
|
11
|
-
import { julesSessionsMenu } from "./interactive/jules-sessions.js";
|
|
12
11
|
import { roadmapsMenu } from "./interactive/roadmaps.js";
|
|
13
12
|
import { strategyMenu } from "./interactive/strategy.js";
|
|
14
13
|
import { changelogMenu } from "./interactive/changelog.js";
|
|
@@ -34,7 +33,6 @@ export async function runInteractiveMode() {
|
|
|
34
33
|
{ name: "Processes", value: "processes", description: "Manage processes" },
|
|
35
34
|
{ name: "---", value: "separator2", description: "" },
|
|
36
35
|
{ name: "VM Sessions", value: "vm", description: "Manage VM sessions" },
|
|
37
|
-
{ name: "Jules Sessions", value: "jules", description: "Manage Jules AI sessions" },
|
|
38
36
|
{ name: "Worktrees", value: "worktrees", description: "Manage Git worktrees for agent isolation" },
|
|
39
37
|
{ name: "---", value: "separator3", description: "" },
|
|
40
38
|
{ name: "Business Operations", value: "business", description: "Billbee, Zendesk, SeaTable, Qdrant" },
|
|
@@ -75,9 +73,6 @@ export async function runInteractiveMode() {
|
|
|
75
73
|
case "vm":
|
|
76
74
|
await vmSessionsMenu();
|
|
77
75
|
break;
|
|
78
|
-
case "jules":
|
|
79
|
-
await julesSessionsMenu();
|
|
80
|
-
break;
|
|
81
76
|
case "worktrees":
|
|
82
77
|
await worktreesMenu();
|
|
83
78
|
break;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*/
|
|
5
|
-
const VERSION = "0.9.7";
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const packageJson = require("../../package.json");
|
|
6
4
|
export function generateLLMContext() {
|
|
7
|
-
return `# Husky CLI Reference (v${
|
|
5
|
+
return `# Husky CLI Reference (v${packageJson.version})
|
|
8
6
|
|
|
9
7
|
> [!CAUTION]
|
|
10
8
|
> ## MANDATORY: You MUST Use Husky CLI
|
|
@@ -146,14 +144,6 @@ husky vm ssh <id> # SSH into VM
|
|
|
146
144
|
husky vm delete <id> # Delete VM
|
|
147
145
|
\`\`\`
|
|
148
146
|
|
|
149
|
-
### Jules Sessions (AI Agent)
|
|
150
|
-
\`\`\`bash
|
|
151
|
-
husky jules list # List Jules sessions
|
|
152
|
-
husky jules create # Create new session
|
|
153
|
-
husky jules status <id> # Get session status
|
|
154
|
-
husky jules logs <id> # View session logs
|
|
155
|
-
\`\`\`
|
|
156
|
-
|
|
157
147
|
### Workers (Multi-Agent Coordination)
|
|
158
148
|
\`\`\`bash
|
|
159
149
|
husky worker whoami # Show current worker identity
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
const PREVIEW_DEPLOY_TRIGGER_ID = "80b3ba55-ae74-41cd-b7d0-a477ecc357b1";
|
|
4
|
+
const PREVIEW_CLEANUP_TRIGGER_ID = "965c3e86-677f-4063-b391-43019f621ea2";
|
|
5
|
+
const GCP_PROJECT = "tigerv0";
|
|
6
|
+
async function apiRequest(method, path, body) {
|
|
7
|
+
const config = getConfig();
|
|
8
|
+
const url = `${config.apiUrl}${path}`;
|
|
9
|
+
const response = await fetch(url, {
|
|
10
|
+
method,
|
|
11
|
+
headers: {
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
"x-api-key": config.apiKey || "",
|
|
14
|
+
},
|
|
15
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
16
|
+
});
|
|
17
|
+
return response;
|
|
18
|
+
}
|
|
19
|
+
async function triggerCloudBuild(triggerId, substitutions) {
|
|
20
|
+
const { execSync } = await import("child_process");
|
|
21
|
+
const subsArgs = Object.entries(substitutions)
|
|
22
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
23
|
+
.join(",");
|
|
24
|
+
try {
|
|
25
|
+
const cmd = `gcloud builds triggers run ${triggerId} --project=${GCP_PROJECT} --branch=main --substitutions=${subsArgs} --format="value(metadata.build.id)" 2>&1`;
|
|
26
|
+
const output = execSync(cmd, { encoding: "utf-8" }).trim();
|
|
27
|
+
const buildId = output.split("\n").pop() || "";
|
|
28
|
+
return { success: true, buildId };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return { success: false, error: message };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const previewCommand = new Command("preview")
|
|
36
|
+
.description("Manage PR preview deployments");
|
|
37
|
+
previewCommand
|
|
38
|
+
.command("list")
|
|
39
|
+
.description("List active preview deployments")
|
|
40
|
+
.option("--json", "Output as JSON")
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
try {
|
|
43
|
+
const response = await apiRequest("GET", "/api/previews");
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
console.error(`Error: ${response.status} ${response.statusText}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const previews = await response.json();
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.log(JSON.stringify(previews, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!previews.length) {
|
|
54
|
+
console.log("No active previews");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.log("\nActive Previews:\n");
|
|
58
|
+
for (const p of previews) {
|
|
59
|
+
console.log(` PR #${p.prNumber}${p.prTitle ? `: ${p.prTitle}` : ""}`);
|
|
60
|
+
console.log(` Status: ${p.status}`);
|
|
61
|
+
console.log(` Dashboard: ${p.dashboardUrl}`);
|
|
62
|
+
console.log(` Terminal: ${p.terminalUrl}`);
|
|
63
|
+
console.log(` Commit: ${p.commitSha.slice(0, 7)}`);
|
|
64
|
+
console.log("");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error("Failed to fetch previews:", error);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
previewCommand
|
|
73
|
+
.command("deploy <pr-number>")
|
|
74
|
+
.description("Deploy a preview for a PR")
|
|
75
|
+
.option("--branch <branch>", "Branch to deploy (default: from PR)")
|
|
76
|
+
.action(async (prNumber, options) => {
|
|
77
|
+
const prNum = parseInt(prNumber, 10);
|
|
78
|
+
if (isNaN(prNum) || prNum <= 0) {
|
|
79
|
+
console.error("Error: PR number must be a positive integer");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
console.log(`Triggering preview deployment for PR #${prNum}...`);
|
|
83
|
+
const result = await triggerCloudBuild(PREVIEW_DEPLOY_TRIGGER_ID, {
|
|
84
|
+
_PR_NUMBER: String(prNum),
|
|
85
|
+
});
|
|
86
|
+
if (result.success) {
|
|
87
|
+
console.log(`Build started: ${result.buildId}`);
|
|
88
|
+
console.log(`\nMonitor at: https://console.cloud.google.com/cloud-build/builds/${result.buildId}?project=${GCP_PROJECT}`);
|
|
89
|
+
console.log("\nPreview URLs will be available once build completes (~5-10 min)");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.error(`Failed to trigger build: ${result.error}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
previewCommand
|
|
97
|
+
.command("cleanup [pr-number]")
|
|
98
|
+
.description("Cleanup preview deployments (specific PR or all merged)")
|
|
99
|
+
.action(async (prNumber) => {
|
|
100
|
+
const substitutions = {};
|
|
101
|
+
if (prNumber) {
|
|
102
|
+
const prNum = parseInt(prNumber, 10);
|
|
103
|
+
if (isNaN(prNum) || prNum <= 0) {
|
|
104
|
+
console.error("Error: PR number must be a positive integer");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
substitutions._PR_NUMBER = String(prNum);
|
|
108
|
+
console.log(`Cleaning up preview for PR #${prNum}...`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log("Cleaning up all merged/closed PR previews...");
|
|
112
|
+
}
|
|
113
|
+
const result = await triggerCloudBuild(PREVIEW_CLEANUP_TRIGGER_ID, substitutions);
|
|
114
|
+
if (result.success) {
|
|
115
|
+
console.log(`Cleanup started: ${result.buildId}`);
|
|
116
|
+
console.log(`\nMonitor at: https://console.cloud.google.com/cloud-build/builds/${result.buildId}?project=${GCP_PROJECT}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.error(`Failed to trigger cleanup: ${result.error}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
previewCommand
|
|
124
|
+
.command("status <pr-number>")
|
|
125
|
+
.description("Get status of a specific preview")
|
|
126
|
+
.option("--json", "Output as JSON")
|
|
127
|
+
.action(async (prNumber, options) => {
|
|
128
|
+
const prNum = parseInt(prNumber, 10);
|
|
129
|
+
if (isNaN(prNum) || prNum <= 0) {
|
|
130
|
+
console.error("Error: PR number must be a positive integer");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const response = await apiRequest("GET", `/api/previews/${prNum}`);
|
|
135
|
+
if (response.status === 404) {
|
|
136
|
+
console.log(`No preview found for PR #${prNum}`);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
console.error(`Error: ${response.status} ${response.statusText}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const preview = await response.json();
|
|
144
|
+
if (options.json) {
|
|
145
|
+
console.log(JSON.stringify(preview, null, 2));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
console.log(`\nPreview for PR #${preview.prNumber}`);
|
|
149
|
+
if (preview.prTitle)
|
|
150
|
+
console.log(` Title: ${preview.prTitle}`);
|
|
151
|
+
console.log(` Status: ${preview.status}`);
|
|
152
|
+
console.log(` Dashboard: ${preview.dashboardUrl}`);
|
|
153
|
+
console.log(` Terminal: ${preview.terminalUrl}`);
|
|
154
|
+
console.log(` Commit: ${preview.commitSha}`);
|
|
155
|
+
console.log(` Created: ${new Date(preview.createdAt).toLocaleString()}`);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error("Failed to fetch preview:", error);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
});
|
package/dist/commands/task.js
CHANGED
|
@@ -6,6 +6,7 @@ import { paginateList, printPaginated } from "../lib/pagination.js";
|
|
|
6
6
|
import { ensureWorkerRegistered, generateSessionId, registerSession } from "../lib/worker.js";
|
|
7
7
|
import { WorktreeManager } from "../lib/worktree.js";
|
|
8
8
|
import { execSync } from "child_process";
|
|
9
|
+
import { resolveProject, fetchProjects, formatProjectList } from "../lib/project-resolver.js";
|
|
9
10
|
export const taskCommand = new Command("task")
|
|
10
11
|
.description("Manage tasks");
|
|
11
12
|
// Helper: Get task ID from --id flag or HUSKY_TASK_ID env var
|
|
@@ -281,6 +282,7 @@ taskCommand
|
|
|
281
282
|
.command("done <id>")
|
|
282
283
|
.description("Mark task as done")
|
|
283
284
|
.option("--pr <url>", "Link to PR")
|
|
285
|
+
.option("--skip-qa", "Skip QA review and mark as done directly")
|
|
284
286
|
.action(async (id, options) => {
|
|
285
287
|
const config = getConfig();
|
|
286
288
|
if (!config.apiUrl) {
|
|
@@ -294,25 +296,36 @@ taskCommand
|
|
|
294
296
|
"Content-Type": "application/json",
|
|
295
297
|
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
296
298
|
},
|
|
297
|
-
body: JSON.stringify({
|
|
299
|
+
body: JSON.stringify({
|
|
300
|
+
prUrl: options.pr,
|
|
301
|
+
skipQA: options.skipQa === true,
|
|
302
|
+
}),
|
|
298
303
|
});
|
|
299
304
|
if (!res.ok) {
|
|
300
305
|
throw new Error(`API error: ${res.status}`);
|
|
301
306
|
}
|
|
302
307
|
const task = await res.json();
|
|
303
|
-
|
|
308
|
+
if (task.qaTriggered) {
|
|
309
|
+
console.log(`✓ Task moved to review: ${task.title}`);
|
|
310
|
+
console.log(` QA pipeline triggered - awaiting verification`);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
console.log(`✓ Completed: ${task.title}`);
|
|
314
|
+
}
|
|
315
|
+
if (task.message) {
|
|
316
|
+
console.log(` ${task.message}`);
|
|
317
|
+
}
|
|
304
318
|
}
|
|
305
319
|
catch (error) {
|
|
306
320
|
console.error("Error completing task:", error);
|
|
307
321
|
process.exit(1);
|
|
308
322
|
}
|
|
309
323
|
});
|
|
310
|
-
// husky task create <title>
|
|
311
324
|
taskCommand
|
|
312
325
|
.command("create <title>")
|
|
313
326
|
.description("Create a new task")
|
|
314
327
|
.option("-d, --description <desc>", "Task description")
|
|
315
|
-
.option("--project <
|
|
328
|
+
.option("--project <project>", "Project name or ID")
|
|
316
329
|
.option("--path <path>", "Path in project")
|
|
317
330
|
.option("-p, --priority <priority>", "Priority (low, medium, high)", "medium")
|
|
318
331
|
.action(async (title, options) => {
|
|
@@ -321,6 +334,29 @@ taskCommand
|
|
|
321
334
|
console.error("Error: API URL not configured.");
|
|
322
335
|
process.exit(1);
|
|
323
336
|
}
|
|
337
|
+
let resolvedProjectId;
|
|
338
|
+
const resolverConfig = { apiUrl: config.apiUrl, apiKey: config.apiKey };
|
|
339
|
+
if (options.project) {
|
|
340
|
+
const resolved = await resolveProject(options.project, resolverConfig);
|
|
341
|
+
if (!resolved) {
|
|
342
|
+
const projects = await fetchProjects(resolverConfig);
|
|
343
|
+
console.error(`\n❌ Project "${options.project}" not found.\n`);
|
|
344
|
+
console.error(formatProjectList(projects));
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
if (resolved.resolvedBy === "name-match") {
|
|
348
|
+
console.log(`ℹ️ Resolved "${options.project}" → ${resolved.projectName} (${resolved.projectId})`);
|
|
349
|
+
}
|
|
350
|
+
if (resolved.resolvedBy === "fuzzy-match") {
|
|
351
|
+
const confirmed = await confirm(`Did you mean "${resolved.projectName}"? (${Math.round(resolved.confidence * 100)}% match)`);
|
|
352
|
+
if (!confirmed) {
|
|
353
|
+
console.log("Cancelled.");
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
console.log(`ℹ️ Using project: ${resolved.projectName} (${resolved.projectId})`);
|
|
357
|
+
}
|
|
358
|
+
resolvedProjectId = resolved.projectId;
|
|
359
|
+
}
|
|
324
360
|
try {
|
|
325
361
|
const res = await fetch(`${config.apiUrl}/api/tasks`, {
|
|
326
362
|
method: "POST",
|
|
@@ -331,13 +367,14 @@ taskCommand
|
|
|
331
367
|
body: JSON.stringify({
|
|
332
368
|
title,
|
|
333
369
|
description: options.description,
|
|
334
|
-
projectId:
|
|
370
|
+
projectId: resolvedProjectId,
|
|
335
371
|
linkedPath: options.path,
|
|
336
372
|
priority: options.priority,
|
|
337
373
|
}),
|
|
338
374
|
});
|
|
339
375
|
if (!res.ok) {
|
|
340
|
-
|
|
376
|
+
const errorData = await res.json().catch(() => ({}));
|
|
377
|
+
throw new Error(errorData.error || `API error: ${res.status}`);
|
|
341
378
|
}
|
|
342
379
|
const task = await res.json();
|
|
343
380
|
console.log(`✓ Created: #${task.id} ${task.title}`);
|
|
@@ -347,7 +384,6 @@ taskCommand
|
|
|
347
384
|
process.exit(1);
|
|
348
385
|
}
|
|
349
386
|
});
|
|
350
|
-
// husky task update <id>
|
|
351
387
|
taskCommand
|
|
352
388
|
.command("update <id>")
|
|
353
389
|
.description("Update task properties")
|
|
@@ -356,12 +392,12 @@ taskCommand
|
|
|
356
392
|
.option("--status <status>", "New status (e.g., backlog, in_progress, review, done, or custom status)")
|
|
357
393
|
.option("--priority <priority>", "New priority (low, medium, high, urgent)")
|
|
358
394
|
.option("--assignee <assignee>", "New assignee (human, llm, unassigned)")
|
|
359
|
-
.option("--project <
|
|
395
|
+
.option("--project <project>", "Link to project (name or ID)")
|
|
360
396
|
.option("--no-worktree", "Skip automatic worktree creation when starting task")
|
|
361
397
|
.option("--json", "Output as JSON")
|
|
362
398
|
.action(async (id, options) => {
|
|
363
399
|
const config = ensureConfig();
|
|
364
|
-
|
|
400
|
+
const resolverConfig = { apiUrl: config.apiUrl, apiKey: config.apiKey };
|
|
365
401
|
const updates = {};
|
|
366
402
|
if (options.title)
|
|
367
403
|
updates.title = options.title;
|
|
@@ -373,8 +409,27 @@ taskCommand
|
|
|
373
409
|
updates.priority = options.priority;
|
|
374
410
|
if (options.assignee)
|
|
375
411
|
updates.assignee = options.assignee;
|
|
376
|
-
if (options.project)
|
|
377
|
-
|
|
412
|
+
if (options.project) {
|
|
413
|
+
const resolved = await resolveProject(options.project, resolverConfig);
|
|
414
|
+
if (!resolved) {
|
|
415
|
+
const projects = await fetchProjects(resolverConfig);
|
|
416
|
+
console.error(`\n❌ Project "${options.project}" not found.\n`);
|
|
417
|
+
console.error(formatProjectList(projects));
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
if (resolved.resolvedBy === "name-match") {
|
|
421
|
+
console.log(`ℹ️ Resolved "${options.project}" → ${resolved.projectName} (${resolved.projectId})`);
|
|
422
|
+
}
|
|
423
|
+
if (resolved.resolvedBy === "fuzzy-match") {
|
|
424
|
+
const confirmed = await confirm(`Did you mean "${resolved.projectName}"? (${Math.round(resolved.confidence * 100)}% match)`);
|
|
425
|
+
if (!confirmed) {
|
|
426
|
+
console.log("Cancelled.");
|
|
427
|
+
process.exit(0);
|
|
428
|
+
}
|
|
429
|
+
console.log(`ℹ️ Using project: ${resolved.projectName} (${resolved.projectId})`);
|
|
430
|
+
}
|
|
431
|
+
updates.projectId = resolved.projectId;
|
|
432
|
+
}
|
|
378
433
|
if (Object.keys(updates).length === 0) {
|
|
379
434
|
console.error("Error: No update options provided. Use --help for available options.");
|
|
380
435
|
process.exit(1);
|