@specbook/cli 0.2.0 → 0.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.
@@ -0,0 +1,593 @@
1
+ // src/standalone.ts
2
+ import { File } from "buffer";
3
+ import { basename, posix } from "path";
4
+ import { parseArgs } from "util";
5
+ import {
6
+ environmentNames
7
+ } from "@specbook/core";
8
+
9
+ // src/run-tests.ts
10
+ import { execFileSync } from "child_process";
11
+ import { appendFileSync, readFileSync } from "fs";
12
+ import { mkdir, writeFile } from "fs/promises";
13
+ import { hostname } from "os";
14
+ import { dirname as dirname2, resolve as resolve2 } from "path";
15
+ import {
16
+ baseUrlFor,
17
+ buildReport,
18
+ collectSecretNames,
19
+ colorEnabled,
20
+ createColors,
21
+ createFetchTransport,
22
+ createListReporter,
23
+ createMockTransport,
24
+ detectCi,
25
+ dotenvProvider,
26
+ environmentScope,
27
+ envProvider,
28
+ formatGithub,
29
+ formatList,
30
+ githubSummary,
31
+ headerLine,
32
+ installFaker,
33
+ MissingSecretsError,
34
+ maskDeep,
35
+ redactEvent,
36
+ redactResults,
37
+ resolveSecrets,
38
+ retryLine,
39
+ runSuite,
40
+ testExitCode,
41
+ toJUnit,
42
+ withoutLocations
43
+ } from "@specbook/core";
44
+
45
+ // src/project.ts
46
+ import { existsSync } from "fs";
47
+ import { readFile } from "fs/promises";
48
+ import { dirname, join, resolve } from "path";
49
+ import { parseDotenv } from "@specbook/core";
50
+ async function readDotenv(path) {
51
+ if (!existsSync(path)) return {};
52
+ return parseDotenv(await readFile(path, "utf8"));
53
+ }
54
+
55
+ // src/version.ts
56
+ import { createRequire } from "module";
57
+ var fixed;
58
+ function fixCliVersion(version) {
59
+ fixed = version;
60
+ }
61
+ function cliVersion() {
62
+ if (fixed !== void 0) return fixed;
63
+ const require2 = createRequire(import.meta.url);
64
+ for (const p of ["../package.json", "../../package.json"])
65
+ try {
66
+ return require2(p).version;
67
+ } catch {
68
+ }
69
+ return "";
70
+ }
71
+
72
+ // src/run-tests.ts
73
+ var titleOf = (r) => r.kind === "sequence" ? r.meta.title : r.input.name ?? r.op.id;
74
+ var tagsOf = (r) => r.kind === "sequence" ? r.meta.tags ?? [] : r.input.tags ?? [];
75
+ function isRunnable(v) {
76
+ const k = v?.kind;
77
+ return k === "request" || k === "sequence";
78
+ }
79
+ function targetsOf(mod, file) {
80
+ const found = isRunnable(mod.default) ? [mod.default] : Object.values(mod).filter(isRunnable);
81
+ return found.map((run) => ({ run, file, title: titleOf(run), tags: tagsOf(run) }));
82
+ }
83
+ var KINDS = ["list", "json", "junit", "github", "ndjson"];
84
+ function gitInfo(root) {
85
+ const git = (...args) => {
86
+ try {
87
+ return execFileSync("git", args, { cwd: root, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
88
+ } catch {
89
+ return void 0;
90
+ }
91
+ };
92
+ return {
93
+ sha: git("rev-parse", "--short", "HEAD"),
94
+ branch: git("rev-parse", "--abbrev-ref", "HEAD")
95
+ };
96
+ }
97
+ var quote = (s) => `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
98
+ var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
99
+ function parseShard(s) {
100
+ const m = /^(\d+)\/(\d+)$/.exec(s);
101
+ if (!m) return void 0;
102
+ const [index, total] = [Number(m[1]), Number(m[2])];
103
+ return index >= 1 && total >= 1 && index <= total ? { index, total } : void 0;
104
+ }
105
+ function select(targets, o) {
106
+ let out = targets;
107
+ if (o.tags.length) out = out.filter((t) => t.tags.some((x) => o.tags.includes(x)));
108
+ try {
109
+ if (o.grep) {
110
+ const re = new RegExp(o.grep, "i");
111
+ out = out.filter((t) => re.test(`${t.file} ${t.title}`));
112
+ }
113
+ if (o.grepInvert) {
114
+ const re = new RegExp(o.grepInvert, "i");
115
+ out = out.filter((t) => !re.test(`${t.file} ${t.title}`));
116
+ }
117
+ } catch (e) {
118
+ return `Bad --grep expression: ${e instanceof Error ? e.message : e}`;
119
+ }
120
+ if (o.shard) {
121
+ const shard = parseShard(o.shard);
122
+ if (!shard) return `Bad --shard "${o.shard}": use i/n, for example 1/3`;
123
+ out = out.filter((_, i) => i % shard.total === shard.index - 1);
124
+ }
125
+ return out;
126
+ }
127
+ var authOf = (config) => config.auth ?? {};
128
+ var serviceAuthOf = (config) => Object.fromEntries(
129
+ Object.entries(config.services).filter(([, s]) => s.auth).map(([n, s]) => [n, s.auth])
130
+ );
131
+ async function resolveRunSecrets(ctx, targets, env, files, secretsFile) {
132
+ const needed = /* @__PURE__ */ new Set();
133
+ for (const t of targets)
134
+ for (const n of await collectSecretNames(
135
+ t,
136
+ authOf(ctx.config),
137
+ ctx.examples.example,
138
+ serviceAuthOf(ctx.config)
139
+ ))
140
+ needed.add(n);
141
+ const providers = [
142
+ envProvider(process.env),
143
+ ...secretsFile ? [dotenvProvider(await readDotenv(resolve2(secretsFile)), "secrets-file")] : [],
144
+ dotenvProvider(files.secrets, `environments/${env}.secrets.env`),
145
+ dotenvProvider(files.shared, "secrets.local.env"),
146
+ ...ctx.config.secretProviders ?? []
147
+ ];
148
+ return resolveSecrets(needed, ctx.config.secrets ?? {}, providers);
149
+ }
150
+ function missingSecretsMessage(e, env, files) {
151
+ const list = `Missing secrets for "${env}":
152
+ ${e.names.join("\n ")}
153
+
154
+ `;
155
+ return files ? `${list}Set them as environment variables, or in .specbook/environments/${env}.secrets.env (or .specbook/secrets.local.env to share across environments).` : `${list}Set them as environment variables.`;
156
+ }
157
+ function transportFor(ctx, env, vars) {
158
+ if (env === "mock") return createMockTransport({ loader: ctx.mock });
159
+ return createFetchTransport({
160
+ baseUrl: (service) => {
161
+ const url = baseUrlFor(ctx.config, service, env, vars);
162
+ if (!url) throw new Error(`No baseUrl for service "${service}" in env "${env}"`);
163
+ return url;
164
+ }
165
+ });
166
+ }
167
+ function originsFor(config, env, vars, basePath = false) {
168
+ if (env === "mock") return {};
169
+ const out = {};
170
+ for (const service of Object.keys(config.services)) {
171
+ try {
172
+ const url = baseUrlFor(config, service, env, vars);
173
+ if (url) {
174
+ const u = new URL(url);
175
+ out[service] = basePath ? `${u.origin}${u.pathname.replace(/\/+$/, "")}` : u.origin;
176
+ }
177
+ } catch {
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+ async function runTests(ctx, o) {
183
+ const specs = (o.reporters.length ? o.reporters : ["list"]).map((s) => {
184
+ const [kind, path] = s.split(/:(.*)/s);
185
+ return { kind, path };
186
+ });
187
+ const bad = specs.find((s) => !KINDS.includes(s.kind));
188
+ if (bad) {
189
+ console.error(`Unknown reporter "${bad.kind}". Use: ${KINDS.join(", ")}`);
190
+ return 3;
191
+ }
192
+ if (process.env.GITHUB_ACTIONS === "true" && !specs.some((s) => s.kind === "github"))
193
+ specs.push({ kind: "github", path: void 0 });
194
+ if (!ctx.environments.includes(o.env)) {
195
+ console.error(`Unknown environment "${o.env}". Known: ${ctx.environments.join(", ")}`);
196
+ return 3;
197
+ }
198
+ const picked = select(ctx.targets, o);
199
+ if (typeof picked === "string") {
200
+ console.error(picked);
201
+ return 3;
202
+ }
203
+ if (picked.length === 0) {
204
+ console.error(
205
+ "Nothing to run: no request or flow found, or none matched --tag, --grep or --shard."
206
+ );
207
+ return 3;
208
+ }
209
+ if (o.list) {
210
+ for (const t of picked)
211
+ console.log(`${t.file} \u203A ${t.title}${t.tags.length ? ` [${t.tags.join(", ")}]` : ""}`);
212
+ console.log(`
213
+ ${picked.length} ${picked.length === 1 ? "test" : "tests"}`);
214
+ return 0;
215
+ }
216
+ if (o.env === "mock" && !ctx.mock) {
217
+ console.error(
218
+ "The mock environment is for development, where the generated examples are. Test a real environment (--env <name>)."
219
+ );
220
+ return 2;
221
+ }
222
+ const envFiles = await ctx.readEnvironment(o.env);
223
+ let secrets = {};
224
+ if (o.env !== "mock") {
225
+ try {
226
+ secrets = await resolveRunSecrets(
227
+ ctx,
228
+ picked.map((t) => t.run),
229
+ o.env,
230
+ envFiles,
231
+ o.secretsFile
232
+ );
233
+ } catch (e) {
234
+ if (e instanceof MissingSecretsError) {
235
+ console.error(missingSecretsMessage(e, o.env, ctx.secretFiles !== false));
236
+ return 2;
237
+ }
238
+ throw e;
239
+ }
240
+ }
241
+ try {
242
+ return await execute(ctx, o, picked, specs, envFiles.vars, secrets);
243
+ } catch (e) {
244
+ console.error(maskDeep(e instanceof Error ? e.message : String(e), Object.values(secrets)));
245
+ return 2;
246
+ }
247
+ }
248
+ async function execute(ctx, o, picked, specs, fileVars, secrets) {
249
+ const config = ctx.config;
250
+ const total = ctx.targets.length;
251
+ const given = Object.fromEntries(
252
+ Object.entries(o.vars).map(([k, v]) => [k, typeof v === "string" ? v : JSON.stringify(v)])
253
+ );
254
+ const vars = environmentScope(config, o.env, { ...fileVars, ...given });
255
+ const transport = transportFor(ctx, o.env, vars);
256
+ const origins = originsFor(config, o.env, vars);
257
+ const addresses = originsFor(config, o.env, vars, true);
258
+ const faker = await ctx.loadFaker();
259
+ const git = { ...ctx.git ?? gitInfo(ctx.root) };
260
+ const ci = detectCi(process.env);
261
+ if (ci?.sha) git.sha = ci.sha.slice(0, 7);
262
+ if (ci?.branch) git.branch = ci.branch;
263
+ const bundle = ctx.bundle;
264
+ const colors = createColors(colorEnabled(process.env, !!process.stdout.isTTY));
265
+ const readSource = ctx.readSource ?? ((file) => {
266
+ try {
267
+ return readFileSync(file, "utf8");
268
+ } catch {
269
+ return void 0;
270
+ }
271
+ });
272
+ const listOptions = {
273
+ colors,
274
+ quiet: o.quiet,
275
+ trace: o.trace,
276
+ origins: addresses,
277
+ cwd: ctx.root,
278
+ readSource,
279
+ rerun: (r) => `${ctx.command}${r.file && ctx.command === "specbook test" ? ` ${r.file}` : ""} --env ${o.env} --grep ${quote(escapeRegExp(r.title))}${o.safe ? " --safe" : ""}`
280
+ };
281
+ const startedAt = /* @__PURE__ */ new Date();
282
+ const list = specs.find((s) => s.kind === "list");
283
+ const streaming = list && !list.path ? createListReporter({ ...listOptions, write: (s) => process.stdout.write(s) }) : void 0;
284
+ const ndjson = specs.find((s) => s.kind === "ndjson");
285
+ const ndjsonLines = [];
286
+ let flow = "";
287
+ const onEvent = (e) => {
288
+ streaming?.onEvent(e);
289
+ if (e.type === "run:start") flow = e.title ?? e.id;
290
+ if (ndjson && e.type !== "done") {
291
+ const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), flow, ...redactEvent(e) });
292
+ if (ndjson.path) ndjsonLines.push(line);
293
+ else process.stdout.write(`${line}
294
+ `);
295
+ }
296
+ };
297
+ if (streaming && !o.quiet) {
298
+ const where = [
299
+ git.branch && git.sha ? `${git.branch}@${git.sha}` : git.sha ?? "",
300
+ bundle ? ["bundle", bundle.sha, `built ${bundle.createdAt.slice(0, 10)}`].filter(Boolean).join(" ") : ""
301
+ ].filter(Boolean);
302
+ process.stdout.write(
303
+ headerLine(
304
+ { version: cliVersion(), env: o.env, picked: picked.length, total, where },
305
+ colors
306
+ )
307
+ );
308
+ }
309
+ const { results, notRun, stopped } = await runSuite(picked, {
310
+ run: {
311
+ env: o.env,
312
+ vars,
313
+ overrides: o.vars,
314
+ inputs: o.inputs,
315
+ transport,
316
+ secrets,
317
+ auth: authOf(config),
318
+ serviceAuth: serviceAuthOf(config),
319
+ locale: o.locale,
320
+ seed: o.seed,
321
+ onEvent,
322
+ example: ctx.examples.example,
323
+ readFile: ctx.examples.readFile,
324
+ readOnly: o.safe === true,
325
+ implicitStatusCheck: true,
326
+ // the flag, else the project's own, else the runner's 30 seconds
327
+ timeoutMs: o.timeoutMs ?? config.timeoutMs
328
+ },
329
+ retries: o.retries,
330
+ bail: o.bail,
331
+ globalTimeoutMs: o.globalTimeoutMs,
332
+ beforeAttempt: () => {
333
+ if (faker) installFaker(faker, config.faker, { locale: o.locale, seed: o.seed });
334
+ },
335
+ onRetry: (t, attempt, allowed) => {
336
+ if (streaming) process.stdout.write(retryLine(colors, t.title, attempt, allowed));
337
+ if (ndjson && !ndjson.path)
338
+ process.stdout.write(
339
+ `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), type: "retry", flow: t.title, attempt })}
340
+ `
341
+ );
342
+ }
343
+ });
344
+ const finishedAt = /* @__PURE__ */ new Date();
345
+ const meta = {
346
+ env: o.env,
347
+ specbook: cliVersion(),
348
+ node: process.version,
349
+ os: `${process.platform}-${process.arch}`,
350
+ git: git.sha || git.branch ? git : void 0,
351
+ ci,
352
+ bundle: bundle ? { sha: bundle.sha, createdAt: bundle.createdAt } : void 0,
353
+ services: origins,
354
+ startedAt: startedAt.toISOString(),
355
+ finishedAt: finishedAt.toISOString()
356
+ };
357
+ const clean = redactResults(ctx.hideLocations ? withoutLocations(results) : results);
358
+ const write = async (path, text) => {
359
+ if (!path) return process.stdout.write(text.endsWith("\n") ? text : `${text}
360
+ `);
361
+ await mkdir(dirname2(resolve2(path)), { recursive: true });
362
+ await writeFile(resolve2(path), text.endsWith("\n") ? text : `${text}
363
+ `);
364
+ };
365
+ for (const s of specs) {
366
+ if (s.kind === "list") {
367
+ if (streaming) streaming.onEnd(clean);
368
+ else
369
+ await write(
370
+ s.path,
371
+ formatList(clean, {
372
+ ...listOptions,
373
+ colors: createColors(false),
374
+ durationMs: finishedAt.getTime() - startedAt.getTime()
375
+ })
376
+ );
377
+ } else if (s.kind === "json") {
378
+ await write(s.path, JSON.stringify(buildReport(clean, meta), null, 2));
379
+ } else if (s.kind === "junit") {
380
+ await write(
381
+ s.path,
382
+ toJUnit(clean, {
383
+ timestamp: meta.startedAt,
384
+ hostname: hostname(),
385
+ origins: addresses,
386
+ properties: { env: o.env, sha: git.sha, branch: git.branch, specbook: meta.specbook }
387
+ })
388
+ );
389
+ } else if (s.kind === "github") {
390
+ await write(s.path, formatGithub(clean, { cwd: ctx.root }));
391
+ const summary = process.env.GITHUB_STEP_SUMMARY;
392
+ if (summary && process.env.GITHUB_ACTIONS === "true")
393
+ appendFileSync(summary, githubSummary(clean, { env: o.env, runUrl: ci?.runUrl }));
394
+ } else if (s.kind === "ndjson" && s.path) {
395
+ const totals = buildReport(clean, meta).totals;
396
+ ndjsonLines.push(JSON.stringify({ ts: meta.finishedAt, type: "summary", totals }));
397
+ await write(s.path, ndjsonLines.join("\n"));
398
+ }
399
+ }
400
+ if (ndjson && !ndjson.path) {
401
+ process.stdout.write(
402
+ `${JSON.stringify({ ts: meta.finishedAt, type: "summary", totals: buildReport(clean, meta).totals })}
403
+ `
404
+ );
405
+ }
406
+ if (stopped) console.error(`${colors.yellow(stopped)}${notRun ? `: ${notRun} not run` : ""}`);
407
+ const code = stopped ? 1 : testExitCode(results);
408
+ if (o.notify && code !== 0) {
409
+ const failed = results.filter((r) => r.status !== "pass");
410
+ try {
411
+ await fetch(o.notify, {
412
+ method: "POST",
413
+ headers: { "content-type": "application/json" },
414
+ body: JSON.stringify({
415
+ text: `specbook: ${failed.length}/${results.length} runs failed in ${o.env}: ${failed.map((r) => r.title).join(", ")}`,
416
+ env: o.env,
417
+ failed: failed.map((r) => ({
418
+ id: r.id,
419
+ title: r.title,
420
+ status: r.status,
421
+ totals: r.totals
422
+ }))
423
+ })
424
+ });
425
+ } catch (e) {
426
+ console.error(`notify failed: ${e instanceof Error ? e.message : e}`);
427
+ }
428
+ }
429
+ return code;
430
+ }
431
+
432
+ // src/test-args.ts
433
+ var UsageError = class extends Error {
434
+ };
435
+ var TEST_FLAGS = {
436
+ env: { type: "string", default: "mock" },
437
+ input: { type: "string", multiple: true },
438
+ var: { type: "string", multiple: true },
439
+ tag: { type: "string", multiple: true },
440
+ reporter: { type: "string", multiple: true },
441
+ "secrets-file": { type: "string" },
442
+ locale: { type: "string" },
443
+ seed: { type: "string" },
444
+ notify: { type: "string" },
445
+ grep: { type: "string" },
446
+ "grep-invert": { type: "string" },
447
+ list: { type: "boolean" },
448
+ shard: { type: "string" },
449
+ bail: { type: "string" },
450
+ retries: { type: "string" },
451
+ timeout: { type: "string" },
452
+ "global-timeout": { type: "string" },
453
+ safe: { type: "boolean" },
454
+ trace: { type: "string" },
455
+ quiet: { type: "boolean" }
456
+ };
457
+ var TEST_HELP = ` --env <name> environment (default: mock, for development)
458
+ --tag <tag> only requests/flows with this tag (repeatable)
459
+ --grep <re> --grep-invert <re> select by file or title
460
+ --list print what would run
461
+ --shard <i/n> run the i-th of n equal parts
462
+ --bail <n> stop after n failed flows
463
+ --retries <n> run a failed flow again up to n times (a pass is "flaky")
464
+ --timeout <ms> per request (default: the config's timeoutMs, else 30000; 0 waits)
465
+ --global-timeout <ms> the whole run
466
+ --safe refuse requests that may change data (use against production)
467
+ --reporter <r[:path]> list (default) | json | junit | github | ndjson (repeatable)
468
+ --trace <when> on-failure (default) | always | off: the request and response of a step
469
+ --quiet only failures and the summary
470
+ --input k=v flow input value (repeatable)
471
+ --var k=v variable override (repeatable)
472
+ --secrets-file <path> dotenv file with secrets
473
+ --locale <l> --seed <n> synthetic data locale / deterministic seed
474
+ --notify <url> POST a summary to a webhook when something fails`;
475
+ var parseValue = (v) => {
476
+ try {
477
+ return JSON.parse(v);
478
+ } catch {
479
+ return v;
480
+ }
481
+ };
482
+ var kv = (list = []) => Object.fromEntries(
483
+ list.map((s) => {
484
+ const i = s.indexOf("=");
485
+ return [s.slice(0, i), parseValue(s.slice(i + 1))];
486
+ })
487
+ );
488
+ var number = (name, v) => {
489
+ if (v === void 0) return void 0;
490
+ const n = Number(v);
491
+ if (!Number.isFinite(n) || n < 0) throw new UsageError(`--${name} takes a number, got "${v}"`);
492
+ return n;
493
+ };
494
+ function testOptions(values, patterns) {
495
+ if (values.trace && !["on-failure", "always", "off"].includes(values.trace))
496
+ throw new UsageError("--trace takes on-failure, always or off");
497
+ return {
498
+ patterns,
499
+ env: values.env,
500
+ inputs: kv(values.input),
501
+ vars: kv(values.var),
502
+ reporters: values.reporter ?? [],
503
+ secretsFile: values["secrets-file"],
504
+ tags: values.tag ?? [],
505
+ grep: values.grep,
506
+ grepInvert: values["grep-invert"],
507
+ list: values.list,
508
+ shard: values.shard,
509
+ bail: number("bail", values.bail),
510
+ retries: number("retries", values.retries),
511
+ timeoutMs: number("timeout", values.timeout),
512
+ globalTimeoutMs: number("global-timeout", values["global-timeout"]),
513
+ safe: values.safe,
514
+ trace: values.trace,
515
+ quiet: values.quiet,
516
+ locale: values.locale,
517
+ seed: values.seed ? Number(values.seed) : void 0,
518
+ notify: values.notify
519
+ };
520
+ }
521
+
522
+ // src/standalone.ts
523
+ var HELP = (name) => `${name} [options] run the tests built into this file
524
+
525
+ ${TEST_HELP}
526
+ --help this text
527
+
528
+ Exit codes: 0 pass, 1 a test failed or errored, 2 could not run (secrets, config), 3 usage.
529
+ There is no mock environment here: name a real one with --env. Secrets come from environment variables.`;
530
+ async function main(data, argv = process.argv.slice(2)) {
531
+ fixCliVersion(data.specbook);
532
+ const name = `node ${basename(process.argv[1] ?? "tests.mjs")}`;
533
+ const options = { ...TEST_FLAGS, help: { type: "boolean", short: "h" } };
534
+ const parse = () => parseArgs({ args: argv, options, allowPositionals: true });
535
+ let parsed;
536
+ try {
537
+ parsed = parse();
538
+ } catch (e) {
539
+ console.error(`${e instanceof Error ? e.message : e}
540
+
541
+ ${HELP(name)}`);
542
+ return 3;
543
+ }
544
+ if (parsed.values.help) {
545
+ console.log(HELP(name));
546
+ return 0;
547
+ }
548
+ let o;
549
+ try {
550
+ o = testOptions(parsed.values, parsed.positionals);
551
+ } catch (e) {
552
+ if (!(e instanceof UsageError)) throw e;
553
+ console.error(e.message);
554
+ return 3;
555
+ }
556
+ const environments = environmentNames(data.config, Object.keys(data.environments));
557
+ const targets = data.modules.flatMap((m) => targetsOf(m.mod, m.file));
558
+ const example = async (op, exampleName) => {
559
+ const hit = data.examples[`${op.service}/${op.id}/${exampleName}`];
560
+ return hit ? { ...hit.doc, source: hit.source, dir: hit.dir } : void 0;
561
+ };
562
+ const readFile2 = async (path, dir) => {
563
+ const key = posix.normalize(posix.join(dir ?? "", path));
564
+ const b64 = data.files[key];
565
+ if (b64 === void 0) throw new Error(`File "${key}" is not in this bundle`);
566
+ return new File([Buffer.from(b64, "base64")], posix.basename(key));
567
+ };
568
+ return runTests(
569
+ {
570
+ command: name,
571
+ root: process.cwd(),
572
+ config: data.config,
573
+ targets,
574
+ environments,
575
+ readEnvironment: async (env) => ({
576
+ vars: data.environments[env] ?? {},
577
+ secrets: {},
578
+ shared: {}
579
+ }),
580
+ examples: { example, readFile: readFile2 },
581
+ loadFaker: async () => data.faker,
582
+ bundle: { sha: data.git?.sha, createdAt: data.createdAt },
583
+ git: {},
584
+ readSource: () => void 0,
585
+ hideLocations: true,
586
+ secretFiles: false
587
+ },
588
+ o
589
+ );
590
+ }
591
+ export {
592
+ main
593
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@specbook/cli",
3
- "version": "0.2.0",
4
- "description": "Specbook command line: init, add, generate, dev, build, run, check, secrets and preview.",
3
+ "version": "0.4.0",
4
+ "description": "Specbook command line: init, add, generate, dev, build, test, bundle, check, secrets and preview.",
5
5
  "keywords": [
6
6
  "specbook",
7
7
  "openapi",
@@ -32,16 +32,18 @@
32
32
  },
33
33
  "files": [
34
34
  "dist",
35
+ "agent-guide.md",
35
36
  "!**/*.test.*"
36
37
  ],
37
38
  "dependencies": {
38
39
  "@apidevtools/swagger-parser": "10.1.1",
39
40
  "@faker-js/faker": "9.3.0",
40
- "@specbook/codegen": "0.2.0",
41
- "@specbook/core": "0.2.0",
42
- "@specbook/ui": "0.2.0",
43
- "@specbook/vite": "0.2.0",
41
+ "@specbook/codegen": "0.4.0",
42
+ "@specbook/core": "0.4.0",
43
+ "@specbook/ui": "0.4.0",
44
+ "@specbook/vite": "0.4.0",
44
45
  "fast-glob": "3.3.2",
46
+ "rolldown": "1.2.13",
45
47
  "swagger2openapi": "7.0.8",
46
48
  "vite": "8.3.4",
47
49
  "vite-node": "6.0.0"