evals 2.2.8 → 2.4.0
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/README.md +68 -53
- package/cli.js +379 -246
- package/onboarding-prompt.md +357 -0
- package/package.json +35 -27
- package/start.ps1 +128 -0
- package/start.sh +186 -0
- package/spawn-interactive.js +0 -51
package/cli.js
CHANGED
|
@@ -1,246 +1,379 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import React, { useState, useEffect } from 'react';
|
|
4
|
-
import { render, Box, Text, useInput, useApp, Static } from 'ink';
|
|
5
|
-
import Gradient from 'ink-gradient';
|
|
6
|
-
import { exec } from 'child_process';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
e(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { render, Box, Text, useInput, useApp, Static } from 'ink';
|
|
5
|
+
import Gradient from 'ink-gradient';
|
|
6
|
+
import { exec, spawn } from 'child_process';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdtempSync, realpathSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { tmpdir } from 'os';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const e = React.createElement;
|
|
13
|
+
|
|
14
|
+
// The onboarding prompt is bundled with this package (onboarding-prompt.md, a
|
|
15
|
+
// copy of the docs landing-page prompt). We read it, write it to a temp file,
|
|
16
|
+
// and tell the agent to read that file — a ~27 KB prompt is too large to pass
|
|
17
|
+
// reliably as a command-line argument.
|
|
18
|
+
// TODO: sync this copy with the docs source (arize.com/docs) later.
|
|
19
|
+
const BUNDLED_PROMPT_PATH = fileURLToPath(new URL('./onboarding-prompt.md', import.meta.url));
|
|
20
|
+
|
|
21
|
+
// Write the bundled prompt to a temp file and return a short seed instruction
|
|
22
|
+
// that points the agent at it.
|
|
23
|
+
function prepareSeedPrompt() {
|
|
24
|
+
const promptText = readFileSync(BUNDLED_PROMPT_PATH, 'utf8');
|
|
25
|
+
const dir = mkdtempSync(join(tmpdir(), 'arize-onboarding-'));
|
|
26
|
+
const promptFile = join(dir, 'onboarding-prompt.md');
|
|
27
|
+
writeFileSync(promptFile, promptText, 'utf8');
|
|
28
|
+
return `Read the file ${promptFile} and follow it to set up Arize AX tracing in this project, walking me through each step and asking me questions as needed.`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Coding agents we can launch interactively, seeded with the prompt.
|
|
32
|
+
// `args(seed)` returns the argv that starts the agent's REPL pre-loaded with `seed`.
|
|
33
|
+
export const AGENTS = [
|
|
34
|
+
{ id: 'claude', label: 'Claude Code', bin: 'claude', args: (s) => [s], installUrl: 'https://docs.claude.com/en/docs/claude-code' },
|
|
35
|
+
{ id: 'codex', label: 'OpenAI Codex', bin: 'codex', args: (s) => [s], installUrl: 'https://developers.openai.com/codex/cli' },
|
|
36
|
+
{ id: 'cursor-agent', label: 'Cursor', bin: 'cursor-agent', args: (s) => [s], installUrl: 'https://docs.cursor.com/en/cli/overview' },
|
|
37
|
+
{ id: 'copilot', label: 'GitHub Copilot', bin: 'copilot', args: (s) => ['-i', s], installUrl: 'https://github.com/features/copilot/cli' },
|
|
38
|
+
{ id: 'gemini', label: 'Gemini CLI', bin: 'gemini', args: (s) => ['-i', s], installUrl: 'https://github.com/google-gemini/gemini-cli' }
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// PATH scan — detects an agent without executing it (running it could hang).
|
|
42
|
+
export function isInstalled(bin) {
|
|
43
|
+
const path = process.env.PATH || '';
|
|
44
|
+
const dirs = path.split(process.platform === 'win32' ? ';' : ':');
|
|
45
|
+
const exts = process.platform === 'win32'
|
|
46
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';')
|
|
47
|
+
: [''];
|
|
48
|
+
for (const dir of dirs) {
|
|
49
|
+
if (!dir) continue;
|
|
50
|
+
for (const ext of exts) {
|
|
51
|
+
if (existsSync(join(dir, bin + ext)) || existsSync(join(dir, bin + ext.toLowerCase()))) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// "ARIZE EVALS" - EXACTLY matched width (both 44 chars)
|
|
60
|
+
const largeLogo = `
|
|
61
|
+
█████╗ ██████╗ ██╗ ███████╗ ███████╗
|
|
62
|
+
██╔══██╗ ██╔══██╗ ██║ ╚══███╔╝ ██╔════╝
|
|
63
|
+
███████║ ██████╔╝ ██║ ███╔╝ █████╗
|
|
64
|
+
██╔══██║ ██╔══██╗ ██║ ███╔╝ ██╔══╝
|
|
65
|
+
██║ ██║ ██║ ██║ ██║ ███████╗ ███████╗
|
|
66
|
+
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝
|
|
67
|
+
███████╗ ██╗ ██╗ █████╗ ██╗ ███████╗
|
|
68
|
+
██╔════╝ ██║ ██║ ██╔══██╗ ██║ ██╔════╝
|
|
69
|
+
█████╗ ██║ ██║ ███████║ ██║ ███████╗
|
|
70
|
+
██╔══╝ ╚██╗ ██╔╝ ██╔══██║ ██║ ╚════██║
|
|
71
|
+
███████╗ ╚████╔╝ ██║ ██║ ███████╗███████║
|
|
72
|
+
╚══════╝ ╚═══╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝`;
|
|
73
|
+
|
|
74
|
+
const mediumLogo = `
|
|
75
|
+
█████╗ ██████╗ ██╗ ███████╗ ███████╗
|
|
76
|
+
██╔══██╗ ██╔══██╗ ██║ ╚══███╔╝ ██╔════╝
|
|
77
|
+
███████║ ██████╔╝ ██║ ███╔╝ █████╗
|
|
78
|
+
██╔══██║ ██╔══██╗ ██║ ███╔╝ ██╔══╝
|
|
79
|
+
██║ ██║ ██║ ██║ ██║ ███████╗ ███████╗
|
|
80
|
+
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝
|
|
81
|
+
█▀▀ █ █ ▄▀█ █ █▀
|
|
82
|
+
██▄ ▀▄▀ █▀█ █▄▄ ▄█`;
|
|
83
|
+
|
|
84
|
+
const smallLogo = `ARIZE
|
|
85
|
+
EVALS`;
|
|
86
|
+
|
|
87
|
+
// Tag every URL the app opens with utm_source=npmevals (idempotent). Applied
|
|
88
|
+
// centrally in openUrlInBrowser so all opened links (the agent install links)
|
|
89
|
+
// carry it. Does not touch URLs inside the onboarding prompt.
|
|
90
|
+
export function withUtm(url) {
|
|
91
|
+
try {
|
|
92
|
+
const u = new URL(url);
|
|
93
|
+
if (!u.searchParams.has('utm_source')) {
|
|
94
|
+
u.searchParams.set('utm_source', 'npmevals');
|
|
95
|
+
}
|
|
96
|
+
return u.toString();
|
|
97
|
+
} catch {
|
|
98
|
+
return url;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function openUrlInBrowser(rawUrl) {
|
|
103
|
+
const url = withUtm(rawUrl);
|
|
104
|
+
const command = process.platform === 'win32'
|
|
105
|
+
? `start ${url}`
|
|
106
|
+
: process.platform === 'darwin'
|
|
107
|
+
? `open "${url}"`
|
|
108
|
+
: `xdg-open "${url}"`;
|
|
109
|
+
|
|
110
|
+
exec(command, (error) => {
|
|
111
|
+
if (error) {
|
|
112
|
+
console.error(`Failed to open browser: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
process.exit(0);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Header with hot pink to dark blue gradient
|
|
119
|
+
function Header() {
|
|
120
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
121
|
+
|
|
122
|
+
let logo;
|
|
123
|
+
if (terminalWidth >= 45) {
|
|
124
|
+
logo = largeLogo;
|
|
125
|
+
} else if (terminalWidth >= 35) {
|
|
126
|
+
logo = mediumLogo;
|
|
127
|
+
} else {
|
|
128
|
+
logo = smallLogo;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return e(Box, { flexDirection: 'column', marginBottom: 1 },
|
|
132
|
+
e(Gradient, { colors: ['#FF008C', '#FF1493', '#8B5CF6', '#1E3A8A'] },
|
|
133
|
+
e(Text, null, logo)
|
|
134
|
+
),
|
|
135
|
+
e(Text, { color: 'gray', italic: true }, 'Evals and Observability for Agentic AI')
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Character-by-character shimmer component
|
|
140
|
+
function ShimmerText({ text, shimmerPos }) {
|
|
141
|
+
const baseColor = '#FF1493';
|
|
142
|
+
const shimmerColors = ['#FF5AA7', '#FF85C0', '#FFB8D9', '#FFE0EE', '#FFB8D9', '#FF85C0', '#FF5AA7'];
|
|
143
|
+
const shimmerWidth = shimmerColors.length;
|
|
144
|
+
|
|
145
|
+
const chars = text.split('').map((char, i) => {
|
|
146
|
+
const distanceFromShimmer = i - shimmerPos;
|
|
147
|
+
|
|
148
|
+
let color = baseColor;
|
|
149
|
+
if (distanceFromShimmer >= 0 && distanceFromShimmer < shimmerWidth) {
|
|
150
|
+
color = shimmerColors[distanceFromShimmer];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return e(Text, { key: i, color, bold: true }, char);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return e(Box, null, ...chars);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Menu item component - single line, hot pink selected with character shimmer
|
|
160
|
+
function MenuItem({ name, subtext, isSelected, shimmerPos }) {
|
|
161
|
+
if (isSelected) {
|
|
162
|
+
return e(Box, null,
|
|
163
|
+
e(Text, { bold: true, color: '#FF1493' }, '❯ '),
|
|
164
|
+
e(ShimmerText, { text: name, shimmerPos }),
|
|
165
|
+
e(Text, { color: 'gray' }, ' — ' + subtext)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return e(Box, null,
|
|
170
|
+
e(Text, { color: 'white' }, ' ' + name),
|
|
171
|
+
e(Text, { color: 'gray' }, ' — ' + subtext)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Build the coding-agent picker items — the entry screen for `npx evals`.
|
|
176
|
+
// Detected agents become launch items; if none are found we offer install
|
|
177
|
+
// links instead so the screen is never a dead end.
|
|
178
|
+
export function buildAgentItems() {
|
|
179
|
+
const detected = AGENTS.filter(a => isInstalled(a.bin));
|
|
180
|
+
if (detected.length > 0) {
|
|
181
|
+
return {
|
|
182
|
+
items: detected.map(a => ({
|
|
183
|
+
name: a.label,
|
|
184
|
+
subtext: 'Launch and walk me through setup',
|
|
185
|
+
launch: a
|
|
186
|
+
})),
|
|
187
|
+
none: false
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
items: AGENTS.map(a => ({
|
|
192
|
+
name: `Install ${a.label}`,
|
|
193
|
+
subtext: 'No supported agent detected — open install docs',
|
|
194
|
+
url: a.installUrl
|
|
195
|
+
})),
|
|
196
|
+
none: true
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Main App component — goes straight into the onboarding flow: pick a coding
|
|
201
|
+
// agent and launch it seeded with the prompt (no top-level menu).
|
|
202
|
+
function App({ onDone }) {
|
|
203
|
+
const [agentState] = useState(() => buildAgentItems());
|
|
204
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
205
|
+
const [shimmerPos, setShimmerPos] = useState(-7); // Start off-screen
|
|
206
|
+
const { exit } = useApp();
|
|
207
|
+
|
|
208
|
+
const items = agentState.items;
|
|
209
|
+
const selectedName = items[selectedIndex].name;
|
|
210
|
+
|
|
211
|
+
// Character-by-character shimmer animation
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
const textLength = selectedName.length;
|
|
214
|
+
const shimmerWidth = 7;
|
|
215
|
+
const totalPositions = textLength + shimmerWidth + 5; // Extra padding for smooth loop
|
|
216
|
+
|
|
217
|
+
const timer = setInterval(() => {
|
|
218
|
+
setShimmerPos(prev => {
|
|
219
|
+
const next = prev + 1;
|
|
220
|
+
return next > totalPositions ? -shimmerWidth : next;
|
|
221
|
+
});
|
|
222
|
+
}, 120); // Speed of shimmer movement
|
|
223
|
+
|
|
224
|
+
return () => clearInterval(timer);
|
|
225
|
+
}, [selectedName]);
|
|
226
|
+
|
|
227
|
+
// Reset shimmer position when selection changes
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
setShimmerPos(-7);
|
|
230
|
+
}, [selectedIndex]);
|
|
231
|
+
|
|
232
|
+
useInput((input, key) => {
|
|
233
|
+
if (input === 'q' || key.escape) {
|
|
234
|
+
exit();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (key.upArrow || input === 'k') {
|
|
239
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : items.length - 1));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (key.downArrow || input === 'j') {
|
|
243
|
+
setSelectedIndex(prev => (prev < items.length - 1 ? prev + 1 : 0));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (key.return) {
|
|
247
|
+
const selected = items[selectedIndex];
|
|
248
|
+
|
|
249
|
+
if (selected.launch) {
|
|
250
|
+
onDone({ type: 'launch', agent: selected.launch });
|
|
251
|
+
exit();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (selected.url) {
|
|
256
|
+
onDone({ type: 'url', url: selected.url });
|
|
257
|
+
exit();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const heading = agentState.none
|
|
264
|
+
? 'No coding agent found on your PATH. Install one, then re-run:'
|
|
265
|
+
: 'Choose your coding agent to instrument your app:';
|
|
266
|
+
|
|
267
|
+
return e(Box, { flexDirection: 'column', padding: 1 },
|
|
268
|
+
e(Static, { items: ['header'] }, (item) => e(Header, { key: item })),
|
|
269
|
+
e(Box, { marginBottom: 1 },
|
|
270
|
+
e(Text, { bold: true, color: 'white' }, heading)
|
|
271
|
+
),
|
|
272
|
+
e(Box, { flexDirection: 'column', paddingX: 1 },
|
|
273
|
+
...items.map((option, index) =>
|
|
274
|
+
e(MenuItem, {
|
|
275
|
+
key: index.toString(),
|
|
276
|
+
name: option.name,
|
|
277
|
+
subtext: option.subtext,
|
|
278
|
+
isSelected: index === selectedIndex,
|
|
279
|
+
shimmerPos: shimmerPos
|
|
280
|
+
})
|
|
281
|
+
)
|
|
282
|
+
),
|
|
283
|
+
e(Box, { marginTop: 1 },
|
|
284
|
+
e(Text, { dimColor: true }, 'Enter to select · ↑↓ to navigate · Esc to quit')
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Launch a coding agent on the real terminal, seeded with the onboarding prompt.
|
|
290
|
+
// Ink has fully unmounted by this point, so the child inherits a clean TTY.
|
|
291
|
+
function launchAgent(agent) {
|
|
292
|
+
const isWin = process.platform === 'win32';
|
|
293
|
+
|
|
294
|
+
let seed;
|
|
295
|
+
try {
|
|
296
|
+
seed = prepareSeedPrompt();
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.error(`Could not read the onboarding prompt: ${err.message}`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log(`\nLaunching ${agent.label}…\n`);
|
|
303
|
+
|
|
304
|
+
const child = spawn(agent.bin, agent.args(seed), {
|
|
305
|
+
stdio: 'inherit',
|
|
306
|
+
shell: isWin // .cmd/.bat shims on Windows need the shell to resolve
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
child.on('error', (err) => {
|
|
310
|
+
console.error(`Could not launch ${agent.bin}: ${err.message}`);
|
|
311
|
+
console.error(`Make sure "${agent.bin}" is on your PATH, then try again.`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
child.on('close', (code) => {
|
|
316
|
+
process.exit(code || 0);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Render the interactive app and act on the user's choice. Exported so it can
|
|
321
|
+
// be driven explicitly; only auto-runs when this file is the entry point (see
|
|
322
|
+
// the guard below), so importing it in tests doesn't launch the TUI.
|
|
323
|
+
export async function main() {
|
|
324
|
+
// Ink needs an interactive terminal.
|
|
325
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
326
|
+
console.error('Error: This command requires an interactive terminal.');
|
|
327
|
+
console.error('Please run `npx evals` from your terminal (not from a script or non-interactive environment).');
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Handle uncaught errors
|
|
332
|
+
process.on('uncaughtException', (error) => {
|
|
333
|
+
console.error('Uncaught exception:', error.message);
|
|
334
|
+
console.error(error.stack);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
339
|
+
console.error('Unhandled rejection at:', promise);
|
|
340
|
+
console.error('Reason:', reason);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Ensure stdout is not buffered (important for Windows)
|
|
345
|
+
if (process.stdout.isTTY) {
|
|
346
|
+
process.stdout.setEncoding('utf8');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
let result = null;
|
|
351
|
+
const app = render(e(App, { onDone: (r) => { result = r; } }), { patchConsole: false });
|
|
352
|
+
|
|
353
|
+
await app.waitUntilExit();
|
|
354
|
+
|
|
355
|
+
if (!result) {
|
|
356
|
+
process.exit(0); // user quit / cancelled
|
|
357
|
+
} else if (result.type === 'url') {
|
|
358
|
+
console.log(`\nOpening ${result.url} in your browser...\n`);
|
|
359
|
+
openUrlInBrowser(result.url);
|
|
360
|
+
} else if (result.type === 'launch') {
|
|
361
|
+
launchAgent(result.agent);
|
|
362
|
+
}
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('Failed to render interactive CLI:', error.message);
|
|
365
|
+
console.error(error.stack);
|
|
366
|
+
if (process.platform === 'win32') {
|
|
367
|
+
console.error('\nNote: If you\'re using cmd.exe, try running in PowerShell or Git Bash instead.');
|
|
368
|
+
}
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Only run when invoked directly (as `evals`/`node cli.js`), not when imported.
|
|
374
|
+
const invokedDirectly =
|
|
375
|
+
process.argv[1] &&
|
|
376
|
+
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
377
|
+
if (invokedDirectly) {
|
|
378
|
+
await main();
|
|
379
|
+
}
|