@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.
- package/dist/commands/auth.js +3 -1
- package/dist/commands/diagrams.d.ts +8 -0
- package/dist/commands/diagrams.js +374 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -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,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();
|