@agentuity/cli 1.0.58 → 1.0.60

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 (45) hide show
  1. package/dist/cmd/build/vite/static-render-worker.d.ts +4 -0
  2. package/dist/cmd/build/vite/static-render-worker.d.ts.map +1 -0
  3. package/dist/cmd/build/vite/static-render-worker.js +58 -0
  4. package/dist/cmd/build/vite/static-render-worker.js.map +1 -0
  5. package/dist/cmd/build/vite/vite-build-worker.d.ts +2 -0
  6. package/dist/cmd/build/vite/vite-build-worker.d.ts.map +1 -0
  7. package/dist/cmd/build/vite/vite-build-worker.js +50 -0
  8. package/dist/cmd/build/vite/vite-build-worker.js.map +1 -0
  9. package/dist/cmd/build/vite/vite-builder.d.ts +1 -0
  10. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  11. package/dist/cmd/build/vite/vite-builder.js +261 -23
  12. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  13. package/dist/cmd/cloud/deploy-fork.d.ts +10 -0
  14. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
  15. package/dist/cmd/cloud/deploy-fork.js +41 -23
  16. package/dist/cmd/cloud/deploy-fork.js.map +1 -1
  17. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  18. package/dist/cmd/cloud/deploy.js +53 -11
  19. package/dist/cmd/cloud/deploy.js.map +1 -1
  20. package/dist/cmd/project/show.d.ts.map +1 -1
  21. package/dist/cmd/project/show.js +9 -0
  22. package/dist/cmd/project/show.js.map +1 -1
  23. package/dist/cmd/support/report.d.ts.map +1 -1
  24. package/dist/cmd/support/report.js +19 -10
  25. package/dist/cmd/support/report.js.map +1 -1
  26. package/dist/steps.d.ts.map +1 -1
  27. package/dist/steps.js +38 -0
  28. package/dist/steps.js.map +1 -1
  29. package/dist/tui.d.ts.map +1 -1
  30. package/dist/tui.js +19 -9
  31. package/dist/tui.js.map +1 -1
  32. package/dist/utils/zip.d.ts.map +1 -1
  33. package/dist/utils/zip.js +19 -10
  34. package/dist/utils/zip.js.map +1 -1
  35. package/package.json +8 -8
  36. package/src/cmd/build/vite/static-render-worker.ts +72 -0
  37. package/src/cmd/build/vite/vite-build-worker.ts +58 -0
  38. package/src/cmd/build/vite/vite-builder.ts +295 -23
  39. package/src/cmd/cloud/deploy-fork.ts +56 -22
  40. package/src/cmd/cloud/deploy.ts +68 -12
  41. package/src/cmd/project/show.ts +9 -0
  42. package/src/cmd/support/report.ts +21 -10
  43. package/src/steps.ts +38 -0
  44. package/src/tui.ts +18 -9
  45. package/src/utils/zip.ts +22 -10
@@ -34,6 +34,16 @@ export interface ForkDeployResult {
34
34
  success: boolean;
35
35
  exitCode: number;
36
36
  diagnostics?: ClientDiagnostics;
37
+ /** Deploy result passed back from child process via temp file */
38
+ deployResult?: {
39
+ urls?: {
40
+ deployment: string;
41
+ latest: string;
42
+ custom?: string[];
43
+ dashboard: string;
44
+ };
45
+ logs?: string[];
46
+ };
37
47
  }
38
48
 
39
49
  /**
@@ -77,6 +87,7 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
77
87
  const reportFile = join(tmpdir(), `agentuity-deploy-${deploymentId}.json`);
78
88
  const cleanLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-logs.txt`);
79
89
  const rawLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-raw.txt`);
90
+ const deployResultFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-result.json`);
80
91
  const rawLogsWriter = createWriteStream(rawLogsFile);
81
92
  let proc: Subprocess | null = null;
82
93
  let cancelled = false;
@@ -152,13 +163,7 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
152
163
  process.on('SIGTERM', sigtermHandler);
153
164
 
154
165
  try {
155
- const childArgs = [
156
- 'agentuity',
157
- 'deploy',
158
- '--child-mode',
159
- `--report-file=${reportFile}`,
160
- ...args,
161
- ];
166
+ const childArgs = ['deploy', '--child-mode', `--report-file=${reportFile}`, ...args];
162
167
 
163
168
  // Pass the deployment info via environment variable (same format as CI builds)
164
169
  const deploymentEnvValue = JSON.stringify({
@@ -167,14 +172,19 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
167
172
  publicKey: deployment.publicKey,
168
173
  });
169
174
 
170
- logger.debug('Spawning child deploy process: bunx %s', childArgs.join(' '));
175
+ // Re-exec the same entry point that the parent is running so that
176
+ // local/dev builds test the current code instead of a stale global
177
+ // install. process.execPath is the bun binary; Bun.main is the
178
+ // script entry (e.g. bin/cli.ts or the compiled binary).
179
+ const cmd = [process.execPath, Bun.main, ...childArgs];
180
+ logger.debug('Spawning child deploy process: %s', cmd.join(' '));
171
181
 
172
182
  // Get terminal dimensions to pass to child
173
183
  const columns = process.stdout.columns || 80;
174
184
  const rows = process.stdout.rows || 24;
175
185
 
176
186
  proc = spawn({
177
- cmd: ['bunx', ...childArgs],
187
+ cmd,
178
188
  cwd: projectDir,
179
189
  env: {
180
190
  ...process.env,
@@ -190,6 +200,8 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
190
200
  LINES: String(rows),
191
201
  // Enable clean log collection for Pulse streaming
192
202
  AGENTUITY_CLEAN_LOGS_FILE: cleanLogsFile,
203
+ // Pass result file path for child to write deploy URLs/logs back
204
+ AGENTUITY_DEPLOY_RESULT_FILE: deployResultFile,
193
205
  },
194
206
  stdin: 'inherit',
195
207
  stdout: 'pipe',
@@ -246,20 +258,34 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
246
258
  }
247
259
  }
248
260
 
261
+ // Read deploy result (URLs, logs) from child process
262
+ let deployResult: ForkDeployResult['deployResult'] | undefined;
263
+ if (existsSync(deployResultFile)) {
264
+ try {
265
+ const resultContent = readFileSync(deployResultFile, 'utf-8');
266
+ deployResult = JSON.parse(resultContent);
267
+ unlinkSync(deployResultFile);
268
+ } catch (err) {
269
+ logger.debug('Failed to read deploy result file: %s', err);
270
+ }
271
+ }
272
+
249
273
  // Stream clean logs to Pulse (prefer clean logs over raw output)
250
274
  if (buildLogsStreamURL) {
251
- let logsContent = '';
275
+ let streamedCleanLogs = false;
252
276
  if (existsSync(cleanLogsFile)) {
253
277
  try {
254
- logsContent = readFileSync(cleanLogsFile, 'utf-8');
278
+ const cleanLogs = Bun.file(cleanLogsFile);
279
+ if (cleanLogs.size > 0) {
280
+ await streamToPulse(buildLogsStreamURL, sdkKey, cleanLogs, logger);
281
+ streamedCleanLogs = true;
282
+ }
255
283
  unlinkSync(cleanLogsFile);
256
284
  } catch (err) {
257
- logger.debug('Failed to read clean logs file: %s', err);
285
+ logger.debug('Failed to stream clean logs file: %s', err);
258
286
  }
259
287
  }
260
- if (logsContent) {
261
- await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
262
- } else if (existsSync(rawLogsFile)) {
288
+ if (!streamedCleanLogs && existsSync(rawLogsFile)) {
263
289
  // Stream raw logs file directly to Pulse without loading into memory
264
290
  await streamToPulse(buildLogsStreamURL, sdkKey, Bun.file(rawLogsFile), logger);
265
291
  }
@@ -299,24 +325,32 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
299
325
  return { success: false, exitCode, diagnostics };
300
326
  }
301
327
 
302
- return { success: true, exitCode, diagnostics };
328
+ return { success: true, exitCode, diagnostics, deployResult };
303
329
  } catch (err) {
304
330
  const errorMessage = err instanceof Error ? err.message : String(err);
305
331
  logger.error('Fork deploy error: %s', errorMessage);
306
332
 
307
333
  if (buildLogsStreamURL) {
308
- let logsContent = '';
334
+ let streamedCleanLogs = false;
309
335
  if (existsSync(cleanLogsFile)) {
310
336
  try {
311
- logsContent = readFileSync(cleanLogsFile, 'utf-8');
337
+ const cleanLogs = Bun.file(cleanLogsFile);
338
+ if (cleanLogs.size > 0) {
339
+ await streamToPulse(buildLogsStreamURL, sdkKey, cleanLogs, logger);
340
+ streamedCleanLogs = true;
341
+ }
312
342
  unlinkSync(cleanLogsFile);
313
343
  } catch {
314
344
  // ignore
315
345
  }
316
346
  }
317
- if (logsContent) {
318
- logsContent += `\n\n--- FORK ERROR ---\n${errorMessage}\n`;
319
- await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
347
+ if (streamedCleanLogs) {
348
+ await streamToPulse(
349
+ buildLogsStreamURL,
350
+ sdkKey,
351
+ `\n\n--- FORK ERROR ---\n${errorMessage}\n`,
352
+ logger
353
+ );
320
354
  } else {
321
355
  // Append error to raw logs file and stream it without loading into memory
322
356
  try {
@@ -383,7 +417,7 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
383
417
  process.off('SIGTERM', sigtermHandler);
384
418
 
385
419
  // Clean up temp files
386
- for (const file of [reportFile, cleanLogsFile, rawLogsFile]) {
420
+ for (const file of [reportFile, cleanLogsFile, rawLogsFile, deployResultFile]) {
387
421
  if (existsSync(file)) {
388
422
  try {
389
423
  unlinkSync(file);
@@ -1,7 +1,16 @@
1
1
  import { createPublicKey } from 'node:crypto';
2
- import { createReadStream, createWriteStream, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import {
3
+ createReadStream,
4
+ createWriteStream,
5
+ existsSync,
6
+ mkdirSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
3
10
  import { tmpdir } from 'node:os';
4
11
  import { join, resolve } from 'node:path';
12
+ import { pipeline } from 'node:stream/promises';
13
+ import { createGzip } from 'node:zlib';
5
14
  import { StructuredError } from '@agentuity/core';
6
15
  import {
7
16
  type BuildMetadata,
@@ -403,6 +412,8 @@ export const deploySubcommand = createSubcommand({
403
412
  success: true,
404
413
  deploymentId: initialDeployment.id,
405
414
  projectId: project.projectId,
415
+ logs: result.deployResult?.logs,
416
+ urls: result.deployResult?.urls,
406
417
  };
407
418
  }
408
419
  let useExistingDeployment = false;
@@ -821,9 +832,10 @@ export const deploySubcommand = createSubcommand({
821
832
  endCodeUploadDiagnostic();
822
833
 
823
834
  progress(70);
824
- ctx.logger.trace('Consuming response body');
825
- // Wait for response body to be consumed before deleting
826
- await resp.arrayBuffer();
835
+ ctx.logger.trace('Cancelling upload response body');
836
+ // No response payload is needed for successful uploads.
837
+ // Cancel to release resources without buffering into memory.
838
+ await resp.body?.cancel();
827
839
  ctx.logger.trace('Deleting encrypted zip');
828
840
  await zipfile.delete();
829
841
  } finally {
@@ -874,27 +886,48 @@ export const deploySubcommand = createSubcommand({
874
886
 
875
887
  bytes += asset.size;
876
888
 
877
- let body: Uint8Array | Blob;
889
+ let body: Blob;
890
+ let gzTempPath: string | undefined;
878
891
  if (asset.contentEncoding === 'gzip') {
879
- const file = Bun.file(filePath);
880
- const ab = await file.arrayBuffer();
881
- const gzipped = Bun.gzipSync(new Uint8Array(ab));
892
+ // Gzip to a temp file so Bun.file() can provide
893
+ // Content-Length to S3 (streaming bodies use chunked
894
+ // transfer encoding which S3 rejects).
895
+ gzTempPath = join(
896
+ tmpdir(),
897
+ `agentuity-asset-${deployment.id}-${Date.now()}-${asset.filename.replace(/\//g, '_')}.gz`
898
+ );
899
+ await pipeline(
900
+ createReadStream(filePath),
901
+ createGzip(),
902
+ createWriteStream(gzTempPath)
903
+ );
882
904
  headers['Content-Encoding'] = 'gzip';
883
- body = gzipped;
905
+ body = Bun.file(gzTempPath);
906
+ const compressedSize = body.size;
884
907
  ctx.logger.trace(
885
- `Compressing ${asset.filename} (${asset.size} -> ${gzipped.byteLength} bytes)`
908
+ `Gzip compressed ${asset.filename} (${asset.size} -> ${compressedSize} bytes)`
886
909
  );
887
910
  } else {
888
911
  body = Bun.file(filePath);
889
912
  }
890
913
 
914
+ const assetGzTempPath = gzTempPath;
891
915
  promises.push(
892
916
  fetch(assetUrl, {
893
917
  method: 'PUT',
894
- duplex: 'half',
895
918
  headers,
896
919
  body,
897
920
  signal: stepCtx.signal,
921
+ }).then((response) => {
922
+ // Clean up temp gzip file after upload completes
923
+ if (assetGzTempPath) {
924
+ try {
925
+ unlinkSync(assetGzTempPath);
926
+ } catch {
927
+ // ignore — file may already be cleaned up
928
+ }
929
+ }
930
+ return response;
898
931
  })
899
932
  );
900
933
  }
@@ -1194,7 +1227,7 @@ export const deploySubcommand = createSubcommand({
1194
1227
  }
1195
1228
 
1196
1229
  // Show deployment URLs
1197
- if (complete?.publicUrls) {
1230
+ if (complete?.publicUrls && !options.json) {
1198
1231
  const lines: string[] = [];
1199
1232
  if (complete.publicUrls.custom?.length) {
1200
1233
  for (const url of complete.publicUrls.custom) {
@@ -1237,6 +1270,29 @@ export const deploySubcommand = createSubcommand({
1237
1270
  }
1238
1271
  clearGlobalCollector();
1239
1272
 
1273
+ // Write deploy result to file for fork parent to consume
1274
+ const deployResultFile = process.env.AGENTUITY_DEPLOY_RESULT_FILE;
1275
+ if (deployResultFile) {
1276
+ try {
1277
+ const resultData = {
1278
+ urls: complete?.publicUrls
1279
+ ? {
1280
+ deployment:
1281
+ complete.publicUrls.vanityDeployment ??
1282
+ complete.publicUrls.deployment,
1283
+ latest: complete.publicUrls.vanityProject ?? complete.publicUrls.latest,
1284
+ custom: complete.publicUrls.custom,
1285
+ dashboard,
1286
+ }
1287
+ : undefined,
1288
+ logs,
1289
+ };
1290
+ writeFileSync(deployResultFile, JSON.stringify(resultData));
1291
+ } catch {
1292
+ // Non-fatal: result file is optional
1293
+ }
1294
+ }
1295
+
1240
1296
  return {
1241
1297
  success: true,
1242
1298
  deploymentId: deployment.id,
@@ -12,6 +12,14 @@ const ProjectShowResponseSchema = z.object({
12
12
  orgId: z.string().describe('Organization ID'),
13
13
  secrets: z.record(z.string(), z.string()).optional().describe('Project secrets (masked)'),
14
14
  env: z.record(z.string(), z.string()).optional().describe('Environment variables'),
15
+ urls: z
16
+ .object({
17
+ dashboard: z.string().describe('Dashboard URL for the project'),
18
+ app: z.string().describe('Public URL for the latest deployment'),
19
+ custom: z.array(z.string()).describe('Custom domain URLs'),
20
+ })
21
+ .optional()
22
+ .describe('Project URLs'),
15
23
  });
16
24
 
17
25
  export const showSubcommand = createSubcommand({
@@ -60,6 +68,7 @@ export const showSubcommand = createSubcommand({
60
68
  orgId: project.orgId,
61
69
  secrets: project.secrets,
62
70
  env: project.env,
71
+ urls: project.urls,
63
72
  };
64
73
  },
65
74
  });
@@ -1,12 +1,12 @@
1
1
  import { createSubcommand } from '../../types';
2
2
  import { z } from 'zod';
3
- import { readFileSync } from 'node:fs';
3
+ import { createWriteStream, readFileSync } from 'node:fs';
4
4
  import { join, basename } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { getLogSessionsInCurrentWindow } from '../../internal-logger';
7
7
  import * as tui from '../../tui';
8
8
  import { randomBytes } from 'node:crypto';
9
- import AdmZip from 'adm-zip';
9
+ import archiver from 'archiver';
10
10
  import { APIResponseSchema } from '@agentuity/server';
11
11
  import { StructuredError } from '@agentuity/core';
12
12
 
@@ -61,10 +61,19 @@ async function createReportZip(sessionDirs: string[]): Promise<string> {
61
61
  throw NoSessionDirectoriesError();
62
62
  }
63
63
 
64
- // Create zip in temp directory
64
+ // Create zip in temp directory, streaming to disk instead of buffering in memory
65
65
  const tempZip = join(tmpdir(), `agentuity-report-${randomBytes(8).toString('hex')}.zip`);
66
66
 
67
- const zip = new AdmZip();
67
+ const output = createWriteStream(tempZip);
68
+ const zip = archiver('zip', { zlib: { level: 9 } });
69
+
70
+ const writeDone = new Promise<void>((resolve, reject) => {
71
+ output.on('close', resolve);
72
+ output.on('error', reject);
73
+ zip.on('error', reject);
74
+ });
75
+
76
+ zip.pipe(output);
68
77
 
69
78
  for (const sessionDir of sessionDirs) {
70
79
  const sessionFile = join(sessionDir, 'session.json');
@@ -75,14 +84,15 @@ async function createReportZip(sessionDirs: string[]): Promise<string> {
75
84
 
76
85
  // Add files with session ID prefix to avoid conflicts
77
86
  if (await Bun.file(sessionFile).exists()) {
78
- zip.addLocalFile(sessionFile, sessionId);
87
+ zip.file(sessionFile, { name: `${sessionId}/session.json` });
79
88
  }
80
89
  if (await Bun.file(logsFile).exists()) {
81
- zip.addLocalFile(logsFile, sessionId);
90
+ zip.file(logsFile, { name: `${sessionId}/logs.jsonl` });
82
91
  }
83
92
  }
84
93
 
85
- zip.writeZip(tempZip);
94
+ await zip.finalize();
95
+ await writeDone;
86
96
 
87
97
  return tempZip;
88
98
  }
@@ -95,14 +105,15 @@ async function uploadReport(
95
105
  zipPath: string,
96
106
  logger: import('../../types').Logger
97
107
  ): Promise<void> {
98
- const fileBuffer = readFileSync(zipPath);
108
+ // Use Bun.file() to stream the zip to S3 without loading it into memory.
109
+ // Bun automatically sets Content-Length from the file size.
110
+ const file = Bun.file(zipPath);
99
111
 
100
112
  const response = await fetch(presignedUrl, {
101
113
  method: 'PUT',
102
- body: fileBuffer,
114
+ body: file,
103
115
  headers: {
104
116
  'Content-Type': 'application/zip',
105
- 'Content-Length': fileBuffer.length.toString(),
106
117
  },
107
118
  });
108
119
 
package/src/steps.ts CHANGED
@@ -10,6 +10,7 @@ import type { LogLevel } from './types';
10
10
  import { ValidationInputError, ValidationOutputError, type IssuesType } from '@agentuity/server';
11
11
  import { clearLastLines, isTTYLike } from './tui';
12
12
  import { appendLog, isLogCollectionEnabled } from './log-collector';
13
+ import { getOutputOptions, isJSONMode } from './output';
13
14
 
14
15
  /**
15
16
  * Error thrown when step execution is interrupted by a signal (e.g., Ctrl+C).
@@ -700,6 +701,43 @@ async function runStepsPlain(steps: Step[]): Promise<void> {
700
701
  * Run a series of steps with animated progress
701
702
  */
702
703
  export async function runSteps(steps: Step[], logLevel?: LogLevel): Promise<void> {
704
+ const outputOptions = getOutputOptions();
705
+
706
+ // In JSON mode, skip all UI rendering
707
+ if (outputOptions && isJSONMode(outputOptions)) {
708
+ const abortController = new AbortController();
709
+ for (const step of steps) {
710
+ if (abortController.signal.aborted) break;
711
+ if (step) {
712
+ const ctx: StepContext = {
713
+ signal: abortController.signal,
714
+ progress: () => {},
715
+ };
716
+ let outcome: StepOutcome;
717
+ try {
718
+ outcome = await step.run(ctx);
719
+ } catch (err) {
720
+ if (err instanceof Error && err.name === 'AbortError') {
721
+ throw new StepInterruptError();
722
+ }
723
+ outcome = {
724
+ status: 'error',
725
+ message: err instanceof Error ? err.message : String(err),
726
+ cause: err instanceof Error ? err : undefined,
727
+ };
728
+ }
729
+ if (outcome.status === 'error') {
730
+ if (outcome.cause instanceof Error && outcome.cause.name === 'AbortError') {
731
+ throw new StepInterruptError();
732
+ }
733
+ const errorMsg = outcome.message || 'An unknown error occurred';
734
+ throw new Error(errorMsg);
735
+ }
736
+ }
737
+ }
738
+ return;
739
+ }
740
+
703
741
  const useTUI = isTTYLike() && (!logLevel || ['info', 'warn', 'error'].includes(logLevel));
704
742
 
705
743
  if (useTUI) {
package/src/tui.ts CHANGED
@@ -299,8 +299,13 @@ export function getSeverityColor(severity: string): (text: string) => string {
299
299
  export function success(message: string): void {
300
300
  const color = getColor('success');
301
301
  const reset = getColor('reset');
302
- // Clear line first to ensure no leftover content from previous output
303
- process.stderr.write(`\r\x1b[2K${color}${ICONS.success} ${message}${reset}\n`);
302
+ if (process.stderr.isTTY) {
303
+ // Clear line first to ensure no leftover content from previous output
304
+ process.stderr.write(`\r\x1b[2K${color}${ICONS.success} ${message}${reset}\n`);
305
+ } else {
306
+ // No ANSI control sequences for non-TTY streams (pipes, command substitution)
307
+ process.stderr.write(`${ICONS.success} ${message}\n`);
308
+ }
304
309
  }
305
310
 
306
311
  /**
@@ -1248,18 +1253,23 @@ export async function spinner<T>(
1248
1253
  const { getOutputOptions, shouldDisableProgress } = await import('./output');
1249
1254
  const outputOptions = getOutputOptions();
1250
1255
  const noProgress = outputOptions ? shouldDisableProgress(outputOptions) : false;
1256
+ const isJsonMode = outputOptions?.json === true;
1251
1257
 
1252
- // If no interactive TTY-like environment or progress disabled, just execute
1253
- // the callback without animation
1254
- if (!isTTYLike() || noProgress) {
1258
+ // If stderr is not a real terminal or progress disabled, just execute
1259
+ // the callback without animation. We check stderr specifically because
1260
+ // the spinner writes ANSI sequences to stderr — isTTYLike() may return
1261
+ // true when stdout is a TTY but stderr is piped (e.g. 2>&1 in $()).
1262
+ if (!process.stderr.isTTY || noProgress) {
1255
1263
  try {
1256
1264
  const result =
1257
1265
  options.type === 'progress'
1258
1266
  ? await options.callback(() => {})
1259
1267
  : options.type === 'logger'
1260
1268
  ? await options.callback((logMessage: string) => {
1261
- // In non-TTY mode, just write logs directly to stdout
1262
- process.stdout.write(logMessage + '\n');
1269
+ // In JSON mode, don't write logs to stdout
1270
+ if (!isJsonMode) {
1271
+ process.stdout.write(logMessage + '\n');
1272
+ }
1263
1273
  })
1264
1274
  : options.type === 'countdown'
1265
1275
  ? await options.callback()
@@ -1269,7 +1279,6 @@ export async function spinner<T>(
1269
1279
 
1270
1280
  // If clearOnSuccess is true, don't show success message
1271
1281
  // Also skip success message in JSON mode
1272
- const isJsonMode = outputOptions?.json === true;
1273
1282
  if (!options.clearOnSuccess && !isJsonMode) {
1274
1283
  const successColor = getColor('success');
1275
1284
  console.error(`${successColor}${ICONS.success} ${message}${reset}`);
@@ -1279,7 +1288,7 @@ export async function spinner<T>(
1279
1288
  } catch (err) {
1280
1289
  const clearOnError =
1281
1290
  (options.type === 'progress' || options.type === 'simple') && options.clearOnError;
1282
- if (!clearOnError) {
1291
+ if (!clearOnError && !isJsonMode) {
1283
1292
  const errorColor = getColor('error');
1284
1293
  console.error(`${errorColor}${ICONS.error} ${message}${reset}`);
1285
1294
  }
package/src/utils/zip.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { readFileSync, lstatSync } from 'node:fs';
2
- import { relative } from 'node:path';
1
+ import { createWriteStream, lstatSync } from 'node:fs';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { dirname, relative } from 'node:path';
3
4
  import { Glob } from 'bun';
4
- import AdmZip from 'adm-zip';
5
+ import archiver from 'archiver';
5
6
  import { toForwardSlash } from './normalize-path';
6
7
 
7
8
  interface Options {
@@ -10,7 +11,20 @@ interface Options {
10
11
  }
11
12
 
12
13
  export async function zipDir(dir: string, outdir: string, options?: Options) {
13
- const zip = new AdmZip();
14
+ await mkdir(dirname(outdir), { recursive: true });
15
+ const output = createWriteStream(outdir);
16
+ const zip = archiver('zip', {
17
+ zlib: { level: 9 },
18
+ });
19
+
20
+ const writeDone = new Promise<void>((resolve, reject) => {
21
+ output.on('close', resolve);
22
+ output.on('error', reject);
23
+ zip.on('error', reject);
24
+ });
25
+
26
+ zip.pipe(output);
27
+
14
28
  const files = await Array.fromAsync(
15
29
  new Glob('**/*').scan({ cwd: dir, absolute: true, dot: true, followSymlinks: false })
16
30
  );
@@ -31,11 +45,8 @@ export async function zipDir(dir: string, outdir: string, options?: Options) {
31
45
  // across machines and would cause EISDIR errors on extraction.
32
46
  const stat = lstatSync(file);
33
47
  if (!stat.isSymbolicLink() && !stat.isDirectory()) {
34
- // Use addFile with explicit Unix permissions (0o644) instead of addLocalFile.
35
- // On Windows, addLocalFile relies on OS file stats which may produce zip entries
36
- // with incorrect Unix permission bits, causing EACCES errors when extracted on Linux.
37
- const data = readFileSync(file);
38
- zip.addFile(rel, data, '', 0o644);
48
+ // Set explicit Unix permissions (0o644) for portability across OSes.
49
+ zip.file(file, { name: rel, mode: 0o644 });
39
50
  }
40
51
  } catch (err) {
41
52
  throw new Error(`Failed to add file to zip: ${rel} (${file})`, { cause: err });
@@ -48,7 +59,8 @@ export async function zipDir(dir: string, outdir: string, options?: Options) {
48
59
  await Bun.sleep(10); // give some time for the progress bar to render
49
60
  }
50
61
  }
51
- await zip.writeZip(outdir);
62
+ await zip.finalize();
63
+ await writeDone;
52
64
  if (options?.progress) {
53
65
  options.progress(100);
54
66
  await Bun.sleep(100); // give some time for the progress bar to render