@simonfestl/husky-cli 0.3.0 → 0.5.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.
@@ -0,0 +1,340 @@
1
+ import { Command } from "commander";
2
+ import { getConfig } from "./config.js";
3
+ export const ideaCommand = new Command("idea")
4
+ .description("Manage ideas");
5
+ // Helper: Ensure API is configured
6
+ function ensureConfig() {
7
+ const config = getConfig();
8
+ if (!config.apiUrl) {
9
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
10
+ process.exit(1);
11
+ }
12
+ return config;
13
+ }
14
+ // husky idea list
15
+ ideaCommand
16
+ .command("list")
17
+ .description("List all ideas")
18
+ .option("--status <status>", "Filter by status (draft, active, archived, converted)")
19
+ .option("--json", "Output as JSON")
20
+ .action(async (options) => {
21
+ const config = ensureConfig();
22
+ try {
23
+ const url = new URL("/api/ideas", config.apiUrl);
24
+ if (options.status) {
25
+ url.searchParams.set("status", options.status);
26
+ }
27
+ const res = await fetch(url.toString(), {
28
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
29
+ });
30
+ if (!res.ok) {
31
+ throw new Error(`API error: ${res.status}`);
32
+ }
33
+ const ideas = await res.json();
34
+ if (options.json) {
35
+ console.log(JSON.stringify(ideas, null, 2));
36
+ }
37
+ else {
38
+ printIdeas(ideas);
39
+ }
40
+ }
41
+ catch (error) {
42
+ console.error("Error fetching ideas:", error);
43
+ process.exit(1);
44
+ }
45
+ });
46
+ // husky idea create <title>
47
+ ideaCommand
48
+ .command("create <title>")
49
+ .description("Create a new idea")
50
+ .option("-d, --description <description>", "Idea description")
51
+ .option("--category <category>", "Idea category")
52
+ .option("--json", "Output as JSON")
53
+ .action(async (title, options) => {
54
+ const config = ensureConfig();
55
+ try {
56
+ const res = await fetch(`${config.apiUrl}/api/ideas`, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
61
+ },
62
+ body: JSON.stringify({
63
+ title,
64
+ description: options.description,
65
+ category: options.category,
66
+ status: "draft",
67
+ }),
68
+ });
69
+ if (!res.ok) {
70
+ throw new Error(`API error: ${res.status}`);
71
+ }
72
+ const idea = await res.json();
73
+ if (options.json) {
74
+ console.log(JSON.stringify(idea, null, 2));
75
+ }
76
+ else {
77
+ console.log(`✓ Created idea: ${idea.title}`);
78
+ console.log(` ID: ${idea.id}`);
79
+ }
80
+ }
81
+ catch (error) {
82
+ console.error("Error creating idea:", error);
83
+ process.exit(1);
84
+ }
85
+ });
86
+ // husky idea get <id>
87
+ ideaCommand
88
+ .command("get <id>")
89
+ .description("Get idea details")
90
+ .option("--json", "Output as JSON")
91
+ .action(async (id, options) => {
92
+ const config = ensureConfig();
93
+ try {
94
+ const res = await fetch(`${config.apiUrl}/api/ideas/${id}`, {
95
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
96
+ });
97
+ if (!res.ok) {
98
+ if (res.status === 404) {
99
+ console.error(`Error: Idea ${id} not found`);
100
+ }
101
+ else {
102
+ console.error(`Error: API returned ${res.status}`);
103
+ }
104
+ process.exit(1);
105
+ }
106
+ const idea = await res.json();
107
+ if (options.json) {
108
+ console.log(JSON.stringify(idea, null, 2));
109
+ }
110
+ else {
111
+ printIdeaDetail(idea);
112
+ }
113
+ }
114
+ catch (error) {
115
+ console.error("Error fetching idea:", error);
116
+ process.exit(1);
117
+ }
118
+ });
119
+ // husky idea update <id>
120
+ ideaCommand
121
+ .command("update <id>")
122
+ .description("Update an idea")
123
+ .option("-t, --title <title>", "New title")
124
+ .option("-d, --description <description>", "New description")
125
+ .option("--status <status>", "New status (draft, active, archived, converted)")
126
+ .option("--category <category>", "New category")
127
+ .option("--json", "Output as JSON")
128
+ .action(async (id, options) => {
129
+ const config = ensureConfig();
130
+ // Build update payload
131
+ const updateData = {};
132
+ if (options.title) {
133
+ updateData.title = options.title;
134
+ }
135
+ if (options.description) {
136
+ updateData.description = options.description;
137
+ }
138
+ if (options.status) {
139
+ const validStatuses = ["draft", "active", "archived", "converted"];
140
+ if (!validStatuses.includes(options.status)) {
141
+ console.error(`Error: Invalid status "${options.status}". Must be one of: ${validStatuses.join(", ")}`);
142
+ process.exit(1);
143
+ }
144
+ updateData.status = options.status;
145
+ }
146
+ if (options.category) {
147
+ updateData.category = options.category;
148
+ }
149
+ if (Object.keys(updateData).length === 0) {
150
+ console.error("Error: No update options provided. Use -t, -d, --status, or --category");
151
+ process.exit(1);
152
+ }
153
+ try {
154
+ const res = await fetch(`${config.apiUrl}/api/ideas/${id}`, {
155
+ method: "PATCH",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
159
+ },
160
+ body: JSON.stringify(updateData),
161
+ });
162
+ if (!res.ok) {
163
+ if (res.status === 404) {
164
+ console.error(`Error: Idea ${id} not found`);
165
+ }
166
+ else {
167
+ const errorBody = await res.json().catch(() => ({}));
168
+ console.error(`Error: API returned ${res.status}`, errorBody.error || "");
169
+ }
170
+ process.exit(1);
171
+ }
172
+ const idea = await res.json();
173
+ if (options.json) {
174
+ console.log(JSON.stringify(idea, null, 2));
175
+ }
176
+ else {
177
+ console.log(`✓ Idea updated successfully`);
178
+ console.log(` Title: ${idea.title}`);
179
+ console.log(` Status: ${idea.status}`);
180
+ if (idea.category) {
181
+ console.log(` Category: ${idea.category}`);
182
+ }
183
+ }
184
+ }
185
+ catch (error) {
186
+ console.error("Error updating idea:", error);
187
+ process.exit(1);
188
+ }
189
+ });
190
+ // husky idea delete <id>
191
+ ideaCommand
192
+ .command("delete <id>")
193
+ .description("Delete an idea")
194
+ .option("--force", "Skip confirmation")
195
+ .option("--json", "Output as JSON")
196
+ .action(async (id, options) => {
197
+ const config = ensureConfig();
198
+ if (!options.force) {
199
+ console.log("Warning: This will permanently delete the idea.");
200
+ console.log("Use --force to confirm deletion.");
201
+ process.exit(1);
202
+ }
203
+ try {
204
+ const res = await fetch(`${config.apiUrl}/api/ideas/${id}`, {
205
+ method: "DELETE",
206
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
207
+ });
208
+ if (!res.ok) {
209
+ if (res.status === 404) {
210
+ console.error(`Error: Idea ${id} not found`);
211
+ }
212
+ else {
213
+ console.error(`Error: API returned ${res.status}`);
214
+ }
215
+ process.exit(1);
216
+ }
217
+ if (options.json) {
218
+ console.log(JSON.stringify({ deleted: true, id }, null, 2));
219
+ }
220
+ else {
221
+ console.log(`✓ Idea deleted`);
222
+ }
223
+ }
224
+ catch (error) {
225
+ console.error("Error deleting idea:", error);
226
+ process.exit(1);
227
+ }
228
+ });
229
+ // husky idea convert <id>
230
+ ideaCommand
231
+ .command("convert <id>")
232
+ .description("Convert an idea to a task")
233
+ .option("--priority <priority>", "Task priority (low, medium, high)", "medium")
234
+ .option("--assignee <assignee>", "Assign to agent or user")
235
+ .option("--json", "Output as JSON")
236
+ .action(async (id, options) => {
237
+ const config = ensureConfig();
238
+ // Validate priority
239
+ const validPriorities = ["low", "medium", "high"];
240
+ if (!validPriorities.includes(options.priority)) {
241
+ console.error(`Error: Invalid priority "${options.priority}". Must be one of: ${validPriorities.join(", ")}`);
242
+ process.exit(1);
243
+ }
244
+ try {
245
+ const res = await fetch(`${config.apiUrl}/api/ideas/${id}/convert`, {
246
+ method: "POST",
247
+ headers: {
248
+ "Content-Type": "application/json",
249
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
250
+ },
251
+ body: JSON.stringify({
252
+ priority: options.priority,
253
+ assignee: options.assignee,
254
+ }),
255
+ });
256
+ if (!res.ok) {
257
+ if (res.status === 404) {
258
+ console.error(`Error: Idea ${id} not found`);
259
+ }
260
+ else {
261
+ const errorBody = await res.json().catch(() => ({}));
262
+ console.error(`Error: API returned ${res.status}`, errorBody.error || "");
263
+ }
264
+ process.exit(1);
265
+ }
266
+ const result = await res.json();
267
+ if (options.json) {
268
+ console.log(JSON.stringify(result, null, 2));
269
+ }
270
+ else {
271
+ console.log(`✓ Idea converted to task`);
272
+ console.log(` Task ID: ${result.taskId}`);
273
+ console.log(` Title: ${result.title}`);
274
+ console.log(` Priority: ${result.priority}`);
275
+ if (result.assignee) {
276
+ console.log(` Assignee: ${result.assignee}`);
277
+ }
278
+ }
279
+ }
280
+ catch (error) {
281
+ console.error("Error converting idea:", error);
282
+ process.exit(1);
283
+ }
284
+ });
285
+ function printIdeas(ideas) {
286
+ if (ideas.length === 0) {
287
+ console.log("\n No ideas found.");
288
+ console.log(" Create one with: husky idea create <title>\n");
289
+ return;
290
+ }
291
+ console.log("\n IDEAS");
292
+ console.log(" " + "─".repeat(70));
293
+ console.log(` ${"ID".padEnd(24)} ${"TITLE".padEnd(30)} ${"STATUS".padEnd(12)} CATEGORY`);
294
+ console.log(" " + "─".repeat(70));
295
+ for (const idea of ideas) {
296
+ const statusIcon = getStatusIcon(idea.status);
297
+ const truncatedTitle = idea.title.length > 28 ? idea.title.substring(0, 25) + "..." : idea.title;
298
+ const category = idea.category || "-";
299
+ console.log(` ${idea.id.padEnd(24)} ${truncatedTitle.padEnd(30)} ${statusIcon} ${idea.status.padEnd(10)} ${category}`);
300
+ }
301
+ // Summary by status
302
+ const draftCount = ideas.filter((i) => i.status === "draft").length;
303
+ const activeCount = ideas.filter((i) => i.status === "active").length;
304
+ const archivedCount = ideas.filter((i) => i.status === "archived").length;
305
+ const convertedCount = ideas.filter((i) => i.status === "converted").length;
306
+ console.log(" " + "─".repeat(70));
307
+ console.log(`\n Summary: ${ideas.length} total`);
308
+ console.log(` · Draft: ${draftCount} ○ Active: ${activeCount} ▷ Converted: ${convertedCount} ✗ Archived: ${archivedCount}`);
309
+ console.log("");
310
+ }
311
+ function printIdeaDetail(idea) {
312
+ console.log(`\n Idea: ${idea.title}`);
313
+ console.log(" " + "─".repeat(50));
314
+ console.log(` ID: ${idea.id}`);
315
+ console.log(` Status: ${idea.status}`);
316
+ if (idea.category) {
317
+ console.log(` Category: ${idea.category}`);
318
+ }
319
+ if (idea.description) {
320
+ console.log(` Description:`);
321
+ console.log(` ${idea.description}`);
322
+ }
323
+ console.log(` Created: ${new Date(idea.createdAt).toLocaleString()}`);
324
+ console.log(` Updated: ${new Date(idea.updatedAt).toLocaleString()}`);
325
+ console.log("");
326
+ }
327
+ function getStatusIcon(status) {
328
+ switch (status) {
329
+ case "draft":
330
+ return "·";
331
+ case "active":
332
+ return "○";
333
+ case "converted":
334
+ return "▷";
335
+ case "archived":
336
+ return "✗";
337
+ default:
338
+ return " ";
339
+ }
340
+ }
@@ -0,0 +1 @@
1
+ export declare function runInteractiveMode(): Promise<void>;