@simonfestl/husky-cli 0.9.5 β 0.9.7
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/dist/commands/llm-context.js +12 -4
- package/dist/commands/service-account.d.ts +2 -0
- package/dist/commands/service-account.js +180 -0
- package/dist/commands/services.d.ts +2 -0
- package/dist/commands/services.js +381 -0
- package/dist/commands/task.js +35 -4
- package/dist/commands/vm.js +230 -6
- package/dist/commands/worktree.js +481 -1
- package/dist/index.js +5 -1
- package/dist/lib/agent-templates.d.ts +20 -0
- package/dist/lib/agent-templates.js +142 -0
- package/dist/lib/worktree.d.ts +26 -0
- package/dist/lib/worktree.js +127 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* LLM Context Generator
|
|
3
3
|
* Outputs markdown reference for LLM agents
|
|
4
4
|
*/
|
|
5
|
-
const VERSION = "0.9.
|
|
5
|
+
const VERSION = "0.9.7";
|
|
6
6
|
export function generateLLMContext() {
|
|
7
7
|
return `# Husky CLI Reference (v${VERSION})
|
|
8
8
|
|
|
@@ -54,11 +54,15 @@ husky config test
|
|
|
54
54
|
husky task list [--status <status>] # List tasks
|
|
55
55
|
husky task get <id> # Get task details
|
|
56
56
|
husky task create # Create new task
|
|
57
|
-
husky task start <id> # Start
|
|
57
|
+
husky task start <id> # Start task (auto-creates worktree!)
|
|
58
|
+
husky task start <id> --no-worktree # Start without worktree
|
|
58
59
|
husky task done <id> [--pr <url>] # Mark task as done
|
|
59
60
|
husky task update <id> # Update task fields
|
|
60
61
|
husky task assign <id> <assignee> # Assign task
|
|
61
62
|
husky task log <id> # View task activity log
|
|
63
|
+
husky task message <id> "msg" # Post status message (positional)
|
|
64
|
+
husky task message -m "msg" --id <id> # Post status message (flags)
|
|
65
|
+
husky task message -m "msg" # Uses HUSKY_TASK_ID env var
|
|
62
66
|
\`\`\`
|
|
63
67
|
|
|
64
68
|
### Projects
|
|
@@ -125,8 +129,12 @@ husky changelog list # List changelogs
|
|
|
125
129
|
\`\`\`bash
|
|
126
130
|
husky worktree list # List worktrees
|
|
127
131
|
husky worktree create <name> # Create isolated worktree
|
|
128
|
-
husky worktree
|
|
129
|
-
husky worktree
|
|
132
|
+
husky worktree create <name> --task-id <id> # Create and register with task
|
|
133
|
+
husky worktree merge <name> # Merge back to base branch
|
|
134
|
+
husky worktree push <name> # Push branch to remote
|
|
135
|
+
husky worktree pr <name> -t "Title" # Create pull request
|
|
136
|
+
husky worktree remove <name> # Remove worktree
|
|
137
|
+
husky worktree sync-stats <name> --task-id <id> # Sync stats to dashboard
|
|
130
138
|
\`\`\`
|
|
131
139
|
|
|
132
140
|
### VM Sessions (Cloud Agents)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
const API_URL = process.env.HUSKY_API_URL || "https://huskyv0-dashboard-474966775596.europe-west1.run.app";
|
|
3
|
+
const API_KEY = process.env.HUSKY_API_KEY || "";
|
|
4
|
+
async function fetchApi(path, options = {}) {
|
|
5
|
+
const response = await fetch(`${API_URL}${path}`, {
|
|
6
|
+
...options,
|
|
7
|
+
headers: {
|
|
8
|
+
"Content-Type": "application/json",
|
|
9
|
+
"x-api-key": API_KEY,
|
|
10
|
+
...options.headers,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
15
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
16
|
+
}
|
|
17
|
+
return response.json();
|
|
18
|
+
}
|
|
19
|
+
function formatPersona(personaType) {
|
|
20
|
+
const icons = {
|
|
21
|
+
"support-agent": "π§",
|
|
22
|
+
"research-agent": "π¬",
|
|
23
|
+
"accounting-agent": "π",
|
|
24
|
+
"marketing-agent": "π’",
|
|
25
|
+
"developer-agent": "π»",
|
|
26
|
+
"devops-agent": "π§",
|
|
27
|
+
};
|
|
28
|
+
return `${icons[personaType] || "π€"} ${personaType}`;
|
|
29
|
+
}
|
|
30
|
+
function formatPermissionLevel(level) {
|
|
31
|
+
const colors = {
|
|
32
|
+
"read-only": "\x1b[90m", // gray
|
|
33
|
+
"developer": "\x1b[34m", // blue
|
|
34
|
+
"admin": "\x1b[31m", // red
|
|
35
|
+
};
|
|
36
|
+
const reset = "\x1b[0m";
|
|
37
|
+
return `${colors[level] || ""}${level}${reset}`;
|
|
38
|
+
}
|
|
39
|
+
export const serviceAccountCommand = new Command("sa")
|
|
40
|
+
.description("Manage Agent Service Accounts")
|
|
41
|
+
.addHelpText("after", `
|
|
42
|
+
Examples:
|
|
43
|
+
husky sa list List all service account bindings
|
|
44
|
+
husky sa get developer-agent Show binding for developer persona
|
|
45
|
+
husky sa sync Sync bindings from GCP config
|
|
46
|
+
husky sa verify Verify current VM's service account
|
|
47
|
+
`);
|
|
48
|
+
// List all bindings
|
|
49
|
+
serviceAccountCommand
|
|
50
|
+
.command("list")
|
|
51
|
+
.description("List all service account bindings")
|
|
52
|
+
.option("--json", "Output as JSON")
|
|
53
|
+
.action(async (options) => {
|
|
54
|
+
try {
|
|
55
|
+
const data = await fetchApi("/api/service-account-bindings");
|
|
56
|
+
const bindings = (data.bindings || []);
|
|
57
|
+
if (options.json) {
|
|
58
|
+
console.log(JSON.stringify(bindings, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (bindings.length === 0) {
|
|
62
|
+
console.log("No service account bindings found.");
|
|
63
|
+
console.log("Run 'husky sa sync' to create bindings from GCP config.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log("\nπ Service Account Bindings\n");
|
|
67
|
+
console.log("β".repeat(80));
|
|
68
|
+
for (const binding of bindings) {
|
|
69
|
+
console.log(`${formatPersona(binding.personaType)}`);
|
|
70
|
+
console.log(` Email: ${binding.serviceAccountEmail}`);
|
|
71
|
+
console.log(` Permission: ${formatPermissionLevel(binding.permissionLevel)}`);
|
|
72
|
+
console.log(` Roles: ${binding.iamRoles.join(", ") || "none"}`);
|
|
73
|
+
if (binding.lastSyncedAt) {
|
|
74
|
+
console.log(` Last Synced: ${new Date(binding.lastSyncedAt).toLocaleString()}`);
|
|
75
|
+
}
|
|
76
|
+
console.log("β".repeat(80));
|
|
77
|
+
}
|
|
78
|
+
console.log(`\nTotal: ${bindings.length} bindings`);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Get specific binding
|
|
86
|
+
serviceAccountCommand
|
|
87
|
+
.command("get <persona>")
|
|
88
|
+
.description("Get service account binding for a persona")
|
|
89
|
+
.option("--json", "Output as JSON")
|
|
90
|
+
.action(async (persona, options) => {
|
|
91
|
+
try {
|
|
92
|
+
// First list all and filter by persona
|
|
93
|
+
const data = await fetchApi("/api/service-account-bindings");
|
|
94
|
+
const bindings = (data.bindings || []);
|
|
95
|
+
const binding = bindings.find((b) => b.personaType === persona);
|
|
96
|
+
if (!binding) {
|
|
97
|
+
console.log(`No binding found for persona: ${persona}`);
|
|
98
|
+
console.log("\nAvailable personas:");
|
|
99
|
+
bindings.forEach((b) => console.log(` - ${b.personaType}`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (options.json) {
|
|
103
|
+
console.log(JSON.stringify(binding, null, 2));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
console.log(`\n${formatPersona(binding.personaType)}\n`);
|
|
107
|
+
console.log(`Email: ${binding.serviceAccountEmail}`);
|
|
108
|
+
console.log(`Permission: ${formatPermissionLevel(binding.permissionLevel)}`);
|
|
109
|
+
console.log(`Project: ${binding.projectId}`);
|
|
110
|
+
console.log(`Roles:`);
|
|
111
|
+
binding.iamRoles.forEach((role) => console.log(` - ${role}`));
|
|
112
|
+
if (binding.lastSyncedAt) {
|
|
113
|
+
console.log(`Last Synced: ${new Date(binding.lastSyncedAt).toLocaleString()}`);
|
|
114
|
+
}
|
|
115
|
+
console.log(`Created: ${new Date(binding.createdAt).toLocaleString()}`);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// Sync from GCP
|
|
123
|
+
serviceAccountCommand
|
|
124
|
+
.command("sync")
|
|
125
|
+
.description("Sync service account bindings from GCP config")
|
|
126
|
+
.action(async () => {
|
|
127
|
+
try {
|
|
128
|
+
console.log("π Syncing service account bindings...\n");
|
|
129
|
+
const data = await fetchApi("/api/service-account-bindings/sync", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
});
|
|
132
|
+
console.log(`β
Synced ${data.synced} bindings\n`);
|
|
133
|
+
for (const result of (data.results || [])) {
|
|
134
|
+
const icon = result.status === "synced" ? "β" : "β";
|
|
135
|
+
console.log(` ${icon} ${result.personaType}: ${result.email || result.status}`);
|
|
136
|
+
}
|
|
137
|
+
console.log("\nRun 'husky sa list' to see all bindings.");
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// Verify current VM's SA (for use on VMs)
|
|
145
|
+
serviceAccountCommand
|
|
146
|
+
.command("verify")
|
|
147
|
+
.description("Verify current VM's service account (run on VM)")
|
|
148
|
+
.action(async () => {
|
|
149
|
+
try {
|
|
150
|
+
// Check if running on GCP by querying metadata service
|
|
151
|
+
const metadataUrl = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email";
|
|
152
|
+
console.log("π Checking GCP metadata service...\n");
|
|
153
|
+
const response = await fetch(metadataUrl, {
|
|
154
|
+
headers: { "Metadata-Flavor": "Google" },
|
|
155
|
+
}).catch(() => null);
|
|
156
|
+
if (!response || !response.ok) {
|
|
157
|
+
console.log("β Not running on a GCP VM (metadata service unavailable)");
|
|
158
|
+
console.log("\nThis command is designed to run on GCP Compute Engine VMs.");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const saEmail = await response.text();
|
|
162
|
+
console.log(`β
Running as service account: ${saEmail}`);
|
|
163
|
+
// Try to match to a persona
|
|
164
|
+
const personaMatch = saEmail.match(/husky-(\w+)@/);
|
|
165
|
+
if (personaMatch) {
|
|
166
|
+
const persona = personaMatch[1] + "-agent";
|
|
167
|
+
console.log(` Persona: ${formatPersona(persona)}`);
|
|
168
|
+
}
|
|
169
|
+
// Report to Husky if we have a task ID
|
|
170
|
+
const taskId = process.env.HUSKY_TASK_ID;
|
|
171
|
+
if (taskId) {
|
|
172
|
+
console.log(`\nπ€ Reporting to Husky (task: ${taskId})...`);
|
|
173
|
+
// The VM startup script handles this, but we can also report here
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
// Helper: Ensure API is configured
|
|
4
|
+
function ensureConfig() {
|
|
5
|
+
const config = getConfig();
|
|
6
|
+
if (!config.apiUrl) {
|
|
7
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
return { apiUrl: config.apiUrl, apiKey: config.apiKey };
|
|
11
|
+
}
|
|
12
|
+
// Helper: Make API request
|
|
13
|
+
async function fetchAPI(apiUrl, apiKey, path, options = {}) {
|
|
14
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
15
|
+
...options,
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
...(apiKey ? { "x-api-key": apiKey } : {}),
|
|
19
|
+
...options.headers,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const error = await response.text();
|
|
24
|
+
throw new Error(`API Error: ${response.status} - ${error}`);
|
|
25
|
+
}
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
export const servicesCommand = new Command("services")
|
|
29
|
+
.description("Manage Agent Services (Agentic Workflow Platform)");
|
|
30
|
+
// husky services list
|
|
31
|
+
servicesCommand
|
|
32
|
+
.command("list")
|
|
33
|
+
.description("List all agent services")
|
|
34
|
+
.option("--project <id>", "Filter by project ID")
|
|
35
|
+
.option("--json", "Output as JSON")
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
const config = ensureConfig();
|
|
38
|
+
try {
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
if (options.project)
|
|
41
|
+
params.set("projectId", options.project);
|
|
42
|
+
const queryString = params.toString();
|
|
43
|
+
const path = queryString ? `/api/services?${queryString}` : "/api/services";
|
|
44
|
+
const services = await fetchAPI(config.apiUrl, config.apiKey, path);
|
|
45
|
+
if (options.json) {
|
|
46
|
+
console.log(JSON.stringify(services, null, 2));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (services.length === 0) {
|
|
50
|
+
console.log("No services found.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
console.log("\n Agent Services");
|
|
54
|
+
console.log(" " + "β".repeat(80));
|
|
55
|
+
for (const s of services) {
|
|
56
|
+
const statusIcon = {
|
|
57
|
+
building: "π¨",
|
|
58
|
+
deployed: "π¦",
|
|
59
|
+
running: "β
",
|
|
60
|
+
failed: "β",
|
|
61
|
+
disabled: "βΈοΈ",
|
|
62
|
+
};
|
|
63
|
+
console.log(` ${statusIcon[s.status] || "β"} ${s.name} (${s.type})`);
|
|
64
|
+
console.log(` ID: ${s.id}`);
|
|
65
|
+
console.log(` Status: ${s.status}`);
|
|
66
|
+
if (s.schedule)
|
|
67
|
+
console.log(` Schedule: ${s.schedule}`);
|
|
68
|
+
if (s.cloudRunUrl)
|
|
69
|
+
console.log(` URL: ${s.cloudRunUrl}`);
|
|
70
|
+
console.log(` Runs: ${s.runCount} (${s.successCount} success, ${s.failureCount} failed)`);
|
|
71
|
+
console.log("");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// husky services get <id>
|
|
80
|
+
servicesCommand
|
|
81
|
+
.command("get <id>")
|
|
82
|
+
.description("Get service details")
|
|
83
|
+
.option("--json", "Output as JSON")
|
|
84
|
+
.action(async (id, options) => {
|
|
85
|
+
const config = ensureConfig();
|
|
86
|
+
try {
|
|
87
|
+
const service = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}`);
|
|
88
|
+
if (options.json) {
|
|
89
|
+
console.log(JSON.stringify(service, null, 2));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(`\n Service: ${service.name}`);
|
|
93
|
+
console.log(" " + "β".repeat(50));
|
|
94
|
+
console.log(` ID: ${service.id}`);
|
|
95
|
+
console.log(` Type: ${service.type}`);
|
|
96
|
+
console.log(` Status: ${service.status}`);
|
|
97
|
+
if (service.description)
|
|
98
|
+
console.log(` Description: ${service.description}`);
|
|
99
|
+
if (service.schedule)
|
|
100
|
+
console.log(` Schedule: ${service.schedule}`);
|
|
101
|
+
if (service.cloudRunUrl)
|
|
102
|
+
console.log(` URL: ${service.cloudRunUrl}`);
|
|
103
|
+
if (service.dockerImage)
|
|
104
|
+
console.log(` Image: ${service.dockerImage}`);
|
|
105
|
+
console.log(` Runs: ${service.runCount} total`);
|
|
106
|
+
console.log(` Success: ${service.successCount}`);
|
|
107
|
+
console.log(` Failures: ${service.failureCount}`);
|
|
108
|
+
if (service.avgDurationMs)
|
|
109
|
+
console.log(` Avg Duration: ${service.avgDurationMs}ms`);
|
|
110
|
+
if (service.lastError)
|
|
111
|
+
console.log(` Last Error: ${service.lastError}`);
|
|
112
|
+
console.log(` Created: ${new Date(service.createdAt).toLocaleString()}`);
|
|
113
|
+
console.log(` Updated: ${new Date(service.updatedAt).toLocaleString()}`);
|
|
114
|
+
console.log("");
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// husky services create <name>
|
|
122
|
+
servicesCommand
|
|
123
|
+
.command("create <name>")
|
|
124
|
+
.description("Create a new agent service")
|
|
125
|
+
.option("-t, --type <type>", "Service type (cloud-run, cloud-function, scheduled-job)", "cloud-run")
|
|
126
|
+
.option("-d, --description <desc>", "Service description")
|
|
127
|
+
.option("-s, --schedule <cron>", 'Cron schedule (e.g., "0 6 * * *")')
|
|
128
|
+
.option("--project <id>", "Link to project")
|
|
129
|
+
.option("--repo <url>", "Source repository URL")
|
|
130
|
+
.option("--path <path>", "Source path in repository")
|
|
131
|
+
.action(async (name, options) => {
|
|
132
|
+
const config = ensureConfig();
|
|
133
|
+
try {
|
|
134
|
+
const service = await fetchAPI(config.apiUrl, config.apiKey, "/api/services", {
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
name,
|
|
138
|
+
type: options.type,
|
|
139
|
+
description: options.description,
|
|
140
|
+
schedule: options.schedule,
|
|
141
|
+
projectId: options.project,
|
|
142
|
+
sourceRepo: options.repo,
|
|
143
|
+
sourcePath: options.path,
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
console.log(`β Created service: ${service.name} (${service.id})`);
|
|
147
|
+
console.log(` Type: ${service.type}`);
|
|
148
|
+
console.log(` Status: ${service.status}`);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
// husky services update <id>
|
|
156
|
+
servicesCommand
|
|
157
|
+
.command("update <id>")
|
|
158
|
+
.description("Update a service")
|
|
159
|
+
.option("-d, --description <desc>", "Update description")
|
|
160
|
+
.option("-s, --schedule <cron>", "Update schedule")
|
|
161
|
+
.option("--status <status>", "Update status")
|
|
162
|
+
.option("--url <url>", "Set Cloud Run URL")
|
|
163
|
+
.option("--image <image>", "Set Docker image")
|
|
164
|
+
.action(async (id, options) => {
|
|
165
|
+
const config = ensureConfig();
|
|
166
|
+
const updates = {};
|
|
167
|
+
if (options.description)
|
|
168
|
+
updates.description = options.description;
|
|
169
|
+
if (options.schedule)
|
|
170
|
+
updates.schedule = options.schedule;
|
|
171
|
+
if (options.status)
|
|
172
|
+
updates.status = options.status;
|
|
173
|
+
if (options.url)
|
|
174
|
+
updates.cloudRunUrl = options.url;
|
|
175
|
+
if (options.image)
|
|
176
|
+
updates.dockerImage = options.image;
|
|
177
|
+
if (Object.keys(updates).length === 0) {
|
|
178
|
+
console.log("No updates specified. Use --help for available options.");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const service = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}`, {
|
|
183
|
+
method: "PATCH",
|
|
184
|
+
body: JSON.stringify(updates),
|
|
185
|
+
});
|
|
186
|
+
console.log(`β Updated service: ${service.name}`);
|
|
187
|
+
const changedFields = Object.keys(updates).join(", ");
|
|
188
|
+
console.log(` Changed: ${changedFields}`);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
// husky services delete <id>
|
|
196
|
+
servicesCommand
|
|
197
|
+
.command("delete <id>")
|
|
198
|
+
.description("Delete a service")
|
|
199
|
+
.option("-f, --force", "Skip confirmation")
|
|
200
|
+
.action(async (id, options) => {
|
|
201
|
+
const config = ensureConfig();
|
|
202
|
+
if (!options.force) {
|
|
203
|
+
console.log("Use --force to confirm deletion.");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}`, { method: "DELETE" });
|
|
208
|
+
console.log(`β Deleted service: ${id}`);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// husky services health
|
|
216
|
+
servicesCommand
|
|
217
|
+
.command("health")
|
|
218
|
+
.description("Check health of all services")
|
|
219
|
+
.option("--json", "Output as JSON")
|
|
220
|
+
.action(async (options) => {
|
|
221
|
+
const config = ensureConfig();
|
|
222
|
+
try {
|
|
223
|
+
const services = await fetchAPI(config.apiUrl, config.apiKey, "/api/services");
|
|
224
|
+
if (options.json) {
|
|
225
|
+
const healthData = services
|
|
226
|
+
.filter((s) => s.status !== "disabled")
|
|
227
|
+
.map((s) => ({
|
|
228
|
+
id: s.id,
|
|
229
|
+
name: s.name,
|
|
230
|
+
healthStatus: s.healthStatus || "unknown",
|
|
231
|
+
status: s.status,
|
|
232
|
+
lastError: s.lastError,
|
|
233
|
+
}));
|
|
234
|
+
console.log(JSON.stringify(healthData, null, 2));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.log("\n Service Health");
|
|
238
|
+
console.log(" " + "β".repeat(50));
|
|
239
|
+
let healthy = 0;
|
|
240
|
+
let unhealthy = 0;
|
|
241
|
+
let unknown = 0;
|
|
242
|
+
for (const s of services) {
|
|
243
|
+
if (s.status === "disabled")
|
|
244
|
+
continue;
|
|
245
|
+
const icon = s.healthStatus === "healthy"
|
|
246
|
+
? "β
"
|
|
247
|
+
: s.healthStatus === "unhealthy"
|
|
248
|
+
? "β"
|
|
249
|
+
: "β";
|
|
250
|
+
console.log(` ${icon} ${s.name}: ${s.healthStatus || "unknown"}`);
|
|
251
|
+
if (s.lastError && s.healthStatus === "unhealthy") {
|
|
252
|
+
console.log(` Last Error: ${s.lastError}`);
|
|
253
|
+
}
|
|
254
|
+
if (s.healthStatus === "healthy")
|
|
255
|
+
healthy++;
|
|
256
|
+
else if (s.healthStatus === "unhealthy")
|
|
257
|
+
unhealthy++;
|
|
258
|
+
else
|
|
259
|
+
unknown++;
|
|
260
|
+
}
|
|
261
|
+
console.log("");
|
|
262
|
+
console.log(` Summary: ${healthy} healthy, ${unhealthy} unhealthy, ${unknown} unknown`);
|
|
263
|
+
console.log("");
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
// husky services run <id>
|
|
271
|
+
servicesCommand
|
|
272
|
+
.command("run <id>")
|
|
273
|
+
.description("Trigger a service run")
|
|
274
|
+
.option("--input <json>", "Input data as JSON")
|
|
275
|
+
.option("--json", "Output as JSON")
|
|
276
|
+
.action(async (id, options) => {
|
|
277
|
+
const config = ensureConfig();
|
|
278
|
+
let inputData = {};
|
|
279
|
+
if (options.input) {
|
|
280
|
+
try {
|
|
281
|
+
inputData = JSON.parse(options.input);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
console.error("Error: --input must be valid JSON");
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const result = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}/run`, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
body: JSON.stringify({ input: inputData }),
|
|
292
|
+
});
|
|
293
|
+
if (options.json) {
|
|
294
|
+
console.log(JSON.stringify(result, null, 2));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
console.log(`β Service run triggered: ${id}`);
|
|
298
|
+
if (result.runId)
|
|
299
|
+
console.log(` Run ID: ${result.runId}`);
|
|
300
|
+
if (result.status)
|
|
301
|
+
console.log(` Status: ${result.status}`);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
// husky services logs <id>
|
|
309
|
+
servicesCommand
|
|
310
|
+
.command("logs <id>")
|
|
311
|
+
.description("View service logs")
|
|
312
|
+
.option("-n, --lines <num>", "Number of log lines", "50")
|
|
313
|
+
.option("--json", "Output as JSON")
|
|
314
|
+
.action(async (id, options) => {
|
|
315
|
+
const config = ensureConfig();
|
|
316
|
+
try {
|
|
317
|
+
const logs = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}/logs?limit=${options.lines}`);
|
|
318
|
+
if (options.json) {
|
|
319
|
+
console.log(JSON.stringify(logs, null, 2));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!logs.entries || logs.entries.length === 0) {
|
|
323
|
+
console.log(`No logs found for service ${id}`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
console.log(`\n Logs for service: ${id}`);
|
|
327
|
+
console.log(" " + "β".repeat(70));
|
|
328
|
+
for (const entry of logs.entries) {
|
|
329
|
+
const timestamp = new Date(entry.timestamp).toLocaleString();
|
|
330
|
+
const level = entry.level?.toUpperCase() || "INFO";
|
|
331
|
+
console.log(` [${timestamp}] ${level}: ${entry.message}`);
|
|
332
|
+
}
|
|
333
|
+
console.log("");
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
// husky services runs <id>
|
|
341
|
+
servicesCommand
|
|
342
|
+
.command("runs <id>")
|
|
343
|
+
.description("List recent runs for a service")
|
|
344
|
+
.option("-n, --limit <num>", "Number of runs to show", "10")
|
|
345
|
+
.option("--json", "Output as JSON")
|
|
346
|
+
.action(async (id, options) => {
|
|
347
|
+
const config = ensureConfig();
|
|
348
|
+
try {
|
|
349
|
+
const runs = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}/runs?limit=${options.limit}`);
|
|
350
|
+
if (options.json) {
|
|
351
|
+
console.log(JSON.stringify(runs, null, 2));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!runs || runs.length === 0) {
|
|
355
|
+
console.log(`No runs found for service ${id}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
console.log(`\n Recent Runs for service: ${id}`);
|
|
359
|
+
console.log(" " + "β".repeat(70));
|
|
360
|
+
for (const run of runs) {
|
|
361
|
+
const statusIcon = run.status === "success"
|
|
362
|
+
? "β
"
|
|
363
|
+
: run.status === "failed"
|
|
364
|
+
? "β"
|
|
365
|
+
: run.status === "running"
|
|
366
|
+
? "βΆ"
|
|
367
|
+
: "βΈοΈ";
|
|
368
|
+
const timestamp = new Date(run.startedAt).toLocaleString();
|
|
369
|
+
const duration = run.durationMs ? `${run.durationMs}ms` : "β";
|
|
370
|
+
console.log(` ${statusIcon} ${run.id.slice(0, 12)} β ${timestamp} β ${duration} β ${run.status}`);
|
|
371
|
+
if (run.error) {
|
|
372
|
+
console.log(` Error: ${run.error}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
console.log("");
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
});
|
package/dist/commands/task.js
CHANGED
|
@@ -221,7 +221,8 @@ taskCommand
|
|
|
221
221
|
taskCommand
|
|
222
222
|
.command("start <id>")
|
|
223
223
|
.description("Start working on a task")
|
|
224
|
-
.
|
|
224
|
+
.option("--no-worktree", "Skip worktree creation")
|
|
225
|
+
.action(async (id, options) => {
|
|
225
226
|
const config = getConfig();
|
|
226
227
|
if (!config.apiUrl) {
|
|
227
228
|
console.error("Error: API URL not configured.");
|
|
@@ -232,6 +233,15 @@ taskCommand
|
|
|
232
233
|
const workerId = await ensureWorkerRegistered(config.apiUrl, config.apiKey || "");
|
|
233
234
|
const sessionId = generateSessionId();
|
|
234
235
|
await registerSession(config.apiUrl, config.apiKey || "", workerId, sessionId);
|
|
236
|
+
// Create worktree for isolation (unless --no-worktree)
|
|
237
|
+
let worktreeInfo = null;
|
|
238
|
+
if (options.worktree !== false) {
|
|
239
|
+
worktreeInfo = createWorktreeForTask(id);
|
|
240
|
+
if (worktreeInfo) {
|
|
241
|
+
console.log(`β Created worktree: ${worktreeInfo.path}`);
|
|
242
|
+
console.log(` Branch: ${worktreeInfo.branch}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
235
245
|
const res = await fetch(`${config.apiUrl}/api/tasks/${id}/start`, {
|
|
236
246
|
method: "POST",
|
|
237
247
|
headers: {
|
|
@@ -242,6 +252,11 @@ taskCommand
|
|
|
242
252
|
agent: "claude-code",
|
|
243
253
|
workerId,
|
|
244
254
|
sessionId,
|
|
255
|
+
// Include worktree info if created
|
|
256
|
+
...(worktreeInfo ? {
|
|
257
|
+
worktreePath: worktreeInfo.path,
|
|
258
|
+
worktreeBranch: worktreeInfo.branch,
|
|
259
|
+
} : {}),
|
|
245
260
|
}),
|
|
246
261
|
});
|
|
247
262
|
if (!res.ok) {
|
|
@@ -251,6 +266,10 @@ taskCommand
|
|
|
251
266
|
console.log(`β Started: ${task.title}`);
|
|
252
267
|
console.log(` Worker: ${workerId}`);
|
|
253
268
|
console.log(` Session: ${sessionId}`);
|
|
269
|
+
// Show hint to cd into worktree
|
|
270
|
+
if (worktreeInfo) {
|
|
271
|
+
console.log(`\nπ‘ To work in isolation: cd ${worktreeInfo.path}`);
|
|
272
|
+
}
|
|
254
273
|
}
|
|
255
274
|
catch (error) {
|
|
256
275
|
console.error("Error starting task:", error);
|
|
@@ -547,11 +566,23 @@ taskCommand
|
|
|
547
566
|
});
|
|
548
567
|
// husky task message <id> "message" - post status message to task
|
|
549
568
|
taskCommand
|
|
550
|
-
.command("message
|
|
569
|
+
.command("message [id] [message]")
|
|
551
570
|
.description("Post a status message to a task")
|
|
552
|
-
.
|
|
571
|
+
.option("-m, --message <text>", "Message text (alternative to positional arg)")
|
|
572
|
+
.option("--id <taskId>", "Task ID (alternative to positional arg, or use HUSKY_TASK_ID)")
|
|
573
|
+
.action(async (idArg, messageArg, options) => {
|
|
553
574
|
const config = ensureConfig();
|
|
554
|
-
|
|
575
|
+
// Support both: `husky task message <id> <msg>` and `husky task message -m <msg> --id <id>`
|
|
576
|
+
const taskId = idArg || options.id || process.env.HUSKY_TASK_ID;
|
|
577
|
+
const message = messageArg || options.message;
|
|
578
|
+
if (!taskId) {
|
|
579
|
+
console.error("Error: Task ID required. Use positional arg, --id, or set HUSKY_TASK_ID");
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
if (!message) {
|
|
583
|
+
console.error("Error: Message required. Use positional arg or -m/--message");
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
555
586
|
try {
|
|
556
587
|
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/status`, {
|
|
557
588
|
method: "POST",
|