primitive-admin 1.0.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 +495 -0
- package/dist/bin/primitive.js +72 -0
- package/dist/bin/primitive.js.map +1 -0
- package/dist/src/commands/admins.js +268 -0
- package/dist/src/commands/admins.js.map +1 -0
- package/dist/src/commands/analytics.js +195 -0
- package/dist/src/commands/analytics.js.map +1 -0
- package/dist/src/commands/apps.js +238 -0
- package/dist/src/commands/apps.js.map +1 -0
- package/dist/src/commands/auth.js +178 -0
- package/dist/src/commands/auth.js.map +1 -0
- package/dist/src/commands/catalog.js +460 -0
- package/dist/src/commands/catalog.js.map +1 -0
- package/dist/src/commands/integrations.js +438 -0
- package/dist/src/commands/integrations.js.map +1 -0
- package/dist/src/commands/prompts.js +999 -0
- package/dist/src/commands/prompts.js.map +1 -0
- package/dist/src/commands/sync.js +598 -0
- package/dist/src/commands/sync.js.map +1 -0
- package/dist/src/commands/users.js +293 -0
- package/dist/src/commands/users.js.map +1 -0
- package/dist/src/commands/waitlist.js +176 -0
- package/dist/src/commands/waitlist.js.map +1 -0
- package/dist/src/commands/workflows.js +876 -0
- package/dist/src/commands/workflows.js.map +1 -0
- package/dist/src/lib/api-client.js +522 -0
- package/dist/src/lib/api-client.js.map +1 -0
- package/dist/src/lib/auth-flow.js +306 -0
- package/dist/src/lib/auth-flow.js.map +1 -0
- package/dist/src/lib/config.js +90 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/fetch.js +43 -0
- package/dist/src/lib/fetch.js.map +1 -0
- package/dist/src/lib/output.js +143 -0
- package/dist/src/lib/output.js.map +1 -0
- package/dist/src/types/index.js +2 -0
- package/dist/src/types/index.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import * as TOML from "@iarna/toml";
|
|
3
|
+
import { ApiClient } from "../lib/api-client.js";
|
|
4
|
+
import { getCurrentAppId } from "../lib/config.js";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { success, error, info, keyValue, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, } from "../lib/output.js";
|
|
7
|
+
function resolveAppId(appId, options) {
|
|
8
|
+
const resolved = appId || options.app || getCurrentAppId();
|
|
9
|
+
if (!resolved) {
|
|
10
|
+
error("No app specified. Use <app-id>, --app, or 'primitive use <app-id>' to set context.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return resolved;
|
|
14
|
+
}
|
|
15
|
+
export function registerPromptsCommands(program) {
|
|
16
|
+
const prompts = program
|
|
17
|
+
.command("prompts")
|
|
18
|
+
.description("Configure LLM prompts, manage variations, and test execution")
|
|
19
|
+
.addHelpText("after", `
|
|
20
|
+
Examples:
|
|
21
|
+
$ primitive prompts list
|
|
22
|
+
$ primitive prompts create --from-file summarizer.toml
|
|
23
|
+
$ primitive prompts execute 01HXY... --vars '{"text":"Hello world"}'
|
|
24
|
+
$ primitive prompts configs activate 01HXY... 01ABC...
|
|
25
|
+
`);
|
|
26
|
+
// List prompts
|
|
27
|
+
prompts
|
|
28
|
+
.command("list")
|
|
29
|
+
.description("List prompts")
|
|
30
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
31
|
+
.option("--app <app-id>", "App ID")
|
|
32
|
+
.option("--status <status>", "Filter by status: draft, active, archived")
|
|
33
|
+
.option("--json", "Output as JSON")
|
|
34
|
+
.action(async (appId, options) => {
|
|
35
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
36
|
+
const client = new ApiClient();
|
|
37
|
+
try {
|
|
38
|
+
const { items } = await client.listPrompts(resolvedAppId, {
|
|
39
|
+
status: options.status,
|
|
40
|
+
});
|
|
41
|
+
if (options.json) {
|
|
42
|
+
json(items);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!items || items.length === 0) {
|
|
46
|
+
info("No prompts found.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log(formatTable(items, [
|
|
50
|
+
{ header: "ID", key: "promptId", format: formatId },
|
|
51
|
+
{ header: "KEY", key: "promptKey" },
|
|
52
|
+
{ header: "NAME", key: "displayName" },
|
|
53
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
54
|
+
{ header: "MODIFIED", key: "modifiedAt", format: formatDate },
|
|
55
|
+
]));
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
error(err.message);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// Create prompt
|
|
63
|
+
prompts
|
|
64
|
+
.command("create")
|
|
65
|
+
.description("Create a new prompt")
|
|
66
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
67
|
+
.option("--app <app-id>", "App ID")
|
|
68
|
+
.option("--key <key>", "Prompt key (unique identifier)")
|
|
69
|
+
.option("--name <name>", "Display name")
|
|
70
|
+
.option("--description <desc>", "Description")
|
|
71
|
+
.option("--provider <provider>", "LLM provider: openrouter, gemini", "openrouter")
|
|
72
|
+
.option("--model <model>", "Model name")
|
|
73
|
+
.option("--system-prompt <prompt>", "System prompt")
|
|
74
|
+
.option("--user-template <template>", "User prompt template")
|
|
75
|
+
.option("--temperature <temp>", "Temperature (0-1)")
|
|
76
|
+
.option("--max-tokens <n>", "Max output tokens")
|
|
77
|
+
.option("--input-schema <json>", "Input schema as JSON")
|
|
78
|
+
.option("--from-file <path>", "Load prompt from TOML file")
|
|
79
|
+
.option("--json", "Output as JSON")
|
|
80
|
+
.action(async (appId, options) => {
|
|
81
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
82
|
+
const client = new ApiClient();
|
|
83
|
+
let payload;
|
|
84
|
+
if (options.fromFile) {
|
|
85
|
+
try {
|
|
86
|
+
const content = readFileSync(options.fromFile, "utf-8");
|
|
87
|
+
const tomlData = TOML.parse(content);
|
|
88
|
+
const prompt = tomlData.prompt || tomlData;
|
|
89
|
+
const config = tomlData.configs?.[0] || {};
|
|
90
|
+
payload = {
|
|
91
|
+
promptKey: prompt.key || prompt.promptKey,
|
|
92
|
+
displayName: prompt.displayName || prompt.name,
|
|
93
|
+
description: prompt.description,
|
|
94
|
+
provider: config.provider || prompt.provider || "openrouter",
|
|
95
|
+
model: config.model || prompt.model,
|
|
96
|
+
systemPrompt: config.systemPrompt || prompt.systemPrompt,
|
|
97
|
+
userPromptTemplate: config.userPromptTemplate || prompt.userPromptTemplate,
|
|
98
|
+
temperature: config.temperature || prompt.temperature,
|
|
99
|
+
maxTokens: config.maxTokens || prompt.maxTokens,
|
|
100
|
+
inputSchema: prompt.inputSchema,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
error(`Failed to read TOML file: ${err.message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (!options.key || !options.name || !options.model || !options.userTemplate) {
|
|
110
|
+
error("Required: --key, --name, --model, --user-template (or use --from-file)");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
payload = {
|
|
114
|
+
promptKey: options.key,
|
|
115
|
+
displayName: options.name,
|
|
116
|
+
description: options.description,
|
|
117
|
+
provider: options.provider,
|
|
118
|
+
model: options.model,
|
|
119
|
+
systemPrompt: options.systemPrompt,
|
|
120
|
+
userPromptTemplate: options.userTemplate,
|
|
121
|
+
temperature: options.temperature,
|
|
122
|
+
maxTokens: options.maxTokens ? parseInt(options.maxTokens) : undefined,
|
|
123
|
+
inputSchema: options.inputSchema,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const result = await client.createPrompt(resolvedAppId, payload);
|
|
128
|
+
if (options.json) {
|
|
129
|
+
json(result);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
success(`Prompt created: ${result.displayName}`);
|
|
133
|
+
keyValue("Prompt ID", result.promptId);
|
|
134
|
+
keyValue("Key", result.promptKey);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
error(err.message);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// Get prompt
|
|
142
|
+
prompts
|
|
143
|
+
.command("get")
|
|
144
|
+
.description("Get prompt details")
|
|
145
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
146
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
147
|
+
.option("--json", "Output as JSON")
|
|
148
|
+
.action(async (promptId, options) => {
|
|
149
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
150
|
+
const client = new ApiClient();
|
|
151
|
+
try {
|
|
152
|
+
const result = await client.getPrompt(resolvedAppId, promptId);
|
|
153
|
+
if (options.json) {
|
|
154
|
+
json(result);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
keyValue("Prompt ID", result.promptId);
|
|
158
|
+
keyValue("Key", result.promptKey);
|
|
159
|
+
keyValue("Name", result.displayName);
|
|
160
|
+
keyValue("Description", result.description);
|
|
161
|
+
keyValue("Status", formatStatus(result.status));
|
|
162
|
+
keyValue("Active Config", result.activeConfigId || "-");
|
|
163
|
+
if (result.configs && result.configs.length > 0) {
|
|
164
|
+
divider();
|
|
165
|
+
info(`Configs (${result.configs.length}):`);
|
|
166
|
+
console.log(formatTable(result.configs, [
|
|
167
|
+
{ header: "ID", key: "configId", format: formatId },
|
|
168
|
+
{ header: "NAME", key: "configName" },
|
|
169
|
+
{ header: "MODEL", key: "model" },
|
|
170
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
171
|
+
]));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
error(err.message);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// Update prompt
|
|
180
|
+
prompts
|
|
181
|
+
.command("update")
|
|
182
|
+
.description("Update a prompt")
|
|
183
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
184
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
185
|
+
.option("--name <name>", "Display name")
|
|
186
|
+
.option("--description <desc>", "Description")
|
|
187
|
+
.option("--status <status>", "Status: draft, active, archived")
|
|
188
|
+
.option("--input-schema <json>", "Input schema as JSON")
|
|
189
|
+
.option("--json", "Output as JSON")
|
|
190
|
+
.action(async (promptId, options) => {
|
|
191
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
192
|
+
const payload = {};
|
|
193
|
+
if (options.name)
|
|
194
|
+
payload.displayName = options.name;
|
|
195
|
+
if (options.description)
|
|
196
|
+
payload.description = options.description;
|
|
197
|
+
if (options.status)
|
|
198
|
+
payload.status = options.status;
|
|
199
|
+
if (options.inputSchema)
|
|
200
|
+
payload.inputSchema = options.inputSchema;
|
|
201
|
+
if (Object.keys(payload).length === 0) {
|
|
202
|
+
error("No update options specified.");
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
const client = new ApiClient();
|
|
206
|
+
try {
|
|
207
|
+
const result = await client.updatePrompt(resolvedAppId, promptId, payload);
|
|
208
|
+
if (options.json) {
|
|
209
|
+
json(result);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
success("Prompt updated.");
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
error(err.message);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
// Delete prompt
|
|
220
|
+
prompts
|
|
221
|
+
.command("delete")
|
|
222
|
+
.description("Delete or archive a prompt")
|
|
223
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
224
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
225
|
+
.option("--hard", "Permanently delete instead of archive")
|
|
226
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
227
|
+
.action(async (promptId, options) => {
|
|
228
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
229
|
+
if (!options.yes) {
|
|
230
|
+
const action = options.hard ? "permanently delete" : "archive";
|
|
231
|
+
const inquirer = await import("inquirer");
|
|
232
|
+
const { confirm } = await inquirer.default.prompt([
|
|
233
|
+
{
|
|
234
|
+
type: "confirm",
|
|
235
|
+
name: "confirm",
|
|
236
|
+
message: `Are you sure you want to ${action} prompt ${promptId}?`,
|
|
237
|
+
default: false,
|
|
238
|
+
},
|
|
239
|
+
]);
|
|
240
|
+
if (!confirm) {
|
|
241
|
+
info("Cancelled.");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const client = new ApiClient();
|
|
246
|
+
try {
|
|
247
|
+
await client.deletePrompt(resolvedAppId, promptId, options.hard);
|
|
248
|
+
success(`Prompt ${options.hard ? "deleted" : "archived"}.`);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
error(err.message);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// Execute prompt
|
|
256
|
+
prompts
|
|
257
|
+
.command("execute")
|
|
258
|
+
.description("Execute a prompt with variables")
|
|
259
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
260
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
261
|
+
.option("--vars <json>", "Input variables as JSON")
|
|
262
|
+
.option("--config <config-id>", "Specific config ID to use")
|
|
263
|
+
.option("--json", "Output as JSON")
|
|
264
|
+
.action(async (promptId, options) => {
|
|
265
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
266
|
+
let variables = {};
|
|
267
|
+
if (options.vars) {
|
|
268
|
+
try {
|
|
269
|
+
variables = JSON.parse(options.vars);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
error("Invalid JSON in --vars");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const client = new ApiClient();
|
|
277
|
+
try {
|
|
278
|
+
const result = await client.executePrompt(resolvedAppId, promptId, {
|
|
279
|
+
variables,
|
|
280
|
+
configId: options.config,
|
|
281
|
+
});
|
|
282
|
+
if (options.json) {
|
|
283
|
+
json(result);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (result.success) {
|
|
287
|
+
success("Execution successful");
|
|
288
|
+
keyValue("Duration", formatDuration(result.metrics?.durationMs));
|
|
289
|
+
keyValue("Tokens", `${result.metrics?.inputTokens || 0} in / ${result.metrics?.outputTokens || 0} out`);
|
|
290
|
+
console.log("\nOutput:");
|
|
291
|
+
console.log(result.output);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
error(`Execution failed: ${result.error}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
error(err.message);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
// Preview prompt
|
|
303
|
+
prompts
|
|
304
|
+
.command("preview")
|
|
305
|
+
.description("Preview rendered prompt without executing")
|
|
306
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
307
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
308
|
+
.option("--vars <json>", "Input variables as JSON")
|
|
309
|
+
.option("--config <config-id>", "Specific config ID to use")
|
|
310
|
+
.option("--json", "Output as JSON")
|
|
311
|
+
.action(async (promptId, options) => {
|
|
312
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
313
|
+
let variables = {};
|
|
314
|
+
if (options.vars) {
|
|
315
|
+
try {
|
|
316
|
+
variables = JSON.parse(options.vars);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
error("Invalid JSON in --vars");
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const client = new ApiClient();
|
|
324
|
+
try {
|
|
325
|
+
const result = await client.previewPrompt(resolvedAppId, promptId, {
|
|
326
|
+
variables,
|
|
327
|
+
configId: options.config,
|
|
328
|
+
});
|
|
329
|
+
if (options.json) {
|
|
330
|
+
json(result);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
keyValue("Provider", result.provider);
|
|
334
|
+
keyValue("Model", result.model);
|
|
335
|
+
if (result.systemPrompt) {
|
|
336
|
+
console.log("\nSystem Prompt:");
|
|
337
|
+
console.log(result.systemPrompt);
|
|
338
|
+
}
|
|
339
|
+
console.log("\nUser Prompt:");
|
|
340
|
+
console.log(result.userPrompt);
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
error(err.message);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
// Configs subcommand
|
|
348
|
+
const configs = prompts.command("configs").description("Manage prompt configs");
|
|
349
|
+
// List configs
|
|
350
|
+
configs
|
|
351
|
+
.command("list")
|
|
352
|
+
.description("List configs for a prompt")
|
|
353
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
354
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
355
|
+
.option("--json", "Output as JSON")
|
|
356
|
+
.action(async (promptId, options) => {
|
|
357
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
358
|
+
const client = new ApiClient();
|
|
359
|
+
try {
|
|
360
|
+
const { items } = await client.listPromptConfigs(resolvedAppId, promptId);
|
|
361
|
+
if (options.json) {
|
|
362
|
+
json(items);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (!items || items.length === 0) {
|
|
366
|
+
info("No configs found.");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
console.log(formatTable(items, [
|
|
370
|
+
{ header: "ID", key: "configId", format: formatId },
|
|
371
|
+
{ header: "NAME", key: "configName" },
|
|
372
|
+
{ header: "PROVIDER", key: "provider" },
|
|
373
|
+
{ header: "MODEL", key: "model" },
|
|
374
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
375
|
+
]));
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
error(err.message);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
// Create config
|
|
383
|
+
configs
|
|
384
|
+
.command("create")
|
|
385
|
+
.description("Create a new config for a prompt")
|
|
386
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
387
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
388
|
+
.option("--name <name>", "Config name")
|
|
389
|
+
.option("--description <desc>", "Description")
|
|
390
|
+
.option("--provider <provider>", "LLM provider: openrouter, gemini", "openrouter")
|
|
391
|
+
.option("--model <model>", "Model name")
|
|
392
|
+
.option("--system-prompt <prompt>", "System prompt")
|
|
393
|
+
.option("--user-template <template>", "User prompt template")
|
|
394
|
+
.option("--temperature <temp>", "Temperature (0-1)")
|
|
395
|
+
.option("--max-tokens <n>", "Max output tokens")
|
|
396
|
+
.option("--json", "Output as JSON")
|
|
397
|
+
.action(async (promptId, options) => {
|
|
398
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
399
|
+
if (!options.name || !options.model || !options.userTemplate) {
|
|
400
|
+
error("Required: --name, --model, --user-template");
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
const payload = {
|
|
404
|
+
configName: options.name,
|
|
405
|
+
description: options.description,
|
|
406
|
+
provider: options.provider,
|
|
407
|
+
model: options.model,
|
|
408
|
+
systemPrompt: options.systemPrompt,
|
|
409
|
+
userPromptTemplate: options.userTemplate,
|
|
410
|
+
temperature: options.temperature,
|
|
411
|
+
maxTokens: options.maxTokens ? parseInt(options.maxTokens) : undefined,
|
|
412
|
+
};
|
|
413
|
+
const client = new ApiClient();
|
|
414
|
+
try {
|
|
415
|
+
const result = await client.createPromptConfig(resolvedAppId, promptId, payload);
|
|
416
|
+
if (options.json) {
|
|
417
|
+
json(result);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
success(`Config created: ${result.configName}`);
|
|
421
|
+
keyValue("Config ID", result.configId);
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
error(err.message);
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
// Activate config
|
|
429
|
+
configs
|
|
430
|
+
.command("activate")
|
|
431
|
+
.description("Set a config as the active config for a prompt")
|
|
432
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
433
|
+
.argument("<config-id>", "Config ID")
|
|
434
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
435
|
+
.action(async (promptId, configId, options) => {
|
|
436
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
437
|
+
const client = new ApiClient();
|
|
438
|
+
try {
|
|
439
|
+
await client.activatePromptConfig(resolvedAppId, promptId, configId);
|
|
440
|
+
success(`Config ${configId} activated.`);
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
error(err.message);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
// Duplicate config
|
|
448
|
+
configs
|
|
449
|
+
.command("duplicate")
|
|
450
|
+
.description("Duplicate a config")
|
|
451
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
452
|
+
.argument("<config-id>", "Config ID to duplicate")
|
|
453
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
454
|
+
.option("--name <name>", "New config name")
|
|
455
|
+
.option("--json", "Output as JSON")
|
|
456
|
+
.action(async (promptId, configId, options) => {
|
|
457
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
458
|
+
const client = new ApiClient();
|
|
459
|
+
try {
|
|
460
|
+
const result = await client.duplicatePromptConfig(resolvedAppId, promptId, configId, {
|
|
461
|
+
configName: options.name,
|
|
462
|
+
});
|
|
463
|
+
if (options.json) {
|
|
464
|
+
json(result);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
success(`Config duplicated: ${result.configName}`);
|
|
468
|
+
keyValue("New Config ID", result.configId);
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
error(err.message);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
// Update config
|
|
476
|
+
configs
|
|
477
|
+
.command("update")
|
|
478
|
+
.description("Update a config")
|
|
479
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
480
|
+
.argument("<config-id>", "Config ID")
|
|
481
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
482
|
+
.option("--name <name>", "Config name")
|
|
483
|
+
.option("--description <desc>", "Description")
|
|
484
|
+
.option("--provider <provider>", "LLM provider: openrouter, gemini")
|
|
485
|
+
.option("--model <model>", "Model name")
|
|
486
|
+
.option("--system-prompt <prompt>", "System prompt")
|
|
487
|
+
.option("--user-template <template>", "User prompt template")
|
|
488
|
+
.option("--temperature <temp>", "Temperature (0-1)")
|
|
489
|
+
.option("--max-tokens <n>", "Max output tokens")
|
|
490
|
+
.option("--status <status>", "Status: active, archived")
|
|
491
|
+
.option("--json", "Output as JSON")
|
|
492
|
+
.action(async (promptId, configId, options) => {
|
|
493
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
494
|
+
const payload = {};
|
|
495
|
+
if (options.name)
|
|
496
|
+
payload.configName = options.name;
|
|
497
|
+
if (options.description)
|
|
498
|
+
payload.description = options.description;
|
|
499
|
+
if (options.provider)
|
|
500
|
+
payload.provider = options.provider;
|
|
501
|
+
if (options.model)
|
|
502
|
+
payload.model = options.model;
|
|
503
|
+
if (options.systemPrompt)
|
|
504
|
+
payload.systemPrompt = options.systemPrompt;
|
|
505
|
+
if (options.userTemplate)
|
|
506
|
+
payload.userPromptTemplate = options.userTemplate;
|
|
507
|
+
if (options.temperature)
|
|
508
|
+
payload.temperature = options.temperature;
|
|
509
|
+
if (options.maxTokens)
|
|
510
|
+
payload.maxTokens = parseInt(options.maxTokens);
|
|
511
|
+
if (options.status)
|
|
512
|
+
payload.status = options.status;
|
|
513
|
+
if (Object.keys(payload).length === 0) {
|
|
514
|
+
error("No update options specified.");
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
const client = new ApiClient();
|
|
518
|
+
try {
|
|
519
|
+
const result = await client.updatePromptConfig(resolvedAppId, promptId, configId, payload);
|
|
520
|
+
if (options.json) {
|
|
521
|
+
json(result);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
success("Config updated.");
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
error(err.message);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
// ============================================
|
|
532
|
+
// TESTS SUBCOMMAND
|
|
533
|
+
// ============================================
|
|
534
|
+
const tests = prompts
|
|
535
|
+
.command("tests")
|
|
536
|
+
.description("Manage and run prompt test cases")
|
|
537
|
+
.addHelpText("after", `
|
|
538
|
+
Examples:
|
|
539
|
+
$ primitive prompts tests list <prompt-id>
|
|
540
|
+
$ primitive prompts tests create <prompt-id> --name "Basic test" --vars '{"input":"hello"}'
|
|
541
|
+
$ primitive prompts tests run <prompt-id> <test-case-id>
|
|
542
|
+
$ primitive prompts tests run-all <prompt-id>
|
|
543
|
+
`);
|
|
544
|
+
// List test cases
|
|
545
|
+
tests
|
|
546
|
+
.command("list")
|
|
547
|
+
.description("List test cases for a prompt")
|
|
548
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
549
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
550
|
+
.option("--json", "Output as JSON")
|
|
551
|
+
.action(async (promptId, options) => {
|
|
552
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
553
|
+
const client = new ApiClient();
|
|
554
|
+
try {
|
|
555
|
+
const { items } = await client.listTestCases(resolvedAppId, "prompt", promptId);
|
|
556
|
+
if (options.json) {
|
|
557
|
+
json(items);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (!items || items.length === 0) {
|
|
561
|
+
info("No test cases found.");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
console.log(formatTable(items, [
|
|
565
|
+
{ header: "ID", key: "testCaseId", format: formatId },
|
|
566
|
+
{ header: "NAME", key: "name" },
|
|
567
|
+
{ header: "CONFIG", key: "configId", format: (v) => v ? formatId(v) : "-" },
|
|
568
|
+
{ header: "CREATED", key: "createdAt", format: formatDate },
|
|
569
|
+
]));
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
error(err.message);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
// Create test case
|
|
577
|
+
tests
|
|
578
|
+
.command("create")
|
|
579
|
+
.description("Create a test case for a prompt")
|
|
580
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
581
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
582
|
+
.option("--name <name>", "Test case name (required)")
|
|
583
|
+
.option("--vars <json>", "Input variables as JSON (required)")
|
|
584
|
+
.option("--pattern <regex>", "Expected output pattern (regex)")
|
|
585
|
+
.option("--contains <json>", "Expected strings to contain (JSON array)")
|
|
586
|
+
.option("--json-subset <json>", "Expected JSON subset to match")
|
|
587
|
+
.option("--config <config-id>", "Config ID to use for this test")
|
|
588
|
+
.option("--evaluator-prompt <prompt-id>", "Evaluator prompt ID")
|
|
589
|
+
.option("--evaluator-config <config-id>", "Evaluator config ID")
|
|
590
|
+
.option("--json", "Output as JSON")
|
|
591
|
+
.action(async (promptId, options) => {
|
|
592
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
593
|
+
if (!options.name || !options.vars) {
|
|
594
|
+
error("Required: --name and --vars");
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
let inputVariables;
|
|
598
|
+
try {
|
|
599
|
+
inputVariables = JSON.parse(options.vars);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
error("Invalid JSON in --vars");
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
let expectedOutputContains;
|
|
606
|
+
if (options.contains) {
|
|
607
|
+
try {
|
|
608
|
+
expectedOutputContains = JSON.parse(options.contains);
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
error("Invalid JSON in --contains");
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
let expectedJsonSubset;
|
|
616
|
+
if (options.jsonSubset) {
|
|
617
|
+
try {
|
|
618
|
+
expectedJsonSubset = JSON.parse(options.jsonSubset);
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
error("Invalid JSON in --json-subset");
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const client = new ApiClient();
|
|
626
|
+
try {
|
|
627
|
+
const result = await client.createTestCase(resolvedAppId, "prompt", promptId, {
|
|
628
|
+
name: options.name,
|
|
629
|
+
inputVariables,
|
|
630
|
+
expectedOutputPattern: options.pattern,
|
|
631
|
+
expectedOutputContains,
|
|
632
|
+
expectedJsonSubset,
|
|
633
|
+
configId: options.config,
|
|
634
|
+
evaluatorPromptId: options.evaluatorPrompt,
|
|
635
|
+
evaluatorConfigId: options.evaluatorConfig,
|
|
636
|
+
});
|
|
637
|
+
if (options.json) {
|
|
638
|
+
json(result);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
success(`Test case created: ${result.name}`);
|
|
642
|
+
keyValue("Test Case ID", result.testCaseId);
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
error(err.message);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
// Get test case
|
|
650
|
+
tests
|
|
651
|
+
.command("get")
|
|
652
|
+
.description("Get test case details")
|
|
653
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
654
|
+
.argument("<test-case-id>", "Test Case ID")
|
|
655
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
656
|
+
.option("--json", "Output as JSON")
|
|
657
|
+
.action(async (promptId, testCaseId, options) => {
|
|
658
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
659
|
+
const client = new ApiClient();
|
|
660
|
+
try {
|
|
661
|
+
const result = await client.getTestCase(resolvedAppId, "prompt", promptId, testCaseId);
|
|
662
|
+
if (options.json) {
|
|
663
|
+
json(result);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
keyValue("Test Case ID", result.testCaseId);
|
|
667
|
+
keyValue("Name", result.name);
|
|
668
|
+
keyValue("Config ID", result.configId || "-");
|
|
669
|
+
keyValue("Created", formatDate(result.createdAt));
|
|
670
|
+
divider();
|
|
671
|
+
info("Input Variables:");
|
|
672
|
+
try {
|
|
673
|
+
const vars = JSON.parse(result.inputVariables || "{}");
|
|
674
|
+
console.log(JSON.stringify(vars, null, 2));
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
console.log(result.inputVariables || "{}");
|
|
678
|
+
}
|
|
679
|
+
if (result.expectedOutputPattern) {
|
|
680
|
+
divider();
|
|
681
|
+
keyValue("Expected Pattern", result.expectedOutputPattern);
|
|
682
|
+
}
|
|
683
|
+
if (result.expectedOutputContains) {
|
|
684
|
+
divider();
|
|
685
|
+
info("Expected Contains:");
|
|
686
|
+
try {
|
|
687
|
+
console.log(JSON.stringify(JSON.parse(result.expectedOutputContains), null, 2));
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
console.log(result.expectedOutputContains);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (result.expectedJsonSubset) {
|
|
694
|
+
divider();
|
|
695
|
+
info("Expected JSON Subset:");
|
|
696
|
+
try {
|
|
697
|
+
console.log(JSON.stringify(JSON.parse(result.expectedJsonSubset), null, 2));
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
console.log(result.expectedJsonSubset);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (result.evaluatorPromptId) {
|
|
704
|
+
divider();
|
|
705
|
+
keyValue("Evaluator Prompt", result.evaluatorPromptId);
|
|
706
|
+
keyValue("Evaluator Config", result.evaluatorConfigId || "-");
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
error(err.message);
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
// Update test case
|
|
715
|
+
tests
|
|
716
|
+
.command("update")
|
|
717
|
+
.description("Update a test case")
|
|
718
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
719
|
+
.argument("<test-case-id>", "Test Case ID")
|
|
720
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
721
|
+
.option("--name <name>", "Test case name")
|
|
722
|
+
.option("--vars <json>", "Input variables as JSON")
|
|
723
|
+
.option("--pattern <regex>", "Expected output pattern (regex)")
|
|
724
|
+
.option("--contains <json>", "Expected strings to contain (JSON array)")
|
|
725
|
+
.option("--json-subset <json>", "Expected JSON subset to match")
|
|
726
|
+
.option("--config <config-id>", "Config ID to use for this test")
|
|
727
|
+
.option("--clear-pattern", "Clear expected pattern")
|
|
728
|
+
.option("--clear-contains", "Clear expected contains")
|
|
729
|
+
.option("--clear-json-subset", "Clear expected JSON subset")
|
|
730
|
+
.option("--json", "Output as JSON")
|
|
731
|
+
.action(async (promptId, testCaseId, options) => {
|
|
732
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
733
|
+
const payload = {};
|
|
734
|
+
if (options.name)
|
|
735
|
+
payload.name = options.name;
|
|
736
|
+
if (options.vars) {
|
|
737
|
+
try {
|
|
738
|
+
payload.inputVariables = JSON.parse(options.vars);
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
error("Invalid JSON in --vars");
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (options.clearPattern) {
|
|
746
|
+
payload.expectedOutputPattern = null;
|
|
747
|
+
}
|
|
748
|
+
else if (options.pattern) {
|
|
749
|
+
payload.expectedOutputPattern = options.pattern;
|
|
750
|
+
}
|
|
751
|
+
if (options.clearContains) {
|
|
752
|
+
payload.expectedOutputContains = null;
|
|
753
|
+
}
|
|
754
|
+
else if (options.contains) {
|
|
755
|
+
try {
|
|
756
|
+
payload.expectedOutputContains = JSON.parse(options.contains);
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
error("Invalid JSON in --contains");
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (options.clearJsonSubset) {
|
|
764
|
+
payload.expectedJsonSubset = null;
|
|
765
|
+
}
|
|
766
|
+
else if (options.jsonSubset) {
|
|
767
|
+
try {
|
|
768
|
+
payload.expectedJsonSubset = JSON.parse(options.jsonSubset);
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
error("Invalid JSON in --json-subset");
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (options.config)
|
|
776
|
+
payload.configId = options.config;
|
|
777
|
+
if (Object.keys(payload).length === 0) {
|
|
778
|
+
error("No update options specified.");
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
const client = new ApiClient();
|
|
782
|
+
try {
|
|
783
|
+
const result = await client.updateTestCase(resolvedAppId, "prompt", promptId, testCaseId, payload);
|
|
784
|
+
if (options.json) {
|
|
785
|
+
json(result);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
success("Test case updated.");
|
|
789
|
+
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
error(err.message);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
// Delete test case
|
|
796
|
+
tests
|
|
797
|
+
.command("delete")
|
|
798
|
+
.description("Delete a test case")
|
|
799
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
800
|
+
.argument("<test-case-id>", "Test Case ID")
|
|
801
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
802
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
803
|
+
.action(async (promptId, testCaseId, options) => {
|
|
804
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
805
|
+
if (!options.yes) {
|
|
806
|
+
const inquirer = await import("inquirer");
|
|
807
|
+
const { confirm } = await inquirer.default.prompt([
|
|
808
|
+
{
|
|
809
|
+
type: "confirm",
|
|
810
|
+
name: "confirm",
|
|
811
|
+
message: `Are you sure you want to delete test case ${testCaseId}?`,
|
|
812
|
+
default: false,
|
|
813
|
+
},
|
|
814
|
+
]);
|
|
815
|
+
if (!confirm) {
|
|
816
|
+
info("Cancelled.");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const client = new ApiClient();
|
|
821
|
+
try {
|
|
822
|
+
await client.deleteTestCase(resolvedAppId, "prompt", promptId, testCaseId);
|
|
823
|
+
success("Test case deleted.");
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
error(err.message);
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
// Run a single test case
|
|
831
|
+
tests
|
|
832
|
+
.command("run")
|
|
833
|
+
.description("Run a single test case")
|
|
834
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
835
|
+
.argument("<test-case-id>", "Test Case ID")
|
|
836
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
837
|
+
.option("--config <config-id>", "Override config ID")
|
|
838
|
+
.option("--json", "Output as JSON")
|
|
839
|
+
.action(async (promptId, testCaseId, options) => {
|
|
840
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
841
|
+
const client = new ApiClient();
|
|
842
|
+
try {
|
|
843
|
+
// Get test case to retrieve its variables
|
|
844
|
+
const testCase = await client.getTestCase(resolvedAppId, "prompt", promptId, testCaseId);
|
|
845
|
+
let variables = {};
|
|
846
|
+
try {
|
|
847
|
+
variables = JSON.parse(testCase.inputVariables || "{}");
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
// Use empty object if parsing fails
|
|
851
|
+
}
|
|
852
|
+
const result = await client.executeBlockTest(resolvedAppId, "prompt", promptId, {
|
|
853
|
+
variables,
|
|
854
|
+
testCaseId,
|
|
855
|
+
configId: options.config || testCase.configId,
|
|
856
|
+
});
|
|
857
|
+
if (options.json) {
|
|
858
|
+
json(result);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const passed = result.verification?.passed;
|
|
862
|
+
if (passed) {
|
|
863
|
+
success("Test PASSED");
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
error("Test FAILED");
|
|
867
|
+
}
|
|
868
|
+
keyValue("Duration", formatDuration(result.metrics?.durationMs));
|
|
869
|
+
keyValue("Tokens", `${result.metrics?.inputTokens || 0} in / ${result.metrics?.outputTokens || 0} out`);
|
|
870
|
+
if (result.verification) {
|
|
871
|
+
divider();
|
|
872
|
+
info("Verification:");
|
|
873
|
+
const v = result.verification;
|
|
874
|
+
keyValue(" Passed", v.passed ? chalk.green("Yes") : chalk.red("No"));
|
|
875
|
+
keyValue(" Checks", `${v.summary?.passed || 0}/${v.summary?.total || 0} passed`);
|
|
876
|
+
if (v.checks && v.checks.length > 0) {
|
|
877
|
+
console.log();
|
|
878
|
+
for (const check of v.checks) {
|
|
879
|
+
const icon = check.passed ? chalk.green("✓") : chalk.red("✗");
|
|
880
|
+
console.log(` ${icon} ${check.name}`);
|
|
881
|
+
if (!check.passed && check.message) {
|
|
882
|
+
console.log(` ${chalk.dim(check.message)}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (result.output) {
|
|
888
|
+
divider();
|
|
889
|
+
info("Output:");
|
|
890
|
+
console.log(result.output);
|
|
891
|
+
}
|
|
892
|
+
if (result.error) {
|
|
893
|
+
divider();
|
|
894
|
+
error(`Error: ${result.error}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
catch (err) {
|
|
898
|
+
error(err.message);
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
// Run all test cases
|
|
903
|
+
tests
|
|
904
|
+
.command("run-all")
|
|
905
|
+
.description("Run all test cases for a prompt")
|
|
906
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
907
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
908
|
+
.option("--config <config-id>", "Override config ID for all tests")
|
|
909
|
+
.option("--json", "Output as JSON")
|
|
910
|
+
.action(async (promptId, options) => {
|
|
911
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
912
|
+
const client = new ApiClient();
|
|
913
|
+
try {
|
|
914
|
+
if (!options.json) {
|
|
915
|
+
info("Running all test cases...");
|
|
916
|
+
}
|
|
917
|
+
const result = await client.runAllTestCases(resolvedAppId, "prompt", promptId, {
|
|
918
|
+
overrideConfigId: options.config,
|
|
919
|
+
});
|
|
920
|
+
if (options.json) {
|
|
921
|
+
json(result);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const { summary, results, comparisonGroup } = result;
|
|
925
|
+
divider();
|
|
926
|
+
keyValue("Comparison Group", comparisonGroup);
|
|
927
|
+
keyValue("Total Tests", summary.total);
|
|
928
|
+
keyValue("Passed", chalk.green(summary.passed));
|
|
929
|
+
keyValue("Failed", summary.failed > 0 ? chalk.red(summary.failed) : "0");
|
|
930
|
+
divider();
|
|
931
|
+
if (results && results.length > 0) {
|
|
932
|
+
for (const r of results) {
|
|
933
|
+
const passed = r.verification?.passed;
|
|
934
|
+
const icon = passed ? chalk.green("✓") : chalk.red("✗");
|
|
935
|
+
const status = passed ? "PASSED" : "FAILED";
|
|
936
|
+
console.log(`${icon} ${r.testCaseName} - ${status}`);
|
|
937
|
+
if (!passed && r.verification?.details?.reason) {
|
|
938
|
+
console.log(` ${chalk.dim(r.verification.details.reason)}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
// Exit with error code if any tests failed
|
|
943
|
+
if (summary.failed > 0) {
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
catch (err) {
|
|
948
|
+
error(err.message);
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
// List test runs
|
|
953
|
+
tests
|
|
954
|
+
.command("runs")
|
|
955
|
+
.description("List recent test runs for a prompt")
|
|
956
|
+
.argument("<prompt-id>", "Prompt ID")
|
|
957
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
958
|
+
.option("--limit <n>", "Maximum number of runs to show", "20")
|
|
959
|
+
.option("--group <group>", "Filter by comparison group")
|
|
960
|
+
.option("--json", "Output as JSON")
|
|
961
|
+
.action(async (promptId, options) => {
|
|
962
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
963
|
+
const client = new ApiClient();
|
|
964
|
+
try {
|
|
965
|
+
const { items } = await client.listTestRuns(resolvedAppId, "prompt", promptId, {
|
|
966
|
+
limit: parseInt(options.limit),
|
|
967
|
+
comparisonGroup: options.group,
|
|
968
|
+
});
|
|
969
|
+
if (options.json) {
|
|
970
|
+
json(items);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (!items || items.length === 0) {
|
|
974
|
+
info("No test runs found.");
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
console.log(formatTable(items, [
|
|
978
|
+
{ header: "RUN ID", key: "runId", format: formatId },
|
|
979
|
+
{ header: "TEST CASE ID", key: "testCaseId", format: (v) => v ? formatId(v) : "-" },
|
|
980
|
+
{
|
|
981
|
+
header: "PASSED",
|
|
982
|
+
key: "verificationPassed",
|
|
983
|
+
format: (v) => v === true
|
|
984
|
+
? chalk.green("Yes")
|
|
985
|
+
: v === false
|
|
986
|
+
? chalk.red("No")
|
|
987
|
+
: "-",
|
|
988
|
+
},
|
|
989
|
+
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
990
|
+
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
991
|
+
]));
|
|
992
|
+
}
|
|
993
|
+
catch (err) {
|
|
994
|
+
error(err.message);
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
//# sourceMappingURL=prompts.js.map
|