@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.
@@ -235,6 +235,74 @@ roadmapCommand
235
235
  process.exit(1);
236
236
  }
237
237
  });
238
+ // husky roadmap update <id>
239
+ roadmapCommand
240
+ .command("update <id>")
241
+ .description("Update a roadmap")
242
+ .option("-n, --name <name>", "New name")
243
+ .option("-d, --description <desc>", "New description")
244
+ .option("--type <type>", "New type (project, global)")
245
+ .option("--status <status>", "New status")
246
+ .option("--json", "Output as JSON")
247
+ .action(async (id, options) => {
248
+ const config = ensureConfig();
249
+ // Build update payload
250
+ const updateData = {};
251
+ if (options.name)
252
+ updateData.name = options.name;
253
+ if (options.description)
254
+ updateData.vision = options.description;
255
+ if (options.type) {
256
+ const validTypes = ["project", "global"];
257
+ if (!validTypes.includes(options.type)) {
258
+ console.error(`Error: Invalid type "${options.type}". Must be one of: ${validTypes.join(", ")}`);
259
+ process.exit(1);
260
+ }
261
+ updateData.type = options.type;
262
+ }
263
+ if (options.status)
264
+ updateData.status = options.status;
265
+ if (Object.keys(updateData).length === 0) {
266
+ console.error("Error: No update options provided. Use -n/--name, -d/--description, --type, or --status");
267
+ process.exit(1);
268
+ }
269
+ try {
270
+ const res = await fetch(`${config.apiUrl}/api/roadmaps/${id}`, {
271
+ method: "PATCH",
272
+ headers: {
273
+ "Content-Type": "application/json",
274
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
275
+ },
276
+ body: JSON.stringify(updateData),
277
+ });
278
+ if (!res.ok) {
279
+ if (res.status === 404) {
280
+ console.error(`Error: Roadmap ${id} not found`);
281
+ }
282
+ else {
283
+ const errorBody = await res.json().catch(() => ({}));
284
+ console.error(`Error: API returned ${res.status}`, errorBody.error || "");
285
+ }
286
+ process.exit(1);
287
+ }
288
+ const roadmap = await res.json();
289
+ if (options.json) {
290
+ console.log(JSON.stringify(roadmap, null, 2));
291
+ }
292
+ else {
293
+ console.log(`✓ Roadmap updated successfully`);
294
+ console.log(` Name: ${roadmap.name}`);
295
+ console.log(` Type: ${roadmap.type}`);
296
+ if (roadmap.vision) {
297
+ console.log(` Vision: ${roadmap.vision}`);
298
+ }
299
+ }
300
+ }
301
+ catch (error) {
302
+ console.error("Error updating roadmap:", error);
303
+ process.exit(1);
304
+ }
305
+ });
238
306
  // husky roadmap delete <id>
239
307
  roadmapCommand
240
308
  .command("delete <id>")
@@ -262,6 +330,221 @@ roadmapCommand
262
330
  process.exit(1);
263
331
  }
264
332
  });
333
+ // husky roadmap list-features <roadmapId>
334
+ roadmapCommand
335
+ .command("list-features <roadmapId>")
336
+ .description("List all features in a roadmap with their IDs and status")
337
+ .option("--json", "Output as JSON")
338
+ .option("--status <status>", "Filter by status (idea, planned, in_progress, done)")
339
+ .option("--priority <priority>", "Filter by priority (must, should, could, wont)")
340
+ .action(async (roadmapId, options) => {
341
+ const config = ensureConfig();
342
+ try {
343
+ const res = await fetch(`${config.apiUrl}/api/roadmaps/${roadmapId}`, {
344
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
345
+ });
346
+ if (!res.ok) {
347
+ if (res.status === 404) {
348
+ console.error(`Error: Roadmap ${roadmapId} not found`);
349
+ }
350
+ else {
351
+ console.error(`Error: API returned ${res.status}`);
352
+ }
353
+ process.exit(1);
354
+ }
355
+ const roadmap = await res.json();
356
+ let features = roadmap.features || [];
357
+ // Apply filters
358
+ if (options.status) {
359
+ features = features.filter((f) => f.status === options.status);
360
+ }
361
+ if (options.priority) {
362
+ features = features.filter((f) => f.priority === options.priority);
363
+ }
364
+ if (options.json) {
365
+ console.log(JSON.stringify(features, null, 2));
366
+ }
367
+ else {
368
+ printFeaturesList(roadmap.name, features, roadmap.phases || []);
369
+ }
370
+ }
371
+ catch (error) {
372
+ console.error("Error fetching roadmap features:", error);
373
+ process.exit(1);
374
+ }
375
+ });
376
+ // husky roadmap update-feature <roadmapId> <featureId>
377
+ roadmapCommand
378
+ .command("update-feature <roadmapId> <featureId>")
379
+ .description("Update a roadmap feature")
380
+ .option("--status <status>", "New status (idea, planned, in_progress, done)")
381
+ .option("--name <name>", "New feature name/title")
382
+ .option("--priority <priority>", "New priority (must, should, could, wont)")
383
+ .option("--description <description>", "New description")
384
+ .option("--phase <phaseId>", "Move to different phase")
385
+ .action(async (roadmapId, featureId, options) => {
386
+ const config = ensureConfig();
387
+ // Build update payload
388
+ const updateData = {};
389
+ if (options.status) {
390
+ const validStatuses = ["idea", "planned", "in_progress", "done"];
391
+ if (!validStatuses.includes(options.status)) {
392
+ console.error(`Error: Invalid status "${options.status}". Must be one of: ${validStatuses.join(", ")}`);
393
+ process.exit(1);
394
+ }
395
+ updateData.status = options.status;
396
+ }
397
+ if (options.name) {
398
+ updateData.title = options.name;
399
+ }
400
+ if (options.priority) {
401
+ const validPriorities = ["must", "should", "could", "wont"];
402
+ if (!validPriorities.includes(options.priority)) {
403
+ console.error(`Error: Invalid priority "${options.priority}". Must be one of: ${validPriorities.join(", ")}`);
404
+ process.exit(1);
405
+ }
406
+ updateData.priority = options.priority;
407
+ }
408
+ if (options.description) {
409
+ updateData.description = options.description;
410
+ }
411
+ if (options.phase) {
412
+ updateData.phaseId = options.phase;
413
+ }
414
+ if (Object.keys(updateData).length === 0) {
415
+ console.error("Error: No update options provided. Use --status, --name, --priority, --description, or --phase");
416
+ process.exit(1);
417
+ }
418
+ try {
419
+ const res = await fetch(`${config.apiUrl}/api/roadmaps/${roadmapId}/features/${featureId}`, {
420
+ method: "PATCH",
421
+ headers: {
422
+ "Content-Type": "application/json",
423
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
424
+ },
425
+ body: JSON.stringify(updateData),
426
+ });
427
+ if (!res.ok) {
428
+ if (res.status === 404) {
429
+ console.error(`Error: Roadmap or feature not found`);
430
+ }
431
+ else {
432
+ const errorBody = await res.json().catch(() => ({}));
433
+ console.error(`Error: API returned ${res.status}`, errorBody.error || "");
434
+ }
435
+ process.exit(1);
436
+ }
437
+ const roadmap = await res.json();
438
+ const updatedFeature = roadmap.features.find((f) => f.id === featureId);
439
+ console.log(`✓ Feature updated successfully`);
440
+ if (updatedFeature) {
441
+ console.log(` Title: ${updatedFeature.title}`);
442
+ console.log(` Status: ${updatedFeature.status}`);
443
+ console.log(` Priority: ${updatedFeature.priority}`);
444
+ }
445
+ }
446
+ catch (error) {
447
+ console.error("Error updating feature:", error);
448
+ process.exit(1);
449
+ }
450
+ });
451
+ // husky roadmap delete-feature <roadmapId> <featureId>
452
+ roadmapCommand
453
+ .command("delete-feature <roadmapId> <featureId>")
454
+ .description("Delete a feature from a roadmap")
455
+ .option("--force", "Skip confirmation")
456
+ .action(async (roadmapId, featureId, options) => {
457
+ const config = ensureConfig();
458
+ if (!options.force) {
459
+ console.log("Warning: This will permanently delete the feature.");
460
+ console.log("Use --force to confirm deletion.");
461
+ process.exit(1);
462
+ }
463
+ try {
464
+ const res = await fetch(`${config.apiUrl}/api/roadmaps/${roadmapId}/features/${featureId}`, {
465
+ method: "DELETE",
466
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
467
+ });
468
+ if (!res.ok) {
469
+ if (res.status === 404) {
470
+ console.error(`Error: Roadmap or feature not found`);
471
+ }
472
+ else {
473
+ const errorBody = await res.json().catch(() => ({}));
474
+ console.error(`Error: API returned ${res.status}`, errorBody.error || "");
475
+ }
476
+ process.exit(1);
477
+ }
478
+ console.log(`✓ Feature deleted`);
479
+ }
480
+ catch (error) {
481
+ console.error("Error deleting feature:", error);
482
+ process.exit(1);
483
+ }
484
+ });
485
+ // husky roadmap convert-feature <roadmapId> <featureId>
486
+ roadmapCommand
487
+ .command("convert-feature <roadmapId> <featureId>")
488
+ .description("Convert a roadmap feature to a task")
489
+ .option("--priority <priority>", "Task priority (low, medium, high)")
490
+ .option("--assignee <assignee>", "Task assignee (human, llm, unassigned)")
491
+ .option("--json", "Output as JSON")
492
+ .action(async (roadmapId, featureId, options) => {
493
+ const config = ensureConfig();
494
+ try {
495
+ // Build optional body
496
+ const body = {};
497
+ if (options.priority) {
498
+ const validPriorities = ["low", "medium", "high"];
499
+ if (!validPriorities.includes(options.priority)) {
500
+ console.error(`Error: Invalid priority "${options.priority}". Must be one of: ${validPriorities.join(", ")}`);
501
+ process.exit(1);
502
+ }
503
+ body.priority = options.priority;
504
+ }
505
+ if (options.assignee) {
506
+ const validAssignees = ["human", "llm", "unassigned"];
507
+ if (!validAssignees.includes(options.assignee)) {
508
+ console.error(`Error: Invalid assignee "${options.assignee}". Must be one of: ${validAssignees.join(", ")}`);
509
+ process.exit(1);
510
+ }
511
+ body.assignee = options.assignee;
512
+ }
513
+ const res = await fetch(`${config.apiUrl}/api/roadmaps/${roadmapId}/features/${featureId}/convert`, {
514
+ method: "POST",
515
+ headers: {
516
+ "Content-Type": "application/json",
517
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
518
+ },
519
+ body: JSON.stringify(body),
520
+ });
521
+ if (!res.ok) {
522
+ if (res.status === 404) {
523
+ console.error(`Error: Roadmap or feature not found`);
524
+ }
525
+ else {
526
+ const errorBody = await res.json().catch(() => ({}));
527
+ console.error(`Error: API returned ${res.status}`, errorBody.error || "");
528
+ }
529
+ process.exit(1);
530
+ }
531
+ const result = await res.json();
532
+ if (options.json) {
533
+ console.log(JSON.stringify(result, null, 2));
534
+ }
535
+ else {
536
+ console.log(`✓ Feature converted to task successfully`);
537
+ console.log(` Task ID: ${result.task.id}`);
538
+ console.log(` Title: ${result.task.title}`);
539
+ console.log(` Priority: ${result.task.priority}`);
540
+ console.log(` Status: ${result.task.status}`);
541
+ }
542
+ }
543
+ catch (error) {
544
+ console.error("Error converting feature to task:", error);
545
+ process.exit(1);
546
+ }
547
+ });
265
548
  function printRoadmaps(roadmaps) {
266
549
  if (roadmaps.length === 0) {
267
550
  console.log("\n No roadmaps found.");
@@ -323,3 +606,38 @@ function printRoadmapDetail(roadmap) {
323
606
  console.log(` Could Have: ${couldCount}`);
324
607
  console.log("");
325
608
  }
609
+ function printFeaturesList(roadmapName, features, phases) {
610
+ if (features.length === 0) {
611
+ console.log("\n No features found.");
612
+ console.log(" Add one with: husky roadmap add-feature <roadmapId> <phaseId> <title>\n");
613
+ return;
614
+ }
615
+ console.log(`\n FEATURES - ${roadmapName}`);
616
+ console.log(" " + "─".repeat(80));
617
+ console.log(` ${"ID".padEnd(24)} ${"TITLE".padEnd(30)} ${"STATUS".padEnd(12)} ${"PRIORITY".padEnd(8)} PHASE`);
618
+ console.log(" " + "─".repeat(80));
619
+ // Create phase lookup map
620
+ const phaseMap = new Map(phases.map((p) => [p.id, p.name]));
621
+ for (const feature of features) {
622
+ const statusIcon = feature.status === "done"
623
+ ? "✓"
624
+ : feature.status === "in_progress"
625
+ ? "▶"
626
+ : feature.status === "planned"
627
+ ? "○"
628
+ : "·";
629
+ const phaseName = phaseMap.get(feature.phaseId) || feature.phaseId;
630
+ const truncatedTitle = feature.title.length > 28 ? feature.title.substring(0, 25) + "..." : feature.title;
631
+ const truncatedPhase = phaseName.length > 15 ? phaseName.substring(0, 12) + "..." : phaseName;
632
+ console.log(` ${feature.id.padEnd(24)} ${truncatedTitle.padEnd(30)} ${statusIcon} ${feature.status.padEnd(10)} ${feature.priority.padEnd(8)} ${truncatedPhase}`);
633
+ }
634
+ // Summary by status
635
+ const ideaCount = features.filter((f) => f.status === "idea").length;
636
+ const plannedCount = features.filter((f) => f.status === "planned").length;
637
+ const inProgressCount = features.filter((f) => f.status === "in_progress").length;
638
+ const doneCount = features.filter((f) => f.status === "done").length;
639
+ console.log(" " + "─".repeat(80));
640
+ console.log(`\n Summary: ${features.length} total`);
641
+ console.log(` · Idea: ${ideaCount} ○ Planned: ${plannedCount} ▶ In Progress: ${inProgressCount} ✓ Done: ${doneCount}`);
642
+ console.log("");
643
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const settingsCommand: Command;
@@ -0,0 +1,153 @@
1
+ import { Command } from "commander";
2
+ import { getConfig } from "./config.js";
3
+ export const settingsCommand = new Command("settings")
4
+ .description("Manage application settings");
5
+ // Helper: Ensure API is configured
6
+ function ensureConfig() {
7
+ const config = getConfig();
8
+ if (!config.apiUrl) {
9
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
10
+ process.exit(1);
11
+ }
12
+ return config;
13
+ }
14
+ // husky settings get [key]
15
+ settingsCommand
16
+ .command("get [key]")
17
+ .description("Get settings (all or specific key)")
18
+ .option("--json", "Output as JSON")
19
+ .action(async (key, options) => {
20
+ const config = ensureConfig();
21
+ try {
22
+ // If no key provided, get all settings
23
+ const url = key
24
+ ? `${config.apiUrl}/api/settings/${encodeURIComponent(key)}`
25
+ : `${config.apiUrl}/api/settings`;
26
+ const res = await fetch(url, {
27
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
28
+ });
29
+ if (!res.ok) {
30
+ if (res.status === 404) {
31
+ console.error(`Error: Setting "${key}" not found`);
32
+ }
33
+ else {
34
+ console.error(`Error: API returned ${res.status}`);
35
+ }
36
+ process.exit(1);
37
+ }
38
+ const data = await res.json();
39
+ if (options.json) {
40
+ console.log(JSON.stringify(data, null, 2));
41
+ }
42
+ else {
43
+ if (key) {
44
+ // Single setting
45
+ printSingleSetting(key, data);
46
+ }
47
+ else {
48
+ // All settings
49
+ printAllSettings(data);
50
+ }
51
+ }
52
+ }
53
+ catch (error) {
54
+ console.error("Error fetching settings:", error);
55
+ process.exit(1);
56
+ }
57
+ });
58
+ // husky settings set <key> <value>
59
+ settingsCommand
60
+ .command("set <key> <value>")
61
+ .description("Set a setting value")
62
+ .option("--json", "Output as JSON")
63
+ .action(async (key, value, options) => {
64
+ const config = ensureConfig();
65
+ // Try to parse value as JSON for complex types (arrays, objects, booleans, numbers)
66
+ let parsedValue = value;
67
+ try {
68
+ parsedValue = JSON.parse(value);
69
+ }
70
+ catch {
71
+ // Keep as string if not valid JSON
72
+ }
73
+ try {
74
+ const res = await fetch(`${config.apiUrl}/api/settings/${encodeURIComponent(key)}`, {
75
+ method: "PUT",
76
+ headers: {
77
+ "Content-Type": "application/json",
78
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
79
+ },
80
+ body: JSON.stringify({ value: parsedValue }),
81
+ });
82
+ if (!res.ok) {
83
+ const errorData = await res.json().catch(() => ({}));
84
+ throw new Error(errorData.error || `API error: ${res.status}`);
85
+ }
86
+ const data = await res.json();
87
+ if (options.json) {
88
+ console.log(JSON.stringify(data, null, 2));
89
+ }
90
+ else {
91
+ console.log(`Setting updated successfully`);
92
+ console.log(` Key: ${key}`);
93
+ console.log(` Value: ${formatValue(parsedValue)}`);
94
+ }
95
+ }
96
+ catch (error) {
97
+ console.error("Error setting value:", error);
98
+ process.exit(1);
99
+ }
100
+ });
101
+ // ============================================
102
+ // OUTPUT FORMATTERS
103
+ // ============================================
104
+ function formatValue(value) {
105
+ if (typeof value === "object") {
106
+ return JSON.stringify(value);
107
+ }
108
+ return String(value);
109
+ }
110
+ function printSingleSetting(key, data) {
111
+ const value = "value" in data ? data.value : data;
112
+ console.log(`\n Setting: ${key}`);
113
+ console.log(" " + "-".repeat(50));
114
+ console.log(` Value: ${formatValue(value)}`);
115
+ if ("updatedAt" in data && data.updatedAt) {
116
+ console.log(` Updated: ${new Date(data.updatedAt).toLocaleString()}`);
117
+ }
118
+ console.log("");
119
+ }
120
+ function printAllSettings(data) {
121
+ // Handle both array and object formats
122
+ const entries = [];
123
+ if (Array.isArray(data)) {
124
+ for (const item of data) {
125
+ entries.push({ key: item.key, value: item.value });
126
+ }
127
+ }
128
+ else {
129
+ for (const [key, value] of Object.entries(data)) {
130
+ if (typeof value === "object" && value !== null && "value" in value) {
131
+ entries.push({ key, value: value.value });
132
+ }
133
+ else {
134
+ entries.push({ key, value: value });
135
+ }
136
+ }
137
+ }
138
+ if (entries.length === 0) {
139
+ console.log("\n No settings found.\n");
140
+ return;
141
+ }
142
+ console.log("\n SETTINGS");
143
+ console.log(" " + "-".repeat(70));
144
+ console.log(` ${"KEY".padEnd(30)} ${"VALUE".padEnd(38)}`);
145
+ console.log(" " + "-".repeat(70));
146
+ for (const entry of entries) {
147
+ const valueStr = formatValue(entry.value);
148
+ const truncatedValue = valueStr.length > 36 ? valueStr.substring(0, 33) + "..." : valueStr;
149
+ console.log(` ${entry.key.padEnd(30)} ${truncatedValue}`);
150
+ }
151
+ console.log(" " + "-".repeat(70));
152
+ console.log(` Total: ${entries.length} setting(s)\n`);
153
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const strategyCommand: Command;