@eve-horizon/cli 0.2.11 → 0.2.13

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.
@@ -1,2537 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleJob = handleJob;
4
- const child_process_1 = require("child_process");
5
- const args_1 = require("../lib/args");
6
- const client_1 = require("../lib/client");
7
- const output_1 = require("../lib/output");
8
- // ============================================================================
9
- // Main Handler
10
- // ============================================================================
11
- async function handleJob(subcommand, positionals, flags, context) {
12
- const json = Boolean(flags.json);
13
- switch (subcommand) {
14
- case 'create':
15
- return handleCreate(flags, context, json);
16
- case 'list':
17
- return handleList(flags, context, json);
18
- case 'ready':
19
- return handleReady(flags, context, json);
20
- case 'blocked':
21
- return handleBlocked(flags, context, json);
22
- case 'show':
23
- return handleShow(positionals, flags, context, json);
24
- case 'current':
25
- return handleCurrent(positionals, flags, context, json);
26
- case 'diagnose':
27
- return handleDiagnose(positionals, context, json);
28
- case 'tree':
29
- return handleTree(positionals, context, json);
30
- case 'update':
31
- return handleUpdate(positionals, flags, context, json);
32
- case 'close':
33
- return handleClose(positionals, flags, context, json);
34
- case 'cancel':
35
- return handleCancel(positionals, flags, context, json);
36
- case 'dep':
37
- return handleDep(positionals, flags, context, json);
38
- case 'claim':
39
- return handleClaim(positionals, flags, context, json);
40
- case 'release':
41
- return handleRelease(positionals, flags, context, json);
42
- case 'attempts':
43
- return handleAttempts(positionals, context, json);
44
- case 'logs':
45
- return handleLogs(positionals, flags, context, json);
46
- case 'submit':
47
- return handleSubmit(positionals, flags, context, json);
48
- case 'approve':
49
- return handleApprove(positionals, flags, context, json);
50
- case 'reject':
51
- return handleReject(positionals, flags, context, json);
52
- case 'result':
53
- return handleResult(positionals, flags, context);
54
- case 'follow':
55
- return handleFollow(positionals, flags, context);
56
- case 'wait':
57
- return handleWait(positionals, flags, context);
58
- case 'watch':
59
- return handleWatch(positionals, flags, context);
60
- case 'runner-logs':
61
- return handleRunnerLogs(positionals, flags, context);
62
- default:
63
- throw new Error('Usage: eve jobs <create|list|ready|blocked|show|current|diagnose|tree|update|close|cancel|dep|claim|release|attempts|logs|submit|approve|reject|result|follow|wait|watch|runner-logs>');
64
- }
65
- }
66
- // ============================================================================
67
- // Subcommand Handlers
68
- // ============================================================================
69
- /**
70
- * eve jobs create --project=X --description="..." [--title="..."] [--parent=X] [--type=task] [--priority=2] [--phase=ready] [--review=none|human|agent]
71
- *
72
- * --description is required (the work prompt sent to the harness)
73
- * --title is optional (defaults to first 64 chars of description's first line)
74
- *
75
- * Scheduling hints (optional, used when scheduler claims the job):
76
- * --harness=mclaude:fast Preferred harness (with optional :variant)
77
- * --worker-type=default Worker type preference
78
- * --permission=auto_edit Permission policy (default, auto_edit, yolo)
79
- * --timeout=3600 Execution timeout in seconds
80
- *
81
- * Environment options:
82
- * --env=<name> Environment name for persistent execution
83
- * --execution-mode=<mode> Execution mode: 'persistent' or 'ephemeral' (default: persistent)
84
- *
85
- * Inline execution:
86
- * --claim Create and immediately claim the job for execution
87
- * --agent=<id> Agent ID for claim (default: $EVE_AGENT_ID or cli-user)
88
- *
89
- * Git controls (optional, override project/manifest defaults):
90
- * --git-ref=<ref> Target ref (branch, tag, or SHA)
91
- * --git-ref-policy=<policy> auto|env|project_default|explicit
92
- * --git-branch=<branch> Branch to create/checkout
93
- * --git-create-branch=<mode> never|if_missing|always
94
- * --git-commit=<policy> never|manual|auto|required
95
- * --git-commit-message=<template> Commit message template
96
- * --git-push=<policy> never|on_success|required
97
- * --git-remote=<remote> Remote to push to (default: origin)
98
- *
99
- * Workspace options (optional):
100
- * --workspace-mode=<mode> job|session|isolated (default: job)
101
- * --workspace-key=<key> Workspace key for session mode
102
- */
103
- async function handleCreate(flags, context, json) {
104
- const parentId = (0, args_1.getStringFlag)(flags, ['parent']);
105
- const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
106
- const description = (0, args_1.getStringFlag)(flags, ['description']);
107
- let title = (0, args_1.getStringFlag)(flags, ['title']);
108
- const issueType = (0, args_1.getStringFlag)(flags, ['type']) ?? 'task';
109
- const priority = (0, args_1.getStringFlag)(flags, ['priority']);
110
- const phase = (0, args_1.getStringFlag)(flags, ['phase']); // API defaults to 'ready'
111
- const review = (0, args_1.getStringFlag)(flags, ['review']) ?? 'none';
112
- const labels = (0, args_1.getStringFlag)(flags, ['labels']);
113
- const assignee = (0, args_1.getStringFlag)(flags, ['assignee']);
114
- const deferUntil = (0, args_1.getStringFlag)(flags, ['defer-until']);
115
- const dueAt = (0, args_1.getStringFlag)(flags, ['due-at']);
116
- // Scheduling hints
117
- const harness = (0, args_1.getStringFlag)(flags, ['harness']);
118
- const profile = (0, args_1.getStringFlag)(flags, ['profile']);
119
- const variant = (0, args_1.getStringFlag)(flags, ['variant']);
120
- const model = (0, args_1.getStringFlag)(flags, ['model']);
121
- const reasoning = (0, args_1.getStringFlag)(flags, ['reasoning']);
122
- const workerType = (0, args_1.getStringFlag)(flags, ['worker-type']);
123
- const permission = (0, args_1.getStringFlag)(flags, ['permission']);
124
- const timeout = (0, args_1.getStringFlag)(flags, ['timeout']);
125
- // Environment options
126
- const envName = (0, args_1.getStringFlag)(flags, ['env']);
127
- const executionMode = (0, args_1.getStringFlag)(flags, ['execution-mode']) ?? 'persistent';
128
- // Validate execution mode
129
- if (executionMode !== 'persistent' && executionMode !== 'ephemeral') {
130
- throw new Error('--execution-mode must be either "persistent" or "ephemeral"');
131
- }
132
- // Inline execution
133
- const shouldClaim = Boolean(flags.claim);
134
- const agentId = (0, args_1.getStringFlag)(flags, ['agent']) ?? process.env.EVE_AGENT_ID ?? 'cli-user';
135
- // For root jobs, project is required. For child jobs, project is inherited from parent.
136
- if (!parentId && !projectId) {
137
- throw new Error('Usage: eve jobs create --project=<id> --description="..." [options]\n' +
138
- ' eve jobs create --parent=<id> --description="..." [options]');
139
- }
140
- if (!description) {
141
- throw new Error('--description is required (the work prompt)');
142
- }
143
- // Auto-generate title from first line of description if not provided
144
- if (!title) {
145
- const firstLine = description.split('\n')[0].trim();
146
- title = firstLine.length > 64 ? firstLine.substring(0, 61) + '...' : firstLine;
147
- }
148
- const body = {
149
- title,
150
- description,
151
- issue_type: issueType,
152
- review_required: review,
153
- execution_mode: executionMode,
154
- };
155
- // Add environment name (can be null)
156
- body.env_name = envName ?? null;
157
- if (parentId) {
158
- body.parent_id = parentId;
159
- }
160
- if (phase) {
161
- body.phase = phase;
162
- }
163
- if (priority !== undefined) {
164
- body.priority = parseInt(priority, 10);
165
- }
166
- if (labels) {
167
- body.labels = labels.split(',').map((l) => l.trim());
168
- }
169
- if (assignee) {
170
- body.assignee = assignee;
171
- }
172
- if (deferUntil) {
173
- body.defer_until = deferUntil;
174
- }
175
- if (dueAt) {
176
- body.due_at = dueAt;
177
- }
178
- if (harness) {
179
- body.harness = harness;
180
- }
181
- if (profile) {
182
- body.harness_profile = profile;
183
- }
184
- if (variant || model || reasoning) {
185
- const allowedEfforts = new Set(['low', 'medium', 'high', 'x-high']);
186
- if (reasoning && !allowedEfforts.has(reasoning)) {
187
- throw new Error('--reasoning must be one of: low, medium, high, x-high');
188
- }
189
- body.harness_options = {
190
- ...(variant ? { variant } : {}),
191
- ...(model ? { model } : {}),
192
- ...(reasoning ? { reasoning_effort: reasoning } : {}),
193
- };
194
- }
195
- // Build hints object if any hint flags are provided
196
- const hints = {};
197
- if (workerType)
198
- hints.worker_type = workerType;
199
- if (permission)
200
- hints.permission_policy = permission;
201
- if (timeout)
202
- hints.timeout_seconds = parseInt(timeout, 10);
203
- if (Object.keys(hints).length > 0) {
204
- body.hints = hints;
205
- }
206
- // Git controls
207
- const gitRef = (0, args_1.getStringFlag)(flags, ['git-ref']);
208
- const gitRefPolicy = (0, args_1.getStringFlag)(flags, ['git-ref-policy']);
209
- const gitBranch = (0, args_1.getStringFlag)(flags, ['git-branch']);
210
- const gitCreateBranch = (0, args_1.getStringFlag)(flags, ['git-create-branch']);
211
- const gitCommit = (0, args_1.getStringFlag)(flags, ['git-commit']);
212
- const gitCommitMessage = (0, args_1.getStringFlag)(flags, ['git-commit-message']);
213
- const gitPush = (0, args_1.getStringFlag)(flags, ['git-push']);
214
- const gitRemote = (0, args_1.getStringFlag)(flags, ['git-remote']);
215
- // Validate git policies
216
- if (gitRefPolicy && !['auto', 'env', 'project_default', 'explicit'].includes(gitRefPolicy)) {
217
- throw new Error('--git-ref-policy must be one of: auto, env, project_default, explicit');
218
- }
219
- if (gitCreateBranch && !['never', 'if_missing', 'always'].includes(gitCreateBranch)) {
220
- throw new Error('--git-create-branch must be one of: never, if_missing, always');
221
- }
222
- if (gitCommit && !['never', 'manual', 'auto', 'required'].includes(gitCommit)) {
223
- throw new Error('--git-commit must be one of: never, manual, auto, required');
224
- }
225
- if (gitPush && !['never', 'on_success', 'required'].includes(gitPush)) {
226
- throw new Error('--git-push must be one of: never, on_success, required');
227
- }
228
- // Build git object if any git flags are provided
229
- const git = {};
230
- if (gitRef)
231
- git.ref = gitRef;
232
- if (gitRefPolicy)
233
- git.ref_policy = gitRefPolicy;
234
- if (gitBranch)
235
- git.branch = gitBranch;
236
- if (gitCreateBranch)
237
- git.create_branch = gitCreateBranch;
238
- if (gitCommit)
239
- git.commit = gitCommit;
240
- if (gitCommitMessage)
241
- git.commit_message = gitCommitMessage;
242
- if (gitPush)
243
- git.push = gitPush;
244
- if (gitRemote)
245
- git.remote = gitRemote;
246
- if (Object.keys(git).length > 0) {
247
- body.git = git;
248
- }
249
- // Workspace options
250
- const workspaceMode = (0, args_1.getStringFlag)(flags, ['workspace-mode']);
251
- const workspaceKey = (0, args_1.getStringFlag)(flags, ['workspace-key']);
252
- // Validate workspace mode
253
- if (workspaceMode && !['job', 'session', 'isolated'].includes(workspaceMode)) {
254
- throw new Error('--workspace-mode must be one of: job, session, isolated');
255
- }
256
- // Build workspace object if any workspace flags are provided
257
- const workspace = {};
258
- if (workspaceMode)
259
- workspace.mode = workspaceMode;
260
- if (workspaceKey)
261
- workspace.key = workspaceKey;
262
- if (Object.keys(workspace).length > 0) {
263
- body.workspace = workspace;
264
- }
265
- // Determine endpoint: root jobs use project endpoint, child jobs can use job-based endpoint
266
- // For simplicity, always use project endpoint. If parent_id is set, the API inherits project from parent.
267
- const resolvedProjectId = projectId ?? (parentId ? await getProjectFromJob(context, parentId) : undefined);
268
- if (!resolvedProjectId) {
269
- throw new Error('Could not determine project. Provide --project or ensure --parent exists.');
270
- }
271
- const response = await (0, client_1.requestJson)(context, `/projects/${resolvedProjectId}/jobs`, {
272
- method: 'POST',
273
- body,
274
- });
275
- // If --claim flag is set, immediately claim the job
276
- if (shouldClaim) {
277
- const claimBody = { agent_id: agentId };
278
- // Use harness from hints if available
279
- if (harness) {
280
- claimBody.harness = harness;
281
- }
282
- const claimResponse = await (0, client_1.requestJson)(context, `/jobs/${response.id}/claim`, { method: 'POST', body: claimBody });
283
- if (json) {
284
- (0, output_1.outputJson)({ job: response, attempt: claimResponse.attempt }, json);
285
- }
286
- else {
287
- console.log(`Created and claimed job: ${response.id}`);
288
- console.log(` Title: ${response.title}`);
289
- console.log(` Phase: active`);
290
- console.log(` Priority: P${response.priority}`);
291
- console.log(` Attempt: #${claimResponse.attempt.attempt_number}`);
292
- console.log(` Agent: ${claimResponse.attempt.agent_id}`);
293
- if (claimResponse.attempt.harness) {
294
- console.log(` Harness: ${claimResponse.attempt.harness}`);
295
- }
296
- if (response.parent_id) {
297
- console.log(` Parent: ${response.parent_id}`);
298
- }
299
- }
300
- return;
301
- }
302
- if (json) {
303
- (0, output_1.outputJson)(response, json);
304
- }
305
- else {
306
- console.log(`Created job: ${response.id}`);
307
- console.log(` Title: ${response.title}`);
308
- console.log(` Phase: ${response.phase}`);
309
- console.log(` Priority: P${response.priority}`);
310
- if (response.parent_id) {
311
- console.log(` Parent: ${response.parent_id}`);
312
- }
313
- if (Object.keys(hints).length > 0) {
314
- console.log(` Hints: ${JSON.stringify(hints)}`);
315
- }
316
- if (Object.keys(git).length > 0) {
317
- console.log(` Git: ${JSON.stringify(git)}`);
318
- }
319
- if (Object.keys(workspace).length > 0) {
320
- console.log(` Workspace: ${JSON.stringify(workspace)}`);
321
- }
322
- }
323
- }
324
- /**
325
- * Parse a relative time string (e.g., "1h", "30m", "2d") into an ISO timestamp
326
- */
327
- function parseSinceValue(since) {
328
- // If it looks like an ISO date, return as-is
329
- if (since.includes('T') || since.includes('-')) {
330
- return since;
331
- }
332
- const match = since.match(/^(\d+)([mhd])$/);
333
- if (!match) {
334
- throw new Error(`Invalid --since format: "${since}". Use formats like "1h", "30m", "2d", or ISO timestamp.`);
335
- }
336
- const value = parseInt(match[1], 10);
337
- const unit = match[2];
338
- const now = new Date();
339
- switch (unit) {
340
- case 'm':
341
- now.setMinutes(now.getMinutes() - value);
342
- break;
343
- case 'h':
344
- now.setHours(now.getHours() - value);
345
- break;
346
- case 'd':
347
- now.setDate(now.getDate() - value);
348
- break;
349
- }
350
- return now.toISOString();
351
- }
352
- /**
353
- * eve jobs list [--project=X] [--phase=ready] [--assignee=X] [--since=1h] [--stuck] [--limit=50] [--offset=0]
354
- * eve jobs list --all [--org=X] [--project=X] [--phase=X] [--limit=50] [--offset=0]
355
- */
356
- async function handleList(flags, context, json) {
357
- // Check for --all flag (admin mode - cross-project listing)
358
- const all = Boolean(flags.all);
359
- if (all) {
360
- // Admin mode: list all jobs across projects
361
- const query = buildQuery({
362
- org_id: (0, args_1.getStringFlag)(flags, ['org']),
363
- project_id: (0, args_1.getStringFlag)(flags, ['project']),
364
- phase: (0, args_1.getStringFlag)(flags, ['phase']),
365
- limit: (0, args_1.getStringFlag)(flags, ['limit']) ?? '50',
366
- offset: (0, args_1.getStringFlag)(flags, ['offset']),
367
- });
368
- const response = await (0, client_1.requestJson)(context, `/jobs${query}`);
369
- if (json) {
370
- (0, output_1.outputJson)(response, json);
371
- }
372
- else {
373
- if (response.jobs.length === 0) {
374
- console.log('No jobs found.');
375
- return;
376
- }
377
- console.log('All jobs (admin view):');
378
- console.log('');
379
- formatJobsTable(response.jobs);
380
- }
381
- return;
382
- }
383
- // Standard mode: project-scoped listing
384
- const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
385
- if (!projectId) {
386
- throw new Error('Usage: eve jobs list --project=<id> [--phase=X] [--assignee=X] [--since=1h] [--stuck]\n eve jobs list --all [--org=X] [--project=X] [--phase=X]');
387
- }
388
- // Parse --since into ISO timestamp
389
- const sinceRaw = (0, args_1.getStringFlag)(flags, ['since']);
390
- const since = sinceRaw ? parseSinceValue(sinceRaw) : undefined;
391
- // --stuck is a boolean flag
392
- const stuck = Boolean(flags.stuck);
393
- const stuckMinutes = (0, args_1.getStringFlag)(flags, ['stuck-minutes']);
394
- const query = buildQuery({
395
- phase: (0, args_1.getStringFlag)(flags, ['phase']),
396
- assignee: (0, args_1.getStringFlag)(flags, ['assignee']),
397
- priority: (0, args_1.getStringFlag)(flags, ['priority']),
398
- since,
399
- stuck: stuck ? 'true' : undefined,
400
- stuck_minutes: stuckMinutes,
401
- limit: (0, args_1.getStringFlag)(flags, ['limit']),
402
- offset: (0, args_1.getStringFlag)(flags, ['offset']),
403
- });
404
- const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/jobs${query}`);
405
- if (json) {
406
- (0, output_1.outputJson)(response, json);
407
- }
408
- else {
409
- if (stuck && response.jobs.length === 0) {
410
- console.log('No stuck jobs found.');
411
- return;
412
- }
413
- formatJobsTable(response.jobs);
414
- }
415
- }
416
- /**
417
- * eve jobs ready [--project=X] [--limit=10]
418
- * Shortcut for showing schedulable jobs
419
- */
420
- async function handleReady(flags, context, json) {
421
- const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
422
- if (!projectId) {
423
- throw new Error('Usage: eve jobs ready --project=<id> [--limit=10]');
424
- }
425
- const query = buildQuery({
426
- limit: (0, args_1.getStringFlag)(flags, ['limit']) ?? '10',
427
- });
428
- const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/jobs/ready${query}`);
429
- if (json) {
430
- (0, output_1.outputJson)(response, json);
431
- }
432
- else {
433
- if (response.jobs.length === 0) {
434
- console.log('No ready jobs found.');
435
- return;
436
- }
437
- console.log('Ready jobs (schedulable):');
438
- console.log('');
439
- formatJobsTable(response.jobs);
440
- }
441
- }
442
- /**
443
- * eve jobs blocked [--project=X]
444
- * Show jobs that are blocked by dependencies
445
- */
446
- async function handleBlocked(flags, context, json) {
447
- const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
448
- if (!projectId) {
449
- throw new Error('Usage: eve jobs blocked --project=<id>');
450
- }
451
- const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/jobs/blocked`);
452
- if (json) {
453
- (0, output_1.outputJson)(response, json);
454
- }
455
- else {
456
- if (response.jobs.length === 0) {
457
- console.log('No blocked jobs found.');
458
- return;
459
- }
460
- console.log('Blocked jobs (waiting on dependencies):');
461
- console.log('');
462
- formatJobsTable(response.jobs);
463
- }
464
- }
465
- /**
466
- * eve jobs show <id>
467
- */
468
- async function handleShow(positionals, flags, context, json) {
469
- const jobId = positionals[0];
470
- const verbose = Boolean(flags.verbose || flags.v);
471
- if (!jobId) {
472
- throw new Error('Usage: eve jobs show <job-id> [--verbose]');
473
- }
474
- const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
475
- // Fetch attempts if verbose
476
- let attempts = [];
477
- let latestAttempt;
478
- if (verbose) {
479
- const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
480
- attempts = attemptsResponse.attempts || [];
481
- // Get result details for the latest attempt
482
- if (attempts.length > 0) {
483
- const latest = attempts[attempts.length - 1];
484
- try {
485
- const result = await (0, client_1.requestJson)(context, `/jobs/${jobId}/result?attempt=${latest.attempt_number}`);
486
- latestAttempt = { ...latest, result };
487
- }
488
- catch {
489
- latestAttempt = { ...latest };
490
- }
491
- }
492
- }
493
- if (json) {
494
- (0, output_1.outputJson)(verbose ? { ...job, attempts, latestAttempt } : job, json);
495
- }
496
- else {
497
- formatJobDetails(job);
498
- if (verbose && attempts.length > 0) {
499
- formatAttemptsVerbose(attempts, latestAttempt);
500
- }
501
- }
502
- }
503
- /**
504
- * eve jobs current [<job-id>] [--json|--tree]
505
- * Defaults to EVE_JOB_ID when present
506
- */
507
- async function handleCurrent(positionals, flags, context, json) {
508
- const jobId = positionals[0] ?? process.env.EVE_JOB_ID;
509
- const tree = (0, args_1.getBooleanFlag)(flags, ['tree']) ?? false;
510
- if (!jobId) {
511
- throw new Error('Usage: eve job current [<job-id>] [--json|--tree]');
512
- }
513
- if (tree) {
514
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/tree`);
515
- if (json) {
516
- (0, output_1.outputJson)(response, json);
517
- }
518
- else {
519
- formatJobTree(response, 0);
520
- }
521
- return;
522
- }
523
- const response = await requestJobContext(context, jobId);
524
- (0, output_1.outputJson)(response, true);
525
- }
526
- /**
527
- * eve jobs diagnose <id>
528
- * Comprehensive job debugging - shows job state, attempts, timeline, logs, and recommendations
529
- */
530
- async function handleDiagnose(positionals, context, json) {
531
- const jobId = positionals[0];
532
- if (!jobId) {
533
- throw new Error('Usage: eve jobs diagnose <job-id>');
534
- }
535
- // Fetch job details
536
- const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
537
- // Fetch attempts
538
- const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
539
- const attempts = attemptsResponse.attempts || [];
540
- // Fetch latest result
541
- let latestResult = null;
542
- if (attempts.length > 0) {
543
- const latest = attempts[attempts.length - 1];
544
- try {
545
- latestResult = await (0, client_1.requestJson)(context, `/jobs/${jobId}/result?attempt=${latest.attempt_number}`);
546
- }
547
- catch {
548
- // No result available
549
- }
550
- }
551
- // Fetch recent logs (if we have attempts)
552
- let logs = [];
553
- if (attempts.length > 0) {
554
- const latest = attempts[attempts.length - 1];
555
- try {
556
- const logsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts/${latest.attempt_number}/logs?limit=50`);
557
- logs = logsResponse.logs || [];
558
- }
559
- catch {
560
- // No logs available
561
- }
562
- }
563
- if (json) {
564
- (0, output_1.outputJson)({ job, attempts, latestResult, logs }, json);
565
- }
566
- else {
567
- formatDiagnose(job, attempts, latestResult, logs);
568
- }
569
- }
570
- /**
571
- * eve jobs tree <id>
572
- * Show job hierarchy
573
- */
574
- async function handleTree(positionals, context, json) {
575
- const jobId = positionals[0];
576
- if (!jobId) {
577
- throw new Error('Usage: eve jobs tree <job-id>');
578
- }
579
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/tree`);
580
- if (json) {
581
- (0, output_1.outputJson)(response, json);
582
- }
583
- else {
584
- formatJobTree(response, 0);
585
- }
586
- }
587
- /**
588
- * eve jobs update <id> --phase=X --priority=X --assignee=X
589
- *
590
- * Git controls (optional, override project/manifest defaults):
591
- * --git-ref=<ref> Target ref (branch, tag, or SHA)
592
- * --git-ref-policy=<policy> auto|env|project_default|explicit
593
- * --git-branch=<branch> Branch to create/checkout
594
- * --git-create-branch=<mode> never|if_missing|always
595
- * --git-commit=<policy> never|manual|auto|required
596
- * --git-commit-message=<template> Commit message template
597
- * --git-push=<policy> never|on_success|required
598
- * --git-remote=<remote> Remote to push to (default: origin)
599
- *
600
- * Workspace options (optional):
601
- * --workspace-mode=<mode> job|session|isolated (default: job)
602
- * --workspace-key=<key> Workspace key for session mode
603
- */
604
- async function handleUpdate(positionals, flags, context, json) {
605
- const jobId = positionals[0];
606
- if (!jobId) {
607
- throw new Error('Usage: eve jobs update <job-id> [--phase=X] [--priority=X] [--assignee=X] [--git-*] [--workspace-*]');
608
- }
609
- const body = {};
610
- const phase = (0, args_1.getStringFlag)(flags, ['phase']);
611
- const priority = (0, args_1.getStringFlag)(flags, ['priority']);
612
- const assignee = (0, args_1.getStringFlag)(flags, ['assignee']);
613
- const title = (0, args_1.getStringFlag)(flags, ['title']);
614
- const description = (0, args_1.getStringFlag)(flags, ['description']);
615
- const labels = (0, args_1.getStringFlag)(flags, ['labels']);
616
- const deferUntil = (0, args_1.getStringFlag)(flags, ['defer-until']);
617
- const dueAt = (0, args_1.getStringFlag)(flags, ['due-at']);
618
- const review = (0, args_1.getStringFlag)(flags, ['review']);
619
- if (phase)
620
- body.phase = phase;
621
- if (priority !== undefined)
622
- body.priority = parseInt(priority, 10);
623
- if (assignee)
624
- body.assignee = assignee;
625
- if (title)
626
- body.title = title;
627
- if (description)
628
- body.description = description;
629
- if (labels)
630
- body.labels = labels.split(',').map((l) => l.trim());
631
- if (deferUntil)
632
- body.defer_until = deferUntil;
633
- if (dueAt)
634
- body.due_at = dueAt;
635
- if (review)
636
- body.review_required = review;
637
- // Git controls
638
- const gitRef = (0, args_1.getStringFlag)(flags, ['git-ref']);
639
- const gitRefPolicy = (0, args_1.getStringFlag)(flags, ['git-ref-policy']);
640
- const gitBranch = (0, args_1.getStringFlag)(flags, ['git-branch']);
641
- const gitCreateBranch = (0, args_1.getStringFlag)(flags, ['git-create-branch']);
642
- const gitCommit = (0, args_1.getStringFlag)(flags, ['git-commit']);
643
- const gitCommitMessage = (0, args_1.getStringFlag)(flags, ['git-commit-message']);
644
- const gitPush = (0, args_1.getStringFlag)(flags, ['git-push']);
645
- const gitRemote = (0, args_1.getStringFlag)(flags, ['git-remote']);
646
- // Validate git policies
647
- if (gitRefPolicy && !['auto', 'env', 'project_default', 'explicit'].includes(gitRefPolicy)) {
648
- throw new Error('--git-ref-policy must be one of: auto, env, project_default, explicit');
649
- }
650
- if (gitCreateBranch && !['never', 'if_missing', 'always'].includes(gitCreateBranch)) {
651
- throw new Error('--git-create-branch must be one of: never, if_missing, always');
652
- }
653
- if (gitCommit && !['never', 'manual', 'auto', 'required'].includes(gitCommit)) {
654
- throw new Error('--git-commit must be one of: never, manual, auto, required');
655
- }
656
- if (gitPush && !['never', 'on_success', 'required'].includes(gitPush)) {
657
- throw new Error('--git-push must be one of: never, on_success, required');
658
- }
659
- // Build git object if any git flags are provided
660
- const git = {};
661
- if (gitRef)
662
- git.ref = gitRef;
663
- if (gitRefPolicy)
664
- git.ref_policy = gitRefPolicy;
665
- if (gitBranch)
666
- git.branch = gitBranch;
667
- if (gitCreateBranch)
668
- git.create_branch = gitCreateBranch;
669
- if (gitCommit)
670
- git.commit = gitCommit;
671
- if (gitCommitMessage)
672
- git.commit_message = gitCommitMessage;
673
- if (gitPush)
674
- git.push = gitPush;
675
- if (gitRemote)
676
- git.remote = gitRemote;
677
- if (Object.keys(git).length > 0) {
678
- body.git = git;
679
- }
680
- // Workspace options
681
- const workspaceMode = (0, args_1.getStringFlag)(flags, ['workspace-mode']);
682
- const workspaceKey = (0, args_1.getStringFlag)(flags, ['workspace-key']);
683
- // Validate workspace mode
684
- if (workspaceMode && !['job', 'session', 'isolated'].includes(workspaceMode)) {
685
- throw new Error('--workspace-mode must be one of: job, session, isolated');
686
- }
687
- // Build workspace object if any workspace flags are provided
688
- const workspace = {};
689
- if (workspaceMode)
690
- workspace.mode = workspaceMode;
691
- if (workspaceKey)
692
- workspace.key = workspaceKey;
693
- if (Object.keys(workspace).length > 0) {
694
- body.workspace = workspace;
695
- }
696
- if (Object.keys(body).length === 0) {
697
- throw new Error('No updates provided. Use --phase, --priority, --assignee, --git-*, --workspace-*, etc.');
698
- }
699
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}`, {
700
- method: 'PATCH',
701
- body,
702
- });
703
- if (json) {
704
- (0, output_1.outputJson)(response, json);
705
- }
706
- else {
707
- console.log(`Updated job: ${response.id}`);
708
- console.log(` Phase: ${response.phase}`);
709
- console.log(` Priority: P${response.priority}`);
710
- if (response.assignee) {
711
- console.log(` Assignee: ${response.assignee}`);
712
- }
713
- if (response.git && Object.keys(response.git).length > 0) {
714
- console.log(` Git: ${JSON.stringify(response.git)}`);
715
- }
716
- if (response.workspace && Object.keys(response.workspace).length > 0) {
717
- console.log(` Workspace: ${JSON.stringify(response.workspace)}`);
718
- }
719
- }
720
- }
721
- /**
722
- * eve jobs close <id> [--reason="..."]
723
- * Mark job as done
724
- */
725
- async function handleClose(positionals, flags, context, json) {
726
- const jobId = positionals[0];
727
- if (!jobId) {
728
- throw new Error('Usage: eve jobs close <job-id> [--reason="..."]');
729
- }
730
- const reason = (0, args_1.getStringFlag)(flags, ['reason']);
731
- const body = {
732
- phase: 'done',
733
- };
734
- if (reason) {
735
- body.close_reason = reason;
736
- }
737
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}`, {
738
- method: 'PATCH',
739
- body,
740
- });
741
- if (json) {
742
- (0, output_1.outputJson)(response, json);
743
- }
744
- else {
745
- console.log(`Closed job: ${response.id}`);
746
- console.log(` Phase: ${response.phase}`);
747
- if (response.close_reason) {
748
- console.log(` Reason: ${response.close_reason}`);
749
- }
750
- }
751
- }
752
- /**
753
- * eve jobs cancel <id> [--reason="..."]
754
- * Mark job as cancelled
755
- */
756
- async function handleCancel(positionals, flags, context, json) {
757
- const jobId = positionals[0];
758
- if (!jobId) {
759
- throw new Error('Usage: eve jobs cancel <job-id> [--reason="..."]');
760
- }
761
- const reason = (0, args_1.getStringFlag)(flags, ['reason']);
762
- const body = {
763
- phase: 'cancelled',
764
- };
765
- if (reason) {
766
- body.close_reason = reason;
767
- }
768
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}`, {
769
- method: 'PATCH',
770
- body,
771
- });
772
- if (json) {
773
- (0, output_1.outputJson)(response, json);
774
- }
775
- else {
776
- console.log(`Cancelled job: ${response.id}`);
777
- console.log(` Phase: ${response.phase}`);
778
- if (response.close_reason) {
779
- console.log(` Reason: ${response.close_reason}`);
780
- }
781
- }
782
- }
783
- /**
784
- * eve jobs dep <add|remove|list> [args]
785
- * Manage job dependencies
786
- */
787
- async function handleDep(positionals, flags, context, json) {
788
- const depSubcommand = positionals[0];
789
- switch (depSubcommand) {
790
- case 'add':
791
- return handleDepAdd(positionals.slice(1), flags, context, json);
792
- case 'remove':
793
- return handleDepRemove(positionals.slice(1), context, json);
794
- case 'list':
795
- return handleDepList(positionals.slice(1), context, json);
796
- default:
797
- throw new Error('Usage: eve jobs dep <add|remove|list> [args]');
798
- }
799
- }
800
- /**
801
- * eve jobs dep add <from> <to> [--type=blocks]
802
- * Add dependency: "from depends on to" (to blocks from)
803
- */
804
- async function handleDepAdd(positionals, flags, context, json) {
805
- const fromId = positionals[0];
806
- const toId = positionals[1];
807
- if (!fromId || !toId) {
808
- throw new Error('Usage: eve jobs dep add <from> <to> [--type=blocks]');
809
- }
810
- const relationType = (0, args_1.getStringFlag)(flags, ['type']) ?? 'blocks';
811
- const response = await (0, client_1.requestJson)(context, `/jobs/${fromId}/dependencies`, {
812
- method: 'POST',
813
- body: {
814
- related_job_id: toId,
815
- relation_type: relationType,
816
- },
817
- });
818
- if (json) {
819
- (0, output_1.outputJson)(response, json);
820
- }
821
- else {
822
- console.log(`Added dependency: ${fromId} depends on ${toId} (${relationType})`);
823
- }
824
- }
825
- /**
826
- * eve jobs dep remove <from> <to>
827
- * Remove dependency
828
- */
829
- async function handleDepRemove(positionals, context, json) {
830
- const fromId = positionals[0];
831
- const toId = positionals[1];
832
- if (!fromId || !toId) {
833
- throw new Error('Usage: eve jobs dep remove <from> <to>');
834
- }
835
- const response = await (0, client_1.requestJson)(context, `/jobs/${fromId}/dependencies/${toId}`, {
836
- method: 'DELETE',
837
- });
838
- if (json) {
839
- (0, output_1.outputJson)(response, json);
840
- }
841
- else {
842
- console.log(`Removed dependency: ${fromId} no longer depends on ${toId}`);
843
- }
844
- }
845
- /**
846
- * eve jobs dep list <id>
847
- * Show dependencies and dependents for a job
848
- */
849
- async function handleDepList(positionals, context, json) {
850
- const jobId = positionals[0];
851
- if (!jobId) {
852
- throw new Error('Usage: eve jobs dep list <job-id>');
853
- }
854
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/dependencies`);
855
- if (json) {
856
- (0, output_1.outputJson)(response, json);
857
- }
858
- else {
859
- formatDependencies(response);
860
- }
861
- }
862
- // ============================================================================
863
- // Helper Functions
864
- // ============================================================================
865
- async function requestJobContext(context, jobId) {
866
- const response = await (0, client_1.requestRaw)(context, `/jobs/${jobId}/context`);
867
- if (response.ok) {
868
- return response.data;
869
- }
870
- if (response.status !== 404) {
871
- const message = typeof response.data === 'string' ? response.data : response.text;
872
- throw new Error(`HTTP ${response.status}: ${message}`);
873
- }
874
- const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
875
- return { job };
876
- }
877
- function buildQuery(params) {
878
- const search = new URLSearchParams();
879
- Object.entries(params).forEach(([key, value]) => {
880
- if (value === undefined || value === '')
881
- return;
882
- search.set(key, String(value));
883
- });
884
- const query = search.toString();
885
- return query ? `?${query}` : '';
886
- }
887
- /**
888
- * Get project ID from an existing job (used when creating child jobs)
889
- */
890
- async function getProjectFromJob(context, jobId) {
891
- try {
892
- const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
893
- return job.project_id;
894
- }
895
- catch {
896
- return undefined;
897
- }
898
- }
899
- /**
900
- * Format jobs as a human-readable table
901
- */
902
- function formatJobsTable(jobs) {
903
- if (jobs.length === 0) {
904
- console.log('No jobs found.');
905
- return;
906
- }
907
- // Calculate column widths
908
- const idWidth = Math.max(4, ...jobs.map((j) => j.id.length));
909
- const phaseWidth = Math.max(5, ...jobs.map((j) => j.phase.length));
910
- const titleWidth = Math.min(50, Math.max(5, ...jobs.map((j) => j.title.length)));
911
- // Header
912
- const header = [
913
- padRight('ID', idWidth),
914
- padRight('P', 2),
915
- padRight('Phase', phaseWidth),
916
- padRight('Type', 8),
917
- padRight('Title', titleWidth),
918
- 'Assignee',
919
- ].join(' ');
920
- console.log(header);
921
- console.log('-'.repeat(header.length));
922
- // Rows
923
- for (const job of jobs) {
924
- const title = job.title.length > titleWidth ? job.title.slice(0, titleWidth - 3) + '...' : job.title;
925
- const row = [
926
- padRight(job.id, idWidth),
927
- padRight(`P${job.priority}`, 2),
928
- padRight(job.phase, phaseWidth),
929
- padRight(job.issue_type, 8),
930
- padRight(title, titleWidth),
931
- job.assignee ?? '-',
932
- ].join(' ');
933
- console.log(row);
934
- }
935
- console.log('');
936
- console.log(`Total: ${jobs.length} job(s)`);
937
- }
938
- /**
939
- * Format a single job's details
940
- */
941
- function formatJobDetails(job) {
942
- console.log(`Job: ${job.id}`);
943
- console.log('');
944
- console.log(` Title: ${job.title}`);
945
- console.log(` Phase: ${job.phase}`);
946
- console.log(` Priority: P${job.priority}`);
947
- console.log(` Type: ${job.issue_type}`);
948
- console.log(` Project: ${job.project_id}`);
949
- if (job.parent_id) {
950
- console.log(` Parent: ${job.parent_id}`);
951
- }
952
- console.log(` Depth: ${job.depth}`);
953
- if (job.description) {
954
- console.log(` Description: ${job.description}`);
955
- }
956
- if (job.labels && job.labels.length > 0) {
957
- console.log(` Labels: ${job.labels.join(', ')}`);
958
- }
959
- if (job.assignee) {
960
- console.log(` Assignee: ${job.assignee}`);
961
- }
962
- console.log(` Review: ${job.review_required}`);
963
- if (job.review_status) {
964
- console.log(` Review Status: ${job.review_status}`);
965
- }
966
- if (job.reviewer) {
967
- console.log(` Reviewer: ${job.reviewer}`);
968
- }
969
- if (job.defer_until) {
970
- console.log(` Defer Until: ${job.defer_until}`);
971
- }
972
- if (job.due_at) {
973
- console.log(` Due At: ${job.due_at}`);
974
- }
975
- // Git configuration
976
- if (job.git && Object.keys(job.git).length > 0) {
977
- console.log('');
978
- console.log(' Git Controls:');
979
- if (job.git.ref) {
980
- console.log(` Ref: ${job.git.ref}`);
981
- }
982
- if (job.git.ref_policy) {
983
- console.log(` Ref Policy: ${job.git.ref_policy}`);
984
- }
985
- if (job.git.branch) {
986
- console.log(` Branch: ${job.git.branch}`);
987
- }
988
- if (job.git.create_branch) {
989
- console.log(` Create Branch: ${job.git.create_branch}`);
990
- }
991
- if (job.git.commit) {
992
- console.log(` Commit: ${job.git.commit}`);
993
- }
994
- if (job.git.commit_message) {
995
- console.log(` Commit Msg: ${job.git.commit_message}`);
996
- }
997
- if (job.git.push) {
998
- console.log(` Push: ${job.git.push}`);
999
- }
1000
- if (job.git.remote) {
1001
- console.log(` Remote: ${job.git.remote}`);
1002
- }
1003
- }
1004
- // Workspace configuration
1005
- if (job.workspace && Object.keys(job.workspace).length > 0) {
1006
- console.log('');
1007
- console.log(' Workspace:');
1008
- if (job.workspace.mode) {
1009
- console.log(` Mode: ${job.workspace.mode}`);
1010
- }
1011
- if (job.workspace.key) {
1012
- console.log(` Key: ${job.workspace.key}`);
1013
- }
1014
- }
1015
- // Resolved Git
1016
- if (job.resolved_git) {
1017
- const rg = job.resolved_git;
1018
- console.log('');
1019
- console.log(' Resolved Git:');
1020
- if (rg.resolved_sha)
1021
- console.log(` SHA: ${rg.resolved_sha}`);
1022
- if (rg.resolved_branch)
1023
- console.log(` Branch: ${rg.resolved_branch}`);
1024
- if (rg.ref_source)
1025
- console.log(` Source: ${rg.ref_source}`);
1026
- if (rg.pushed !== undefined)
1027
- console.log(` Pushed: ${rg.pushed}`);
1028
- if (rg.commits?.length)
1029
- console.log(` Commits: ${rg.commits.join(', ')}`);
1030
- }
1031
- console.log('');
1032
- console.log(` Created: ${formatDate(job.created_at)}`);
1033
- console.log(` Updated: ${formatDate(job.updated_at)}`);
1034
- if (job.closed_at) {
1035
- console.log(` Closed: ${formatDate(job.closed_at)}`);
1036
- }
1037
- if (job.close_reason) {
1038
- console.log(` Close Reason: ${job.close_reason}`);
1039
- }
1040
- }
1041
- /**
1042
- * Format attempts with verbose details (for --verbose flag)
1043
- */
1044
- function formatAttemptsVerbose(attempts, latestAttempt) {
1045
- console.log('');
1046
- console.log('Attempts:');
1047
- for (const attempt of attempts) {
1048
- const isLatest = latestAttempt && attempt.id === latestAttempt.id;
1049
- const statusIcon = attempt.status === 'succeeded' ? '✓' :
1050
- attempt.status === 'failed' ? '✗' :
1051
- attempt.status === 'running' ? '▶' : '○';
1052
- console.log(` ${statusIcon} Attempt #${attempt.attempt_number} (${attempt.status})`);
1053
- console.log(` ID: ${attempt.id}`);
1054
- console.log(` Started: ${formatDate(attempt.started_at)}`);
1055
- if (attempt.ended_at) {
1056
- console.log(` Ended: ${formatDate(attempt.ended_at)}`);
1057
- }
1058
- if (attempt.harness) {
1059
- console.log(` Harness: ${attempt.harness}`);
1060
- }
1061
- if (attempt.agent_id) {
1062
- console.log(` Agent: ${attempt.agent_id}`);
1063
- }
1064
- // Show result details for latest attempt
1065
- if (isLatest && latestAttempt?.result) {
1066
- const r = latestAttempt.result;
1067
- if (r.exitCode !== null) {
1068
- console.log(` Exit: ${r.exitCode}`);
1069
- }
1070
- if (r.durationMs !== null) {
1071
- console.log(` Duration: ${(r.durationMs / 1000).toFixed(1)}s`);
1072
- }
1073
- if (r.errorMessage) {
1074
- console.log(` Error: ${r.errorMessage}`);
1075
- }
1076
- if (r.tokenInput || r.tokenOutput) {
1077
- console.log(` Tokens: ${r.tokenInput || 0} in / ${r.tokenOutput || 0} out`);
1078
- }
1079
- }
1080
- console.log('');
1081
- }
1082
- }
1083
- /**
1084
- * Format comprehensive diagnostic output
1085
- */
1086
- function formatDiagnose(job, attempts, latestResult, logs) {
1087
- // Header
1088
- console.log('╭────────────────────────────────────────────────────────────────╮');
1089
- console.log(`│ Job Diagnosis: ${job.id.padEnd(47)} │`);
1090
- console.log('╰────────────────────────────────────────────────────────────────╯');
1091
- console.log('');
1092
- // Status summary
1093
- // Note: 'failed' is not a valid job phase - failures are 'cancelled' with a close_reason
1094
- const statusIcon = job.phase === 'done' ? '✓' :
1095
- job.phase === 'cancelled' ? '⊘' :
1096
- job.phase === 'active' ? '▶' : '○';
1097
- console.log(`Status: ${statusIcon} ${job.phase.toUpperCase()}`);
1098
- console.log(` Priority: P${job.priority}`);
1099
- console.log(` Assignee: ${job.assignee || '(none)'}`);
1100
- console.log(` Created: ${formatDate(job.created_at)}`);
1101
- console.log(` Updated: ${formatDate(job.updated_at)}`);
1102
- console.log('');
1103
- // Title and description
1104
- console.log(`Title: ${job.title}`);
1105
- if (job.description && job.description !== job.title) {
1106
- const desc = job.description.length > 200
1107
- ? job.description.substring(0, 200) + '...'
1108
- : job.description;
1109
- console.log(`Description: ${desc}`);
1110
- }
1111
- console.log('');
1112
- // Attempts timeline
1113
- console.log('Timeline:');
1114
- console.log(` ${formatDate(job.created_at)} - Job created (phase: ready)`);
1115
- for (const attempt of attempts) {
1116
- const statusIcon = attempt.status === 'succeeded' ? '✓' :
1117
- attempt.status === 'failed' ? '✗' :
1118
- attempt.status === 'running' ? '▶' : '○';
1119
- console.log(` ${formatDate(attempt.started_at)} - Attempt #${attempt.attempt_number} started`);
1120
- if (attempt.ended_at) {
1121
- console.log(` ${formatDate(attempt.ended_at)} - Attempt #${attempt.attempt_number} ${statusIcon} ${attempt.status}`);
1122
- }
1123
- }
1124
- if (job.closed_at) {
1125
- console.log(` ${formatDate(job.closed_at)} - Job closed (${job.phase})`);
1126
- }
1127
- console.log('');
1128
- // Lifecycle timeline from logs
1129
- const lifecycleLogs = logs.filter(l => l.type?.startsWith('lifecycle_'));
1130
- if (lifecycleLogs.length > 0) {
1131
- console.log('Execution Timeline:');
1132
- for (const log of lifecycleLogs) {
1133
- const content = log.line;
1134
- const phase = content?.phase || 'unknown';
1135
- const action = content?.action || '';
1136
- const duration = content?.duration_ms;
1137
- const success = content?.success;
1138
- const error = content?.error;
1139
- if (action === 'end') {
1140
- const durationStr = duration ? `${duration}ms` : '';
1141
- const statusIcon = success === false ? '✗' : '✓';
1142
- const errorStr = error ? ` - ${error.split('\n')[0]}` : '';
1143
- console.log(` ${statusIcon} ${phase}: ${durationStr}${errorStr}`);
1144
- }
1145
- }
1146
- console.log('');
1147
- }
1148
- // Latest attempt details
1149
- if (attempts.length > 0) {
1150
- const latest = attempts[attempts.length - 1];
1151
- console.log(`Latest Attempt (#${latest.attempt_number}):`);
1152
- console.log(` Status: ${latest.status}`);
1153
- console.log(` Harness: ${latest.harness || '(default)'}`);
1154
- console.log(` Agent: ${latest.agent_id || '(none)'}`);
1155
- if (latestResult) {
1156
- if (latestResult.exitCode !== null) {
1157
- console.log(` Exit Code: ${latestResult.exitCode}`);
1158
- }
1159
- if (latestResult.durationMs !== null) {
1160
- console.log(` Duration: ${(latestResult.durationMs / 1000).toFixed(1)}s`);
1161
- }
1162
- if (latestResult.errorMessage) {
1163
- console.log('');
1164
- console.log('Error Message:');
1165
- console.log(` ${latestResult.errorMessage}`);
1166
- }
1167
- }
1168
- console.log('');
1169
- }
1170
- // Recent logs
1171
- if (logs.length > 0) {
1172
- console.log('Recent Logs:');
1173
- const recentLogs = logs.slice(-10);
1174
- for (const log of recentLogs) {
1175
- const time = formatDate(log.timestamp);
1176
- let msg = '';
1177
- try {
1178
- msg = log.line != null
1179
- ? JSON.stringify(log.line).substring(0, 100)
1180
- : '(empty)';
1181
- }
1182
- catch {
1183
- msg = '(error formatting log)';
1184
- }
1185
- console.log(` ${time} ${msg}`);
1186
- }
1187
- console.log('');
1188
- }
1189
- // Diagnosis / recommendations
1190
- console.log('Diagnosis:');
1191
- if (job.phase === 'done') {
1192
- console.log(' ✓ Job completed successfully');
1193
- }
1194
- else if (job.phase === 'cancelled') {
1195
- // Cancelled jobs may have a failure reason in close_reason or attempt error
1196
- if (latestResult?.errorMessage?.includes('git clone')) {
1197
- console.log(' ✗ Git clone failed - check repo URL and credentials');
1198
- console.log(' Hint: Ensure GITHUB_TOKEN is set in project/org secrets');
1199
- }
1200
- else if (latestResult?.errorMessage?.includes('Service')) {
1201
- console.log(' ✗ Service provisioning failed');
1202
- console.log(' Hint: Check .eve/services.yaml and container logs');
1203
- }
1204
- else if (latestResult?.errorMessage) {
1205
- console.log(` ⊘ Cancelled: ${latestResult.errorMessage}`);
1206
- }
1207
- else if (job.close_reason) {
1208
- console.log(` ⊘ Cancelled: ${job.close_reason}`);
1209
- }
1210
- else {
1211
- console.log(' ⊘ Job was cancelled');
1212
- }
1213
- }
1214
- else if (job.phase === 'active' && attempts.length > 0) {
1215
- const latest = attempts[attempts.length - 1];
1216
- if (latest.status === 'running') {
1217
- const startedAt = new Date(latest.started_at).getTime();
1218
- const elapsed = Math.round((Date.now() - startedAt) / 1000);
1219
- if (elapsed > 300) {
1220
- console.log(` ⚠ Attempt running for ${elapsed}s - may be stuck`);
1221
- console.log(' Hint: Check orchestrator/worker logs');
1222
- }
1223
- else {
1224
- console.log(` ▶ Attempt in progress (${elapsed}s elapsed)`);
1225
- }
1226
- }
1227
- }
1228
- else if (job.phase === 'ready') {
1229
- console.log(' ○ Job is ready, waiting to be claimed');
1230
- if (job.assignee) {
1231
- console.log(` Assigned to: ${job.assignee}`);
1232
- }
1233
- }
1234
- }
1235
- /**
1236
- * Format job hierarchy as a tree
1237
- */
1238
- function formatJobTree(node, indent) {
1239
- const prefix = indent === 0 ? '' : ' '.repeat(indent - 1) + (indent > 0 ? '|- ' : '');
1240
- const phaseIcon = getPhaseIcon(node.phase);
1241
- const line = `${prefix}${phaseIcon} ${node.id} [P${node.priority}] ${node.title}`;
1242
- console.log(line);
1243
- if (node.children && node.children.length > 0) {
1244
- for (const child of node.children) {
1245
- formatJobTree(child, indent + 1);
1246
- }
1247
- }
1248
- }
1249
- /**
1250
- * Get an ASCII icon for a phase
1251
- */
1252
- function getPhaseIcon(phase) {
1253
- switch (phase) {
1254
- case 'idea':
1255
- return '[?]';
1256
- case 'backlog':
1257
- return '[ ]';
1258
- case 'ready':
1259
- return '[>]';
1260
- case 'active':
1261
- return '[*]';
1262
- case 'review':
1263
- return '[R]';
1264
- case 'done':
1265
- return '[x]';
1266
- case 'cancelled':
1267
- return '[-]';
1268
- default:
1269
- return '[ ]';
1270
- }
1271
- }
1272
- /**
1273
- * Format a date string for display
1274
- */
1275
- function formatDate(dateStr) {
1276
- try {
1277
- const date = new Date(dateStr);
1278
- return date.toLocaleString();
1279
- }
1280
- catch {
1281
- return dateStr;
1282
- }
1283
- }
1284
- /**
1285
- * Format dependencies output
1286
- */
1287
- function formatDependencies(response) {
1288
- console.log('');
1289
- // Dependencies (jobs this one depends on)
1290
- if (response.dependencies.length > 0) {
1291
- console.log('Dependencies (this job depends on):');
1292
- for (const dep of response.dependencies) {
1293
- const phaseIcon = getPhaseIcon(dep.phase);
1294
- const isBlocking = response.blocking.some((b) => b.id === dep.id);
1295
- const blockingWarning = isBlocking ? ' ⚠️ BLOCKING' : '';
1296
- const relationType = dep.relation_type !== 'blocks' ? ` (${dep.relation_type})` : '';
1297
- console.log(` ${dep.id} ${phaseIcon} "${dep.title}"${relationType}${blockingWarning}`);
1298
- }
1299
- }
1300
- else {
1301
- console.log('Dependencies (this job depends on): None');
1302
- }
1303
- console.log('');
1304
- // Dependents (jobs that depend on this one)
1305
- if (response.dependents.length > 0) {
1306
- console.log('Dependents (jobs depending on this):');
1307
- for (const dependent of response.dependents) {
1308
- const phaseIcon = getPhaseIcon(dependent.phase);
1309
- const relationType = dependent.relation_type !== 'blocks' ? ` (${dependent.relation_type})` : '';
1310
- console.log(` ${dependent.id} ${phaseIcon} "${dependent.title}"${relationType}`);
1311
- }
1312
- }
1313
- else {
1314
- console.log('Dependents (jobs depending on this): None');
1315
- }
1316
- console.log('');
1317
- }
1318
- /**
1319
- * eve jobs claim <id> [--agent=X] [--harness=X]
1320
- * Claims a job, creating an attempt and transitioning to active phase
1321
- */
1322
- async function handleClaim(positionals, flags, context, json) {
1323
- const jobId = positionals[0];
1324
- if (!jobId) {
1325
- throw new Error('Usage: eve jobs claim <job-id> [--agent=X] [--harness=X]');
1326
- }
1327
- const agentId = (0, args_1.getStringFlag)(flags, ['agent']) ?? 'cli-user';
1328
- const harness = (0, args_1.getStringFlag)(flags, ['harness']);
1329
- const body = {
1330
- agent_id: agentId,
1331
- };
1332
- if (harness) {
1333
- body.harness = harness;
1334
- }
1335
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/claim`, {
1336
- method: 'POST',
1337
- body,
1338
- });
1339
- if (json) {
1340
- (0, output_1.outputJson)(response, json);
1341
- }
1342
- else {
1343
- console.log(`Claimed job: ${jobId}`);
1344
- console.log('');
1345
- console.log(` Attempt: #${response.attempt.attempt_number}`);
1346
- console.log(` Attempt ID: ${response.attempt.id}`);
1347
- console.log(` Status: ${response.attempt.status}`);
1348
- console.log(` Agent: ${response.attempt.agent_id}`);
1349
- if (response.attempt.harness) {
1350
- console.log(` Harness: ${response.attempt.harness}`);
1351
- }
1352
- console.log(` Started: ${formatDate(response.attempt.started_at)}`);
1353
- }
1354
- }
1355
- /**
1356
- * eve jobs release <id> [--agent=X] [--reason="..."]
1357
- * Releases a job, ending the current attempt and setting back to ready
1358
- */
1359
- async function handleRelease(positionals, flags, context, json) {
1360
- const jobId = positionals[0];
1361
- if (!jobId) {
1362
- throw new Error('Usage: eve jobs release <job-id> [--agent=X] [--reason="..."]');
1363
- }
1364
- const agentId = (0, args_1.getStringFlag)(flags, ['agent']) ?? 'cli-user';
1365
- const reason = (0, args_1.getStringFlag)(flags, ['reason']);
1366
- const body = {
1367
- agent_id: agentId,
1368
- };
1369
- if (reason) {
1370
- body.reason = reason;
1371
- }
1372
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/release`, {
1373
- method: 'POST',
1374
- body,
1375
- });
1376
- if (json) {
1377
- (0, output_1.outputJson)(response, json);
1378
- }
1379
- else {
1380
- console.log(`Released job: ${jobId}`);
1381
- console.log('');
1382
- console.log(` Phase: ${response.job.phase}`);
1383
- console.log(` Assignee: ${response.job.assignee ?? '(none)'}`);
1384
- if (reason) {
1385
- console.log(` Reason: ${reason}`);
1386
- }
1387
- }
1388
- }
1389
- /**
1390
- * eve jobs attempts <id>
1391
- * Lists all attempts for a job
1392
- */
1393
- async function handleAttempts(positionals, context, json) {
1394
- const jobId = positionals[0];
1395
- if (!jobId) {
1396
- throw new Error('Usage: eve jobs attempts <job-id>');
1397
- }
1398
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
1399
- if (json) {
1400
- (0, output_1.outputJson)(response, json);
1401
- }
1402
- else {
1403
- if (response.attempts.length === 0) {
1404
- console.log('No attempts found for this job.');
1405
- return;
1406
- }
1407
- console.log(`Attempts for job: ${jobId}`);
1408
- console.log('');
1409
- for (const attempt of response.attempts) {
1410
- const statusIcon = getStatusIcon(attempt.status);
1411
- const duration = attempt.ended_at
1412
- ? formatDuration(attempt.started_at, attempt.ended_at)
1413
- : 'running';
1414
- console.log(`${statusIcon} Attempt #${attempt.attempt_number} (${attempt.id})`);
1415
- console.log(` Status: ${attempt.status}`);
1416
- console.log(` Trigger: ${attempt.trigger_type}`);
1417
- if (attempt.agent_id) {
1418
- console.log(` Agent: ${attempt.agent_id}`);
1419
- }
1420
- if (attempt.harness) {
1421
- console.log(` Harness: ${attempt.harness}`);
1422
- }
1423
- console.log(` Started: ${formatDate(attempt.started_at)}`);
1424
- if (attempt.ended_at) {
1425
- console.log(` Ended: ${formatDate(attempt.ended_at)}`);
1426
- }
1427
- console.log(` Duration: ${duration}`);
1428
- if (attempt.result_summary) {
1429
- console.log(` Summary: ${attempt.result_summary}`);
1430
- }
1431
- console.log('');
1432
- }
1433
- console.log(`Total: ${response.attempts.length} attempt(s)`);
1434
- }
1435
- }
1436
- /**
1437
- * eve jobs logs <id> [--attempt=N] [--after=N] [--follow]
1438
- * View execution logs for a job attempt
1439
- */
1440
- async function handleLogs(positionals, flags, context, json) {
1441
- const jobId = positionals[0];
1442
- if (!jobId) {
1443
- throw new Error('Usage: eve jobs logs <job-id> [--attempt=N] [--after=N]');
1444
- }
1445
- // Get attempt number (default to latest)
1446
- const attemptStr = (0, args_1.getStringFlag)(flags, ['attempt']);
1447
- let attemptNum;
1448
- if (attemptStr) {
1449
- attemptNum = parseInt(attemptStr, 10);
1450
- }
1451
- else {
1452
- // Find the latest attempt
1453
- const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
1454
- if (attemptsResponse.attempts.length === 0) {
1455
- console.log('No attempts found for this job.');
1456
- return;
1457
- }
1458
- attemptNum = Math.max(...attemptsResponse.attempts.map(a => a.attempt_number));
1459
- }
1460
- const afterStr = (0, args_1.getStringFlag)(flags, ['after']);
1461
- const afterQuery = afterStr ? `?after=${afterStr}` : '';
1462
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts/${attemptNum}/logs${afterQuery}`);
1463
- if (json) {
1464
- (0, output_1.outputJson)(response, json);
1465
- }
1466
- else {
1467
- if (response.logs.length === 0) {
1468
- console.log(`No logs found for attempt #${attemptNum}.`);
1469
- return;
1470
- }
1471
- console.log(`Logs for job ${jobId}, attempt #${attemptNum}:`);
1472
- console.log('');
1473
- for (const log of response.logs) {
1474
- formatLogEntry(log);
1475
- }
1476
- console.log('');
1477
- console.log(`Total: ${response.logs.length} log entries`);
1478
- }
1479
- }
1480
- /**
1481
- * Format a single log entry for display
1482
- */
1483
- function formatLogEntry(log) {
1484
- const line = log.line;
1485
- const timestamp = new Date(log.timestamp).toLocaleTimeString();
1486
- // Common log line formats from harnesses
1487
- // Type can be at top level (from API) or inside line (from content)
1488
- const type = log.type || line.type || 'log';
1489
- // If type starts with 'lifecycle_', format as lifecycle event
1490
- if (type.startsWith('lifecycle_')) {
1491
- const content = line;
1492
- const phase = content.phase || 'unknown';
1493
- const action = content.action || 'unknown';
1494
- const duration = content.duration_ms;
1495
- const success = content.success;
1496
- const error = content.error;
1497
- const meta = content.meta || {};
1498
- if (action === 'start') {
1499
- const detail = formatLifecycleMeta(phase, meta);
1500
- console.log(`[${timestamp}] ${getLifecycleIcon(phase)} Starting ${phase}${detail}...`);
1501
- }
1502
- else if (action === 'end') {
1503
- const durationStr = duration ? ` (${duration}ms)` : '';
1504
- if (success === false && error) {
1505
- console.log(`[${timestamp}] ${getLifecycleIcon(phase)} ${capitalize(phase)} failed${durationStr}: ${error}`);
1506
- }
1507
- else {
1508
- console.log(`[${timestamp}] ${getLifecycleIcon(phase)} ${capitalize(phase)} completed${durationStr}`);
1509
- }
1510
- }
1511
- else if (action === 'log') {
1512
- const msg = meta.message || JSON.stringify(meta);
1513
- console.log(`[${timestamp}] > ${msg}`);
1514
- }
1515
- return;
1516
- }
1517
- const message = line.message || line.text || '';
1518
- const tool = line.tool;
1519
- const toolInput = line.tool_input;
1520
- const toolResult = line.tool_result;
1521
- // Format based on log type
1522
- switch (type) {
1523
- case 'assistant':
1524
- case 'text':
1525
- console.log(`[${timestamp}] 🤖 ${message || JSON.stringify(line)}`);
1526
- break;
1527
- case 'tool_use':
1528
- console.log(`[${timestamp}] 🔧 ${tool || 'tool'}: ${toolInput || JSON.stringify(line)}`);
1529
- break;
1530
- case 'tool_result':
1531
- const resultPreview = (toolResult || '').substring(0, 100);
1532
- console.log(`[${timestamp}] → ${resultPreview}${(toolResult?.length || 0) > 100 ? '...' : ''}`);
1533
- break;
1534
- case 'error':
1535
- console.log(`[${timestamp}] ❌ ${message || JSON.stringify(line)}`);
1536
- break;
1537
- case 'status':
1538
- console.log(`[${timestamp}] ℹ️ ${message || JSON.stringify(line)}`);
1539
- break;
1540
- default:
1541
- // Generic JSON output for unknown types
1542
- console.log(`[${timestamp}] ${JSON.stringify(line)}`);
1543
- }
1544
- }
1545
- /**
1546
- * Get icon for lifecycle phase
1547
- */
1548
- function getLifecycleIcon(phase) {
1549
- switch (phase) {
1550
- case 'workspace': return '📁';
1551
- case 'hook': return '🪝';
1552
- case 'secrets': return '🔐';
1553
- case 'services': return '🐳';
1554
- case 'harness': return '🤖';
1555
- case 'runner': return '☸️';
1556
- default: return '⚙️';
1557
- }
1558
- }
1559
- /**
1560
- * Capitalize first letter of string
1561
- */
1562
- function capitalize(str) {
1563
- return str.charAt(0).toUpperCase() + str.slice(1);
1564
- }
1565
- /**
1566
- * Format lifecycle metadata for display
1567
- */
1568
- function formatLifecycleMeta(phase, meta) {
1569
- switch (phase) {
1570
- case 'workspace':
1571
- const repoUrl = meta.repo_url || '';
1572
- const branch = meta.branch || '';
1573
- return branch ? ` (${repoUrl}@${branch})` : repoUrl ? ` (${repoUrl})` : '';
1574
- case 'hook':
1575
- return meta.hook_name ? ` "${meta.hook_name}"` : '';
1576
- case 'secrets':
1577
- return '';
1578
- case 'services':
1579
- const svcName = meta.service_name || '';
1580
- return svcName ? ` "${svcName}"` : '';
1581
- case 'harness':
1582
- return meta.harness ? ` ${meta.harness}` : '';
1583
- case 'runner':
1584
- return meta.pod_name ? ` (${meta.pod_name})` : '';
1585
- default:
1586
- return '';
1587
- }
1588
- }
1589
- /**
1590
- * eve jobs submit <id> --summary="..."
1591
- * Submit a job for review
1592
- */
1593
- async function handleSubmit(positionals, flags, context, json) {
1594
- const jobId = positionals[0];
1595
- if (!jobId) {
1596
- throw new Error('Usage: eve jobs submit <job-id> --summary="..."');
1597
- }
1598
- const summary = (0, args_1.getStringFlag)(flags, ['summary']);
1599
- if (!summary) {
1600
- throw new Error('--summary is required');
1601
- }
1602
- // Use a default agent ID if not provided (could be from context in future)
1603
- const agentId = (0, args_1.getStringFlag)(flags, ['agent-id']) ?? 'cli-user';
1604
- const body = {
1605
- agent_id: agentId,
1606
- summary,
1607
- };
1608
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/submit`, {
1609
- method: 'POST',
1610
- body,
1611
- });
1612
- if (json) {
1613
- (0, output_1.outputJson)(response, json);
1614
- }
1615
- else {
1616
- console.log(`Submitted job for review: ${response.id}`);
1617
- console.log(` Phase: ${response.phase}`);
1618
- console.log(` Review Status: ${response.review_status ?? 'N/A'}`);
1619
- console.log(` Summary: ${summary}`);
1620
- }
1621
- }
1622
- /**
1623
- * eve jobs approve <id> [--comment="..."]
1624
- * Approve a job in review
1625
- */
1626
- async function handleApprove(positionals, flags, context, json) {
1627
- const jobId = positionals[0];
1628
- if (!jobId) {
1629
- throw new Error('Usage: eve jobs approve <job-id> [--comment="..."]');
1630
- }
1631
- const comment = (0, args_1.getStringFlag)(flags, ['comment']);
1632
- const reviewerId = (0, args_1.getStringFlag)(flags, ['reviewer-id']) ?? 'cli-user';
1633
- const body = {
1634
- reviewer_id: reviewerId,
1635
- };
1636
- if (comment) {
1637
- body.comment = comment;
1638
- }
1639
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/approve`, {
1640
- method: 'POST',
1641
- body,
1642
- });
1643
- if (json) {
1644
- (0, output_1.outputJson)(response, json);
1645
- }
1646
- else {
1647
- console.log(`Approved job: ${response.id}`);
1648
- console.log(` Phase: ${response.phase}`);
1649
- console.log(` Review Status: ${response.review_status ?? 'N/A'}`);
1650
- console.log(` Reviewer: ${response.reviewer ?? 'N/A'}`);
1651
- if (comment) {
1652
- console.log(` Comment: ${comment}`);
1653
- }
1654
- }
1655
- }
1656
- /**
1657
- * eve jobs reject <id> --reason="..."
1658
- * Reject a job in review
1659
- */
1660
- async function handleReject(positionals, flags, context, json) {
1661
- const jobId = positionals[0];
1662
- if (!jobId) {
1663
- throw new Error('Usage: eve jobs reject <job-id> --reason="..."');
1664
- }
1665
- const reason = (0, args_1.getStringFlag)(flags, ['reason']);
1666
- if (!reason) {
1667
- throw new Error('--reason is required');
1668
- }
1669
- const reviewerId = (0, args_1.getStringFlag)(flags, ['reviewer-id']) ?? 'cli-user';
1670
- const body = {
1671
- reviewer_id: reviewerId,
1672
- reason,
1673
- };
1674
- const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/reject`, {
1675
- method: 'POST',
1676
- body,
1677
- });
1678
- if (json) {
1679
- (0, output_1.outputJson)(response, json);
1680
- }
1681
- else {
1682
- console.log(`Rejected job: ${response.id}`);
1683
- console.log(` Phase: ${response.phase}`);
1684
- console.log(` Review Status: ${response.review_status ?? 'N/A'}`);
1685
- console.log(` Reviewer: ${response.reviewer ?? 'N/A'}`);
1686
- console.log(` Reason: ${reason}`);
1687
- console.log('');
1688
- console.log('A new attempt has been created automatically for retry.');
1689
- }
1690
- }
1691
- /**
1692
- * eve jobs result <id> [--format=text|json|full] [--attempt=N]
1693
- * Fetch and display the result from a completed job attempt
1694
- */
1695
- async function handleResult(positionals, flags, context) {
1696
- const jobId = positionals[0];
1697
- if (!jobId) {
1698
- throw new Error('Usage: eve jobs result <job-id> [--format=text|json|full] [--attempt=N]');
1699
- }
1700
- const format = (0, args_1.getStringFlag)(flags, ['format']) ?? 'text';
1701
- const attemptStr = (0, args_1.getStringFlag)(flags, ['attempt']);
1702
- // Validate format
1703
- if (!['text', 'json', 'full'].includes(format)) {
1704
- throw new Error('Invalid format. Use --format=text|json|full');
1705
- }
1706
- // Build query params
1707
- const query = attemptStr ? `?attempt=${attemptStr}` : '';
1708
- let response;
1709
- try {
1710
- response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/result${query}`);
1711
- }
1712
- catch (error) {
1713
- const err = error;
1714
- // Check for specific error cases
1715
- if (err.statusCode === 404) {
1716
- console.error(`Error: Job '${jobId}' not found.`);
1717
- process.exit(1);
1718
- }
1719
- throw error;
1720
- }
1721
- // Handle job still running
1722
- if (response.status === 'running' || response.status === 'pending') {
1723
- console.error(`Error: Job is still ${response.status}. Use 'eve job wait ${jobId}' to wait for completion.`);
1724
- process.exit(1);
1725
- }
1726
- // Handle failed job
1727
- if (response.status === 'failed') {
1728
- console.error(`Job failed (attempt #${response.attemptNumber}):`);
1729
- if (response.errorMessage) {
1730
- console.error(` ${response.errorMessage}`);
1731
- }
1732
- else {
1733
- console.error(' No error message available.');
1734
- }
1735
- process.exit(1);
1736
- }
1737
- // Format output based on --format flag
1738
- switch (format) {
1739
- case 'text':
1740
- formatResultText(response);
1741
- break;
1742
- case 'json':
1743
- formatResultJson(response);
1744
- break;
1745
- case 'full':
1746
- formatResultFull(response);
1747
- break;
1748
- }
1749
- }
1750
- /**
1751
- * Format result as plain text (default)
1752
- */
1753
- function formatResultText(response) {
1754
- // Display preview URL if present in result_json.pipeline_output
1755
- if (response.resultJson) {
1756
- const pipelineOutput = response.resultJson.pipeline_output;
1757
- if (pipelineOutput?.deploy?.preview_url) {
1758
- console.log(`Preview: ${pipelineOutput.deploy.preview_url}`);
1759
- console.log('');
1760
- }
1761
- }
1762
- if (response.resultText) {
1763
- console.log(response.resultText);
1764
- }
1765
- else {
1766
- console.log('(no result text)');
1767
- }
1768
- }
1769
- /**
1770
- * Format result as JSON
1771
- */
1772
- function formatResultJson(response) {
1773
- // Extract preview URL if present in result_json.pipeline_output
1774
- let previewUrl;
1775
- if (response.resultJson) {
1776
- const pipelineOutput = response.resultJson.pipeline_output;
1777
- previewUrl = pipelineOutput?.deploy?.preview_url;
1778
- }
1779
- const output = {
1780
- success: response.status === 'succeeded',
1781
- exitCode: response.exitCode,
1782
- resultText: response.resultText,
1783
- resultJson: response.resultJson,
1784
- durationMs: response.durationMs,
1785
- tokenUsage: response.tokenInput !== null || response.tokenOutput !== null
1786
- ? { input: response.tokenInput, output: response.tokenOutput }
1787
- : null,
1788
- ...(previewUrl ? { previewUrl } : {}),
1789
- };
1790
- console.log(JSON.stringify(output, null, 2));
1791
- }
1792
- /**
1793
- * Format result with full details
1794
- */
1795
- function formatResultFull(response) {
1796
- console.log(`Job: ${response.jobId}`);
1797
- console.log(`Attempt: ${response.attemptNumber}`);
1798
- console.log(`Status: ${response.status}`);
1799
- if (response.durationMs !== null) {
1800
- const durationSec = Math.round(response.durationMs / 1000);
1801
- console.log(`Duration: ${durationSec}s`);
1802
- }
1803
- if (response.tokenInput !== null || response.tokenOutput !== null) {
1804
- const inputStr = response.tokenInput !== null ? formatNumber(response.tokenInput) : '0';
1805
- const outputStr = response.tokenOutput !== null ? formatNumber(response.tokenOutput) : '0';
1806
- console.log(`Tokens: ${inputStr} in / ${outputStr} out`);
1807
- }
1808
- // Display preview URL if present in result_json.pipeline_output
1809
- if (response.resultJson) {
1810
- const pipelineOutput = response.resultJson.pipeline_output;
1811
- if (pipelineOutput?.deploy?.preview_url) {
1812
- console.log(`Preview: ${pipelineOutput.deploy.preview_url}`);
1813
- }
1814
- // Display git metadata if present in result_json
1815
- const resolvedGit = response.resultJson.resolved_git;
1816
- if (resolvedGit) {
1817
- console.log('');
1818
- console.log('Git:');
1819
- if (resolvedGit.resolved_sha)
1820
- console.log(` SHA: ${resolvedGit.resolved_sha}`);
1821
- if (resolvedGit.resolved_branch)
1822
- console.log(` Branch: ${resolvedGit.resolved_branch}`);
1823
- if (resolvedGit.ref_source)
1824
- console.log(` Source: ${resolvedGit.ref_source}`);
1825
- if (resolvedGit.pushed !== undefined)
1826
- console.log(` Pushed: ${resolvedGit.pushed}`);
1827
- if (resolvedGit.commits?.length)
1828
- console.log(` Commits: ${resolvedGit.commits.join(', ')}`);
1829
- }
1830
- }
1831
- console.log('');
1832
- console.log('Result:');
1833
- if (response.resultText) {
1834
- console.log(response.resultText);
1835
- }
1836
- else {
1837
- console.log('(no result text)');
1838
- }
1839
- }
1840
- /**
1841
- * Format a number with commas for readability
1842
- */
1843
- function formatNumber(num) {
1844
- return num.toLocaleString();
1845
- }
1846
- // Exit codes for wait command
1847
- const EXIT_CODE_TIMEOUT = 124;
1848
- const EXIT_CODE_CANCELLED = 125;
1849
- /**
1850
- * eve job wait <id> [--timeout=300] [--quiet] [--verbose] [--json]
1851
- * Wait for a job to complete, polling the wait endpoint
1852
- */
1853
- async function handleWait(positionals, flags, context) {
1854
- const jobId = positionals[0];
1855
- if (!jobId) {
1856
- throw new Error('Usage: eve job wait <job-id> [--timeout=300] [--quiet] [--verbose] [--json]');
1857
- }
1858
- const maxTimeout = parseInt((0, args_1.getStringFlag)(flags, ['timeout']) ?? '300', 10);
1859
- const quiet = Boolean(flags.quiet);
1860
- const verbose = Boolean(flags.verbose);
1861
- const json = Boolean(flags.json);
1862
- const startTime = Date.now();
1863
- let totalElapsed = 0;
1864
- // Track state changes for verbose mode
1865
- let lastPhase;
1866
- let lastStatus;
1867
- let lastPeriodicUpdate = 0;
1868
- const periodicUpdateInterval = 5; // seconds
1869
- // Progress output
1870
- if (!quiet && !json) {
1871
- if (verbose) {
1872
- console.log(`[0s] Waiting for job ${jobId}...`);
1873
- }
1874
- else {
1875
- console.log(`Waiting for ${jobId}...`);
1876
- }
1877
- }
1878
- // Poll with increasing timeout values to reduce request count
1879
- // Start with 5s, increase to 30s, cap at 60s per poll
1880
- let pollTimeout = 5;
1881
- while (totalElapsed < maxTimeout) {
1882
- // Calculate remaining time and cap poll timeout (must be integer for API)
1883
- const remainingTime = maxTimeout - totalElapsed;
1884
- const currentPollTimeout = Math.floor(Math.min(pollTimeout, remainingTime, 60));
1885
- const response = await (0, client_1.requestRaw)(context, `/jobs/${jobId}/wait?timeout=${currentPollTimeout}`);
1886
- if (response.status === 200) {
1887
- // Job completed - response.data is JobResultResponse
1888
- const result = response.data;
1889
- const totalDuration = Math.round((Date.now() - startTime) / 1000);
1890
- // Show error message immediately in verbose mode if job failed
1891
- if (verbose && !quiet && !json && result.status === 'failed' && result.errorMessage) {
1892
- console.log(`[${totalDuration}s] Job failed: ${result.errorMessage}`);
1893
- }
1894
- if (json) {
1895
- (0, output_1.outputJson)({
1896
- jobId: result.jobId,
1897
- status: result.status,
1898
- exitCode: result.exitCode,
1899
- durationMs: result.durationMs,
1900
- resultText: result.resultText,
1901
- resultJson: result.resultJson,
1902
- waitDurationSeconds: totalDuration,
1903
- }, true);
1904
- }
1905
- else if (!quiet) {
1906
- if (verbose) {
1907
- console.log(`[${totalDuration}s] Completed`);
1908
- }
1909
- else {
1910
- console.log(`[+] Completed in ${totalDuration}s`);
1911
- }
1912
- console.log('');
1913
- if (result.resultText) {
1914
- console.log(result.resultText);
1915
- }
1916
- else if (result.resultJson) {
1917
- console.log(JSON.stringify(result.resultJson, null, 2));
1918
- }
1919
- }
1920
- // Exit with job's exit code or based on status
1921
- if (result.status === 'succeeded') {
1922
- process.exit(result.exitCode ?? 0);
1923
- }
1924
- else if (result.status === 'failed') {
1925
- process.exit(result.exitCode ?? 1);
1926
- }
1927
- else if (result.status === 'cancelled') {
1928
- process.exit(EXIT_CODE_CANCELLED);
1929
- }
1930
- // Default exit code
1931
- process.exit(result.exitCode ?? 0);
1932
- }
1933
- else if (response.status === 202) {
1934
- // Job still running - continue polling
1935
- const timeoutResponse = response.data;
1936
- totalElapsed = Math.round((Date.now() - startTime) / 1000);
1937
- if (!quiet && !json) {
1938
- if (verbose) {
1939
- // Check for phase changes
1940
- if (lastPhase !== undefined && lastPhase !== timeoutResponse.phase) {
1941
- console.log(`[${totalElapsed}s] Phase: ${lastPhase} → ${timeoutResponse.phase}`);
1942
- }
1943
- lastPhase = timeoutResponse.phase;
1944
- // Check for status changes
1945
- if (lastStatus !== undefined && lastStatus !== timeoutResponse.status) {
1946
- console.log(`[${totalElapsed}s] Status: ${lastStatus} → ${timeoutResponse.status}`);
1947
- }
1948
- lastStatus = timeoutResponse.status;
1949
- // Show periodic elapsed time updates
1950
- if (totalElapsed - lastPeriodicUpdate >= periodicUpdateInterval) {
1951
- console.log(`[${totalElapsed}s] Still waiting... (phase: ${timeoutResponse.phase})`);
1952
- lastPeriodicUpdate = totalElapsed;
1953
- }
1954
- }
1955
- else {
1956
- process.stdout.write(`\rWaiting for ${jobId}... (${totalElapsed}s, ${timeoutResponse.status})`);
1957
- }
1958
- }
1959
- // Increase poll timeout for next iteration (with cap)
1960
- pollTimeout = Math.min(pollTimeout * 1.5, 60);
1961
- }
1962
- else if (response.status === 404) {
1963
- console.error(`Error: Job '${jobId}' not found.`);
1964
- process.exit(1);
1965
- }
1966
- else {
1967
- // Unexpected error
1968
- const message = typeof response.data === 'string' ? response.data : response.text;
1969
- throw new Error(`HTTP ${response.status}: ${message}`);
1970
- }
1971
- }
1972
- // Overall timeout reached
1973
- if (!quiet && !json) {
1974
- console.log('');
1975
- }
1976
- if (json) {
1977
- (0, output_1.outputJson)({
1978
- jobId,
1979
- status: 'timeout',
1980
- message: `Timeout after ${maxTimeout}s`,
1981
- waitDurationSeconds: maxTimeout,
1982
- }, true);
1983
- }
1984
- else {
1985
- console.error(`Timeout: Job did not complete within ${maxTimeout}s.`);
1986
- }
1987
- process.exit(EXIT_CODE_TIMEOUT);
1988
- }
1989
- /**
1990
- * eve job watch <id> [--timeout=300]
1991
- * Watch a job by combining status polling + log streaming
1992
- * Shows phase/status changes like `wait --verbose` while streaming logs like `follow`
1993
- */
1994
- async function handleWatch(positionals, flags, context) {
1995
- const jobId = positionals[0];
1996
- if (!jobId) {
1997
- throw new Error('Usage: eve job watch <job-id> [--timeout=300]');
1998
- }
1999
- const maxTimeout = parseInt((0, args_1.getStringFlag)(flags, ['timeout']) ?? '300', 10);
2000
- console.log(`Watching job ${jobId}...`);
2001
- const startTime = Date.now();
2002
- let lastPhase;
2003
- let lastStatus;
2004
- // Set up SSE log streaming
2005
- const url = `${context.apiUrl}/jobs/${jobId}/stream`;
2006
- const headers = {
2007
- Accept: 'text/event-stream',
2008
- };
2009
- if (context.token) {
2010
- headers.Authorization = `Bearer ${context.token}`;
2011
- }
2012
- let exitCode = 0;
2013
- let streamEnded = false;
2014
- let jobCompleted = false;
2015
- // Start status polling in background
2016
- const statusPollingPromise = (async () => {
2017
- while (!jobCompleted && !streamEnded) {
2018
- await new Promise(resolve => setTimeout(resolve, 3000)); // Poll every 3 seconds
2019
- if (jobCompleted || streamEnded)
2020
- break;
2021
- const elapsed = Math.round((Date.now() - startTime) / 1000);
2022
- // Check timeout
2023
- if (elapsed >= maxTimeout) {
2024
- console.log('');
2025
- console.error(`Timeout: Job did not complete within ${maxTimeout}s.`);
2026
- process.exit(EXIT_CODE_TIMEOUT);
2027
- }
2028
- try {
2029
- // Quick status check
2030
- const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
2031
- // Track phase changes
2032
- if (lastPhase !== undefined && lastPhase !== job.phase) {
2033
- console.log(`[${elapsed}s] Phase: ${lastPhase} → ${job.phase}`);
2034
- }
2035
- lastPhase = job.phase;
2036
- // Check if job reached terminal state
2037
- // Note: 'failed' is not a valid job phase - only 'done' and 'cancelled' are terminal
2038
- if (job.phase === 'done' || job.phase === 'cancelled') {
2039
- jobCompleted = true;
2040
- break;
2041
- }
2042
- // Get latest attempt to track status changes
2043
- const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
2044
- if (attemptsResponse.attempts.length > 0) {
2045
- const latest = attemptsResponse.attempts[attemptsResponse.attempts.length - 1];
2046
- if (lastStatus !== undefined && lastStatus !== latest.status) {
2047
- console.log(`[${elapsed}s] Attempt status: ${lastStatus} → ${latest.status}`);
2048
- }
2049
- lastStatus = latest.status;
2050
- }
2051
- }
2052
- catch (error) {
2053
- // Ignore errors during polling, rely on SSE stream
2054
- }
2055
- }
2056
- })();
2057
- // Start SSE log streaming
2058
- try {
2059
- const response = await fetch(url, { headers });
2060
- if (!response.ok) {
2061
- const text = await response.text();
2062
- throw new Error(`HTTP ${response.status}: ${text}`);
2063
- }
2064
- if (!response.body) {
2065
- throw new Error('No response body received');
2066
- }
2067
- const reader = response.body.getReader();
2068
- const decoder = new TextDecoder();
2069
- let buffer = '';
2070
- while (true) {
2071
- const { done, value } = await reader.read();
2072
- if (done) {
2073
- break;
2074
- }
2075
- buffer += decoder.decode(value, { stream: true });
2076
- // Process complete SSE events from buffer
2077
- const lines = buffer.split('\n');
2078
- buffer = lines.pop() ?? ''; // Keep incomplete line in buffer
2079
- let eventType = '';
2080
- let eventData = '';
2081
- for (const line of lines) {
2082
- if (line.startsWith('event:')) {
2083
- eventType = line.slice(6).trim();
2084
- }
2085
- else if (line.startsWith('data:')) {
2086
- eventData = line.slice(5).trim();
2087
- }
2088
- else if (line === '' && eventData) {
2089
- // Empty line marks end of event
2090
- exitCode = processWatchSSEEvent(eventType, eventData);
2091
- if (exitCode !== -1) {
2092
- // Event signals we should exit
2093
- streamEnded = true;
2094
- jobCompleted = true;
2095
- await statusPollingPromise; // Wait for polling to finish
2096
- process.exit(exitCode);
2097
- }
2098
- eventType = '';
2099
- eventData = '';
2100
- }
2101
- }
2102
- }
2103
- // Process any remaining data
2104
- if (buffer.trim()) {
2105
- const remainingLines = buffer.split('\n');
2106
- let eventType = '';
2107
- let eventData = '';
2108
- for (const line of remainingLines) {
2109
- if (line.startsWith('event:')) {
2110
- eventType = line.slice(6).trim();
2111
- }
2112
- else if (line.startsWith('data:')) {
2113
- eventData = line.slice(5).trim();
2114
- }
2115
- }
2116
- if (eventData) {
2117
- exitCode = processWatchSSEEvent(eventType, eventData);
2118
- if (exitCode !== -1) {
2119
- streamEnded = true;
2120
- jobCompleted = true;
2121
- await statusPollingPromise;
2122
- process.exit(exitCode);
2123
- }
2124
- }
2125
- }
2126
- // Stream ended
2127
- streamEnded = true;
2128
- jobCompleted = true;
2129
- await statusPollingPromise;
2130
- const totalDuration = Math.round((Date.now() - startTime) / 1000);
2131
- console.log('');
2132
- console.log(`[${totalDuration}s] Stream ended.`);
2133
- process.exit(exitCode);
2134
- }
2135
- catch (error) {
2136
- streamEnded = true;
2137
- jobCompleted = true;
2138
- await statusPollingPromise;
2139
- const err = error;
2140
- console.error(`Error watching job: ${err.message}`);
2141
- process.exit(1);
2142
- }
2143
- }
2144
- /**
2145
- * Process a single SSE event for watch command
2146
- * Returns exit code if we should exit, or -1 to continue
2147
- */
2148
- function processWatchSSEEvent(eventType, eventData) {
2149
- let parsed;
2150
- try {
2151
- parsed = JSON.parse(eventData);
2152
- }
2153
- catch {
2154
- // If we can't parse, ignore it
2155
- return -1;
2156
- }
2157
- // Handle different event types
2158
- switch (eventType) {
2159
- case 'log': {
2160
- const logEvent = parsed;
2161
- formatFollowLogLine(logEvent);
2162
- return -1;
2163
- }
2164
- case 'complete': {
2165
- const completeEvent = parsed;
2166
- console.log('');
2167
- console.log('[+] Completed');
2168
- if (completeEvent.result) {
2169
- console.log('');
2170
- console.log(completeEvent.result);
2171
- }
2172
- return completeEvent.exit_code ?? 0;
2173
- }
2174
- case 'error': {
2175
- const errorEvent = parsed;
2176
- console.log('');
2177
- console.log('[!] Error');
2178
- if (errorEvent.error) {
2179
- console.log(` ${errorEvent.error}`);
2180
- }
2181
- return errorEvent.exit_code ?? 1;
2182
- }
2183
- default:
2184
- return -1;
2185
- }
2186
- }
2187
- /**
2188
- * eve job follow <id> [--raw] [--no-result]
2189
- * Stream logs from a running job in real-time via SSE
2190
- */
2191
- async function handleFollow(positionals, flags, context) {
2192
- const jobId = positionals[0];
2193
- if (!jobId) {
2194
- throw new Error('Usage: eve job follow <job-id> [--raw] [--no-result]');
2195
- }
2196
- const raw = Boolean(flags.raw);
2197
- const showResult = !flags['no-result'];
2198
- console.log(`Following ${jobId}...`);
2199
- const url = `${context.apiUrl}/jobs/${jobId}/stream`;
2200
- const headers = {
2201
- Accept: 'text/event-stream',
2202
- };
2203
- if (context.token) {
2204
- headers.Authorization = `Bearer ${context.token}`;
2205
- }
2206
- let exitCode = 0;
2207
- try {
2208
- const response = await fetch(url, { headers });
2209
- if (!response.ok) {
2210
- const text = await response.text();
2211
- throw new Error(`HTTP ${response.status}: ${text}`);
2212
- }
2213
- if (!response.body) {
2214
- throw new Error('No response body received');
2215
- }
2216
- const reader = response.body.getReader();
2217
- const decoder = new TextDecoder();
2218
- let buffer = '';
2219
- while (true) {
2220
- const { done, value } = await reader.read();
2221
- if (done) {
2222
- break;
2223
- }
2224
- buffer += decoder.decode(value, { stream: true });
2225
- // Process complete SSE events from buffer
2226
- const lines = buffer.split('\n');
2227
- buffer = lines.pop() ?? ''; // Keep incomplete line in buffer
2228
- let eventType = '';
2229
- let eventData = '';
2230
- for (const line of lines) {
2231
- if (line.startsWith('event:')) {
2232
- eventType = line.slice(6).trim();
2233
- }
2234
- else if (line.startsWith('data:')) {
2235
- eventData = line.slice(5).trim();
2236
- }
2237
- else if (line === '' && eventData) {
2238
- // Empty line marks end of event
2239
- exitCode = processSSEEvent(eventType, eventData, raw, showResult);
2240
- if (exitCode !== -1) {
2241
- // Event signals we should exit
2242
- process.exit(exitCode);
2243
- }
2244
- eventType = '';
2245
- eventData = '';
2246
- }
2247
- }
2248
- }
2249
- // Process any remaining data
2250
- if (buffer.trim()) {
2251
- const remainingLines = buffer.split('\n');
2252
- let eventType = '';
2253
- let eventData = '';
2254
- for (const line of remainingLines) {
2255
- if (line.startsWith('event:')) {
2256
- eventType = line.slice(6).trim();
2257
- }
2258
- else if (line.startsWith('data:')) {
2259
- eventData = line.slice(5).trim();
2260
- }
2261
- }
2262
- if (eventData) {
2263
- exitCode = processSSEEvent(eventType, eventData, raw, showResult);
2264
- if (exitCode !== -1) {
2265
- process.exit(exitCode);
2266
- }
2267
- }
2268
- }
2269
- // Stream ended without explicit complete/error event
2270
- console.log('');
2271
- console.log('Stream ended.');
2272
- process.exit(0);
2273
- }
2274
- catch (error) {
2275
- const err = error;
2276
- console.error(`Error following job: ${err.message}`);
2277
- process.exit(1);
2278
- }
2279
- }
2280
- /**
2281
- * Process a single SSE event
2282
- * Returns exit code if we should exit, or -1 to continue
2283
- */
2284
- function processSSEEvent(eventType, eventData, raw, showResult) {
2285
- let parsed;
2286
- try {
2287
- parsed = JSON.parse(eventData);
2288
- }
2289
- catch {
2290
- // If we can't parse, treat as raw text
2291
- if (raw) {
2292
- console.log(eventData);
2293
- }
2294
- return -1;
2295
- }
2296
- // Handle different event types
2297
- switch (eventType) {
2298
- case 'log': {
2299
- const logEvent = parsed;
2300
- if (raw) {
2301
- console.log(JSON.stringify(logEvent));
2302
- }
2303
- else {
2304
- formatFollowLogLine(logEvent);
2305
- }
2306
- return -1;
2307
- }
2308
- case 'complete': {
2309
- const completeEvent = parsed;
2310
- if (raw) {
2311
- console.log(JSON.stringify(completeEvent));
2312
- }
2313
- else {
2314
- console.log('[+] Completed');
2315
- if (showResult && completeEvent.result) {
2316
- console.log('');
2317
- console.log(completeEvent.result);
2318
- }
2319
- }
2320
- return completeEvent.exit_code ?? 0;
2321
- }
2322
- case 'error': {
2323
- const errorEvent = parsed;
2324
- if (raw) {
2325
- console.log(JSON.stringify(errorEvent));
2326
- }
2327
- else {
2328
- console.log('[!] Error');
2329
- if (errorEvent.error) {
2330
- console.log(` ${errorEvent.error}`);
2331
- }
2332
- }
2333
- return errorEvent.exit_code ?? 1;
2334
- }
2335
- default: {
2336
- // Unknown event type, still output if raw mode
2337
- if (raw) {
2338
- console.log(JSON.stringify(parsed));
2339
- }
2340
- return -1;
2341
- }
2342
- }
2343
- }
2344
- /**
2345
- * Format a log line for human-readable output
2346
- */
2347
- function formatFollowLogLine(event) {
2348
- const timestamp = new Date(event.timestamp).toLocaleTimeString();
2349
- const line = event.line;
2350
- const type = event.type || line.type || 'log';
2351
- // If type starts with 'lifecycle_', format as lifecycle event
2352
- if (type.startsWith('lifecycle_')) {
2353
- const content = line;
2354
- const phase = content.phase || 'unknown';
2355
- const action = content.action || 'unknown';
2356
- const duration = content.duration_ms;
2357
- const success = content.success;
2358
- const error = content.error;
2359
- const meta = content.meta || {};
2360
- if (action === 'start') {
2361
- const detail = formatLifecycleMeta(phase, meta);
2362
- console.log(`[${timestamp}] ${getLifecycleIcon(phase)} Starting ${phase}${detail}...`);
2363
- }
2364
- else if (action === 'end') {
2365
- const durationStr = duration ? ` (${duration}ms)` : '';
2366
- if (success === false && error) {
2367
- console.log(`[${timestamp}] ${getLifecycleIcon(phase)} ${capitalize(phase)} failed${durationStr}: ${error}`);
2368
- }
2369
- else {
2370
- console.log(`[${timestamp}] ${getLifecycleIcon(phase)} ${capitalize(phase)} completed${durationStr}`);
2371
- }
2372
- }
2373
- else if (action === 'log') {
2374
- const msg = meta.message || JSON.stringify(meta);
2375
- console.log(`[${timestamp}] > ${msg}`);
2376
- }
2377
- return;
2378
- }
2379
- // Extract common fields
2380
- const message = line.message || line.text || '';
2381
- const tool = line.tool;
2382
- const toolInput = line.tool_input;
2383
- // Format based on event/log type
2384
- switch (type) {
2385
- case 'assistant':
2386
- case 'text':
2387
- if (message) {
2388
- console.log(`[${timestamp}] ${message}`);
2389
- }
2390
- break;
2391
- case 'tool_use':
2392
- if (tool) {
2393
- // Show tool name with optional truncated input
2394
- const inputPreview = toolInput ? ` ${toolInput.substring(0, 60)}${toolInput.length > 60 ? '...' : ''}` : '';
2395
- console.log(`[${timestamp}] Tool: ${tool}${inputPreview}`);
2396
- }
2397
- break;
2398
- case 'tool_result':
2399
- // Don't show tool results in follow mode (too verbose)
2400
- break;
2401
- case 'status':
2402
- console.log(`[${timestamp}] ${message || JSON.stringify(line)}`);
2403
- break;
2404
- case 'error':
2405
- console.log(`[${timestamp}] Error: ${message || JSON.stringify(line)}`);
2406
- break;
2407
- default:
2408
- // For unknown types, show as JSON if it has content
2409
- if (message) {
2410
- console.log(`[${timestamp}] ${message}`);
2411
- }
2412
- else if (Object.keys(line).length > 0) {
2413
- console.log(`[${timestamp}] ${JSON.stringify(line)}`);
2414
- }
2415
- }
2416
- }
2417
- /**
2418
- * Get status icon for attempt status
2419
- */
2420
- function getStatusIcon(status) {
2421
- switch (status) {
2422
- case 'pending':
2423
- return '[⏸]';
2424
- case 'running':
2425
- return '[▶]';
2426
- case 'succeeded':
2427
- return '[✓]';
2428
- case 'failed':
2429
- return '[✗]';
2430
- case 'cancelled':
2431
- return '[○]';
2432
- default:
2433
- return '[ ]';
2434
- }
2435
- }
2436
- /**
2437
- * Format duration between two dates
2438
- */
2439
- function formatDuration(startStr, endStr) {
2440
- try {
2441
- const start = new Date(startStr).getTime();
2442
- const end = new Date(endStr).getTime();
2443
- const durationMs = end - start;
2444
- const seconds = Math.floor(durationMs / 1000);
2445
- const minutes = Math.floor(seconds / 60);
2446
- const hours = Math.floor(minutes / 60);
2447
- if (hours > 0) {
2448
- const remainingMinutes = minutes % 60;
2449
- return `${hours}h ${remainingMinutes}m`;
2450
- }
2451
- if (minutes > 0) {
2452
- const remainingSeconds = seconds % 60;
2453
- return `${minutes}m ${remainingSeconds}s`;
2454
- }
2455
- return `${seconds}s`;
2456
- }
2457
- catch {
2458
- return 'unknown';
2459
- }
2460
- }
2461
- /**
2462
- * Pad a string to the right with spaces
2463
- */
2464
- function padRight(str, width) {
2465
- if (str.length >= width)
2466
- return str;
2467
- return str + ' '.repeat(width - str.length);
2468
- }
2469
- /**
2470
- * eve job runner-logs <id> [--attempt N]
2471
- * Stream K8s runner pod logs for a job
2472
- */
2473
- async function handleRunnerLogs(positionals, flags, context) {
2474
- const jobId = positionals[0];
2475
- if (!jobId) {
2476
- throw new Error('Usage: eve job runner-logs <job-id> [--attempt N]');
2477
- }
2478
- const attemptStr = (0, args_1.getStringFlag)(flags, ['attempt']);
2479
- // Fetch job with attempts to get runtime_meta
2480
- const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
2481
- if (!attemptsResponse.attempts || attemptsResponse.attempts.length === 0) {
2482
- console.error(`No attempts found for job ${jobId}`);
2483
- process.exit(1);
2484
- }
2485
- // Select target attempt
2486
- let targetAttempt;
2487
- if (attemptStr) {
2488
- const attemptNum = parseInt(attemptStr, 10);
2489
- targetAttempt = attemptsResponse.attempts.find(a => a.attempt_number === attemptNum);
2490
- if (!targetAttempt) {
2491
- console.error(`Attempt #${attemptNum} not found for job ${jobId}`);
2492
- process.exit(1);
2493
- }
2494
- }
2495
- else {
2496
- // Use latest attempt
2497
- targetAttempt = attemptsResponse.attempts[attemptsResponse.attempts.length - 1];
2498
- }
2499
- if (!targetAttempt) {
2500
- console.error(`No attempt found for job ${jobId}`);
2501
- process.exit(1);
2502
- }
2503
- console.log(`Streaming logs for job ${jobId}, attempt #${targetAttempt.attempt_number}...`);
2504
- // Try to get pod_name from runtime_meta
2505
- const podName = targetAttempt.runtime_meta?.pod_name;
2506
- let kubectlArgs;
2507
- if (podName) {
2508
- // Use pod name directly
2509
- console.log(`Using pod: ${podName}`);
2510
- kubectlArgs = ['-n', 'eve', 'logs', '-f', podName];
2511
- }
2512
- else {
2513
- // Fall back to label-based lookup
2514
- console.log(`No pod_name in runtime_meta, using label selector: job-id=${jobId}`);
2515
- kubectlArgs = ['-n', 'eve', 'logs', '-f', '-l', `job-id=${jobId}`];
2516
- }
2517
- // Spawn kubectl process
2518
- const kubectl = (0, child_process_1.spawn)('kubectl', kubectlArgs, {
2519
- stdio: 'inherit',
2520
- });
2521
- // Handle process exit
2522
- return new Promise((resolve, reject) => {
2523
- kubectl.on('error', (error) => {
2524
- reject(new Error(`Failed to execute kubectl: ${error.message}`));
2525
- });
2526
- kubectl.on('exit', (code) => {
2527
- if (code === 0) {
2528
- resolve();
2529
- }
2530
- else {
2531
- // Non-zero exit is common (e.g., pod terminated, not found)
2532
- // Don't treat as error
2533
- resolve();
2534
- }
2535
- });
2536
- });
2537
- }