@simonfestl/husky-cli 0.3.0 → 0.5.1
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/README.md +228 -58
- package/dist/commands/changelog.d.ts +2 -0
- package/dist/commands/changelog.js +401 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +400 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +101 -1
- package/dist/commands/department.d.ts +2 -0
- package/dist/commands/department.js +240 -0
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +411 -0
- package/dist/commands/idea.d.ts +2 -0
- package/dist/commands/idea.js +340 -0
- package/dist/commands/interactive.d.ts +1 -0
- package/dist/commands/interactive.js +1397 -0
- package/dist/commands/jules.d.ts +2 -0
- package/dist/commands/jules.js +593 -0
- package/dist/commands/process.d.ts +2 -0
- package/dist/commands/process.js +289 -0
- package/dist/commands/project.d.ts +2 -0
- package/dist/commands/project.js +473 -0
- package/dist/commands/roadmap.js +318 -0
- package/dist/commands/settings.d.ts +2 -0
- package/dist/commands/settings.js +153 -0
- package/dist/commands/strategy.d.ts +2 -0
- package/dist/commands/strategy.js +706 -0
- package/dist/commands/task.js +244 -1
- package/dist/commands/vm-config.d.ts +2 -0
- package/dist/commands/vm-config.js +318 -0
- package/dist/commands/vm.d.ts +2 -0
- package/dist/commands/vm.js +621 -0
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +545 -0
- package/dist/index.js +35 -2
- package/package.json +8 -2
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
export const changelogCommand = new Command("changelog")
|
|
5
|
+
.description("Generate and manage changelogs");
|
|
6
|
+
// Helper: Ensure API is configured
|
|
7
|
+
function ensureConfig() {
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
if (!config.apiUrl) {
|
|
10
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
// Helper: Get commits from git
|
|
16
|
+
function getGitCommits(since, until) {
|
|
17
|
+
try {
|
|
18
|
+
// Build git log command
|
|
19
|
+
let range = "";
|
|
20
|
+
if (since && until) {
|
|
21
|
+
range = `${since}..${until}`;
|
|
22
|
+
}
|
|
23
|
+
else if (since) {
|
|
24
|
+
range = `${since}..HEAD`;
|
|
25
|
+
}
|
|
26
|
+
else if (until) {
|
|
27
|
+
range = until;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
range = "HEAD~50..HEAD"; // Default: last 50 commits
|
|
31
|
+
}
|
|
32
|
+
// Format: hash|shortHash|subject|body|author|date
|
|
33
|
+
const format = "%H|%h|%s|%b|%an|%aI";
|
|
34
|
+
const output = execSync(`git log ${range} --format="${format}" --no-merges`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
|
|
35
|
+
if (!output.trim()) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const commits = [];
|
|
39
|
+
// Split by double newlines between commits, but handle body newlines
|
|
40
|
+
const lines = output.trim().split("\n");
|
|
41
|
+
let currentCommit = [];
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
// Check if this line starts a new commit (40 char hash followed by |)
|
|
44
|
+
if (/^[a-f0-9]{40}\|/.test(line)) {
|
|
45
|
+
if (currentCommit.length > 0) {
|
|
46
|
+
parseCommitLine(currentCommit.join("\n"), commits);
|
|
47
|
+
}
|
|
48
|
+
currentCommit = [line];
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
currentCommit.push(line);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Don't forget the last commit
|
|
55
|
+
if (currentCommit.length > 0) {
|
|
56
|
+
parseCommitLine(currentCommit.join("\n"), commits);
|
|
57
|
+
}
|
|
58
|
+
return commits;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error("Error getting git commits:", error);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function parseCommitLine(line, commits) {
|
|
66
|
+
const parts = line.split("|");
|
|
67
|
+
if (parts.length >= 6) {
|
|
68
|
+
const hash = parts[0];
|
|
69
|
+
const shortHash = parts[1];
|
|
70
|
+
const message = parts[2];
|
|
71
|
+
const body = parts[3];
|
|
72
|
+
const author = parts[4];
|
|
73
|
+
const date = parts[5];
|
|
74
|
+
// Extract PR number from message if present (e.g., "(#123)")
|
|
75
|
+
const prMatch = message.match(/\(#(\d+)\)/);
|
|
76
|
+
const prNumber = prMatch ? prMatch[1] : undefined;
|
|
77
|
+
commits.push({
|
|
78
|
+
hash,
|
|
79
|
+
shortHash,
|
|
80
|
+
message,
|
|
81
|
+
body,
|
|
82
|
+
author,
|
|
83
|
+
date,
|
|
84
|
+
prNumber,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// husky changelog generate [--since <ref>] [--until <ref>] [--version <version>] [--project <id>]
|
|
89
|
+
changelogCommand
|
|
90
|
+
.command("generate")
|
|
91
|
+
.description("Generate a changelog from git commits")
|
|
92
|
+
.option("--since <ref>", "Git ref to start from (tag, commit, or branch)")
|
|
93
|
+
.option("--until <ref>", "Git ref to end at (default: HEAD)")
|
|
94
|
+
.option("-v, --version <version>", "Version for this changelog (required)")
|
|
95
|
+
.option("-p, --project <id>", "Project ID (required)")
|
|
96
|
+
.option("--dry-run", "Show what would be generated without saving")
|
|
97
|
+
.action(async (options) => {
|
|
98
|
+
const config = ensureConfig();
|
|
99
|
+
if (!options.version) {
|
|
100
|
+
console.error("Error: --version is required");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
if (!options.project) {
|
|
104
|
+
console.error("Error: --project is required");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
console.log("Collecting git commits...");
|
|
108
|
+
const commits = getGitCommits(options.since, options.until);
|
|
109
|
+
if (commits.length === 0) {
|
|
110
|
+
console.error("No commits found in the specified range");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
console.log(`Found ${commits.length} commits`);
|
|
114
|
+
if (options.dryRun) {
|
|
115
|
+
console.log("\nCommits to analyze:");
|
|
116
|
+
for (const commit of commits.slice(0, 10)) {
|
|
117
|
+
console.log(` ${commit.shortHash}: ${commit.message}`);
|
|
118
|
+
}
|
|
119
|
+
if (commits.length > 10) {
|
|
120
|
+
console.log(` ... and ${commits.length - 10} more`);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.log("Generating changelog with AI...");
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(`${config.apiUrl}/api/changelogs/generate`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: {
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
projectId: options.project,
|
|
134
|
+
version: options.version,
|
|
135
|
+
since: options.since,
|
|
136
|
+
until: options.until,
|
|
137
|
+
commits,
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const error = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
142
|
+
throw new Error(error.error || `API error: ${res.status}`);
|
|
143
|
+
}
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
const changelog = data.changelog;
|
|
146
|
+
console.log(`\n[OK] Changelog ${changelog.version} generated!`);
|
|
147
|
+
console.log(` ID: ${changelog.id}`);
|
|
148
|
+
console.log(` Entries: ${changelog.entries.length}`);
|
|
149
|
+
console.log(` Status: ${changelog.status}`);
|
|
150
|
+
if (changelog.entries.length > 0) {
|
|
151
|
+
console.log("\n Entries:");
|
|
152
|
+
for (const entry of changelog.entries) {
|
|
153
|
+
const typeIcon = getTypeIcon(entry.type);
|
|
154
|
+
console.log(` ${typeIcon} [${entry.type}] ${entry.title}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!data.aiGenerated) {
|
|
158
|
+
console.log("\n Note: AI not configured, used basic parsing");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.error("Error generating changelog:", error);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// husky changelog list [--project <id>]
|
|
167
|
+
changelogCommand
|
|
168
|
+
.command("list")
|
|
169
|
+
.description("List changelogs")
|
|
170
|
+
.option("-p, --project <id>", "Filter by project ID")
|
|
171
|
+
.option("--json", "Output as JSON")
|
|
172
|
+
.action(async (options) => {
|
|
173
|
+
const config = ensureConfig();
|
|
174
|
+
try {
|
|
175
|
+
const url = new URL("/api/changelogs", config.apiUrl);
|
|
176
|
+
if (options.project) {
|
|
177
|
+
url.searchParams.set("projectId", options.project);
|
|
178
|
+
}
|
|
179
|
+
const res = await fetch(url.toString(), {
|
|
180
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
181
|
+
});
|
|
182
|
+
if (!res.ok) {
|
|
183
|
+
throw new Error(`API error: ${res.status}`);
|
|
184
|
+
}
|
|
185
|
+
const changelogs = await res.json();
|
|
186
|
+
if (options.json) {
|
|
187
|
+
console.log(JSON.stringify(changelogs, null, 2));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (changelogs.length === 0) {
|
|
191
|
+
console.log("No changelogs found");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
console.log("\n Changelogs");
|
|
195
|
+
console.log(" " + "-".repeat(60));
|
|
196
|
+
for (const changelog of changelogs) {
|
|
197
|
+
const statusIcon = changelog.status === "published" ? "[OK]" : "[DRAFT]";
|
|
198
|
+
const date = new Date(changelog.releaseDate).toLocaleDateString();
|
|
199
|
+
console.log(` ${statusIcon} ${changelog.version.padEnd(15)} ${date.padEnd(12)} ${changelog.entries.length} entries`);
|
|
200
|
+
}
|
|
201
|
+
console.log("");
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.error("Error fetching changelogs:", error);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// husky changelog show <id>
|
|
209
|
+
changelogCommand
|
|
210
|
+
.command("show <id>")
|
|
211
|
+
.description("Show changelog details")
|
|
212
|
+
.option("--json", "Output as JSON")
|
|
213
|
+
.option("--markdown", "Output as Markdown")
|
|
214
|
+
.action(async (id, options) => {
|
|
215
|
+
const config = ensureConfig();
|
|
216
|
+
try {
|
|
217
|
+
const res = await fetch(`${config.apiUrl}/api/changelogs/${id}`, {
|
|
218
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
219
|
+
});
|
|
220
|
+
if (!res.ok) {
|
|
221
|
+
if (res.status === 404) {
|
|
222
|
+
console.error("Changelog not found");
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.error(`API error: ${res.status}`);
|
|
226
|
+
}
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const changelog = await res.json();
|
|
230
|
+
if (options.json) {
|
|
231
|
+
console.log(JSON.stringify(changelog, null, 2));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (options.markdown) {
|
|
235
|
+
console.log(formatAsMarkdown(changelog));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// Default: formatted output
|
|
239
|
+
console.log(`\n Changelog: ${changelog.version}`);
|
|
240
|
+
console.log(" " + "-".repeat(50));
|
|
241
|
+
console.log(` ID: ${changelog.id}`);
|
|
242
|
+
console.log(` Status: ${changelog.status}`);
|
|
243
|
+
console.log(` Date: ${new Date(changelog.releaseDate).toLocaleDateString()}`);
|
|
244
|
+
console.log(` Entries: ${changelog.entries.length}`);
|
|
245
|
+
if (changelog.entries.length > 0) {
|
|
246
|
+
// Group entries by type
|
|
247
|
+
const byType = {};
|
|
248
|
+
for (const entry of changelog.entries) {
|
|
249
|
+
if (!byType[entry.type]) {
|
|
250
|
+
byType[entry.type] = [];
|
|
251
|
+
}
|
|
252
|
+
byType[entry.type].push(entry);
|
|
253
|
+
}
|
|
254
|
+
console.log("\n Changes:");
|
|
255
|
+
const typeOrder = ["breaking", "feature", "fix", "docs", "chore"];
|
|
256
|
+
for (const type of typeOrder) {
|
|
257
|
+
const entries = byType[type];
|
|
258
|
+
if (entries && entries.length > 0) {
|
|
259
|
+
const icon = getTypeIcon(type);
|
|
260
|
+
console.log(`\n ${icon} ${getTypeLabel(type)}`);
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
const prStr = entry.prNumber ? ` (#${entry.prNumber})` : "";
|
|
263
|
+
const hashStr = entry.commitHash ? ` [${entry.commitHash}]` : "";
|
|
264
|
+
console.log(` - ${entry.title}${prStr}${hashStr}`);
|
|
265
|
+
if (entry.description) {
|
|
266
|
+
console.log(` ${entry.description}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.log("");
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
console.error("Error fetching changelog:", error);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
// husky changelog publish <id>
|
|
280
|
+
changelogCommand
|
|
281
|
+
.command("publish <id>")
|
|
282
|
+
.description("Publish a changelog (change status to published)")
|
|
283
|
+
.action(async (id) => {
|
|
284
|
+
const config = ensureConfig();
|
|
285
|
+
try {
|
|
286
|
+
const res = await fetch(`${config.apiUrl}/api/changelogs/${id}`, {
|
|
287
|
+
method: "PATCH",
|
|
288
|
+
headers: {
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify({
|
|
293
|
+
status: "published",
|
|
294
|
+
releaseDate: new Date().toISOString(),
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
if (res.status === 404) {
|
|
299
|
+
console.error("Changelog not found");
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.error(`API error: ${res.status}`);
|
|
303
|
+
}
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
const changelog = await res.json();
|
|
307
|
+
console.log(`[OK] Changelog ${changelog.version} published!`);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
console.error("Error publishing changelog:", error);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
// husky changelog delete <id>
|
|
315
|
+
changelogCommand
|
|
316
|
+
.command("delete <id>")
|
|
317
|
+
.description("Delete a changelog")
|
|
318
|
+
.option("-y, --yes", "Skip confirmation")
|
|
319
|
+
.action(async (id, options) => {
|
|
320
|
+
const config = ensureConfig();
|
|
321
|
+
if (!options.yes) {
|
|
322
|
+
console.log("Are you sure you want to delete this changelog? Use --yes to confirm.");
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const res = await fetch(`${config.apiUrl}/api/changelogs/${id}`, {
|
|
327
|
+
method: "DELETE",
|
|
328
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
329
|
+
});
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
if (res.status === 404) {
|
|
332
|
+
console.error("Changelog not found");
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
console.error(`API error: ${res.status}`);
|
|
336
|
+
}
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
console.log("[OK] Changelog deleted");
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
console.error("Error deleting changelog:", error);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
// Helper functions
|
|
347
|
+
function getTypeIcon(type) {
|
|
348
|
+
const icons = {
|
|
349
|
+
feature: "[NEW]",
|
|
350
|
+
fix: "[FIX]",
|
|
351
|
+
breaking: "[!!!]",
|
|
352
|
+
docs: "[DOC]",
|
|
353
|
+
chore: "[CHG]",
|
|
354
|
+
};
|
|
355
|
+
return icons[type] || "[?]";
|
|
356
|
+
}
|
|
357
|
+
function getTypeLabel(type) {
|
|
358
|
+
const labels = {
|
|
359
|
+
feature: "New Features",
|
|
360
|
+
fix: "Bug Fixes",
|
|
361
|
+
breaking: "Breaking Changes",
|
|
362
|
+
docs: "Documentation",
|
|
363
|
+
chore: "Maintenance",
|
|
364
|
+
};
|
|
365
|
+
return labels[type] || type;
|
|
366
|
+
}
|
|
367
|
+
function formatAsMarkdown(changelog) {
|
|
368
|
+
let md = `# ${changelog.version}\n\n`;
|
|
369
|
+
md += `*Released: ${new Date(changelog.releaseDate).toLocaleDateString()}*\n\n`;
|
|
370
|
+
// Group entries by type
|
|
371
|
+
const byType = {};
|
|
372
|
+
for (const entry of changelog.entries) {
|
|
373
|
+
if (!byType[entry.type]) {
|
|
374
|
+
byType[entry.type] = [];
|
|
375
|
+
}
|
|
376
|
+
byType[entry.type].push(entry);
|
|
377
|
+
}
|
|
378
|
+
const typeOrder = ["breaking", "feature", "fix", "docs", "chore"];
|
|
379
|
+
const typeEmoji = {
|
|
380
|
+
breaking: "Breaking Changes",
|
|
381
|
+
feature: "New Features",
|
|
382
|
+
fix: "Bug Fixes",
|
|
383
|
+
docs: "Documentation",
|
|
384
|
+
chore: "Maintenance",
|
|
385
|
+
};
|
|
386
|
+
for (const type of typeOrder) {
|
|
387
|
+
const entries = byType[type];
|
|
388
|
+
if (entries && entries.length > 0) {
|
|
389
|
+
md += `## ${typeEmoji[type] || type}\n\n`;
|
|
390
|
+
for (const entry of entries) {
|
|
391
|
+
const prStr = entry.prNumber ? ` (#${entry.prNumber})` : "";
|
|
392
|
+
md += `- ${entry.title}${prStr}\n`;
|
|
393
|
+
if (entry.description) {
|
|
394
|
+
md += ` ${entry.description}\n`;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
md += "\n";
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return md;
|
|
401
|
+
}
|