@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.
- package/CHANGELOG.md +46 -0
- package/dist/src/commands/audit.d.ts +1 -3
- package/dist/src/commands/audit.d.ts.map +1 -1
- package/dist/src/commands/audit.js +4 -53
- package/dist/src/commands/audit.js.map +1 -1
- package/dist/src/commands/build-manifest-types.js +1 -1
- package/dist/src/commands/build-tools.d.ts +3 -10
- package/dist/src/commands/build-tools.d.ts.map +1 -1
- package/dist/src/commands/build-tools.js +5 -118
- package/dist/src/commands/build-tools.js.map +1 -1
- 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 +53 -34
- 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/deploy.d.ts +1 -1
- package/dist/src/commands/deploy.d.ts.map +1 -1
- package/dist/src/commands/deploy.js +3 -61
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/deployments.d.ts.map +1 -1
- package/dist/src/commands/deployments.js +3 -36
- package/dist/src/commands/deployments.js.map +1 -1
- package/dist/src/commands/dev.d.ts.map +1 -1
- package/dist/src/commands/dev.js +7 -10
- package/dist/src/commands/dev.js.map +1 -1
- package/dist/src/commands/experiment.d.ts +1 -3
- package/dist/src/commands/experiment.d.ts.map +1 -1
- package/dist/src/commands/experiment.js +4 -102
- package/dist/src/commands/experiment.js.map +1 -1
- package/dist/src/commands/promote.d.ts.map +1 -1
- package/dist/src/commands/promote.js +3 -21
- package/dist/src/commands/promote.js.map +1 -1
- package/dist/src/commands/rollback.d.ts.map +1 -1
- package/dist/src/commands/rollback.js +3 -24
- package/dist/src/commands/rollback.js.map +1 -1
- package/dist/src/commands/secrets.d.ts.map +1 -1
- package/dist/src/commands/secrets.js +2 -102
- package/dist/src/commands/secrets.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/commands/status.d.ts.map +1 -1
- package/dist/src/commands/status.js +3 -49
- package/dist/src/commands/status.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -8
- package/src/commands/audit.ts +4 -71
- package/src/commands/build-manifest-types.ts +1 -1
- package/src/commands/build-tools.ts +5 -142
- package/src/commands/build.test.ts +14 -9
- package/src/commands/build.ts +73 -33
- package/src/commands/chat.ts +5 -5
- package/src/commands/deploy.test.ts +2 -13
- package/src/commands/deploy.ts +5 -67
- package/src/commands/deployments.ts +3 -39
- package/src/commands/dev.ts +7 -10
- package/src/commands/experiment.ts +4 -110
- package/src/commands/promote.ts +3 -21
- package/src/commands/rollback.ts +3 -24
- package/src/commands/secrets.test.ts +12 -134
- package/src/commands/secrets.ts +2 -116
- package/src/commands/serve.ts +46 -93
- package/src/commands/status.ts +3 -51
- package/src/e2e-commands.test.ts +18 -17
- package/dist/src/shared/platform-client.d.ts +0 -123
- package/dist/src/shared/platform-client.d.ts.map +0 -1
- package/dist/src/shared/platform-client.js +0 -280
- package/dist/src/shared/platform-client.js.map +0 -1
- package/src/commands/audit.test.ts +0 -92
- package/src/commands/experiment.test.ts +0 -125
- package/src/shared/platform-client.test.ts +0 -106
- 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.
|
|
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.
|
|
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.
|
|
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",
|
package/src/commands/audit.ts
CHANGED
|
@@ -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
|
-
*
|
|
15
|
+
* Hosted audit trails are not available from the OSS CLI.
|
|
19
16
|
*/
|
|
20
17
|
export async function runAudit(options: AuditOptions): Promise<void> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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,
|
|
209
|
-
*
|
|
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
|
-
|
|
247
|
-
|
|
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('
|
|
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,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
|
|
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',
|
|
127
|
-
describe: '
|
|
167
|
+
describe: 'Include local custom-tool build metadata',
|
|
128
168
|
default: false,
|
|
129
169
|
}),
|
|
130
170
|
handler: async (argv) => {
|
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
|
});
|
|
@@ -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
|
|
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('
|
|
95
|
+
expect(messages.some((m) => m.includes('not included in the OSS CLI'))).toBe(true);
|
|
107
96
|
} finally {
|
|
108
97
|
stderrSpy.mockRestore();
|
|
109
98
|
}
|