@agentuity/cli 0.0.111 → 0.0.112

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 (99) hide show
  1. package/bin/cli.ts +4 -0
  2. package/dist/agents-docs.d.ts +5 -4
  3. package/dist/agents-docs.d.ts.map +1 -1
  4. package/dist/agents-docs.js +28 -8
  5. package/dist/agents-docs.js.map +1 -1
  6. package/dist/cmd/auth/apikey.d.ts +2 -0
  7. package/dist/cmd/auth/apikey.d.ts.map +1 -0
  8. package/dist/cmd/auth/apikey.js +31 -0
  9. package/dist/cmd/auth/apikey.js.map +1 -0
  10. package/dist/cmd/auth/index.d.ts.map +1 -1
  11. package/dist/cmd/auth/index.js +9 -1
  12. package/dist/cmd/auth/index.js.map +1 -1
  13. package/dist/cmd/build/ast.d.ts.map +1 -1
  14. package/dist/cmd/build/ast.js +103 -2
  15. package/dist/cmd/build/ast.js.map +1 -1
  16. package/dist/cmd/build/entry-generator.d.ts +2 -1
  17. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  18. package/dist/cmd/build/entry-generator.js +152 -9
  19. package/dist/cmd/build/entry-generator.js.map +1 -1
  20. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  21. package/dist/cmd/build/vite/agent-discovery.js +4 -3
  22. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  23. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  24. package/dist/cmd/build/vite/index.js +2 -1
  25. package/dist/cmd/build/vite/index.js.map +1 -1
  26. package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
  27. package/dist/cmd/build/vite/registry-generator.js +45 -0
  28. package/dist/cmd/build/vite/registry-generator.js.map +1 -1
  29. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  30. package/dist/cmd/build/vite/vite-builder.js +2 -1
  31. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  32. package/dist/cmd/cloud/deploy-fork.d.ts +32 -0
  33. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -0
  34. package/dist/cmd/cloud/deploy-fork.js +258 -0
  35. package/dist/cmd/cloud/deploy-fork.js.map +1 -0
  36. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  37. package/dist/cmd/cloud/deploy.js +62 -3
  38. package/dist/cmd/cloud/deploy.js.map +1 -1
  39. package/dist/cmd/cloud/sandbox/get.d.ts.map +1 -1
  40. package/dist/cmd/cloud/sandbox/get.js +19 -0
  41. package/dist/cmd/cloud/sandbox/get.js.map +1 -1
  42. package/dist/cmd/cloud/ssh.d.ts.map +1 -1
  43. package/dist/cmd/cloud/ssh.js +9 -3
  44. package/dist/cmd/cloud/ssh.js.map +1 -1
  45. package/dist/cmd/dev/index.d.ts.map +1 -1
  46. package/dist/cmd/dev/index.js +18 -12
  47. package/dist/cmd/dev/index.js.map +1 -1
  48. package/dist/config.js +1 -1
  49. package/dist/config.js.map +1 -1
  50. package/dist/log-collector.d.ts +30 -0
  51. package/dist/log-collector.d.ts.map +1 -0
  52. package/dist/log-collector.js +74 -0
  53. package/dist/log-collector.js.map +1 -0
  54. package/dist/output.d.ts.map +1 -1
  55. package/dist/output.js +2 -1
  56. package/dist/output.js.map +1 -1
  57. package/dist/steps.d.ts.map +1 -1
  58. package/dist/steps.js +48 -3
  59. package/dist/steps.js.map +1 -1
  60. package/dist/tui/box.d.ts.map +1 -1
  61. package/dist/tui/box.js +1 -6
  62. package/dist/tui/box.js.map +1 -1
  63. package/dist/tui/symbols.d.ts.map +1 -1
  64. package/dist/tui/symbols.js +4 -0
  65. package/dist/tui/symbols.js.map +1 -1
  66. package/dist/tui.d.ts +21 -12
  67. package/dist/tui.d.ts.map +1 -1
  68. package/dist/tui.js +74 -25
  69. package/dist/tui.js.map +1 -1
  70. package/dist/types.d.ts +71 -0
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js.map +1 -1
  73. package/dist/typescript-errors.d.ts.map +1 -1
  74. package/dist/typescript-errors.js +2 -2
  75. package/dist/typescript-errors.js.map +1 -1
  76. package/package.json +5 -5
  77. package/src/agents-docs.ts +42 -8
  78. package/src/cmd/auth/apikey.ts +36 -0
  79. package/src/cmd/auth/index.ts +9 -1
  80. package/src/cmd/build/ast.ts +120 -2
  81. package/src/cmd/build/entry-generator.ts +157 -10
  82. package/src/cmd/build/vite/agent-discovery.ts +4 -1
  83. package/src/cmd/build/vite/index.ts +2 -1
  84. package/src/cmd/build/vite/registry-generator.ts +47 -0
  85. package/src/cmd/build/vite/vite-builder.ts +2 -1
  86. package/src/cmd/cloud/deploy-fork.ts +296 -0
  87. package/src/cmd/cloud/deploy.ts +70 -3
  88. package/src/cmd/cloud/sandbox/get.ts +17 -0
  89. package/src/cmd/cloud/ssh.ts +13 -3
  90. package/src/cmd/dev/index.ts +18 -13
  91. package/src/config.ts +1 -1
  92. package/src/log-collector.ts +77 -0
  93. package/src/output.ts +2 -1
  94. package/src/steps.ts +52 -4
  95. package/src/tui/box.ts +1 -7
  96. package/src/tui/symbols.ts +5 -0
  97. package/src/tui.ts +77 -25
  98. package/src/types.ts +85 -0
  99. package/src/typescript-errors.ts +2 -1
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Deploy fork wrapper
3
+ *
4
+ * This module implements a fork-based deployment wrapper that:
5
+ * 1. Spawns the deploy command as a child process using bunx
6
+ * 2. Tees stdout/stderr to both the terminal and a Pulse stream
7
+ * 3. On failure, sends diagnostics to the API
8
+ *
9
+ * This approach captures crashes, Bun runtime issues, and all output
10
+ * for debugging failed deployments.
11
+ */
12
+
13
+ import { spawn, type Subprocess } from 'bun';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
17
+ import type { APIClient } from '../../api';
18
+ import { getUserAgent } from '../../api';
19
+ import { isUnicode } from '../../tui/symbols';
20
+ import { projectDeploymentFail, type ClientDiagnostics, type Deployment } from '@agentuity/server';
21
+ import type { Logger } from '@agentuity/core';
22
+
23
+ export interface ForkDeployOptions {
24
+ projectDir: string;
25
+ apiClient: APIClient;
26
+ logger: Logger;
27
+ sdkKey: string;
28
+ deployment: Deployment;
29
+ args: string[];
30
+ }
31
+
32
+ export interface ForkDeployResult {
33
+ success: boolean;
34
+ exitCode: number;
35
+ diagnostics?: ClientDiagnostics;
36
+ }
37
+
38
+ /**
39
+ * Stream data to a Pulse stream URL
40
+ */
41
+ async function streamToPulse(
42
+ streamURL: string,
43
+ sdkKey: string,
44
+ data: string,
45
+ logger: Logger
46
+ ): Promise<void> {
47
+ try {
48
+ const response = await fetch(streamURL, {
49
+ method: 'PUT',
50
+ headers: {
51
+ 'Content-Type': 'text/plain',
52
+ Authorization: `Bearer ${sdkKey}`,
53
+ 'User-Agent': getUserAgent(),
54
+ },
55
+ body: data,
56
+ });
57
+
58
+ if (!response.ok) {
59
+ logger.error('Failed to stream to Pulse: %s', response.status);
60
+ }
61
+ } catch (err) {
62
+ logger.error('Error streaming to Pulse: %s', err);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Run the deploy command as a forked child process
68
+ */
69
+ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkDeployResult> {
70
+ const { projectDir, apiClient, logger, sdkKey, deployment, args } = options;
71
+
72
+ const deploymentId = deployment.id;
73
+ const buildLogsStreamURL = deployment.buildLogsStreamURL;
74
+ const reportFile = join(tmpdir(), `agentuity-deploy-${deploymentId}.json`);
75
+ const cleanLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-logs.txt`);
76
+ let outputBuffer = '';
77
+ let proc: Subprocess | null = null;
78
+
79
+ try {
80
+ const childArgs = [
81
+ 'agentuity',
82
+ 'deploy',
83
+ '--child-mode',
84
+ `--report-file=${reportFile}`,
85
+ ...args,
86
+ ];
87
+
88
+ // Pass the deployment info via environment variable (same format as CI builds)
89
+ const deploymentEnvValue = JSON.stringify({
90
+ id: deployment.id,
91
+ orgId: deployment.orgId,
92
+ publicKey: deployment.publicKey,
93
+ });
94
+
95
+ logger.debug('Spawning child deploy process: bunx %s', childArgs.join(' '));
96
+
97
+ // Get terminal dimensions to pass to child
98
+ const columns = process.stdout.columns || 80;
99
+ const rows = process.stdout.rows || 24;
100
+
101
+ proc = spawn({
102
+ cmd: ['bunx', ...childArgs],
103
+ cwd: projectDir,
104
+ env: {
105
+ ...process.env,
106
+ AGENTUITY_FORK_PARENT: '1',
107
+ AGENTUITY_DEPLOYMENT: deploymentEnvValue,
108
+ // Force color and unicode output since child stdout/stderr are piped (not TTY)
109
+ FORCE_COLOR: '1',
110
+ // Only force unicode if parent terminal supports it
111
+ ...(isUnicode ? { FORCE_UNICODE: '1' } : {}),
112
+ // Pass terminal dimensions
113
+ COLUMNS: String(columns),
114
+ LINES: String(rows),
115
+ // Enable clean log collection for Pulse streaming
116
+ AGENTUITY_CLEAN_LOGS_FILE: cleanLogsFile,
117
+ },
118
+ stdin: 'inherit',
119
+ stdout: 'pipe',
120
+ stderr: 'pipe',
121
+ });
122
+
123
+ const handleOutput = async (stream: ReadableStream<Uint8Array>, isStderr: boolean) => {
124
+ const reader = stream.getReader();
125
+ const decoder = new TextDecoder();
126
+ const target = isStderr ? process.stderr : process.stdout;
127
+
128
+ try {
129
+ while (true) {
130
+ const { done, value } = await reader.read();
131
+ if (done) break;
132
+
133
+ const text = decoder.decode(value, { stream: true });
134
+ outputBuffer += text;
135
+ target.write(value);
136
+ }
137
+ } catch (err) {
138
+ logger.debug('Stream read error: %s', err);
139
+ }
140
+ };
141
+
142
+ const stdoutPromise =
143
+ proc.stdout && typeof proc.stdout !== 'number'
144
+ ? handleOutput(proc.stdout, false)
145
+ : Promise.resolve();
146
+ const stderrPromise =
147
+ proc.stderr && typeof proc.stderr !== 'number'
148
+ ? handleOutput(proc.stderr, true)
149
+ : Promise.resolve();
150
+
151
+ await Promise.all([stdoutPromise, stderrPromise]);
152
+
153
+ const exitCode = await proc.exited;
154
+ logger.debug('Child process exited with code: %d', exitCode);
155
+
156
+ let diagnostics: ClientDiagnostics | undefined;
157
+
158
+ if (existsSync(reportFile)) {
159
+ try {
160
+ const reportContent = readFileSync(reportFile, 'utf-8');
161
+ diagnostics = JSON.parse(reportContent) as ClientDiagnostics;
162
+ unlinkSync(reportFile);
163
+ } catch (err) {
164
+ logger.debug('Failed to read report file: %s', err);
165
+ }
166
+ }
167
+
168
+ // Stream clean logs to Pulse (prefer clean logs over raw output)
169
+ if (buildLogsStreamURL) {
170
+ let logsContent = '';
171
+ if (existsSync(cleanLogsFile)) {
172
+ try {
173
+ logsContent = readFileSync(cleanLogsFile, 'utf-8');
174
+ unlinkSync(cleanLogsFile);
175
+ } catch (err) {
176
+ logger.debug('Failed to read clean logs file: %s', err);
177
+ }
178
+ }
179
+ // Fall back to raw output if no clean logs
180
+ if (!logsContent && outputBuffer) {
181
+ logsContent = outputBuffer;
182
+ }
183
+ if (logsContent) {
184
+ await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
185
+ }
186
+ }
187
+
188
+ if (exitCode !== 0) {
189
+ const errorMessage = `Deploy process exited with code ${exitCode}`;
190
+
191
+ if (!diagnostics) {
192
+ diagnostics = {
193
+ success: false,
194
+ errors: [
195
+ {
196
+ type: 'general',
197
+ scope: 'deploy',
198
+ message: errorMessage,
199
+ code: 'DEPLOY_CRASH',
200
+ },
201
+ ],
202
+ warnings: [],
203
+ diagnostics: [],
204
+ error: errorMessage,
205
+ };
206
+ } else if (!diagnostics.error) {
207
+ diagnostics.error = errorMessage;
208
+ }
209
+
210
+ try {
211
+ await projectDeploymentFail(apiClient, deploymentId, {
212
+ error: errorMessage,
213
+ diagnostics,
214
+ });
215
+ } catch (err) {
216
+ logger.error('Failed to report deployment failure: %s', err);
217
+ }
218
+
219
+ return { success: false, exitCode, diagnostics };
220
+ }
221
+
222
+ return { success: true, exitCode, diagnostics };
223
+ } catch (err) {
224
+ const errorMessage = err instanceof Error ? err.message : String(err);
225
+ logger.error('Fork deploy error: %s', errorMessage);
226
+
227
+ if (buildLogsStreamURL) {
228
+ let logsContent = '';
229
+ if (existsSync(cleanLogsFile)) {
230
+ try {
231
+ logsContent = readFileSync(cleanLogsFile, 'utf-8');
232
+ unlinkSync(cleanLogsFile);
233
+ } catch {
234
+ // ignore
235
+ }
236
+ }
237
+ if (!logsContent) {
238
+ logsContent = outputBuffer;
239
+ }
240
+ logsContent += `\n\n--- FORK ERROR ---\n${errorMessage}\n`;
241
+ await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
242
+ }
243
+
244
+ try {
245
+ await projectDeploymentFail(apiClient, deploymentId, {
246
+ error: errorMessage,
247
+ diagnostics: {
248
+ success: false,
249
+ errors: [
250
+ {
251
+ type: 'general',
252
+ scope: 'deploy',
253
+ message: errorMessage,
254
+ code: 'DEPLOY_FORK_ERROR',
255
+ },
256
+ ],
257
+ warnings: [],
258
+ diagnostics: [],
259
+ error: errorMessage,
260
+ },
261
+ });
262
+ } catch (failErr) {
263
+ logger.error('Failed to report deployment failure: %s', failErr);
264
+ }
265
+
266
+ return {
267
+ success: false,
268
+ exitCode: 1,
269
+ diagnostics: {
270
+ success: false,
271
+ errors: [
272
+ {
273
+ type: 'general',
274
+ scope: 'deploy',
275
+ message: errorMessage,
276
+ code: 'DEPLOY_FORK_ERROR',
277
+ },
278
+ ],
279
+ warnings: [],
280
+ diagnostics: [],
281
+ error: errorMessage,
282
+ },
283
+ };
284
+ } finally {
285
+ // Clean up temp files
286
+ for (const file of [reportFile, cleanLogsFile]) {
287
+ if (existsSync(file)) {
288
+ try {
289
+ unlinkSync(file);
290
+ } catch {
291
+ // ignore
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
@@ -53,6 +53,7 @@ import * as domain from '../../domain';
53
53
  import { ErrorCode } from '../../errors';
54
54
  import { typecheck } from '../build/typecheck';
55
55
  import { BuildReportCollector, setGlobalCollector, clearGlobalCollector } from '../../build-report';
56
+ import { runForkedDeploy } from './deploy-fork';
56
57
 
57
58
  const DeploymentCancelledError = StructuredError(
58
59
  'DeploymentCancelled',
@@ -107,6 +108,11 @@ export const deploySubcommand = createSubcommand({
107
108
  .describe(
108
109
  'file path to save build report JSON with errors, warnings, and diagnostics'
109
110
  ),
111
+ childMode: z
112
+ .boolean()
113
+ .optional()
114
+ .default(false)
115
+ .describe('Internal: run as forked child process'),
110
116
  })
111
117
  ),
112
118
  response: DeployResponseSchema,
@@ -140,8 +146,67 @@ export const deploySubcommand = createSubcommand({
140
146
  );
141
147
  }
142
148
 
143
- // Check for pre-created deployment from CI build environment
149
+ // Check if we're running as a forked child process
150
+ const isChildProcess = opts.childMode || process.env.AGENTUITY_FORK_PARENT === '1';
144
151
  const deploymentEnv = process.env.AGENTUITY_DEPLOYMENT;
152
+
153
+ // If not in child mode and no pre-created deployment, run as fork wrapper to capture crashes
154
+ // (CI builds set AGENTUITY_DEPLOYMENT, fork wrapper also sets it for the child)
155
+ if (!isChildProcess && !deploymentEnv) {
156
+ logger.debug('Running deploy as fork wrapper');
157
+
158
+ // First, create the deployment to get the ID, publicKey, and stream URL
159
+ const deploymentConfig = project.deployment ?? {};
160
+ const initialDeployment = await projectDeploymentCreate(
161
+ apiClient,
162
+ project.projectId,
163
+ deploymentConfig
164
+ );
165
+
166
+ logger.debug('Created deployment: %s', initialDeployment.id);
167
+
168
+ // Build args to pass to child, excluding child-mode specific ones
169
+ const childArgs: string[] = [];
170
+ if (opts.logsUrl) childArgs.push(`--logs-url=${opts.logsUrl}`);
171
+ if (opts.trigger) childArgs.push(`--trigger=${opts.trigger}`);
172
+ if (opts.commitUrl) childArgs.push(`--commit-url=${opts.commitUrl}`);
173
+ if (opts.message) childArgs.push(`--message=${opts.message}`);
174
+ if (opts.commit) childArgs.push(`--commit=${opts.commit}`);
175
+ if (opts.branch) childArgs.push(`--branch=${opts.branch}`);
176
+ if (opts.provider) childArgs.push(`--provider=${opts.provider}`);
177
+ if (opts.repo) childArgs.push(`--repo=${opts.repo}`);
178
+ if (opts.event) childArgs.push(`--event=${opts.event}`);
179
+ if (opts.pullRequestNumber)
180
+ childArgs.push(`--pull-request-number=${opts.pullRequestNumber}`);
181
+ if (opts.pullRequestUrl) childArgs.push(`--pull-request-url=${opts.pullRequestUrl}`);
182
+
183
+ const result = await runForkedDeploy({
184
+ projectDir,
185
+ apiClient,
186
+ logger,
187
+ sdkKey: sdkKey!,
188
+ deployment: initialDeployment,
189
+ args: childArgs,
190
+ });
191
+
192
+ if (!result.success) {
193
+ const appUrl = getAppBaseURL(
194
+ process.env.AGENTUITY_REGION ?? config?.name,
195
+ config?.overrides
196
+ );
197
+ const deploymentLink = `${appUrl}/projects/${project.projectId}/deployments/${initialDeployment.id}`;
198
+ tui.fatal(
199
+ `Deployment failed: ${tui.link(deploymentLink, 'Deployment Page')}`,
200
+ ErrorCode.BUILD_FAILED
201
+ );
202
+ }
203
+
204
+ return {
205
+ success: true,
206
+ deploymentId: initialDeployment.id,
207
+ projectId: project.projectId,
208
+ };
209
+ }
145
210
  let useExistingDeployment = false;
146
211
  if (deploymentEnv) {
147
212
  const ExistingDeploymentSchema = z.object({
@@ -267,7 +332,9 @@ export const deploySubcommand = createSubcommand({
267
332
  label: 'Sync Env & Secrets',
268
333
  run: async () => {
269
334
  try {
270
- if (useExistingDeployment) {
335
+ const isCIBuild =
336
+ useExistingDeployment && process.env.AGENTUITY_FORK_PARENT !== '1';
337
+ if (isCIBuild) {
271
338
  return stepSkipped('skipped in CI build');
272
339
  }
273
340
  // Read env file
@@ -307,7 +374,7 @@ export const deploySubcommand = createSubcommand({
307
374
  label: 'Create Deployment',
308
375
  run: async () => {
309
376
  if (useExistingDeployment) {
310
- return stepSkipped('skipped in CI build');
377
+ return stepSkipped('using pre-created deployment');
311
378
  }
312
379
  try {
313
380
  deployment = await projectDeploymentCreate(
@@ -5,6 +5,12 @@ import { createSandboxClient } from './util';
5
5
  import { getCommand } from '../../../command-prefix';
6
6
  import { sandboxGet } from '@agentuity/server';
7
7
 
8
+ const SandboxResourcesSchema = z.object({
9
+ memory: z.string().optional().describe('Memory limit (e.g., "512Mi", "1Gi")'),
10
+ cpu: z.string().optional().describe('CPU limit (e.g., "500m", "1000m")'),
11
+ disk: z.string().optional().describe('Disk limit (e.g., "1Gi", "10Gi")'),
12
+ });
13
+
8
14
  const SandboxGetResponseSchema = z.object({
9
15
  sandboxId: z.string().describe('Sandbox ID'),
10
16
  status: z.string().describe('Current status'),
@@ -17,6 +23,7 @@ const SandboxGetResponseSchema = z.object({
17
23
  stderrStreamUrl: z.string().optional().describe('URL to stderr output stream'),
18
24
  dependencies: z.array(z.string()).optional().describe('Apt packages installed'),
19
25
  metadata: z.record(z.string(), z.unknown()).optional().describe('User-defined metadata'),
26
+ resources: SandboxResourcesSchema.optional().describe('Resource limits'),
20
27
  });
21
28
 
22
29
  export const getSubcommand = createCommand({
@@ -85,6 +92,15 @@ export const getSubcommand = createCommand({
85
92
  if (result.dependencies && result.dependencies.length > 0) {
86
93
  console.log(`${tui.muted('Dependencies:')} ${result.dependencies.join(', ')}`);
87
94
  }
95
+ if (result.resources) {
96
+ const resourceParts: string[] = [];
97
+ if (result.resources.memory) resourceParts.push(`memory=${result.resources.memory}`);
98
+ if (result.resources.cpu) resourceParts.push(`cpu=${result.resources.cpu}`);
99
+ if (result.resources.disk) resourceParts.push(`disk=${result.resources.disk}`);
100
+ if (resourceParts.length > 0) {
101
+ console.log(`${tui.muted('Resources:')} ${resourceParts.join(', ')}`);
102
+ }
103
+ }
88
104
  if (result.metadata && Object.keys(result.metadata).length > 0) {
89
105
  console.log(`${tui.muted('Metadata:')} ${JSON.stringify(result.metadata)}`);
90
106
  }
@@ -102,6 +118,7 @@ export const getSubcommand = createCommand({
102
118
  stderrStreamUrl: result.stderrStreamUrl,
103
119
  dependencies: result.dependencies,
104
120
  metadata: result.metadata,
121
+ resources: result.resources,
105
122
  };
106
123
  },
107
124
  });
@@ -4,7 +4,7 @@ import * as tui from '../../tui';
4
4
  import { getIONHost } from '../../config';
5
5
  import { getCommand } from '../../command-prefix';
6
6
  const args = z.object({
7
- identifier: z.string().optional().describe('The project or deployment id to use'),
7
+ identifier: z.string().optional().describe('The project, deployment, or sandbox id to use'),
8
8
  command: z.string().optional().describe('The command to run'),
9
9
  });
10
10
 
@@ -14,7 +14,7 @@ const options = z.object({
14
14
 
15
15
  export const sshSubcommand = createSubcommand({
16
16
  name: 'ssh',
17
- description: 'SSH into a cloud project',
17
+ description: 'SSH into a cloud project or sandbox',
18
18
  tags: ['read-only', 'slow', 'requires-auth', 'requires-deployment'],
19
19
  idempotent: true,
20
20
  examples: [
@@ -24,6 +24,10 @@ export const sshSubcommand = createSubcommand({
24
24
  command: getCommand('cloud ssh deploy_abc123xyz'),
25
25
  description: 'SSH into specific deployment',
26
26
  },
27
+ {
28
+ command: getCommand('cloud ssh sbx_abc123xyz'),
29
+ description: 'SSH into a sandbox',
30
+ },
27
31
  { command: getCommand("cloud ssh 'ps aux'"), description: 'Run command and exit' },
28
32
  {
29
33
  command: getCommand("cloud ssh proj_abc123xyz 'tail -f /var/log/app.log'"),
@@ -47,7 +51,13 @@ export const sshSubcommand = createSubcommand({
47
51
  let identifier = args?.identifier;
48
52
  let command = args?.command;
49
53
 
50
- if (!(identifier?.startsWith('proj_') || identifier?.startsWith('deploy_'))) {
54
+ if (
55
+ !(
56
+ identifier?.startsWith('proj_') ||
57
+ identifier?.startsWith('deploy_') ||
58
+ identifier?.startsWith('sbx_')
59
+ )
60
+ ) {
51
61
  command = identifier;
52
62
  identifier = undefined;
53
63
  }
@@ -707,7 +707,7 @@ export const command = createCommand({
707
707
  generateRouteRegistry(srcDir, routeInfoList);
708
708
  logger.debug('Agent and route registries generated for dev mode');
709
709
 
710
- // Step 3: Generate entry file with workbench config
710
+ // Step 3: Generate entry file with workbench and analytics config
711
711
  // Note: vitePort is NOT passed here - the app reads process.env.VITE_PORT at runtime
712
712
  const { generateEntryFile } = await import('../build/entry-generator');
713
713
  await generateEntryFile({
@@ -717,6 +717,7 @@ export const command = createCommand({
717
717
  logger,
718
718
  mode: 'dev',
719
719
  workbench: workbenchConfigData.enabled ? workbenchConfigData : undefined,
720
+ analytics: agentuityConfig?.analytics,
720
721
  });
721
722
 
722
723
  // Step 4: Bundle the app with LLM patches (dev mode = no minification)
@@ -783,19 +784,24 @@ export const command = createCommand({
783
784
  console.log('');
784
785
  fileWatcher.resume();
785
786
  // wait for a file change or shutdown to trigger a recompile
786
- while (true) {
787
- if (shutdownRequested) {
788
- return;
789
- }
790
- if (shouldRestart) {
791
- break;
792
- }
787
+ while (!shutdownRequested && !shouldRestart) {
793
788
  await tui.spinner({
794
789
  message: 'Waiting for changes...',
795
790
  clearOnSuccess: true,
796
- callback: () => Bun.sleep(1000),
791
+ callback: async () => {
792
+ // Check more frequently so CTRL+C is responsive
793
+ for (let i = 0; i < 10; i++) {
794
+ if (shutdownRequested || shouldRestart) {
795
+ return;
796
+ }
797
+ await Bun.sleep(100);
798
+ }
799
+ },
797
800
  });
798
801
  }
802
+ if (shutdownRequested) {
803
+ return;
804
+ }
799
805
  }
800
806
  } catch (error) {
801
807
  tui.error(`Failed to build dev bundle: ${error}`);
@@ -817,10 +823,6 @@ export const command = createCommand({
817
823
  }
818
824
 
819
825
  try {
820
- // Set environment variables for LLM provider patches BEFORE starting server
821
- // These must be set so the bundled patches can route LLM calls through AI Gateway
822
- const serviceUrls = getServiceUrls(project?.region);
823
-
824
826
  // Load SDK key from project .env files for AI Gateway routing
825
827
  // This must be set so the bundled AI SDK patches can inject the API key
826
828
  if (!process.env.AGENTUITY_SDK_KEY) {
@@ -848,6 +850,9 @@ export const command = createCommand({
848
850
  process.env.AGENTUITY_PORT = process.env.PORT;
849
851
 
850
852
  if (project) {
853
+ // Set environment variables for LLM provider patches
854
+ // These must be set so the bundled patches can route LLM calls through AI Gateway
855
+ const serviceUrls = getServiceUrls(project.region);
851
856
  process.env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
852
857
  process.env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
853
858
  process.env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
package/src/config.ts CHANGED
@@ -556,7 +556,7 @@ export const InitialProjectConfigSchema = z.intersection(
556
556
  type InitialProjectConfig = z.infer<typeof InitialProjectConfigSchema>;
557
557
 
558
558
  export async function createProjectConfig(dir: string, config: InitialProjectConfig) {
559
- const { sdkKey, ...sanitizedConfig } = config;
559
+ const { sdkKey, skipGitSetup: _skipGitSetup, ...sanitizedConfig } = config;
560
560
 
561
561
  // generate the project config
562
562
  const configPath = join(dir, 'agentuity.json');
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Log Collector for clean build logs
3
+ *
4
+ * Provides a mechanism to collect clean, non-animated log output
5
+ * for streaming to external services (like Pulse) while keeping
6
+ * animated TUI output for the user's terminal.
7
+ *
8
+ * Usage:
9
+ * - Set AGENTUITY_CLEAN_LOGS_FILE env var to a file path
10
+ * - TUI components call appendLog() for final state messages
11
+ * - Logs are written to the file for the parent process to read
12
+ */
13
+
14
+ import { appendFileSync, writeFileSync } from 'node:fs';
15
+
16
+ /**
17
+ * Get the clean logs file path from environment
18
+ */
19
+ function getCleanLogsFile(): string | undefined {
20
+ return process.env.AGENTUITY_CLEAN_LOGS_FILE;
21
+ }
22
+
23
+ /**
24
+ * Disable log collection (called on write errors to prevent repeated failures)
25
+ */
26
+ function disableLogCollection(error: unknown): void {
27
+ console.debug('Log collection disabled due to write error: %s', error);
28
+ delete process.env.AGENTUITY_CLEAN_LOGS_FILE;
29
+ }
30
+
31
+ /**
32
+ * Check if log collection is enabled (via environment variable)
33
+ */
34
+ export function isLogCollectionEnabled(): boolean {
35
+ return !!getCleanLogsFile();
36
+ }
37
+
38
+ /**
39
+ * Initialize the clean logs file (clears any existing content)
40
+ */
41
+ export function initCleanLogsFile(filePath: string): void {
42
+ try {
43
+ writeFileSync(filePath, '');
44
+ process.env.AGENTUITY_CLEAN_LOGS_FILE = filePath;
45
+ } catch (err) {
46
+ console.debug('Failed to initialize clean logs file: %s', err);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Append a clean log line (no ANSI codes, no animation)
52
+ * Only appends if collection is enabled
53
+ */
54
+ export function appendLog(message: string): void {
55
+ const file = getCleanLogsFile();
56
+ if (file) {
57
+ try {
58
+ appendFileSync(file, message + '\n');
59
+ } catch (err) {
60
+ disableLogCollection(err);
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Append multiple log lines
67
+ */
68
+ export function appendLogs(messages: string[]): void {
69
+ const file = getCleanLogsFile();
70
+ if (file) {
71
+ try {
72
+ appendFileSync(file, messages.join('\n') + '\n');
73
+ } catch (err) {
74
+ disableLogCollection(err);
75
+ }
76
+ }
77
+ }
package/src/output.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { GlobalOptions } from './types';
2
+ import { isTTYLike } from './tui';
2
3
 
3
4
  /**
4
5
  * Output formatting utilities for agent-friendly CLI
@@ -59,7 +60,7 @@ export function shouldDisableColors(options: GlobalOptions): boolean {
59
60
  return false;
60
61
  }
61
62
  // auto mode - disable in JSON/quiet mode or non-TTY
62
- return options.json === true || options.quiet === true || !process.stdout.isTTY;
63
+ return options.json === true || options.quiet === true || !isTTYLike();
63
64
  }
64
65
 
65
66
  /**