@specbook/cli 0.4.0 → 0.5.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/agent-guide.md CHANGED
@@ -106,7 +106,7 @@ export default sequence({
106
106
  - `request.with({ ... })` changes a request for one step (for example its `assert`).
107
107
  - Also: `.input(name, ctx => controls)` asks a person for values, `.each(name, items, make)` repeats a request per item, `.check(name, fn)` asserts on the flow's state, `.use(otherFlow)` includes another flow.
108
108
  - Controls are `c.number`, `c.text`, `c.select`, `c.multiselect`, `c.boolean`, `c.date`, `c.object`, `c.array`. A flow also runs unattended in CI and as a monitor, so every input and every `.input()` control needs a default that makes sense with nobody to ask.
109
- - For generated data: `import { faker } from '@specbook/core/faker'` and use it in a default, for example `c.text({ default: () => faker.person.fullName() })`.
109
+ - For generated data: `import { faker } from '@specbook/core/faker'` and use it in a default, for example `c.text({ default: () => faker.person.fullName() })`. It is typed, and `specbook init` installs `@faker-js/faker`. A `{{faker.person.firstName}}` placeholder (`faker.<module>.<method>`, no arguments) also works in a field.
110
110
  - Each step is also a request you can run alone.
111
111
 
112
112
  ### Recipe: list, check, pick, act
package/dist/bin.js CHANGED
@@ -243,35 +243,6 @@ function exampleBoolean(key) {
243
243
  key.toLowerCase()
244
244
  );
245
245
  }
246
- var FAKE_BY_FORMAT = {
247
- email: "internet.email",
248
- uuid: "string.uuid",
249
- uri: "internet.url",
250
- url: "internet.url",
251
- hostname: "internet.domainName",
252
- ipv4: "internet.ipv4"
253
- };
254
- var FAKE_BY_NAME = [
255
- [/^(first_?name|given_?name)$/, "person.firstName"],
256
- [/^(last_?name|family_?name|surname)$/, "person.lastName"],
257
- [/^(full_?name|display_?name)$/, "person.fullName"],
258
- [/^name$/, "word.noun"],
259
- [/^(user_?name|login|handle)$/, "internet.username"],
260
- [/^e?-?mail(_?address)?$/, "internet.email"],
261
- [/^(phone|phone_?number|mobile)$/, "phone.number"],
262
- [/^(city|town)$/, "location.city"],
263
- [/^country$/, "location.country"],
264
- [/^(postal_?code|zip|zip_?code)$/, "location.zipCode"],
265
- [/^(line1|address|street|address_?line_?1)$/, "location.streetAddress"],
266
- [/^(company|organization|org)$/, "company.name"],
267
- [/^(title|subject)$/, "lorem.sentence"],
268
- [/^(description|summary|bio|about|body|comment|note|notes|message)$/, "lorem.sentence"],
269
- [/^(url|website|homepage)$/, "internet.url"]
270
- ];
271
- function fakerFor(key, format) {
272
- const path = format && FAKE_BY_FORMAT[format] || FAKE_BY_NAME.find(([re2]) => re2.test(key.toLowerCase()))?.[1];
273
- return path ? `{{faker.${path}}}` : void 0;
274
- }
275
246
  var MAX_DEPTH = 6;
276
247
  var PLACEHOLDER_VALUE = /^(string|str|<string>|\{string\}|string\d+|null)$/i;
277
248
  function sanitizeExample(value, key = "", format, status) {
@@ -308,18 +279,14 @@ function fits(schema, value) {
308
279
  }
309
280
  }
310
281
  var HARD_DEPTH = 14;
311
- function synthesize(schema, resolve16, depth = 0, seen = [], key = "", status, fake = false, sparse = false) {
282
+ function synthesize(schema, resolve16, depth = 0, seen = [], key = "", status, sparse = false) {
312
283
  if (!schema) return null;
313
- const again = (s, k = key, d = depth, sn = seen) => synthesize(s, resolve16, d, sn, k, status, fake, sparse);
284
+ const again = (s, k = key, d = depth, sn = seen) => synthesize(s, resolve16, d, sn, k, status, sparse);
314
285
  if (schema.$ref) {
315
286
  const target = resolve16(schema.$ref);
316
287
  if (seen.includes(schema.$ref)) return again(target, key, Math.max(depth, MAX_DEPTH), []);
317
288
  return again(target, key, depth, [...seen, schema.$ref]);
318
289
  }
319
- if (fake && schema.type === "string" && !schema.enum) {
320
- const generated = fakerFor(key, schema.format);
321
- if (generated) return generated;
322
- }
323
290
  if (schema.example !== void 0 && fits(schema, schema.example))
324
291
  return schema.enum ? schema.example : sanitizeExample(schema.example, key, schema.format, status);
325
292
  if (schema.default !== void 0 && fits(schema, schema.default)) return schema.default;
@@ -725,13 +692,12 @@ ${typeBlocks.join("\n\n") || "export {}"}
725
692
  const fileFieldPaths = body2?.encoding === "multipart" ? fileFields(body2.schema, resolve16) : [];
726
693
  const wholeBodyFile = body2?.encoding === "binary";
727
694
  const sample = `./${SAMPLE_FILE}`;
728
- const paramsFor = (fake) => {
695
+ const paramsFor = () => {
729
696
  const pick = (where) => {
730
697
  const out = {};
731
698
  for (const p of group(where)) {
732
699
  if (p.example !== void 0) out[p.name] = p.example;
733
- else if (p.required)
734
- out[p.name] = synthesize(p.schema, resolve16, 0, [], p.name, void 0, fake);
700
+ else if (p.required) out[p.name] = synthesize(p.schema, resolve16, 0, [], p.name);
735
701
  }
736
702
  return Object.keys(out).length ? out : void 0;
737
703
  };
@@ -752,7 +718,7 @@ ${typeBlocks.join("\n\n") || "export {}"}
752
718
  title: ex.summary ?? key,
753
719
  summary: ex.description,
754
720
  source: "spec",
755
- ...paramsFor(false),
721
+ ...paramsFor(),
756
722
  body: bodyFor(sanitizeExample(ex.value))
757
723
  });
758
724
  }
@@ -761,26 +727,14 @@ ${typeBlocks.join("\n\n") || "export {}"}
761
727
  name: take("example"),
762
728
  title: "Example",
763
729
  source: "spec",
764
- ...paramsFor(false),
730
+ ...paramsFor(),
765
731
  body: bodyFor(sanitizeExample(body2.media.example))
766
732
  });
767
733
  const plain = {
768
- ...paramsFor(false),
769
- body: body2?.schema ? bodyFor(synthesize(body2.schema, resolve16, 0, [], "", void 0, false, true)) : void 0
770
- };
771
- const dynamic = {
772
- ...paramsFor(true),
773
- body: body2?.schema ? bodyFor(synthesize(body2.schema, resolve16, 0, [], "", void 0, true, true)) : void 0
734
+ ...paramsFor(),
735
+ body: body2?.schema ? bodyFor(synthesize(body2.schema, resolve16, 0, [], "", void 0, true)) : void 0
774
736
  };
775
737
  docs.push({ name: take("default"), title: "Default", source: "generated", ...plain });
776
- if (JSON.stringify(dynamic) !== JSON.stringify(plain))
777
- docs.push({
778
- name: take("dynamic"),
779
- title: "Dynamic",
780
- summary: "Generated values, fresh on every play.",
781
- source: "generated",
782
- ...dynamic
783
- });
784
738
  for (const d of docs)
785
739
  exampleFiles.push({
786
740
  path: `examples/${id}/${d.name}.example.json`,
@@ -2108,7 +2062,7 @@ function diffAssertion(a3, context = 3) {
2108
2062
  return [];
2109
2063
  }
2110
2064
 
2111
- // ../core/src/faker.ts
2065
+ // ../core/src/faker-runtime.ts
2112
2066
  var KEY = "__specbookFaker";
2113
2067
  var store = globalThis;
2114
2068
  var faker = new Proxy({}, {
@@ -2129,6 +2083,12 @@ function fakerLocaleNames(config = {}, locale) {
2129
2083
  const name = locale ?? config.default ?? "en";
2130
2084
  return [.../* @__PURE__ */ new Set([...config.locales?.[name] ?? [name, "en"], "base"])];
2131
2085
  }
2086
+ function fakerLocaleExports(config = {}) {
2087
+ const names = /* @__PURE__ */ new Set(["en", ...fakerLocaleNames(config)]);
2088
+ for (const name of Object.keys(config.locales ?? {}))
2089
+ for (const n of fakerLocaleNames(config, name)) names.add(n);
2090
+ return [...names].sort();
2091
+ }
2132
2092
  function installFaker(mod, config = {}, opts = {}) {
2133
2093
  const name = opts.locale ?? config.default ?? "en";
2134
2094
  const locales = fakerLocaleNames(config, name).map((n) => {
@@ -2161,15 +2121,25 @@ function isDeferred(name) {
2161
2121
  return DEFERRED_NAMESPACES.includes(ns) && name.includes(".");
2162
2122
  }
2163
2123
  var isFakerName = (name) => name.startsWith("faker.");
2164
- function fromFaker(path) {
2165
- let parent = faker;
2166
- let cur = faker;
2167
- for (const part of path.split(".").slice(1)) {
2168
- parent = cur;
2169
- cur = cur?.[part];
2170
- if (cur === void 0) return { found: false };
2124
+ var FakerPlaceholderError = class extends TemplateError {
2125
+ constructor(variable, message) {
2126
+ super(variable);
2127
+ this.message = message;
2128
+ this.name = "FakerPlaceholderError";
2171
2129
  }
2172
- return { found: true, value: typeof cur === "function" ? cur.call(parent) : cur };
2130
+ message;
2131
+ };
2132
+ function fromFaker(path) {
2133
+ const [, group, method, ...rest] = path.split(".");
2134
+ const unknown = () => new FakerPlaceholderError(
2135
+ path,
2136
+ `Unknown faker generator "{{${path}}}": use faker.<module>.<method>`
2137
+ );
2138
+ if (!group || !method || rest.length > 0) throw unknown();
2139
+ const mod = faker[group];
2140
+ const fn = typeof mod === "object" && mod !== null ? mod[method] : void 0;
2141
+ if (typeof fn !== "function" || method === "constructor") throw unknown();
2142
+ return { found: true, value: fn.call(mod) };
2173
2143
  }
2174
2144
  var steps = (path) => path.replace(/\[(\d+)\]/g, ".$1").split(".");
2175
2145
  function lookup(scope, path) {
@@ -2184,7 +2154,19 @@ function lookup(scope, path) {
2184
2154
  }
2185
2155
  return { found: true, value: cur };
2186
2156
  }
2157
+ function rejectFakerCall(s) {
2158
+ if (!s.includes("faker")) return;
2159
+ const left = /\{\{\s*(faker\.[\s\S]*?)(?:\}\}|$)/.exec(s.replace(EMBED, ""));
2160
+ if (left) {
2161
+ const name = left[1].trim();
2162
+ throw new FakerPlaceholderError(
2163
+ name,
2164
+ `"{{${name}}}" is not a faker placeholder: placeholders take no arguments. Use faker in a request file for those.`
2165
+ );
2166
+ }
2167
+ }
2187
2168
  function renderString(s, scope) {
2169
+ rejectFakerCall(s);
2188
2170
  const whole = WHOLE.exec(s);
2189
2171
  if (whole) {
2190
2172
  const name = whole[1];
@@ -2944,21 +2926,21 @@ function formatSummary(results, o = {}) {
2944
2926
  const c = o.colors ?? createColors(false);
2945
2927
  const t = reportTotals(results);
2946
2928
  const part = (n, word, paintFn) => n ? paintFn(`${n} ${word}`) : "";
2947
- const join19 = (...parts) => parts.filter(Boolean).join(c.dim(" | ")) || c.dim("none");
2929
+ const join20 = (...parts) => parts.filter(Boolean).join(c.dim(" | ")) || c.dim("none");
2948
2930
  const lines2 = [
2949
2931
  "",
2950
- `${c.bold(" Flows ")} ${join19(
2932
+ `${c.bold(" Flows ")} ${join20(
2951
2933
  part(t.flows.failed, "failed", c.red),
2952
2934
  part(t.flows.errored, "errored", c.yellow),
2953
2935
  part(t.flows.flaky, "flaky", c.yellow),
2954
2936
  part(t.flows.passed, "passed", c.green)
2955
2937
  )} ${c.dim(`(${results.length})`)}`,
2956
- `${c.bold(" Steps ")} ${join19(
2938
+ `${c.bold(" Steps ")} ${join20(
2957
2939
  part(t.steps.failed, "failed", c.red),
2958
2940
  part(t.steps.skipped, "skipped", c.dim),
2959
2941
  part(t.steps.passed, "passed", c.green)
2960
2942
  )}`,
2961
- `${c.bold(" Assertions ")} ${join19(
2943
+ `${c.bold(" Assertions ")} ${join20(
2962
2944
  part(t.assertions.failed, "failed", c.red),
2963
2945
  part(t.assertions.passed, "passed", c.green)
2964
2946
  )}`
@@ -5501,6 +5483,16 @@ var faker2 = new Proxy({}, {
5501
5483
  return current[key];
5502
5484
  }
5503
5485
  });
5486
+ function fakerLocaleNames2(config = {}, locale) {
5487
+ const name = locale ?? config.default ?? "en";
5488
+ return [.../* @__PURE__ */ new Set([...config.locales?.[name] ?? [name, "en"], "base"])];
5489
+ }
5490
+ function fakerLocaleExports2(config = {}) {
5491
+ const names = /* @__PURE__ */ new Set(["en", ...fakerLocaleNames2(config)]);
5492
+ for (const name of Object.keys(config.locales ?? {}))
5493
+ for (const n of fakerLocaleNames2(config, name)) names.add(n);
5494
+ return [...names].sort();
5495
+ }
5504
5496
  var NAME2 = String.raw`[\w.$\[\]-]+`;
5505
5497
  var WHOLE2 = new RegExp(String.raw`^\s*\{\{\s*(${NAME2})\s*\}\}\s*$`);
5506
5498
  var EMBED2 = new RegExp(String.raw`\{\{\s*(${NAME2})\s*\}\}`, "g");
@@ -5518,15 +5510,25 @@ function isDeferred2(name) {
5518
5510
  return DEFERRED_NAMESPACES2.includes(ns) && name.includes(".");
5519
5511
  }
5520
5512
  var isFakerName2 = (name) => name.startsWith("faker.");
5521
- function fromFaker2(path) {
5522
- let parent = faker2;
5523
- let cur = faker2;
5524
- for (const part of path.split(".").slice(1)) {
5525
- parent = cur;
5526
- cur = cur?.[part];
5527
- if (cur === void 0) return { found: false };
5513
+ var FakerPlaceholderError2 = class extends TemplateError2 {
5514
+ constructor(variable, message) {
5515
+ super(variable);
5516
+ this.message = message;
5517
+ this.name = "FakerPlaceholderError";
5528
5518
  }
5529
- return { found: true, value: typeof cur === "function" ? cur.call(parent) : cur };
5519
+ message;
5520
+ };
5521
+ function fromFaker2(path) {
5522
+ const [, group, method, ...rest] = path.split(".");
5523
+ const unknown = () => new FakerPlaceholderError2(
5524
+ path,
5525
+ `Unknown faker generator "{{${path}}}": use faker.<module>.<method>`
5526
+ );
5527
+ if (!group || !method || rest.length > 0) throw unknown();
5528
+ const mod = faker2[group];
5529
+ const fn = typeof mod === "object" && mod !== null ? mod[method] : void 0;
5530
+ if (typeof fn !== "function" || method === "constructor") throw unknown();
5531
+ return { found: true, value: fn.call(mod) };
5530
5532
  }
5531
5533
  var steps2 = (path) => path.replace(/\[(\d+)\]/g, ".$1").split(".");
5532
5534
  function lookup2(scope, path) {
@@ -5541,7 +5543,19 @@ function lookup2(scope, path) {
5541
5543
  }
5542
5544
  return { found: true, value: cur };
5543
5545
  }
5546
+ function rejectFakerCall2(s) {
5547
+ if (!s.includes("faker")) return;
5548
+ const left = /\{\{\s*(faker\.[\s\S]*?)(?:\}\}|$)/.exec(s.replace(EMBED2, ""));
5549
+ if (left) {
5550
+ const name = left[1].trim();
5551
+ throw new FakerPlaceholderError2(
5552
+ name,
5553
+ `"{{${name}}}" is not a faker placeholder: placeholders take no arguments. Use faker in a request file for those.`
5554
+ );
5555
+ }
5556
+ }
5544
5557
  function renderString2(s, scope) {
5558
+ rejectFakerCall2(s);
5545
5559
  const whole = WHOLE2.exec(s);
5546
5560
  if (whole) {
5547
5561
  const name = whole[1];
@@ -6221,7 +6235,7 @@ async function scanExamples(root, outDir, operations) {
6221
6235
  list.sort((a3, b) => order(a3) - order(b) || a3.name.localeCompare(b.name));
6222
6236
  return { examples, assets };
6223
6237
  }
6224
- var order = (e) => e.source === "project" ? 0 : e.name === "default" ? 2 : e.name === "dynamic" ? 3 : 1;
6238
+ var order = (e) => e.source === "project" ? 0 : e.name === "default" ? 2 : 1;
6225
6239
  var IGNORE2 = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
6226
6240
  async function scanProject(root) {
6227
6241
  const rel2 = (e) => e.map((f2) => relative3(root, f2).split(sep3).join("/")).sort();
@@ -6508,7 +6522,7 @@ async function readBody2(req) {
6508
6522
  req.on("error", fail);
6509
6523
  });
6510
6524
  }
6511
- function fakerLocales(entry) {
6525
+ function fakerLocales(entry, cfg) {
6512
6526
  const names = /* @__PURE__ */ new Set(["en", "base"]);
6513
6527
  const dir = join5(dirname2(entry), "locale");
6514
6528
  if (existsSync3(dir))
@@ -6516,8 +6530,11 @@ function fakerLocales(entry) {
6516
6530
  const m = /^([A-Za-z0-9_]+)\.js$/.exec(f2);
6517
6531
  if (m) names.add(m[1]);
6518
6532
  }
6519
- return [...names].sort();
6533
+ if (!cfg) return [...names].sort();
6534
+ const wanted = new Set(fakerLocaleExports2(cfg));
6535
+ return [...names].filter((n) => wanted.has(n)).sort();
6520
6536
  }
6537
+ var USES_FAKER = /@specbook\/core\/faker|\{\{\s*faker\./;
6521
6538
  function slim(manifests) {
6522
6539
  return Object.fromEntries(
6523
6540
  Object.entries(manifests).map(([k, m]) => [
@@ -6616,6 +6633,14 @@ function specbook(options) {
6616
6633
  fakerEntry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
6617
6634
  } catch {
6618
6635
  }
6636
+ if (!fakerEntry) {
6637
+ for (const f2 of [...files.requests, ...files.sequences]) {
6638
+ if (USES_FAKER.test(await readFile6(join5(root, f2), "utf8").catch(() => "")))
6639
+ throw new Error(
6640
+ `${f2} uses faker, but @faker-js/faker is not installed in this project. Add it to devDependencies (npm install --save-dev @faker-js/faker).`
6641
+ );
6642
+ }
6643
+ }
6619
6644
  const operationEntries = Object.values(manifests).flatMap((m) => {
6620
6645
  const mm = m;
6621
6646
  return mm.operations.map(
@@ -6987,7 +7012,10 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
6987
7012
  const r = await this.resolve("@faker-js/faker", join5(root, "package.json"), {
6988
7013
  skipSelf: true
6989
7014
  });
6990
- if (!r) return "export {}";
7015
+ if (!r)
7016
+ throw new Error(
7017
+ "@faker-js/faker could not be resolved from this project. Add it to devDependencies."
7018
+ );
6991
7019
  const pkg = JSON.stringify(r.id);
6992
7020
  if (id.startsWith(RESOLVED_FAKER_LOCALE))
6993
7021
  return `export { ${id.slice(RESOLVED_FAKER_LOCALE.length)} as default } from ${pkg}`;
@@ -6996,7 +7024,7 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
6996
7024
  entry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
6997
7025
  } catch {
6998
7026
  }
6999
- const names = fakerLocales(entry);
7027
+ const names = fakerLocales(entry, options.faker);
7000
7028
  return `export { Faker, base } from ${pkg}
7001
7029
  export const locales = {
7002
7030
  ${names.map((n) => ` ${JSON.stringify(n)}: () => import(${JSON.stringify(`${VIRTUAL_FAKER_LOCALE}${n}`)}).then((m) => m.default)`).join(",\n")}
@@ -7484,16 +7512,44 @@ ${entry}`
7484
7512
  import { execFileSync as execFileSync3 } from "child_process";
7485
7513
  import { createHash as createHash2 } from "crypto";
7486
7514
  import { existsSync as existsSync7, statSync as statSync2 } from "fs";
7487
- import { chmod, mkdir as mkdir6, readdir as readdir3, readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
7515
+ import { chmod, mkdir as mkdir6, readdir as readdir3, readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
7488
7516
  import { createRequire as createRequire4 } from "module";
7489
- import { basename as basename4, dirname as dirname6, join as join12, relative as relative9, resolve as resolve9, sep as sep7 } from "path";
7517
+ import { basename as basename4, dirname as dirname6, join as join13, relative as relative9, resolve as resolve9, sep as sep7 } from "path";
7490
7518
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
7491
7519
  import { rolldown } from "rolldown";
7492
7520
 
7493
- // src/ops.ts
7521
+ // src/faker-use.ts
7494
7522
  import { readFile as readFile10 } from "fs/promises";
7495
- import { join as join9, relative as relative7, sep as sep5 } from "path";
7523
+ import { join as join9 } from "path";
7496
7524
  import fg6 from "fast-glob";
7525
+ var USES_FAKER2 = /@specbook\/core\/faker|\{\{\s*faker\./;
7526
+ var IGNORE3 = [
7527
+ "**/node_modules/**",
7528
+ "**/.generated/**",
7529
+ "**/dist/**",
7530
+ "**/.git/**",
7531
+ "**/openapi/**",
7532
+ "**/*.d.ts",
7533
+ "**/package.json",
7534
+ "**/package-lock.json",
7535
+ "**/tsconfig*.json"
7536
+ ];
7537
+ async function projectUsesFaker(root) {
7538
+ const files = await fg6(["**/*.ts", "**/*.json", ".specbook/**/*.env"], {
7539
+ cwd: root,
7540
+ ignore: IGNORE3,
7541
+ dot: true,
7542
+ absolute: false
7543
+ });
7544
+ for (const f2 of files)
7545
+ if (USES_FAKER2.test(await readFile10(join9(root, f2), "utf8").catch(() => ""))) return true;
7546
+ return false;
7547
+ }
7548
+
7549
+ // src/ops.ts
7550
+ import { readFile as readFile11 } from "fs/promises";
7551
+ import { join as join10, relative as relative7, sep as sep5 } from "path";
7552
+ import fg7 from "fast-glob";
7497
7553
  import { parseSync } from "vite";
7498
7554
  var prefixOf = (project) => (project.config.codegen?.importPrefix ?? "#api").replace(/\/+$/, "");
7499
7555
  var IMPORT_RE = /import\s+(type\s+)?(?:([\w$]+)\s*,?\s*)?(?:\*\s+as\s+[\w$]+|\{([^}]*)\})?\s*from\s*['"]([^'"]+)['"]/g;
@@ -7540,7 +7596,7 @@ function importedOperations(file, text, prefix = "#api") {
7540
7596
  return [...found.values()];
7541
7597
  }
7542
7598
  async function projectSources(root) {
7543
- return fg6(["**/*.ts", ".specbook/**/*.ts"], {
7599
+ return fg7(["**/*.ts", ".specbook/**/*.ts"], {
7544
7600
  cwd: root,
7545
7601
  absolute: true,
7546
7602
  dot: true,
@@ -7558,7 +7614,7 @@ async function usedOperations(project) {
7558
7614
  const out = { named: /* @__PURE__ */ new Map(), whole: /* @__PURE__ */ new Set(), where: /* @__PURE__ */ new Map() };
7559
7615
  const prefix = prefixOf(project);
7560
7616
  for (const file of (await projectSources(project.root)).sort()) {
7561
- const text = await readFile10(file, "utf8").catch(() => "");
7617
+ const text = await readFile11(file, "utf8").catch(() => "");
7562
7618
  if (!text.includes(prefix)) continue;
7563
7619
  const rel2 = relative7(project.root, file).split(sep5).join("/");
7564
7620
  for (const imp of importedOperations(file, text, prefix)) {
@@ -7593,10 +7649,10 @@ async function usedOperationKeys(project, outDir) {
7593
7649
  }
7594
7650
  return new Set([...keys].filter((k) => known.has(k)));
7595
7651
  }
7596
- var metaFile = (outDir, service) => join9(outDir, service, "opmeta.json");
7652
+ var metaFile = (outDir, service) => join10(outDir, service, "opmeta.json");
7597
7653
  async function readJson(file) {
7598
7654
  try {
7599
- return JSON.parse(await readFile10(file, "utf8"));
7655
+ return JSON.parse(await readFile11(file, "utf8"));
7600
7656
  } catch {
7601
7657
  return void 0;
7602
7658
  }
@@ -7630,11 +7686,11 @@ async function unknownOperationsMessage(project, outDir) {
7630
7686
  }
7631
7687
 
7632
7688
  // src/runtime.ts
7633
- import { readFile as readFile12 } from "fs/promises";
7689
+ import { readFile as readFile13 } from "fs/promises";
7634
7690
  import { createRequire as createRequire3 } from "module";
7635
- import { join as join11, relative as relative8, resolve as resolve8, sep as sep6 } from "path";
7691
+ import { join as join12, relative as relative8, resolve as resolve8, sep as sep6 } from "path";
7636
7692
  import { pathToFileURL } from "url";
7637
- import fg7 from "fast-glob";
7693
+ import fg8 from "fast-glob";
7638
7694
 
7639
7695
  // src/run-tests.ts
7640
7696
  import { execFileSync as execFileSync2 } from "child_process";
@@ -7645,8 +7701,8 @@ import { dirname as dirname5, resolve as resolve7 } from "path";
7645
7701
 
7646
7702
  // src/project.ts
7647
7703
  import { existsSync as existsSync6 } from "fs";
7648
- import { readFile as readFile11 } from "fs/promises";
7649
- import { dirname as dirname4, join as join10, resolve as resolve6 } from "path";
7704
+ import { readFile as readFile12 } from "fs/promises";
7705
+ import { dirname as dirname4, join as join11, resolve as resolve6 } from "path";
7650
7706
  var CANDIDATES = [
7651
7707
  ".specbook/specbook.config.ts",
7652
7708
  "specbook.config.ts",
@@ -7656,7 +7712,7 @@ function findConfigFile(cwd) {
7656
7712
  let dir = resolve6(cwd);
7657
7713
  for (; ; ) {
7658
7714
  for (const c of CANDIDATES) {
7659
- const p = join10(dir, c);
7715
+ const p = join11(dir, c);
7660
7716
  if (existsSync6(p)) return p;
7661
7717
  }
7662
7718
  const up = dirname4(dir);
@@ -7665,7 +7721,7 @@ function findConfigFile(cwd) {
7665
7721
  }
7666
7722
  }
7667
7723
  function rootOf(configFile) {
7668
- return configFile.endsWith(`${join10(".specbook", "specbook.config.ts")}`) ? dirname4(dirname4(configFile)) : dirname4(configFile);
7724
+ return configFile.endsWith(`${join11(".specbook", "specbook.config.ts")}`) ? dirname4(dirname4(configFile)) : dirname4(configFile);
7669
7725
  }
7670
7726
  async function loadProject(cwd, loader) {
7671
7727
  const configFile = findConfigFile(cwd);
@@ -7676,7 +7732,7 @@ async function loadProject(cwd, loader) {
7676
7732
  }
7677
7733
  async function readDotenv(path) {
7678
7734
  if (!existsSync6(path)) return {};
7679
- return parseDotenv(await readFile11(path, "utf8"));
7735
+ return parseDotenv(await readFile12(path, "utf8"));
7680
7736
  }
7681
7737
 
7682
7738
  // src/version.ts
@@ -8055,7 +8111,7 @@ async function execute2(ctx, o, picked, specs, fileVars, secrets) {
8055
8111
 
8056
8112
  // src/runtime.ts
8057
8113
  async function discoverTargets(project, loader, patterns) {
8058
- const files = (await fg7(patterns, {
8114
+ const files = (await fg8(patterns, {
8059
8115
  cwd: project.root,
8060
8116
  absolute: true,
8061
8117
  ignore: ["**/node_modules/**", "**/.generated/**"]
@@ -8074,7 +8130,7 @@ function mockLoaderFor(project) {
8074
8130
  const manifest = (service) => {
8075
8131
  let m = manifests.get(service);
8076
8132
  if (!m) {
8077
- m = readFile12(join11(outDir, service, "manifest.json"), "utf8").then(JSON.parse);
8133
+ m = readFile13(join12(outDir, service, "manifest.json"), "utf8").then(JSON.parse);
8078
8134
  manifests.set(service, m);
8079
8135
  }
8080
8136
  return m;
@@ -8088,8 +8144,8 @@ function mockLoaderFor(project) {
8088
8144
  for (const name of [String(status), "default"]) {
8089
8145
  try {
8090
8146
  return JSON.parse(
8091
- await readFile12(
8092
- join11(outDir, service, "examples", opId2, `${name}.response.json`),
8147
+ await readFile13(
8148
+ join12(outDir, service, "examples", opId2, `${name}.response.json`),
8093
8149
  "utf8"
8094
8150
  )
8095
8151
  );
@@ -8102,7 +8158,7 @@ function mockLoaderFor(project) {
8102
8158
  }
8103
8159
  async function loadFaker(project) {
8104
8160
  try {
8105
- const entry = createRequire3(join11(project.root, "package.json")).resolve("@faker-js/faker");
8161
+ const entry = createRequire3(join12(project.root, "package.json")).resolve("@faker-js/faker");
8106
8162
  return await import(pathToFileURL(entry).href);
8107
8163
  } catch {
8108
8164
  return void 0;
@@ -8130,9 +8186,9 @@ function git(root) {
8130
8186
  function standalonePath() {
8131
8187
  const here = dirname6(fileURLToPath(import.meta.url));
8132
8188
  for (const p of [
8133
- join12(here, "standalone.js"),
8134
- join12(here, "..", "standalone.js"),
8135
- join12(here, "..", "standalone.ts")
8189
+ join13(here, "standalone.js"),
8190
+ join13(here, "..", "standalone.js"),
8191
+ join13(here, "..", "standalone.ts")
8136
8192
  ])
8137
8193
  if (existsSync7(p)) return p;
8138
8194
  throw new Error("The test runner is missing from this installation of specbook");
@@ -8140,7 +8196,7 @@ function standalonePath() {
8140
8196
  async function walk(dir) {
8141
8197
  const entries = await readdir3(dir, { withFileTypes: true }).catch(() => []);
8142
8198
  return (await Promise.all(
8143
- entries.map((e) => e.isDirectory() ? walk(join12(dir, e.name)) : [join12(dir, e.name)])
8199
+ entries.map((e) => e.isDirectory() ? walk(join13(dir, e.name)) : [join13(dir, e.name)])
8144
8200
  )).flat();
8145
8201
  }
8146
8202
  var SKIP = /(^|\/)(node_modules|\.generated|\.git|dist|dist-[^/]+)(\/|$)/;
@@ -8148,15 +8204,15 @@ var CODE = /\.(ts|tsx|mts|cts|js|mjs|cjs|md)$/;
8148
8204
  async function embed(project, outDir, used) {
8149
8205
  const root = project.root;
8150
8206
  const out = { examples: {}, files: {}, bytes: 0 };
8151
- const read = async (file) => JSON.parse(await readFile13(file, "utf8"));
8207
+ const read = async (file) => JSON.parse(await readFile14(file, "utf8"));
8152
8208
  for (const [service, ops] of Object.entries(used))
8153
8209
  for (const id of Object.keys(ops)) {
8154
- const dir = join12(outDir, service, "examples", id);
8210
+ const dir = join13(outDir, service, "examples", id);
8155
8211
  for (const f2 of await readdir3(dir).catch(() => [])) {
8156
8212
  const m = /^(.+)\.example\.json$/.exec(f2);
8157
8213
  if (!m) continue;
8158
8214
  out.examples[`${service}/${id}/${m[1]}`] = {
8159
- doc: await read(join12(dir, f2)),
8215
+ doc: await read(join13(dir, f2)),
8160
8216
  source: "generated",
8161
8217
  dir: ""
8162
8218
  };
@@ -8181,24 +8237,21 @@ async function embed(project, outDir, used) {
8181
8237
  for (const abs of await walk(folder)) {
8182
8238
  const rel2 = posix(relative9(root, abs));
8183
8239
  if (SKIP.test(rel2) || CODE.test(rel2) || rel2.endsWith(".example.json")) continue;
8184
- const data2 = await readFile13(abs);
8240
+ const data2 = await readFile14(abs);
8185
8241
  out.files[rel2] = data2.toString("base64");
8186
8242
  out.bytes += data2.length;
8187
8243
  }
8188
8244
  return out;
8189
8245
  }
8190
- async function fakerSource(project, sources) {
8246
+ async function fakerSource(project) {
8191
8247
  let entry;
8192
8248
  try {
8193
- entry = createRequire4(join12(project.root, "package.json")).resolve("@faker-js/faker");
8249
+ entry = createRequire4(join13(project.root, "package.json")).resolve("@faker-js/faker");
8194
8250
  } catch {
8195
8251
  return void 0;
8196
8252
  }
8197
- if (!project.config.faker && !/faker/.test(sources)) return void 0;
8198
8253
  const cfg = project.config.faker ?? {};
8199
- const wanted = /* @__PURE__ */ new Set(["Faker", ...fakerLocaleNames(cfg)]);
8200
- for (const name of Object.keys(cfg.locales ?? {}))
8201
- for (const n of fakerLocaleNames(cfg, name)) wanted.add(n);
8254
+ const wanted = /* @__PURE__ */ new Set(["Faker", ...fakerLocaleExports(cfg)]);
8202
8255
  const have = new Set(Object.keys(await import(pathToFileURL2(entry).href)));
8203
8256
  const names = [...wanted].filter((n) => have.has(n)).sort();
8204
8257
  return `export { ${names.join(", ")} } from ${JSON.stringify(entry)}
@@ -8228,11 +8281,10 @@ async function bundleCommand(project, loader, o) {
8228
8281
  const files = [...new Set(chosen.map((t) => t.file))].sort();
8229
8282
  const ops = await usedMetadata(project, outDir);
8230
8283
  const sourceText = (await Promise.all(
8231
- [project.configFile, ...files.map((f2) => join12(root, f2))].map(
8232
- (f2) => readFile13(f2, "utf8").catch(() => "")
8284
+ [project.configFile, ...files.map((f2) => join13(root, f2))].map(
8285
+ (f2) => readFile14(f2, "utf8").catch(() => "")
8233
8286
  )
8234
8287
  )).join("\n");
8235
- const faker3 = await fakerSource(project, sourceText);
8236
8288
  const embedded = await embed(project, outDir, ops.services);
8237
8289
  const environments = {};
8238
8290
  for (const env of await listEnvironmentFiles(root))
@@ -8246,48 +8298,48 @@ async function bundleCommand(project, loader, o) {
8246
8298
  examples: embedded.examples,
8247
8299
  files: embedded.files
8248
8300
  };
8249
- const entry = [
8250
- `import { main } from ${JSON.stringify(standalonePath())}`,
8251
- `import config from ${JSON.stringify(project.configFile)}`,
8252
- ...files.map((f2, i) => `import * as m${i} from ${JSON.stringify(join12(root, f2))}`),
8253
- ...faker3 ? [`import * as fakerModule from ${JSON.stringify(FAKER)}`] : [],
8254
- `const data = { ...${JSON.stringify(data2)}, config,`,
8255
- ` modules: [${files.map((f2, i) => `{ file: ${JSON.stringify(f2)}, mod: m${i} }`).join(", ")}],`,
8256
- ` faker: ${faker3 ? "fakerModule" : "undefined"} }`,
8257
- "main(data).then((code) => process.exit(code), (e) => {",
8258
- " console.error(e instanceof Error ? e.message : e)",
8259
- " process.exit(2)",
8260
- "})"
8261
- ].join("\n");
8262
8301
  const ours = createRequire4(import.meta.url);
8263
- const plugin = {
8264
- name: "specbook-bundle",
8265
- resolveId(id) {
8266
- if (id === ENTRY) return ENTRY;
8267
- if (id === FAKER) return FAKER;
8268
- if (id.startsWith(`${prefix}/`) && !id.slice(prefix.length + 1).includes("/"))
8269
- return `\0specbook:api:${id.slice(prefix.length + 1)}`;
8270
- if (id === "@specbook/core" || id.startsWith("@specbook/core/")) return ours.resolve(id);
8271
- return void 0;
8272
- },
8273
- load(id) {
8274
- if (id === ENTRY) return entry;
8275
- if (id === FAKER) return faker3;
8276
- if (!id.startsWith("\0specbook:api:")) return void 0;
8277
- const service = id.slice("\0specbook:api:".length);
8278
- const metas = ops.services[service];
8279
- if (!metas) throw new Error(`Service "${service}" has no operations your requests use`);
8280
- return [
8281
- "import { defineOperation } from '@specbook/core'",
8282
- ...Object.entries(metas).map(
8283
- ([opId2, meta]) => `export const ${opId2} = /* @__PURE__ */ defineOperation(${JSON.stringify(meta)})`
8284
- ),
8285
- ""
8286
- ].join("\n");
8287
- }
8288
- };
8289
8302
  const warnings = [];
8290
- try {
8303
+ const compile = async (faker3) => {
8304
+ const entry = [
8305
+ `import { main } from ${JSON.stringify(standalonePath())}`,
8306
+ `import config from ${JSON.stringify(project.configFile)}`,
8307
+ ...files.map((f2, i) => `import * as m${i} from ${JSON.stringify(join13(root, f2))}`),
8308
+ ...faker3 ? [`import * as fakerModule from ${JSON.stringify(FAKER)}`] : [],
8309
+ `const data = { ...${JSON.stringify(data2)}, config,`,
8310
+ ` modules: [${files.map((f2, i) => `{ file: ${JSON.stringify(f2)}, mod: m${i} }`).join(", ")}],`,
8311
+ ` faker: ${faker3 ? "fakerModule" : "undefined"} }`,
8312
+ "main(data).then((code) => process.exit(code), (e) => {",
8313
+ " console.error(e instanceof Error ? e.message : e)",
8314
+ " process.exit(2)",
8315
+ "})"
8316
+ ].join("\n");
8317
+ const plugin = {
8318
+ name: "specbook-bundle",
8319
+ resolveId(id) {
8320
+ if (id === ENTRY) return ENTRY;
8321
+ if (id === FAKER) return FAKER;
8322
+ if (id.startsWith(`${prefix}/`) && !id.slice(prefix.length + 1).includes("/"))
8323
+ return `\0specbook:api:${id.slice(prefix.length + 1)}`;
8324
+ if (id === "@specbook/core" || id.startsWith("@specbook/core/")) return ours.resolve(id);
8325
+ return void 0;
8326
+ },
8327
+ load(id) {
8328
+ if (id === ENTRY) return entry;
8329
+ if (id === FAKER) return faker3;
8330
+ if (!id.startsWith("\0specbook:api:")) return void 0;
8331
+ const service = id.slice("\0specbook:api:".length);
8332
+ const metas = ops.services[service];
8333
+ if (!metas) throw new Error(`Service "${service}" has no operations your requests use`);
8334
+ return [
8335
+ "import { defineOperation } from '@specbook/core'",
8336
+ ...Object.entries(metas).map(
8337
+ ([opId2, meta]) => `export const ${opId2} = /* @__PURE__ */ defineOperation(${JSON.stringify(meta)})`
8338
+ ),
8339
+ ""
8340
+ ].join("\n");
8341
+ }
8342
+ };
8291
8343
  const build2 = await rolldown({
8292
8344
  input: ENTRY,
8293
8345
  platform: "node",
@@ -8300,7 +8352,7 @@ async function bundleCommand(project, loader, o) {
8300
8352
  }
8301
8353
  });
8302
8354
  await mkdir6(dirname6(out), { recursive: true });
8303
- await build2.write({
8355
+ const written = await build2.write({
8304
8356
  file: out,
8305
8357
  format: "esm",
8306
8358
  minify: true,
@@ -8308,6 +8360,31 @@ async function bundleCommand(project, loader, o) {
8308
8360
  banner: "#!/usr/bin/env node"
8309
8361
  });
8310
8362
  await build2.close();
8363
+ return written.output.some(
8364
+ (c) => c.type === "chunk" && Object.keys(c.modules).some((m) => m.endsWith("/core/src/faker.ts"))
8365
+ );
8366
+ };
8367
+ const noFaker = () => console.error(
8368
+ "These tests use faker, but @faker-js/faker is not installed in this project. Add it to devDependencies (npm install --save-dev @faker-js/faker)."
8369
+ );
8370
+ try {
8371
+ const text = `${sourceText}
8372
+ ${JSON.stringify(embedded.examples)}
8373
+ ${JSON.stringify(environments)}`;
8374
+ let faker3 = USES_FAKER2.test(text) ? await fakerSource(project) : void 0;
8375
+ if (USES_FAKER2.test(text) && !faker3) {
8376
+ noFaker();
8377
+ return 2;
8378
+ }
8379
+ const importsFaker = await compile(faker3);
8380
+ if (importsFaker && !faker3) {
8381
+ faker3 = await fakerSource(project);
8382
+ if (!faker3) {
8383
+ noFaker();
8384
+ return 2;
8385
+ }
8386
+ await compile(faker3);
8387
+ }
8311
8388
  } catch (e) {
8312
8389
  console.error(
8313
8390
  `Could not compile the tests into one file:
@@ -8319,7 +8396,7 @@ ${e instanceof Error ? e.message : e}`
8319
8396
  return 2;
8320
8397
  }
8321
8398
  await chmod(out, 493);
8322
- const bytes = await readFile13(out);
8399
+ const bytes = await readFile14(out);
8323
8400
  const hash = sha2562(bytes);
8324
8401
  await writeFile7(`${out}.sha256`, `${hash} ${basename4(out)}
8325
8402
  `);
@@ -8339,9 +8416,9 @@ ${e instanceof Error ? e.message : e}`
8339
8416
 
8340
8417
  // src/commands/check.ts
8341
8418
  import { existsSync as existsSync8 } from "fs";
8342
- import { readFile as readFile14 } from "fs/promises";
8343
- import { join as join13, resolve as resolve10 } from "path";
8344
- import fg8 from "fast-glob";
8419
+ import { readFile as readFile15 } from "fs/promises";
8420
+ import { join as join14, resolve as resolve10 } from "path";
8421
+ import fg9 from "fast-glob";
8345
8422
  async function checkCommand(project, loader) {
8346
8423
  const errors = [];
8347
8424
  const warnings = [];
@@ -8360,8 +8437,8 @@ async function checkCommand(project, loader) {
8360
8437
  if (untracked.includes(name)) continue;
8361
8438
  let drift = 0;
8362
8439
  for (const f2 of files2) {
8363
- const p = join13(outDir, name, f2.path);
8364
- if (!existsSync8(p) || await readFile14(p, "utf8") !== f2.content) drift++;
8440
+ const p = join14(outDir, name, f2.path);
8441
+ if (!existsSync8(p) || await readFile15(p, "utf8") !== f2.content) drift++;
8365
8442
  }
8366
8443
  if (drift)
8367
8444
  errors.push(
@@ -8440,7 +8517,7 @@ async function checkCommand(project, loader) {
8440
8517
  ...fileVarNames
8441
8518
  ]);
8442
8519
  const declared = new Set(Object.keys(cfg.secrets ?? {}));
8443
- const files = await fg8("**/*.req.ts", {
8520
+ const files = await fg9("**/*.req.ts", {
8444
8521
  cwd: project.root,
8445
8522
  absolute: true,
8446
8523
  ignore: ["**/node_modules/**", "**/.generated/**"]
@@ -8504,13 +8581,13 @@ async function generateCommand(project, loader, only) {
8504
8581
 
8505
8582
  // src/commands/init.ts
8506
8583
  import { existsSync as existsSync10 } from "fs";
8507
- import { mkdir as mkdir8, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
8508
- import { join as join15 } from "path";
8584
+ import { mkdir as mkdir8, readFile as readFile17, writeFile as writeFile9 } from "fs/promises";
8585
+ import { join as join16 } from "path";
8509
8586
 
8510
8587
  // src/commands/agents.ts
8511
8588
  import { existsSync as existsSync9, realpathSync } from "fs";
8512
- import { mkdir as mkdir7, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
8513
- import { join as join14 } from "path";
8589
+ import { mkdir as mkdir7, readFile as readFile16, writeFile as writeFile8 } from "fs/promises";
8590
+ import { join as join15 } from "path";
8514
8591
  import { fileURLToPath as fileURLToPath2 } from "url";
8515
8592
  var FILES = ["AGENTS.md", "CLAUDE.md"];
8516
8593
  var START = "<!-- specbook:start";
@@ -8548,18 +8625,18 @@ var importsAgents = (text) => /^@AGENTS\.md\b/m.test(text);
8548
8625
  async function readGuide() {
8549
8626
  for (const rel2 of ["../agent-guide.md", "../../agent-guide.md"])
8550
8627
  try {
8551
- return await readFile15(fileURLToPath2(new URL(rel2, import.meta.url)), "utf8");
8628
+ return await readFile16(fileURLToPath2(new URL(rel2, import.meta.url)), "utf8");
8552
8629
  } catch {
8553
8630
  }
8554
8631
  throw new Error("agent-guide.md is missing from the @specbook/cli package");
8555
8632
  }
8556
8633
  async function agentsStep(cwd, agents) {
8557
8634
  if (agents === false) return;
8558
- const dir = join14(cwd, ".specbook");
8635
+ const dir = join15(cwd, ".specbook");
8559
8636
  await mkdir7(dir, { recursive: true });
8560
8637
  const guide = await readGuide();
8561
8638
  await writeFile8(
8562
- join14(dir, "agent-guide.md"),
8639
+ join15(dir, "agent-guide.md"),
8563
8640
  `<!-- Written by specbook init v${cliVersion()}. Re-run \`specbook init\` to refresh it, and put project rules in AGENTS.md. -->
8564
8641
 
8565
8642
  ${guide}`
@@ -8568,15 +8645,15 @@ ${guide}`
8568
8645
  const seen = /* @__PURE__ */ new Set();
8569
8646
  const found = [];
8570
8647
  for (const f2 of FILES) {
8571
- if (!existsSync9(join14(cwd, f2))) continue;
8572
- const real = realpathSync(join14(cwd, f2));
8648
+ if (!existsSync9(join15(cwd, f2))) continue;
8649
+ const real = realpathSync(join15(cwd, f2));
8573
8650
  if (seen.has(real)) continue;
8574
8651
  seen.add(real);
8575
8652
  found.push(f2);
8576
8653
  }
8577
8654
  if (found.length === 0) {
8578
8655
  await writeFile8(
8579
- join14(cwd, "AGENTS.md"),
8656
+ join15(cwd, "AGENTS.md"),
8580
8657
  `# AGENTS.md
8581
8658
 
8582
8659
  Instructions for AI coding agents working in this repository.
@@ -8594,8 +8671,8 @@ ${agentBlock()}
8594
8671
  return;
8595
8672
  }
8596
8673
  for (const file of found) {
8597
- const path = join14(cwd, file);
8598
- const text = await readFile15(path, "utf8");
8674
+ const path = join15(cwd, file);
8675
+ const text = await readFile16(path, "utf8");
8599
8676
  if (file === "CLAUDE.md" && found.includes("AGENTS.md") && importsAgents(text)) {
8600
8677
  console.log("CLAUDE.md imports AGENTS.md: its section covers Claude Code too");
8601
8678
  continue;
@@ -8615,10 +8692,11 @@ ${agentBlock()}
8615
8692
  }
8616
8693
 
8617
8694
  // src/commands/init.ts
8695
+ var FAKER_VERSION = "9.3.0";
8618
8696
  async function initCommand(cwd, opts = {}) {
8619
- const dir = join15(cwd, ".specbook");
8697
+ const dir = join16(cwd, ".specbook");
8620
8698
  await mkdir8(dir, { recursive: true });
8621
- const cfg = join15(dir, "specbook.config.ts");
8699
+ const cfg = join16(dir, "specbook.config.ts");
8622
8700
  if (!existsSync10(cfg)) {
8623
8701
  await writeFile9(
8624
8702
  cfg,
@@ -8639,28 +8717,35 @@ export default defineConfig({
8639
8717
  console.log("\u2713 created .specbook/specbook.config.ts");
8640
8718
  }
8641
8719
  const envDir = await ensureEnvironmentsDir(cwd);
8642
- const example = join15(envDir, "staging.secrets.env.example");
8720
+ const example = join16(envDir, "staging.secrets.env.example");
8643
8721
  if (!existsSync10(example)) {
8644
8722
  await writeFile9(
8645
8723
  example,
8646
8724
  "# Copy to staging.secrets.env (gitignored) and fill in. One KEY=value per line.\n# YOUR_API_KEY=\n"
8647
8725
  );
8648
8726
  await writeFile9(
8649
- join15(envDir, "staging.env"),
8727
+ join16(envDir, "staging.env"),
8650
8728
  '# Non-secret variables for the "staging" environment (committed).\n# region=eu-west-1\n'
8651
8729
  );
8652
8730
  console.log("\u2713 created .specbook/environments/ (staging.env, staging.secrets.env.example)");
8653
8731
  }
8654
- const pkgPath = join15(cwd, "package.json");
8655
- const pkg = existsSync10(pkgPath) ? JSON.parse(await readFile16(pkgPath, "utf8")) : { name: "my-apis", private: true, type: "module" };
8732
+ const pkgPath = join16(cwd, "package.json");
8733
+ const pkg = existsSync10(pkgPath) ? JSON.parse(await readFile17(pkgPath, "utf8")) : { name: "my-apis", private: true, type: "module" };
8656
8734
  pkg.imports = { ...pkg.imports ?? {}, "#api/*": "./.generated/*/index.ts" };
8657
8735
  pkg.scripts = { ...pkg.scripts ?? {} };
8658
8736
  if (!pkg.scripts.prepare) pkg.scripts.prepare = "specbook generate";
8737
+ pkg.devDependencies = { ...pkg.devDependencies ?? {} };
8738
+ const hasFaker = ["dependencies", "devDependencies", "peerDependencies"].some(
8739
+ (k) => pkg[k]?.["@faker-js/faker"]
8740
+ );
8741
+ if (!hasFaker) pkg.devDependencies["@faker-js/faker"] = FAKER_VERSION;
8659
8742
  await writeFile9(pkgPath, `${JSON.stringify(pkg, null, 2)}
8660
8743
  `);
8661
- console.log('\u2713 added the "#api/*" subpath import and a "prepare" script to package.json');
8662
- const gi = join15(cwd, ".gitignore");
8663
- const current = existsSync10(gi) ? await readFile16(gi, "utf8") : "";
8744
+ console.log(
8745
+ `\u2713 added the "#api/*" subpath import, a "prepare" script${hasFaker ? "" : " and @faker-js/faker"} to package.json`
8746
+ );
8747
+ const gi = join16(cwd, ".gitignore");
8748
+ const current = existsSync10(gi) ? await readFile17(gi, "utf8") : "";
8664
8749
  const missing = [".generated", ".specbook/secrets.local.env"].filter(
8665
8750
  (entry) => !current.split("\n").some((l) => l.trim() === entry)
8666
8751
  );
@@ -8681,13 +8766,13 @@ import { existsSync as existsSync11, readFileSync as readFileSync2 } from "fs";
8681
8766
  import { mkdir as mkdir9, mkdtemp as mkdtemp2, rm as rm4, symlink, writeFile as writeFile11 } from "fs/promises";
8682
8767
  import { createRequire as createRequire6 } from "module";
8683
8768
  import { tmpdir as tmpdir2 } from "os";
8684
- import { dirname as dirname8, extname, join as join17, resolve as resolve12 } from "path";
8769
+ import { dirname as dirname8, extname, join as join18, resolve as resolve12 } from "path";
8685
8770
  import SwaggerParser4 from "@apidevtools/swagger-parser";
8686
8771
 
8687
8772
  // src/commands/ui.ts
8688
- import { copyFile, cp, readFile as readFile17, stat, writeFile as writeFile10 } from "fs/promises";
8773
+ import { copyFile, cp, readFile as readFile18, stat, writeFile as writeFile10 } from "fs/promises";
8689
8774
  import { createRequire as createRequire5 } from "module";
8690
- import { basename as basename5, dirname as dirname7, join as join16, resolve as resolve11, sep as sep8 } from "path";
8775
+ import { basename as basename5, dirname as dirname7, join as join17, resolve as resolve11, sep as sep8 } from "path";
8691
8776
  import { fileURLToPath as fileURLToPath3 } from "url";
8692
8777
  import { build, createServer } from "vite";
8693
8778
  function uiRoot() {
@@ -8708,9 +8793,9 @@ function enclosingNodeModules(file) {
8708
8793
  function dependencyFolders(root) {
8709
8794
  const found = /* @__PURE__ */ new Set([
8710
8795
  ...enclosingNodeModules(fileURLToPath3(import.meta.url)),
8711
- ...enclosingNodeModules(join16(root, "package.json"))
8796
+ ...enclosingNodeModules(join17(root, "package.json"))
8712
8797
  ]);
8713
- for (const from of [join16(root, "package.json"), fileURLToPath3(import.meta.url)]) {
8798
+ for (const from of [join17(root, "package.json"), fileURLToPath3(import.meta.url)]) {
8714
8799
  const require2 = createRequire5(from);
8715
8800
  for (const name of ["vite", "prism-code-editor", "preact", "@preact/signals"]) {
8716
8801
  try {
@@ -8735,7 +8820,12 @@ function uiConfig(project, operations) {
8735
8820
  rolldownOptions: { transform: { jsx: { runtime: "automatic", importSource: "preact" } } }
8736
8821
  },
8737
8822
  plugins: [
8738
- specbook({ projectRoot: project.root, outDir: project.config.codegen?.outDir, operations })
8823
+ specbook({
8824
+ projectRoot: project.root,
8825
+ outDir: project.config.codegen?.outDir,
8826
+ operations,
8827
+ faker: project.config.faker
8828
+ })
8739
8829
  ],
8740
8830
  server: {
8741
8831
  fs: {
@@ -8768,7 +8858,7 @@ async function devCommand(project, port = 5173) {
8768
8858
  }
8769
8859
  async function buildCommand(project, outDir, options = {}) {
8770
8860
  const out = resolve11(project.root, outDir);
8771
- const generated = join16(project.root, project.config.codegen?.outDir ?? ".generated");
8861
+ const generated = join17(project.root, project.config.codegen?.outDir ?? ".generated");
8772
8862
  const operations = options.minimal ? await usedOperationKeys(project, generated) : void 0;
8773
8863
  if (operations && operations.size === 0)
8774
8864
  throw new Error("Nothing to publish: no request or flow imports an operation (see --minimal)");
@@ -8790,12 +8880,12 @@ async function buildCommand(project, outDir, options = {}) {
8790
8880
  }
8791
8881
  }
8792
8882
  });
8793
- await copyMetadata(join16(generated, "specbook.json"), join16(out, "specbook.json"), operations);
8883
+ await copyMetadata(join17(generated, "specbook.json"), join17(out, "specbook.json"), operations);
8794
8884
  }
8795
8885
  async function copyMetadata(from, to, operations) {
8796
8886
  try {
8797
8887
  if (!operations) return await copyFile(from, to);
8798
- const meta = JSON.parse(await readFile17(from, "utf8"));
8888
+ const meta = JSON.parse(await readFile18(from, "utf8"));
8799
8889
  for (const [svc, s] of Object.entries(meta.services)) {
8800
8890
  s.operations = Object.fromEntries(
8801
8891
  Object.entries(s.operations).filter(([id]) => operations.has(`${svc}/${id}`))
@@ -8808,7 +8898,7 @@ async function copyMetadata(from, to, operations) {
8808
8898
  }
8809
8899
  }
8810
8900
  async function copyViewer(project, outDir, viewerDir = resolve11(uiRoot(), "dist-viewer")) {
8811
- const has2 = await stat(join16(viewerDir, "index.html")).then(
8901
+ const has2 = await stat(join17(viewerDir, "index.html")).then(
8812
8902
  () => true,
8813
8903
  () => false
8814
8904
  );
@@ -8827,9 +8917,9 @@ var slug3 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/
8827
8917
  async function link(root, name) {
8828
8918
  try {
8829
8919
  let dir = dirname8(createRequire6(import.meta.url).resolve(name));
8830
- while (!existsSync11(join17(dir, "package.json")) || JSON.parse(readFileSync2(join17(dir, "package.json"), "utf8")).name !== name)
8920
+ while (!existsSync11(join18(dir, "package.json")) || JSON.parse(readFileSync2(join18(dir, "package.json"), "utf8")).name !== name)
8831
8921
  dir = dirname8(dir);
8832
- const dest = join17(root, "node_modules", ...name.split("/"));
8922
+ const dest = join18(root, "node_modules", ...name.split("/"));
8833
8923
  await mkdir9(dirname8(dest), { recursive: true });
8834
8924
  await symlink(dir, dest, "dir");
8835
8925
  } catch {
@@ -8850,7 +8940,7 @@ async function previewCommand(source, o = {}) {
8850
8940
  const { servers } = openApiServers(api, source);
8851
8941
  const where = servers.length === 0 ? "" : servers.length === 1 && servers[0]?.name === "live" ? `, live calls go to ${servers[0].url}` : `
8852
8942
  Environments: ${servers.map((s) => `${s.name} \u2192 ${s.url}`).join(", ")}`;
8853
- const root = await mkdtemp2(join17(tmpdir2(), "specbook-preview-"));
8943
+ const root = await mkdtemp2(join18(tmpdir2(), "specbook-preview-"));
8854
8944
  const cleanup = () => rm4(root, { recursive: true, force: true }).catch(() => {
8855
8945
  });
8856
8946
  process.once("SIGINT", () => cleanup().then(() => process.exit(0)));
@@ -8860,7 +8950,7 @@ async function previewCommand(source, o = {}) {
8860
8950
  const res = await fetch(source);
8861
8951
  if (!res.ok) throw new Error(`Could not fetch ${source} (${res.status})`);
8862
8952
  const ext = /ya?ml/.test(extname(new URL(source).pathname)) ? ".yaml" : ".json";
8863
- specFile = join17(root, `openapi/api${ext}`);
8953
+ specFile = join18(root, `openapi/api${ext}`);
8864
8954
  await mkdir9(dirname8(specFile), { recursive: true });
8865
8955
  await writeFile11(specFile, await res.text());
8866
8956
  }
@@ -8895,14 +8985,14 @@ async function previewCommand(source, o = {}) {
8895
8985
  }
8896
8986
  }
8897
8987
  };
8898
- const configFile = join17(root, ".specbook/specbook.config.ts");
8988
+ const configFile = join18(root, ".specbook/specbook.config.ts");
8899
8989
  await mkdir9(dirname8(configFile), { recursive: true });
8900
8990
  await writeFile11(configFile, `export default ${JSON.stringify(config, null, 2)}
8901
8991
  `);
8902
8992
  await link(root, "@specbook/core");
8903
8993
  await link(root, "@faker-js/faker");
8904
8994
  console.log(`Generating "${title}" \u2026`);
8905
- const out = await generateToDisk({ spec: specFile, service, outDir: join17(root, ".generated") });
8995
+ const out = await generateToDisk({ spec: specFile, service, outDir: join18(root, ".generated") });
8906
8996
  console.log(`\u2713 ${out.operations.length} operations${where}`);
8907
8997
  const project = { root, configFile, config };
8908
8998
  const server = await devCommand(project, o.port ?? 5173);
@@ -8971,18 +9061,18 @@ import { resolve as resolve15 } from "path";
8971
9061
 
8972
9062
  // src/examples.ts
8973
9063
  import { File } from "buffer";
8974
- import { readFile as readFile18 } from "fs/promises";
8975
- import { basename as basename6, dirname as dirname9, join as join18, resolve as resolve14 } from "path";
8976
- import fg9 from "fast-glob";
8977
- var IGNORE3 = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
9064
+ import { readFile as readFile19 } from "fs/promises";
9065
+ import { basename as basename6, dirname as dirname9, join as join19, resolve as resolve14 } from "path";
9066
+ import fg10 from "fast-glob";
9067
+ var IGNORE4 = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
8978
9068
  function exampleLoadersFor(project) {
8979
9069
  const outDir = resolve14(project.root, project.config.codegen?.outDir ?? ".generated");
8980
9070
  const example = async (op, name) => {
8981
9071
  const rel2 = `${op.service}/${op.id}/${name}.example.json`;
8982
- const [custom] = await fg9(`**/${rel2}`, { cwd: project.root, absolute: true, ignore: IGNORE3 });
8983
- const file = custom ?? join18(outDir, op.service, "examples", op.id, `${name}.example.json`);
9072
+ const [custom] = await fg10(`**/${rel2}`, { cwd: project.root, absolute: true, ignore: IGNORE4 });
9073
+ const file = custom ?? join19(outDir, op.service, "examples", op.id, `${name}.example.json`);
8984
9074
  try {
8985
- const doc = JSON.parse(await readFile18(file, "utf8"));
9075
+ const doc = JSON.parse(await readFile19(file, "utf8"));
8986
9076
  return { ...doc, source: custom ? "project" : "generated", dir: dirname9(file) };
8987
9077
  } catch {
8988
9078
  return void 0;
@@ -8990,7 +9080,7 @@ function exampleLoadersFor(project) {
8990
9080
  };
8991
9081
  const read = async (path, dir) => {
8992
9082
  const abs = resolve14(dir ?? project.root, path);
8993
- return new File([await readFile18(abs)], basename6(abs));
9083
+ return new File([await readFile19(abs)], basename6(abs));
8994
9084
  };
8995
9085
  return { example, readFile: read };
8996
9086
  }
@@ -9021,7 +9111,8 @@ async function testCommand(project, loader, o) {
9021
9111
  readEnvironment: (env) => readEnvironment(project.root, env),
9022
9112
  examples: exampleLoadersFor(project),
9023
9113
  mock: mockLoaderFor(project),
9024
- loadFaker: () => loadFaker(project),
9114
+ // only a project that uses faker pays for importing it
9115
+ loadFaker: async () => await projectUsesFaker(project.root) ? loadFaker(project) : void 0,
9025
9116
  readSource: (file) => {
9026
9117
  try {
9027
9118
  return readFileSync3(file, "utf8");
@@ -9105,7 +9196,7 @@ var TEST_HELP = ` --env <name> environment (default: mock, for
9105
9196
  --input k=v flow input value (repeatable)
9106
9197
  --var k=v variable override (repeatable)
9107
9198
  --secrets-file <path> dotenv file with secrets
9108
- --locale <l> --seed <n> synthetic data locale / deterministic seed
9199
+ --locale <l> --seed <n> generated values: locale / seed for a repeatable run
9109
9200
  --notify <url> POST a summary to a webhook when something fails`;
9110
9201
  var parseValue = (v2) => {
9111
9202
  try {
@@ -470,7 +470,7 @@ var TEST_HELP = ` --env <name> environment (default: mock, for
470
470
  --input k=v flow input value (repeatable)
471
471
  --var k=v variable override (repeatable)
472
472
  --secrets-file <path> dotenv file with secrets
473
- --locale <l> --seed <n> synthetic data locale / deterministic seed
473
+ --locale <l> --seed <n> generated values: locale / seed for a repeatable run
474
474
  --notify <url> POST a summary to a webhook when something fails`;
475
475
  var parseValue = (v) => {
476
476
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specbook/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Specbook command line: init, add, generate, dev, build, test, bundle, check, secrets and preview.",
5
5
  "keywords": [
6
6
  "specbook",
@@ -38,10 +38,10 @@
38
38
  "dependencies": {
39
39
  "@apidevtools/swagger-parser": "10.1.1",
40
40
  "@faker-js/faker": "9.3.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",
41
+ "@specbook/codegen": "0.5.0",
42
+ "@specbook/core": "0.5.0",
43
+ "@specbook/ui": "0.5.0",
44
+ "@specbook/vite": "0.5.0",
45
45
  "fast-glob": "3.3.2",
46
46
  "rolldown": "1.2.13",
47
47
  "swagger2openapi": "7.0.8",