primitive-admin 1.0.52 → 1.0.54
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 +38 -10
- package/dist/bin/primitive.js +6 -0
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/auth.js +16 -6
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +84 -7
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/catalog.js +3 -3
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collections.js +12 -9
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/database-types.js +11 -1
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.js +197 -74
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/group-type-configs.js +1 -9
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/groups.js +12 -6
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.d.ts +161 -0
- package/dist/src/commands/guides.js +502 -84
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/init.js +77 -45
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +2 -2
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +344 -0
- package/dist/src/commands/metadata.js.map +1 -0
- package/dist/src/commands/prompts.js +2 -2
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +1 -0
- package/dist/src/commands/rule-sets.js +25 -3
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.d.ts +2 -0
- package/dist/src/commands/scripts.js +667 -0
- package/dist/src/commands/scripts.js.map +1 -0
- package/dist/src/commands/sync.d.ts +212 -16
- package/dist/src/commands/sync.js +1688 -287
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/vars.d.ts +8 -0
- package/dist/src/commands/vars.js +110 -0
- package/dist/src/commands/vars.js.map +1 -0
- package/dist/src/commands/webhooks.js +17 -3
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +30 -0
- package/dist/src/commands/workflows.js +651 -52
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +123 -19
- package/dist/src/lib/api-client.js +128 -4
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/batch.d.ts +26 -0
- package/dist/src/lib/batch.js +32 -0
- package/dist/src/lib/batch.js.map +1 -0
- package/dist/src/lib/block-layout.d.ts +160 -0
- package/dist/src/lib/block-layout.js +451 -0
- package/dist/src/lib/block-layout.js.map +1 -0
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +61 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +127 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +68 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +168 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/config-toml.d.ts +10 -0
- package/dist/src/lib/config-toml.js +42 -0
- package/dist/src/lib/config-toml.js.map +1 -0
- package/dist/src/lib/config.d.ts +16 -0
- package/dist/src/lib/config.js +41 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/constants.d.ts +9 -0
- package/dist/src/lib/constants.js +9 -0
- package/dist/src/lib/constants.js.map +1 -1
- package/dist/src/lib/csv.d.ts +1 -2
- package/dist/src/lib/csv.js +3 -8
- package/dist/src/lib/csv.js.map +1 -1
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +35 -7
- package/dist/src/lib/db-codegen/dbGenerator.js +384 -125
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/db-codegen/dbNaming.d.ts +29 -0
- package/dist/src/lib/db-codegen/dbNaming.js +58 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +114 -5
- package/dist/src/lib/db-codegen/dbTemplates.js +163 -16
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +7 -3
- package/dist/src/lib/db-codegen/dbTsTypes.js +23 -1
- package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +52 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/init-config.d.ts +13 -0
- package/dist/src/lib/init-config.js +21 -2
- package/dist/src/lib/init-config.js.map +1 -1
- package/dist/src/lib/output.d.ts +42 -5
- package/dist/src/lib/output.js +70 -4
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/query-operators.d.ts +43 -0
- package/dist/src/lib/query-operators.js +80 -0
- package/dist/src/lib/query-operators.js.map +1 -0
- package/dist/src/lib/template.d.ts +9 -5
- package/dist/src/lib/template.js +58 -45
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/toml-database-config.d.ts +10 -19
- package/dist/src/lib/toml-database-config.js +28 -59
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +147 -0
- package/dist/src/lib/toml-metadata-config.js +463 -0
- package/dist/src/lib/toml-metadata-config.js.map +1 -0
- package/dist/src/lib/toml-native-form.d.ts +46 -0
- package/dist/src/lib/toml-native-form.js +78 -0
- package/dist/src/lib/toml-native-form.js.map +1 -0
- package/dist/src/lib/toml-params-validator.d.ts +34 -0
- package/dist/src/lib/toml-params-validator.js +118 -3
- package/dist/src/lib/toml-params-validator.js.map +1 -1
- package/dist/src/lib/workflow-apply.d.ts +86 -0
- package/dist/src/lib/workflow-apply.js +127 -0
- package/dist/src/lib/workflow-apply.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +74 -0
- package/dist/src/lib/workflow-codegen/generator.js +215 -0
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
- package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
- package/dist/src/lib/workflow-codegen/naming.js +81 -0
- package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +64 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js +282 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
- package/dist/src/lib/workflow-fragments.js +3 -3
- package/dist/src/lib/workflow-fragments.js.map +1 -1
- package/dist/src/lib/workflow-payload.d.ts +68 -0
- package/dist/src/lib/workflow-payload.js +150 -0
- package/dist/src/lib/workflow-payload.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.d.ts +1 -1
- package/dist/src/lib/workflow-toml-validator.js +37 -1
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/types/index.d.ts +11 -0
- package/dist/src/validators.d.ts +4 -3
- package/dist/src/validators.js +6 -5
- package/dist/src/validators.js.map +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { ApiClient } from "../lib/api-client.js";
|
|
2
|
+
import { resolveAppId } from "../lib/config.js";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { success, error, info, keyValue, result as printResult, formatTable, formatId, formatDate, formatDuration, json, divider, } from "../lib/output.js";
|
|
5
|
+
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a user-supplied script reference (either a `scriptId` or the
|
|
8
|
+
* script's `name`) to its `scriptId`. Scripts are named handles, so the CLI
|
|
9
|
+
* accepts a name for convenience the way `sync` refers to them by name; the
|
|
10
|
+
* block-testing API keys on `scriptId` (the block id). If a raw id matches an
|
|
11
|
+
* existing script it is used directly; otherwise we list the app's scripts and
|
|
12
|
+
* match on name (case-sensitive, then case-insensitive as a fallback).
|
|
13
|
+
*/
|
|
14
|
+
async function resolveScriptId(client, appId, ref) {
|
|
15
|
+
const { items } = await client.listScripts(appId);
|
|
16
|
+
const byId = items.find((s) => s.scriptId === ref);
|
|
17
|
+
if (byId)
|
|
18
|
+
return byId.scriptId;
|
|
19
|
+
const byName = items.find((s) => s.name === ref);
|
|
20
|
+
if (byName)
|
|
21
|
+
return byName.scriptId;
|
|
22
|
+
const byNameCi = items.find((s) => String(s.name).toLowerCase() === ref.toLowerCase());
|
|
23
|
+
if (byNameCi)
|
|
24
|
+
return byNameCi.scriptId;
|
|
25
|
+
throw new Error(`Script not found: ${ref}`);
|
|
26
|
+
}
|
|
27
|
+
export function registerScriptsCommands(program) {
|
|
28
|
+
const scripts = program
|
|
29
|
+
.command("scripts")
|
|
30
|
+
.description("Inspect Rhai scripts (transforms), their versions, and tests")
|
|
31
|
+
.addHelpText("after", `
|
|
32
|
+
Examples:
|
|
33
|
+
$ primitive scripts list
|
|
34
|
+
$ primitive scripts get my-transform
|
|
35
|
+
$ primitive scripts configs list my-transform
|
|
36
|
+
$ primitive scripts tests list my-transform
|
|
37
|
+
$ primitive scripts tests create my-transform --name "Basic" --vars '{"x":1}'
|
|
38
|
+
$ primitive scripts tests run my-transform <test-case-id>
|
|
39
|
+
$ primitive scripts tests run-all my-transform
|
|
40
|
+
|
|
41
|
+
Scripts (transforms) are authored via 'sync' from transforms/<name>.rhai
|
|
42
|
+
files. Test cases live under transforms/<name>.tests/*.toml and round-trip
|
|
43
|
+
through 'sync pull' / 'sync push'. Both the script reference and the test
|
|
44
|
+
commands accept either a script id or a script name.
|
|
45
|
+
`);
|
|
46
|
+
// List scripts
|
|
47
|
+
scripts
|
|
48
|
+
.command("list")
|
|
49
|
+
.description("List scripts (transforms) for an app")
|
|
50
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
51
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
52
|
+
.option("--json", "Output as JSON")
|
|
53
|
+
.action(async (appId, options) => {
|
|
54
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
55
|
+
const client = new ApiClient();
|
|
56
|
+
try {
|
|
57
|
+
const { items } = await client.listScripts(resolvedAppId);
|
|
58
|
+
if (options.json) {
|
|
59
|
+
json(items);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!items || items.length === 0) {
|
|
63
|
+
info("No scripts found.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(formatTable(items, [
|
|
67
|
+
{ header: "ID", key: "scriptId", format: formatId },
|
|
68
|
+
{ header: "NAME", key: "name" },
|
|
69
|
+
{
|
|
70
|
+
header: "ACTIVE CONFIG",
|
|
71
|
+
key: "activeConfigId",
|
|
72
|
+
format: (v) => (v ? formatId(v) : "-"),
|
|
73
|
+
},
|
|
74
|
+
{ header: "MODIFIED", key: "modifiedAt", format: formatDate },
|
|
75
|
+
]));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
error(err.message);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Get a script
|
|
83
|
+
scripts
|
|
84
|
+
.command("get")
|
|
85
|
+
.description("Get a script's details and versions")
|
|
86
|
+
.argument("<script>", "Script ID or name")
|
|
87
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
88
|
+
.option("--json", "Output as JSON")
|
|
89
|
+
.action(async (script, options) => {
|
|
90
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
91
|
+
const client = new ApiClient();
|
|
92
|
+
try {
|
|
93
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
94
|
+
const result = await client.getScript(resolvedAppId, scriptId);
|
|
95
|
+
if (options.json) {
|
|
96
|
+
json(result);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
printResult("Script ID", result.scriptId);
|
|
100
|
+
printResult("Name", result.name);
|
|
101
|
+
printResult("Description", result.description || "-");
|
|
102
|
+
printResult("Active Config", result.activeConfigId || "-");
|
|
103
|
+
printResult("Created", formatDate(result.createdAt));
|
|
104
|
+
const configs = Array.isArray(result.configs) ? result.configs : [];
|
|
105
|
+
if (configs.length > 0) {
|
|
106
|
+
divider();
|
|
107
|
+
info("Versions:");
|
|
108
|
+
console.log(formatTable(configs, [
|
|
109
|
+
{ header: "CONFIG ID", key: "configId", format: formatId },
|
|
110
|
+
{ header: "NAME", key: "configName" },
|
|
111
|
+
{ header: "STATUS", key: "status" },
|
|
112
|
+
{
|
|
113
|
+
header: "ACTIVE",
|
|
114
|
+
key: "configId",
|
|
115
|
+
format: (v) => v === result.activeConfigId ? chalk.green("Yes") : "-",
|
|
116
|
+
},
|
|
117
|
+
{ header: "CONTENT HASH", key: "contentHash", format: formatId },
|
|
118
|
+
]));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
error(err.message);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// ============================================
|
|
127
|
+
// CONFIGS (VERSIONS) SUBCOMMAND
|
|
128
|
+
// ============================================
|
|
129
|
+
const configs = scripts
|
|
130
|
+
.command("configs")
|
|
131
|
+
.description("Inspect a script's versioned configs");
|
|
132
|
+
configs
|
|
133
|
+
.command("list")
|
|
134
|
+
.description("List a script's versions (configs)")
|
|
135
|
+
.argument("<script>", "Script ID or name")
|
|
136
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
137
|
+
.option("--json", "Output as JSON")
|
|
138
|
+
.action(async (script, options) => {
|
|
139
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
140
|
+
const client = new ApiClient();
|
|
141
|
+
try {
|
|
142
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
143
|
+
const header = await client.getScript(resolvedAppId, scriptId);
|
|
144
|
+
const { items } = await client.listScriptConfigs(resolvedAppId, scriptId);
|
|
145
|
+
if (options.json) {
|
|
146
|
+
json(items);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!items || items.length === 0) {
|
|
150
|
+
info("No versions found.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
console.log(formatTable(items, [
|
|
154
|
+
{ header: "CONFIG ID", key: "configId", format: formatId },
|
|
155
|
+
{ header: "NAME", key: "configName" },
|
|
156
|
+
{ header: "STATUS", key: "status" },
|
|
157
|
+
{
|
|
158
|
+
header: "ACTIVE",
|
|
159
|
+
key: "configId",
|
|
160
|
+
format: (v) => v === header.activeConfigId ? chalk.green("Yes") : "-",
|
|
161
|
+
},
|
|
162
|
+
{ header: "CONTENT HASH", key: "contentHash", format: formatId },
|
|
163
|
+
{ header: "CREATED", key: "createdAt", format: formatDate },
|
|
164
|
+
]));
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
error(err.message);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// ============================================
|
|
172
|
+
// TESTS SUBCOMMAND — script block testing (blockType "script")
|
|
173
|
+
// ============================================
|
|
174
|
+
const tests = scripts
|
|
175
|
+
.command("tests")
|
|
176
|
+
.description("Manage and run script test cases")
|
|
177
|
+
.addHelpText("after", `
|
|
178
|
+
Test modes:
|
|
179
|
+
Active version — omit --config; runs against the script's active config.
|
|
180
|
+
Pinned version — pass --config <config-id> to run a specific version.
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
$ primitive scripts tests list my-transform
|
|
184
|
+
$ primitive scripts tests create my-transform --name "Basic" --vars '{"x":1}'
|
|
185
|
+
$ primitive scripts tests run my-transform <test-case-id>
|
|
186
|
+
$ primitive scripts tests run my-transform <test-case-id> --config <config-id>
|
|
187
|
+
$ primitive scripts tests run-all my-transform
|
|
188
|
+
$ primitive scripts tests runs my-transform
|
|
189
|
+
`);
|
|
190
|
+
// List test cases
|
|
191
|
+
tests
|
|
192
|
+
.command("list")
|
|
193
|
+
.description("List test cases for a script")
|
|
194
|
+
.argument("<script>", "Script ID or name")
|
|
195
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
196
|
+
.option("--json", "Output as JSON")
|
|
197
|
+
.action(async (script, options) => {
|
|
198
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
199
|
+
const client = new ApiClient();
|
|
200
|
+
try {
|
|
201
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
202
|
+
const { items } = await client.listTestCases(resolvedAppId, "script", scriptId);
|
|
203
|
+
if (options.json) {
|
|
204
|
+
json(items);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!items || items.length === 0) {
|
|
208
|
+
info("No test cases found.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
console.log(formatTable(items, [
|
|
212
|
+
{ header: "ID", key: "testCaseId", format: formatId },
|
|
213
|
+
{ header: "NAME", key: "name" },
|
|
214
|
+
{
|
|
215
|
+
header: "CONFIG",
|
|
216
|
+
key: "configId",
|
|
217
|
+
format: (v) => (v ? formatId(v) : "-"),
|
|
218
|
+
},
|
|
219
|
+
{ header: "CREATED", key: "createdAt", format: formatDate },
|
|
220
|
+
]));
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
error(err.message);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// Create test case
|
|
228
|
+
tests
|
|
229
|
+
.command("create")
|
|
230
|
+
.description("Create a test case for a script")
|
|
231
|
+
.argument("<script>", "Script ID or name")
|
|
232
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
233
|
+
.option("--name <name>", "Test case name (required)")
|
|
234
|
+
.option("--vars <json>", "Input variables as JSON (required)")
|
|
235
|
+
.option("--pattern <regex>", "Expected output pattern (regex)")
|
|
236
|
+
.option("--contains <json>", "Expected strings to contain (JSON array)")
|
|
237
|
+
.option("--json-subset <json>", "Expected JSON subset to match")
|
|
238
|
+
.option("--config <config-id>", "Config ID to pin this test to a version")
|
|
239
|
+
.option("--json", "Output as JSON")
|
|
240
|
+
.action(async (script, options) => {
|
|
241
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
242
|
+
if (!options.name || !options.vars) {
|
|
243
|
+
error("Required: --name and --vars");
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
let inputVariables;
|
|
247
|
+
try {
|
|
248
|
+
inputVariables = JSON.parse(options.vars);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
error("Invalid JSON in --vars");
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
let expectedOutputContains;
|
|
255
|
+
if (options.contains) {
|
|
256
|
+
try {
|
|
257
|
+
expectedOutputContains = JSON.parse(options.contains);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
error("Invalid JSON in --contains");
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
let expectedJsonSubset;
|
|
265
|
+
if (options.jsonSubset) {
|
|
266
|
+
try {
|
|
267
|
+
expectedJsonSubset = JSON.parse(options.jsonSubset);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
error("Invalid JSON in --json-subset");
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const client = new ApiClient();
|
|
275
|
+
try {
|
|
276
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
277
|
+
const result = await client.createTestCase(resolvedAppId, "script", scriptId, {
|
|
278
|
+
name: options.name,
|
|
279
|
+
inputVariables,
|
|
280
|
+
expectedOutputPattern: options.pattern,
|
|
281
|
+
expectedOutputContains,
|
|
282
|
+
expectedJsonSubset,
|
|
283
|
+
configId: options.config,
|
|
284
|
+
});
|
|
285
|
+
if (options.json) {
|
|
286
|
+
json(result);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
success(`Test case created: ${result.name}`);
|
|
290
|
+
keyValue("Test Case ID", result.testCaseId);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
error(err.message);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
// Get test case
|
|
298
|
+
tests
|
|
299
|
+
.command("get")
|
|
300
|
+
.description("Get test case details")
|
|
301
|
+
.argument("<script>", "Script ID or name")
|
|
302
|
+
.argument("<test-case-id>", "Test Case ID")
|
|
303
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
304
|
+
.option("--json", "Output as JSON")
|
|
305
|
+
.action(async (script, testCaseId, options) => {
|
|
306
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
307
|
+
const client = new ApiClient();
|
|
308
|
+
try {
|
|
309
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
310
|
+
const result = await client.getTestCase(resolvedAppId, "script", scriptId, testCaseId);
|
|
311
|
+
if (options.json) {
|
|
312
|
+
json(result);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
printResult("Test Case ID", result.testCaseId);
|
|
316
|
+
printResult("Name", result.name);
|
|
317
|
+
printResult("Config ID", result.configId || "-");
|
|
318
|
+
printResult("Created", formatDate(result.createdAt));
|
|
319
|
+
divider();
|
|
320
|
+
info("Input Variables:");
|
|
321
|
+
try {
|
|
322
|
+
console.log(JSON.stringify(JSON.parse(result.inputVariables || "{}"), null, 2));
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
console.log(result.inputVariables || "{}");
|
|
326
|
+
}
|
|
327
|
+
if (result.expectedOutputPattern) {
|
|
328
|
+
divider();
|
|
329
|
+
printResult("Expected Pattern", result.expectedOutputPattern);
|
|
330
|
+
}
|
|
331
|
+
if (result.expectedOutputContains) {
|
|
332
|
+
divider();
|
|
333
|
+
info("Expected Contains:");
|
|
334
|
+
try {
|
|
335
|
+
console.log(JSON.stringify(JSON.parse(result.expectedOutputContains), null, 2));
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
console.log(result.expectedOutputContains);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (result.expectedJsonSubset) {
|
|
342
|
+
divider();
|
|
343
|
+
info("Expected JSON Subset:");
|
|
344
|
+
try {
|
|
345
|
+
console.log(JSON.stringify(JSON.parse(result.expectedJsonSubset), null, 2));
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
console.log(result.expectedJsonSubset);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
error(err.message);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
// Update test case
|
|
358
|
+
tests
|
|
359
|
+
.command("update")
|
|
360
|
+
.description("Update a test case")
|
|
361
|
+
.argument("<script>", "Script ID or name")
|
|
362
|
+
.argument("<test-case-id>", "Test Case ID")
|
|
363
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
364
|
+
.option("--name <name>", "Test case name")
|
|
365
|
+
.option("--vars <json>", "Input variables as JSON")
|
|
366
|
+
.option("--pattern <regex>", "Expected output pattern (regex)")
|
|
367
|
+
.option("--contains <json>", "Expected strings to contain (JSON array)")
|
|
368
|
+
.option("--json-subset <json>", "Expected JSON subset to match")
|
|
369
|
+
.option("--config <config-id>", "Config ID to pin this test to a version")
|
|
370
|
+
.option("--clear-pattern", "Clear expected pattern")
|
|
371
|
+
.option("--clear-contains", "Clear expected contains")
|
|
372
|
+
.option("--clear-json-subset", "Clear expected JSON subset")
|
|
373
|
+
.option("--json", "Output as JSON")
|
|
374
|
+
.action(async (script, testCaseId, options) => {
|
|
375
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
376
|
+
const payload = {};
|
|
377
|
+
if (options.name)
|
|
378
|
+
payload.name = options.name;
|
|
379
|
+
if (options.vars) {
|
|
380
|
+
try {
|
|
381
|
+
payload.inputVariables = JSON.parse(options.vars);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
error("Invalid JSON in --vars");
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (options.clearPattern) {
|
|
389
|
+
payload.expectedOutputPattern = null;
|
|
390
|
+
}
|
|
391
|
+
else if (options.pattern) {
|
|
392
|
+
payload.expectedOutputPattern = options.pattern;
|
|
393
|
+
}
|
|
394
|
+
if (options.clearContains) {
|
|
395
|
+
payload.expectedOutputContains = null;
|
|
396
|
+
}
|
|
397
|
+
else if (options.contains) {
|
|
398
|
+
try {
|
|
399
|
+
payload.expectedOutputContains = JSON.parse(options.contains);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
error("Invalid JSON in --contains");
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (options.clearJsonSubset) {
|
|
407
|
+
payload.expectedJsonSubset = null;
|
|
408
|
+
}
|
|
409
|
+
else if (options.jsonSubset) {
|
|
410
|
+
try {
|
|
411
|
+
payload.expectedJsonSubset = JSON.parse(options.jsonSubset);
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
error("Invalid JSON in --json-subset");
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (options.config)
|
|
419
|
+
payload.configId = options.config;
|
|
420
|
+
if (Object.keys(payload).length === 0) {
|
|
421
|
+
error("No update options specified.");
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
const client = new ApiClient();
|
|
425
|
+
try {
|
|
426
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
427
|
+
const result = await client.updateTestCase(resolvedAppId, "script", scriptId, testCaseId, payload);
|
|
428
|
+
if (options.json) {
|
|
429
|
+
json(result);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
success("Test case updated.");
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
error(err.message);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
// Delete test case
|
|
440
|
+
tests
|
|
441
|
+
.command("delete")
|
|
442
|
+
.description("Delete a test case")
|
|
443
|
+
.argument("<script>", "Script ID or name")
|
|
444
|
+
.argument("<test-case-id>", "Test Case ID")
|
|
445
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
446
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
447
|
+
.action(async (script, testCaseId, options) => {
|
|
448
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
449
|
+
if (!options.yes) {
|
|
450
|
+
let confirm;
|
|
451
|
+
try {
|
|
452
|
+
confirm = await confirmPrompt(`Are you sure you want to delete test case ${testCaseId}?`);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
error(err.message);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
if (!confirm) {
|
|
459
|
+
info("Cancelled.");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const client = new ApiClient();
|
|
464
|
+
try {
|
|
465
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
466
|
+
await client.deleteTestCase(resolvedAppId, "script", scriptId, testCaseId);
|
|
467
|
+
success("Test case deleted.");
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
error(err.message);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
// Run a single test case
|
|
475
|
+
tests
|
|
476
|
+
.command("run")
|
|
477
|
+
.description("Run a single test case")
|
|
478
|
+
.argument("<script>", "Script ID or name")
|
|
479
|
+
.argument("<test-case-id>", "Test Case ID")
|
|
480
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
481
|
+
.option("--config <config-id>", "Pin to a specific version (defaults to the test case's config, else the active version)")
|
|
482
|
+
.option("--json", "Output as JSON")
|
|
483
|
+
.action(async (script, testCaseId, options) => {
|
|
484
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
485
|
+
const client = new ApiClient();
|
|
486
|
+
try {
|
|
487
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
488
|
+
const testCase = await client.getTestCase(resolvedAppId, "script", scriptId, testCaseId);
|
|
489
|
+
let variables = {};
|
|
490
|
+
try {
|
|
491
|
+
variables = JSON.parse(testCase.inputVariables || "{}");
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
// Use empty object if parsing fails
|
|
495
|
+
}
|
|
496
|
+
const result = await client.executeBlockTest(resolvedAppId, "script", scriptId, {
|
|
497
|
+
variables,
|
|
498
|
+
testCaseId,
|
|
499
|
+
configId: options.config || testCase.configId,
|
|
500
|
+
});
|
|
501
|
+
if (options.json) {
|
|
502
|
+
json(result);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const passed = result.verification?.passed;
|
|
506
|
+
if (passed) {
|
|
507
|
+
success("Test PASSED");
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
error("Test FAILED");
|
|
511
|
+
}
|
|
512
|
+
keyValue("Duration", formatDuration(result.metrics?.durationMs));
|
|
513
|
+
if (result.verification) {
|
|
514
|
+
divider();
|
|
515
|
+
info("Verification:");
|
|
516
|
+
const v = result.verification;
|
|
517
|
+
keyValue(" Passed", v.passed ? chalk.green("Yes") : chalk.red("No"));
|
|
518
|
+
if (v.summary) {
|
|
519
|
+
keyValue(" Checks", `${v.summary?.passed || 0}/${v.summary?.total || 0} passed`);
|
|
520
|
+
}
|
|
521
|
+
if (v.checks && v.checks.length > 0) {
|
|
522
|
+
console.log();
|
|
523
|
+
for (const check of v.checks) {
|
|
524
|
+
const icon = check.passed ? chalk.green("✓") : chalk.red("✗");
|
|
525
|
+
console.log(` ${icon} ${check.name}`);
|
|
526
|
+
if (!check.passed && check.message) {
|
|
527
|
+
console.log(` ${chalk.dim(check.message)}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (result.output) {
|
|
533
|
+
divider();
|
|
534
|
+
info("Output:");
|
|
535
|
+
console.log(result.output);
|
|
536
|
+
}
|
|
537
|
+
if (result.error) {
|
|
538
|
+
divider();
|
|
539
|
+
error(`Error: ${result.error}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
error(err.message);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
// Run all test cases
|
|
548
|
+
tests
|
|
549
|
+
.command("run-all")
|
|
550
|
+
.description("Run all test cases for a script, or a specific subset")
|
|
551
|
+
.argument("<script>", "Script ID or name")
|
|
552
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
553
|
+
.option("--config <config-id>", "Override config ID for all tests (pin to a version)")
|
|
554
|
+
.option("--test-cases <ids>", "Comma-separated list of test case IDs to run (runs only these)")
|
|
555
|
+
.option("--json", "Output as JSON")
|
|
556
|
+
.action(async (script, options) => {
|
|
557
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
558
|
+
const client = new ApiClient();
|
|
559
|
+
try {
|
|
560
|
+
let testCaseIds;
|
|
561
|
+
if (options.testCases) {
|
|
562
|
+
testCaseIds = options.testCases
|
|
563
|
+
.split(",")
|
|
564
|
+
.map((id) => id.trim())
|
|
565
|
+
.filter(Boolean);
|
|
566
|
+
if (testCaseIds.length === 0) {
|
|
567
|
+
error("No valid test case IDs provided in --test-cases");
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
572
|
+
if (!options.json) {
|
|
573
|
+
if (testCaseIds) {
|
|
574
|
+
info(`Running ${testCaseIds.length} selected test case(s)...`);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
info("Running all test cases...");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const result = await client.runAllTestCases(resolvedAppId, "script", scriptId, {
|
|
581
|
+
overrideConfigId: options.config,
|
|
582
|
+
testCaseIds,
|
|
583
|
+
});
|
|
584
|
+
if (options.json) {
|
|
585
|
+
json(result);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const { summary, runs, comparisonGroup } = result;
|
|
589
|
+
divider();
|
|
590
|
+
keyValue("Comparison Group", comparisonGroup);
|
|
591
|
+
keyValue("Total Tests", summary.total);
|
|
592
|
+
keyValue("Passed", chalk.green(summary.passed));
|
|
593
|
+
keyValue("Failed", summary.failed > 0 ? chalk.red(summary.failed) : "0");
|
|
594
|
+
divider();
|
|
595
|
+
if (runs && runs.length > 0) {
|
|
596
|
+
for (const r of runs) {
|
|
597
|
+
const passed = r.verification?.passed;
|
|
598
|
+
const icon = passed ? chalk.green("✓") : chalk.red("✗");
|
|
599
|
+
const status = passed ? "PASSED" : "FAILED";
|
|
600
|
+
console.log(`${icon} ${r.testCaseName} - ${status}`);
|
|
601
|
+
if (!passed && r.verification?.details?.reason) {
|
|
602
|
+
console.log(` ${chalk.dim(r.verification.details.reason)}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (summary.failed > 0) {
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
error(err.message);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
// List test runs
|
|
616
|
+
tests
|
|
617
|
+
.command("runs")
|
|
618
|
+
.description("List recent test runs for a script")
|
|
619
|
+
.argument("<script>", "Script ID or name")
|
|
620
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
621
|
+
.option("--limit <n>", "Maximum number of runs to show", "20")
|
|
622
|
+
.option("--group <group>", "Filter by comparison group")
|
|
623
|
+
.option("--json", "Output as JSON")
|
|
624
|
+
.action(async (script, options) => {
|
|
625
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
626
|
+
const client = new ApiClient();
|
|
627
|
+
try {
|
|
628
|
+
const scriptId = await resolveScriptId(client, resolvedAppId, script);
|
|
629
|
+
const { items } = await client.listTestRuns(resolvedAppId, "script", scriptId, {
|
|
630
|
+
limit: parseInt(options.limit),
|
|
631
|
+
comparisonGroup: options.group,
|
|
632
|
+
});
|
|
633
|
+
if (options.json) {
|
|
634
|
+
json(items);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (!items || items.length === 0) {
|
|
638
|
+
info("No test runs found.");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
console.log(formatTable(items, [
|
|
642
|
+
{ header: "RUN ID", key: "runId", format: formatId },
|
|
643
|
+
{
|
|
644
|
+
header: "TEST CASE ID",
|
|
645
|
+
key: "testCaseId",
|
|
646
|
+
format: (v) => (v ? formatId(v) : "-"),
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
header: "PASSED",
|
|
650
|
+
key: "verificationPassed",
|
|
651
|
+
format: (v) => v === true
|
|
652
|
+
? chalk.green("Yes")
|
|
653
|
+
: v === false
|
|
654
|
+
? chalk.red("No")
|
|
655
|
+
: "-",
|
|
656
|
+
},
|
|
657
|
+
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
658
|
+
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
659
|
+
]));
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
error(err.message);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=scripts.js.map
|