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