@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.
- package/dist/commands/mermaid.d.ts +9 -0
- package/dist/commands/mermaid.js +231 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
|
@@ -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();
|