@agentuity/cli 1.0.59 → 1.0.61

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 (54) hide show
  1. package/dist/cmd/build/ci.d.ts +1 -1
  2. package/dist/cmd/build/ci.d.ts.map +1 -1
  3. package/dist/cmd/build/ci.js +49 -37
  4. package/dist/cmd/build/ci.js.map +1 -1
  5. package/dist/cmd/build/index.d.ts.map +1 -1
  6. package/dist/cmd/build/index.js +0 -3
  7. package/dist/cmd/build/index.js.map +1 -1
  8. package/dist/cmd/build/vite/static-render-worker.d.ts +4 -0
  9. package/dist/cmd/build/vite/static-render-worker.d.ts.map +1 -0
  10. package/dist/cmd/build/vite/static-render-worker.js +58 -0
  11. package/dist/cmd/build/vite/static-render-worker.js.map +1 -0
  12. package/dist/cmd/build/vite/vite-build-worker.d.ts +2 -0
  13. package/dist/cmd/build/vite/vite-build-worker.d.ts.map +1 -0
  14. package/dist/cmd/build/vite/vite-build-worker.js +50 -0
  15. package/dist/cmd/build/vite/vite-build-worker.js.map +1 -0
  16. package/dist/cmd/build/vite/vite-builder.d.ts +1 -0
  17. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  18. package/dist/cmd/build/vite/vite-builder.js +261 -23
  19. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  20. package/dist/cmd/cloud/deploy-fork.d.ts +10 -0
  21. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
  22. package/dist/cmd/cloud/deploy-fork.js +41 -23
  23. package/dist/cmd/cloud/deploy-fork.js.map +1 -1
  24. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  25. package/dist/cmd/cloud/deploy.js +53 -11
  26. package/dist/cmd/cloud/deploy.js.map +1 -1
  27. package/dist/cmd/project/show.d.ts.map +1 -1
  28. package/dist/cmd/project/show.js +9 -0
  29. package/dist/cmd/project/show.js.map +1 -1
  30. package/dist/cmd/support/report.d.ts.map +1 -1
  31. package/dist/cmd/support/report.js +19 -10
  32. package/dist/cmd/support/report.js.map +1 -1
  33. package/dist/steps.d.ts.map +1 -1
  34. package/dist/steps.js +38 -0
  35. package/dist/steps.js.map +1 -1
  36. package/dist/tui.d.ts.map +1 -1
  37. package/dist/tui.js +6 -4
  38. package/dist/tui.js.map +1 -1
  39. package/dist/utils/zip.d.ts.map +1 -1
  40. package/dist/utils/zip.js +19 -10
  41. package/dist/utils/zip.js.map +1 -1
  42. package/package.json +8 -8
  43. package/src/cmd/build/ci.ts +61 -49
  44. package/src/cmd/build/index.ts +0 -4
  45. package/src/cmd/build/vite/static-render-worker.ts +72 -0
  46. package/src/cmd/build/vite/vite-build-worker.ts +58 -0
  47. package/src/cmd/build/vite/vite-builder.ts +295 -23
  48. package/src/cmd/cloud/deploy-fork.ts +56 -22
  49. package/src/cmd/cloud/deploy.ts +68 -12
  50. package/src/cmd/project/show.ts +9 -0
  51. package/src/cmd/support/report.ts +21 -10
  52. package/src/steps.ts +38 -0
  53. package/src/tui.ts +6 -4
  54. package/src/utils/zip.ts +22 -10
@@ -7,7 +7,7 @@ import { ErrorCode } from '../../errors';
7
7
  import * as tui from '../../tui';
8
8
 
9
9
  export interface CIBuildOptions {
10
- url: string;
10
+ url?: string;
11
11
  directory?: string;
12
12
  trigger?: string;
13
13
  event?: string;
@@ -110,60 +110,72 @@ export async function runCIBuild(opts: CIBuildOptions, _logger: Logger): Promise
110
110
  let pendingExitCode: number | undefined;
111
111
 
112
112
  try {
113
- tempDir = await mkdtemp(join(tmpdir(), 'agentuity-ci-build-'));
114
- const sourceZipPath = join(tempDir, 'source.zip');
115
- const extractPath = join(tempDir, 'build');
116
-
117
- tui.info('1️⃣ Downloading source code from GitHub...');
118
- await downloadSource(opts.url, sourceZipPath);
119
-
120
- tui.info('2️⃣ Unzipping source code from GitHub...');
121
- await mkdir(extractPath, { recursive: true });
122
- const unzipExit = await runCommand(
123
- ['unzip', '-q', sourceZipPath, '-d', extractPath],
124
- tempDir
125
- );
126
- if (unzipExit !== 0 && unzipExit !== 1) {
127
- tui.error(`Failed to unzip source archive (exit ${unzipExit})`);
128
- pendingExitCode = unzipExit;
129
- return;
130
- }
131
-
132
- const extractedEntries = await readdir(extractPath, { withFileTypes: true });
133
- const extractedDirs = extractedEntries.filter((entry) => entry.isDirectory());
134
- if (extractedDirs.length !== 1) {
135
- tui.fatal(
136
- `Expected one root directory after unzip, found ${extractedDirs.length}`,
137
- ErrorCode.BUILD_FAILED
113
+ let projectDir: string;
114
+
115
+ if (opts.url) {
116
+ // Download and extract source from URL
117
+ tempDir = await mkdtemp(join(tmpdir(), 'agentuity-ci-build-'));
118
+ const sourceZipPath = join(tempDir, 'source.zip');
119
+ const extractPath = join(tempDir, 'build');
120
+
121
+ tui.info('1️⃣ Downloading source code from GitHub...');
122
+ await downloadSource(opts.url, sourceZipPath);
123
+
124
+ tui.info('2️⃣ Unzipping source code from GitHub...');
125
+ await mkdir(extractPath, { recursive: true });
126
+ const unzipExit = await runCommand(
127
+ ['unzip', '-q', sourceZipPath, '-d', extractPath],
128
+ tempDir
138
129
  );
139
- }
130
+ if (unzipExit !== 0 && unzipExit !== 1) {
131
+ tui.error(`Failed to unzip source archive (exit ${unzipExit})`);
132
+ pendingExitCode = unzipExit;
133
+ return;
134
+ }
140
135
 
141
- const sourceRoot = extractedDirs.at(0);
142
- if (!sourceRoot) {
143
- tui.fatal('Could not determine extracted source directory', ErrorCode.BUILD_FAILED);
144
- }
136
+ const extractedEntries = await readdir(extractPath, { withFileTypes: true });
137
+ const extractedDirs = extractedEntries.filter((entry) => entry.isDirectory());
138
+ if (extractedDirs.length !== 1) {
139
+ tui.fatal(
140
+ `Expected one root directory after unzip, found ${extractedDirs.length}`,
141
+ ErrorCode.BUILD_FAILED
142
+ );
143
+ }
145
144
 
146
- const sourceRootDir = join(extractPath, sourceRoot.name);
147
- let projectDir = sourceRootDir;
148
- if (opts.directory) {
149
- projectDir = join(sourceRootDir, opts.directory);
150
- }
145
+ const sourceRoot = extractedDirs.at(0);
146
+ if (!sourceRoot) {
147
+ tui.fatal('Could not determine extracted source directory', ErrorCode.BUILD_FAILED);
148
+ }
151
149
 
152
- const projectStats = await stat(projectDir).catch(() => null);
153
- if (!projectStats?.isDirectory()) {
154
- tui.fatal(`Build directory not found: ${projectDir}`, ErrorCode.CONFIG_INVALID);
155
- }
150
+ const sourceRootDir = join(extractPath, sourceRoot.name);
151
+ projectDir = sourceRootDir;
152
+ if (opts.directory) {
153
+ projectDir = join(sourceRootDir, opts.directory);
154
+ }
156
155
 
157
- // Resolve symlinks and verify the project dir is within the source root
158
- const realProjectDir = await realpath(projectDir).catch(() => null);
159
- const realSourceRoot = await realpath(sourceRootDir).catch(() => null);
160
- if (!realProjectDir || !realSourceRoot || !realProjectDir.startsWith(realSourceRoot)) {
161
- tui.fatal(
162
- 'Directory path escapes the source root (path traversal denied)',
163
- ErrorCode.CONFIG_INVALID
164
- );
156
+ const projectStats = await stat(projectDir).catch(() => null);
157
+ if (!projectStats?.isDirectory()) {
158
+ tui.fatal(`Build directory not found: ${projectDir}`, ErrorCode.CONFIG_INVALID);
159
+ }
160
+
161
+ // Resolve symlinks and verify the project dir is within the source root
162
+ const realProjectDir = await realpath(projectDir).catch(() => null);
163
+ const realSourceRoot = await realpath(sourceRootDir).catch(() => null);
164
+ if (!realProjectDir || !realSourceRoot || !realProjectDir.startsWith(realSourceRoot)) {
165
+ tui.fatal(
166
+ 'Directory path escapes the source root (path traversal denied)',
167
+ ErrorCode.CONFIG_INVALID
168
+ );
169
+ }
170
+ projectDir = realProjectDir;
171
+ } else {
172
+ // No URL — use current working directory (source already present, e.g. snapshot-based deploy)
173
+ tui.info('1️⃣ Using local source (no download URL provided)...');
174
+ projectDir = process.cwd();
175
+ if (opts.directory) {
176
+ projectDir = join(projectDir, opts.directory);
177
+ }
165
178
  }
166
- projectDir = realProjectDir;
167
179
 
168
180
  const sdkKey = process.env.AGENTUITY_SDK_KEY;
169
181
  if (sdkKey) {
@@ -57,10 +57,6 @@ export const command = createCommand({
57
57
  const { opts, projectDir, project } = ctx;
58
58
 
59
59
  if (opts.ci) {
60
- if (!opts.url) {
61
- tui.fatal('--url is required when using --ci mode', ErrorCode.CONFIG_INVALID);
62
- }
63
-
64
60
  const { runCIBuild } = await import('./ci');
65
61
  await runCIBuild(
66
62
  {
@@ -0,0 +1,72 @@
1
+ import { format } from 'node:util';
2
+ import type { Logger } from '../../../types';
3
+ import { runStaticRender } from './static-renderer';
4
+
5
+ function createWorkerLogger(): Logger {
6
+ const write = (writer: (...args: unknown[]) => void, args: unknown[]) => {
7
+ writer(format(...args));
8
+ };
9
+ let loggerRef: Logger;
10
+
11
+ loggerRef = {
12
+ trace: (...args: unknown[]) => write(console.debug, args),
13
+ debug: (...args: unknown[]) => write(console.debug, args),
14
+ info: (...args: unknown[]) => write(console.log, args),
15
+ warn: (...args: unknown[]) => write(console.warn, args),
16
+ error: (...args: unknown[]) => write(console.error, args),
17
+ child: () => loggerRef,
18
+ fatal: (...args: unknown[]) => {
19
+ const message = format(...args);
20
+ console.error(message);
21
+ throw new Error(message);
22
+ },
23
+ };
24
+
25
+ return loggerRef;
26
+ }
27
+
28
+ export interface StaticRenderWorkerOptions {
29
+ rootDir: string;
30
+ }
31
+
32
+ async function main(): Promise<void> {
33
+ const optionsPath = process.argv[2];
34
+ if (!optionsPath) {
35
+ throw new Error('Missing worker options file path argument');
36
+ }
37
+
38
+ const optionsFile = Bun.file(optionsPath);
39
+ if (!(await optionsFile.exists())) {
40
+ throw new Error(`Worker options file does not exist: ${optionsPath}`);
41
+ }
42
+
43
+ const options = JSON.parse(await optionsFile.text()) as StaticRenderWorkerOptions;
44
+ const logger = createWorkerLogger();
45
+
46
+ // Load user plugins from agentuity.config.ts (can't serialize plugin functions)
47
+ const { loadAgentuityConfig } = await import('./config-loader');
48
+ const config = await loadAgentuityConfig(options.rootDir, logger);
49
+ const userPlugins = config?.plugins || [];
50
+
51
+ const result = await runStaticRender({
52
+ rootDir: options.rootDir,
53
+ logger,
54
+ userPlugins,
55
+ });
56
+
57
+ // Write result to stdout for parent to parse
58
+ console.log(JSON.stringify(result));
59
+ }
60
+
61
+ void main()
62
+ .then(() => {
63
+ process.exit(0);
64
+ })
65
+ .catch((error) => {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ console.error(`[static-render-worker] ${message}`);
68
+ if (error instanceof Error && error.stack) {
69
+ console.error(error.stack);
70
+ }
71
+ process.exit(1);
72
+ });
@@ -0,0 +1,58 @@
1
+ import { format } from 'node:util';
2
+ import type { Logger } from '../../../types';
3
+ import { runViteBuild, type ViteBuildWorkerOptions } from './vite-builder';
4
+
5
+ function createWorkerLogger(): Logger {
6
+ const write = (writer: (...args: unknown[]) => void, args: unknown[]) => {
7
+ writer(format(...args));
8
+ };
9
+ let loggerRef: Logger;
10
+
11
+ loggerRef = {
12
+ trace: (...args: unknown[]) => write(console.debug, args),
13
+ debug: (...args: unknown[]) => write(console.debug, args),
14
+ info: (...args: unknown[]) => write(console.log, args),
15
+ warn: (...args: unknown[]) => write(console.warn, args),
16
+ error: (...args: unknown[]) => write(console.error, args),
17
+ child: () => loggerRef,
18
+ fatal: (...args: unknown[]) => {
19
+ const message = format(...args);
20
+ console.error(message);
21
+ throw new Error(message);
22
+ },
23
+ };
24
+
25
+ return loggerRef;
26
+ }
27
+
28
+ async function main(): Promise<void> {
29
+ const optionsPath = process.argv[2];
30
+ if (!optionsPath) {
31
+ throw new Error('Missing worker options file path argument');
32
+ }
33
+
34
+ const optionsFile = Bun.file(optionsPath);
35
+ if (!(await optionsFile.exists())) {
36
+ throw new Error(`Worker options file does not exist: ${optionsPath}`);
37
+ }
38
+
39
+ const options = JSON.parse(await optionsFile.text()) as ViteBuildWorkerOptions;
40
+
41
+ await runViteBuild({
42
+ ...options,
43
+ logger: createWorkerLogger(),
44
+ });
45
+ }
46
+
47
+ void main()
48
+ .then(() => {
49
+ process.exit(0);
50
+ })
51
+ .catch((error) => {
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ console.error(`[vite-build-worker] ${message}`);
54
+ if (error instanceof Error && error.stack) {
55
+ console.error(error.stack);
56
+ }
57
+ process.exit(1);
58
+ });
@@ -6,7 +6,11 @@
6
6
 
7
7
  import { join } from 'node:path';
8
8
  import { existsSync, renameSync, rmSync } from 'node:fs';
9
+ import { randomUUID } from 'node:crypto';
9
10
  import { createRequire } from 'node:module';
11
+ import { tmpdir } from 'node:os';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { StructuredError } from '@agentuity/core';
10
14
  import type { InlineConfig, Plugin } from 'vite';
11
15
  import type { Logger, DeployOptions } from '../../../types';
12
16
  import { browserEnvPlugin } from './browser-env-plugin';
@@ -15,6 +19,8 @@ import { beaconPlugin } from './beacon-plugin';
15
19
  import { publicAssetPathPlugin } from './public-asset-path-plugin';
16
20
  import type { BuildReportCollector } from '../../../build-report';
17
21
 
22
+ const BuildFailedError = StructuredError('BuildFailedError');
23
+
18
24
  /**
19
25
  * Vite plugin to flatten the output structure for index.html
20
26
  *
@@ -70,6 +76,232 @@ export interface ViteBuildOptions {
70
76
  profile?: string;
71
77
  }
72
78
 
79
+ export type ViteBuildWorkerOptions = Omit<ViteBuildOptions, 'logger' | 'collector'>;
80
+
81
+ /**
82
+ * Drain a subprocess stream, forwarding each chunk to the callback
83
+ * without accumulating in memory. Only the last `tailBytes` of output
84
+ * are retained so we can include them in error messages on failure.
85
+ */
86
+ async function drainSubprocessStream(
87
+ stream: ReadableStream<Uint8Array>,
88
+ onChunk: (chunk: string) => void,
89
+ tailBytes = 4096
90
+ ): Promise<string> {
91
+ const reader = stream.getReader();
92
+ const decoder = new TextDecoder();
93
+ // Ring buffer that keeps only the trailing output for error context
94
+ let tail = '';
95
+
96
+ while (true) {
97
+ const { done, value } = await reader.read();
98
+ if (done) break;
99
+ const text = decoder.decode(value, { stream: true });
100
+ if (!text) continue;
101
+ onChunk(text);
102
+ tail = (tail + text).slice(-tailBytes);
103
+ }
104
+
105
+ const finalText = decoder.decode();
106
+ if (finalText) {
107
+ onChunk(finalText);
108
+ tail = (tail + finalText).slice(-tailBytes);
109
+ }
110
+
111
+ return tail;
112
+ }
113
+
114
+ /**
115
+ * Detect actually available memory for subprocess heap sizing.
116
+ * Reads cgroup v2/v1 max and current usage to compute free memory.
117
+ * Falls back to os.freemem() if cgroup files aren't available.
118
+ */
119
+ async function detectAvailableMemory(
120
+ logger: Logger,
121
+ label: string
122
+ ): Promise<Record<string, string>> {
123
+ let cgroupMaxBytes = 0;
124
+ let cgroupCurrentBytes = 0;
125
+ let availableBytes = 0;
126
+ try {
127
+ if (process.platform === 'linux') {
128
+ // Try cgroup v2
129
+ const cgroupMax = Bun.file('/sys/fs/cgroup/memory.max');
130
+ const cgroupCurrent = Bun.file('/sys/fs/cgroup/memory.current');
131
+ if ((await cgroupMax.exists()) && (await cgroupCurrent.exists())) {
132
+ const maxStr = (await cgroupMax.text()).trim();
133
+ const currentStr = (await cgroupCurrent.text()).trim();
134
+ // "max" means unlimited — use total system memory
135
+ if (maxStr === 'max') {
136
+ const { totalmem } = await import('node:os');
137
+ cgroupMaxBytes = totalmem();
138
+ } else if (/^\d+$/.test(maxStr)) {
139
+ cgroupMaxBytes = Number(maxStr);
140
+ }
141
+ if (/^\d+$/.test(currentStr)) {
142
+ cgroupCurrentBytes = Number(currentStr);
143
+ }
144
+ if (cgroupMaxBytes > 0) {
145
+ availableBytes =
146
+ cgroupCurrentBytes > 0 ? cgroupMaxBytes - cgroupCurrentBytes : cgroupMaxBytes;
147
+ }
148
+ }
149
+ // Fallback to cgroup v1
150
+ if (!availableBytes) {
151
+ const v1Limit = Bun.file('/sys/fs/cgroup/memory/memory.limit_in_bytes');
152
+ const v1Usage = Bun.file('/sys/fs/cgroup/memory/memory.usage_in_bytes');
153
+ if ((await v1Limit.exists()) && (await v1Usage.exists())) {
154
+ const limitStr = (await v1Limit.text()).trim();
155
+ const usageStr = (await v1Usage.text()).trim();
156
+ if (/^\d+$/.test(limitStr) && /^\d+$/.test(usageStr)) {
157
+ cgroupMaxBytes = Number(limitStr);
158
+ cgroupCurrentBytes = Number(usageStr);
159
+ availableBytes = cgroupMaxBytes - cgroupCurrentBytes;
160
+ }
161
+ }
162
+ }
163
+ }
164
+ if (!availableBytes) {
165
+ const { freemem } = await import('node:os');
166
+ availableBytes = freemem();
167
+ }
168
+ } catch {
169
+ // If detection fails, let JSC use its own defaults
170
+ }
171
+
172
+ const jscEnv: Record<string, string> = {};
173
+ if (availableBytes > 0) {
174
+ const maxMb = Math.round(cgroupMaxBytes / 1024 / 1024);
175
+ const usedMb = Math.round(cgroupCurrentBytes / 1024 / 1024);
176
+ const availMb = Math.round(availableBytes / 1024 / 1024);
177
+ const maxHeap = Math.round(availableBytes * 0.8);
178
+ const maxHeapMb = Math.round(maxHeap / 1024 / 1024);
179
+ jscEnv.BUN_JSC_forceRAMSize = String(availableBytes);
180
+ jscEnv.BUN_JSC_gcMaxHeapSize = String(maxHeap);
181
+ logger.debug(
182
+ `[${label}] Memory: cgroup max=${maxMb} MiB, used=${usedMb} MiB, available=${availMb} MiB, gcMaxHeapSize=${maxHeapMb} MiB`
183
+ );
184
+ } else {
185
+ logger.debug(`[${label}] Could not detect available memory, using JSC defaults`);
186
+ }
187
+ return jscEnv;
188
+ }
189
+
190
+ function resolveWorkerScript(name: string): string {
191
+ const tsPath = fileURLToPath(new URL(`./${name}.ts`, import.meta.url));
192
+ const jsPath = fileURLToPath(new URL(`./${name}.js`, import.meta.url));
193
+ return existsSync(tsPath) ? tsPath : jsPath;
194
+ }
195
+
196
+ /**
197
+ * Spawn an isolated worker subprocess with JSC memory tuning.
198
+ * Returns the captured stdout tail (for parsing results) and throws on failure.
199
+ */
200
+ async function spawnIsolatedWorker(opts: {
201
+ workerName: string;
202
+ label: string;
203
+ optionsJson: unknown;
204
+ cwd: string;
205
+ logger: Logger;
206
+ }): Promise<string> {
207
+ const { workerName, label, optionsJson, cwd, logger } = opts;
208
+ const workerScript = resolveWorkerScript(workerName);
209
+ const optionsPath = join(tmpdir(), `agentuity-${label}-${randomUUID()}.json`);
210
+
211
+ try {
212
+ await Bun.write(optionsPath, JSON.stringify(optionsJson));
213
+ const jscEnv = await detectAvailableMemory(logger, label);
214
+
215
+ const proc = Bun.spawn({
216
+ cmd: [process.execPath, workerScript, optionsPath],
217
+ cwd,
218
+ env: { ...process.env, ...jscEnv },
219
+ stdin: 'ignore',
220
+ stdout: 'pipe',
221
+ stderr: 'pipe',
222
+ });
223
+
224
+ const stdoutPromise =
225
+ proc.stdout && typeof proc.stdout !== 'number'
226
+ ? drainSubprocessStream(proc.stdout, (chunk) => {
227
+ const text = chunk.trim();
228
+ if (text) logger.debug(`[${label}] ${text}`);
229
+ })
230
+ : Promise.resolve('');
231
+
232
+ const stderrPromise =
233
+ proc.stderr && typeof proc.stderr !== 'number'
234
+ ? drainSubprocessStream(proc.stderr, (chunk) => {
235
+ const text = chunk.trim();
236
+ if (text) logger.error(`[${label}] ${text}`);
237
+ })
238
+ : Promise.resolve('');
239
+
240
+ const [stdout, stderr, exitCode] = await Promise.all([
241
+ stdoutPromise,
242
+ stderrPromise,
243
+ proc.exited,
244
+ ]);
245
+
246
+ if (exitCode !== 0) {
247
+ const errorOutput = stderr.trim() || stdout.trim();
248
+ const suffix = errorOutput ? `: ${errorOutput}` : '';
249
+ throw new BuildFailedError({
250
+ message: `${label} failed with exit code ${exitCode}${suffix}`,
251
+ });
252
+ }
253
+ return stdout;
254
+ } catch (error) {
255
+ if (error instanceof Error && error.name === 'BuildFailedError') throw error;
256
+ throw new BuildFailedError({
257
+ message: `Failed to run ${label}: ${error instanceof Error ? error.message : String(error)}`,
258
+ });
259
+ } finally {
260
+ rmSync(optionsPath, { force: true });
261
+ }
262
+ }
263
+
264
+ async function runViteBuildInSubprocess(
265
+ options: ViteBuildWorkerOptions,
266
+ logger: Logger
267
+ ): Promise<void> {
268
+ await spawnIsolatedWorker({
269
+ workerName: 'vite-build-worker',
270
+ label: `vite-build-worker:${options.mode}`,
271
+ optionsJson: options,
272
+ cwd: options.rootDir,
273
+ logger,
274
+ });
275
+ }
276
+
277
+ async function runStaticRenderInSubprocess(
278
+ rootDir: string,
279
+ logger: Logger
280
+ ): Promise<{ routes: number; duration: number }> {
281
+ const stdout = await spawnIsolatedWorker({
282
+ workerName: 'static-render-worker',
283
+ label: 'static-render-worker',
284
+ optionsJson: { rootDir },
285
+ cwd: rootDir,
286
+ logger,
287
+ });
288
+
289
+ // Worker writes JSON result as last line of stdout
290
+ try {
291
+ // Find the last JSON line in the tail
292
+ const lines = stdout.split('\n').filter((l) => l.trim());
293
+ for (let i = lines.length - 1; i >= 0; i--) {
294
+ const line = lines[i]!.trim();
295
+ if (line.startsWith('{')) {
296
+ return JSON.parse(line);
297
+ }
298
+ }
299
+ } catch {
300
+ // Parse failure — return defaults
301
+ }
302
+ return { routes: 0, duration: 0 };
303
+ }
304
+
73
305
  /**
74
306
  * Run a Vite build for the specified mode
75
307
  * Uses inline Vite config (customizable via agentuity.config.ts)
@@ -256,6 +488,9 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
256
488
  // Copy public files to output for CDN upload (production builds only)
257
489
  // In dev mode, Vite serves them directly from src/web/public/
258
490
  copyPublicDir: !dev,
491
+ // Skip compressed size reporting to save memory — we measure
492
+ // sizes during the asset upload phase instead.
493
+ reportCompressedSize: false,
259
494
  },
260
495
  logLevel: isViteDebug ? 'info' : 'warn',
261
496
  };
@@ -322,6 +557,7 @@ interface BuildResult {
322
557
  */
323
558
  export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Promise<BuildResult> {
324
559
  const { rootDir, projectId = '', dev = false, logger, collector } = options;
560
+ const { logger: _logger, collector: _collector, ...workerBaseOptions } = options;
325
561
 
326
562
  if (!dev) {
327
563
  rmSync(join(rootDir, '.agentuity'), { force: true, recursive: true });
@@ -393,13 +629,26 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
393
629
  logger.debug('Building client assets...');
394
630
  const endClientDiagnostic = collector?.startDiagnostic('client-build');
395
631
  const started = Date.now();
396
- await runViteBuild({
397
- ...options,
398
- mode: 'client',
399
- workbenchEnabled: workbenchConfig.enabled,
400
- workbenchRoute: workbenchConfig.route,
401
- analyticsEnabled,
402
- });
632
+ if (dev) {
633
+ await runViteBuild({
634
+ ...options,
635
+ mode: 'client',
636
+ workbenchEnabled: workbenchConfig.enabled,
637
+ workbenchRoute: workbenchConfig.route,
638
+ analyticsEnabled,
639
+ });
640
+ } else {
641
+ await runViteBuildInSubprocess(
642
+ {
643
+ ...workerBaseOptions,
644
+ mode: 'client',
645
+ workbenchEnabled: workbenchConfig.enabled,
646
+ workbenchRoute: workbenchConfig.route,
647
+ analyticsEnabled,
648
+ },
649
+ logger
650
+ );
651
+ }
403
652
  result.client.included = true;
404
653
  result.client.duration = Date.now() - started;
405
654
  endClientDiagnostic?.();
@@ -411,15 +660,22 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
411
660
  if (config?.render === 'static' && hasWebFrontend) {
412
661
  logger.debug('Running static rendering (pre-rendering all routes)...');
413
662
  const endStaticDiagnostic = collector?.startDiagnostic('static-render');
414
- const { runStaticRender } = await import('./static-renderer');
415
- const staticResult = await runStaticRender({
416
- rootDir,
417
- logger,
418
- userPlugins: config?.plugins || [],
419
- });
420
- result.static.included = true;
421
- result.static.duration = staticResult.duration;
422
- result.static.routes = staticResult.routes;
663
+ if (dev) {
664
+ const { runStaticRender } = await import('./static-renderer');
665
+ const staticResult = await runStaticRender({
666
+ rootDir,
667
+ logger,
668
+ userPlugins: config?.plugins || [],
669
+ });
670
+ result.static.included = true;
671
+ result.static.duration = staticResult.duration;
672
+ result.static.routes = staticResult.routes;
673
+ } else {
674
+ const staticResult = await runStaticRenderInSubprocess(rootDir, logger);
675
+ result.static.included = true;
676
+ result.static.duration = staticResult.duration;
677
+ result.static.routes = staticResult.routes;
678
+ }
423
679
  endStaticDiagnostic?.();
424
680
  }
425
681
 
@@ -428,12 +684,24 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
428
684
  logger.debug('Building workbench assets...');
429
685
  const endWorkbenchDiagnostic = collector?.startDiagnostic('workbench-build');
430
686
  const started = Date.now();
431
- await runViteBuild({
432
- ...options,
433
- mode: 'workbench',
434
- workbenchRoute: workbenchConfig.route,
435
- workbenchEnabled: true,
436
- });
687
+ if (dev) {
688
+ await runViteBuild({
689
+ ...options,
690
+ mode: 'workbench',
691
+ workbenchRoute: workbenchConfig.route,
692
+ workbenchEnabled: true,
693
+ });
694
+ } else {
695
+ await runViteBuildInSubprocess(
696
+ {
697
+ ...workerBaseOptions,
698
+ mode: 'workbench',
699
+ workbenchRoute: workbenchConfig.route,
700
+ workbenchEnabled: true,
701
+ },
702
+ logger
703
+ );
704
+ }
437
705
  result.workbench.included = true;
438
706
  result.workbench.duration = Date.now() - started;
439
707
  endWorkbenchDiagnostic?.();
@@ -443,7 +711,11 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
443
711
  logger.debug('Building server...');
444
712
  const endServerDiagnostic = collector?.startDiagnostic('server-build');
445
713
  const serverStarted = Date.now();
446
- await runViteBuild({ ...options, mode: 'server' });
714
+ if (dev) {
715
+ await runViteBuild({ ...options, mode: 'server' });
716
+ } else {
717
+ await runViteBuildInSubprocess({ ...workerBaseOptions, mode: 'server' }, logger);
718
+ }
447
719
  result.server.included = true;
448
720
  result.server.duration = Date.now() - serverStarted;
449
721
  endServerDiagnostic?.();