@simonfestl/husky-cli 0.6.1 → 0.7.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/README.md CHANGED
@@ -39,6 +39,8 @@ husky
39
39
  husky task list # List all tasks
40
40
  husky task list --status in_progress # Filter by status
41
41
  husky task list --json # JSON output
42
+ husky task list -i # Interactive pagination
43
+ husky task list --per-page 10 --page 2 # Paginated output
42
44
  husky task create "Fix login bug" --priority high
43
45
  husky task get <task-id>
44
46
  husky task start <task-id>
@@ -50,7 +52,9 @@ husky task delete <task-id>
50
52
  ### Project Management
51
53
 
52
54
  ```bash
53
- husky project list
55
+ husky project list # List all projects
56
+ husky project list -i # Interactive pagination
57
+ husky project list --per-page 5 --page 1 # Paginated output
54
58
  husky project create "New Project" --description "..."
55
59
  husky project get <project-id>
56
60
  husky project update <project-id> --status active
@@ -63,7 +67,9 @@ husky project delete-knowledge <project-id> <knowledge-id>
63
67
  ### Workflow Management
64
68
 
65
69
  ```bash
66
- husky workflow list
70
+ husky workflow list # List all workflows
71
+ husky workflow list -i # Interactive pagination
72
+ husky workflow list --per-page 5 # Paginated output
67
73
  husky workflow create "Onboarding" --department <id>
68
74
  husky workflow get <workflow-id>
69
75
  husky workflow update <workflow-id> --name "Updated"
@@ -78,7 +84,9 @@ husky workflow generate-mermaid <workflow-id> # Mermaid diagram
78
84
  ### Idea Management
79
85
 
80
86
  ```bash
81
- husky idea list
87
+ husky idea list # List all ideas
88
+ husky idea list -i # Interactive pagination
89
+ husky idea list --per-page 10 # Paginated output
82
90
  husky idea create "New feature idea" --category feature
83
91
  husky idea get <idea-id>
84
92
  husky idea update <idea-id> --status approved
@@ -237,6 +245,35 @@ husky completion zsh >> ~/.zshrc
237
245
  husky completion fish > ~/.config/fish/completions/husky.fish
238
246
  ```
239
247
 
248
+ ## Pagination
249
+
250
+ List commands support pagination with two modes:
251
+
252
+ ### Interactive Pagination (`-i`)
253
+
254
+ Navigate through results using arrow keys:
255
+
256
+ ```bash
257
+ husky task list -i # Interactive mode with arrow key navigation
258
+ husky project list -i # Works for projects, ideas, workflows
259
+ ```
260
+
261
+ Features:
262
+ - Use `↑`/`↓` to navigate items
263
+ - `←` Previous page / `→` Next page
264
+ - Press `Enter` to select and view details
265
+ - Press `Esc` or select "Exit" to return
266
+
267
+ ### Static Pagination (`--page`, `--per-page`)
268
+
269
+ For scripting or quick page views:
270
+
271
+ ```bash
272
+ husky task list --per-page 10 # Show first 10 items
273
+ husky task list --per-page 10 --page 2 # Show items 11-20
274
+ husky idea list -n 5 -p 3 # Short form: 5 items, page 3
275
+ ```
276
+
240
277
  ## Environment Variables
241
278
 
242
279
  | Variable | Description |
@@ -299,6 +336,15 @@ husky --version
299
336
 
300
337
  ## Changelog
301
338
 
339
+ ### v0.6.2 (2026-01-06)
340
+ - Added: Interactive pagination for list commands (`-i` flag)
341
+ - Added: Static pagination (`--page`, `--per-page` flags)
342
+ - Improved: Tasks, projects, ideas, workflows now support pagination
343
+
344
+ ### v0.6.1 (2026-01-06)
345
+ - Added: Roadmap phase management (`update-phase`, `delete-phase`)
346
+ - Fixed: Various bug fixes
347
+
302
348
  ### v0.6.0 (2026-01-06)
303
349
  - Added: Git Worktree support for multi-agent isolation
304
350
  - Added: `husky worktree` commands (create, list, merge, remove, etc.)
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { getConfig } from "./config.js";
3
+ import { paginateList, printPaginated } from "../lib/pagination.js";
3
4
  export const ideaCommand = new Command("idea")
4
5
  .description("Manage ideas");
5
6
  // Helper: Ensure API is configured
@@ -11,12 +12,22 @@ function ensureConfig() {
11
12
  }
12
13
  return config;
13
14
  }
15
+ // Status labels and icons
16
+ const STATUS_CONFIG = {
17
+ draft: { label: "Draft", icon: "📝" },
18
+ active: { label: "Active", icon: "💡" },
19
+ archived: { label: "Archived", icon: "📦" },
20
+ converted: { label: "Converted", icon: "✅" },
21
+ };
14
22
  // husky idea list
15
23
  ideaCommand
16
24
  .command("list")
17
25
  .description("List all ideas")
18
26
  .option("--status <status>", "Filter by status (draft, active, archived, converted)")
19
27
  .option("--json", "Output as JSON")
28
+ .option("-p, --page <num>", "Page number (starts at 1)", "1")
29
+ .option("-n, --per-page <num>", "Items per page (default: all)")
30
+ .option("-i, --interactive", "Interactive pagination with arrow keys")
20
31
  .action(async (options) => {
21
32
  const config = ensureConfig();
22
33
  try {
@@ -33,10 +44,47 @@ ideaCommand
33
44
  const ideas = await res.json();
34
45
  if (options.json) {
35
46
  console.log(JSON.stringify(ideas, null, 2));
47
+ return;
36
48
  }
37
- else {
38
- printIdeas(ideas);
49
+ // Interactive pagination mode
50
+ if (options.interactive) {
51
+ await paginateList({
52
+ items: ideas,
53
+ pageSize: 10,
54
+ title: "Ideas",
55
+ emptyMessage: "No ideas found.",
56
+ renderItem: (idea) => {
57
+ const cfg = STATUS_CONFIG[idea.status] || { label: idea.status, icon: "○" };
58
+ return ` ${cfg.icon} ${idea.title.slice(0, 40).padEnd(40)} │ ${cfg.label}`;
59
+ },
60
+ selectableItems: true,
61
+ onSelect: async (idea) => {
62
+ console.clear();
63
+ console.log(`\n Idea: ${idea.title}`);
64
+ console.log(" " + "─".repeat(50));
65
+ console.log(` ID: ${idea.id}`);
66
+ console.log(` Status: ${STATUS_CONFIG[idea.status]?.label || idea.status}`);
67
+ if (idea.category)
68
+ console.log(` Category: ${idea.category}`);
69
+ if (idea.description)
70
+ console.log(` Desc: ${idea.description}`);
71
+ console.log("");
72
+ },
73
+ });
74
+ return;
75
+ }
76
+ // Simple pagination mode
77
+ if (options.perPage) {
78
+ const pageNum = parseInt(options.page, 10) - 1;
79
+ const pageSize = parseInt(options.perPage, 10);
80
+ printPaginated(ideas, pageNum, pageSize, (idea) => {
81
+ const cfg = STATUS_CONFIG[idea.status] || { label: idea.status, icon: "○" };
82
+ return ` ${cfg.icon} ${idea.id.slice(0, 8)} │ ${idea.title}`;
83
+ }, "Ideas");
84
+ return;
39
85
  }
86
+ // Default list
87
+ printIdeas(ideas);
40
88
  }
41
89
  catch (error) {
42
90
  console.error("Error fetching ideas:", error);
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { getConfig } from "./config.js";
3
+ import { paginateList, printPaginated } from "../lib/pagination.js";
3
4
  export const projectCommand = new Command("project")
4
5
  .description("Manage projects and knowledge");
5
6
  // Helper: Ensure API is configured
@@ -37,6 +38,9 @@ projectCommand
37
38
  .option("--status <status>", "Filter by status (active, archived)")
38
39
  .option("--work-status <workStatus>", "Filter by work status (planning, in_progress, review, completed, on_hold)")
39
40
  .option("--archived", "Include archived projects")
41
+ .option("-p, --page <num>", "Page number (starts at 1)", "1")
42
+ .option("-n, --per-page <num>", "Items per page (default: all)")
43
+ .option("-i, --interactive", "Interactive pagination with arrow keys")
40
44
  .action(async (options) => {
41
45
  const config = ensureConfig();
42
46
  try {
@@ -60,10 +64,51 @@ projectCommand
60
64
  }
61
65
  if (options.json) {
62
66
  console.log(JSON.stringify(projects, null, 2));
67
+ return;
63
68
  }
64
- else {
65
- printProjects(projects);
69
+ // Interactive pagination mode
70
+ if (options.interactive) {
71
+ await paginateList({
72
+ items: projects,
73
+ pageSize: 10,
74
+ title: "Projects",
75
+ emptyMessage: "No projects found.",
76
+ renderItem: (project) => {
77
+ const statusIcon = project.status === "archived" ? "📦" : "📁";
78
+ const workStatusLabel = WORK_STATUS_LABELS[project.workStatus] || project.workStatus;
79
+ return ` ${statusIcon} ${project.name.slice(0, 30).padEnd(30)} │ ${workStatusLabel}`;
80
+ },
81
+ selectableItems: true,
82
+ onSelect: async (project) => {
83
+ console.clear();
84
+ console.log(`\n Project: ${project.name}`);
85
+ console.log(" " + "─".repeat(50));
86
+ console.log(` ID: ${project.id}`);
87
+ console.log(` Status: ${project.status}`);
88
+ console.log(` Work: ${WORK_STATUS_LABELS[project.workStatus]}`);
89
+ if (project.description)
90
+ console.log(` Desc: ${project.description}`);
91
+ if (project.techStack)
92
+ console.log(` Tech: ${project.techStack}`);
93
+ if (project.githubRepo)
94
+ console.log(` GitHub: ${project.githubRepo}`);
95
+ console.log("");
96
+ },
97
+ });
98
+ return;
99
+ }
100
+ // Simple pagination mode
101
+ if (options.perPage) {
102
+ const pageNum = parseInt(options.page, 10) - 1;
103
+ const pageSize = parseInt(options.perPage, 10);
104
+ printPaginated(projects, pageNum, pageSize, (project) => {
105
+ const statusIcon = project.status === "archived" ? "📦" : "📁";
106
+ return ` ${statusIcon} ${project.id.slice(0, 8)} │ ${project.name}`;
107
+ }, "Projects");
108
+ return;
66
109
  }
110
+ // Default: standard list
111
+ printProjects(projects);
67
112
  }
68
113
  catch (error) {
69
114
  console.error("Error fetching projects:", error);
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  import { getConfig } from "./config.js";
3
3
  import * as fs from "fs";
4
4
  import * as readline from "readline";
5
+ import { paginateList, printPaginated } from "../lib/pagination.js";
5
6
  export const taskCommand = new Command("task")
6
7
  .description("Manage tasks");
7
8
  // Helper: Get task ID from --id flag or HUSKY_TASK_ID env var
@@ -40,6 +41,10 @@ taskCommand
40
41
  .command("list")
41
42
  .description("List all tasks")
42
43
  .option("-s, --status <status>", "Filter by status")
44
+ .option("-p, --page <num>", "Page number (starts at 1)", "1")
45
+ .option("-n, --per-page <num>", "Items per page (default: all)")
46
+ .option("-i, --interactive", "Interactive pagination with arrow keys")
47
+ .option("--json", "Output as JSON")
43
48
  .action(async (options) => {
44
49
  const config = getConfig();
45
50
  if (!config.apiUrl) {
@@ -58,6 +63,49 @@ taskCommand
58
63
  throw new Error(`API error: ${res.status}`);
59
64
  }
60
65
  const tasks = await res.json();
66
+ // JSON output
67
+ if (options.json) {
68
+ console.log(JSON.stringify(tasks, null, 2));
69
+ return;
70
+ }
71
+ // Interactive pagination mode
72
+ if (options.interactive) {
73
+ await paginateList({
74
+ items: tasks,
75
+ pageSize: 10,
76
+ title: "Tasks",
77
+ emptyMessage: "No tasks found.",
78
+ renderItem: (task) => {
79
+ const statusIcon = task.status === "done" ? "✓" : task.status === "in_progress" ? "▶" : "○";
80
+ const priorityIcon = task.priority === "urgent" ? "🔴" : task.priority === "high" ? "🟠" : "";
81
+ return ` ${statusIcon} ${task.id.slice(0, 8)} │ ${task.title.slice(0, 40).padEnd(40)} ${priorityIcon}`;
82
+ },
83
+ selectableItems: true,
84
+ onSelect: async (task) => {
85
+ console.clear();
86
+ console.log(`\n Task: ${task.title}`);
87
+ console.log(" " + "─".repeat(50));
88
+ console.log(` ID: ${task.id}`);
89
+ console.log(` Status: ${task.status}`);
90
+ console.log(` Priority: ${task.priority}`);
91
+ if (task.agent)
92
+ console.log(` Agent: ${task.agent}`);
93
+ console.log("");
94
+ },
95
+ });
96
+ return;
97
+ }
98
+ // Simple pagination mode
99
+ if (options.perPage) {
100
+ const pageNum = parseInt(options.page, 10) - 1;
101
+ const pageSize = parseInt(options.perPage, 10);
102
+ printPaginated(tasks, pageNum, pageSize, (task) => {
103
+ const statusIcon = task.status === "done" ? "✓" : task.status === "in_progress" ? "▶" : "○";
104
+ return ` ${statusIcon} ${task.id.slice(0, 8)} │ ${task.title}`;
105
+ }, "Tasks");
106
+ return;
107
+ }
108
+ // Default: grouped by status (original behavior)
61
109
  printTasks(tasks);
62
110
  }
63
111
  catch (error) {
@@ -569,6 +569,13 @@ function printVMSessionDetail(session) {
569
569
  if (session.workflowId) {
570
570
  console.log(` Workflow ID: ${session.workflowId}`);
571
571
  }
572
+ // Pool info
573
+ if (session.sessionType) {
574
+ console.log(` Session Type: ${session.sessionType === 'pool' ? 'Pool VM' : 'On-Demand'}`);
575
+ if (session.poolVmId) {
576
+ console.log(` Pool VM ID: ${session.poolVmId}`);
577
+ }
578
+ }
572
579
  console.log(`\n Prompt:`);
573
580
  console.log(` ${session.prompt}`);
574
581
  if (session.costEstimate !== undefined) {
@@ -619,3 +626,221 @@ function printLog(log) {
619
626
  }
620
627
  console.log(`${prefix}${timestamp} ${level} ${source} ${log.message}`);
621
628
  }
629
+ // ============================================
630
+ // VM POOL COMMANDS
631
+ // ============================================
632
+ const poolCommand = new Command("pool")
633
+ .description("Manage VM pool (admin)");
634
+ // husky vm pool status
635
+ poolCommand
636
+ .command("status")
637
+ .description("Show VM pool status")
638
+ .option("--json", "Output as JSON")
639
+ .action(async (options) => {
640
+ const config = ensureConfig();
641
+ try {
642
+ const res = await fetch(`${config.apiUrl}/api/admin/vm-pool`, {
643
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
644
+ });
645
+ if (!res.ok) {
646
+ const errorData = await res.json().catch(() => ({}));
647
+ throw new Error(errorData.error || `API error: ${res.status}`);
648
+ }
649
+ const data = await res.json();
650
+ const vms = data.vms || [];
651
+ if (options.json) {
652
+ console.log(JSON.stringify(data, null, 2));
653
+ }
654
+ else {
655
+ printPoolStatus(vms);
656
+ }
657
+ }
658
+ catch (error) {
659
+ console.error("Error fetching VM pool status:", error);
660
+ process.exit(1);
661
+ }
662
+ });
663
+ // husky vm pool init
664
+ poolCommand
665
+ .command("init")
666
+ .description("Initialize VM pool (1 HOT + 4 COLD VMs)")
667
+ .option("--json", "Output as JSON")
668
+ .action(async (options) => {
669
+ const config = ensureConfig();
670
+ console.log("Initializing VM pool...");
671
+ console.log("This will create 1 HOT VM + 4 COLD VMs\n");
672
+ try {
673
+ const res = await fetch(`${config.apiUrl}/api/admin/vm-pool/initialize`, {
674
+ method: "POST",
675
+ headers: {
676
+ "Content-Type": "application/json",
677
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
678
+ },
679
+ });
680
+ if (!res.ok) {
681
+ const errorData = await res.json().catch(() => ({}));
682
+ throw new Error(errorData.error || `API error: ${res.status}`);
683
+ }
684
+ const data = await res.json();
685
+ if (options.json) {
686
+ console.log(JSON.stringify(data, null, 2));
687
+ }
688
+ else {
689
+ console.log(`VM Pool initialized!`);
690
+ console.log(` HOT VMs: ${data.hotCount || 1}`);
691
+ console.log(` COLD VMs: ${data.coldCount || 4}`);
692
+ console.log(`\nRun 'husky vm pool status' to see pool details`);
693
+ }
694
+ }
695
+ catch (error) {
696
+ console.error("Error initializing VM pool:", error);
697
+ process.exit(1);
698
+ }
699
+ });
700
+ // husky vm pool suspend <id>
701
+ poolCommand
702
+ .command("suspend <id>")
703
+ .description("Suspend a pool VM")
704
+ .option("--json", "Output as JSON")
705
+ .action(async (id, options) => {
706
+ const config = ensureConfig();
707
+ console.log(`Suspending VM ${id}...\n`);
708
+ try {
709
+ const res = await fetch(`${config.apiUrl}/api/admin/vm-pool/${id}/suspend`, {
710
+ method: "POST",
711
+ headers: {
712
+ "Content-Type": "application/json",
713
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
714
+ },
715
+ });
716
+ if (!res.ok) {
717
+ const errorData = await res.json().catch(() => ({}));
718
+ throw new Error(errorData.error || `API error: ${res.status}`);
719
+ }
720
+ const data = await res.json();
721
+ if (options.json) {
722
+ console.log(JSON.stringify(data, null, 2));
723
+ }
724
+ else {
725
+ console.log(`VM suspended successfully`);
726
+ console.log(` VM Name: ${data.vmName}`);
727
+ }
728
+ }
729
+ catch (error) {
730
+ console.error("Error suspending VM:", error);
731
+ process.exit(1);
732
+ }
733
+ });
734
+ // husky vm pool resume <id>
735
+ poolCommand
736
+ .command("resume <id>")
737
+ .description("Resume a suspended pool VM")
738
+ .option("--json", "Output as JSON")
739
+ .action(async (id, options) => {
740
+ const config = ensureConfig();
741
+ console.log(`Resuming VM ${id}...\n`);
742
+ try {
743
+ const res = await fetch(`${config.apiUrl}/api/admin/vm-pool/${id}/resume`, {
744
+ method: "POST",
745
+ headers: {
746
+ "Content-Type": "application/json",
747
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
748
+ },
749
+ });
750
+ if (!res.ok) {
751
+ const errorData = await res.json().catch(() => ({}));
752
+ throw new Error(errorData.error || `API error: ${res.status}`);
753
+ }
754
+ const data = await res.json();
755
+ if (options.json) {
756
+ console.log(JSON.stringify(data, null, 2));
757
+ }
758
+ else {
759
+ console.log(`VM resumed successfully`);
760
+ console.log(` VM Name: ${data.vmName}`);
761
+ }
762
+ }
763
+ catch (error) {
764
+ console.error("Error resuming VM:", error);
765
+ process.exit(1);
766
+ }
767
+ });
768
+ // husky vm pool setup <id>
769
+ poolCommand
770
+ .command("setup <id>")
771
+ .description("Start Claude subscription setup for a pool VM")
772
+ .option("--json", "Output as JSON")
773
+ .action(async (id, options) => {
774
+ const config = ensureConfig();
775
+ console.log(`Starting subscription setup for VM ${id}...\n`);
776
+ try {
777
+ const res = await fetch(`${config.apiUrl}/api/admin/vm-pool/${id}/setup`, {
778
+ method: "POST",
779
+ headers: {
780
+ "Content-Type": "application/json",
781
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
782
+ },
783
+ });
784
+ if (!res.ok) {
785
+ const errorData = await res.json().catch(() => ({}));
786
+ throw new Error(errorData.error || `API error: ${res.status}`);
787
+ }
788
+ const data = await res.json();
789
+ if (options.json) {
790
+ console.log(JSON.stringify(data, null, 2));
791
+ }
792
+ else {
793
+ console.log(`Setup session created!`);
794
+ console.log(` Session ID: ${data.sessionId}`);
795
+ console.log(` VM Name: ${data.vmName}`);
796
+ console.log(` VM IP: ${data.vmIpAddress || "pending"}`);
797
+ console.log(`\nInstructions:`);
798
+ for (const instruction of data.instructions || []) {
799
+ console.log(` ${instruction}`);
800
+ }
801
+ console.log(`\nExpires: ${data.expiresAt}`);
802
+ }
803
+ }
804
+ catch (error) {
805
+ console.error("Error starting setup:", error);
806
+ process.exit(1);
807
+ }
808
+ });
809
+ // Print helper for pool status
810
+ function printPoolStatus(vms) {
811
+ if (vms.length === 0) {
812
+ console.log("\n VM Pool is empty.");
813
+ console.log(" Initialize with: husky vm pool init\n");
814
+ return;
815
+ }
816
+ // Calculate stats
817
+ const total = vms.length;
818
+ const available = vms.filter(v => v.status === 'available').length;
819
+ const inUse = vms.filter(v => v.status === 'in_use').length;
820
+ const suspended = vms.filter(v => v.status === 'suspended').length;
821
+ const hotVms = vms.filter(v => v.tier === 'hot').length;
822
+ const authenticated = vms.filter(v => v.subscriptionAuth).length;
823
+ // Estimate monthly cost
824
+ const runningCost = (available + inUse) * 12.24; // $12.24/month for e2-small running
825
+ const suspendedCost = suspended * 0.30; // $0.30/month for suspended
826
+ const totalCost = runningCost + suspendedCost;
827
+ console.log("\n VM POOL STATUS");
828
+ console.log(" " + "=".repeat(70));
829
+ console.log(` Total: ${total} | Available: ${available} | In Use: ${inUse} | Suspended: ${suspended}`);
830
+ console.log(` HOT VMs: ${hotVms} | Authenticated: ${authenticated}/${total}`);
831
+ console.log(` Est. Monthly Cost: ~$${totalCost.toFixed(2)}`);
832
+ console.log(" " + "-".repeat(70));
833
+ console.log(` ${"ID".padEnd(24)} ${"NAME".padEnd(18)} ${"STATUS".padEnd(12)} ${"TIER".padEnd(6)} ${"AUTH".padEnd(5)} LAST USED`);
834
+ console.log(" " + "-".repeat(70));
835
+ for (const vm of vms) {
836
+ const status = vm.status.toUpperCase().padEnd(12);
837
+ const tier = vm.tier.toUpperCase().padEnd(6);
838
+ const auth = vm.subscriptionAuth ? "Yes" : "No";
839
+ const lastUsed = new Date(vm.lastUsedAt).toLocaleDateString();
840
+ const truncatedName = vm.vmName.length > 16 ? vm.vmName.substring(0, 13) + "..." : vm.vmName;
841
+ console.log(` ${vm.id.padEnd(24)} ${truncatedName.padEnd(18)} ${status} ${tier} ${auth.padEnd(5)} ${lastUsed}`);
842
+ }
843
+ console.log("");
844
+ }
845
+ // Add pool command to vm command
846
+ vmCommand.addCommand(poolCommand);
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { getConfig } from "./config.js";
3
+ import { paginateList, printPaginated } from "../lib/pagination.js";
3
4
  export const workflowCommand = new Command("workflow")
4
5
  .description("Manage workflows and workflow steps");
5
6
  // Helper: Ensure API is configured
@@ -11,12 +12,28 @@ function ensureConfig() {
11
12
  }
12
13
  return config;
13
14
  }
15
+ // Status icons
16
+ const STATUS_ICONS = {
17
+ draft: "📝",
18
+ active: "▶️",
19
+ paused: "⏸️",
20
+ archived: "📦",
21
+ };
22
+ // Action labels
23
+ const ACTION_LABELS = {
24
+ manual: "Manual",
25
+ semi_automated: "Semi-Auto",
26
+ fully_automated: "Auto",
27
+ };
14
28
  // husky workflow list
15
29
  workflowCommand
16
30
  .command("list")
17
31
  .description("List all workflows")
18
32
  .option("--value-stream <valueStream>", "Filter by value stream")
19
33
  .option("--json", "Output as JSON")
34
+ .option("-p, --page <num>", "Page number (starts at 1)", "1")
35
+ .option("-n, --per-page <num>", "Items per page (default: all)")
36
+ .option("-i, --interactive", "Interactive pagination with arrow keys")
20
37
  .action(async (options) => {
21
38
  const config = ensureConfig();
22
39
  try {
@@ -33,10 +50,49 @@ workflowCommand
33
50
  const workflows = await res.json();
34
51
  if (options.json) {
35
52
  console.log(JSON.stringify(workflows, null, 2));
36
- }
37
- else {
38
- printWorkflows(workflows);
39
- }
53
+ return;
54
+ }
55
+ // Interactive pagination mode
56
+ if (options.interactive) {
57
+ await paginateList({
58
+ items: workflows,
59
+ pageSize: 10,
60
+ title: "Workflows",
61
+ emptyMessage: "No workflows found.",
62
+ renderItem: (wf) => {
63
+ const icon = STATUS_ICONS[wf.status] || "○";
64
+ const action = ACTION_LABELS[wf.action] || wf.action;
65
+ return ` ${icon} ${wf.name.slice(0, 35).padEnd(35)} │ ${action}`;
66
+ },
67
+ selectableItems: true,
68
+ onSelect: async (wf) => {
69
+ console.clear();
70
+ console.log(`\n Workflow: ${wf.name}`);
71
+ console.log(" " + "─".repeat(50));
72
+ console.log(` ID: ${wf.id}`);
73
+ console.log(` Status: ${wf.status}`);
74
+ console.log(` Action: ${ACTION_LABELS[wf.action]}`);
75
+ console.log(` ValueStream: ${wf.valueStream}`);
76
+ console.log(` Autonomy: ${wf.autonomyWeight}%`);
77
+ if (wf.description)
78
+ console.log(` Desc: ${wf.description}`);
79
+ console.log("");
80
+ },
81
+ });
82
+ return;
83
+ }
84
+ // Simple pagination mode
85
+ if (options.perPage) {
86
+ const pageNum = parseInt(options.page, 10) - 1;
87
+ const pageSize = parseInt(options.perPage, 10);
88
+ printPaginated(workflows, pageNum, pageSize, (wf) => {
89
+ const icon = STATUS_ICONS[wf.status] || "○";
90
+ return ` ${icon} ${wf.id.slice(0, 8)} │ ${wf.name}`;
91
+ }, "Workflows");
92
+ return;
93
+ }
94
+ // Default list
95
+ printWorkflows(workflows);
40
96
  }
41
97
  catch (error) {
42
98
  console.error("Error fetching workflows:", error);
@@ -91,9 +147,9 @@ workflowCommand
91
147
  .command("create <name>")
92
148
  .description("Create a new workflow")
93
149
  .option("-d, --description <description>", "Workflow description")
94
- .option("--value-stream <valueStream>", "Value stream", "general")
95
- .option("--status <status>", "Workflow status (draft, active, paused, archived)", "draft")
96
- .option("--action <action>", "Action type (manual, semi_automated, fully_automated)", "manual")
150
+ .option("--value-stream <valueStream>", "Value stream (order_to_delivery, procure_to_pay, returns_management, product_lifecycle, customer_service, finance_admin)", "customer_service")
151
+ .option("--status <status>", "Workflow status (owner_only, approval_needed, supervised, delegated, automated)", "owner_only")
152
+ .option("--action <action>", "Action type (automate, hire, document, fix, ok)", "document")
97
153
  .option("--json", "Output as JSON")
98
154
  .action(async (name, options) => {
99
155
  const config = ensureConfig();
@@ -0,0 +1,36 @@
1
+ export interface PaginationOptions<T> {
2
+ items: T[];
3
+ pageSize?: number;
4
+ renderItem: (item: T, index: number) => string;
5
+ title?: string;
6
+ emptyMessage?: string;
7
+ selectableItems?: boolean;
8
+ onSelect?: (item: T) => Promise<void>;
9
+ }
10
+ export interface PaginationResult<T> {
11
+ selectedItem: T | null;
12
+ action: "select" | "exit";
13
+ }
14
+ /**
15
+ * Interactive paginated list with arrow key navigation
16
+ *
17
+ * @example
18
+ * await paginateList({
19
+ * items: tasks,
20
+ * pageSize: 10,
21
+ * renderItem: (t, i) => `${t.id} - ${t.title}`,
22
+ * title: "Tasks",
23
+ * selectableItems: true,
24
+ * onSelect: async (task) => console.log(`Selected: ${task.title}`)
25
+ * });
26
+ */
27
+ export declare function paginateList<T>(options: PaginationOptions<T>): Promise<PaginationResult<T>>;
28
+ /**
29
+ * Simple paginated display without interactivity
30
+ * Useful for CLI commands that just want to show paginated output
31
+ */
32
+ export declare function printPaginated<T>(items: T[], page: number, pageSize: number, renderItem: (item: T, index: number) => string, title?: string): {
33
+ hasMore: boolean;
34
+ totalPages: number;
35
+ currentPage: number;
36
+ };
@@ -0,0 +1,180 @@
1
+ import { select } from "@inquirer/prompts";
2
+ const NAVIGATION_SEPARATOR = "──────────────";
3
+ /**
4
+ * Interactive paginated list with arrow key navigation
5
+ *
6
+ * @example
7
+ * await paginateList({
8
+ * items: tasks,
9
+ * pageSize: 10,
10
+ * renderItem: (t, i) => `${t.id} - ${t.title}`,
11
+ * title: "Tasks",
12
+ * selectableItems: true,
13
+ * onSelect: async (task) => console.log(`Selected: ${task.title}`)
14
+ * });
15
+ */
16
+ export async function paginateList(options) {
17
+ const { items, pageSize = 10, renderItem, title = "Items", emptyMessage = "No items found.", selectableItems = false, onSelect, } = options;
18
+ if (items.length === 0) {
19
+ console.log(`\n ${emptyMessage}`);
20
+ return { selectedItem: null, action: "exit" };
21
+ }
22
+ const totalPages = Math.ceil(items.length / pageSize);
23
+ let currentPage = 0;
24
+ while (true) {
25
+ const startIndex = currentPage * pageSize;
26
+ const endIndex = Math.min(startIndex + pageSize, items.length);
27
+ const pageItems = items.slice(startIndex, endIndex);
28
+ // Clear screen and show header
29
+ console.clear();
30
+ console.log(`\n ${title} (${items.length} total)`);
31
+ console.log(` Page ${currentPage + 1} of ${totalPages}`);
32
+ console.log(" " + "─".repeat(50));
33
+ // Build choices for current page
34
+ const choices = [];
35
+ // Add page items
36
+ pageItems.forEach((item, idx) => {
37
+ const globalIndex = startIndex + idx;
38
+ const displayText = renderItem(item, globalIndex);
39
+ choices.push({
40
+ name: displayText,
41
+ value: selectableItems ? `item:${globalIndex}` : `view:${globalIndex}`,
42
+ description: selectableItems ? "Select this item" : undefined,
43
+ });
44
+ });
45
+ // Add separator
46
+ choices.push({
47
+ name: NAVIGATION_SEPARATOR,
48
+ value: "separator",
49
+ description: "",
50
+ });
51
+ // Navigation options
52
+ const navOptions = [];
53
+ if (currentPage > 0) {
54
+ navOptions.push({
55
+ name: "← Previous Page",
56
+ value: "prev",
57
+ description: `Go to page ${currentPage}`,
58
+ });
59
+ }
60
+ if (currentPage < totalPages - 1) {
61
+ navOptions.push({
62
+ name: "→ Next Page",
63
+ value: "next",
64
+ description: `Go to page ${currentPage + 2}`,
65
+ });
66
+ }
67
+ if (totalPages > 2) {
68
+ navOptions.push({
69
+ name: "⇢ Jump to Page...",
70
+ value: "jump",
71
+ description: `Go to a specific page (1-${totalPages})`,
72
+ });
73
+ }
74
+ navOptions.push({
75
+ name: "✕ Exit",
76
+ value: "exit",
77
+ description: "Return to previous menu",
78
+ });
79
+ choices.push(...navOptions);
80
+ try {
81
+ const answer = await select({
82
+ message: "Navigate with ↑↓, select with Enter:",
83
+ choices,
84
+ pageSize: pageSize + 5, // Room for navigation
85
+ });
86
+ if (answer === "separator") {
87
+ // User selected separator, ignore and continue
88
+ continue;
89
+ }
90
+ if (answer === "prev") {
91
+ currentPage = Math.max(0, currentPage - 1);
92
+ continue;
93
+ }
94
+ if (answer === "next") {
95
+ currentPage = Math.min(totalPages - 1, currentPage + 1);
96
+ continue;
97
+ }
98
+ if (answer === "jump") {
99
+ const jumpChoice = await selectPageNumber(totalPages, currentPage);
100
+ if (jumpChoice !== null) {
101
+ currentPage = jumpChoice;
102
+ }
103
+ continue;
104
+ }
105
+ if (answer === "exit") {
106
+ return { selectedItem: null, action: "exit" };
107
+ }
108
+ // Handle item selection
109
+ if (answer.startsWith("item:") || answer.startsWith("view:")) {
110
+ const index = parseInt(answer.split(":")[1], 10);
111
+ const selectedItem = items[index];
112
+ if (selectableItems && onSelect && selectedItem) {
113
+ await onSelect(selectedItem);
114
+ }
115
+ return { selectedItem, action: "select" };
116
+ }
117
+ }
118
+ catch (error) {
119
+ // User pressed Ctrl+C or escape
120
+ return { selectedItem: null, action: "exit" };
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Helper to select a page number
126
+ */
127
+ async function selectPageNumber(totalPages, currentPage) {
128
+ const choices = [];
129
+ for (let i = 0; i < totalPages; i++) {
130
+ choices.push({
131
+ name: `Page ${i + 1}${i === currentPage ? " (current)" : ""}`,
132
+ value: i.toString(),
133
+ });
134
+ }
135
+ choices.push({
136
+ name: "Cancel",
137
+ value: "cancel",
138
+ });
139
+ try {
140
+ const answer = await select({
141
+ message: "Select page:",
142
+ choices,
143
+ pageSize: Math.min(totalPages + 1, 15),
144
+ });
145
+ if (answer === "cancel") {
146
+ return null;
147
+ }
148
+ return parseInt(answer, 10);
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ }
154
+ /**
155
+ * Simple paginated display without interactivity
156
+ * Useful for CLI commands that just want to show paginated output
157
+ */
158
+ export function printPaginated(items, page, pageSize, renderItem, title) {
159
+ const totalPages = Math.ceil(items.length / pageSize);
160
+ const currentPage = Math.min(Math.max(0, page), totalPages - 1);
161
+ const startIndex = currentPage * pageSize;
162
+ const endIndex = Math.min(startIndex + pageSize, items.length);
163
+ const pageItems = items.slice(startIndex, endIndex);
164
+ if (title) {
165
+ console.log(`\n ${title}`);
166
+ console.log(" " + "─".repeat(50));
167
+ }
168
+ pageItems.forEach((item, idx) => {
169
+ console.log(renderItem(item, startIndex + idx));
170
+ });
171
+ if (totalPages > 1) {
172
+ console.log("");
173
+ console.log(` Page ${currentPage + 1} of ${totalPages} (${items.length} total)`);
174
+ }
175
+ return {
176
+ hasMore: currentPage < totalPages - 1,
177
+ totalPages,
178
+ currentPage,
179
+ };
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {