@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 +49 -3
- package/dist/commands/idea.js +50 -2
- package/dist/commands/project.js +47 -2
- package/dist/commands/task.js +48 -0
- package/dist/commands/vm.js +225 -0
- package/dist/commands/workflow.js +63 -7
- package/dist/lib/pagination.d.ts +36 -0
- package/dist/lib/pagination.js +180 -0
- package/package.json +1 -1
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.)
|
package/dist/commands/idea.js
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
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);
|
package/dist/commands/project.js
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
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);
|
package/dist/commands/task.js
CHANGED
|
@@ -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) {
|
package/dist/commands/vm.js
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
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", "
|
|
95
|
-
.option("--status <status>", "Workflow status (
|
|
96
|
-
.option("--action <action>", "Action type (
|
|
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
|
+
}
|