@simonfestl/husky-cli 0.6.0 → 0.6.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.
- package/README.md +72 -3
- package/dist/commands/idea.js +50 -2
- package/dist/commands/project.js +47 -2
- package/dist/commands/roadmap.js +107 -0
- package/dist/commands/task.js +48 -0
- package/dist/commands/workflow.js +63 -7
- package/dist/index.js +1 -1
- 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
|
|
@@ -160,6 +168,8 @@ husky roadmap create "Q1 2025" --description "..."
|
|
|
160
168
|
husky roadmap update <roadmap-id> --name "Updated"
|
|
161
169
|
husky roadmap delete <roadmap-id>
|
|
162
170
|
husky roadmap add-phase <roadmap-id> --name "Phase 1"
|
|
171
|
+
husky roadmap update-phase <roadmap-id> <phase-id> --name "Updated"
|
|
172
|
+
husky roadmap delete-phase <roadmap-id> <phase-id> --force
|
|
163
173
|
husky roadmap add-feature <roadmap-id> --phase <id> --title "Feature"
|
|
164
174
|
husky roadmap list-features <roadmap-id>
|
|
165
175
|
husky roadmap update-feature <roadmap-id> <feature-id> --status done
|
|
@@ -187,6 +197,20 @@ husky vm-config update <config-id> --machine-type e2-standard-2
|
|
|
187
197
|
husky vm-config delete <config-id>
|
|
188
198
|
```
|
|
189
199
|
|
|
200
|
+
### Git Worktree Management
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
husky worktree list # List all worktrees
|
|
204
|
+
husky worktree create <session-name> # Create worktree
|
|
205
|
+
husky worktree info <session-name> # Show details
|
|
206
|
+
husky worktree status [session-name] # Show status
|
|
207
|
+
husky worktree cd <session-name> # Print path
|
|
208
|
+
husky worktree merge <session-name> # Merge to base
|
|
209
|
+
husky worktree remove <session-name> # Remove worktree
|
|
210
|
+
husky worktree branches # List husky/* branches
|
|
211
|
+
husky worktree cleanup # Clean stale worktrees
|
|
212
|
+
```
|
|
213
|
+
|
|
190
214
|
### Settings
|
|
191
215
|
|
|
192
216
|
```bash
|
|
@@ -221,6 +245,35 @@ husky completion zsh >> ~/.zshrc
|
|
|
221
245
|
husky completion fish > ~/.config/fish/completions/husky.fish
|
|
222
246
|
```
|
|
223
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
|
+
|
|
224
277
|
## Environment Variables
|
|
225
278
|
|
|
226
279
|
| Variable | Description |
|
|
@@ -283,6 +336,22 @@ husky --version
|
|
|
283
336
|
|
|
284
337
|
## Changelog
|
|
285
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
|
+
|
|
348
|
+
### v0.6.0 (2026-01-06)
|
|
349
|
+
- Added: Git Worktree support for multi-agent isolation
|
|
350
|
+
- Added: `husky worktree` commands (create, list, merge, remove, etc.)
|
|
351
|
+
- Added: MergeLock mechanism for safe concurrent operations
|
|
352
|
+
- Refactored: Interactive mode into modular components
|
|
353
|
+
- Improved: Based on Auto-Claude's worktree architecture
|
|
354
|
+
|
|
286
355
|
### v0.5.0 (2026-01-06)
|
|
287
356
|
- Full Dashboard feature parity (69 new commands)
|
|
288
357
|
- Added: project, workflow, idea, department, vm, jules, process, strategy, settings, vm-config commands
|
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/roadmap.js
CHANGED
|
@@ -152,6 +152,113 @@ roadmapCommand
|
|
|
152
152
|
process.exit(1);
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
|
+
// husky roadmap update-phase <roadmapId> <phaseId>
|
|
156
|
+
roadmapCommand
|
|
157
|
+
.command("update-phase <roadmapId> <phaseId>")
|
|
158
|
+
.description("Update a roadmap phase")
|
|
159
|
+
.option("-n, --name <name>", "New phase name")
|
|
160
|
+
.option("-d, --description <description>", "New phase description")
|
|
161
|
+
.option("--status <status>", "Phase status (planned, in_progress, completed)")
|
|
162
|
+
.option("--order <order>", "Phase order (0-based)")
|
|
163
|
+
.action(async (roadmapId, phaseId, options) => {
|
|
164
|
+
const config = ensureConfig();
|
|
165
|
+
// Build update payload
|
|
166
|
+
const updateData = { phaseId };
|
|
167
|
+
if (options.name)
|
|
168
|
+
updateData.name = options.name;
|
|
169
|
+
if (options.description)
|
|
170
|
+
updateData.description = options.description;
|
|
171
|
+
if (options.status) {
|
|
172
|
+
const validStatuses = ["planned", "in_progress", "completed"];
|
|
173
|
+
if (!validStatuses.includes(options.status)) {
|
|
174
|
+
console.error(`Error: Invalid status "${options.status}". Must be one of: ${validStatuses.join(", ")}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
updateData.status = options.status;
|
|
178
|
+
}
|
|
179
|
+
if (options.order !== undefined) {
|
|
180
|
+
const order = parseInt(options.order, 10);
|
|
181
|
+
if (isNaN(order) || order < 0) {
|
|
182
|
+
console.error("Error: Order must be a non-negative integer");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
updateData.order = order;
|
|
186
|
+
}
|
|
187
|
+
if (Object.keys(updateData).length === 1) {
|
|
188
|
+
console.error("Error: No update options provided. Use -n/--name, -d/--description, --status, or --order");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const res = await fetch(`${config.apiUrl}/api/roadmaps/${roadmapId}/phases`, {
|
|
193
|
+
method: "PATCH",
|
|
194
|
+
headers: {
|
|
195
|
+
"Content-Type": "application/json",
|
|
196
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify(updateData),
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
if (res.status === 404) {
|
|
202
|
+
console.error(`Error: Roadmap or phase not found`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
206
|
+
console.error(`Error: API returned ${res.status}`, errorBody.error || "");
|
|
207
|
+
}
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
const roadmap = await res.json();
|
|
211
|
+
const updatedPhase = roadmap.phases.find((p) => p.id === phaseId);
|
|
212
|
+
console.log(`✓ Phase updated successfully`);
|
|
213
|
+
if (updatedPhase) {
|
|
214
|
+
console.log(` Name: ${updatedPhase.name}`);
|
|
215
|
+
console.log(` Status: ${updatedPhase.status}`);
|
|
216
|
+
console.log(` Order: ${updatedPhase.order}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
console.error("Error updating phase:", error);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
// husky roadmap delete-phase <roadmapId> <phaseId>
|
|
225
|
+
roadmapCommand
|
|
226
|
+
.command("delete-phase <roadmapId> <phaseId>")
|
|
227
|
+
.description("Delete a phase from a roadmap (including all its features)")
|
|
228
|
+
.option("--force", "Skip confirmation")
|
|
229
|
+
.action(async (roadmapId, phaseId, options) => {
|
|
230
|
+
const config = ensureConfig();
|
|
231
|
+
if (!options.force) {
|
|
232
|
+
console.log("Warning: This will permanently delete the phase and all its features.");
|
|
233
|
+
console.log("Use --force to confirm deletion.");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const url = new URL(`/api/roadmaps/${roadmapId}/phases`, config.apiUrl);
|
|
238
|
+
url.searchParams.set("phaseId", phaseId);
|
|
239
|
+
const res = await fetch(url.toString(), {
|
|
240
|
+
method: "DELETE",
|
|
241
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
242
|
+
});
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
if (res.status === 404) {
|
|
245
|
+
console.error(`Error: Roadmap or phase not found`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
249
|
+
console.error(`Error: API returned ${res.status}`, errorBody.error || "");
|
|
250
|
+
}
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const roadmap = await res.json();
|
|
254
|
+
console.log(`✓ Phase deleted`);
|
|
255
|
+
console.log(` Remaining phases: ${roadmap.phases.length}`);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error("Error deleting phase:", error);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
155
262
|
// husky roadmap add-feature <roadmapId> <phaseId> <title>
|
|
156
263
|
roadmapCommand
|
|
157
264
|
.command("add-feature <roadmapId> <phaseId> <title>")
|
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) {
|
|
@@ -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();
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ const program = new Command();
|
|
|
23
23
|
program
|
|
24
24
|
.name("husky")
|
|
25
25
|
.description("CLI for Huskyv0 Task Orchestration with Claude Agent")
|
|
26
|
-
.version("0.
|
|
26
|
+
.version("0.6.0");
|
|
27
27
|
program.addCommand(taskCommand);
|
|
28
28
|
program.addCommand(configCommand);
|
|
29
29
|
program.addCommand(agentCommand);
|
|
@@ -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
|
+
}
|