@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/CHANGELOG.md +33 -0
- package/dist/src/commands/build.d.ts +23 -5
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +52 -33
- package/dist/src/commands/build.js.map +1 -1
- package/dist/src/commands/chat.d.ts +1 -1
- package/dist/src/commands/chat.js +5 -5
- package/dist/src/commands/chat.js.map +1 -1
- package/dist/src/commands/serve.d.ts +2 -11
- package/dist/src/commands/serve.d.ts.map +1 -1
- package/dist/src/commands/serve.js +44 -87
- package/dist/src/commands/serve.js.map +1 -1
- package/dist/src/shared/platform-client.d.ts +6 -19
- package/dist/src/shared/platform-client.d.ts.map +1 -1
- package/dist/src/shared/platform-client.js +4 -21
- package/dist/src/shared/platform-client.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/commands/build.test.ts +14 -9
- package/src/commands/build.ts +72 -32
- package/src/commands/chat.ts +5 -5
- package/src/commands/serve.ts +46 -93
- package/src/e2e-commands.test.ts +18 -17
- package/src/shared/platform-client.test.ts +0 -36
- package/src/shared/platform-client.ts +10 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amodalai/amodal",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
31
|
-
"@amodalai/core": "0.3.
|
|
32
|
-
"@amodalai/db": "0.3.
|
|
33
|
-
"@amodalai/runtime": "0.3.
|
|
34
|
-
"@amodalai/studio": "0.3.
|
|
35
|
-
"@amodalai/runtime-app": "0.3.
|
|
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('
|
|
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
|
|
52
|
-
const code = await runBuild({cwd: testDir, output:
|
|
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
|
|
58
|
-
|
|
59
|
-
expect(
|
|
60
|
-
expect(
|
|
61
|
-
|
|
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
|
});
|
package/src/commands/build.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
51
|
-
* 5.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
144
|
+
const manifestPath = join(outDir, 'manifest.json');
|
|
145
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
107
146
|
|
|
108
|
-
process.stderr.write(`[build]
|
|
109
|
-
process.stderr.write(`[build]
|
|
110
|
-
process.stderr.write(`[build]
|
|
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: '
|
|
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
|
|
163
|
+
describe: 'Output directory (default: ./build)',
|
|
124
164
|
})
|
|
125
165
|
.option('tools', {
|
|
126
166
|
type: 'boolean',
|
package/src/commands/chat.ts
CHANGED
|
@@ -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,
|
|
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 <
|
|
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
|
|
61
|
-
serverInstance = await
|
|
62
|
-
|
|
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
|
});
|
package/src/commands/serve.ts
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
92
|
-
|
|
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
|
|
89
|
+
return false;
|
|
116
90
|
}
|
|
117
91
|
|
|
118
|
-
return
|
|
92
|
+
return true;
|
|
119
93
|
}
|
|
120
94
|
|
|
121
95
|
export const serveCommand: CommandModule = {
|
|
122
96
|
command: 'serve',
|
|
123
|
-
describe: '
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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
|
};
|
package/src/e2e-commands.test.ts
CHANGED
|
@@ -147,15 +147,20 @@ describe('E2E Commands: Local repo', () => {
|
|
|
147
147
|
|
|
148
148
|
// --- build ---
|
|
149
149
|
|
|
150
|
-
it('should
|
|
151
|
-
const
|
|
152
|
-
const code = await runBuild({cwd: repoDir, output:
|
|
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
|
|
157
|
-
|
|
158
|
-
expect((
|
|
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
|
-
// ---
|
|
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
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
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('
|
|
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,
|