@amodalai/amodal 0.3.89 → 0.3.91

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 (75) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/src/commands/audit.d.ts +1 -3
  3. package/dist/src/commands/audit.d.ts.map +1 -1
  4. package/dist/src/commands/audit.js +4 -53
  5. package/dist/src/commands/audit.js.map +1 -1
  6. package/dist/src/commands/build-manifest-types.js +1 -1
  7. package/dist/src/commands/build-tools.d.ts +3 -10
  8. package/dist/src/commands/build-tools.d.ts.map +1 -1
  9. package/dist/src/commands/build-tools.js +5 -118
  10. package/dist/src/commands/build-tools.js.map +1 -1
  11. package/dist/src/commands/build.d.ts +23 -5
  12. package/dist/src/commands/build.d.ts.map +1 -1
  13. package/dist/src/commands/build.js +53 -34
  14. package/dist/src/commands/build.js.map +1 -1
  15. package/dist/src/commands/chat.d.ts +1 -1
  16. package/dist/src/commands/chat.js +5 -5
  17. package/dist/src/commands/chat.js.map +1 -1
  18. package/dist/src/commands/deploy.d.ts +1 -1
  19. package/dist/src/commands/deploy.d.ts.map +1 -1
  20. package/dist/src/commands/deploy.js +3 -61
  21. package/dist/src/commands/deploy.js.map +1 -1
  22. package/dist/src/commands/deployments.d.ts.map +1 -1
  23. package/dist/src/commands/deployments.js +3 -36
  24. package/dist/src/commands/deployments.js.map +1 -1
  25. package/dist/src/commands/dev.d.ts.map +1 -1
  26. package/dist/src/commands/dev.js +7 -10
  27. package/dist/src/commands/dev.js.map +1 -1
  28. package/dist/src/commands/experiment.d.ts +1 -3
  29. package/dist/src/commands/experiment.d.ts.map +1 -1
  30. package/dist/src/commands/experiment.js +4 -102
  31. package/dist/src/commands/experiment.js.map +1 -1
  32. package/dist/src/commands/promote.d.ts.map +1 -1
  33. package/dist/src/commands/promote.js +3 -21
  34. package/dist/src/commands/promote.js.map +1 -1
  35. package/dist/src/commands/rollback.d.ts.map +1 -1
  36. package/dist/src/commands/rollback.js +3 -24
  37. package/dist/src/commands/rollback.js.map +1 -1
  38. package/dist/src/commands/secrets.d.ts.map +1 -1
  39. package/dist/src/commands/secrets.js +2 -102
  40. package/dist/src/commands/secrets.js.map +1 -1
  41. package/dist/src/commands/serve.d.ts +2 -11
  42. package/dist/src/commands/serve.d.ts.map +1 -1
  43. package/dist/src/commands/serve.js +44 -87
  44. package/dist/src/commands/serve.js.map +1 -1
  45. package/dist/src/commands/status.d.ts.map +1 -1
  46. package/dist/src/commands/status.js +3 -49
  47. package/dist/src/commands/status.js.map +1 -1
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +7 -8
  50. package/src/commands/audit.ts +4 -71
  51. package/src/commands/build-manifest-types.ts +1 -1
  52. package/src/commands/build-tools.ts +5 -142
  53. package/src/commands/build.test.ts +14 -9
  54. package/src/commands/build.ts +73 -33
  55. package/src/commands/chat.ts +5 -5
  56. package/src/commands/deploy.test.ts +2 -13
  57. package/src/commands/deploy.ts +5 -67
  58. package/src/commands/deployments.ts +3 -39
  59. package/src/commands/dev.ts +7 -10
  60. package/src/commands/experiment.ts +4 -110
  61. package/src/commands/promote.ts +3 -21
  62. package/src/commands/rollback.ts +3 -24
  63. package/src/commands/secrets.test.ts +12 -134
  64. package/src/commands/secrets.ts +2 -116
  65. package/src/commands/serve.ts +46 -93
  66. package/src/commands/status.ts +3 -51
  67. package/src/e2e-commands.test.ts +18 -17
  68. package/dist/src/shared/platform-client.d.ts +0 -123
  69. package/dist/src/shared/platform-client.d.ts.map +0 -1
  70. package/dist/src/shared/platform-client.js +0 -280
  71. package/dist/src/shared/platform-client.js.map +0 -1
  72. package/src/commands/audit.test.ts +0 -92
  73. package/src/commands/experiment.test.ts +0 -125
  74. package/src/shared/platform-client.test.ts +0 -106
  75. package/src/shared/platform-client.ts +0 -367
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amodalai/amodal",
3
- "version": "0.3.89",
3
+ "version": "0.3.91",
4
4
  "description": "Amodal CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,7 +18,6 @@
18
18
  "amodal": "dist/src/main.js"
19
19
  },
20
20
  "dependencies": {
21
- "@aws-sdk/client-s3": "^3.700.0",
22
21
  "ink": "^6.8.0",
23
22
  "ink-spinner": "^5.0.0",
24
23
  "ink-text-input": "^6.0.0",
@@ -27,12 +26,12 @@
27
26
  "react": "^19.2.4",
28
27
  "yargs": "^17.7.2",
29
28
  "zod": "^4.3.6",
30
- "@amodalai/types": "0.3.89",
31
- "@amodalai/core": "0.3.89",
32
- "@amodalai/db": "0.3.89",
33
- "@amodalai/runtime": "0.3.89",
34
- "@amodalai/studio": "0.3.89",
35
- "@amodalai/runtime-app": "0.3.89"
29
+ "@amodalai/types": "0.3.91",
30
+ "@amodalai/core": "0.3.91",
31
+ "@amodalai/db": "0.3.91",
32
+ "@amodalai/runtime": "0.3.91",
33
+ "@amodalai/studio": "0.3.91",
34
+ "@amodalai/runtime-app": "0.3.91"
36
35
  },
37
36
  "devDependencies": {
38
37
  "@types/node": "^20.11.24",
@@ -5,86 +5,19 @@
5
5
  */
6
6
 
7
7
  import type {CommandModule} from 'yargs';
8
- import {resolvePlatformConfig} from '../shared/platform-client.js';
9
8
 
10
9
  export interface AuditOptions {
11
10
  sessionId: string;
12
11
  format?: 'json' | 'table';
13
- platformUrl?: string;
14
- platformApiKey?: string;
15
12
  }
16
13
 
17
14
  /**
18
- * Retrieve audit trail for a session from the platform.
15
+ * Hosted audit trails are not available from the OSS CLI.
19
16
  */
20
17
  export async function runAudit(options: AuditOptions): Promise<void> {
21
- let platformUrl: string;
22
- let apiKey: string;
23
- try {
24
- const config = await resolvePlatformConfig({
25
- url: options.platformUrl,
26
- apiKey: options.platformApiKey,
27
- });
28
- platformUrl = config.url;
29
- apiKey = config.apiKey;
30
- } catch (err) {
31
- const msg = err instanceof Error ? err.message : String(err);
32
- process.stderr.write(`[audit] ${msg}\n`);
33
- process.exit(1);
34
- }
35
-
36
- try {
37
- const response = await fetch(`${platformUrl}/api/audit/sessions/${options.sessionId}`, {
38
- headers: {'Authorization': `Bearer ${apiKey}`},
39
- });
40
-
41
- if (!response.ok) {
42
- process.stderr.write(`[audit] HTTP ${response.status}: ${await response.text()}\n`);
43
- process.exit(1);
44
- }
45
-
46
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- platform response
47
- const data = await response.json() as {
48
- sessionId: string;
49
- events: Array<{
50
- id: string;
51
- eventType: string;
52
- data: Record<string, unknown>;
53
- tokenCount: number | null;
54
- durationMs: number | null;
55
- createdAt: string;
56
- }>;
57
- };
58
-
59
- if (options.format === 'json') {
60
- process.stdout.write(JSON.stringify(data, null, 2) + '\n');
61
- return;
62
- }
63
-
64
- // Table format
65
- const events = data.events;
66
- if (events.length === 0) {
67
- process.stdout.write('No audit events found for this session.\n');
68
- return;
69
- }
70
-
71
- const typeWidth = Math.max(10, ...events.map((e) => e.eventType.length));
72
- process.stdout.write(`\nSession: ${data.sessionId}\n`);
73
- process.stdout.write(`${'Type'.padEnd(typeWidth)} ${'Time'.padEnd(24)} Details\n`);
74
- process.stdout.write('-'.repeat(typeWidth + 50) + '\n');
75
-
76
- for (const event of events) {
77
- const time = event.createdAt.slice(0, 24);
78
- const details = event.durationMs ? `${event.durationMs}ms` : '';
79
- process.stdout.write(`${event.eventType.padEnd(typeWidth)} ${time.padEnd(24)} ${details}\n`);
80
- }
81
-
82
- process.stdout.write(`\nTotal: ${events.length} events\n`);
83
- } catch (err) {
84
- const msg = err instanceof Error ? err.message : String(err);
85
- process.stderr.write(`[audit] Error: ${msg}\n`);
86
- process.exit(1);
87
- }
18
+ void options;
19
+ process.stderr.write('[audit] Hosted audit retrieval is not included in the OSS CLI.\n');
20
+ process.exit(1);
88
21
  }
89
22
 
90
23
  export const auditCommand: CommandModule = {
@@ -10,7 +10,7 @@ import {z} from 'zod';
10
10
  * Schema for a single tool entry in the build manifest.
11
11
  */
12
12
  export const BuildManifestToolSchema = z.object({
13
- /** Daytona snapshot ID for fast workspace creation */
13
+ /** Sandbox snapshot ID for fast workspace creation */
14
14
  snapshotId: z.string().min(1),
15
15
  /** Content hash of the tool's source files */
16
16
  imageHash: z.string().min(1),
@@ -7,7 +7,6 @@
7
7
  import {createHash} from 'node:crypto';
8
8
  import {readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync} from 'node:fs';
9
9
  import {join, relative} from 'node:path';
10
- import {execSync} from 'node:child_process';
11
10
  import type {LoadedTool} from '@amodalai/core';
12
11
  import type {BuildManifest} from './build-manifest-types.js';
13
12
 
@@ -76,139 +75,13 @@ function loadExistingManifest(repoPath: string): BuildManifest | null {
76
75
  }
77
76
 
78
77
  /**
79
- * Read the platform config from amodal.json.
80
- */
81
- function getPlatformConfig(repoPath: string): {apiUrl: string; apiKey: string} | null {
82
- try {
83
- const configPath = join(repoPath, 'amodal.json');
84
- const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
85
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
86
- const config = raw as Record<string, unknown>;
87
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
88
- const platform = config['platform'] as {projectId?: string; apiKey?: string} | undefined;
89
- if (!platform?.apiKey) return null;
90
-
91
- const apiUrl = (process.env['PLATFORM_API_URL'] ?? 'https://api.amodal.dev').replace(/\/$/, '');
92
- return {apiUrl, apiKey: platform.apiKey};
93
- } catch {
94
- return null;
95
- }
96
- }
97
-
98
- /**
99
- * Create a tar.gz archive of the tool directory.
100
- * Excludes node_modules, .git, __pycache__, etc.
101
- */
102
- export function createToolArchive(tool: LoadedTool): Buffer {
103
- // Use system tar — available on macOS and Linux
104
- const tarOutput = execSync(
105
- 'tar -czf - ' +
106
- '--exclude=node_modules --exclude=.git --exclude=__pycache__ ' +
107
- '--exclude=.venv --exclude=dist ' +
108
- '-C ' + JSON.stringify(tool.location) + ' .',
109
- {maxBuffer: 50 * 1024 * 1024}, // 50MB max
110
- );
111
- return Buffer.from(tarOutput);
112
- }
113
-
114
- const POLL_INTERVAL_MS = 3000;
115
- const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes max
116
-
117
- /**
118
- * Upload a tool bundle to the platform API and wait for the build to complete.
119
- *
120
- * 1. POST /api/tools/build — starts the build (Vercel creates a Daytona
121
- * sandbox, uploads files, kicks off setup commands asynchronously)
122
- * 2. Poll GET /api/tools/build/:id — waits for setup to finish, then
123
- * the platform snapshots the sandbox and returns the snapshotId
124
- *
125
- * The ISV never needs Daytona credentials. The platform owns the infra.
126
- */
127
- async function buildToolOnPlatform(
128
- platformUrl: string,
129
- apiKey: string,
130
- tool: LoadedTool,
131
- imageHash: string,
132
- ): Promise<string> {
133
- const archive = createToolArchive(tool);
134
- const fileCount = listFilesRecursive(tool.location).length;
135
-
136
- process.stderr.write(`[build-tools] ${tool.name}: uploading ${fileCount} files (${(archive.length / 1024).toFixed(1)} KB)\n`);
137
-
138
- // 1. Start the build
139
- const startResponse = await fetch(`${platformUrl}/api/tools/build`, {
140
- method: 'POST',
141
- headers: {
142
- 'Authorization': `Bearer ${apiKey}`,
143
- 'Content-Type': 'application/gzip',
144
- 'X-Tool-Name': tool.name,
145
- 'X-Tool-Hash': imageHash,
146
- 'X-Sandbox-Language': tool.sandboxLanguage,
147
- 'X-Has-Setup-Script': String(tool.hasSetupScript),
148
- 'X-Has-Requirements-Txt': String(tool.hasRequirementsTxt),
149
- 'X-Has-Dockerfile': String(tool.hasDockerfile),
150
- 'X-Has-Package-Json': String(tool.hasPackageJson),
151
- },
152
- body: archive,
153
- });
154
-
155
- if (!startResponse.ok) {
156
- const text = await startResponse.text().catch(() => '');
157
- throw new Error(`Platform tool build failed to start (${startResponse.status}): ${text}`);
158
- }
159
-
160
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
161
- const buildResult = await startResponse.json() as {id: string; status: string; snapshotId?: string; error?: string};
162
-
163
- // If already complete (no setup needed), return immediately
164
- if (buildResult.status === 'complete' && buildResult.snapshotId) {
165
- return buildResult.snapshotId;
166
- }
167
- if (buildResult.status === 'failed') {
168
- throw new Error(`Tool build failed: ${buildResult.error ?? 'unknown error'}`);
169
- }
170
-
171
- // 2. Poll until complete
172
- const buildId = buildResult.id;
173
- const startTime = Date.now();
174
-
175
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
176
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
177
-
178
- const pollResponse = await fetch(`${platformUrl}/api/tools/build/${buildId}`, {
179
- headers: {'Authorization': `Bearer ${apiKey}`},
180
- });
181
-
182
- if (!pollResponse.ok) {
183
- throw new Error(`Failed to poll build status (${pollResponse.status})`);
184
- }
185
-
186
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
187
- const poll = await pollResponse.json() as {status: string; snapshotId?: string; error?: string};
188
-
189
- if (poll.status === 'complete' && poll.snapshotId) {
190
- return poll.snapshotId;
191
- }
192
- if (poll.status === 'failed') {
193
- throw new Error(`Tool build failed: ${poll.error ?? 'unknown error'}`);
194
- }
195
-
196
- process.stderr.write(`[build-tools] ${tool.name}: ${poll.status}...\n`);
197
- }
198
-
199
- throw new Error(`Tool build timed out after ${POLL_TIMEOUT_MS / 1000}s`);
200
- }
201
-
202
- /**
203
- * Build Daytona sandbox snapshots for custom tools.
78
+ * Build local custom-tool manifest entries.
204
79
  *
205
80
  * For each tool:
206
81
  * 1. Compute a content hash of all files in the tool directory
207
82
  * 2. If hash matches existing manifest entry, skip (cached)
208
- * 3. Otherwise, upload to the platform API which builds the image
209
- * and creates the Daytona snapshot using the platform's Daytona key
210
- *
211
- * The ISV never touches Daytona directly. The platform owns the infra.
83
+ * 3. Otherwise, generate a local placeholder snapshot id. Hosted control
84
+ * planes own sandbox image builds outside the OSS CLI.
212
85
  *
213
86
  * Writes .amodal/build-manifest.json locally for caching.
214
87
  * The manifest is also included in the deploy snapshot.
@@ -220,8 +93,6 @@ export async function buildToolTemplates(
220
93
  const existing = loadExistingManifest(repoPath);
221
94
  const existingTools = existing?.tools ?? {};
222
95
 
223
- const platform = getPlatformConfig(repoPath);
224
-
225
96
  const builtTools: Record<string, {snapshotId: string; imageHash: string; sandboxLanguage: string; hasDockerfile: boolean; hasSetupScript: boolean}> = {};
226
97
  let skipped = 0;
227
98
  let built = 0;
@@ -243,16 +114,8 @@ export async function buildToolTemplates(
243
114
  process.stderr.write(`[build-tools] ${tool.name}: building (${tool.sandboxLanguage})\n`);
244
115
  }
245
116
 
246
- let snapshotId: string;
247
- if (platform) {
248
- // Upload to platform API → platform builds on Daytona
249
- snapshotId = await buildToolOnPlatform(platform.apiUrl, platform.apiKey, tool, imageHash);
250
- } else {
251
- // No platform configured — generate a placeholder
252
- // (local dev mode — tools run in-process, snapshots not needed)
253
- snapshotId = `local-${imageHash.slice(0, 16)}`;
254
- process.stderr.write(`[build-tools] ${tool.name}: no platform configured, using local placeholder\n`);
255
- }
117
+ const snapshotId = `local-${imageHash.slice(0, 16)}`;
118
+ process.stderr.write(`[build-tools] ${tool.name}: using local placeholder\n`);
256
119
 
257
120
  builtTools[tool.name] = {snapshotId, imageHash, sandboxLanguage: tool.sandboxLanguage, hasDockerfile: tool.hasDockerfile, hasSetupScript: tool.hasSetupScript};
258
121
  built++;
@@ -39,7 +39,7 @@ describe('runBuild', () => {
39
39
  expect(stderrWrites.some((s) => s.includes('amodal.json'))).toBe(true);
40
40
  });
41
41
 
42
- it('builds a snapshot and writes resolved-config.json', async () => {
42
+ it('packs a tarball and writes a manifest', async () => {
43
43
  mkdirSync(testDir, {recursive: true});
44
44
  writeFileSync(join(testDir, 'amodal.json'), JSON.stringify({
45
45
  name: 'test-agent',
@@ -48,16 +48,21 @@ describe('runBuild', () => {
48
48
  }));
49
49
 
50
50
  const {runBuild} = await import('./build.js');
51
- const outputPath = join(testDir, 'output.json');
52
- const code = await runBuild({cwd: testDir, output: outputPath});
51
+ const outputDir = join(testDir, 'build-out');
52
+ const code = await runBuild({cwd: testDir, output: outputDir});
53
53
 
54
54
  expect(code).toBe(0);
55
- expect(existsSync(outputPath)).toBe(true);
56
55
 
57
- const snapshot = JSON.parse(readFileSync(outputPath, 'utf-8'));
58
- expect(snapshot.deployId).toMatch(/^deploy-[0-9a-f]{7}$/);
59
- expect(snapshot.config.name).toBe('test-agent');
60
- expect(snapshot.source).toBe('cli');
61
- expect(stderrWrites.some((s) => s.includes('Snapshot'))).toBe(true);
56
+ const tarballPath = join(outputDir, 'agent.tar.gz');
57
+ const manifestPath = join(outputDir, 'manifest.json');
58
+ expect(existsSync(tarballPath)).toBe(true);
59
+ expect(existsSync(manifestPath)).toBe(true);
60
+
61
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
62
+ expect(typeof manifest.deployId).toBe('string');
63
+ expect(manifest.deployId.length).toBeGreaterThan(0);
64
+ expect(manifest.agentName).toBe('test-agent');
65
+ expect(manifest.source).toBe('cli');
66
+ expect(stderrWrites.some((s) => s.includes('Wrote tarball'))).toBe(true);
62
67
  });
63
68
  });
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * @license
3
- * Copyright 2025 Amodal Labs, Inc.
3
+ * Copyright 2026 Amodal Labs, Inc.
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
 
7
- import {writeFileSync} from 'node:fs';
7
+ import {writeFileSync, mkdirSync} from 'node:fs';
8
8
  import {join, resolve} from 'node:path';
9
9
  import {execSync} from 'node:child_process';
10
+ import {randomUUID} from 'node:crypto';
10
11
  import type {CommandModule} from 'yargs';
11
- import {loadRepo, buildSnapshot, serializeSnapshot, snapshotSizeBytes} from '@amodalai/core';
12
+ import {loadRepo} from '@amodalai/core';
13
+ import type {ToolBuildManifest} from '@amodalai/types';
12
14
  import {buildToolTemplates} from './build-tools.js';
13
15
  import {findRepoRoot} from '../shared/repo-discovery.js';
14
16
  import {runValidate} from './validate.js';
@@ -20,8 +22,22 @@ export interface BuildOptions {
20
22
  }
21
23
 
22
24
  /**
23
- * Get the current user from git config or fallback.
25
+ * Audit + handoff metadata `amodal build` emits alongside the bundle
26
+ * tarball. Captures who built it, from where, against which commit —
27
+ * the minimum a runtime / platform needs without re-parsing the bundle.
24
28
  */
29
+ export interface DeployManifest {
30
+ deployId: string;
31
+ createdAt: string;
32
+ createdBy: string;
33
+ source: 'cli' | 'github' | 'admin-ui';
34
+ commitSha?: string;
35
+ branch?: string;
36
+ message?: string;
37
+ agentName: string;
38
+ toolBuildManifest?: ToolBuildManifest;
39
+ }
40
+
25
41
  function getCurrentUser(): string {
26
42
  try {
27
43
  return execSync('git config user.email', {encoding: 'utf-8'}).trim();
@@ -30,9 +46,6 @@ function getCurrentUser(): string {
30
46
  }
31
47
  }
32
48
 
33
- /**
34
- * Get the current git commit SHA, or undefined if not in a git repo.
35
- */
36
49
  function getGitSha(): string | undefined {
37
50
  try {
38
51
  return execSync('git rev-parse HEAD', {encoding: 'utf-8'}).trim();
@@ -41,14 +54,23 @@ function getGitSha(): string | undefined {
41
54
  }
42
55
  }
43
56
 
57
+ function getGitBranch(): string | undefined {
58
+ try {
59
+ return execSync('git rev-parse --abbrev-ref HEAD', {encoding: 'utf-8'}).trim();
60
+ } catch {
61
+ return undefined;
62
+ }
63
+ }
64
+
44
65
  /**
45
- * Build a deploy snapshot from the local repo.
66
+ * Build a deploy artifact (tarball + manifest) from the local repo.
46
67
  *
47
68
  * 1. Find repo root
48
69
  * 2. Validate configuration
49
- * 3. Load and resolve repo
50
- * 4. Build snapshot
51
- * 5. Write to output file
70
+ * 3. Load and resolve repo (verifies it parses)
71
+ * 4. Optionally build tool sandbox snapshots
72
+ * 5. Pack the repo (including `node_modules`) into a tarball
73
+ * 6. Write a manifest describing the build
52
74
  */
53
75
  export async function runBuild(options: BuildOptions = {}): Promise<number> {
54
76
  let repoPath: string;
@@ -68,7 +90,7 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
68
90
  return 1;
69
91
  }
70
92
 
71
- // Load repo
93
+ // Load repo to verify it parses cleanly
72
94
  process.stderr.write('[build] Loading repo...\n');
73
95
  let repo;
74
96
  try {
@@ -80,51 +102,69 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
80
102
  }
81
103
 
82
104
  // Build tool sandbox snapshots if requested
83
- let buildManifest;
105
+ let toolBuildManifest: ToolBuildManifest | undefined;
84
106
  if (options.tools && repo.tools.length > 0) {
85
107
  process.stderr.write(`[build] Building ${repo.tools.length} tool sandbox(es)...\n`);
86
- buildManifest = await buildToolTemplates(repoPath, repo.tools);
108
+ toolBuildManifest = await buildToolTemplates(repoPath, repo.tools);
87
109
  } else if (options.tools && repo.tools.length === 0) {
88
110
  process.stderr.write('[build] No tools found in tools/ directory\n');
89
111
  }
90
112
 
91
- // Build snapshot (includes tool metadata + build manifest if available)
92
- const snapshot = buildSnapshot(repo, {
113
+ const outDir = options.output ? resolve(options.output) : join(repoPath, 'build');
114
+ mkdirSync(outDir, {recursive: true});
115
+
116
+ // Pack the repo into a tarball. Excludes things that don't belong in
117
+ // a deploy artifact: VCS dir, prior build output, environment files.
118
+ const tarballPath = join(outDir, 'agent.tar.gz');
119
+ process.stderr.write(`[build] Packing ${repoPath} → ${tarballPath}\n`);
120
+ try {
121
+ execSync(
122
+ `tar -czf "${tarballPath}" ` +
123
+ `--exclude='.git' --exclude='build' --exclude='.env' --exclude='.env.local' ` +
124
+ `-C "${repoPath}" .`,
125
+ {stdio: 'pipe'},
126
+ );
127
+ } catch (err) {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ process.stderr.write(`[build] tar failed: ${msg}\n`);
130
+ return 1;
131
+ }
132
+
133
+ const manifest: DeployManifest = {
134
+ deployId: randomUUID(),
135
+ createdAt: new Date().toISOString(),
93
136
  createdBy: getCurrentUser(),
94
137
  source: 'cli',
95
- commitSha: getGitSha(),
96
- buildManifest,
97
- });
98
-
99
- // Serialize and write
100
- const serialized = serializeSnapshot(snapshot);
101
- const size = snapshotSizeBytes(serialized);
102
- const outputPath = options.output
103
- ? resolve(options.output)
104
- : join(repoPath, 'resolved-config.json');
138
+ agentName: repo.config.name,
139
+ ...(getGitSha() ? {commitSha: getGitSha()} : {}),
140
+ ...(getGitBranch() ? {branch: getGitBranch()} : {}),
141
+ ...(toolBuildManifest ? {toolBuildManifest} : {}),
142
+ };
105
143
 
106
- writeFileSync(outputPath, serialized);
144
+ const manifestPath = join(outDir, 'manifest.json');
145
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
107
146
 
108
- process.stderr.write(`[build] Snapshot ${snapshot.deployId} written to ${outputPath}\n`);
109
- process.stderr.write(`[build] Size: ${(size / 1024).toFixed(1)} KB\n`);
110
- process.stderr.write(`[build] Connections: ${Object.keys(snapshot.connections).length}, Skills: ${snapshot.skills.length}, Automations: ${snapshot.automations.length}, Knowledge: ${snapshot.knowledge.length}, Tools: ${repo.tools.length}\n`);
147
+ process.stderr.write(`[build] Wrote tarball ${tarballPath}\n`);
148
+ process.stderr.write(`[build] Wrote manifest ${manifestPath}\n`);
149
+ process.stderr.write(`[build] deployId=${manifest.deployId} commit=${manifest.commitSha ?? '(no git)'} branch=${manifest.branch ?? '-'}\n`);
150
+ process.stderr.write(`[build] Bundle: connections=${repo.connections.size + repo.externalConnections.size}, skills=${repo.skills.length}, automations=${repo.automations.length}, knowledge=${repo.knowledge.length}, tools=${repo.tools.length}\n`);
111
151
 
112
152
  return 0;
113
153
  }
114
154
 
115
155
  export const buildCommand: CommandModule = {
116
156
  command: 'build',
117
- describe: 'Build a deploy snapshot from the local repo',
157
+ describe: 'Pack a deploy artifact (tarball + manifest) from the local repo',
118
158
  builder: (yargs) =>
119
159
  yargs
120
160
  .option('output', {
121
161
  type: 'string',
122
162
  alias: 'o',
123
- describe: 'Output file path (default: resolved-config.json)',
163
+ describe: 'Output directory (default: ./build)',
124
164
  })
125
165
  .option('tools', {
126
166
  type: 'boolean',
127
- describe: 'Build Daytona sandbox snapshots for custom tools',
167
+ describe: 'Include local custom-tool build metadata',
128
168
  default: false,
129
169
  }),
130
170
  handler: async (argv) => {
@@ -8,7 +8,7 @@ import type http from 'node:http';
8
8
  import {createElement} from 'react';
9
9
  import {render} from 'ink';
10
10
  import type {CommandModule} from 'yargs';
11
- import {createLocalServer, createSnapshotServer} from '@amodalai/runtime';
11
+ import {createLocalServer, createBundleServer} from '@amodalai/runtime';
12
12
  import {findRepoRoot} from '../shared/repo-discovery.js';
13
13
  import {runConnectionPreflight, printPreflightTable} from '../shared/connection-preflight.js';
14
14
  import {ChatApp} from '../ui/ChatApp.js';
@@ -28,7 +28,7 @@ export interface ChatOptions {
28
28
  *
29
29
  * Three modes:
30
30
  * --url <remote> → connect to an already-running server (no local boot)
31
- * --config <file> → boot from a snapshot file
31
+ * --config <dir> → boot from an extracted bundle directory (offline replay)
32
32
  * (default) → boot from the local repo
33
33
  */
34
34
  export async function runChat(options: ChatOptions): Promise<void> {
@@ -57,9 +57,9 @@ export async function runChat(options: ChatOptions): Promise<void> {
57
57
  let repoPath: string | undefined;
58
58
 
59
59
  if (options.config) {
60
- process.stderr.write(`[chat] Loading snapshot from ${options.config}\n`);
61
- serverInstance = await createSnapshotServer({
62
- snapshotPath: options.config,
60
+ process.stderr.write(`[chat] Loading bundle from ${options.config}\n`);
61
+ serverInstance = await createBundleServer({
62
+ bundlePath: options.config,
63
63
  port,
64
64
  host: '127.0.0.1',
65
65
  });
@@ -20,13 +20,6 @@ vi.mock('./validate.js', () => ({
20
20
  runValidate: (...args: unknown[]) => mockRunValidate(...args),
21
21
  }));
22
22
 
23
- const mockPlatformCreate = vi.fn();
24
- vi.mock('../shared/platform-client.js', () => ({
25
- PlatformClient: {
26
- create: (...args: unknown[]) => mockPlatformCreate(...args),
27
- },
28
- }));
29
-
30
23
  // Import after mock
31
24
  const {runDeploy} = await import('./deploy.js');
32
25
 
@@ -49,9 +42,6 @@ describe('deploy command', () => {
49
42
 
50
43
  mockFindRepoRoot.mockReturnValue(testDir);
51
44
  mockRunValidate.mockResolvedValue(0); // Validation passes
52
- mockPlatformCreate.mockResolvedValue({
53
- uploadSnapshot: vi.fn().mockResolvedValue({id: 'deploy-test123', environment: 'production'}),
54
- });
55
45
  });
56
46
 
57
47
  afterEach(() => {
@@ -95,15 +85,14 @@ describe('deploy command', () => {
95
85
  stderrSpy.mockRestore();
96
86
  });
97
87
 
98
- it('returns 1 when platform not configured and not dry-run', async () => {
99
- mockPlatformCreate.mockRejectedValue(new Error('Platform URL not found. Run `amodal login`.'));
88
+ it('returns 1 for cloud deploy in OSS CLI', async () => {
100
89
  const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
101
90
 
102
91
  try {
103
92
  const code = await runDeploy({cwd: testDir});
104
93
  expect(code).toBe(1);
105
94
  const messages = stderrSpy.mock.calls.map((c) => String(c[0]));
106
- expect(messages.some((m) => m.includes('Platform'))).toBe(true);
95
+ expect(messages.some((m) => m.includes('not included in the OSS CLI'))).toBe(true);
107
96
  } finally {
108
97
  stderrSpy.mockRestore();
109
98
  }