@simonfestl/husky-cli 0.9.5 β†’ 0.9.6

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.
@@ -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";
5
+ const VERSION = "0.9.6";
6
6
  export function generateLLMContext() {
7
7
  return `# Husky CLI Reference (v${VERSION})
8
8
 
@@ -125,8 +125,12 @@ husky changelog list # List changelogs
125
125
  \`\`\`bash
126
126
  husky worktree list # List worktrees
127
127
  husky worktree create <name> # Create isolated worktree
128
- husky worktree delete <name> # Delete worktree
129
- husky worktree switch <name> # Switch to worktree
128
+ husky worktree create <name> --task-id <id> # Create and register with task
129
+ husky worktree merge <name> # Merge back to base branch
130
+ husky worktree push <name> # Push branch to remote
131
+ husky worktree pr <name> -t "Title" # Create pull request
132
+ husky worktree remove <name> # Remove worktree
133
+ husky worktree sync-stats <name> --task-id <id> # Sync stats to dashboard
130
134
  \`\`\`
131
135
 
132
136
  ### VM Sessions (Cloud Agents)
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const serviceAccountCommand: Command;
@@ -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,2 @@
1
+ import { Command } from "commander";
2
+ export declare const servicesCommand: Command;
@@ -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
+ });