@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.
Files changed (34) hide show
  1. package/dist/cmd/build/ci.d.ts.map +1 -1
  2. package/dist/cmd/build/ci.js +21 -5
  3. package/dist/cmd/build/ci.js.map +1 -1
  4. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
  5. package/dist/cmd/cloud/deploy-fork.js +36 -15
  6. package/dist/cmd/cloud/deploy-fork.js.map +1 -1
  7. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  8. package/dist/cmd/cloud/sandbox/exec.js +73 -22
  9. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  10. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  11. package/dist/cmd/cloud/sandbox/run.js +6 -0
  12. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  13. package/dist/cmd/cloud/sandbox/snapshot/build.js +2 -2
  14. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  15. package/dist/errors.d.ts +24 -10
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +42 -12
  18. package/dist/errors.js.map +1 -1
  19. package/dist/schema-generator.d.ts.map +1 -1
  20. package/dist/schema-generator.js +2 -12
  21. package/dist/schema-generator.js.map +1 -1
  22. package/dist/utils/stream-capture.d.ts +9 -0
  23. package/dist/utils/stream-capture.d.ts.map +1 -0
  24. package/dist/utils/stream-capture.js +34 -0
  25. package/dist/utils/stream-capture.js.map +1 -0
  26. package/package.json +6 -6
  27. package/src/cmd/build/ci.ts +21 -5
  28. package/src/cmd/cloud/deploy-fork.ts +39 -16
  29. package/src/cmd/cloud/sandbox/exec.ts +116 -18
  30. package/src/cmd/cloud/sandbox/run.ts +6 -0
  31. package/src/cmd/cloud/sandbox/snapshot/build.ts +2 -2
  32. package/src/errors.ts +44 -12
  33. package/src/schema-generator.ts +2 -12
  34. 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
- let outputBuffer = '';
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
- const text = decoder.decode(value, { stream: true });
207
- outputBuffer += text;
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 (!logsContent) {
311
- logsContent = outputBuffer;
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('using combined output stream (stdout === stderr): %s', stdoutStreamUrl);
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 from: %s', stdoutStreamUrl);
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 from: %s', stderrStreamUrl);
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
- const finalExecution = await executionGet(client, {
195
- executionId: execution.executionId,
196
- orgId,
197
- wait: EXECUTION_WAIT_DURATION,
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
- await Promise.all(streamPromises);
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
- logger.debug('fetching stream: %s', url);
268
- const response = await fetch(url, { signal });
269
- logger.debug('stream response status: %d', response.status);
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 response not ok or no body');
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('stream EOF');
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
- logger.debug('stream chunk: %d bytes', value.length);
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 = 2,
10
- AUTH_ERROR = 3,
11
- NOT_FOUND = 4,
12
- PERMISSION_ERROR = 5,
13
- NETWORK_ERROR = 6,
14
- FILE_ERROR = 7,
15
- USER_CANCELLED = 8,
16
- BUILD_FAILED = 9,
17
- SECURITY_ERROR = 10,
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.GENERAL_ERROR;
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
  }
@@ -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
+ }