@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,2 @@
1
+ import { Command } from "commander";
2
+ export declare const julesCommand: Command;
@@ -0,0 +1,593 @@
1
+ import { Command } from "commander";
2
+ import { getConfig } from "./config.js";
3
+ import * as readline from "readline";
4
+ export const julesCommand = new Command("jules")
5
+ .description("Manage Jules AI coding sessions");
6
+ // Helper: Ensure API is configured
7
+ function ensureConfig() {
8
+ const config = getConfig();
9
+ if (!config.apiUrl) {
10
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
11
+ process.exit(1);
12
+ }
13
+ return config;
14
+ }
15
+ // Helper: Prompt for confirmation
16
+ async function confirm(message) {
17
+ const rl = readline.createInterface({
18
+ input: process.stdin,
19
+ output: process.stdout,
20
+ });
21
+ return new Promise((resolve) => {
22
+ rl.question(`${message} (y/N): `, (answer) => {
23
+ rl.close();
24
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
25
+ });
26
+ });
27
+ }
28
+ // Status display config
29
+ const STATUS_CONFIG = {
30
+ pending: { label: "Pending", icon: "[...]" },
31
+ planning: { label: "Planning", icon: "[P]" },
32
+ awaiting_approval: { label: "Awaiting Approval", icon: "[!]" },
33
+ executing: { label: "Executing", icon: "[>]" },
34
+ completed: { label: "Completed", icon: "[OK]" },
35
+ failed: { label: "Failed", icon: "[X]" },
36
+ cancelled: { label: "Cancelled", icon: "[-]" },
37
+ };
38
+ // husky jules list
39
+ julesCommand
40
+ .command("list")
41
+ .description("List all Jules sessions")
42
+ .option("--json", "Output as JSON")
43
+ .option("--status <status>", "Filter by status (pending, planning, awaiting_approval, executing, completed, failed, cancelled)")
44
+ .option("--source <source>", "Filter by source ID")
45
+ .action(async (options) => {
46
+ const config = ensureConfig();
47
+ try {
48
+ const url = new URL("/api/jules-sessions", config.apiUrl);
49
+ if (options.status) {
50
+ url.searchParams.set("status", options.status);
51
+ }
52
+ const res = await fetch(url.toString(), {
53
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
54
+ });
55
+ if (!res.ok) {
56
+ throw new Error(`API error: ${res.status}`);
57
+ }
58
+ const data = await res.json();
59
+ let sessions = data.sessions || [];
60
+ // Filter by source if specified
61
+ if (options.source) {
62
+ sessions = sessions.filter((s) => s.sourceId === options.source || s.sourceName?.includes(options.source));
63
+ }
64
+ if (options.json) {
65
+ console.log(JSON.stringify(sessions, null, 2));
66
+ }
67
+ else {
68
+ printSessions(sessions);
69
+ }
70
+ }
71
+ catch (error) {
72
+ console.error("Error fetching Jules sessions:", error);
73
+ process.exit(1);
74
+ }
75
+ });
76
+ // husky jules create
77
+ julesCommand
78
+ .command("create")
79
+ .description("Create a new Jules session")
80
+ .requiredOption("-n, --name <name>", "Session name")
81
+ .requiredOption("--source <source>", "Source ID (use 'jules sources' to list available sources)")
82
+ .option("--prompt <prompt>", "Task prompt/description")
83
+ .option("--branch <branch>", "Git branch to work on")
84
+ .option("--source-name <sourceName>", "Source name (cached for display)")
85
+ .option("--task <taskId>", "Link to a task ID")
86
+ .option("--workflow <workflowId>", "Link to a workflow ID")
87
+ .option("--no-approval", "Skip plan approval (auto-approve)")
88
+ .option("--json", "Output as JSON")
89
+ .action(async (options) => {
90
+ const config = ensureConfig();
91
+ if (!options.prompt) {
92
+ console.error("Error: --prompt is required. Provide a description of what Jules should do.");
93
+ process.exit(1);
94
+ }
95
+ try {
96
+ const res = await fetch(`${config.apiUrl}/api/jules-sessions`, {
97
+ method: "POST",
98
+ headers: {
99
+ "Content-Type": "application/json",
100
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
101
+ },
102
+ body: JSON.stringify({
103
+ name: options.name,
104
+ prompt: options.prompt,
105
+ sourceId: options.source,
106
+ sourceName: options.sourceName,
107
+ branch: options.branch,
108
+ taskId: options.task,
109
+ workflowId: options.workflow,
110
+ requirePlanApproval: options.approval !== false,
111
+ }),
112
+ });
113
+ if (!res.ok) {
114
+ const errorBody = await res.json().catch(() => ({}));
115
+ throw new Error(errorBody.error || `API error: ${res.status}`);
116
+ }
117
+ const session = await res.json();
118
+ if (options.json) {
119
+ console.log(JSON.stringify(session, null, 2));
120
+ }
121
+ else {
122
+ console.log(`Created Jules session: ${session.name}`);
123
+ console.log(` ID: ${session.id}`);
124
+ console.log(` Jules Session ID: ${session.julesSessionId}`);
125
+ console.log(` Status: ${STATUS_CONFIG[session.status].label}`);
126
+ console.log(` Source: ${session.sourceName || session.sourceId}`);
127
+ if (session.branch) {
128
+ console.log(` Branch: ${session.branch}`);
129
+ }
130
+ console.log(` Plan Approval: ${session.requirePlanApproval ? "Required" : "Auto-approve"}`);
131
+ }
132
+ }
133
+ catch (error) {
134
+ console.error("Error creating Jules session:", error);
135
+ process.exit(1);
136
+ }
137
+ });
138
+ // husky jules get <id>
139
+ julesCommand
140
+ .command("get <id>")
141
+ .description("Get Jules session details")
142
+ .option("--json", "Output as JSON")
143
+ .option("--activities", "Include activities in output")
144
+ .action(async (id, options) => {
145
+ const config = ensureConfig();
146
+ try {
147
+ const res = await fetch(`${config.apiUrl}/api/jules-sessions/${id}`, {
148
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
149
+ });
150
+ if (!res.ok) {
151
+ if (res.status === 404) {
152
+ console.error(`Error: Jules session ${id} not found`);
153
+ }
154
+ else {
155
+ console.error(`Error: API returned ${res.status}`);
156
+ }
157
+ process.exit(1);
158
+ }
159
+ const session = await res.json();
160
+ if (options.json) {
161
+ if (!options.activities && session.activities) {
162
+ delete session.activities;
163
+ }
164
+ console.log(JSON.stringify(session, null, 2));
165
+ }
166
+ else {
167
+ printSessionDetail(session, options.activities);
168
+ }
169
+ }
170
+ catch (error) {
171
+ console.error("Error fetching Jules session:", error);
172
+ process.exit(1);
173
+ }
174
+ });
175
+ // husky jules update <id>
176
+ julesCommand
177
+ .command("update <id>")
178
+ .description("Update a Jules session")
179
+ .option("-n, --name <name>", "New name")
180
+ .option("--status <status>", "New status")
181
+ .option("--json", "Output as JSON")
182
+ .action(async (id, options) => {
183
+ const config = ensureConfig();
184
+ // Build update payload
185
+ const updateData = {};
186
+ if (options.name)
187
+ updateData.name = options.name;
188
+ if (options.status) {
189
+ const validStatuses = ["pending", "planning", "awaiting_approval", "executing", "completed", "failed", "cancelled"];
190
+ if (!validStatuses.includes(options.status)) {
191
+ console.error(`Error: Invalid status "${options.status}". Must be one of: ${validStatuses.join(", ")}`);
192
+ process.exit(1);
193
+ }
194
+ updateData.status = options.status;
195
+ }
196
+ if (Object.keys(updateData).length === 0) {
197
+ console.error("Error: No update options provided. Use -n or --status");
198
+ process.exit(1);
199
+ }
200
+ try {
201
+ const res = await fetch(`${config.apiUrl}/api/jules-sessions/${id}`, {
202
+ method: "PATCH",
203
+ headers: {
204
+ "Content-Type": "application/json",
205
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
206
+ },
207
+ body: JSON.stringify(updateData),
208
+ });
209
+ if (!res.ok) {
210
+ if (res.status === 404) {
211
+ console.error(`Error: Jules session ${id} not found`);
212
+ }
213
+ else {
214
+ const errorBody = await res.json().catch(() => ({}));
215
+ console.error(`Error: ${errorBody.error || `API returned ${res.status}`}`);
216
+ }
217
+ process.exit(1);
218
+ }
219
+ const session = await res.json();
220
+ if (options.json) {
221
+ console.log(JSON.stringify(session, null, 2));
222
+ }
223
+ else {
224
+ console.log(`Jules session updated successfully`);
225
+ console.log(` Name: ${session.name}`);
226
+ console.log(` Status: ${STATUS_CONFIG[session.status].label}`);
227
+ }
228
+ }
229
+ catch (error) {
230
+ console.error("Error updating Jules session:", error);
231
+ process.exit(1);
232
+ }
233
+ });
234
+ // husky jules delete <id>
235
+ julesCommand
236
+ .command("delete <id>")
237
+ .description("Delete a Jules session")
238
+ .option("--force", "Skip confirmation")
239
+ .option("--json", "Output as JSON")
240
+ .action(async (id, options) => {
241
+ const config = ensureConfig();
242
+ if (!options.force) {
243
+ // First fetch session details to show what will be deleted
244
+ try {
245
+ const getRes = await fetch(`${config.apiUrl}/api/jules-sessions/${id}`, {
246
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
247
+ });
248
+ if (!getRes.ok) {
249
+ if (getRes.status === 404) {
250
+ console.error(`Error: Jules session ${id} not found`);
251
+ }
252
+ else {
253
+ console.error(`Error: API returned ${getRes.status}`);
254
+ }
255
+ process.exit(1);
256
+ }
257
+ const session = await getRes.json();
258
+ const confirmed = await confirm(`Delete Jules session "${session.name}" (${id})?`);
259
+ if (!confirmed) {
260
+ console.log("Deletion cancelled.");
261
+ process.exit(0);
262
+ }
263
+ }
264
+ catch (error) {
265
+ console.error("Error fetching session:", error);
266
+ process.exit(1);
267
+ }
268
+ }
269
+ try {
270
+ const res = await fetch(`${config.apiUrl}/api/jules-sessions/${id}`, {
271
+ method: "DELETE",
272
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
273
+ });
274
+ if (!res.ok) {
275
+ if (res.status === 404) {
276
+ console.error(`Error: Jules session ${id} not found`);
277
+ }
278
+ else {
279
+ console.error(`Error: API returned ${res.status}`);
280
+ }
281
+ process.exit(1);
282
+ }
283
+ if (options.json) {
284
+ console.log(JSON.stringify({ deleted: true, id }, null, 2));
285
+ }
286
+ else {
287
+ console.log(`Jules session deleted`);
288
+ }
289
+ }
290
+ catch (error) {
291
+ console.error("Error deleting Jules session:", error);
292
+ process.exit(1);
293
+ }
294
+ });
295
+ // husky jules message <id>
296
+ julesCommand
297
+ .command("message <id>")
298
+ .description("Send a message to a Jules session")
299
+ .requiredOption("-m, --message <message>", "Message content")
300
+ .option("--json", "Output as JSON")
301
+ .action(async (id, options) => {
302
+ const config = ensureConfig();
303
+ try {
304
+ const res = await fetch(`${config.apiUrl}/api/jules-sessions/${id}/message`, {
305
+ method: "POST",
306
+ headers: {
307
+ "Content-Type": "application/json",
308
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
309
+ },
310
+ body: JSON.stringify({
311
+ prompt: options.message,
312
+ }),
313
+ });
314
+ if (!res.ok) {
315
+ if (res.status === 404) {
316
+ console.error(`Error: Jules session ${id} not found`);
317
+ }
318
+ else {
319
+ const errorBody = await res.json().catch(() => ({}));
320
+ console.error(`Error: ${errorBody.error || `API returned ${res.status}`}`);
321
+ }
322
+ process.exit(1);
323
+ }
324
+ const session = await res.json();
325
+ if (options.json) {
326
+ console.log(JSON.stringify(session, null, 2));
327
+ }
328
+ else {
329
+ console.log(`Message sent to Jules session`);
330
+ console.log(` Status: ${STATUS_CONFIG[session.status].label}`);
331
+ if (session.planSummary) {
332
+ console.log(` Plan: ${session.planSummary.substring(0, 100)}...`);
333
+ }
334
+ }
335
+ }
336
+ catch (error) {
337
+ console.error("Error sending message:", error);
338
+ process.exit(1);
339
+ }
340
+ });
341
+ // husky jules approve <id>
342
+ julesCommand
343
+ .command("approve <id>")
344
+ .description("Approve a Jules session plan")
345
+ .option("--feedback <feedback>", "Optional feedback for the approval")
346
+ .option("--reject", "Reject the plan instead of approving")
347
+ .option("--json", "Output as JSON")
348
+ .action(async (id, options) => {
349
+ const config = ensureConfig();
350
+ try {
351
+ const res = await fetch(`${config.apiUrl}/api/jules-sessions/${id}/approve`, {
352
+ method: "POST",
353
+ headers: {
354
+ "Content-Type": "application/json",
355
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
356
+ },
357
+ body: JSON.stringify({
358
+ approved: !options.reject,
359
+ feedback: options.feedback,
360
+ }),
361
+ });
362
+ if (!res.ok) {
363
+ if (res.status === 404) {
364
+ console.error(`Error: Jules session ${id} not found`);
365
+ }
366
+ else {
367
+ const errorBody = await res.json().catch(() => ({}));
368
+ console.error(`Error: ${errorBody.error || `API returned ${res.status}`}`);
369
+ }
370
+ process.exit(1);
371
+ }
372
+ const session = await res.json();
373
+ if (options.json) {
374
+ console.log(JSON.stringify(session, null, 2));
375
+ }
376
+ else {
377
+ if (options.reject) {
378
+ console.log(`Jules plan rejected`);
379
+ }
380
+ else {
381
+ console.log(`Jules plan approved`);
382
+ }
383
+ console.log(` Status: ${STATUS_CONFIG[session.status].label}`);
384
+ }
385
+ }
386
+ catch (error) {
387
+ console.error("Error processing approval:", error);
388
+ process.exit(1);
389
+ }
390
+ });
391
+ // husky jules activities <id>
392
+ julesCommand
393
+ .command("activities <id>")
394
+ .description("Get session activities")
395
+ .option("--json", "Output as JSON")
396
+ .option("--limit <n>", "Limit number of activities", "20")
397
+ .option("--sync", "Sync activities from Jules API")
398
+ .action(async (id, options) => {
399
+ const config = ensureConfig();
400
+ try {
401
+ const url = new URL(`/api/jules-sessions/${id}/activities`, config.apiUrl);
402
+ if (options.sync) {
403
+ url.searchParams.set("sync", "true");
404
+ }
405
+ const res = await fetch(url.toString(), {
406
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
407
+ });
408
+ if (!res.ok) {
409
+ if (res.status === 404) {
410
+ console.error(`Error: Jules session ${id} not found`);
411
+ }
412
+ else {
413
+ console.error(`Error: API returned ${res.status}`);
414
+ }
415
+ process.exit(1);
416
+ }
417
+ const data = await res.json();
418
+ let activities = data.activities || [];
419
+ // Apply limit
420
+ const limit = parseInt(options.limit, 10);
421
+ if (limit > 0 && activities.length > limit) {
422
+ activities = activities.slice(-limit);
423
+ }
424
+ if (options.json) {
425
+ console.log(JSON.stringify(activities, null, 2));
426
+ }
427
+ else {
428
+ printActivities(activities);
429
+ }
430
+ }
431
+ catch (error) {
432
+ console.error("Error fetching activities:", error);
433
+ process.exit(1);
434
+ }
435
+ });
436
+ // husky jules sources
437
+ julesCommand
438
+ .command("sources")
439
+ .description("List available Jules sources (GitHub repositories)")
440
+ .option("--json", "Output as JSON")
441
+ .action(async (options) => {
442
+ const config = ensureConfig();
443
+ try {
444
+ const res = await fetch(`${config.apiUrl}/api/jules-sessions/sources`, {
445
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
446
+ });
447
+ if (!res.ok) {
448
+ throw new Error(`API error: ${res.status}`);
449
+ }
450
+ const data = await res.json();
451
+ if (!data.configured) {
452
+ console.error("Error: Jules API is not configured. Add JULES_API_KEY to Secret Manager.");
453
+ process.exit(1);
454
+ }
455
+ const sources = data.sources || [];
456
+ if (options.json) {
457
+ console.log(JSON.stringify(sources, null, 2));
458
+ }
459
+ else {
460
+ printSources(sources);
461
+ }
462
+ }
463
+ catch (error) {
464
+ console.error("Error fetching Jules sources:", error);
465
+ process.exit(1);
466
+ }
467
+ });
468
+ // Print helpers
469
+ function printSessions(sessions) {
470
+ if (sessions.length === 0) {
471
+ console.log("\n No Jules sessions found.");
472
+ console.log(" Create one with: husky jules create -n <name> --source <sourceId> --prompt <prompt>\n");
473
+ return;
474
+ }
475
+ console.log("\n JULES SESSIONS");
476
+ console.log(" " + "-".repeat(100));
477
+ console.log(` ${"ID".padEnd(24)} ${"NAME".padEnd(30)} ${"STATUS".padEnd(18)} ${"SOURCE".padEnd(20)} CREATED`);
478
+ console.log(" " + "-".repeat(100));
479
+ for (const session of sessions) {
480
+ const truncatedName = session.name.length > 28 ? session.name.substring(0, 25) + "..." : session.name;
481
+ const sourceName = session.sourceName || session.sourceId;
482
+ const truncatedSource = sourceName.length > 18 ? sourceName.substring(0, 15) + "..." : sourceName;
483
+ const statusLabel = STATUS_CONFIG[session.status]?.label || session.status;
484
+ const createdAt = new Date(session.createdAt).toLocaleDateString();
485
+ console.log(` ${session.id.padEnd(24)} ${truncatedName.padEnd(30)} ${statusLabel.padEnd(18)} ${truncatedSource.padEnd(20)} ${createdAt}`);
486
+ }
487
+ console.log("");
488
+ }
489
+ function printSessionDetail(session, showActivities) {
490
+ const statusConfig = STATUS_CONFIG[session.status];
491
+ console.log(`\n Jules Session: ${session.name}`);
492
+ console.log(" " + "-".repeat(60));
493
+ console.log(` ID: ${session.id}`);
494
+ console.log(` Jules Session ID: ${session.julesSessionId}`);
495
+ console.log(` Status: ${statusConfig.icon} ${statusConfig.label}`);
496
+ console.log(` Source: ${session.sourceName || session.sourceId}`);
497
+ if (session.branch) {
498
+ console.log(` Branch: ${session.branch}`);
499
+ }
500
+ console.log(` Plan Approval: ${session.requirePlanApproval ? "Required" : "Auto-approve"}`);
501
+ console.log(`\n Prompt:`);
502
+ const promptLines = session.prompt.split("\n").slice(0, 5);
503
+ for (const line of promptLines) {
504
+ console.log(` ${line.substring(0, 80)}`);
505
+ }
506
+ if (session.prompt.split("\n").length > 5) {
507
+ console.log(` ...`);
508
+ }
509
+ if (session.planSummary) {
510
+ console.log(`\n Plan Summary:`);
511
+ console.log(` ${session.planSummary.substring(0, 200)}`);
512
+ if (session.planSummary.length > 200) {
513
+ console.log(` ...`);
514
+ }
515
+ }
516
+ if (session.prUrl) {
517
+ console.log(`\n Pull Request:`);
518
+ console.log(` URL: ${session.prUrl}`);
519
+ if (session.prNumber) {
520
+ console.log(` Number: #${session.prNumber}`);
521
+ }
522
+ if (session.commitSha) {
523
+ console.log(` Commit: ${session.commitSha.substring(0, 7)}`);
524
+ }
525
+ }
526
+ if (session.lastError) {
527
+ console.log(`\n Last Error:`);
528
+ console.log(` ${session.lastError.substring(0, 200)}`);
529
+ }
530
+ if (session.taskId) {
531
+ console.log(`\n Linked Task: ${session.taskId}`);
532
+ }
533
+ if (session.workflowId) {
534
+ console.log(` Linked Workflow: ${session.workflowId}`);
535
+ }
536
+ console.log(`\n Timestamps:`);
537
+ console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`);
538
+ console.log(` Updated: ${new Date(session.updatedAt).toLocaleString()}`);
539
+ if (session.startedAt) {
540
+ console.log(` Started: ${new Date(session.startedAt).toLocaleString()}`);
541
+ }
542
+ if (session.completedAt) {
543
+ console.log(` Completed: ${new Date(session.completedAt).toLocaleString()}`);
544
+ }
545
+ if (showActivities && session.activities && session.activities.length > 0) {
546
+ console.log(`\n Activities (${session.activities.length}):`);
547
+ console.log(" " + "-".repeat(60));
548
+ printActivities(session.activities.slice(-10));
549
+ }
550
+ console.log("");
551
+ }
552
+ function printActivities(activities) {
553
+ if (activities.length === 0) {
554
+ console.log("\n No activities found.\n");
555
+ return;
556
+ }
557
+ const typeIcons = {
558
+ plan: "[PLAN]",
559
+ message: "[MSG]",
560
+ code_change: "[CODE]",
561
+ pr_created: "[PR]",
562
+ error: "[ERR]",
563
+ };
564
+ console.log("\n ACTIVITIES");
565
+ console.log(" " + "-".repeat(80));
566
+ for (const activity of activities) {
567
+ const icon = typeIcons[activity.type] || `[${activity.type.toUpperCase()}]`;
568
+ const timestamp = new Date(activity.timestamp).toLocaleString();
569
+ const content = activity.content.substring(0, 60);
570
+ console.log(` ${timestamp} ${icon.padEnd(8)} ${content}`);
571
+ if (activity.content.length > 60) {
572
+ console.log(` ${activity.content.substring(60, 120)}...`);
573
+ }
574
+ }
575
+ console.log("");
576
+ }
577
+ function printSources(sources) {
578
+ if (sources.length === 0) {
579
+ console.log("\n No Jules sources configured.");
580
+ console.log(" Connect a GitHub repository in the Jules dashboard.\n");
581
+ return;
582
+ }
583
+ console.log("\n JULES SOURCES (GitHub Repositories)");
584
+ console.log(" " + "-".repeat(90));
585
+ console.log(` ${"ID".padEnd(24)} ${"FULL NAME".padEnd(35)} ${"DEFAULT BRANCH".padEnd(15)} CONNECTED`);
586
+ console.log(" " + "-".repeat(90));
587
+ for (const source of sources) {
588
+ const truncatedName = source.fullName.length > 33 ? source.fullName.substring(0, 30) + "..." : source.fullName;
589
+ const connected = source.connected ? "Yes" : "No";
590
+ console.log(` ${source.id.padEnd(24)} ${truncatedName.padEnd(35)} ${source.defaultBranch.padEnd(15)} ${connected}`);
591
+ }
592
+ console.log("");
593
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const processCommand: Command;