@agentuity/cli 1.0.56 → 1.0.58

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.
@@ -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);
@@ -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',
package/src/tui.ts CHANGED
@@ -25,6 +25,12 @@ function ensureCursorRestoration(): void {
25
25
  exitHandlerInstalled = true;
26
26
 
27
27
  const restoreCursor = () => {
28
+ // Only write ANSI escape sequences when stderr is a real terminal.
29
+ // Writing to non-TTY streams (pipes, command substitution, etc.)
30
+ // pollutes captured output with invisible control characters.
31
+ if (!process.stderr.isTTY) {
32
+ return;
33
+ }
28
34
  // Skip cursor restoration in CI - terminals don't support these sequences
29
35
  if (process.env.CI) {
30
36
  return;
@@ -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
+ }