@amodalai/amodal 0.3.89 → 0.3.90

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amodalai/amodal",
3
- "version": "0.3.89",
3
+ "version": "0.3.90",
4
4
  "description": "Amodal CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,12 +27,12 @@
27
27
  "react": "^19.2.4",
28
28
  "yargs": "^17.7.2",
29
29
  "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"
30
+ "@amodalai/types": "0.3.90",
31
+ "@amodalai/core": "0.3.90",
32
+ "@amodalai/db": "0.3.90",
33
+ "@amodalai/runtime": "0.3.90",
34
+ "@amodalai/studio": "0.3.90",
35
+ "@amodalai/runtime-app": "0.3.90"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.11.24",
@@ -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,47 +102,65 @@ 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',
@@ -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
  });
@@ -1,20 +1,18 @@
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 {execSync} from 'node:child_process';
8
+ import {mkdtempSync, statSync} from 'node:fs';
9
+ import {tmpdir} from 'node:os';
10
+ import {join, resolve} from 'node:path';
7
11
  import type {CommandModule} from 'yargs';
8
- import {loadSnapshotFromFile, snapshotToBundle} from '@amodalai/core';
9
- import type {AgentBundle} from '@amodalai/core';
10
- import {createLocalServer, initLogLevel, interceptConsole} from '@amodalai/runtime';
11
- import {PlatformClient} from '../shared/platform-client.js';
12
+ import {createBundleServer, initLogLevel, interceptConsole} from '@amodalai/runtime';
12
13
 
13
14
  export interface ServeOptions {
14
15
  config?: string;
15
- platform?: boolean;
16
- project?: string;
17
- env?: string;
18
16
  port?: number;
19
17
  host?: string;
20
18
  verbose?: number;
@@ -24,83 +22,59 @@ export interface ServeOptions {
24
22
  const DEFAULT_PORT = 3847;
25
23
 
26
24
  /**
27
- * Load a repo from a snapshot file or from the platform.
25
+ * Resolve the `--config` argument to a directory `createBundleServer`
26
+ * can load via `loadRepoFromDisk`. Accepts either a directory or a
27
+ * `.tar.gz` tarball produced by `amodal build` (extracted to a tempdir).
28
28
  */
29
- async function loadFromSource(options: ServeOptions): Promise<AgentBundle | null> {
30
- if (options.config) {
31
- process.stderr.write(`[serve] Loading snapshot from ${options.config}...\n`);
32
- try {
33
- const snapshot = await loadSnapshotFromFile(options.config);
34
- const repo = snapshotToBundle(snapshot, options.config);
35
- process.stderr.write(`[serve] Loaded ${snapshot.deployId} (${snapshot.skills.length} skills, ${Object.keys(snapshot.connections).length} connections)\n`);
36
- return repo;
37
- } catch (err) {
38
- const msg = err instanceof Error ? err.message : String(err);
39
- process.stderr.write(`[serve] Failed to load snapshot: ${msg}\n`);
40
- return null;
41
- }
29
+ function resolveBundlePath(configArg: string): string {
30
+ const absolute = resolve(configArg);
31
+ const stat = statSync(absolute);
32
+ if (stat.isDirectory()) return absolute;
33
+
34
+ if (absolute.endsWith('.tar.gz') || absolute.endsWith('.tgz')) {
35
+ const dir = mkdtempSync(join(tmpdir(), 'amodal-serve-'));
36
+ process.stderr.write(`[serve] Extracting ${absolute} → ${dir}\n`);
37
+ execSync(`tar -xzf "${absolute}" -C "${dir}"`, {stdio: 'pipe'});
38
+ return dir;
42
39
  }
43
40
 
44
- if (options.platform) {
45
- process.stderr.write('[serve] Fetching active snapshot from platform...\n');
46
- let client: PlatformClient;
47
- try {
48
- client = await PlatformClient.create();
49
- } catch (err) {
50
- const msg = err instanceof Error ? err.message : String(err);
51
- process.stderr.write(`[serve] ${msg}\n`);
52
- return null;
53
- }
54
-
55
- const environment = options.env ?? 'production';
56
- try {
57
- const snapshot = await client.getActiveSnapshot(environment);
58
- const repo = snapshotToBundle(snapshot, `platform:${environment}`);
59
- process.stderr.write(`[serve] Loaded ${snapshot.deployId} from ${environment}\n`);
60
- return repo;
61
- } catch (err) {
62
- const msg = err instanceof Error ? err.message : String(err);
63
- process.stderr.write(`[serve] Failed to fetch snapshot: ${msg}\n`);
64
- return null;
65
- }
66
- }
67
-
68
- process.stderr.write('[serve] Specify --config <file> or --platform to load a snapshot.\n');
69
- return null;
41
+ throw new Error(`Unsupported --config target: ${absolute} (expected a directory or .tar.gz tarball)`);
70
42
  }
71
43
 
72
- /**
73
- * Load an agent runtime from a snapshot (file or platform) and start the server.
74
- *
75
- * Returns the loaded repo, or exits with error.
76
- */
77
- export async function runServe(options: ServeOptions): Promise<AgentBundle | null> {
44
+ export async function runServe(options: ServeOptions): Promise<boolean> {
78
45
  initLogLevel({verbosity: options.verbose ?? 0, quiet: options.quiet ?? false});
79
46
  interceptConsole();
80
47
 
81
- const repo = await loadFromSource(options);
82
- if (!repo) return null;
48
+ if (!options.config) {
49
+ process.stderr.write('[serve] --config <dir-or-tarball> is required.\n');
50
+ return false;
51
+ }
52
+
53
+ let bundlePath: string;
54
+ try {
55
+ bundlePath = resolveBundlePath(options.config);
56
+ } catch (err) {
57
+ const msg = err instanceof Error ? err.message : String(err);
58
+ process.stderr.write(`[serve] ${msg}\n`);
59
+ return false;
60
+ }
83
61
 
84
- // Start the runtime server
85
62
  const port = options.port ?? DEFAULT_PORT;
86
63
  const host = options.host ?? '0.0.0.0';
87
64
 
88
- process.stderr.write(`[serve] Starting server on ${host}:${port}...\n`);
65
+ process.stderr.write(`[serve] Starting bundle server from ${bundlePath} on ${host}:${port}...\n`);
89
66
 
90
67
  try {
91
- const server = await createLocalServer({
92
- repoPath: repo.origin,
68
+ const server = await createBundleServer({
69
+ bundlePath,
93
70
  port,
94
71
  host,
95
- hotReload: false,
96
72
  corsOrigin: '*',
97
73
  });
98
74
 
99
75
  await server.start();
76
+ process.stderr.write(`[serve] Serving at http://${host}:${port}\n`);
100
77
 
101
- process.stderr.write(`[serve] Agent "${repo.config.name}" serving at http://${host}:${port}\n`);
102
-
103
- // Graceful shutdown
104
78
  const shutdown = async (signal: string): Promise<void> => {
105
79
  process.stderr.write(`\n[serve] Received ${signal}, shutting down...\n`);
106
80
  await server.stop();
@@ -112,33 +86,21 @@ export async function runServe(options: ServeOptions): Promise<AgentBundle | nul
112
86
  } catch (err) {
113
87
  const msg = err instanceof Error ? err.message : String(err);
114
88
  process.stderr.write(`[serve] Failed to start server: ${msg}\n`);
115
- return null;
89
+ return false;
116
90
  }
117
91
 
118
- return repo;
92
+ return true;
119
93
  }
120
94
 
121
95
  export const serveCommand: CommandModule = {
122
96
  command: 'serve',
123
- describe: 'Load and serve an agent from a snapshot',
97
+ describe: 'Serve an agent from a built bundle (directory or .tar.gz from `amodal build`)',
124
98
  builder: (yargs) =>
125
99
  yargs
126
100
  .option('config', {
127
101
  type: 'string',
128
- describe: 'Path to resolved-config.json snapshot file',
129
- })
130
- .option('platform', {
131
- type: 'boolean',
132
- describe: 'Fetch active snapshot from platform',
133
- default: false,
134
- })
135
- .option('project', {
136
- type: 'string',
137
- describe: 'Platform project name',
138
- })
139
- .option('env', {
140
- type: 'string',
141
- describe: 'Platform environment (default: production)',
102
+ describe: 'Path to a bundle directory or .tar.gz tarball',
103
+ demandOption: true,
142
104
  })
143
105
  .option('port', {
144
106
  type: 'number',
@@ -161,15 +123,9 @@ export const serveCommand: CommandModule = {
161
123
  default: false,
162
124
  }),
163
125
  handler: async (argv) => {
164
- const repo = await runServe({
165
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
166
- config: argv['config'] as string | undefined,
167
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
168
- platform: argv['platform'] as boolean | undefined,
169
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
170
- project: argv['project'] as string | undefined,
126
+ const ok = await runServe({
171
127
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
172
- env: argv['env'] as string | undefined,
128
+ config: argv['config'] as string,
173
129
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
174
130
  port: argv['port'] as number | undefined,
175
131
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
@@ -179,9 +135,6 @@ export const serveCommand: CommandModule = {
179
135
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
180
136
  quiet: argv['quiet'] as boolean,
181
137
  });
182
- if (!repo) {
183
- process.exit(1);
184
- }
185
- // Server is running — process stays alive until SIGTERM/SIGINT
138
+ if (!ok) process.exit(1);
186
139
  },
187
140
  };
@@ -147,15 +147,20 @@ describe('E2E Commands: Local repo', () => {
147
147
 
148
148
  // --- build ---
149
149
 
150
- it('should build a snapshot from the repo', async () => {
151
- const outputPath = join(repoDir, 'test-snapshot.json');
152
- const code = await runBuild({cwd: repoDir, output: outputPath});
150
+ it('should pack a tarball + manifest from the repo', async () => {
151
+ const outputDir = join(repoDir, 'test-build');
152
+ const code = await runBuild({cwd: repoDir, output: outputDir});
153
153
  expect(code).toBe(0);
154
- expect(existsSync(outputPath)).toBe(true);
155
154
 
156
- const snapshot = JSON.parse(readFileSync(outputPath, 'utf-8')) as Record<string, unknown>;
157
- expect(snapshot['deployId']).toBeDefined();
158
- expect((snapshot['config'] as Record<string, unknown>)['name']).toBe('e2e-commands-test');
155
+ const tarballPath = join(outputDir, 'agent.tar.gz');
156
+ const manifestPath = join(outputDir, 'manifest.json');
157
+ expect(existsSync(tarballPath)).toBe(true);
158
+ expect(existsSync(manifestPath)).toBe(true);
159
+
160
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as Record<string, unknown>;
161
+ expect(manifest['deployId']).toBeDefined();
162
+ expect(manifest['agentName']).toBe('e2e-commands-test');
163
+ expect(manifest['source']).toBe('cli');
159
164
  });
160
165
 
161
166
  // --- docker init ---
@@ -272,16 +277,12 @@ describe.skipIf(!hasDb)('E2E Commands: Runtime', () => {
272
277
  await runEval({cwd: repoDir});
273
278
  });
274
279
 
275
- // --- snapshot server ---
276
-
277
- it('should boot a snapshot server and serve health', async () => {
278
- const snapshotPath = join(repoDir, 'cmd-test-snapshot.json');
279
- const buildCode = await runBuild({cwd: repoDir, output: snapshotPath});
280
- expect(buildCode).toBe(0);
280
+ // --- bundle server ---
281
281
 
282
- const {createSnapshotServer} = await import('@amodalai/runtime');
283
- const snapServer = await createSnapshotServer({
284
- snapshotPath,
282
+ it('should boot a bundle server and serve health', async () => {
283
+ const {createBundleServer} = await import('@amodalai/runtime');
284
+ const snapServer = await createBundleServer({
285
+ bundlePath: repoDir,
285
286
  port: 0,
286
287
  host: '127.0.0.1',
287
288
  });
@@ -295,7 +296,7 @@ describe.skipIf(!hasDb)('E2E Commands: Runtime', () => {
295
296
  expect(resp.ok).toBe(true);
296
297
  const data = (await resp.json()) as Record<string, unknown>;
297
298
  expect(data['status']).toBe('ok');
298
- expect(data['mode']).toBe('snapshot');
299
+ expect(data['mode']).toBe('bundle');
299
300
  expect(data['agent_name']).toBe('e2e-commands-test');
300
301
  } finally {
301
302
  await snapServer.stop();
@@ -41,42 +41,6 @@ describe('PlatformClient', () => {
41
41
  expect(client).toBeDefined();
42
42
  });
43
43
 
44
- it('uploads snapshot via POST', async () => {
45
- const mockResponse = {
46
- id: 'deploy-abc1234',
47
- environment: 'production',
48
- isActive: true,
49
- createdAt: new Date().toISOString(),
50
- createdBy: 'test',
51
- source: 'cli',
52
- snapshotSize: 1024,
53
- };
54
-
55
- globalThis.fetch = vi.fn().mockResolvedValue({
56
- ok: true,
57
- json: () => Promise.resolve(mockResponse),
58
- });
59
-
60
- const client = new PlatformClient({url: 'http://localhost:4000', apiKey: 'key'});
61
- const result = await client.uploadSnapshot({
62
- deployId: 'deploy-abc1234',
63
- createdAt: new Date().toISOString(),
64
- createdBy: 'test',
65
- source: 'cli',
66
- config: {name: 'test', version: '1.0', models: {main: {provider: 'a', model: 'b'}}},
67
- connections: {},
68
- skills: [],
69
- automations: [],
70
- knowledge: [],
71
- });
72
-
73
- expect(result.id).toBe('deploy-abc1234');
74
-
75
- const fetchCall = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
76
- expect(fetchCall[0]).toBe('http://localhost:4000/api/snapshot-deployments');
77
- expect(fetchCall[1].method).toBe('POST');
78
- });
79
-
80
44
  it('lists deployments via GET', async () => {
81
45
  globalThis.fetch = vi.fn().mockResolvedValue({
82
46
  ok: true,