@simonfestl/husky-cli 0.5.1 → 0.6.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.
Files changed (37) hide show
  1. package/dist/commands/config.js +4 -3
  2. package/dist/commands/idea.js +9 -7
  3. package/dist/commands/interactive/changelog.d.ts +1 -0
  4. package/dist/commands/interactive/changelog.js +398 -0
  5. package/dist/commands/interactive/departments.d.ts +1 -0
  6. package/dist/commands/interactive/departments.js +242 -0
  7. package/dist/commands/interactive/ideas.d.ts +1 -0
  8. package/dist/commands/interactive/ideas.js +311 -0
  9. package/dist/commands/interactive/jules-sessions.d.ts +1 -0
  10. package/dist/commands/interactive/jules-sessions.js +460 -0
  11. package/dist/commands/interactive/processes.d.ts +1 -0
  12. package/dist/commands/interactive/processes.js +271 -0
  13. package/dist/commands/interactive/projects.d.ts +1 -0
  14. package/dist/commands/interactive/projects.js +297 -0
  15. package/dist/commands/interactive/roadmaps.d.ts +1 -0
  16. package/dist/commands/interactive/roadmaps.js +650 -0
  17. package/dist/commands/interactive/strategy.d.ts +1 -0
  18. package/dist/commands/interactive/strategy.js +790 -0
  19. package/dist/commands/interactive/tasks.d.ts +1 -0
  20. package/dist/commands/interactive/tasks.js +415 -0
  21. package/dist/commands/interactive/utils.d.ts +15 -0
  22. package/dist/commands/interactive/utils.js +54 -0
  23. package/dist/commands/interactive/vm-sessions.d.ts +1 -0
  24. package/dist/commands/interactive/vm-sessions.js +319 -0
  25. package/dist/commands/interactive/workflows.d.ts +1 -0
  26. package/dist/commands/interactive/workflows.js +442 -0
  27. package/dist/commands/interactive/worktrees.d.ts +6 -0
  28. package/dist/commands/interactive/worktrees.js +354 -0
  29. package/dist/commands/interactive.js +118 -1208
  30. package/dist/commands/worktree.d.ts +2 -0
  31. package/dist/commands/worktree.js +404 -0
  32. package/dist/index.js +3 -1
  33. package/dist/lib/merge-lock.d.ts +83 -0
  34. package/dist/lib/merge-lock.js +242 -0
  35. package/dist/lib/worktree.d.ts +133 -0
  36. package/dist/lib/worktree.js +473 -0
  37. package/package.json +1 -1
@@ -4,13 +4,14 @@ import { join } from "path";
4
4
  import { homedir } from "os";
5
5
  const CONFIG_DIR = join(homedir(), ".husky");
6
6
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
- // API Key validation - must be at least 16 characters and alphanumeric with dashes/underscores
7
+ // API Key validation - must be at least 16 characters, alphanumeric + common key chars (base64, JWT, etc.)
8
8
  function validateApiKey(key) {
9
9
  if (key.length < 16) {
10
10
  return { valid: false, error: "API key must be at least 16 characters long" };
11
11
  }
12
- if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
13
- return { valid: false, error: "API key must only contain letters, numbers, dashes, and underscores" };
12
+ // Allow: letters, numbers, dashes, underscores, dots, plus, slash, equals (base64/JWT compatible)
13
+ if (!/^[a-zA-Z0-9_\-\.+/=]+$/.test(key)) {
14
+ return { valid: false, error: "API key contains invalid characters" };
14
15
  }
15
16
  return { valid: true };
16
17
  }
@@ -293,10 +293,12 @@ function printIdeas(ideas) {
293
293
  console.log(` ${"ID".padEnd(24)} ${"TITLE".padEnd(30)} ${"STATUS".padEnd(12)} CATEGORY`);
294
294
  console.log(" " + "─".repeat(70));
295
295
  for (const idea of ideas) {
296
- const statusIcon = getStatusIcon(idea.status);
297
- const truncatedTitle = idea.title.length > 28 ? idea.title.substring(0, 25) + "..." : idea.title;
296
+ const statusIcon = getStatusIcon(idea.status || "draft");
297
+ const title = idea.title || "(untitled)";
298
+ const truncatedTitle = title.length > 28 ? title.substring(0, 25) + "..." : title;
298
299
  const category = idea.category || "-";
299
- console.log(` ${idea.id.padEnd(24)} ${truncatedTitle.padEnd(30)} ${statusIcon} ${idea.status.padEnd(10)} ${category}`);
300
+ const status = idea.status || "draft";
301
+ console.log(` ${idea.id.padEnd(24)} ${truncatedTitle.padEnd(30)} ${statusIcon} ${status.padEnd(10)} ${category}`);
300
302
  }
301
303
  // Summary by status
302
304
  const draftCount = ideas.filter((i) => i.status === "draft").length;
@@ -309,10 +311,10 @@ function printIdeas(ideas) {
309
311
  console.log("");
310
312
  }
311
313
  function printIdeaDetail(idea) {
312
- console.log(`\n Idea: ${idea.title}`);
314
+ console.log(`\n Idea: ${idea.title || "(untitled)"}`);
313
315
  console.log(" " + "─".repeat(50));
314
316
  console.log(` ID: ${idea.id}`);
315
- console.log(` Status: ${idea.status}`);
317
+ console.log(` Status: ${idea.status || "draft"}`);
316
318
  if (idea.category) {
317
319
  console.log(` Category: ${idea.category}`);
318
320
  }
@@ -320,8 +322,8 @@ function printIdeaDetail(idea) {
320
322
  console.log(` Description:`);
321
323
  console.log(` ${idea.description}`);
322
324
  }
323
- console.log(` Created: ${new Date(idea.createdAt).toLocaleString()}`);
324
- console.log(` Updated: ${new Date(idea.updatedAt).toLocaleString()}`);
325
+ console.log(` Created: ${idea.createdAt ? new Date(idea.createdAt).toLocaleString() : "-"}`);
326
+ console.log(` Updated: ${idea.updatedAt ? new Date(idea.updatedAt).toLocaleString() : "-"}`);
325
327
  console.log("");
326
328
  }
327
329
  function getStatusIcon(status) {
@@ -0,0 +1 @@
1
+ export declare function changelogMenu(): Promise<void>;
@@ -0,0 +1,398 @@
1
+ import { select, input, confirm } from "@inquirer/prompts";
2
+ import { execSync } from "child_process";
3
+ import { ensureConfig, pressEnterToContinue, truncate, formatDate } from "./utils.js";
4
+ const TYPE_LABELS = {
5
+ feature: "New Features",
6
+ fix: "Bug Fixes",
7
+ breaking: "Breaking Changes",
8
+ docs: "Documentation",
9
+ chore: "Maintenance",
10
+ };
11
+ const TYPE_ICONS = {
12
+ feature: "[NEW]",
13
+ fix: "[FIX]",
14
+ breaking: "[!!!]",
15
+ docs: "[DOC]",
16
+ chore: "[CHG]",
17
+ };
18
+ export async function changelogMenu() {
19
+ const config = ensureConfig();
20
+ const menuItems = [
21
+ { name: "List changelogs", value: "list" },
22
+ { name: "View changelog", value: "view" },
23
+ { name: "Generate changelog", value: "generate" },
24
+ { name: "Publish changelog", value: "publish" },
25
+ { name: "Delete changelog", value: "delete" },
26
+ { name: "Back to main menu", value: "back" },
27
+ ];
28
+ const choice = await select({
29
+ message: "Changelog:",
30
+ choices: menuItems,
31
+ });
32
+ switch (choice) {
33
+ case "list":
34
+ await listChangelogs(config);
35
+ break;
36
+ case "view":
37
+ await viewChangelog(config);
38
+ break;
39
+ case "generate":
40
+ await generateChangelog(config);
41
+ break;
42
+ case "publish":
43
+ await publishChangelog(config);
44
+ break;
45
+ case "delete":
46
+ await deleteChangelog(config);
47
+ break;
48
+ case "back":
49
+ return;
50
+ }
51
+ }
52
+ async function fetchChangelogs(config, projectId) {
53
+ const url = new URL("/api/changelogs", config.apiUrl);
54
+ if (projectId) {
55
+ url.searchParams.set("projectId", projectId);
56
+ }
57
+ const res = await fetch(url.toString(), {
58
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
59
+ });
60
+ if (!res.ok)
61
+ throw new Error(`API returned ${res.status}`);
62
+ return res.json();
63
+ }
64
+ async function fetchProjects(config) {
65
+ const res = await fetch(`${config.apiUrl}/api/projects`, {
66
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
67
+ });
68
+ if (!res.ok)
69
+ throw new Error(`API returned ${res.status}`);
70
+ return res.json();
71
+ }
72
+ async function selectChangelog(config, message) {
73
+ const changelogs = await fetchChangelogs(config);
74
+ if (changelogs.length === 0) {
75
+ console.log("\n No changelogs found.\n");
76
+ await pressEnterToContinue();
77
+ return null;
78
+ }
79
+ const choices = changelogs.map((c) => ({
80
+ name: `[${c.status}] ${c.version} (${c.entries.length} entries) - ${formatDate(c.releaseDate)}`,
81
+ value: c.id,
82
+ }));
83
+ choices.push({ name: "Cancel", value: "__cancel__" });
84
+ const changelogId = await select({ message, choices });
85
+ if (changelogId === "__cancel__")
86
+ return null;
87
+ // Fetch full changelog
88
+ const res = await fetch(`${config.apiUrl}/api/changelogs/${changelogId}`, {
89
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
90
+ });
91
+ if (!res.ok)
92
+ return null;
93
+ return res.json();
94
+ }
95
+ async function listChangelogs(config) {
96
+ try {
97
+ const changelogs = await fetchChangelogs(config);
98
+ console.log("\n CHANGELOGS");
99
+ console.log(" " + "-".repeat(70));
100
+ if (changelogs.length === 0) {
101
+ console.log(" No changelogs found.");
102
+ }
103
+ else {
104
+ for (const cl of changelogs) {
105
+ const statusIcon = cl.status === "published" ? "[OK]" : "[DRAFT]";
106
+ console.log(` ${statusIcon} ${cl.version.padEnd(15)} ${formatDate(cl.releaseDate).padEnd(12)} ${cl.entries.length} entries`);
107
+ console.log(` ID: ${cl.id}`);
108
+ }
109
+ }
110
+ console.log("");
111
+ await pressEnterToContinue();
112
+ }
113
+ catch (error) {
114
+ console.error("\n Error fetching changelogs:", error);
115
+ await pressEnterToContinue();
116
+ }
117
+ }
118
+ async function viewChangelog(config) {
119
+ try {
120
+ const changelog = await selectChangelog(config, "Select changelog to view:");
121
+ if (!changelog)
122
+ return;
123
+ console.log(`\n Changelog: ${changelog.version}`);
124
+ console.log(" " + "-".repeat(60));
125
+ console.log(` ID: ${changelog.id}`);
126
+ console.log(` Status: ${changelog.status}`);
127
+ console.log(` Date: ${formatDate(changelog.releaseDate)}`);
128
+ console.log(` Entries: ${changelog.entries.length}`);
129
+ if (changelog.entries.length > 0) {
130
+ // Group by type
131
+ const byType = {};
132
+ for (const entry of changelog.entries) {
133
+ if (!byType[entry.type]) {
134
+ byType[entry.type] = [];
135
+ }
136
+ byType[entry.type].push(entry);
137
+ }
138
+ console.log("\n Changes:");
139
+ const typeOrder = ["breaking", "feature", "fix", "docs", "chore"];
140
+ for (const type of typeOrder) {
141
+ const entries = byType[type];
142
+ if (entries && entries.length > 0) {
143
+ const icon = TYPE_ICONS[type] || "[?]";
144
+ const label = TYPE_LABELS[type] || type;
145
+ console.log(`\n ${icon} ${label}`);
146
+ for (const entry of entries) {
147
+ const prStr = entry.prNumber ? ` (#${entry.prNumber})` : "";
148
+ console.log(` - ${truncate(entry.title, 55)}${prStr}`);
149
+ }
150
+ }
151
+ }
152
+ }
153
+ console.log("");
154
+ await pressEnterToContinue();
155
+ }
156
+ catch (error) {
157
+ console.error("\n Error viewing changelog:", error);
158
+ await pressEnterToContinue();
159
+ }
160
+ }
161
+ async function generateChangelog(config) {
162
+ try {
163
+ // Select project
164
+ const projects = await fetchProjects(config);
165
+ if (projects.length === 0) {
166
+ console.log("\n No projects found. Create a project first.\n");
167
+ await pressEnterToContinue();
168
+ return;
169
+ }
170
+ const projectChoices = projects.map((p) => ({
171
+ name: p.name,
172
+ value: p.id,
173
+ }));
174
+ projectChoices.push({ name: "Cancel", value: "__cancel__" });
175
+ const projectId = await select({ message: "Select project:", choices: projectChoices });
176
+ if (projectId === "__cancel__")
177
+ return;
178
+ const version = await input({
179
+ message: "Version (e.g., 1.0.0):",
180
+ validate: (v) => (v.length > 0 ? true : "Version required"),
181
+ });
182
+ const since = await input({
183
+ message: "Git ref to start from (tag, commit, or empty for last 50 commits):",
184
+ });
185
+ const until = await input({
186
+ message: "Git ref to end at (default: HEAD):",
187
+ default: "HEAD",
188
+ });
189
+ console.log("\n Collecting git commits...\n");
190
+ const commits = getGitCommits(since || undefined, until);
191
+ if (commits.length === 0) {
192
+ console.log(" No commits found in the specified range.\n");
193
+ await pressEnterToContinue();
194
+ return;
195
+ }
196
+ console.log(` Found ${commits.length} commits.`);
197
+ const proceed = await confirm({
198
+ message: "Generate changelog with AI?",
199
+ default: true,
200
+ });
201
+ if (!proceed) {
202
+ console.log("\n Cancelled.\n");
203
+ await pressEnterToContinue();
204
+ return;
205
+ }
206
+ console.log("\n Generating changelog with AI... This may take a moment.\n");
207
+ const res = await fetch(`${config.apiUrl}/api/changelogs/generate`, {
208
+ method: "POST",
209
+ headers: {
210
+ "Content-Type": "application/json",
211
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
212
+ },
213
+ body: JSON.stringify({
214
+ projectId,
215
+ version,
216
+ since: since || undefined,
217
+ until,
218
+ commits,
219
+ }),
220
+ });
221
+ if (!res.ok) {
222
+ const error = await res.json().catch(() => ({}));
223
+ console.error(`\n Error: ${error.error || `API returned ${res.status}`}\n`);
224
+ await pressEnterToContinue();
225
+ return;
226
+ }
227
+ const data = await res.json();
228
+ const changelog = data.changelog;
229
+ console.log(` ✓ Changelog ${changelog.version} generated!`);
230
+ console.log(` ID: ${changelog.id}`);
231
+ console.log(` Entries: ${changelog.entries.length}`);
232
+ console.log(` Status: ${changelog.status}`);
233
+ if (changelog.entries.length > 0) {
234
+ console.log("\n Entries:");
235
+ for (const entry of changelog.entries.slice(0, 5)) {
236
+ const icon = TYPE_ICONS[entry.type] || "[?]";
237
+ console.log(` ${icon} ${truncate(entry.title, 50)}`);
238
+ }
239
+ if (changelog.entries.length > 5) {
240
+ console.log(` ... and ${changelog.entries.length - 5} more`);
241
+ }
242
+ }
243
+ console.log("");
244
+ await pressEnterToContinue();
245
+ }
246
+ catch (error) {
247
+ console.error("\n Error generating changelog:", error);
248
+ await pressEnterToContinue();
249
+ }
250
+ }
251
+ async function publishChangelog(config) {
252
+ try {
253
+ const changelogs = await fetchChangelogs(config);
254
+ const draftChangelogs = changelogs.filter((c) => c.status === "draft");
255
+ if (draftChangelogs.length === 0) {
256
+ console.log("\n No draft changelogs to publish.\n");
257
+ await pressEnterToContinue();
258
+ return;
259
+ }
260
+ const choices = draftChangelogs.map((c) => ({
261
+ name: `${c.version} (${c.entries.length} entries)`,
262
+ value: c.id,
263
+ }));
264
+ choices.push({ name: "Cancel", value: "__cancel__" });
265
+ const changelogId = await select({ message: "Select changelog to publish:", choices });
266
+ if (changelogId === "__cancel__")
267
+ return;
268
+ const changelog = draftChangelogs.find((c) => c.id === changelogId);
269
+ const confirmed = await confirm({
270
+ message: `Publish changelog ${changelog?.version}?`,
271
+ default: true,
272
+ });
273
+ if (!confirmed) {
274
+ console.log("\n Cancelled.\n");
275
+ await pressEnterToContinue();
276
+ return;
277
+ }
278
+ const res = await fetch(`${config.apiUrl}/api/changelogs/${changelogId}`, {
279
+ method: "PATCH",
280
+ headers: {
281
+ "Content-Type": "application/json",
282
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
283
+ },
284
+ body: JSON.stringify({
285
+ status: "published",
286
+ releaseDate: new Date().toISOString(),
287
+ }),
288
+ });
289
+ if (!res.ok) {
290
+ console.error(`\n Error: API returned ${res.status}\n`);
291
+ await pressEnterToContinue();
292
+ return;
293
+ }
294
+ console.log(`\n ✓ Changelog ${changelog?.version} published!\n`);
295
+ await pressEnterToContinue();
296
+ }
297
+ catch (error) {
298
+ console.error("\n Error publishing changelog:", error);
299
+ await pressEnterToContinue();
300
+ }
301
+ }
302
+ async function deleteChangelog(config) {
303
+ try {
304
+ const changelog = await selectChangelog(config, "Select changelog to delete:");
305
+ if (!changelog)
306
+ return;
307
+ const confirmed = await confirm({
308
+ message: `Delete changelog ${changelog.version}? This cannot be undone.`,
309
+ default: false,
310
+ });
311
+ if (!confirmed) {
312
+ console.log("\n Cancelled.\n");
313
+ await pressEnterToContinue();
314
+ return;
315
+ }
316
+ const res = await fetch(`${config.apiUrl}/api/changelogs/${changelog.id}`, {
317
+ method: "DELETE",
318
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
319
+ });
320
+ if (!res.ok) {
321
+ console.error(`\n Error: API returned ${res.status}\n`);
322
+ await pressEnterToContinue();
323
+ return;
324
+ }
325
+ console.log(`\n ✓ Changelog deleted.\n`);
326
+ await pressEnterToContinue();
327
+ }
328
+ catch (error) {
329
+ console.error("\n Error deleting changelog:", error);
330
+ await pressEnterToContinue();
331
+ }
332
+ }
333
+ // Git helpers
334
+ function getGitCommits(since, until) {
335
+ try {
336
+ let range = "";
337
+ if (since && until) {
338
+ range = `${since}..${until}`;
339
+ }
340
+ else if (since) {
341
+ range = `${since}..HEAD`;
342
+ }
343
+ else if (until) {
344
+ range = until;
345
+ }
346
+ else {
347
+ range = "HEAD~50..HEAD";
348
+ }
349
+ const format = "%H|%h|%s|%b|%an|%aI";
350
+ const output = execSync(`git log ${range} --format="${format}" --no-merges`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
351
+ if (!output.trim()) {
352
+ return [];
353
+ }
354
+ const commits = [];
355
+ const lines = output.trim().split("\n");
356
+ let currentCommit = [];
357
+ for (const line of lines) {
358
+ if (/^[a-f0-9]{40}\|/.test(line)) {
359
+ if (currentCommit.length > 0) {
360
+ parseCommitLine(currentCommit.join("\n"), commits);
361
+ }
362
+ currentCommit = [line];
363
+ }
364
+ else {
365
+ currentCommit.push(line);
366
+ }
367
+ }
368
+ if (currentCommit.length > 0) {
369
+ parseCommitLine(currentCommit.join("\n"), commits);
370
+ }
371
+ return commits;
372
+ }
373
+ catch {
374
+ return [];
375
+ }
376
+ }
377
+ function parseCommitLine(line, commits) {
378
+ const parts = line.split("|");
379
+ if (parts.length >= 6) {
380
+ const hash = parts[0];
381
+ const shortHash = parts[1];
382
+ const message = parts[2];
383
+ const body = parts[3];
384
+ const author = parts[4];
385
+ const date = parts[5];
386
+ const prMatch = message.match(/\(#(\d+)\)/);
387
+ const prNumber = prMatch ? prMatch[1] : undefined;
388
+ commits.push({
389
+ hash,
390
+ shortHash,
391
+ message,
392
+ body,
393
+ author,
394
+ date,
395
+ prNumber,
396
+ });
397
+ }
398
+ }
@@ -0,0 +1 @@
1
+ export declare function departmentsMenu(): Promise<void>;