@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.
- package/dist/commands/llm-context.js +7 -3
- 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/vm.js +230 -6
- package/dist/commands/worktree.js +481 -1
- package/dist/index.js +4 -0
- 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.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
|
|
129
|
-
husky 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,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
|
+
});
|