@simonfestl/husky-cli 1.4.0 → 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/biz/tickets.js +31 -1
- 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
|
@@ -126,6 +126,8 @@ ticketsCommand
|
|
|
126
126
|
.description("Update ticket properties")
|
|
127
127
|
.option("--status <status>", "New status (open, pending, solved)")
|
|
128
128
|
.option("--priority <priority>", "New priority (low, normal, high, urgent)")
|
|
129
|
+
.option("-f, --field <field...>", "Set custom field (format: field_id=value or name=value)")
|
|
130
|
+
.option("--json", "Output as JSON")
|
|
129
131
|
.action(async (id, options) => {
|
|
130
132
|
try {
|
|
131
133
|
const client = ZendeskClient.fromConfig();
|
|
@@ -134,13 +136,41 @@ ticketsCommand
|
|
|
134
136
|
updates.status = options.status;
|
|
135
137
|
if (options.priority)
|
|
136
138
|
updates.priority = options.priority;
|
|
139
|
+
if (options.field && options.field.length > 0) {
|
|
140
|
+
updates.custom_fields = options.field.map((f) => {
|
|
141
|
+
const [key, ...valueParts] = f.split("=");
|
|
142
|
+
const value = valueParts.join("=");
|
|
143
|
+
const fieldId = parseInt(key, 10);
|
|
144
|
+
if (isNaN(fieldId)) {
|
|
145
|
+
throw new Error(`Invalid field ID: ${key}. Use numeric field ID (e.g., 28080124674706=value)`);
|
|
146
|
+
}
|
|
147
|
+
if (value === "true")
|
|
148
|
+
return { id: fieldId, value: true };
|
|
149
|
+
if (value === "false")
|
|
150
|
+
return { id: fieldId, value: false };
|
|
151
|
+
if (value === "null" || value === "")
|
|
152
|
+
return { id: fieldId, value: null };
|
|
153
|
+
const numValue = parseInt(value, 10);
|
|
154
|
+
if (!isNaN(numValue) && String(numValue) === value) {
|
|
155
|
+
return { id: fieldId, value: numValue };
|
|
156
|
+
}
|
|
157
|
+
return { id: fieldId, value };
|
|
158
|
+
});
|
|
159
|
+
}
|
|
137
160
|
if (Object.keys(updates).length === 0) {
|
|
138
|
-
console.error("Error: Provide --status or --
|
|
161
|
+
console.error("Error: Provide --status, --priority, or --field");
|
|
139
162
|
process.exit(1);
|
|
140
163
|
}
|
|
141
164
|
const ticket = await client.updateTicket(parseInt(id, 10), updates);
|
|
165
|
+
if (options.json) {
|
|
166
|
+
console.log(JSON.stringify(ticket, null, 2));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
142
169
|
console.log(`✓ Updated ticket #${ticket.id}`);
|
|
143
170
|
console.log(` Status: ${ticket.status}, Priority: ${ticket.priority || "normal"}`);
|
|
171
|
+
if (updates.custom_fields) {
|
|
172
|
+
console.log(` Custom fields updated: ${updates.custom_fields.length}`);
|
|
173
|
+
}
|
|
144
174
|
}
|
|
145
175
|
catch (error) {
|
|
146
176
|
console.error("Error:", error.message);
|
|
@@ -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();
|