@agentuity/cli 2.0.0-beta.1 → 2.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.
Files changed (112) hide show
  1. package/bin/cli.ts +3 -1
  2. package/dist/cmd/build/ci.d.ts +1 -1
  3. package/dist/cmd/build/ci.d.ts.map +1 -1
  4. package/dist/cmd/build/ci.js +70 -63
  5. package/dist/cmd/build/ci.js.map +1 -1
  6. package/dist/cmd/build/index.d.ts.map +1 -1
  7. package/dist/cmd/build/index.js +0 -3
  8. package/dist/cmd/build/index.js.map +1 -1
  9. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  10. package/dist/cmd/build/vite/agent-discovery.js +26 -2
  11. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  12. package/dist/cmd/build/vite/route-discovery.d.ts +5 -0
  13. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  14. package/dist/cmd/build/vite/route-discovery.js +13 -11
  15. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  16. package/dist/cmd/build/vite/static-renderer.d.ts +3 -2
  17. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  18. package/dist/cmd/build/vite/static-renderer.js +28 -58
  19. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  20. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  21. package/dist/cmd/build/vite/vite-builder.js +33 -0
  22. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  23. package/dist/cmd/cloud/deploy-fork.d.ts +10 -0
  24. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
  25. package/dist/cmd/cloud/deploy-fork.js +71 -32
  26. package/dist/cmd/cloud/deploy-fork.js.map +1 -1
  27. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  28. package/dist/cmd/cloud/deploy.js +53 -11
  29. package/dist/cmd/cloud/deploy.js.map +1 -1
  30. package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
  31. package/dist/cmd/cloud/sandbox/create.js +5 -0
  32. package/dist/cmd/cloud/sandbox/create.js.map +1 -1
  33. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  34. package/dist/cmd/cloud/sandbox/exec.js +76 -66
  35. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  36. package/dist/cmd/cloud/sandbox/job/index.d.ts.map +1 -1
  37. package/dist/cmd/cloud/sandbox/job/index.js +12 -1
  38. package/dist/cmd/cloud/sandbox/job/index.js.map +1 -1
  39. package/dist/cmd/cloud/sandbox/job/logs.d.ts +3 -0
  40. package/dist/cmd/cloud/sandbox/job/logs.d.ts.map +1 -0
  41. package/dist/cmd/cloud/sandbox/job/logs.js +124 -0
  42. package/dist/cmd/cloud/sandbox/job/logs.js.map +1 -0
  43. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  44. package/dist/cmd/cloud/sandbox/run.js +14 -2
  45. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  46. package/dist/cmd/cloud/sandbox/snapshot/build.js +2 -2
  47. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  48. package/dist/cmd/coder/hub-url.d.ts.map +1 -1
  49. package/dist/cmd/coder/hub-url.js +3 -1
  50. package/dist/cmd/coder/hub-url.js.map +1 -1
  51. package/dist/cmd/coder/start.js +6 -6
  52. package/dist/cmd/coder/start.js.map +1 -1
  53. package/dist/cmd/coder/tui-init.d.ts +2 -2
  54. package/dist/cmd/coder/tui-init.js +2 -2
  55. package/dist/cmd/coder/tui-init.js.map +1 -1
  56. package/dist/cmd/project/show.d.ts.map +1 -1
  57. package/dist/cmd/project/show.js +9 -0
  58. package/dist/cmd/project/show.js.map +1 -1
  59. package/dist/cmd/support/report.d.ts.map +1 -1
  60. package/dist/cmd/support/report.js +19 -10
  61. package/dist/cmd/support/report.js.map +1 -1
  62. package/dist/errors.d.ts +24 -10
  63. package/dist/errors.d.ts.map +1 -1
  64. package/dist/errors.js +42 -12
  65. package/dist/errors.js.map +1 -1
  66. package/dist/schema-generator.d.ts.map +1 -1
  67. package/dist/schema-generator.js +2 -12
  68. package/dist/schema-generator.js.map +1 -1
  69. package/dist/steps.d.ts.map +1 -1
  70. package/dist/steps.js +38 -0
  71. package/dist/steps.js.map +1 -1
  72. package/dist/tui.d.ts.map +1 -1
  73. package/dist/tui.js +25 -9
  74. package/dist/tui.js.map +1 -1
  75. package/dist/utils/stream-capture.d.ts +9 -0
  76. package/dist/utils/stream-capture.d.ts.map +1 -0
  77. package/dist/utils/stream-capture.js +34 -0
  78. package/dist/utils/stream-capture.js.map +1 -0
  79. package/dist/utils/stream-url.d.ts +23 -0
  80. package/dist/utils/stream-url.d.ts.map +1 -0
  81. package/dist/utils/stream-url.js +153 -0
  82. package/dist/utils/stream-url.js.map +1 -0
  83. package/dist/utils/zip.d.ts.map +1 -1
  84. package/dist/utils/zip.js +19 -10
  85. package/dist/utils/zip.js.map +1 -1
  86. package/package.json +9 -7
  87. package/src/cmd/build/ci.ts +82 -80
  88. package/src/cmd/build/index.ts +0 -4
  89. package/src/cmd/build/vite/agent-discovery.ts +30 -5
  90. package/src/cmd/build/vite/route-discovery.ts +25 -12
  91. package/src/cmd/build/vite/static-renderer.ts +33 -64
  92. package/src/cmd/build/vite/vite-builder.ts +36 -0
  93. package/src/cmd/cloud/deploy-fork.ts +90 -33
  94. package/src/cmd/cloud/deploy.ts +68 -12
  95. package/src/cmd/cloud/sandbox/create.ts +7 -0
  96. package/src/cmd/cloud/sandbox/exec.ts +102 -90
  97. package/src/cmd/cloud/sandbox/job/index.ts +12 -1
  98. package/src/cmd/cloud/sandbox/job/logs.ts +139 -0
  99. package/src/cmd/cloud/sandbox/run.ts +16 -2
  100. package/src/cmd/cloud/sandbox/snapshot/build.ts +2 -2
  101. package/src/cmd/coder/hub-url.ts +3 -1
  102. package/src/cmd/coder/start.ts +6 -6
  103. package/src/cmd/coder/tui-init.ts +4 -4
  104. package/src/cmd/project/show.ts +9 -0
  105. package/src/cmd/support/report.ts +21 -10
  106. package/src/errors.ts +44 -12
  107. package/src/schema-generator.ts +2 -12
  108. package/src/steps.ts +38 -0
  109. package/src/tui.ts +24 -9
  110. package/src/utils/stream-capture.ts +39 -0
  111. package/src/utils/stream-url.ts +226 -0
  112. package/src/utils/zip.ts +22 -10
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Static Renderer
3
3
  *
4
- * When `render: 'static'` is set in agentuity.config.ts, this module:
5
- * 1. Runs a Vite SSR build to create a server-side entry point
4
+ * When `src/web/entry-server.tsx` exists, this module:
5
+ * 1. Runs a Vite SSR build (as a subprocess to avoid in-process Bun/Vite issues)
6
6
  * 2. Imports the built entry-server.js
7
7
  * 3. Discovers routes to pre-render:
8
8
  * - If `routeTree` is exported: auto-discovers all non-parameterized routes
@@ -14,7 +14,6 @@
14
14
  */
15
15
 
16
16
  import { join } from 'node:path';
17
- import { createRequire } from 'node:module';
18
17
  import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
19
18
  import type { Logger } from '../../../types';
20
19
 
@@ -74,6 +73,7 @@ function extractRoutePaths(node: RouteTreeNode): string[] {
74
73
  export interface StaticRenderOptions {
75
74
  rootDir: string;
76
75
  logger: Logger;
76
+ dev?: boolean;
77
77
  }
78
78
 
79
79
  export interface StaticRenderResult {
@@ -82,7 +82,7 @@ export interface StaticRenderResult {
82
82
  }
83
83
 
84
84
  export async function runStaticRender(options: StaticRenderOptions): Promise<StaticRenderResult> {
85
- const { rootDir, logger } = options;
85
+ const { rootDir, logger, dev = false } = options;
86
86
  const started = Date.now();
87
87
 
88
88
  const clientDir = join(rootDir, '.agentuity/client');
@@ -106,71 +106,40 @@ export async function runStaticRender(options: StaticRenderOptions): Promise<Sta
106
106
  );
107
107
  }
108
108
 
109
- const isViteDebug =
110
- process.env.AGENTUITY_VITE_DEBUG === '1' || process.env.AGENTUITY_VITE_DEBUG === 'true';
111
- if (isViteDebug) {
112
- logger.debug('Vite debug logging enabled via AGENTUITY_VITE_DEBUG');
113
- const existing = process.env.DEBUG || '';
114
- if (!existing.includes('vite:')) {
115
- process.env.DEBUG = existing ? `${existing},vite:*` : 'vite:*';
109
+ // Step 1: Vite SSR build (subprocess)
110
+ // Run as a subprocess to avoid in-process Bun/Vite module resolution issues
111
+ // that cause @mdx-js/rollup and other plugins to fail during SSR builds.
112
+ // This matches the approach used for client builds in vite-builder.ts.
113
+ logger.debug('Running Vite SSR build for static rendering (subprocess)...');
114
+
115
+ const buildMode = dev ? 'development' : 'production';
116
+
117
+ const viteProcess = Bun.spawn(
118
+ [
119
+ 'bun',
120
+ 'x',
121
+ 'vite',
122
+ 'build',
123
+ '--ssr',
124
+ entryServerPath,
125
+ '--outDir',
126
+ ssrOutDir,
127
+ '--mode',
128
+ buildMode,
129
+ ],
130
+ {
131
+ cwd: rootDir,
132
+ stdout: 'inherit',
133
+ stderr: 'inherit',
116
134
  }
117
- }
118
-
119
- // Step 1: Vite SSR build
120
- // This resolves import.meta.glob, MDX imports, and other Vite-specific APIs
121
- logger.debug('Running Vite SSR build for static rendering...');
122
-
123
- const projectRequire = createRequire(join(rootDir, 'package.json'));
124
- let vitePath = 'vite';
125
- try {
126
- vitePath = projectRequire.resolve('vite');
127
- } catch {
128
- // Use CLI's bundled version
129
- }
135
+ );
130
136
 
131
- const { build: viteBuild, loadConfigFromFile, mergeConfig } = await import(vitePath);
137
+ const exitCode = await viteProcess.exited;
132
138
 
133
- // Load vite.config.ts if it exists (v2 approach)
134
- let userConfig: import('vite').InlineConfig = {};
135
- const viteConfigPath = join(rootDir, 'vite.config.ts');
136
-
137
- if (await Bun.file(viteConfigPath).exists()) {
138
- try {
139
- const loaded = await loadConfigFromFile(
140
- { command: 'build', mode: 'production' },
141
- viteConfigPath
142
- );
143
- if (loaded?.config) {
144
- userConfig = loaded.config as import('vite').InlineConfig;
145
- logger.debug('Loaded vite.config.ts for SSR build');
146
- }
147
- } catch (error) {
148
- logger.warn('Failed to load vite.config.ts: %s', error);
149
- }
139
+ if (exitCode !== 0) {
140
+ throw new Error(`Vite SSR build exited with code ${exitCode}`);
150
141
  }
151
142
 
152
- // Merge user config with SSR build settings
153
- const ssrConfig = mergeConfig(userConfig, {
154
- root: rootDir,
155
- build: {
156
- ssr: entryServerPath,
157
- outDir: ssrOutDir,
158
- rollupOptions: {
159
- output: {
160
- format: 'esm',
161
- },
162
- },
163
- },
164
- ssr: {
165
- // Bundle all dependencies for SSR — we need import.meta.glob, MDX, etc.
166
- // resolved at build time. Node built-ins are still externalized.
167
- noExternal: true,
168
- },
169
- logLevel: isViteDebug ? 'info' : 'warn',
170
- });
171
-
172
- await viteBuild(ssrConfig);
173
-
174
143
  // Steps 2–4: wrapped in try-finally so SSR artifacts are always cleaned up,
175
144
  // even if an exception is thrown during module import, validation, or rendering.
176
145
  let routeCount = 0;
@@ -95,6 +95,27 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
95
95
  const buildMode = dev ? 'development' : 'production';
96
96
  const clientOutDir = join(rootDir, '.agentuity/client');
97
97
 
98
+ // Ensure vite.config.ts exists (fallback for projects created before v2 template update)
99
+ const viteConfigPath = join(rootDir, 'vite.config.ts');
100
+ if (!existsSync(viteConfigPath)) {
101
+ logger.debug('Generating fallback vite.config.ts');
102
+ const fallbackConfig = `import react from '@vitejs/plugin-react';
103
+ import { defineConfig } from 'vite';
104
+ import { join } from 'node:path';
105
+
106
+ export default defineConfig({
107
+ plugins: [react()],
108
+ root: '.',
109
+ build: {
110
+ rollupOptions: {
111
+ input: join(__dirname, 'src/web/index.html'),
112
+ },
113
+ },
114
+ });
115
+ `;
116
+ await Bun.write(viteConfigPath, fallbackConfig);
117
+ }
118
+
98
119
  logger.debug('Spawning vite build for client (subprocess mode)');
99
120
  logger.debug(' outDir: %s', clientOutDir);
100
121
  logger.debug(' mode: %s', buildMode);
@@ -229,6 +250,20 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
229
250
  workbenchRoute: workbenchConfig.route,
230
251
  analyticsEnabled,
231
252
  });
253
+
254
+ // Normalize index.html location: vite may output to src/web/index.html
255
+ // depending on the project's vite.config.ts configuration
256
+ const clientDir = join(rootDir, '.agentuity/client');
257
+ const nestedIndexHtml = join(clientDir, 'src/web/index.html');
258
+ const rootIndexHtml = join(clientDir, 'index.html');
259
+ if (existsSync(nestedIndexHtml) && !existsSync(rootIndexHtml)) {
260
+ const { renameSync, mkdirSync: mkdirSyncFs } = await import('node:fs');
261
+ // Ensure target directory exists
262
+ mkdirSyncFs(clientDir, { recursive: true });
263
+ renameSync(nestedIndexHtml, rootIndexHtml);
264
+ logger.debug('Moved index.html from src/web/ to client root');
265
+ }
266
+
232
267
  result.client.included = true;
233
268
  result.client.duration = Date.now() - started;
234
269
  endClientDiagnostic?.();
@@ -245,6 +280,7 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
245
280
  const staticResult = await runStaticRender({
246
281
  rootDir,
247
282
  logger,
283
+ dev,
248
284
  });
249
285
  result.static.included = true;
250
286
  result.static.duration = staticResult.duration;
@@ -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';
@@ -34,15 +34,27 @@ 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
  /**
40
- * Stream data to a Pulse stream URL
50
+ * Stream data to a Pulse stream URL.
51
+ * Accepts a string, Blob/BunFile, or ReadableStream as the body to avoid
52
+ * loading large outputs into memory.
41
53
  */
42
54
  async function streamToPulse(
43
55
  streamURL: string,
44
56
  sdkKey: string,
45
- data: string,
57
+ data: string | Blob | ReadableStream<Uint8Array>,
46
58
  logger: Logger
47
59
  ): Promise<void> {
48
60
  try {
@@ -74,7 +86,9 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
74
86
  const buildLogsStreamURL = deployment.buildLogsStreamURL;
75
87
  const reportFile = join(tmpdir(), `agentuity-deploy-${deploymentId}.json`);
76
88
  const cleanLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-logs.txt`);
77
- let outputBuffer = '';
89
+ const rawLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-raw.txt`);
90
+ const deployResultFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-result.json`);
91
+ const rawLogsWriter = createWriteStream(rawLogsFile);
78
92
  let proc: Subprocess | null = null;
79
93
  let cancelled = false;
80
94
 
@@ -149,13 +163,7 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
149
163
  process.on('SIGTERM', sigtermHandler);
150
164
 
151
165
  try {
152
- const childArgs = [
153
- 'agentuity',
154
- 'deploy',
155
- '--child-mode',
156
- `--report-file=${reportFile}`,
157
- ...args,
158
- ];
166
+ const childArgs = ['deploy', '--child-mode', `--report-file=${reportFile}`, ...args];
159
167
 
160
168
  // Pass the deployment info via environment variable (same format as CI builds)
161
169
  const deploymentEnvValue = JSON.stringify({
@@ -164,14 +172,19 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
164
172
  publicKey: deployment.publicKey,
165
173
  });
166
174
 
167
- 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(' '));
168
181
 
169
182
  // Get terminal dimensions to pass to child
170
183
  const columns = process.stdout.columns || 80;
171
184
  const rows = process.stdout.rows || 24;
172
185
 
173
186
  proc = spawn({
174
- cmd: ['bunx', ...childArgs],
187
+ cmd,
175
188
  cwd: projectDir,
176
189
  env: {
177
190
  ...process.env,
@@ -187,6 +200,8 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
187
200
  LINES: String(rows),
188
201
  // Enable clean log collection for Pulse streaming
189
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,
190
205
  },
191
206
  stdin: 'inherit',
192
207
  stdout: 'pipe',
@@ -195,7 +210,6 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
195
210
 
196
211
  const handleOutput = async (stream: ReadableStream<Uint8Array>, isStderr: boolean) => {
197
212
  const reader = stream.getReader();
198
- const decoder = new TextDecoder();
199
213
  const target = isStderr ? process.stderr : process.stdout;
200
214
 
201
215
  try {
@@ -203,8 +217,9 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
203
217
  const { done, value } = await reader.read();
204
218
  if (done) break;
205
219
 
206
- const text = decoder.decode(value, { stream: true });
207
- outputBuffer += text;
220
+ // Stream raw bytes to disk instead of accumulating in memory.
221
+ // This prevents OOM / ERR_STRING_TOO_LONG crashes on large builds.
222
+ rawLogsWriter.write(value);
208
223
  target.write(value);
209
224
  }
210
225
  } catch (err) {
@@ -223,6 +238,11 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
223
238
 
224
239
  await Promise.all([stdoutPromise, stderrPromise]);
225
240
 
241
+ // Close the raw logs writer so the file is fully flushed before reading
242
+ await new Promise<void>((resolve) => {
243
+ rawLogsWriter.end(resolve);
244
+ });
245
+
226
246
  const exitCode = await proc.exited;
227
247
  logger.debug('Child process exited with code: %d', exitCode);
228
248
 
@@ -238,23 +258,36 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
238
258
  }
239
259
  }
240
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
+
241
273
  // Stream clean logs to Pulse (prefer clean logs over raw output)
242
274
  if (buildLogsStreamURL) {
243
- let logsContent = '';
275
+ let streamedCleanLogs = false;
244
276
  if (existsSync(cleanLogsFile)) {
245
277
  try {
246
- 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
+ }
247
283
  unlinkSync(cleanLogsFile);
248
284
  } catch (err) {
249
- logger.debug('Failed to read clean logs file: %s', err);
285
+ logger.debug('Failed to stream clean logs file: %s', err);
250
286
  }
251
287
  }
252
- // Fall back to raw output if no clean logs
253
- if (!logsContent && outputBuffer) {
254
- logsContent = outputBuffer;
255
- }
256
- if (logsContent) {
257
- await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
288
+ if (!streamedCleanLogs && existsSync(rawLogsFile)) {
289
+ // Stream raw logs file directly to Pulse without loading into memory
290
+ await streamToPulse(buildLogsStreamURL, sdkKey, Bun.file(rawLogsFile), logger);
258
291
  }
259
292
  }
260
293
 
@@ -292,26 +325,50 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
292
325
  return { success: false, exitCode, diagnostics };
293
326
  }
294
327
 
295
- return { success: true, exitCode, diagnostics };
328
+ return { success: true, exitCode, diagnostics, deployResult };
296
329
  } catch (err) {
297
330
  const errorMessage = err instanceof Error ? err.message : String(err);
298
331
  logger.error('Fork deploy error: %s', errorMessage);
299
332
 
300
333
  if (buildLogsStreamURL) {
301
- let logsContent = '';
334
+ let streamedCleanLogs = false;
302
335
  if (existsSync(cleanLogsFile)) {
303
336
  try {
304
- 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
+ }
305
342
  unlinkSync(cleanLogsFile);
306
343
  } catch {
307
344
  // ignore
308
345
  }
309
346
  }
310
- if (!logsContent) {
311
- logsContent = outputBuffer;
347
+ if (streamedCleanLogs) {
348
+ await streamToPulse(
349
+ buildLogsStreamURL,
350
+ sdkKey,
351
+ `\n\n--- FORK ERROR ---\n${errorMessage}\n`,
352
+ logger
353
+ );
354
+ } else {
355
+ // Append error to raw logs file and stream it without loading into memory
356
+ try {
357
+ appendFileSync(rawLogsFile, `\n\n--- FORK ERROR ---\n${errorMessage}\n`);
358
+ } catch {
359
+ // ignore — file may not exist if child never produced output
360
+ }
361
+ if (existsSync(rawLogsFile)) {
362
+ await streamToPulse(buildLogsStreamURL, sdkKey, Bun.file(rawLogsFile), logger);
363
+ } else {
364
+ await streamToPulse(
365
+ buildLogsStreamURL,
366
+ sdkKey,
367
+ `--- FORK ERROR ---\n${errorMessage}\n`,
368
+ logger
369
+ );
370
+ }
312
371
  }
313
- logsContent += `\n\n--- FORK ERROR ---\n${errorMessage}\n`;
314
- await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
315
372
  }
316
373
 
317
374
  try {
@@ -360,7 +417,7 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
360
417
  process.off('SIGTERM', sigtermHandler);
361
418
 
362
419
  // Clean up temp files
363
- for (const file of [reportFile, cleanLogsFile]) {
420
+ for (const file of [reportFile, cleanLogsFile, rawLogsFile, deployResultFile]) {
364
421
  if (existsSync(file)) {
365
422
  try {
366
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,
@@ -83,6 +83,12 @@ export const createSubcommand = createCommand({
83
83
  .optional()
84
84
  .describe('Apt packages to install (can be specified multiple times)'),
85
85
  metadata: z.string().optional().describe('JSON object of user-defined metadata'),
86
+ scope: z
87
+ .array(z.string())
88
+ .optional()
89
+ .describe(
90
+ 'Permission scopes for service access (e.g., kv:read, aigateway, services:write)'
91
+ ),
86
92
  port: z
87
93
  .number()
88
94
  .int()
@@ -195,6 +201,7 @@ export const createSubcommand = createCommand({
195
201
  snapshot: opts.snapshot,
196
202
  dependencies: opts.dependency,
197
203
  metadata,
204
+ scopes: opts.scope,
198
205
  },
199
206
  orgId,
200
207
  });