@simonfestl/husky-cli 1.31.0 → 1.32.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.
@@ -10,10 +10,12 @@ const API_KEY_ROLES = [
10
10
  * Fetch VM Identity Token from GCP Metadata Server
11
11
  * This token is cryptographically signed by Google and proves the caller is running on a specific GCP VM.
12
12
  * Returns null if not running on GCP or metadata server is unavailable.
13
+ *
14
+ * Note: format=full is required to include google.compute_engine claims (instance_name, project_id, etc.)
13
15
  */
14
16
  async function getVMIdentityToken(audience = "husky-api") {
15
17
  try {
16
- const url = `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=${audience}`;
18
+ const url = `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=${audience}&format=full`;
17
19
  const res = await fetch(url, {
18
20
  headers: { "Metadata-Flavor": "Google" },
19
21
  signal: AbortSignal.timeout(3000), // 3 second timeout
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Husky Diagrams Command
3
+ *
4
+ * Generate and manage AI-powered code visualization diagrams (Mermaid)
5
+ */
6
+ import { Command } from "commander";
7
+ export declare const diagramsCommand: Command;
8
+ export default diagramsCommand;
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Husky Diagrams Command
3
+ *
4
+ * Generate and manage AI-powered code visualization diagrams (Mermaid)
5
+ */
6
+ import { Command } from "commander";
7
+ import chalk from "chalk";
8
+ import { getApiClient } from "../lib/api-client.js";
9
+ // Mermaid diagram type patterns
10
+ const DIAGRAM_PATTERNS = [
11
+ { name: "flowchart", pattern: /^(flowchart|graph)\s+(TB|BT|LR|RL|TD)/i },
12
+ { name: "sequenceDiagram", pattern: /^sequenceDiagram/i },
13
+ { name: "classDiagram", pattern: /^classDiagram/i },
14
+ { name: "stateDiagram", pattern: /^stateDiagram(-v2)?/i },
15
+ ];
16
+ /**
17
+ * Validate Mermaid diagram syntax
18
+ */
19
+ function validateMermaid(code) {
20
+ const errors = [];
21
+ const warnings = [];
22
+ let diagramType = null;
23
+ const lines = code.trim().split("\n").map((l) => l.trim());
24
+ const cleanCode = lines.join("\n");
25
+ if (!cleanCode) {
26
+ return { valid: false, diagramType: null, errors: ["Empty diagram"], warnings: [] };
27
+ }
28
+ for (const type of DIAGRAM_PATTERNS) {
29
+ if (type.pattern.test(cleanCode)) {
30
+ diagramType = type.name;
31
+ break;
32
+ }
33
+ }
34
+ if (!diagramType) {
35
+ errors.push("Unknown diagram type");
36
+ }
37
+ // Check bracket balance
38
+ const brackets = { "[": 0, "{": 0, "(": 0 };
39
+ for (const char of cleanCode) {
40
+ if (char === "[")
41
+ brackets["["]++;
42
+ else if (char === "]")
43
+ brackets["["]--;
44
+ else if (char === "{")
45
+ brackets["{"]++;
46
+ else if (char === "}")
47
+ brackets["{"]--;
48
+ else if (char === "(")
49
+ brackets["("]++;
50
+ else if (char === ")")
51
+ brackets["("]--;
52
+ }
53
+ for (const [bracket, count] of Object.entries(brackets)) {
54
+ if (count !== 0) {
55
+ errors.push(`Unbalanced brackets: ${bracket}`);
56
+ }
57
+ }
58
+ // Check subgraph balance
59
+ const subgraphCount = (cleanCode.match(/\bsubgraph\b/gi) || []).length;
60
+ const endCount = (cleanCode.match(/^\s*end\s*$/gim) || []).length;
61
+ if (subgraphCount > endCount) {
62
+ errors.push(`Missing 'end' for subgraph`);
63
+ }
64
+ return { valid: errors.length === 0, diagramType, errors, warnings };
65
+ }
66
+ // AI Prompts for diagram generation
67
+ const DIAGRAM_PROMPTS = {
68
+ flowchart: `Analyze this codebase and create a Mermaid flowchart showing:
69
+ - Main entry points and their execution flow
70
+ - Key decision points and branches
71
+ - Integration with external services
72
+ - Error handling paths
73
+
74
+ Focus on the high-level architecture, not implementation details.
75
+ Output ONLY valid Mermaid flowchart code starting with "flowchart TD".
76
+ Do not include any explanation or markdown code blocks.`,
77
+ sequence: `Analyze this codebase and create a Mermaid sequence diagram showing:
78
+ - Key user interactions and their flow through the system
79
+ - API request/response patterns
80
+ - Service-to-service communication
81
+ - Database interactions
82
+
83
+ Include the most common/important flows only.
84
+ Output ONLY valid Mermaid sequence diagram code starting with "sequenceDiagram".
85
+ Do not include any explanation or markdown code blocks.`,
86
+ class: `Analyze this codebase and create a Mermaid class diagram showing:
87
+ - Main entities/models and their relationships
88
+ - Key interfaces and their implementations
89
+ - Important inheritance hierarchies
90
+ - Composition relationships
91
+
92
+ Focus on domain models and core abstractions.
93
+ Output ONLY valid Mermaid class diagram code starting with "classDiagram".
94
+ Do not include any explanation or markdown code blocks.`,
95
+ architecture: `Analyze this codebase and create a Mermaid architecture diagram showing:
96
+ - System components and their boundaries
97
+ - External dependencies (databases, APIs, services)
98
+ - Data flow between components
99
+ - Deployment topology
100
+
101
+ Use subgraphs to group related components.
102
+ Output ONLY valid Mermaid flowchart code with subgraphs starting with "flowchart TB".
103
+ Do not include any explanation or markdown code blocks.`,
104
+ };
105
+ export const diagramsCommand = new Command("diagrams").description("Generate and manage AI-powered code visualization diagrams");
106
+ // husky diagrams list
107
+ diagramsCommand
108
+ .command("list")
109
+ .description("List diagrams")
110
+ .option("--repo <url>", "Filter by repository URL")
111
+ .option("--type <type>", "Filter by type (flowchart, sequence, class, architecture)")
112
+ .option("--json", "Output as JSON")
113
+ .action(async (options) => {
114
+ try {
115
+ const api = getApiClient();
116
+ const params = new URLSearchParams();
117
+ if (options.repo)
118
+ params.set("repoUrl", options.repo);
119
+ if (options.type)
120
+ params.set("type", options.type);
121
+ const response = await api.get(`/api/diagrams?${params.toString()}`);
122
+ if (options.json) {
123
+ console.log(JSON.stringify(response.diagrams, null, 2));
124
+ return;
125
+ }
126
+ if (response.diagrams.length === 0) {
127
+ console.log(chalk.yellow("No diagrams found"));
128
+ return;
129
+ }
130
+ console.log(chalk.bold(`\nDiagrams (${response.diagrams.length}):\n`));
131
+ for (const diagram of response.diagrams) {
132
+ const typeColor = diagram.type === "flowchart"
133
+ ? chalk.blue
134
+ : diagram.type === "sequence"
135
+ ? chalk.magenta
136
+ : diagram.type === "class"
137
+ ? chalk.green
138
+ : chalk.yellow;
139
+ console.log(` ${chalk.bold(diagram.title)}`);
140
+ console.log(` ID: ${diagram.id}`);
141
+ console.log(` Type: ${typeColor(diagram.type)}`);
142
+ console.log(` Repo: ${diagram.repoUrl}`);
143
+ console.log(` Updated: ${new Date(diagram.updatedAt).toLocaleString()}`);
144
+ console.log();
145
+ }
146
+ }
147
+ catch (error) {
148
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : error);
149
+ process.exit(1);
150
+ }
151
+ });
152
+ // husky diagrams get <id>
153
+ diagramsCommand
154
+ .command("get <id>")
155
+ .description("Get a specific diagram")
156
+ .option("--json", "Output as JSON")
157
+ .option("--code", "Output only the Mermaid code")
158
+ .action(async (id, options) => {
159
+ try {
160
+ const api = getApiClient();
161
+ const diagram = await api.get(`/api/diagrams/${id}`);
162
+ if (options.code) {
163
+ console.log(diagram.mermaidCode);
164
+ return;
165
+ }
166
+ if (options.json) {
167
+ console.log(JSON.stringify(diagram, null, 2));
168
+ return;
169
+ }
170
+ console.log(chalk.bold(`\n${diagram.title}\n`));
171
+ console.log(`Type: ${diagram.type}`);
172
+ console.log(`Repo: ${diagram.repoUrl}`);
173
+ if (diagram.description)
174
+ console.log(`Description: ${diagram.description}`);
175
+ if (diagram.gitCommitSha)
176
+ console.log(`Commit: ${diagram.gitCommitSha}`);
177
+ console.log(`Generated by: ${diagram.generatedBy}`);
178
+ console.log();
179
+ console.log(chalk.dim("Mermaid Code:"));
180
+ console.log(chalk.cyan(diagram.mermaidCode));
181
+ }
182
+ catch (error) {
183
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : error);
184
+ process.exit(1);
185
+ }
186
+ });
187
+ // husky diagrams validate <file-or-code>
188
+ diagramsCommand
189
+ .command("validate <input>")
190
+ .description("Validate Mermaid diagram syntax")
191
+ .option("--json", "Output as JSON")
192
+ .action(async (input, options) => {
193
+ try {
194
+ const { readFileSync, existsSync } = await import("fs");
195
+ let code;
196
+ if (existsSync(input)) {
197
+ code = readFileSync(input, "utf-8");
198
+ }
199
+ else {
200
+ code = input;
201
+ }
202
+ const result = validateMermaid(code);
203
+ if (options.json) {
204
+ console.log(JSON.stringify(result, null, 2));
205
+ process.exit(result.valid ? 0 : 1);
206
+ }
207
+ if (result.diagramType) {
208
+ console.log(`Diagram type: ${result.diagramType}`);
209
+ }
210
+ if (result.errors.length > 0) {
211
+ console.log(chalk.red("\nErrors:"));
212
+ for (const err of result.errors) {
213
+ console.log(` - ${err}`);
214
+ }
215
+ }
216
+ if (result.warnings.length > 0) {
217
+ console.log(chalk.yellow("\nWarnings:"));
218
+ for (const warn of result.warnings) {
219
+ console.log(` - ${warn}`);
220
+ }
221
+ }
222
+ if (result.valid) {
223
+ console.log(chalk.green("\nDiagram syntax is valid"));
224
+ }
225
+ else {
226
+ console.log(chalk.red("\nDiagram has syntax errors"));
227
+ process.exit(1);
228
+ }
229
+ }
230
+ catch (error) {
231
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : error);
232
+ process.exit(1);
233
+ }
234
+ });
235
+ // husky diagrams generate
236
+ diagramsCommand
237
+ .command("generate")
238
+ .description("Generate diagrams for a codebase using AI")
239
+ .option("--repo <url>", "Repository URL (required for upload)")
240
+ .option("--path <path>", "Local path to analyze", ".")
241
+ .option("--types <types>", "Diagram types (comma-separated)", "flowchart,architecture")
242
+ .option("--commit <sha>", "Git commit SHA")
243
+ .option("--upload", "Upload to Husky API")
244
+ .option("--json", "Output as JSON")
245
+ .action(async (options) => {
246
+ try {
247
+ const { readdirSync, readFileSync } = await import("fs");
248
+ const { join, relative } = await import("path");
249
+ const types = options.types.split(",").map((t) => t.trim());
250
+ // Collect codebase summary
251
+ console.log(chalk.blue("Analyzing codebase..."));
252
+ const collectFiles = (dir, files = []) => {
253
+ const entries = readdirSync(dir, { withFileTypes: true });
254
+ for (const entry of entries) {
255
+ const fullPath = join(dir, entry.name);
256
+ if (entry.isDirectory()) {
257
+ if (!["node_modules", ".git", "dist", ".next", "coverage"].includes(entry.name)) {
258
+ collectFiles(fullPath, files);
259
+ }
260
+ }
261
+ else if (entry.isFile() && /\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
262
+ files.push(fullPath);
263
+ }
264
+ }
265
+ return files;
266
+ };
267
+ const files = collectFiles(options.path);
268
+ console.log(`Found ${files.length} source files`);
269
+ // Generate diagrams
270
+ const results = [];
271
+ for (const type of types) {
272
+ if (!DIAGRAM_PROMPTS[type]) {
273
+ console.log(chalk.yellow(`Unknown diagram type: ${type}, skipping`));
274
+ continue;
275
+ }
276
+ console.log(chalk.blue(`Generating ${type} diagram...`));
277
+ // For now, create a placeholder diagram
278
+ // In production, this would call the Anthropic API
279
+ const mermaidCode = generatePlaceholderDiagram(type);
280
+ const validation = validateMermaid(mermaidCode);
281
+ results.push({
282
+ type,
283
+ title: `${type.charAt(0).toUpperCase() + type.slice(1)} Diagram`,
284
+ mermaidCode,
285
+ valid: validation.valid,
286
+ });
287
+ if (validation.valid) {
288
+ console.log(chalk.green(` ${type}: Valid`));
289
+ }
290
+ else {
291
+ console.log(chalk.yellow(` ${type}: ${validation.errors.join(", ")}`));
292
+ }
293
+ }
294
+ // Upload if requested
295
+ if (options.upload && options.repo) {
296
+ console.log(chalk.blue("\nUploading to Husky API..."));
297
+ const api = getApiClient();
298
+ const diagrams = results
299
+ .filter((r) => r.valid)
300
+ .map((r) => ({
301
+ repoUrl: options.repo,
302
+ type: r.type,
303
+ title: r.title,
304
+ mermaidCode: r.mermaidCode,
305
+ gitCommitSha: options.commit,
306
+ generatedBy: "cli",
307
+ }));
308
+ if (diagrams.length > 0) {
309
+ const response = await api.post(`/api/diagrams/batch`, { diagrams });
310
+ console.log(chalk.green(`Uploaded ${response.created} diagrams`));
311
+ }
312
+ }
313
+ if (options.json) {
314
+ console.log(JSON.stringify(results, null, 2));
315
+ }
316
+ else {
317
+ console.log(chalk.bold(`\nGenerated ${results.length} diagrams`));
318
+ for (const result of results) {
319
+ console.log(` ${result.valid ? chalk.green("✓") : chalk.red("✗")} ${result.type}`);
320
+ }
321
+ }
322
+ }
323
+ catch (error) {
324
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : error);
325
+ process.exit(1);
326
+ }
327
+ });
328
+ // Helper function to generate placeholder diagrams
329
+ function generatePlaceholderDiagram(type) {
330
+ switch (type) {
331
+ case "flowchart":
332
+ return `flowchart TD
333
+ A[Start] --> B{Check Input}
334
+ B -->|Valid| C[Process]
335
+ B -->|Invalid| D[Error Handler]
336
+ C --> E[Output]
337
+ D --> E
338
+ E --> F[End]`;
339
+ case "sequence":
340
+ return `sequenceDiagram
341
+ participant User
342
+ participant API
343
+ participant DB
344
+ User->>API: Request
345
+ API->>DB: Query
346
+ DB-->>API: Result
347
+ API-->>User: Response`;
348
+ case "class":
349
+ return `classDiagram
350
+ class Application {
351
+ +start()
352
+ +stop()
353
+ }
354
+ class Service {
355
+ +process()
356
+ }
357
+ Application --> Service`;
358
+ case "architecture":
359
+ return `flowchart TB
360
+ subgraph Frontend
361
+ A[Web App]
362
+ end
363
+ subgraph Backend
364
+ B[API Server]
365
+ C[Database]
366
+ end
367
+ A --> B
368
+ B --> C`;
369
+ default:
370
+ return `flowchart TD
371
+ A[Start] --> B[End]`;
372
+ }
373
+ }
374
+ export default diagramsCommand;
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ import { imageCommand } from "./commands/image.js";
37
37
  import { authCommand } from "./commands/auth.js";
38
38
  import { businessCommand } from "./commands/business.js";
39
39
  import { planCommand } from "./commands/plan.js";
40
+ import { diagramsCommand } from "./commands/diagrams.js";
40
41
  // Read version from package.json
41
42
  const require = createRequire(import.meta.url);
42
43
  const packageJson = require("../package.json");
@@ -81,6 +82,7 @@ program.addCommand(imageCommand);
81
82
  program.addCommand(authCommand);
82
83
  program.addCommand(businessCommand);
83
84
  program.addCommand(planCommand);
85
+ program.addCommand(diagramsCommand);
84
86
  // Handle --llm flag specially
85
87
  if (process.argv.includes("--llm")) {
86
88
  printLLMContext();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {