@simonfestl/husky-cli 0.5.0 → 0.5.2

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 (31) hide show
  1. package/README.md +21 -25
  2. package/dist/commands/config.d.ts +1 -0
  3. package/dist/commands/config.js +10 -3
  4. package/dist/commands/idea.js +9 -7
  5. package/dist/commands/interactive/changelog.d.ts +1 -0
  6. package/dist/commands/interactive/changelog.js +398 -0
  7. package/dist/commands/interactive/departments.d.ts +1 -0
  8. package/dist/commands/interactive/departments.js +242 -0
  9. package/dist/commands/interactive/ideas.d.ts +1 -0
  10. package/dist/commands/interactive/ideas.js +311 -0
  11. package/dist/commands/interactive/jules-sessions.d.ts +1 -0
  12. package/dist/commands/interactive/jules-sessions.js +460 -0
  13. package/dist/commands/interactive/processes.d.ts +1 -0
  14. package/dist/commands/interactive/processes.js +271 -0
  15. package/dist/commands/interactive/projects.d.ts +1 -0
  16. package/dist/commands/interactive/projects.js +297 -0
  17. package/dist/commands/interactive/roadmaps.d.ts +1 -0
  18. package/dist/commands/interactive/roadmaps.js +650 -0
  19. package/dist/commands/interactive/strategy.d.ts +1 -0
  20. package/dist/commands/interactive/strategy.js +790 -0
  21. package/dist/commands/interactive/tasks.d.ts +1 -0
  22. package/dist/commands/interactive/tasks.js +415 -0
  23. package/dist/commands/interactive/utils.d.ts +15 -0
  24. package/dist/commands/interactive/utils.js +54 -0
  25. package/dist/commands/interactive/vm-sessions.d.ts +1 -0
  26. package/dist/commands/interactive/vm-sessions.js +319 -0
  27. package/dist/commands/interactive/workflows.d.ts +1 -0
  28. package/dist/commands/interactive/workflows.js +442 -0
  29. package/dist/commands/interactive.js +150 -1135
  30. package/dist/index.js +1 -1
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -251,39 +251,35 @@ npm link
251
251
 
252
252
  ## Publishing / Release
253
253
 
254
- The CLI is automatically published to npm via GitHub Actions using OIDC Trusted Publishing.
255
-
256
254
  ### Publishing a New Version
257
255
 
258
- 1. **Update version in `package.json`:**
259
- ```bash
260
- cd packages/cli
261
- npm version patch # or minor/major
262
- ```
256
+ ```bash
257
+ cd packages/cli
258
+
259
+ # 1. Bump version
260
+ npm version patch # or minor/major
263
261
 
264
- 2. **Commit and push:**
265
- ```bash
266
- git add .
267
- git commit -m "chore(cli): bump version to x.x.x"
268
- git push origin main
269
- ```
262
+ # 2. Publish to npm (opens browser for authentication)
263
+ npm publish --access public
270
264
 
271
- 3. **Automatic publishing:**
272
- - Changes in `packages/cli/**` trigger sync to `simon-sfxecom/husky-cli` (public repo)
273
- - Public repo publishes to npm with OIDC provenance
274
- - No `NPM_TOKEN` secret required (OIDC Trusted Publishing)
265
+ # 3. Commit and push
266
+ git add .
267
+ git commit -m "chore(cli): release vX.X.X"
268
+ git push origin main
269
+ ```
275
270
 
276
- ### Manual Trigger
271
+ ### Updating on Other Devices
277
272
 
278
- If needed, the workflow can be triggered manually:
279
- - GitHub > Actions > "Sync CLI to Public Repo" > "Run workflow"
273
+ ```bash
274
+ # Update to latest version
275
+ npm update -g @simonfestl/husky-cli
280
276
 
281
- ### npm Trusted Publishing Setup
277
+ # Or reinstall
278
+ npm install -g @simonfestl/husky-cli
282
279
 
283
- The package uses OIDC-based publishing. Configuration on npmjs.com:
284
- - Package Settings > Publishing access > Configure trusted publishing
285
- - Repository: `simon-sfxecom/husky-cli`
286
- - Workflow: `publish.yml`
280
+ # Check installed version
281
+ husky --version
282
+ ```
287
283
 
288
284
  ## Changelog
289
285
 
@@ -4,5 +4,6 @@ interface Config {
4
4
  apiKey?: string;
5
5
  }
6
6
  export declare function getConfig(): Config;
7
+ export declare function setConfig(key: "apiUrl" | "apiKey", value: string): void;
7
8
  export declare const configCommand: Command;
8
9
  export {};
@@ -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
  }
@@ -45,6 +46,12 @@ function saveConfig(config) {
45
46
  }
46
47
  writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
47
48
  }
49
+ // Helper to set a single config value (used by interactive mode)
50
+ export function setConfig(key, value) {
51
+ const config = getConfig();
52
+ config[key] = value;
53
+ saveConfig(config);
54
+ }
48
55
  export const configCommand = new Command("config")
49
56
  .description("Manage CLI configuration");
50
57
  // husky config set <key> <value>
@@ -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>;