@amodalai/amodal 0.1.6 → 0.1.9
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 +45 -0
- package/dist/src/commands/chat.d.ts.map +1 -1
- package/dist/src/commands/chat.js +38 -2
- package/dist/src/commands/chat.js.map +1 -1
- package/dist/src/commands/deploy.d.ts.map +1 -1
- package/dist/src/commands/deploy.js +16 -1
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/dev.d.ts +1 -0
- package/dist/src/commands/dev.d.ts.map +1 -1
- package/dist/src/commands/dev.js +33 -33
- package/dist/src/commands/dev.js.map +1 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/login.d.ts +5 -0
- package/dist/src/commands/login.d.ts.map +1 -1
- package/dist/src/commands/login.js +70 -4
- package/dist/src/commands/login.js.map +1 -1
- package/dist/src/commands/validate.d.ts +1 -0
- package/dist/src/commands/validate.d.ts.map +1 -1
- package/dist/src/commands/validate.js +228 -4
- package/dist/src/commands/validate.js.map +1 -1
- package/dist/src/shared/connection-preflight.d.ts +52 -0
- package/dist/src/shared/connection-preflight.d.ts.map +1 -0
- package/dist/src/shared/connection-preflight.js +204 -0
- package/dist/src/shared/connection-preflight.js.map +1 -0
- package/dist/src/shared/platform-client.d.ts +14 -1
- package/dist/src/shared/platform-client.d.ts.map +1 -1
- package/dist/src/shared/platform-client.js +103 -2
- package/dist/src/shared/platform-client.js.map +1 -1
- package/dist/src/shared/tarball.d.ts +13 -0
- package/dist/src/shared/tarball.d.ts.map +1 -0
- package/dist/src/shared/tarball.js +21 -0
- package/dist/src/shared/tarball.js.map +1 -0
- package/dist/src/ui/ChatApp.d.ts.map +1 -1
- package/dist/src/ui/ChatApp.js +8 -7
- package/dist/src/ui/ChatApp.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/commands/chat.ts +38 -2
- package/src/commands/deploy.ts +17 -1
- package/src/commands/dev.ts +35 -32
- package/src/commands/index.ts +2 -1
- package/src/commands/login.ts +76 -4
- package/src/commands/validate.test.ts +169 -16
- package/src/commands/validate.ts +272 -4
- package/src/shared/connection-preflight.ts +251 -0
- package/src/shared/platform-client.ts +115 -2
- package/src/shared/tarball.ts +30 -0
- package/src/ui/ChatApp.tsx +8 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amodalai/amodal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Amodal CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"semver": "^7.6.0",
|
|
31
31
|
"yargs": "^17.7.2",
|
|
32
32
|
"zod": "^4.3.6",
|
|
33
|
-
"@amodalai/
|
|
34
|
-
"@amodalai/
|
|
33
|
+
"@amodalai/runtime": "0.1.9",
|
|
34
|
+
"@amodalai/core": "0.1.9"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@amodalai/runtime-app": "0.1.
|
|
37
|
+
"@amodalai/runtime-app": "0.1.9"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@amodalai/runtime-app": {
|
package/src/commands/chat.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {render} from 'ink';
|
|
|
10
10
|
import type {CommandModule} from 'yargs';
|
|
11
11
|
import {createLocalServer, createSnapshotServer} from '@amodalai/runtime';
|
|
12
12
|
import {findRepoRoot} from '../shared/repo-discovery.js';
|
|
13
|
+
import {runConnectionPreflight, printPreflightTable} from '../shared/connection-preflight.js';
|
|
13
14
|
import {ChatApp} from '../ui/ChatApp.js';
|
|
14
15
|
|
|
15
16
|
export interface ChatOptions {
|
|
@@ -53,6 +54,7 @@ export async function runChat(options: ChatOptions): Promise<void> {
|
|
|
53
54
|
// Mode 2 & 3: Boot a local server
|
|
54
55
|
const port = options.port ?? 0;
|
|
55
56
|
let serverInstance: {app: unknown; start: () => Promise<unknown>; stop: () => Promise<void>};
|
|
57
|
+
let repoPath: string | undefined;
|
|
56
58
|
|
|
57
59
|
if (options.config) {
|
|
58
60
|
process.stderr.write(`[chat] Loading snapshot from ${options.config}\n`);
|
|
@@ -62,7 +64,6 @@ export async function runChat(options: ChatOptions): Promise<void> {
|
|
|
62
64
|
host: '127.0.0.1',
|
|
63
65
|
});
|
|
64
66
|
} else {
|
|
65
|
-
let repoPath: string;
|
|
66
67
|
try {
|
|
67
68
|
repoPath = findRepoRoot(options.cwd);
|
|
68
69
|
} catch (err) {
|
|
@@ -86,11 +87,46 @@ export async function runChat(options: ChatOptions): Promise<void> {
|
|
|
86
87
|
const actualPort = typeof addr === 'object' && addr !== null ? addr.port : port;
|
|
87
88
|
const baseUrl = `http://127.0.0.1:${actualPort}`;
|
|
88
89
|
|
|
90
|
+
// Preflight connection check (non-blocking)
|
|
91
|
+
if (repoPath) {
|
|
92
|
+
const preflight = await runConnectionPreflight(repoPath);
|
|
93
|
+
if (preflight.results.length > 0) {
|
|
94
|
+
process.stderr.write('\n');
|
|
95
|
+
printPreflightTable(preflight.results);
|
|
96
|
+
if (preflight.hasFailures) {
|
|
97
|
+
process.stderr.write('\n WARNING: Some connections failed. The agent may not work correctly.\n');
|
|
98
|
+
}
|
|
99
|
+
process.stderr.write('\n');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Resolve "latest" to an actual session ID
|
|
104
|
+
let resumeId = options.resume;
|
|
105
|
+
if (resumeId === 'latest') {
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch(`${baseUrl}/sessions`);
|
|
108
|
+
if (res.ok) {
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
110
|
+
const body = await res.json() as {sessions: Array<{id: string}>};
|
|
111
|
+
if (body.sessions.length > 0) {
|
|
112
|
+
resumeId = body.sessions[0].id;
|
|
113
|
+
process.stderr.write(`[chat] Resuming session ${resumeId}\n`);
|
|
114
|
+
} else {
|
|
115
|
+
process.stderr.write('[chat] No previous sessions found, starting fresh\n');
|
|
116
|
+
resumeId = undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
process.stderr.write('[chat] Could not fetch sessions, starting fresh\n');
|
|
121
|
+
resumeId = undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
89
125
|
const {waitUntilExit} = render(
|
|
90
126
|
createElement(ChatApp, {
|
|
91
127
|
baseUrl,
|
|
92
128
|
tenantId,
|
|
93
|
-
resumeSessionId:
|
|
129
|
+
resumeSessionId: resumeId,
|
|
94
130
|
fullscreen: options.fullscreen,
|
|
95
131
|
}),
|
|
96
132
|
);
|
package/src/commands/deploy.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {execSync} from 'node:child_process';
|
|
8
|
+
import {createReadStream} from 'node:fs';
|
|
8
9
|
import type {CommandModule} from 'yargs';
|
|
9
10
|
import {loadRepo, buildSnapshot, serializeSnapshot, snapshotSizeBytes} from '@amodalai/core';
|
|
10
11
|
import {findRepoRoot} from '../shared/repo-discovery.js';
|
|
11
12
|
import {PlatformClient} from '../shared/platform-client.js';
|
|
12
13
|
import {runValidate} from './validate.js';
|
|
14
|
+
import {createRepoTarball} from '../shared/tarball.js';
|
|
15
|
+
import {readProjectLink} from './link.js';
|
|
13
16
|
|
|
14
17
|
export interface DeployOptions {
|
|
15
18
|
cwd?: string;
|
|
@@ -107,11 +110,24 @@ export async function runDeploy(options: DeployOptions = {}): Promise<number> {
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
try {
|
|
110
|
-
|
|
113
|
+
// Create tarball of the repo for server-side runtime-app build
|
|
114
|
+
process.stderr.write('[deploy] Packaging repo...\n');
|
|
115
|
+
const tarballPath = await createRepoTarball(repoPath);
|
|
116
|
+
|
|
117
|
+
// Read appId from project link
|
|
118
|
+
const projectLink = await readProjectLink();
|
|
119
|
+
const appId = projectLink?.appId;
|
|
120
|
+
|
|
121
|
+
const result = await client.deployWithRepo(snapshot, createReadStream(tarballPath), {environment, appId});
|
|
111
122
|
process.stderr.write(`[deploy] Deployed ${result.id} to ${result.environment}\n`);
|
|
112
123
|
if (result.message) {
|
|
113
124
|
process.stderr.write(`[deploy] Message: ${result.message}\n`);
|
|
114
125
|
}
|
|
126
|
+
|
|
127
|
+
// Cleanup tarball
|
|
128
|
+
const {unlinkSync} = await import('node:fs');
|
|
129
|
+
try { unlinkSync(tarballPath); } catch { /* best-effort */ }
|
|
130
|
+
|
|
115
131
|
return 0;
|
|
116
132
|
} catch (err) {
|
|
117
133
|
const msg = err instanceof Error ? err.message : String(err);
|
package/src/commands/dev.ts
CHANGED
|
@@ -10,20 +10,13 @@ import path from 'node:path';
|
|
|
10
10
|
import {fileURLToPath} from 'node:url';
|
|
11
11
|
import {createLocalServer} from '@amodalai/runtime';
|
|
12
12
|
import {findRepoRoot} from '../shared/repo-discovery.js';
|
|
13
|
-
|
|
14
|
-
async function loadRuntimeApp(): Promise<typeof import('@amodalai/runtime-app/dev') | null> {
|
|
15
|
-
try {
|
|
16
|
-
return await import('@amodalai/runtime-app/dev');
|
|
17
|
-
} catch {
|
|
18
|
-
// Runtime app is optional — server-only mode without the frontend
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
13
|
+
import {runConnectionPreflight, printPreflightTable} from '../shared/connection-preflight.js';
|
|
22
14
|
|
|
23
15
|
export interface DevOptions {
|
|
24
16
|
cwd?: string;
|
|
25
17
|
port?: number;
|
|
26
18
|
host?: string;
|
|
19
|
+
resume?: string;
|
|
27
20
|
}
|
|
28
21
|
|
|
29
22
|
const DEFAULT_PORT = 3847;
|
|
@@ -47,29 +40,22 @@ export async function runDev(options: DevOptions = {}): Promise<void> {
|
|
|
47
40
|
process.stderr.write(`[dev] Starting dev server for ${repoPath}\n`);
|
|
48
41
|
|
|
49
42
|
try {
|
|
50
|
-
// Try to load the runtime app for the dev UI
|
|
51
|
-
let appMiddleware: ((req: unknown, res: unknown, next: unknown) => void) | undefined;
|
|
52
43
|
let staticAppDir: string | undefined;
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (existsSync(path.join(dir, 'index.html'))) {
|
|
69
|
-
process.stderr.write('[dev] Serving pre-built runtime app\n');
|
|
70
|
-
staticAppDir = dir;
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
45
|
+
// Use pre-built static assets for the SPA for the SPA.
|
|
46
|
+
// Vite dev middleware is only used inside the monorepo with `pnpm dev`.
|
|
47
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const candidates = [
|
|
49
|
+
// esbuild bundle: bundle/app/
|
|
50
|
+
path.resolve(scriptDir, 'app'),
|
|
51
|
+
// global/local install: <pkg root>/node_modules/@amodalai/runtime-app/dist/
|
|
52
|
+
path.resolve(scriptDir, '..', '..', '..', 'node_modules', '@amodalai', 'runtime-app', 'dist'),
|
|
53
|
+
];
|
|
54
|
+
for (const dir of candidates) {
|
|
55
|
+
if (existsSync(path.join(dir, 'index.html'))) {
|
|
56
|
+
process.stderr.write('[dev] Serving pre-built runtime app\n');
|
|
57
|
+
staticAppDir = dir;
|
|
58
|
+
break;
|
|
73
59
|
}
|
|
74
60
|
}
|
|
75
61
|
|
|
@@ -79,12 +65,23 @@ export async function runDev(options: DevOptions = {}): Promise<void> {
|
|
|
79
65
|
host,
|
|
80
66
|
hotReload: true,
|
|
81
67
|
corsOrigin: '*',
|
|
82
|
-
appMiddleware,
|
|
83
68
|
staticAppDir,
|
|
69
|
+
resumeSessionId: options.resume,
|
|
84
70
|
});
|
|
85
71
|
|
|
86
72
|
await server.start();
|
|
87
73
|
|
|
74
|
+
// Preflight connection check (non-blocking)
|
|
75
|
+
const preflight = await runConnectionPreflight(repoPath);
|
|
76
|
+
if (preflight.results.length > 0) {
|
|
77
|
+
process.stderr.write('\n');
|
|
78
|
+
printPreflightTable(preflight.results);
|
|
79
|
+
if (preflight.hasFailures) {
|
|
80
|
+
process.stderr.write('\n WARNING: Some connections failed. The agent may not work correctly.\n');
|
|
81
|
+
}
|
|
82
|
+
process.stderr.write('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
88
85
|
// Graceful shutdown
|
|
89
86
|
const shutdown = async (signal: string): Promise<void> => {
|
|
90
87
|
process.stderr.write(`\n[dev] Received ${signal}, shutting down...\n`);
|
|
@@ -113,12 +110,18 @@ export const devCommand: CommandModule = {
|
|
|
113
110
|
type: 'string',
|
|
114
111
|
describe: 'Host to bind to',
|
|
115
112
|
},
|
|
113
|
+
resume: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
describe: 'Resume a previous session by ID or "latest"',
|
|
116
|
+
},
|
|
116
117
|
},
|
|
117
118
|
handler: async (argv) => {
|
|
118
119
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
119
120
|
const port = argv['port'] as number | undefined;
|
|
120
121
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
121
122
|
const host = argv['host'] as string | undefined;
|
|
122
|
-
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
124
|
+
const resume = argv['resume'] as string | undefined;
|
|
125
|
+
await runDev({port, host, resume});
|
|
123
126
|
},
|
|
124
127
|
};
|
package/src/commands/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {updateCommand} from './update.js';
|
|
|
16
16
|
import {diffCommand} from './diff.js';
|
|
17
17
|
import {searchCommand} from './search.js';
|
|
18
18
|
import {publishCommand} from './publish.js';
|
|
19
|
-
import {loginCommand} from './login.js';
|
|
19
|
+
import {loginCommand, logoutCommand} from './login.js';
|
|
20
20
|
import {linkCommand} from './link.js';
|
|
21
21
|
import {syncCommand} from './sync.js';
|
|
22
22
|
import {secretsCommand} from './secrets.js';
|
|
@@ -54,6 +54,7 @@ export const amodalCommands = [
|
|
|
54
54
|
searchCommand,
|
|
55
55
|
publishCommand,
|
|
56
56
|
loginCommand,
|
|
57
|
+
logoutCommand,
|
|
57
58
|
linkCommand,
|
|
58
59
|
syncCommand,
|
|
59
60
|
// Platform
|
package/src/commands/login.ts
CHANGED
|
@@ -143,12 +143,34 @@ export async function runLogin(options: LoginOptions = {}): Promise<number> {
|
|
|
143
143
|
const platformUrl = options.platformUrl ?? process.env['PLATFORM_API_URL'] ?? DEFAULT_PLATFORM_URL;
|
|
144
144
|
const adminUrl = options.adminUrl ?? process.env['ADMIN_UI_URL'] ?? DEFAULT_ADMIN_UI_URL;
|
|
145
145
|
|
|
146
|
-
// Check if already logged in
|
|
146
|
+
// Check if already logged in — try refreshing the token first
|
|
147
147
|
const rc = await readRcFile();
|
|
148
148
|
if (rc.platform?.url === platformUrl && rc.platform.token) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
// Try the existing token
|
|
150
|
+
const meRes = await fetch(`${platformUrl}/api/me`, {
|
|
151
|
+
headers: {Authorization: `Bearer ${rc.platform.token}`},
|
|
152
|
+
}).catch(() => null);
|
|
153
|
+
|
|
154
|
+
if (meRes?.ok) {
|
|
155
|
+
process.stderr.write(`[login] Already logged in to ${platformUrl}\n`);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Token expired — try refresh
|
|
160
|
+
if (rc.platform.refreshToken) {
|
|
161
|
+
process.stderr.write('[login] Token expired, refreshing...\n');
|
|
162
|
+
const refreshed = await refreshAccessToken(platformUrl, rc.platform.refreshToken);
|
|
163
|
+
if (refreshed) {
|
|
164
|
+
rc.platform.token = refreshed.token;
|
|
165
|
+
rc.platform.refreshToken = refreshed.refreshToken;
|
|
166
|
+
await writeRcFile(rc);
|
|
167
|
+
process.stderr.write('[login] Token refreshed successfully.\n');
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Refresh failed — proceed to re-login
|
|
173
|
+
process.stderr.write('[login] Session expired. Re-authenticating...\n');
|
|
152
174
|
}
|
|
153
175
|
|
|
154
176
|
// Start local callback server
|
|
@@ -234,6 +256,56 @@ export async function runLogin(options: LoginOptions = {}): Promise<number> {
|
|
|
234
256
|
}
|
|
235
257
|
}
|
|
236
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Refresh an expired Supabase access token using the refresh token.
|
|
261
|
+
*/
|
|
262
|
+
async function refreshAccessToken(
|
|
263
|
+
platformUrl: string,
|
|
264
|
+
refreshToken: string,
|
|
265
|
+
): Promise<{token: string; refreshToken: string} | null> {
|
|
266
|
+
try {
|
|
267
|
+
const res = await fetch(`${platformUrl}/api/auth/refresh`, {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: {'Content-Type': 'application/json'},
|
|
270
|
+
body: JSON.stringify({refresh_token: refreshToken}),
|
|
271
|
+
});
|
|
272
|
+
if (!res.ok) return null;
|
|
273
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
274
|
+
const data = (await res.json()) as {access_token?: string; refresh_token?: string};
|
|
275
|
+
if (!data.access_token) return null;
|
|
276
|
+
return {
|
|
277
|
+
token: data.access_token,
|
|
278
|
+
refreshToken: data.refresh_token ?? refreshToken,
|
|
279
|
+
};
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Log out by removing saved credentials.
|
|
287
|
+
*/
|
|
288
|
+
export async function runLogout(): Promise<number> {
|
|
289
|
+
const rc = await readRcFile();
|
|
290
|
+
if (!rc.platform) {
|
|
291
|
+
process.stderr.write('[logout] Not logged in.\n');
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
delete rc.platform;
|
|
295
|
+
await writeRcFile(rc);
|
|
296
|
+
process.stderr.write('[logout] Logged out.\n');
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const logoutCommand: CommandModule = {
|
|
301
|
+
command: 'logout',
|
|
302
|
+
describe: 'Log out from the amodal platform',
|
|
303
|
+
handler: async () => {
|
|
304
|
+
const code = await runLogout();
|
|
305
|
+
process.exit(code);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
|
|
237
309
|
export const loginCommand: CommandModule = {
|
|
238
310
|
command: 'login',
|
|
239
311
|
describe: 'Log in to the amodal platform',
|
|
@@ -15,10 +15,17 @@ vi.mock('../shared/repo-discovery.js', () => ({
|
|
|
15
15
|
findRepoRoot: mockFindRepoRoot,
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
+
const mockMcpManager = {
|
|
19
|
+
startServers: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
getServerInfo: vi.fn().mockReturnValue([]),
|
|
21
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
};
|
|
23
|
+
|
|
18
24
|
vi.mock('@amodalai/core', () => ({
|
|
19
25
|
loadRepo: mockLoadRepo,
|
|
20
26
|
readLockFile: mockReadLockFile,
|
|
21
27
|
resolveAllPackages: mockResolveAllPackages,
|
|
28
|
+
McpManager: vi.fn(() => mockMcpManager),
|
|
22
29
|
}));
|
|
23
30
|
|
|
24
31
|
describe('runValidate', () => {
|
|
@@ -38,7 +45,7 @@ describe('runValidate', () => {
|
|
|
38
45
|
});
|
|
39
46
|
|
|
40
47
|
const {runValidate} = await import('./validate.js');
|
|
41
|
-
const result = await runValidate();
|
|
48
|
+
const result = await runValidate({skipTest: true});
|
|
42
49
|
expect(result).toBe(0);
|
|
43
50
|
});
|
|
44
51
|
|
|
@@ -50,7 +57,7 @@ describe('runValidate', () => {
|
|
|
50
57
|
});
|
|
51
58
|
|
|
52
59
|
const {runValidate} = await import('./validate.js');
|
|
53
|
-
const result = await runValidate();
|
|
60
|
+
const result = await runValidate({skipTest: true});
|
|
54
61
|
expect(result).toBe(0); // warnings don't cause failure
|
|
55
62
|
});
|
|
56
63
|
|
|
@@ -62,7 +69,7 @@ describe('runValidate', () => {
|
|
|
62
69
|
});
|
|
63
70
|
|
|
64
71
|
const {runValidate} = await import('./validate.js');
|
|
65
|
-
const result = await runValidate();
|
|
72
|
+
const result = await runValidate({skipTest: true});
|
|
66
73
|
expect(result).toBe(0);
|
|
67
74
|
});
|
|
68
75
|
|
|
@@ -74,7 +81,7 @@ describe('runValidate', () => {
|
|
|
74
81
|
});
|
|
75
82
|
|
|
76
83
|
const {runValidate} = await import('./validate.js');
|
|
77
|
-
const result = await runValidate();
|
|
84
|
+
const result = await runValidate({skipTest: true});
|
|
78
85
|
expect(result).toBe(1);
|
|
79
86
|
});
|
|
80
87
|
|
|
@@ -86,7 +93,7 @@ describe('runValidate', () => {
|
|
|
86
93
|
});
|
|
87
94
|
|
|
88
95
|
const {runValidate} = await import('./validate.js');
|
|
89
|
-
const result = await runValidate();
|
|
96
|
+
const result = await runValidate({skipTest: true});
|
|
90
97
|
expect(result).toBe(0);
|
|
91
98
|
});
|
|
92
99
|
|
|
@@ -94,7 +101,7 @@ describe('runValidate', () => {
|
|
|
94
101
|
mockLoadRepo.mockRejectedValue(new Error('Config parse failed'));
|
|
95
102
|
|
|
96
103
|
const {runValidate} = await import('./validate.js');
|
|
97
|
-
const result = await runValidate();
|
|
104
|
+
const result = await runValidate({skipTest: true});
|
|
98
105
|
expect(result).toBe(1);
|
|
99
106
|
});
|
|
100
107
|
|
|
@@ -104,7 +111,7 @@ describe('runValidate', () => {
|
|
|
104
111
|
});
|
|
105
112
|
|
|
106
113
|
const {runValidate} = await import('./validate.js');
|
|
107
|
-
const result = await runValidate();
|
|
114
|
+
const result = await runValidate({skipTest: true});
|
|
108
115
|
expect(result).toBe(1);
|
|
109
116
|
});
|
|
110
117
|
|
|
@@ -119,7 +126,7 @@ describe('runValidate', () => {
|
|
|
119
126
|
});
|
|
120
127
|
|
|
121
128
|
const {runValidate} = await import('./validate.js');
|
|
122
|
-
const result = await runValidate();
|
|
129
|
+
const result = await runValidate({skipTest: true});
|
|
123
130
|
expect(result).toBe(2);
|
|
124
131
|
});
|
|
125
132
|
|
|
@@ -131,7 +138,7 @@ describe('runValidate', () => {
|
|
|
131
138
|
});
|
|
132
139
|
|
|
133
140
|
const {runValidate} = await import('./validate.js');
|
|
134
|
-
const result = await runValidate();
|
|
141
|
+
const result = await runValidate({skipTest: true});
|
|
135
142
|
expect(result).toBe(1); // 1 error
|
|
136
143
|
});
|
|
137
144
|
|
|
@@ -152,7 +159,7 @@ describe('runValidate', () => {
|
|
|
152
159
|
});
|
|
153
160
|
|
|
154
161
|
const {runValidate} = await import('./validate.js');
|
|
155
|
-
const result = await runValidate({packages: true});
|
|
162
|
+
const result = await runValidate({packages: true, skipTest: true});
|
|
156
163
|
expect(result).toBe(0);
|
|
157
164
|
expect(mockResolveAllPackages).toHaveBeenCalled();
|
|
158
165
|
});
|
|
@@ -173,7 +180,7 @@ describe('runValidate', () => {
|
|
|
173
180
|
});
|
|
174
181
|
|
|
175
182
|
const {runValidate} = await import('./validate.js');
|
|
176
|
-
const result = await runValidate({packages: true});
|
|
183
|
+
const result = await runValidate({packages: true, skipTest: true});
|
|
177
184
|
expect(result).toBe(0); // warnings only
|
|
178
185
|
});
|
|
179
186
|
|
|
@@ -193,7 +200,7 @@ describe('runValidate', () => {
|
|
|
193
200
|
});
|
|
194
201
|
|
|
195
202
|
const {runValidate} = await import('./validate.js');
|
|
196
|
-
const result = await runValidate({packages: true});
|
|
203
|
+
const result = await runValidate({packages: true, skipTest: true});
|
|
197
204
|
expect(result).toBe(1);
|
|
198
205
|
});
|
|
199
206
|
|
|
@@ -206,7 +213,7 @@ describe('runValidate', () => {
|
|
|
206
213
|
mockReadLockFile.mockResolvedValue(null);
|
|
207
214
|
|
|
208
215
|
const {runValidate} = await import('./validate.js');
|
|
209
|
-
const result = await runValidate({packages: true});
|
|
216
|
+
const result = await runValidate({packages: true, skipTest: true});
|
|
210
217
|
expect(result).toBe(0);
|
|
211
218
|
expect(mockResolveAllPackages).not.toHaveBeenCalled();
|
|
212
219
|
});
|
|
@@ -219,7 +226,7 @@ describe('runValidate', () => {
|
|
|
219
226
|
});
|
|
220
227
|
|
|
221
228
|
const {runValidate} = await import('./validate.js');
|
|
222
|
-
const result = await runValidate();
|
|
229
|
+
const result = await runValidate({skipTest: true});
|
|
223
230
|
expect(result).toBe(0);
|
|
224
231
|
expect(mockReadLockFile).not.toHaveBeenCalled();
|
|
225
232
|
});
|
|
@@ -234,7 +241,7 @@ describe('runValidate', () => {
|
|
|
234
241
|
mockResolveAllPackages.mockRejectedValue(new Error('Disk error'));
|
|
235
242
|
|
|
236
243
|
const {runValidate} = await import('./validate.js');
|
|
237
|
-
const result = await runValidate({packages: true});
|
|
244
|
+
const result = await runValidate({packages: true, skipTest: true});
|
|
238
245
|
expect(result).toBe(1);
|
|
239
246
|
});
|
|
240
247
|
|
|
@@ -254,7 +261,153 @@ describe('runValidate', () => {
|
|
|
254
261
|
});
|
|
255
262
|
|
|
256
263
|
const {runValidate} = await import('./validate.js');
|
|
257
|
-
const result = await runValidate({packages: true});
|
|
264
|
+
const result = await runValidate({packages: true, skipTest: true});
|
|
258
265
|
expect(result).toBe(0); // warning only
|
|
259
266
|
});
|
|
267
|
+
|
|
268
|
+
// Live connection testing
|
|
269
|
+
describe('live connection tests', () => {
|
|
270
|
+
const mockFetch = vi.fn();
|
|
271
|
+
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
274
|
+
mockFetch.mockReset();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should test REST connections and report pass on 200', async () => {
|
|
278
|
+
mockLoadRepo.mockResolvedValue({
|
|
279
|
+
connections: new Map([['myapi', {
|
|
280
|
+
surface: [{method: 'GET', path: '/test'}],
|
|
281
|
+
access: {},
|
|
282
|
+
spec: {baseUrl: 'https://api.example.com', auth: {type: 'none'}},
|
|
283
|
+
}]]),
|
|
284
|
+
skills: [],
|
|
285
|
+
automations: [],
|
|
286
|
+
});
|
|
287
|
+
mockFetch.mockResolvedValue({status: 200, redirected: false});
|
|
288
|
+
|
|
289
|
+
const {runValidate} = await import('./validate.js');
|
|
290
|
+
const result = await runValidate({skipTest: false});
|
|
291
|
+
expect(result).toBe(0);
|
|
292
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
293
|
+
'https://api.example.com',
|
|
294
|
+
expect.objectContaining({method: 'GET'}),
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should report fail on 401', async () => {
|
|
299
|
+
mockLoadRepo.mockResolvedValue({
|
|
300
|
+
connections: new Map([['myapi', {
|
|
301
|
+
surface: [{method: 'GET', path: '/test'}],
|
|
302
|
+
access: {},
|
|
303
|
+
spec: {baseUrl: 'https://api.example.com', auth: {type: 'bearer', token: 'env:BAD_KEY'}},
|
|
304
|
+
}]]),
|
|
305
|
+
skills: [],
|
|
306
|
+
automations: [],
|
|
307
|
+
});
|
|
308
|
+
mockFetch.mockResolvedValue({status: 401, redirected: false});
|
|
309
|
+
|
|
310
|
+
const {runValidate} = await import('./validate.js');
|
|
311
|
+
const result = await runValidate({skipTest: false});
|
|
312
|
+
expect(result).toBe(1);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should use testPath when available', async () => {
|
|
316
|
+
mockLoadRepo.mockResolvedValue({
|
|
317
|
+
connections: new Map([['myapi', {
|
|
318
|
+
surface: [{method: 'GET', path: '/me'}],
|
|
319
|
+
access: {},
|
|
320
|
+
spec: {baseUrl: 'https://api.example.com/v2', testPath: '/me', auth: {type: 'none'}},
|
|
321
|
+
}]]),
|
|
322
|
+
skills: [],
|
|
323
|
+
automations: [],
|
|
324
|
+
});
|
|
325
|
+
mockFetch.mockResolvedValue({status: 200, redirected: false});
|
|
326
|
+
|
|
327
|
+
const {runValidate} = await import('./validate.js');
|
|
328
|
+
const result = await runValidate({skipTest: false});
|
|
329
|
+
expect(result).toBe(0);
|
|
330
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
331
|
+
'https://api.example.com/v2/me',
|
|
332
|
+
expect.objectContaining({method: 'GET'}),
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should report network errors as fail', async () => {
|
|
337
|
+
mockLoadRepo.mockResolvedValue({
|
|
338
|
+
connections: new Map([['myapi', {
|
|
339
|
+
surface: [{method: 'GET', path: '/test'}],
|
|
340
|
+
access: {},
|
|
341
|
+
spec: {baseUrl: 'https://unreachable.example.com', auth: {type: 'none'}},
|
|
342
|
+
}]]),
|
|
343
|
+
skills: [],
|
|
344
|
+
automations: [],
|
|
345
|
+
});
|
|
346
|
+
mockFetch.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
347
|
+
|
|
348
|
+
const {runValidate} = await import('./validate.js');
|
|
349
|
+
const result = await runValidate({skipTest: false});
|
|
350
|
+
expect(result).toBe(1);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should test MCP servers and report pass on connected', async () => {
|
|
354
|
+
mockMcpManager.getServerInfo.mockReturnValue([
|
|
355
|
+
{name: 'xpoz', status: 'connected', tools: ['search', 'get'], error: undefined},
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
mockLoadRepo.mockResolvedValue({
|
|
359
|
+
connections: new Map(),
|
|
360
|
+
skills: [],
|
|
361
|
+
automations: [],
|
|
362
|
+
mcpServers: {xpoz: {transport: 'http', url: 'https://mcp.xpoz.ai/mcp'}},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const {runValidate} = await import('./validate.js');
|
|
366
|
+
const result = await runValidate({skipTest: false});
|
|
367
|
+
expect(result).toBe(0);
|
|
368
|
+
expect(mockMcpManager.startServers).toHaveBeenCalled();
|
|
369
|
+
expect(mockMcpManager.shutdown).toHaveBeenCalled();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should report MCP server failure', async () => {
|
|
373
|
+
mockMcpManager.getServerInfo.mockReturnValue([
|
|
374
|
+
{name: 'broken', status: 'error', tools: [], error: 'Connection refused'},
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
mockLoadRepo.mockResolvedValue({
|
|
378
|
+
connections: new Map(),
|
|
379
|
+
skills: [],
|
|
380
|
+
automations: [],
|
|
381
|
+
mcpServers: {broken: {transport: 'http', url: 'https://broken.example.com/mcp'}},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const {runValidate} = await import('./validate.js');
|
|
385
|
+
const result = await runValidate({skipTest: false});
|
|
386
|
+
expect(result).toBe(1);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should handle redirect + retry for auth stripping', async () => {
|
|
390
|
+
mockLoadRepo.mockResolvedValue({
|
|
391
|
+
connections: new Map([['myapi', {
|
|
392
|
+
surface: [{method: 'GET', path: '/test'}],
|
|
393
|
+
access: {},
|
|
394
|
+
spec: {baseUrl: 'https://api.example.com/v2', auth: {type: 'bearer', token: 'env:MY_TOKEN'}},
|
|
395
|
+
}]]),
|
|
396
|
+
skills: [],
|
|
397
|
+
automations: [],
|
|
398
|
+
});
|
|
399
|
+
// First call: redirected, lost auth → 401
|
|
400
|
+
// Second call: retry with auth → 200
|
|
401
|
+
mockFetch
|
|
402
|
+
.mockResolvedValueOnce({status: 401, redirected: true, url: 'https://api.example.com/v2/'})
|
|
403
|
+
.mockResolvedValueOnce({status: 200, redirected: false});
|
|
404
|
+
|
|
405
|
+
process.env['MY_TOKEN'] = 'test-token';
|
|
406
|
+
const {runValidate} = await import('./validate.js');
|
|
407
|
+
const result = await runValidate({skipTest: false});
|
|
408
|
+
expect(result).toBe(0);
|
|
409
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
410
|
+
delete process.env['MY_TOKEN'];
|
|
411
|
+
});
|
|
412
|
+
});
|
|
260
413
|
});
|