@agentuity/cli 1.0.55 → 1.0.57
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/dist/cmd/build/ci.d.ts.map +1 -1
- package/dist/cmd/build/ci.js +21 -5
- package/dist/cmd/build/ci.js.map +1 -1
- package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy-fork.js +36 -15
- package/dist/cmd/cloud/deploy-fork.js.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.js +73 -22
- package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
- package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/run.js +6 -0
- package/dist/cmd/cloud/sandbox/run.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.js +2 -2
- package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
- package/dist/errors.d.ts +24 -10
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +42 -12
- package/dist/errors.js.map +1 -1
- package/dist/schema-generator.d.ts.map +1 -1
- package/dist/schema-generator.js +2 -12
- package/dist/schema-generator.js.map +1 -1
- package/dist/utils/stream-capture.d.ts +9 -0
- package/dist/utils/stream-capture.d.ts.map +1 -0
- package/dist/utils/stream-capture.js +34 -0
- package/dist/utils/stream-capture.js.map +1 -0
- package/package.json +6 -6
- package/src/cmd/build/ci.ts +21 -5
- package/src/cmd/cloud/deploy-fork.ts +39 -16
- package/src/cmd/cloud/sandbox/exec.ts +116 -18
- package/src/cmd/cloud/sandbox/run.ts +6 -0
- package/src/cmd/cloud/sandbox/snapshot/build.ts +2 -2
- package/src/errors.ts +44 -12
- package/src/schema-generator.ts +2 -12
- package/src/utils/stream-capture.ts +39 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { spawn, type Subprocess } from 'bun';
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
-
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
16
|
+
import { appendFileSync, createWriteStream, existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
17
17
|
import type { APIClient } from '../../api';
|
|
18
18
|
import { getUserAgent } from '../../api';
|
|
19
19
|
import { isUnicode } from '../../tui/symbols';
|
|
@@ -37,12 +37,14 @@ export interface ForkDeployResult {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Stream data to a Pulse stream URL
|
|
40
|
+
* Stream data to a Pulse stream URL.
|
|
41
|
+
* Accepts a string, Blob/BunFile, or ReadableStream as the body to avoid
|
|
42
|
+
* loading large outputs into memory.
|
|
41
43
|
*/
|
|
42
44
|
async function streamToPulse(
|
|
43
45
|
streamURL: string,
|
|
44
46
|
sdkKey: string,
|
|
45
|
-
data: string
|
|
47
|
+
data: string | Blob | ReadableStream<Uint8Array>,
|
|
46
48
|
logger: Logger
|
|
47
49
|
): Promise<void> {
|
|
48
50
|
try {
|
|
@@ -74,7 +76,8 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
74
76
|
const buildLogsStreamURL = deployment.buildLogsStreamURL;
|
|
75
77
|
const reportFile = join(tmpdir(), `agentuity-deploy-${deploymentId}.json`);
|
|
76
78
|
const cleanLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-logs.txt`);
|
|
77
|
-
|
|
79
|
+
const rawLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-raw.txt`);
|
|
80
|
+
const rawLogsWriter = createWriteStream(rawLogsFile);
|
|
78
81
|
let proc: Subprocess | null = null;
|
|
79
82
|
let cancelled = false;
|
|
80
83
|
|
|
@@ -195,7 +198,6 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
195
198
|
|
|
196
199
|
const handleOutput = async (stream: ReadableStream<Uint8Array>, isStderr: boolean) => {
|
|
197
200
|
const reader = stream.getReader();
|
|
198
|
-
const decoder = new TextDecoder();
|
|
199
201
|
const target = isStderr ? process.stderr : process.stdout;
|
|
200
202
|
|
|
201
203
|
try {
|
|
@@ -203,8 +205,9 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
203
205
|
const { done, value } = await reader.read();
|
|
204
206
|
if (done) break;
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
// Stream raw bytes to disk instead of accumulating in memory.
|
|
209
|
+
// This prevents OOM / ERR_STRING_TOO_LONG crashes on large builds.
|
|
210
|
+
rawLogsWriter.write(value);
|
|
208
211
|
target.write(value);
|
|
209
212
|
}
|
|
210
213
|
} catch (err) {
|
|
@@ -223,6 +226,11 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
223
226
|
|
|
224
227
|
await Promise.all([stdoutPromise, stderrPromise]);
|
|
225
228
|
|
|
229
|
+
// Close the raw logs writer so the file is fully flushed before reading
|
|
230
|
+
await new Promise<void>((resolve) => {
|
|
231
|
+
rawLogsWriter.end(resolve);
|
|
232
|
+
});
|
|
233
|
+
|
|
226
234
|
const exitCode = await proc.exited;
|
|
227
235
|
logger.debug('Child process exited with code: %d', exitCode);
|
|
228
236
|
|
|
@@ -249,12 +257,11 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
249
257
|
logger.debug('Failed to read clean logs file: %s', err);
|
|
250
258
|
}
|
|
251
259
|
}
|
|
252
|
-
// Fall back to raw output if no clean logs
|
|
253
|
-
if (!logsContent && outputBuffer) {
|
|
254
|
-
logsContent = outputBuffer;
|
|
255
|
-
}
|
|
256
260
|
if (logsContent) {
|
|
257
261
|
await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
|
|
262
|
+
} else if (existsSync(rawLogsFile)) {
|
|
263
|
+
// Stream raw logs file directly to Pulse without loading into memory
|
|
264
|
+
await streamToPulse(buildLogsStreamURL, sdkKey, Bun.file(rawLogsFile), logger);
|
|
258
265
|
}
|
|
259
266
|
}
|
|
260
267
|
|
|
@@ -307,11 +314,27 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
307
314
|
// ignore
|
|
308
315
|
}
|
|
309
316
|
}
|
|
310
|
-
if (
|
|
311
|
-
logsContent
|
|
317
|
+
if (logsContent) {
|
|
318
|
+
logsContent += `\n\n--- FORK ERROR ---\n${errorMessage}\n`;
|
|
319
|
+
await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
|
|
320
|
+
} else {
|
|
321
|
+
// Append error to raw logs file and stream it without loading into memory
|
|
322
|
+
try {
|
|
323
|
+
appendFileSync(rawLogsFile, `\n\n--- FORK ERROR ---\n${errorMessage}\n`);
|
|
324
|
+
} catch {
|
|
325
|
+
// ignore — file may not exist if child never produced output
|
|
326
|
+
}
|
|
327
|
+
if (existsSync(rawLogsFile)) {
|
|
328
|
+
await streamToPulse(buildLogsStreamURL, sdkKey, Bun.file(rawLogsFile), logger);
|
|
329
|
+
} else {
|
|
330
|
+
await streamToPulse(
|
|
331
|
+
buildLogsStreamURL,
|
|
332
|
+
sdkKey,
|
|
333
|
+
`--- FORK ERROR ---\n${errorMessage}\n`,
|
|
334
|
+
logger
|
|
335
|
+
);
|
|
336
|
+
}
|
|
312
337
|
}
|
|
313
|
-
logsContent += `\n\n--- FORK ERROR ---\n${errorMessage}\n`;
|
|
314
|
-
await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
|
|
315
338
|
}
|
|
316
339
|
|
|
317
340
|
try {
|
|
@@ -360,7 +383,7 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
360
383
|
process.off('SIGTERM', sigtermHandler);
|
|
361
384
|
|
|
362
385
|
// Clean up temp files
|
|
363
|
-
for (const file of [reportFile, cleanLogsFile]) {
|
|
386
|
+
for (const file of [reportFile, cleanLogsFile, rawLogsFile]) {
|
|
364
387
|
if (existsSync(file)) {
|
|
365
388
|
try {
|
|
366
389
|
unlinkSync(file);
|
|
@@ -98,6 +98,8 @@ export const execSubcommand = createCommand({
|
|
|
98
98
|
process.on('SIGTERM', handleSignal);
|
|
99
99
|
|
|
100
100
|
try {
|
|
101
|
+
logger.debug('[exec] calling sandboxExecute for %s', args.sandboxId);
|
|
102
|
+
const executeStart = Date.now();
|
|
101
103
|
const execution = await sandboxExecute(client, {
|
|
102
104
|
sandboxId: args.sandboxId,
|
|
103
105
|
options: {
|
|
@@ -107,6 +109,13 @@ export const execSubcommand = createCommand({
|
|
|
107
109
|
},
|
|
108
110
|
orgId,
|
|
109
111
|
});
|
|
112
|
+
logger.debug(
|
|
113
|
+
'[exec] sandboxExecute returned in %dms: executionId=%s, stdoutUrl=%s, stderrUrl=%s',
|
|
114
|
+
Date.now() - executeStart,
|
|
115
|
+
execution.executionId,
|
|
116
|
+
execution.stdoutStreamUrl ?? 'none',
|
|
117
|
+
execution.stderrStreamUrl ?? 'none'
|
|
118
|
+
);
|
|
110
119
|
|
|
111
120
|
if (execution.autoResumed && !options.json) {
|
|
112
121
|
tui.warning('Sandbox was automatically resumed from suspended state');
|
|
@@ -116,10 +125,17 @@ export const execSubcommand = createCommand({
|
|
|
116
125
|
const stderrStreamUrl = execution.stderrStreamUrl;
|
|
117
126
|
const streamAbortController = new AbortController();
|
|
118
127
|
const streamPromises: Promise<void>[] = [];
|
|
128
|
+
const streamLabels: string[] = [];
|
|
119
129
|
|
|
120
130
|
// Check if stdout and stderr are the same stream (combined output)
|
|
121
131
|
const isCombinedOutput =
|
|
122
132
|
stdoutStreamUrl && stderrStreamUrl && stdoutStreamUrl === stderrStreamUrl;
|
|
133
|
+
logger.debug(
|
|
134
|
+
'[exec] stream mode: combined=%s, stdoutUrl=%s, stderrUrl=%s',
|
|
135
|
+
isCombinedOutput,
|
|
136
|
+
stdoutStreamUrl ?? 'none',
|
|
137
|
+
stderrStreamUrl ?? 'none'
|
|
138
|
+
);
|
|
123
139
|
|
|
124
140
|
// Set up stream capture — in JSON mode, capture to buffers;
|
|
125
141
|
// when streams are separate, capture stdout/stderr independently
|
|
@@ -153,9 +169,11 @@ export const execSubcommand = createCommand({
|
|
|
153
169
|
|
|
154
170
|
if (isCombinedOutput) {
|
|
155
171
|
// Stream combined output to stdout only to avoid duplicates
|
|
156
|
-
logger.debug('
|
|
172
|
+
logger.debug('[exec] starting combined stream: %s', stdoutStreamUrl);
|
|
173
|
+
streamLabels.push('combined');
|
|
157
174
|
streamPromises.push(
|
|
158
175
|
streamUrlToWritable(
|
|
176
|
+
'combined',
|
|
159
177
|
stdoutStreamUrl,
|
|
160
178
|
stdoutWritable,
|
|
161
179
|
streamAbortController.signal,
|
|
@@ -164,9 +182,11 @@ export const execSubcommand = createCommand({
|
|
|
164
182
|
);
|
|
165
183
|
} else {
|
|
166
184
|
if (stdoutStreamUrl) {
|
|
167
|
-
logger.debug('starting stdout stream
|
|
185
|
+
logger.debug('[exec] starting stdout stream: %s', stdoutStreamUrl);
|
|
186
|
+
streamLabels.push('stdout');
|
|
168
187
|
streamPromises.push(
|
|
169
188
|
streamUrlToWritable(
|
|
189
|
+
'stdout',
|
|
170
190
|
stdoutStreamUrl,
|
|
171
191
|
stdoutWritable,
|
|
172
192
|
streamAbortController.signal,
|
|
@@ -176,9 +196,11 @@ export const execSubcommand = createCommand({
|
|
|
176
196
|
}
|
|
177
197
|
|
|
178
198
|
if (stderrStreamUrl) {
|
|
179
|
-
logger.debug('starting stderr stream
|
|
199
|
+
logger.debug('[exec] starting stderr stream: %s', stderrStreamUrl);
|
|
200
|
+
streamLabels.push('stderr');
|
|
180
201
|
streamPromises.push(
|
|
181
202
|
streamUrlToWritable(
|
|
203
|
+
'stderr',
|
|
182
204
|
stderrStreamUrl,
|
|
183
205
|
stderrWritable,
|
|
184
206
|
streamAbortController.signal,
|
|
@@ -188,17 +210,63 @@ export const execSubcommand = createCommand({
|
|
|
188
210
|
}
|
|
189
211
|
}
|
|
190
212
|
|
|
213
|
+
logger.debug(
|
|
214
|
+
'[exec] %d stream(s) started [%s], now long-polling executionGet',
|
|
215
|
+
streamPromises.length,
|
|
216
|
+
streamLabels.join(', ')
|
|
217
|
+
);
|
|
218
|
+
|
|
191
219
|
// Use server-side long-polling to wait for execution completion
|
|
192
220
|
// This is more efficient than client-side polling and provides immediate
|
|
193
221
|
// error detection if the sandbox is terminated
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
222
|
+
let finalExecution: Awaited<ReturnType<typeof executionGet>>;
|
|
223
|
+
const pollStart = Date.now();
|
|
224
|
+
try {
|
|
225
|
+
finalExecution = await executionGet(client, {
|
|
226
|
+
executionId: execution.executionId,
|
|
227
|
+
orgId,
|
|
228
|
+
wait: EXECUTION_WAIT_DURATION,
|
|
229
|
+
});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// Abort any active stream readers before rethrowing so they
|
|
232
|
+
// don't keep running after the execution poll has failed.
|
|
233
|
+
streamAbortController.abort();
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
logger.debug(
|
|
237
|
+
'[exec] executionGet returned in %dms: status=%s, exitCode=%s',
|
|
238
|
+
Date.now() - pollStart,
|
|
239
|
+
finalExecution.status,
|
|
240
|
+
finalExecution.exitCode ?? 'undefined'
|
|
241
|
+
);
|
|
199
242
|
|
|
200
|
-
// Wait for all streams to reach EOF (Pulse blocks until true EOF)
|
|
201
|
-
|
|
243
|
+
// Wait for all streams to reach EOF (Pulse blocks until true EOF).
|
|
244
|
+
// Safety: execution is confirmed complete so all data has been written
|
|
245
|
+
// and complete/v2 sent. If Pulse doesn't close the response within
|
|
246
|
+
// a grace period (e.g. cross-server routing delay, stale metadata
|
|
247
|
+
// cache), abort the streams to prevent an indefinite hang.
|
|
248
|
+
if (streamPromises.length > 0) {
|
|
249
|
+
logger.debug('[exec] waiting for %d stream(s) to EOF', streamPromises.length);
|
|
250
|
+
const streamWaitStart = Date.now();
|
|
251
|
+
let graceTriggered = false;
|
|
252
|
+
const streamGrace = setTimeout(() => {
|
|
253
|
+
graceTriggered = true;
|
|
254
|
+
logger.debug(
|
|
255
|
+
'[exec] stream grace period (5s) expired after execution complete — aborting streams'
|
|
256
|
+
);
|
|
257
|
+
streamAbortController.abort();
|
|
258
|
+
}, 5_000);
|
|
259
|
+
try {
|
|
260
|
+
await Promise.all(streamPromises);
|
|
261
|
+
} finally {
|
|
262
|
+
clearTimeout(streamGrace);
|
|
263
|
+
}
|
|
264
|
+
logger.debug(
|
|
265
|
+
'[exec] all streams done in %dms (graceTriggered=%s)',
|
|
266
|
+
Date.now() - streamWaitStart,
|
|
267
|
+
graceTriggered
|
|
268
|
+
);
|
|
269
|
+
}
|
|
202
270
|
|
|
203
271
|
// Ensure stdout is fully flushed before continuing
|
|
204
272
|
if (!options.json && process.stdout.writable) {
|
|
@@ -258,42 +326,72 @@ export const execSubcommand = createCommand({
|
|
|
258
326
|
});
|
|
259
327
|
|
|
260
328
|
async function streamUrlToWritable(
|
|
329
|
+
label: string,
|
|
261
330
|
url: string,
|
|
262
331
|
writable: NodeJS.WritableStream,
|
|
263
332
|
signal: AbortSignal,
|
|
264
333
|
logger: Logger
|
|
265
334
|
): Promise<void> {
|
|
335
|
+
const streamStart = Date.now();
|
|
266
336
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
337
|
+
// Signal to Pulse that this is a v2 stream so it waits for v2 metadata
|
|
338
|
+
// instead of falling back to the legacy download path on a short timeout.
|
|
339
|
+
const v2Url = new URL(url);
|
|
340
|
+
v2Url.searchParams.set('v', '2');
|
|
341
|
+
logger.debug('[stream:%s] fetching: %s', label, v2Url.href);
|
|
342
|
+
const response = await fetch(v2Url.href, { signal });
|
|
343
|
+
logger.debug(
|
|
344
|
+
'[stream:%s] response status=%d in %dms',
|
|
345
|
+
label,
|
|
346
|
+
response.status,
|
|
347
|
+
Date.now() - streamStart
|
|
348
|
+
);
|
|
270
349
|
|
|
271
350
|
if (!response.ok || !response.body) {
|
|
272
|
-
logger.debug('stream
|
|
351
|
+
logger.debug('[stream:%s] not ok or no body — returning', label);
|
|
273
352
|
return;
|
|
274
353
|
}
|
|
275
354
|
|
|
276
355
|
const reader = response.body.getReader();
|
|
356
|
+
let chunks = 0;
|
|
357
|
+
let totalBytes = 0;
|
|
277
358
|
|
|
278
359
|
// Read until EOF - Pulse will block until data is available
|
|
279
360
|
while (true) {
|
|
280
361
|
const { done, value } = await reader.read();
|
|
281
362
|
if (done) {
|
|
282
|
-
logger.debug(
|
|
363
|
+
logger.debug(
|
|
364
|
+
'[stream:%s] EOF after %dms (%d chunks, %d bytes)',
|
|
365
|
+
label,
|
|
366
|
+
Date.now() - streamStart,
|
|
367
|
+
chunks,
|
|
368
|
+
totalBytes
|
|
369
|
+
);
|
|
283
370
|
break;
|
|
284
371
|
}
|
|
285
372
|
|
|
286
373
|
if (value) {
|
|
287
|
-
|
|
374
|
+
chunks++;
|
|
375
|
+
totalBytes += value.length;
|
|
376
|
+
if (chunks <= 3 || chunks % 100 === 0) {
|
|
377
|
+
logger.debug(
|
|
378
|
+
'[stream:%s] chunk #%d: %d bytes (total: %d bytes, +%dms)',
|
|
379
|
+
label,
|
|
380
|
+
chunks,
|
|
381
|
+
value.length,
|
|
382
|
+
totalBytes,
|
|
383
|
+
Date.now() - streamStart
|
|
384
|
+
);
|
|
385
|
+
}
|
|
288
386
|
await writeAndDrain(writable, value);
|
|
289
387
|
}
|
|
290
388
|
}
|
|
291
389
|
} catch (err) {
|
|
292
390
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
293
|
-
logger.debug('stream aborted');
|
|
391
|
+
logger.debug('[stream:%s] aborted after %dms', label, Date.now() - streamStart);
|
|
294
392
|
return;
|
|
295
393
|
}
|
|
296
|
-
logger.debug('stream error: %s', err);
|
|
394
|
+
logger.debug('[stream:%s] error after %dms: %s', label, Date.now() - streamStart, err);
|
|
297
395
|
}
|
|
298
396
|
}
|
|
299
397
|
|
|
@@ -198,6 +198,12 @@ export const runSubcommand = createCommand({
|
|
|
198
198
|
if (!options.json) {
|
|
199
199
|
tui.error(`failed with exit code ${result.exitCode} in ${duration}ms`);
|
|
200
200
|
}
|
|
201
|
+
// Use process.exit() directly rather than process.exitCode to ensure
|
|
202
|
+
// the exit code propagates reliably across all runtimes (Bun/Node).
|
|
203
|
+
// process.exitCode can be overwritten by later async cleanup.
|
|
204
|
+
if (!options.json) {
|
|
205
|
+
process.exit(result.exitCode);
|
|
206
|
+
}
|
|
201
207
|
process.exitCode = result.exitCode;
|
|
202
208
|
}
|
|
203
209
|
|
|
@@ -17,7 +17,7 @@ import { z } from 'zod';
|
|
|
17
17
|
import { getCommand } from '../../../../command-prefix';
|
|
18
18
|
import { getCatalystAPIClient } from '../../../../config';
|
|
19
19
|
import { encryptFIPSKEMDEMStream } from '../../../../crypto/box';
|
|
20
|
-
import { ErrorCode } from '../../../../errors';
|
|
20
|
+
import { ErrorCode, getExitCode } from '../../../../errors';
|
|
21
21
|
import * as tui from '../../../../tui';
|
|
22
22
|
import { createCommand } from '../../../../types';
|
|
23
23
|
import { validateAptDependencies } from '../../../../utils/apt-validator';
|
|
@@ -941,7 +941,7 @@ export const buildSubcommand = createCommand({
|
|
|
941
941
|
2
|
|
942
942
|
)
|
|
943
943
|
);
|
|
944
|
-
process.exit(ErrorCode.MALWARE_DETECTED);
|
|
944
|
+
process.exit(getExitCode(ErrorCode.MALWARE_DETECTED));
|
|
945
945
|
}
|
|
946
946
|
|
|
947
947
|
console.log('');
|
package/src/errors.ts
CHANGED
|
@@ -1,22 +1,51 @@
|
|
|
1
1
|
import type { Logger } from './types';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Standard exit codes for the CLI
|
|
4
|
+
* Standard exit codes for the CLI.
|
|
5
|
+
*
|
|
6
|
+
* Values start at 10 to avoid collisions with Unix signal numbers (1-9)
|
|
7
|
+
* and shell conventions (e.g. 2 = misuse of builtins, 9 = SIGKILL/OOM).
|
|
8
|
+
* Range 10-125 is safe — below shell-reserved 126-128 and signal-death
|
|
9
|
+
* codes 128+N.
|
|
10
|
+
*
|
|
11
|
+
* 0 and 1 are kept as universal success/failure codes.
|
|
5
12
|
*/
|
|
6
13
|
export enum ExitCode {
|
|
7
14
|
SUCCESS = 0,
|
|
8
15
|
GENERAL_ERROR = 1,
|
|
9
|
-
VALIDATION_ERROR =
|
|
10
|
-
AUTH_ERROR =
|
|
11
|
-
NOT_FOUND =
|
|
12
|
-
PERMISSION_ERROR =
|
|
13
|
-
NETWORK_ERROR =
|
|
14
|
-
FILE_ERROR =
|
|
15
|
-
USER_CANCELLED =
|
|
16
|
-
BUILD_FAILED =
|
|
17
|
-
SECURITY_ERROR =
|
|
16
|
+
VALIDATION_ERROR = 10,
|
|
17
|
+
AUTH_ERROR = 11,
|
|
18
|
+
NOT_FOUND = 12,
|
|
19
|
+
PERMISSION_ERROR = 13,
|
|
20
|
+
NETWORK_ERROR = 14,
|
|
21
|
+
FILE_ERROR = 15,
|
|
22
|
+
USER_CANCELLED = 16,
|
|
23
|
+
BUILD_FAILED = 17,
|
|
24
|
+
SECURITY_ERROR = 18,
|
|
25
|
+
PAYMENT_REQUIRED = 19,
|
|
26
|
+
UPGRADE_REQUIRED = 20,
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Human-readable descriptions for each exit code.
|
|
31
|
+
* This is the single source of truth consumed by the schema generator and AI help.
|
|
32
|
+
*/
|
|
33
|
+
export const exitCodeDescriptions: Record<number, string> = {
|
|
34
|
+
[ExitCode.SUCCESS]: 'Success',
|
|
35
|
+
[ExitCode.GENERAL_ERROR]: 'General error',
|
|
36
|
+
[ExitCode.VALIDATION_ERROR]: 'Validation error (invalid arguments or options)',
|
|
37
|
+
[ExitCode.AUTH_ERROR]: 'Authentication error (login required or credentials invalid)',
|
|
38
|
+
[ExitCode.NOT_FOUND]: 'Resource not found (project, file, deployment, etc.)',
|
|
39
|
+
[ExitCode.PERMISSION_ERROR]: 'Permission denied (insufficient access rights)',
|
|
40
|
+
[ExitCode.NETWORK_ERROR]: 'Network error (API unreachable or timeout)',
|
|
41
|
+
[ExitCode.FILE_ERROR]: 'File system error (file read/write failed)',
|
|
42
|
+
[ExitCode.USER_CANCELLED]: 'User cancelled (operation aborted by user)',
|
|
43
|
+
[ExitCode.BUILD_FAILED]: 'Build failed',
|
|
44
|
+
[ExitCode.SECURITY_ERROR]: 'Security error (malware detected)',
|
|
45
|
+
[ExitCode.PAYMENT_REQUIRED]: 'Payment required (plan upgrade needed)',
|
|
46
|
+
[ExitCode.UPGRADE_REQUIRED]: 'Upgrade required (CLI version too old)',
|
|
47
|
+
};
|
|
48
|
+
|
|
20
49
|
/**
|
|
21
50
|
* Standard error codes for the CLI
|
|
22
51
|
*/
|
|
@@ -152,7 +181,11 @@ export function getExitCode(errorCode: ErrorCode): ExitCode {
|
|
|
152
181
|
|
|
153
182
|
// Payment required - user needs to upgrade their plan
|
|
154
183
|
case ErrorCode.PAYMENT_REQUIRED:
|
|
155
|
-
return ExitCode.
|
|
184
|
+
return ExitCode.PAYMENT_REQUIRED;
|
|
185
|
+
|
|
186
|
+
// Upgrade required - CLI version too old
|
|
187
|
+
case ErrorCode.UPGRADE_REQUIRED:
|
|
188
|
+
return ExitCode.UPGRADE_REQUIRED;
|
|
156
189
|
|
|
157
190
|
// Resource conflicts and other errors
|
|
158
191
|
case ErrorCode.RESOURCE_ALREADY_EXISTS:
|
|
@@ -162,7 +195,6 @@ export function getExitCode(errorCode: ErrorCode): ExitCode {
|
|
|
162
195
|
case ErrorCode.RUNTIME_ERROR:
|
|
163
196
|
case ErrorCode.INTERNAL_ERROR:
|
|
164
197
|
case ErrorCode.NOT_IMPLEMENTED:
|
|
165
|
-
case ErrorCode.UPGRADE_REQUIRED:
|
|
166
198
|
default:
|
|
167
199
|
return ExitCode.GENERAL_ERROR;
|
|
168
200
|
}
|
package/src/schema-generator.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { CommandDefinition, SubcommandDefinition, CommandSchemas } from './types';
|
|
3
|
+
import { exitCodeDescriptions } from './errors';
|
|
3
4
|
import { parseArgsSchema, parseOptionsSchema } from './schema-parser';
|
|
4
5
|
import * as z from 'zod';
|
|
5
6
|
|
|
@@ -313,18 +314,7 @@ export function generateCLISchema(
|
|
|
313
314
|
name: 'agentuity',
|
|
314
315
|
version,
|
|
315
316
|
description: 'Agentuity CLI',
|
|
316
|
-
exitCodes: {
|
|
317
|
-
0: 'Success',
|
|
318
|
-
1: 'General error',
|
|
319
|
-
2: 'Validation error (invalid arguments or options)',
|
|
320
|
-
3: 'Authentication error (login required or credentials invalid)',
|
|
321
|
-
4: 'Resource not found (project, file, deployment, etc.)',
|
|
322
|
-
5: 'Permission denied (insufficient access rights)',
|
|
323
|
-
6: 'Network error (API unreachable or timeout)',
|
|
324
|
-
7: 'File system error (file read/write failed)',
|
|
325
|
-
8: 'User cancelled (operation aborted by user)',
|
|
326
|
-
9: 'Build failed',
|
|
327
|
-
},
|
|
317
|
+
exitCodes: { ...exitCodeDescriptions },
|
|
328
318
|
globalOptions: [
|
|
329
319
|
{
|
|
330
320
|
name: 'config',
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stream a ReadableStream of raw bytes to a file on disk.
|
|
5
|
+
*
|
|
6
|
+
* This mirrors the pattern used by the deploy fork wrapper to capture child
|
|
7
|
+
* process stdout/stderr without accumulating the output in memory. Returns
|
|
8
|
+
* the total number of bytes written.
|
|
9
|
+
*/
|
|
10
|
+
export async function captureStreamToFile(
|
|
11
|
+
stream: ReadableStream<Uint8Array>,
|
|
12
|
+
filePath: string
|
|
13
|
+
): Promise<number> {
|
|
14
|
+
const writer = createWriteStream(filePath);
|
|
15
|
+
const reader = stream.getReader();
|
|
16
|
+
let totalBytes = 0;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
while (true) {
|
|
20
|
+
const { done, value } = await reader.read();
|
|
21
|
+
if (done) break;
|
|
22
|
+
|
|
23
|
+
const ok = writer.write(value);
|
|
24
|
+
totalBytes += value.byteLength;
|
|
25
|
+
|
|
26
|
+
// Respect backpressure: wait for drain when the internal buffer is full
|
|
27
|
+
if (!ok) {
|
|
28
|
+
await new Promise<void>((resolve) => writer.once('drain', resolve));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
await new Promise<void>((resolve, reject) => {
|
|
33
|
+
writer.once('error', reject);
|
|
34
|
+
writer.end(resolve);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return totalBytes;
|
|
39
|
+
}
|