@simonfestl/husky-cli 1.4.1 → 1.5.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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Husky Mermaid Command
3
+ *
4
+ * Validates Mermaid diagram syntax to catch common AI mistakes
5
+ * (missing brackets, invalid keywords, etc.)
6
+ */
7
+ import { Command } from "commander";
8
+ export declare const mermaidCommand: Command;
9
+ export default mermaidCommand;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Husky Mermaid Command
3
+ *
4
+ * Validates Mermaid diagram syntax to catch common AI mistakes
5
+ * (missing brackets, invalid keywords, etc.)
6
+ */
7
+ import { Command } from "commander";
8
+ import { readFileSync, existsSync } from "fs";
9
+ // Mermaid diagram type patterns
10
+ const DIAGRAM_TYPES = [
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
+ { name: "erDiagram", pattern: /^erDiagram/i },
16
+ { name: "gantt", pattern: /^gantt/i },
17
+ { name: "pie", pattern: /^pie/i },
18
+ { name: "gitGraph", pattern: /^gitGraph/i },
19
+ { name: "mindmap", pattern: /^mindmap/i },
20
+ { name: "timeline", pattern: /^timeline/i },
21
+ { name: "journey", pattern: /^journey/i },
22
+ ];
23
+ /**
24
+ * Validate Mermaid diagram syntax
25
+ */
26
+ function validateMermaid(code) {
27
+ const errors = [];
28
+ const warnings = [];
29
+ let diagramType = null;
30
+ // Clean up code
31
+ const lines = code.trim().split("\n").map(l => l.trim());
32
+ const cleanCode = lines.join("\n");
33
+ // Check for empty input
34
+ if (!cleanCode) {
35
+ return { valid: false, diagramType: null, errors: ["Empty diagram"], warnings: [] };
36
+ }
37
+ // Detect diagram type
38
+ for (const type of DIAGRAM_TYPES) {
39
+ if (type.pattern.test(cleanCode)) {
40
+ diagramType = type.name;
41
+ break;
42
+ }
43
+ }
44
+ if (!diagramType) {
45
+ errors.push(`Unknown diagram type. First line should be one of: flowchart, sequenceDiagram, classDiagram, etc.`);
46
+ errors.push(`Got: "${lines[0].substring(0, 50)}..."`);
47
+ }
48
+ // Check bracket balance
49
+ const brackets = { "[": 0, "{": 0, "(": 0, "<": 0 };
50
+ const bracketPairs = { "[": "]", "{": "}", "(": ")", "<": ">" };
51
+ for (let i = 0; i < cleanCode.length; i++) {
52
+ const char = cleanCode[i];
53
+ if (char in brackets) {
54
+ brackets[char]++;
55
+ }
56
+ else if (char === "]") {
57
+ brackets["["]--;
58
+ }
59
+ else if (char === "}") {
60
+ brackets["{"]--;
61
+ }
62
+ else if (char === ")") {
63
+ brackets["("]--;
64
+ }
65
+ else if (char === ">") {
66
+ // Only count > if it's not part of an arrow
67
+ const prev = cleanCode[i - 1];
68
+ if (prev !== "-" && prev !== "=") {
69
+ brackets["<"]--;
70
+ }
71
+ }
72
+ }
73
+ for (const [open, count] of Object.entries(brackets)) {
74
+ if (count > 0) {
75
+ errors.push(`Unmatched '${open}' - missing ${count} closing '${bracketPairs[open]}'`);
76
+ }
77
+ else if (count < 0) {
78
+ errors.push(`Unmatched '${bracketPairs[open]}' - ${Math.abs(count)} extra closing bracket(s)`);
79
+ }
80
+ }
81
+ // Check for common syntax issues based on diagram type
82
+ if (diagramType === "flowchart" || diagramType?.startsWith("graph")) {
83
+ // Check node definitions
84
+ const nodePattern = /([A-Za-z0-9_]+)\s*(\[|\(|\{|\[\[|\(\(|\{\{)/g;
85
+ let match;
86
+ while ((match = nodePattern.exec(cleanCode)) !== null) {
87
+ const nodeId = match[1];
88
+ const bracket = match[2];
89
+ const fullMatch = match[0];
90
+ // Check if bracket is properly closed on same line (simple check)
91
+ const lineWithNode = lines.find(l => l.includes(fullMatch));
92
+ if (lineWithNode) {
93
+ const closingBracket = bracket === "[[" ? "]]" :
94
+ bracket === "((" ? "))" :
95
+ bracket === "{{" ? "}}" :
96
+ bracket === "[" ? "]" :
97
+ bracket === "(" ? ")" :
98
+ bracket === "{" ? "}" : "";
99
+ if (!lineWithNode.includes(closingBracket)) {
100
+ warnings.push(`Node '${nodeId}' might have unclosed bracket '${bracket}'`);
101
+ }
102
+ }
103
+ }
104
+ // Check arrow syntax
105
+ const invalidArrows = cleanCode.match(/[^-=]>[^>|{(\[]/g);
106
+ if (invalidArrows) {
107
+ warnings.push(`Possible invalid arrow syntax. Use '-->' or '==>' for connections.`);
108
+ }
109
+ }
110
+ if (diagramType === "sequenceDiagram") {
111
+ // Check participant definitions
112
+ const hasParticipant = /participant\s+\w+/i.test(cleanCode);
113
+ const hasActor = /actor\s+\w+/i.test(cleanCode);
114
+ if (!hasParticipant && !hasActor) {
115
+ warnings.push("No 'participant' or 'actor' defined. Consider adding them for clarity.");
116
+ }
117
+ }
118
+ // Check for common typos
119
+ const typos = {
120
+ "subgrah": "subgraph",
121
+ "paticipant": "participant",
122
+ "sequnce": "sequence",
123
+ "flowcart": "flowchart",
124
+ "digram": "diagram",
125
+ };
126
+ for (const [typo, correct] of Object.entries(typos)) {
127
+ if (cleanCode.toLowerCase().includes(typo)) {
128
+ errors.push(`Typo detected: '${typo}' should be '${correct}'`);
129
+ }
130
+ }
131
+ // Check subgraph balance (only count "end" at start of line or after whitespace, not inside labels)
132
+ const subgraphCount = (cleanCode.match(/\bsubgraph\b/gi) || []).length;
133
+ // Match "end" only when it's a standalone keyword (start of line or after spaces, not inside brackets)
134
+ const endMatches = cleanCode.match(/^\s*end\s*$/gim) || [];
135
+ const endCount = endMatches.length;
136
+ if (subgraphCount > endCount) {
137
+ errors.push(`Missing 'end' for subgraph - found ${subgraphCount} subgraph(s) but only ${endCount} end(s)`);
138
+ }
139
+ else if (endCount > subgraphCount && subgraphCount > 0) {
140
+ warnings.push(`Extra 'end' keyword(s) - found ${endCount} end(s) but only ${subgraphCount} subgraph(s)`);
141
+ }
142
+ // Check for unclosed quotes
143
+ const quoteCount = (cleanCode.match(/"/g) || []).length;
144
+ if (quoteCount % 2 !== 0) {
145
+ errors.push(`Unclosed quote - found ${quoteCount} quote(s), should be even`);
146
+ }
147
+ return {
148
+ valid: errors.length === 0,
149
+ diagramType,
150
+ errors,
151
+ warnings,
152
+ };
153
+ }
154
+ export const mermaidCommand = new Command("mermaid")
155
+ .description("Validate Mermaid diagram syntax");
156
+ // husky mermaid validate <code-or-file>
157
+ mermaidCommand
158
+ .command("validate <input>")
159
+ .description("Validate Mermaid diagram syntax (pass code or file path)")
160
+ .option("--file", "Treat input as file path")
161
+ .option("--json", "Output as JSON")
162
+ .action((input, options) => {
163
+ let code;
164
+ if (options.file || existsSync(input)) {
165
+ // Read from file
166
+ if (!existsSync(input)) {
167
+ console.error(`Error: File not found: ${input}`);
168
+ process.exit(1);
169
+ }
170
+ code = readFileSync(input, "utf-8");
171
+ }
172
+ else {
173
+ // Use input as code directly
174
+ code = input;
175
+ }
176
+ const result = validateMermaid(code);
177
+ if (options.json) {
178
+ console.log(JSON.stringify(result, null, 2));
179
+ process.exit(result.valid ? 0 : 1);
180
+ }
181
+ // Human-readable output
182
+ if (result.diagramType) {
183
+ console.log(`Diagram type: ${result.diagramType}`);
184
+ }
185
+ if (result.errors.length > 0) {
186
+ console.log("\nErrors:");
187
+ for (const err of result.errors) {
188
+ console.log(` - ${err}`);
189
+ }
190
+ }
191
+ if (result.warnings.length > 0) {
192
+ console.log("\nWarnings:");
193
+ for (const warn of result.warnings) {
194
+ console.log(` - ${warn}`);
195
+ }
196
+ }
197
+ if (result.valid) {
198
+ console.log("\nDiagram syntax is valid");
199
+ }
200
+ else {
201
+ console.log("\nDiagram has syntax errors");
202
+ process.exit(1);
203
+ }
204
+ });
205
+ // husky mermaid check (alias for validate with stdin support)
206
+ mermaidCommand
207
+ .command("check")
208
+ .description("Check Mermaid syntax from stdin")
209
+ .option("--json", "Output as JSON")
210
+ .action(async (options) => {
211
+ // Read from stdin
212
+ const chunks = [];
213
+ for await (const chunk of process.stdin) {
214
+ chunks.push(chunk);
215
+ }
216
+ const code = Buffer.concat(chunks).toString("utf-8");
217
+ const result = validateMermaid(code);
218
+ if (options.json) {
219
+ console.log(JSON.stringify(result, null, 2));
220
+ }
221
+ else {
222
+ if (result.valid) {
223
+ console.log(`Valid ${result.diagramType || "diagram"}`);
224
+ }
225
+ else {
226
+ console.log(`Invalid: ${result.errors.join(", ")}`);
227
+ }
228
+ }
229
+ process.exit(result.valid ? 0 : 1);
230
+ });
231
+ export default mermaidCommand;
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ import { chatCommand } from "./commands/chat.js";
28
28
  import { previewCommand } from "./commands/preview.js";
29
29
  import { initCommand } from "./commands/init.js";
30
30
  import { brainCommand } from "./commands/brain.js";
31
+ import { mermaidCommand } from "./commands/mermaid.js";
31
32
  // Read version from package.json
32
33
  const require = createRequire(import.meta.url);
33
34
  const packageJson = require("../package.json");
@@ -63,6 +64,7 @@ program.addCommand(llmCommand);
63
64
  program.addCommand(initCommand);
64
65
  program.addCommand(agentMsgCommand);
65
66
  program.addCommand(brainCommand);
67
+ program.addCommand(mermaidCommand);
66
68
  // Handle --llm flag specially
67
69
  if (process.argv.includes("--llm")) {
68
70
  printLLMContext();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {