@specbook/cli 0.3.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/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) => {
@@ -2143,8 +2103,10 @@ function installFaker(mod, config = {}, opts = {}) {
2143
2103
  }
2144
2104
 
2145
2105
  // ../core/src/template.ts
2146
- var WHOLE = /^\s*\{\{\s*([\w.$-]+)\s*\}\}\s*$/;
2147
- var EMBED = /\{\{\s*([\w.$-]+)\s*\}\}/g;
2106
+ var NAME = String.raw`[\w.$\[\]-]+`;
2107
+ var WHOLE = new RegExp(String.raw`^\s*\{\{\s*(${NAME})\s*\}\}\s*$`);
2108
+ var EMBED = new RegExp(String.raw`\{\{\s*(${NAME})\s*\}\}`, "g");
2109
+ var rootName = (name) => name.split(/[.[]/, 1)[0];
2148
2110
  var DEFERRED_NAMESPACES = ["secrets", "session"];
2149
2111
  var TemplateError = class extends Error {
2150
2112
  constructor(variable) {
@@ -2159,21 +2121,32 @@ function isDeferred(name) {
2159
2121
  return DEFERRED_NAMESPACES.includes(ns) && name.includes(".");
2160
2122
  }
2161
2123
  var isFakerName = (name) => name.startsWith("faker.");
2162
- function fromFaker(path) {
2163
- let parent = faker;
2164
- let cur = faker;
2165
- for (const part of path.split(".").slice(1)) {
2166
- parent = cur;
2167
- cur = cur?.[part];
2168
- 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";
2169
2129
  }
2170
- 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) };
2171
2143
  }
2144
+ var steps = (path) => path.replace(/\[(\d+)\]/g, ".$1").split(".");
2172
2145
  function lookup(scope, path) {
2173
2146
  if (isFakerName(path)) return fromFaker(path);
2174
2147
  let cur = scope;
2175
- for (const part of path.split(".")) {
2176
- if (cur !== null && typeof cur === "object" && part in cur) {
2148
+ for (const part of steps(path)) {
2149
+ if (cur !== null && typeof cur === "object" && Object.hasOwn(cur, part)) {
2177
2150
  cur = cur[part];
2178
2151
  } else {
2179
2152
  return { found: false };
@@ -2181,7 +2154,19 @@ function lookup(scope, path) {
2181
2154
  }
2182
2155
  return { found: true, value: cur };
2183
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
+ }
2184
2168
  function renderString(s, scope) {
2169
+ rejectFakerCall(s);
2185
2170
  const whole = WHOLE.exec(s);
2186
2171
  if (whole) {
2187
2172
  const name = whole[1];
@@ -2862,7 +2847,7 @@ function createColors(enabled) {
2862
2847
  // ../core/src/reporters/list.ts
2863
2848
  var MARKS = { pass: "\u2713", fail: "\u2717", error: "!", skipped: "\u2013" };
2864
2849
  var formatDuration = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(2)}s` : `${Math.floor(ms / 6e4)}m ${Math.round(ms % 6e4 / 1e3)}s`;
2865
- var leaves = (steps) => steps.flatMap((s) => s.children?.length ? leaves(s.children) : [s]);
2850
+ var leaves = (steps3) => steps3.flatMap((s) => s.children?.length ? leaves(s.children) : [s]);
2866
2851
  function paint(status, c, text) {
2867
2852
  return status === "pass" ? c.green(text) : status === "skipped" ? c.dim(text) : status === "error" ? c.yellow(text) : c.red(text);
2868
2853
  }
@@ -2941,21 +2926,21 @@ function formatSummary(results, o = {}) {
2941
2926
  const c = o.colors ?? createColors(false);
2942
2927
  const t = reportTotals(results);
2943
2928
  const part = (n, word, paintFn) => n ? paintFn(`${n} ${word}`) : "";
2944
- const join18 = (...parts) => parts.filter(Boolean).join(c.dim(" | ")) || c.dim("none");
2929
+ const join20 = (...parts) => parts.filter(Boolean).join(c.dim(" | ")) || c.dim("none");
2945
2930
  const lines2 = [
2946
2931
  "",
2947
- `${c.bold(" Flows ")} ${join18(
2932
+ `${c.bold(" Flows ")} ${join20(
2948
2933
  part(t.flows.failed, "failed", c.red),
2949
2934
  part(t.flows.errored, "errored", c.yellow),
2950
2935
  part(t.flows.flaky, "flaky", c.yellow),
2951
2936
  part(t.flows.passed, "passed", c.green)
2952
2937
  )} ${c.dim(`(${results.length})`)}`,
2953
- `${c.bold(" Steps ")} ${join18(
2938
+ `${c.bold(" Steps ")} ${join20(
2954
2939
  part(t.steps.failed, "failed", c.red),
2955
2940
  part(t.steps.skipped, "skipped", c.dim),
2956
2941
  part(t.steps.passed, "passed", c.green)
2957
2942
  )}`,
2958
- `${c.bold(" Assertions ")} ${join18(
2943
+ `${c.bold(" Assertions ")} ${join20(
2959
2944
  part(t.assertions.failed, "failed", c.red),
2960
2945
  part(t.assertions.passed, "passed", c.green)
2961
2946
  )}`
@@ -3030,7 +3015,7 @@ var esc = (s) => String(s).replace(
3030
3015
  /[<>&"']|[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g,
3031
3016
  (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&apos;" })[c] ?? ""
3032
3017
  );
3033
- var leafSteps = (steps) => steps.flatMap((s) => s.children?.length ? leafSteps(s.children) : [s]);
3018
+ var leafSteps = (steps3) => steps3.flatMap((s) => s.children?.length ? leafSteps(s.children) : [s]);
3034
3019
  function toJUnit(results, o = {}) {
3035
3020
  const suites = results.map((r) => {
3036
3021
  const flat = leafSteps(r.steps);
@@ -3225,6 +3210,31 @@ function buildUrl(base, req) {
3225
3210
  return `${base.replace(/\/+$/, "")}${req.url}${q2 ? `?${q2}` : ""}`;
3226
3211
  }
3227
3212
  var DEFAULT_TIMEOUT_MS = 3e4;
3213
+ var WHY = {
3214
+ ECONNREFUSED: "connection refused",
3215
+ ENOTFOUND: "host not found",
3216
+ EAI_AGAIN: "host not found",
3217
+ ECONNRESET: "connection reset",
3218
+ ETIMEDOUT: "connection timed out",
3219
+ UND_ERR_CONNECT_TIMEOUT: "connection timed out",
3220
+ EHOSTUNREACH: "host unreachable",
3221
+ ENETUNREACH: "network unreachable"
3222
+ };
3223
+ function unreachable2(e, url) {
3224
+ const cause = e?.cause;
3225
+ if (!(e instanceof TypeError) || !cause) return e;
3226
+ const code = cause.code ?? cause.errors?.find((x2) => x2.code)?.code;
3227
+ const tls = code && /^CERT_|^ERR_TLS|SELF_SIGNED|UNABLE_TO_VERIFY/.test(code) ? "certificate problem" : "";
3228
+ const own = typeof cause.message === "string" && cause.message.length <= 120 ? cause.message : "";
3229
+ const why = code && WHY[code] || tls || own;
3230
+ if (!why) return e;
3231
+ let host = url;
3232
+ try {
3233
+ host = new URL(url).host;
3234
+ } catch {
3235
+ }
3236
+ return new Error(`Could not reach ${host}: ${why}.`, { cause: e });
3237
+ }
3228
3238
  function createFetchTransport(opts) {
3229
3239
  return async (req, signal) => {
3230
3240
  if (signal?.aborted) throw new Error("Stopped before the API answered.");
@@ -3243,8 +3253,9 @@ function createFetchTransport(opts) {
3243
3253
  ctl.abort();
3244
3254
  }, req.timeoutMs) : void 0;
3245
3255
  signal?.addEventListener("abort", () => ctl.abort());
3256
+ const url = buildUrl(opts.baseUrl(req.service), req);
3246
3257
  try {
3247
- const res = await f2(buildUrl(opts.baseUrl(req.service), req), {
3258
+ const res = await f2(url, {
3248
3259
  method: req.method,
3249
3260
  headers,
3250
3261
  body: body2,
@@ -3268,7 +3279,7 @@ function createFetchTransport(opts) {
3268
3279
  } catch (e) {
3269
3280
  if (timedOut) throw new Error(`Timed out after ${req.timeoutMs}ms: no answer from the API.`);
3270
3281
  if (signal?.aborted) throw new Error("Stopped before the API answered.");
3271
- throw e;
3282
+ throw unreachable2(e, url);
3272
3283
  } finally {
3273
3284
  if (timer) clearTimeout(timer);
3274
3285
  }
@@ -3318,7 +3329,7 @@ function usesOf(names, layers, from) {
3318
3329
  const out = [];
3319
3330
  for (const name of names) {
3320
3331
  if (isDeferred(name) || isFakerName(name)) continue;
3321
- const key = name.split(".")[0];
3332
+ const key = rootName(name);
3322
3333
  if (seen.has(key)) continue;
3323
3334
  seen.add(key);
3324
3335
  for (let i = layers.length - 1; i >= 0; i--) {
@@ -3587,7 +3598,7 @@ async function execute(rt2, def, name, flow, kind = "request", noAuth = false) {
3587
3598
  emit(rt2, { type: "step:end", name, status: result.status });
3588
3599
  return { result, response, authResults };
3589
3600
  }
3590
- function summarize(steps) {
3601
+ function summarize(steps3) {
3591
3602
  const t = { steps: 0, passed: 0, failed: 0, skipped: 0, assertions: { passed: 0, failed: 0 } };
3592
3603
  const visit = (s) => {
3593
3604
  if (s.children?.length) {
@@ -3600,21 +3611,21 @@ function summarize(steps) {
3600
3611
  else t.failed++;
3601
3612
  for (const a3 of s.assertions) a3.pass ? t.assertions.passed++ : t.assertions.failed++;
3602
3613
  };
3603
- steps.forEach(visit);
3614
+ steps3.forEach(visit);
3604
3615
  return t;
3605
3616
  }
3606
- function overall(steps) {
3617
+ function overall(steps3) {
3607
3618
  const all = [];
3608
3619
  const walk2 = (s) => {
3609
3620
  all.push(s);
3610
3621
  s.children?.forEach(walk2);
3611
3622
  };
3612
- steps.forEach(walk2);
3623
+ steps3.forEach(walk2);
3613
3624
  if (all.some((s) => s.status === "error")) return "error";
3614
3625
  if (all.some((s) => s.status === "fail")) return "fail";
3615
3626
  return "pass";
3616
3627
  }
3617
- function finish(rt2, kind, meta, steps, startedAt) {
3628
+ function finish(rt2, kind, meta, steps3, startedAt) {
3618
3629
  const result = {
3619
3630
  schemaVersion: 1,
3620
3631
  runId: rid(),
@@ -3626,11 +3637,11 @@ function finish(rt2, kind, meta, steps, startedAt) {
3626
3637
  seed: rt2.o.seed,
3627
3638
  usedDrafts: rt2.o.usedDrafts ?? false,
3628
3639
  tags: meta.tags ?? [],
3629
- status: overall(steps),
3640
+ status: overall(steps3),
3630
3641
  startedAt: new Date(startedAt).toISOString(),
3631
3642
  durationMs: (rt2.o.now?.() ?? now0()) - startedAt,
3632
- totals: summarize(steps),
3633
- steps
3643
+ totals: summarize(steps3),
3644
+ steps: steps3
3634
3645
  };
3635
3646
  const masked = maskDeep(result, rt2.mask);
3636
3647
  emit(rt2, { type: "done", result: masked });
@@ -3718,7 +3729,7 @@ async function runSequence(seq, o) {
3718
3729
  kind: "sequence",
3719
3730
  tags: seq.meta.tags
3720
3731
  });
3721
- const steps = [];
3732
+ const steps3 = [];
3722
3733
  const captured = {};
3723
3734
  const from = {};
3724
3735
  const remember = (values, step) => {
@@ -3729,7 +3740,7 @@ async function runSequence(seq, o) {
3729
3740
  try {
3730
3741
  inputs = resolveControls(seq.meta.inputs ?? {}, o.inputs);
3731
3742
  } catch (e) {
3732
- steps.push({
3743
+ steps3.push({
3733
3744
  name: "inputs",
3734
3745
  kind: "input",
3735
3746
  status: "error",
@@ -3738,7 +3749,7 @@ async function runSequence(seq, o) {
3738
3749
  error: e.message,
3739
3750
  durationMs: 0
3740
3751
  });
3741
- return finish(rt2, "sequence", seq.meta, steps, t0);
3752
+ return finish(rt2, "sequence", seq.meta, steps3, t0);
3742
3753
  }
3743
3754
  const vars = o.vars ?? {};
3744
3755
  const ctx = () => ({
@@ -3748,7 +3759,7 @@ async function runSequence(seq, o) {
3748
3759
  env: o.env
3749
3760
  });
3750
3761
  let failed = false;
3751
- const skip = (name, kind) => steps.push({ name, kind, status: "skipped", assertions: [], captured: {}, durationMs: 0 });
3762
+ const skip = (name, kind) => steps3.push({ name, kind, status: "skipped", assertions: [], captured: {}, durationMs: 0 });
3752
3763
  for (const node of seq.nodes) {
3753
3764
  if (failed && !seq.meta.continueOnFailure) {
3754
3765
  skip(node.name, node.kind === "step" ? "request" : node.kind);
@@ -3767,7 +3778,7 @@ async function runSequence(seq, o) {
3767
3778
  captured,
3768
3779
  from
3769
3780
  });
3770
- steps.push(...authResults, result);
3781
+ steps3.push(...authResults, result);
3771
3782
  remember(result.captured, node.name);
3772
3783
  if (result.status !== "pass") failed = true;
3773
3784
  } else if (node.kind === "each") {
@@ -3803,7 +3814,7 @@ async function runSequence(seq, o) {
3803
3814
  parent.captured = collected;
3804
3815
  parent.durationMs = (o.now?.() ?? now0()) - p0;
3805
3816
  remember(collected, node.name);
3806
- steps.push(parent);
3817
+ steps3.push(parent);
3807
3818
  if (parent.status !== "pass") failed = true;
3808
3819
  } else if (node.kind === "input") {
3809
3820
  const controls = node.controls(ctx());
@@ -3812,7 +3823,7 @@ async function runSequence(seq, o) {
3812
3823
  const given = o.onInput ? await o.onInput({ name: node.name, controls: described }) : o.inputs ?? {};
3813
3824
  const values = resolveControls(controls, given);
3814
3825
  inputs = { ...inputs, ...values };
3815
- steps.push({
3826
+ steps3.push({
3816
3827
  name: node.name,
3817
3828
  kind: "input",
3818
3829
  status: "pass",
@@ -3842,12 +3853,12 @@ async function runSequence(seq, o) {
3842
3853
  s.assertions = a3.results;
3843
3854
  if (s.status === "pass" && a3.results.some((r) => !r.pass)) s.status = "fail";
3844
3855
  emit(rt2, { type: "step:end", name: node.name, status: s.status });
3845
- steps.push(s);
3856
+ steps3.push(s);
3846
3857
  if (s.status !== "pass") failed = true;
3847
3858
  }
3848
3859
  } catch (e) {
3849
3860
  const isControl = e instanceof ControlError;
3850
- steps.push({
3861
+ steps3.push({
3851
3862
  name: node.name,
3852
3863
  kind: node.kind === "step" ? "request" : node.kind,
3853
3864
  status: "error",
@@ -3859,7 +3870,7 @@ async function runSequence(seq, o) {
3859
3870
  failed = true;
3860
3871
  }
3861
3872
  }
3862
- return finish(rt2, "sequence", seq.meta, steps, t0);
3873
+ return finish(rt2, "sequence", seq.meta, steps3, t0);
3863
3874
  }
3864
3875
 
3865
3876
  // ../core/src/runner/suite.ts
@@ -3919,7 +3930,7 @@ function headerLine(h2, colors2) {
3919
3930
  var retryLine = (colors2, title, attempt, allowed) => ` ${colors2.yellow("\u21BB")} retrying ${title} (attempt ${attempt} of ${allowed})
3920
3931
  `;
3921
3932
  function withoutLocations(results) {
3922
- const strip = (steps) => steps.map((s) => ({
3933
+ const strip = (steps3) => steps3.map((s) => ({
3923
3934
  ...s,
3924
3935
  assertions: s.assertions.map((a3) => ({ ...a3, location: void 0 })),
3925
3936
  ...s.children ? { children: strip(s.children) } : {}
@@ -5472,8 +5483,19 @@ var faker2 = new Proxy({}, {
5472
5483
  return current[key];
5473
5484
  }
5474
5485
  });
5475
- var WHOLE2 = /^\s*\{\{\s*([\w.$-]+)\s*\}\}\s*$/;
5476
- var EMBED2 = /\{\{\s*([\w.$-]+)\s*\}\}/g;
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
+ }
5496
+ var NAME2 = String.raw`[\w.$\[\]-]+`;
5497
+ var WHOLE2 = new RegExp(String.raw`^\s*\{\{\s*(${NAME2})\s*\}\}\s*$`);
5498
+ var EMBED2 = new RegExp(String.raw`\{\{\s*(${NAME2})\s*\}\}`, "g");
5477
5499
  var DEFERRED_NAMESPACES2 = ["secrets", "session"];
5478
5500
  var TemplateError2 = class extends Error {
5479
5501
  constructor(variable) {
@@ -5488,21 +5510,32 @@ function isDeferred2(name) {
5488
5510
  return DEFERRED_NAMESPACES2.includes(ns) && name.includes(".");
5489
5511
  }
5490
5512
  var isFakerName2 = (name) => name.startsWith("faker.");
5491
- function fromFaker2(path) {
5492
- let parent = faker2;
5493
- let cur = faker2;
5494
- for (const part of path.split(".").slice(1)) {
5495
- parent = cur;
5496
- cur = cur?.[part];
5497
- 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";
5498
5518
  }
5499
- 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) };
5500
5532
  }
5533
+ var steps2 = (path) => path.replace(/\[(\d+)\]/g, ".$1").split(".");
5501
5534
  function lookup2(scope, path) {
5502
5535
  if (isFakerName2(path)) return fromFaker2(path);
5503
5536
  let cur = scope;
5504
- for (const part of path.split(".")) {
5505
- if (cur !== null && typeof cur === "object" && part in cur) {
5537
+ for (const part of steps2(path)) {
5538
+ if (cur !== null && typeof cur === "object" && Object.hasOwn(cur, part)) {
5506
5539
  cur = cur[part];
5507
5540
  } else {
5508
5541
  return { found: false };
@@ -5510,7 +5543,19 @@ function lookup2(scope, path) {
5510
5543
  }
5511
5544
  return { found: true, value: cur };
5512
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
+ }
5513
5557
  function renderString2(s, scope) {
5558
+ rejectFakerCall2(s);
5514
5559
  const whole = WHOLE2.exec(s);
5515
5560
  if (whole) {
5516
5561
  const name = whole[1];
@@ -6143,7 +6188,7 @@ async function writeEnvKey(file, key, value) {
6143
6188
  await writeFile3(file, setDotenvKey(current, key, value));
6144
6189
  }
6145
6190
  var IGNORE = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
6146
- var NAME = /\.example\.json$/;
6191
+ var NAME22 = /\.example\.json$/;
6147
6192
  async function scanExamples(root, outDir, operations) {
6148
6193
  const examples = {};
6149
6194
  const assets = {};
@@ -6161,7 +6206,7 @@ async function scanExamples(root, outDir, operations) {
6161
6206
  const [svc, , id, file] = relative22(gen, abs).split(sep22);
6162
6207
  if (!operations.has(`${svc}/${id}`)) continue;
6163
6208
  add(`${svc}/${id}`, {
6164
- name: file.replace(NAME, ""),
6209
+ name: file.replace(NAME22, ""),
6165
6210
  abs,
6166
6211
  source: "generated",
6167
6212
  dir: `${svc}/${id}@generated`
@@ -6175,7 +6220,7 @@ async function scanExamples(root, outDir, operations) {
6175
6220
  const key = `${svc}/${id}`;
6176
6221
  if (!svc || !id || !operations.has(key)) continue;
6177
6222
  add(key, {
6178
- name: file.replace(NAME, ""),
6223
+ name: file.replace(NAME22, ""),
6179
6224
  abs,
6180
6225
  source: "project",
6181
6226
  dir: `${svc}/${id}@project`
@@ -6190,7 +6235,7 @@ async function scanExamples(root, outDir, operations) {
6190
6235
  list.sort((a3, b) => order(a3) - order(b) || a3.name.localeCompare(b.name));
6191
6236
  return { examples, assets };
6192
6237
  }
6193
- 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;
6194
6239
  var IGNORE2 = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
6195
6240
  async function scanProject(root) {
6196
6241
  const rel2 = (e) => e.map((f2) => relative3(root, f2).split(sep3).join("/")).sort();
@@ -6477,7 +6522,7 @@ async function readBody2(req) {
6477
6522
  req.on("error", fail);
6478
6523
  });
6479
6524
  }
6480
- function fakerLocales(entry) {
6525
+ function fakerLocales(entry, cfg) {
6481
6526
  const names = /* @__PURE__ */ new Set(["en", "base"]);
6482
6527
  const dir = join5(dirname2(entry), "locale");
6483
6528
  if (existsSync3(dir))
@@ -6485,8 +6530,11 @@ function fakerLocales(entry) {
6485
6530
  const m = /^([A-Za-z0-9_]+)\.js$/.exec(f2);
6486
6531
  if (m) names.add(m[1]);
6487
6532
  }
6488
- 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();
6489
6536
  }
6537
+ var USES_FAKER = /@specbook\/core\/faker|\{\{\s*faker\./;
6490
6538
  function slim(manifests) {
6491
6539
  return Object.fromEntries(
6492
6540
  Object.entries(manifests).map(([k, m]) => [
@@ -6585,6 +6633,14 @@ function specbook(options) {
6585
6633
  fakerEntry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
6586
6634
  } catch {
6587
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
+ }
6588
6644
  const operationEntries = Object.values(manifests).flatMap((m) => {
6589
6645
  const mm = m;
6590
6646
  return mm.operations.map(
@@ -6956,7 +7012,10 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
6956
7012
  const r = await this.resolve("@faker-js/faker", join5(root, "package.json"), {
6957
7013
  skipSelf: true
6958
7014
  });
6959
- 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
+ );
6960
7019
  const pkg = JSON.stringify(r.id);
6961
7020
  if (id.startsWith(RESOLVED_FAKER_LOCALE))
6962
7021
  return `export { ${id.slice(RESOLVED_FAKER_LOCALE.length)} as default } from ${pkg}`;
@@ -6965,7 +7024,7 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
6965
7024
  entry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
6966
7025
  } catch {
6967
7026
  }
6968
- const names = fakerLocales(entry);
7027
+ const names = fakerLocales(entry, options.faker);
6969
7028
  return `export { Faker, base } from ${pkg}
6970
7029
  export const locales = {
6971
7030
  ${names.map((n) => ` ${JSON.stringify(n)}: () => import(${JSON.stringify(`${VIRTUAL_FAKER_LOCALE}${n}`)}).then((m) => m.default)`).join(",\n")}
@@ -7453,16 +7512,44 @@ ${entry}`
7453
7512
  import { execFileSync as execFileSync3 } from "child_process";
7454
7513
  import { createHash as createHash2 } from "crypto";
7455
7514
  import { existsSync as existsSync7, statSync as statSync2 } from "fs";
7456
- 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";
7457
7516
  import { createRequire as createRequire4 } from "module";
7458
- 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";
7459
7518
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
7460
7519
  import { rolldown } from "rolldown";
7461
7520
 
7462
- // src/ops.ts
7521
+ // src/faker-use.ts
7463
7522
  import { readFile as readFile10 } from "fs/promises";
7464
- import { join as join9, relative as relative7, sep as sep5 } from "path";
7523
+ import { join as join9 } from "path";
7465
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";
7466
7553
  import { parseSync } from "vite";
7467
7554
  var prefixOf = (project) => (project.config.codegen?.importPrefix ?? "#api").replace(/\/+$/, "");
7468
7555
  var IMPORT_RE = /import\s+(type\s+)?(?:([\w$]+)\s*,?\s*)?(?:\*\s+as\s+[\w$]+|\{([^}]*)\})?\s*from\s*['"]([^'"]+)['"]/g;
@@ -7509,7 +7596,7 @@ function importedOperations(file, text, prefix = "#api") {
7509
7596
  return [...found.values()];
7510
7597
  }
7511
7598
  async function projectSources(root) {
7512
- return fg6(["**/*.ts", ".specbook/**/*.ts"], {
7599
+ return fg7(["**/*.ts", ".specbook/**/*.ts"], {
7513
7600
  cwd: root,
7514
7601
  absolute: true,
7515
7602
  dot: true,
@@ -7527,7 +7614,7 @@ async function usedOperations(project) {
7527
7614
  const out = { named: /* @__PURE__ */ new Map(), whole: /* @__PURE__ */ new Set(), where: /* @__PURE__ */ new Map() };
7528
7615
  const prefix = prefixOf(project);
7529
7616
  for (const file of (await projectSources(project.root)).sort()) {
7530
- const text = await readFile10(file, "utf8").catch(() => "");
7617
+ const text = await readFile11(file, "utf8").catch(() => "");
7531
7618
  if (!text.includes(prefix)) continue;
7532
7619
  const rel2 = relative7(project.root, file).split(sep5).join("/");
7533
7620
  for (const imp of importedOperations(file, text, prefix)) {
@@ -7562,10 +7649,10 @@ async function usedOperationKeys(project, outDir) {
7562
7649
  }
7563
7650
  return new Set([...keys].filter((k) => known.has(k)));
7564
7651
  }
7565
- var metaFile = (outDir, service) => join9(outDir, service, "opmeta.json");
7652
+ var metaFile = (outDir, service) => join10(outDir, service, "opmeta.json");
7566
7653
  async function readJson(file) {
7567
7654
  try {
7568
- return JSON.parse(await readFile10(file, "utf8"));
7655
+ return JSON.parse(await readFile11(file, "utf8"));
7569
7656
  } catch {
7570
7657
  return void 0;
7571
7658
  }
@@ -7599,11 +7686,11 @@ async function unknownOperationsMessage(project, outDir) {
7599
7686
  }
7600
7687
 
7601
7688
  // src/runtime.ts
7602
- import { readFile as readFile12 } from "fs/promises";
7689
+ import { readFile as readFile13 } from "fs/promises";
7603
7690
  import { createRequire as createRequire3 } from "module";
7604
- 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";
7605
7692
  import { pathToFileURL } from "url";
7606
- import fg7 from "fast-glob";
7693
+ import fg8 from "fast-glob";
7607
7694
 
7608
7695
  // src/run-tests.ts
7609
7696
  import { execFileSync as execFileSync2 } from "child_process";
@@ -7614,8 +7701,8 @@ import { dirname as dirname5, resolve as resolve7 } from "path";
7614
7701
 
7615
7702
  // src/project.ts
7616
7703
  import { existsSync as existsSync6 } from "fs";
7617
- import { readFile as readFile11 } from "fs/promises";
7618
- 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";
7619
7706
  var CANDIDATES = [
7620
7707
  ".specbook/specbook.config.ts",
7621
7708
  "specbook.config.ts",
@@ -7625,7 +7712,7 @@ function findConfigFile(cwd) {
7625
7712
  let dir = resolve6(cwd);
7626
7713
  for (; ; ) {
7627
7714
  for (const c of CANDIDATES) {
7628
- const p = join10(dir, c);
7715
+ const p = join11(dir, c);
7629
7716
  if (existsSync6(p)) return p;
7630
7717
  }
7631
7718
  const up = dirname4(dir);
@@ -7634,7 +7721,7 @@ function findConfigFile(cwd) {
7634
7721
  }
7635
7722
  }
7636
7723
  function rootOf(configFile) {
7637
- 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);
7638
7725
  }
7639
7726
  async function loadProject(cwd, loader) {
7640
7727
  const configFile = findConfigFile(cwd);
@@ -7645,7 +7732,7 @@ async function loadProject(cwd, loader) {
7645
7732
  }
7646
7733
  async function readDotenv(path) {
7647
7734
  if (!existsSync6(path)) return {};
7648
- return parseDotenv(await readFile11(path, "utf8"));
7735
+ return parseDotenv(await readFile12(path, "utf8"));
7649
7736
  }
7650
7737
 
7651
7738
  // src/version.ts
@@ -7757,13 +7844,16 @@ function transportFor(ctx, env, vars) {
7757
7844
  }
7758
7845
  });
7759
7846
  }
7760
- function originsFor(config, env, vars) {
7847
+ function originsFor(config, env, vars, basePath = false) {
7761
7848
  if (env === "mock") return {};
7762
7849
  const out = {};
7763
7850
  for (const service of Object.keys(config.services)) {
7764
7851
  try {
7765
7852
  const url = baseUrlFor(config, service, env, vars);
7766
- if (url) out[service] = new URL(url).origin;
7853
+ if (url) {
7854
+ const u = new URL(url);
7855
+ out[service] = basePath ? `${u.origin}${u.pathname.replace(/\/+$/, "")}` : u.origin;
7856
+ }
7767
7857
  } catch {
7768
7858
  }
7769
7859
  }
@@ -7844,6 +7934,7 @@ async function execute2(ctx, o, picked, specs, fileVars, secrets) {
7844
7934
  const vars = environmentScope(config, o.env, { ...fileVars, ...given });
7845
7935
  const transport = transportFor(ctx, o.env, vars);
7846
7936
  const origins = originsFor(config, o.env, vars);
7937
+ const addresses = originsFor(config, o.env, vars, true);
7847
7938
  const faker3 = await ctx.loadFaker();
7848
7939
  const git2 = { ...ctx.git ?? gitInfo(ctx.root) };
7849
7940
  const ci = detectCi(process.env);
@@ -7862,7 +7953,7 @@ async function execute2(ctx, o, picked, specs, fileVars, secrets) {
7862
7953
  colors: colors2,
7863
7954
  quiet: o.quiet,
7864
7955
  trace: o.trace,
7865
- origins,
7956
+ origins: addresses,
7866
7957
  cwd: ctx.root,
7867
7958
  readSource,
7868
7959
  rerun: (r) => `${ctx.command}${r.file && ctx.command === "specbook test" ? ` ${r.file}` : ""} --env ${o.env} --grep ${quote3(escapeRegExp(r.title))}${o.safe ? " --safe" : ""}`
@@ -7971,7 +8062,7 @@ async function execute2(ctx, o, picked, specs, fileVars, secrets) {
7971
8062
  toJUnit(clean, {
7972
8063
  timestamp: meta.startedAt,
7973
8064
  hostname: hostname(),
7974
- origins,
8065
+ origins: addresses,
7975
8066
  properties: { env: o.env, sha: git2.sha, branch: git2.branch, specbook: meta.specbook }
7976
8067
  })
7977
8068
  );
@@ -8020,7 +8111,7 @@ async function execute2(ctx, o, picked, specs, fileVars, secrets) {
8020
8111
 
8021
8112
  // src/runtime.ts
8022
8113
  async function discoverTargets(project, loader, patterns) {
8023
- const files = (await fg7(patterns, {
8114
+ const files = (await fg8(patterns, {
8024
8115
  cwd: project.root,
8025
8116
  absolute: true,
8026
8117
  ignore: ["**/node_modules/**", "**/.generated/**"]
@@ -8039,7 +8130,7 @@ function mockLoaderFor(project) {
8039
8130
  const manifest = (service) => {
8040
8131
  let m = manifests.get(service);
8041
8132
  if (!m) {
8042
- m = readFile12(join11(outDir, service, "manifest.json"), "utf8").then(JSON.parse);
8133
+ m = readFile13(join12(outDir, service, "manifest.json"), "utf8").then(JSON.parse);
8043
8134
  manifests.set(service, m);
8044
8135
  }
8045
8136
  return m;
@@ -8053,8 +8144,8 @@ function mockLoaderFor(project) {
8053
8144
  for (const name of [String(status), "default"]) {
8054
8145
  try {
8055
8146
  return JSON.parse(
8056
- await readFile12(
8057
- join11(outDir, service, "examples", opId2, `${name}.response.json`),
8147
+ await readFile13(
8148
+ join12(outDir, service, "examples", opId2, `${name}.response.json`),
8058
8149
  "utf8"
8059
8150
  )
8060
8151
  );
@@ -8067,7 +8158,7 @@ function mockLoaderFor(project) {
8067
8158
  }
8068
8159
  async function loadFaker(project) {
8069
8160
  try {
8070
- 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");
8071
8162
  return await import(pathToFileURL(entry).href);
8072
8163
  } catch {
8073
8164
  return void 0;
@@ -8095,9 +8186,9 @@ function git(root) {
8095
8186
  function standalonePath() {
8096
8187
  const here = dirname6(fileURLToPath(import.meta.url));
8097
8188
  for (const p of [
8098
- join12(here, "standalone.js"),
8099
- join12(here, "..", "standalone.js"),
8100
- join12(here, "..", "standalone.ts")
8189
+ join13(here, "standalone.js"),
8190
+ join13(here, "..", "standalone.js"),
8191
+ join13(here, "..", "standalone.ts")
8101
8192
  ])
8102
8193
  if (existsSync7(p)) return p;
8103
8194
  throw new Error("The test runner is missing from this installation of specbook");
@@ -8105,7 +8196,7 @@ function standalonePath() {
8105
8196
  async function walk(dir) {
8106
8197
  const entries = await readdir3(dir, { withFileTypes: true }).catch(() => []);
8107
8198
  return (await Promise.all(
8108
- 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)])
8109
8200
  )).flat();
8110
8201
  }
8111
8202
  var SKIP = /(^|\/)(node_modules|\.generated|\.git|dist|dist-[^/]+)(\/|$)/;
@@ -8113,15 +8204,15 @@ var CODE = /\.(ts|tsx|mts|cts|js|mjs|cjs|md)$/;
8113
8204
  async function embed(project, outDir, used) {
8114
8205
  const root = project.root;
8115
8206
  const out = { examples: {}, files: {}, bytes: 0 };
8116
- const read = async (file) => JSON.parse(await readFile13(file, "utf8"));
8207
+ const read = async (file) => JSON.parse(await readFile14(file, "utf8"));
8117
8208
  for (const [service, ops] of Object.entries(used))
8118
8209
  for (const id of Object.keys(ops)) {
8119
- const dir = join12(outDir, service, "examples", id);
8210
+ const dir = join13(outDir, service, "examples", id);
8120
8211
  for (const f2 of await readdir3(dir).catch(() => [])) {
8121
8212
  const m = /^(.+)\.example\.json$/.exec(f2);
8122
8213
  if (!m) continue;
8123
8214
  out.examples[`${service}/${id}/${m[1]}`] = {
8124
- doc: await read(join12(dir, f2)),
8215
+ doc: await read(join13(dir, f2)),
8125
8216
  source: "generated",
8126
8217
  dir: ""
8127
8218
  };
@@ -8146,24 +8237,21 @@ async function embed(project, outDir, used) {
8146
8237
  for (const abs of await walk(folder)) {
8147
8238
  const rel2 = posix(relative9(root, abs));
8148
8239
  if (SKIP.test(rel2) || CODE.test(rel2) || rel2.endsWith(".example.json")) continue;
8149
- const data2 = await readFile13(abs);
8240
+ const data2 = await readFile14(abs);
8150
8241
  out.files[rel2] = data2.toString("base64");
8151
8242
  out.bytes += data2.length;
8152
8243
  }
8153
8244
  return out;
8154
8245
  }
8155
- async function fakerSource(project, sources) {
8246
+ async function fakerSource(project) {
8156
8247
  let entry;
8157
8248
  try {
8158
- entry = createRequire4(join12(project.root, "package.json")).resolve("@faker-js/faker");
8249
+ entry = createRequire4(join13(project.root, "package.json")).resolve("@faker-js/faker");
8159
8250
  } catch {
8160
8251
  return void 0;
8161
8252
  }
8162
- if (!project.config.faker && !/faker/.test(sources)) return void 0;
8163
8253
  const cfg = project.config.faker ?? {};
8164
- const wanted = /* @__PURE__ */ new Set(["Faker", ...fakerLocaleNames(cfg)]);
8165
- for (const name of Object.keys(cfg.locales ?? {}))
8166
- for (const n of fakerLocaleNames(cfg, name)) wanted.add(n);
8254
+ const wanted = /* @__PURE__ */ new Set(["Faker", ...fakerLocaleExports(cfg)]);
8167
8255
  const have = new Set(Object.keys(await import(pathToFileURL2(entry).href)));
8168
8256
  const names = [...wanted].filter((n) => have.has(n)).sort();
8169
8257
  return `export { ${names.join(", ")} } from ${JSON.stringify(entry)}
@@ -8193,11 +8281,10 @@ async function bundleCommand(project, loader, o) {
8193
8281
  const files = [...new Set(chosen.map((t) => t.file))].sort();
8194
8282
  const ops = await usedMetadata(project, outDir);
8195
8283
  const sourceText = (await Promise.all(
8196
- [project.configFile, ...files.map((f2) => join12(root, f2))].map(
8197
- (f2) => readFile13(f2, "utf8").catch(() => "")
8284
+ [project.configFile, ...files.map((f2) => join13(root, f2))].map(
8285
+ (f2) => readFile14(f2, "utf8").catch(() => "")
8198
8286
  )
8199
8287
  )).join("\n");
8200
- const faker3 = await fakerSource(project, sourceText);
8201
8288
  const embedded = await embed(project, outDir, ops.services);
8202
8289
  const environments = {};
8203
8290
  for (const env of await listEnvironmentFiles(root))
@@ -8211,48 +8298,48 @@ async function bundleCommand(project, loader, o) {
8211
8298
  examples: embedded.examples,
8212
8299
  files: embedded.files
8213
8300
  };
8214
- const entry = [
8215
- `import { main } from ${JSON.stringify(standalonePath())}`,
8216
- `import config from ${JSON.stringify(project.configFile)}`,
8217
- ...files.map((f2, i) => `import * as m${i} from ${JSON.stringify(join12(root, f2))}`),
8218
- ...faker3 ? [`import * as fakerModule from ${JSON.stringify(FAKER)}`] : [],
8219
- `const data = { ...${JSON.stringify(data2)}, config,`,
8220
- ` modules: [${files.map((f2, i) => `{ file: ${JSON.stringify(f2)}, mod: m${i} }`).join(", ")}],`,
8221
- ` faker: ${faker3 ? "fakerModule" : "undefined"} }`,
8222
- "main(data).then((code) => process.exit(code), (e) => {",
8223
- " console.error(e instanceof Error ? e.message : e)",
8224
- " process.exit(2)",
8225
- "})"
8226
- ].join("\n");
8227
8301
  const ours = createRequire4(import.meta.url);
8228
- const plugin = {
8229
- name: "specbook-bundle",
8230
- resolveId(id) {
8231
- if (id === ENTRY) return ENTRY;
8232
- if (id === FAKER) return FAKER;
8233
- if (id.startsWith(`${prefix}/`) && !id.slice(prefix.length + 1).includes("/"))
8234
- return `\0specbook:api:${id.slice(prefix.length + 1)}`;
8235
- if (id === "@specbook/core" || id.startsWith("@specbook/core/")) return ours.resolve(id);
8236
- return void 0;
8237
- },
8238
- load(id) {
8239
- if (id === ENTRY) return entry;
8240
- if (id === FAKER) return faker3;
8241
- if (!id.startsWith("\0specbook:api:")) return void 0;
8242
- const service = id.slice("\0specbook:api:".length);
8243
- const metas = ops.services[service];
8244
- if (!metas) throw new Error(`Service "${service}" has no operations your requests use`);
8245
- return [
8246
- "import { defineOperation } from '@specbook/core'",
8247
- ...Object.entries(metas).map(
8248
- ([opId2, meta]) => `export const ${opId2} = /* @__PURE__ */ defineOperation(${JSON.stringify(meta)})`
8249
- ),
8250
- ""
8251
- ].join("\n");
8252
- }
8253
- };
8254
8302
  const warnings = [];
8255
- 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
+ };
8256
8343
  const build2 = await rolldown({
8257
8344
  input: ENTRY,
8258
8345
  platform: "node",
@@ -8265,7 +8352,7 @@ async function bundleCommand(project, loader, o) {
8265
8352
  }
8266
8353
  });
8267
8354
  await mkdir6(dirname6(out), { recursive: true });
8268
- await build2.write({
8355
+ const written = await build2.write({
8269
8356
  file: out,
8270
8357
  format: "esm",
8271
8358
  minify: true,
@@ -8273,18 +8360,43 @@ async function bundleCommand(project, loader, o) {
8273
8360
  banner: "#!/usr/bin/env node"
8274
8361
  });
8275
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
+ }
8276
8388
  } catch (e) {
8277
8389
  console.error(
8278
8390
  `Could not compile the tests into one file:
8279
8391
  ${e instanceof Error ? e.message : e}`
8280
8392
  );
8281
8393
  console.error(
8282
- "A package that cannot be compiled in (a native addon, a computed require) can stay an import: --external <package>."
8394
+ "A package that cannot be compiled in (a native addon, a computed require) can stay an import: --external <package>. A browser cannot load it, so replace it with fetch or plain code if the request must also run in the portal."
8283
8395
  );
8284
8396
  return 2;
8285
8397
  }
8286
8398
  await chmod(out, 493);
8287
- const bytes = await readFile13(out);
8399
+ const bytes = await readFile14(out);
8288
8400
  const hash = sha2562(bytes);
8289
8401
  await writeFile7(`${out}.sha256`, `${hash} ${basename4(out)}
8290
8402
  `);
@@ -8304,9 +8416,9 @@ ${e instanceof Error ? e.message : e}`
8304
8416
 
8305
8417
  // src/commands/check.ts
8306
8418
  import { existsSync as existsSync8 } from "fs";
8307
- import { readFile as readFile14 } from "fs/promises";
8308
- import { join as join13, resolve as resolve10 } from "path";
8309
- 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";
8310
8422
  async function checkCommand(project, loader) {
8311
8423
  const errors = [];
8312
8424
  const warnings = [];
@@ -8325,8 +8437,8 @@ async function checkCommand(project, loader) {
8325
8437
  if (untracked.includes(name)) continue;
8326
8438
  let drift = 0;
8327
8439
  for (const f2 of files2) {
8328
- const p = join13(outDir, name, f2.path);
8329
- 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++;
8330
8442
  }
8331
8443
  if (drift)
8332
8444
  errors.push(
@@ -8405,7 +8517,7 @@ async function checkCommand(project, loader) {
8405
8517
  ...fileVarNames
8406
8518
  ]);
8407
8519
  const declared = new Set(Object.keys(cfg.secrets ?? {}));
8408
- const files = await fg8("**/*.req.ts", {
8520
+ const files = await fg9("**/*.req.ts", {
8409
8521
  cwd: project.root,
8410
8522
  absolute: true,
8411
8523
  ignore: ["**/node_modules/**", "**/.generated/**"]
@@ -8432,9 +8544,9 @@ async function checkCommand(project, loader) {
8432
8544
  ...def.dataPatches
8433
8545
  ]);
8434
8546
  const init2 = new Set(Object.keys(def.input.init ?? {}));
8435
- const roots = new Set([...used2].map((n) => n.split(".")[0]));
8547
+ const roots = new Set([...used2].map(rootName));
8436
8548
  for (const name of used2) {
8437
- const root = name.split(".")[0];
8549
+ const root = rootName(name);
8438
8550
  if (name.startsWith("secrets.")) {
8439
8551
  if (!declared.has(name.slice(8)))
8440
8552
  warnings.push(`[${rel2}] secret "${name.slice(8)}" is not declared in config.secrets`);
@@ -8468,15 +8580,125 @@ async function generateCommand(project, loader, only) {
8468
8580
  }
8469
8581
 
8470
8582
  // src/commands/init.ts
8471
- import { existsSync as existsSync9 } from "fs";
8472
- import { mkdir as mkdir7, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
8473
- import { join as join14 } from "path";
8474
- async function initCommand(cwd) {
8475
- const dir = join14(cwd, ".specbook");
8583
+ import { existsSync as existsSync10 } from "fs";
8584
+ import { mkdir as mkdir8, readFile as readFile17, writeFile as writeFile9 } from "fs/promises";
8585
+ import { join as join16 } from "path";
8586
+
8587
+ // src/commands/agents.ts
8588
+ import { existsSync as existsSync9, realpathSync } from "fs";
8589
+ import { mkdir as mkdir7, readFile as readFile16, writeFile as writeFile8 } from "fs/promises";
8590
+ import { join as join15 } from "path";
8591
+ import { fileURLToPath as fileURLToPath2 } from "url";
8592
+ var FILES = ["AGENTS.md", "CLAUDE.md"];
8593
+ var START = "<!-- specbook:start";
8594
+ var END = "<!-- specbook:end -->";
8595
+ function agentBlock(eol = "\n") {
8596
+ return [
8597
+ `${START} (written by specbook init; \`specbook init --agents\` refreshes it) -->`,
8598
+ "## Specbook",
8599
+ "",
8600
+ "This project uses Specbook: requests (`requests/**/*.req.ts`) and flows (`sequences/**/*.seq.ts`) built from OpenAPI specs run as",
8601
+ "the docs, as tests and as monitors. Before adding or changing one, read `.specbook/agent-guide.md`. In short:",
8602
+ "",
8603
+ "- Import operations from `#api/<service>`. Never edit `.generated/`.",
8604
+ "- Label every `assert`, and use `capture` to pass values between steps.",
8605
+ "- Write isomorphic code: requests and flows also run in the browser, so no Node built-ins, `Buffer` or `process.env`.",
8606
+ "- Run `npx specbook check` and `npx specbook test` before you finish.",
8607
+ END
8608
+ ].join(eol);
8609
+ }
8610
+ function withBlock(text) {
8611
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
8612
+ const block = agentBlock(eol);
8613
+ const start = text.indexOf(START);
8614
+ if (start >= 0) {
8615
+ const end = text.indexOf(END, start);
8616
+ if (end < 0) return { text, status: "unterminated" };
8617
+ const next = text.slice(0, start) + block + text.slice(end + END.length);
8618
+ return { text: next, status: next === text ? "unchanged" : "updated" };
8619
+ }
8620
+ if (!text.trim()) return { text: `${block}${eol}`, status: "added" };
8621
+ const lead = text.endsWith("\n") ? "" : eol;
8622
+ return { text: `${text}${lead}${eol}${block}${eol}`, status: "added" };
8623
+ }
8624
+ var importsAgents = (text) => /^@AGENTS\.md\b/m.test(text);
8625
+ async function readGuide() {
8626
+ for (const rel2 of ["../agent-guide.md", "../../agent-guide.md"])
8627
+ try {
8628
+ return await readFile16(fileURLToPath2(new URL(rel2, import.meta.url)), "utf8");
8629
+ } catch {
8630
+ }
8631
+ throw new Error("agent-guide.md is missing from the @specbook/cli package");
8632
+ }
8633
+ async function agentsStep(cwd, agents) {
8634
+ if (agents === false) return;
8635
+ const dir = join15(cwd, ".specbook");
8476
8636
  await mkdir7(dir, { recursive: true });
8477
- const cfg = join14(dir, "specbook.config.ts");
8478
- if (!existsSync9(cfg)) {
8637
+ const guide = await readGuide();
8638
+ await writeFile8(
8639
+ join15(dir, "agent-guide.md"),
8640
+ `<!-- Written by specbook init v${cliVersion()}. Re-run \`specbook init\` to refresh it, and put project rules in AGENTS.md. -->
8641
+
8642
+ ${guide}`
8643
+ );
8644
+ console.log("\u2713 wrote .specbook/agent-guide.md");
8645
+ const seen = /* @__PURE__ */ new Set();
8646
+ const found = [];
8647
+ for (const f2 of FILES) {
8648
+ if (!existsSync9(join15(cwd, f2))) continue;
8649
+ const real = realpathSync(join15(cwd, f2));
8650
+ if (seen.has(real)) continue;
8651
+ seen.add(real);
8652
+ found.push(f2);
8653
+ }
8654
+ if (found.length === 0) {
8479
8655
  await writeFile8(
8656
+ join15(cwd, "AGENTS.md"),
8657
+ `# AGENTS.md
8658
+
8659
+ Instructions for AI coding agents working in this repository.
8660
+
8661
+ ${agentBlock()}
8662
+ `
8663
+ );
8664
+ console.log("\u2713 created AGENTS.md (read by Claude Code, Codex and other coding agents)");
8665
+ return;
8666
+ }
8667
+ if (!agents) {
8668
+ console.log(
8669
+ `${found.join(" and ")} already ${found.length > 1 ? "exist" : "exists"} and was not changed. To add Specbook's section to ${found.length > 1 ? "them" : "it"}: specbook init --agents`
8670
+ );
8671
+ return;
8672
+ }
8673
+ for (const file of found) {
8674
+ const path = join15(cwd, file);
8675
+ const text = await readFile16(path, "utf8");
8676
+ if (file === "CLAUDE.md" && found.includes("AGENTS.md") && importsAgents(text)) {
8677
+ console.log("CLAUDE.md imports AGENTS.md: its section covers Claude Code too");
8678
+ continue;
8679
+ }
8680
+ const r = withBlock(text);
8681
+ if (r.status === "unterminated") {
8682
+ console.log(`! ${file} has a "${START}" marker with no end marker; left unchanged`);
8683
+ continue;
8684
+ }
8685
+ if (r.status === "unchanged") {
8686
+ console.log(`${file} already has Specbook's section`);
8687
+ continue;
8688
+ }
8689
+ await writeFile8(path, r.text);
8690
+ console.log(`\u2713 ${r.status === "added" ? "added" : "updated"} Specbook's section in ${file}`);
8691
+ }
8692
+ }
8693
+
8694
+ // src/commands/init.ts
8695
+ var FAKER_VERSION = "9.3.0";
8696
+ async function initCommand(cwd, opts = {}) {
8697
+ const dir = join16(cwd, ".specbook");
8698
+ await mkdir8(dir, { recursive: true });
8699
+ const cfg = join16(dir, "specbook.config.ts");
8700
+ if (!existsSync10(cfg)) {
8701
+ await writeFile9(
8480
8702
  cfg,
8481
8703
  `import { defineConfig } from '@specbook/core'
8482
8704
 
@@ -8495,55 +8717,63 @@ export default defineConfig({
8495
8717
  console.log("\u2713 created .specbook/specbook.config.ts");
8496
8718
  }
8497
8719
  const envDir = await ensureEnvironmentsDir(cwd);
8498
- const example = join14(envDir, "staging.secrets.env.example");
8499
- if (!existsSync9(example)) {
8500
- await writeFile8(
8720
+ const example = join16(envDir, "staging.secrets.env.example");
8721
+ if (!existsSync10(example)) {
8722
+ await writeFile9(
8501
8723
  example,
8502
8724
  "# Copy to staging.secrets.env (gitignored) and fill in. One KEY=value per line.\n# YOUR_API_KEY=\n"
8503
8725
  );
8504
- await writeFile8(
8505
- join14(envDir, "staging.env"),
8726
+ await writeFile9(
8727
+ join16(envDir, "staging.env"),
8506
8728
  '# Non-secret variables for the "staging" environment (committed).\n# region=eu-west-1\n'
8507
8729
  );
8508
8730
  console.log("\u2713 created .specbook/environments/ (staging.env, staging.secrets.env.example)");
8509
8731
  }
8510
- const pkgPath = join14(cwd, "package.json");
8511
- const pkg = existsSync9(pkgPath) ? JSON.parse(await readFile15(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" };
8512
8734
  pkg.imports = { ...pkg.imports ?? {}, "#api/*": "./.generated/*/index.ts" };
8513
8735
  pkg.scripts = { ...pkg.scripts ?? {} };
8514
8736
  if (!pkg.scripts.prepare) pkg.scripts.prepare = "specbook generate";
8515
- await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
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;
8742
+ await writeFile9(pkgPath, `${JSON.stringify(pkg, null, 2)}
8516
8743
  `);
8517
- console.log('\u2713 added the "#api/*" subpath import and a "prepare" script to package.json');
8518
- const gi = join14(cwd, ".gitignore");
8519
- const current = existsSync9(gi) ? await readFile15(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") : "";
8520
8749
  const missing = [".generated", ".specbook/secrets.local.env"].filter(
8521
8750
  (entry) => !current.split("\n").some((l) => l.trim() === entry)
8522
8751
  );
8523
8752
  if (missing.length) {
8524
- await writeFile8(
8753
+ await writeFile9(
8525
8754
  gi,
8526
8755
  `${current}${current.endsWith("\n") || !current ? "" : "\n"}${missing.join("\n")}
8527
8756
  `
8528
8757
  );
8529
8758
  console.log(`\u2713 gitignored ${missing.join(", ")}`);
8530
8759
  }
8760
+ await agentsStep(cwd, opts.agents);
8531
8761
  }
8532
8762
 
8533
8763
  // src/commands/preview.ts
8534
8764
  import { spawn } from "child_process";
8535
- import { existsSync as existsSync10, readFileSync as readFileSync2 } from "fs";
8536
- import { mkdir as mkdir8, mkdtemp as mkdtemp2, rm as rm4, symlink, writeFile as writeFile10 } from "fs/promises";
8765
+ import { existsSync as existsSync11, readFileSync as readFileSync2 } from "fs";
8766
+ import { mkdir as mkdir9, mkdtemp as mkdtemp2, rm as rm4, symlink, writeFile as writeFile11 } from "fs/promises";
8537
8767
  import { createRequire as createRequire6 } from "module";
8538
8768
  import { tmpdir as tmpdir2 } from "os";
8539
- import { dirname as dirname8, extname, join as join16, resolve as resolve12 } from "path";
8769
+ import { dirname as dirname8, extname, join as join18, resolve as resolve12 } from "path";
8540
8770
  import SwaggerParser4 from "@apidevtools/swagger-parser";
8541
8771
 
8542
8772
  // src/commands/ui.ts
8543
- import { copyFile, cp, readFile as readFile16, stat, writeFile as writeFile9 } from "fs/promises";
8773
+ import { copyFile, cp, readFile as readFile18, stat, writeFile as writeFile10 } from "fs/promises";
8544
8774
  import { createRequire as createRequire5 } from "module";
8545
- import { basename as basename5, dirname as dirname7, join as join15, resolve as resolve11, sep as sep8 } from "path";
8546
- import { fileURLToPath as fileURLToPath2 } from "url";
8775
+ import { basename as basename5, dirname as dirname7, join as join17, resolve as resolve11, sep as sep8 } from "path";
8776
+ import { fileURLToPath as fileURLToPath3 } from "url";
8547
8777
  import { build, createServer } from "vite";
8548
8778
  function uiRoot() {
8549
8779
  const require2 = createRequire5(import.meta.url);
@@ -8562,10 +8792,10 @@ function enclosingNodeModules(file) {
8562
8792
  }
8563
8793
  function dependencyFolders(root) {
8564
8794
  const found = /* @__PURE__ */ new Set([
8565
- ...enclosingNodeModules(fileURLToPath2(import.meta.url)),
8566
- ...enclosingNodeModules(join15(root, "package.json"))
8795
+ ...enclosingNodeModules(fileURLToPath3(import.meta.url)),
8796
+ ...enclosingNodeModules(join17(root, "package.json"))
8567
8797
  ]);
8568
- for (const from of [join15(root, "package.json"), fileURLToPath2(import.meta.url)]) {
8798
+ for (const from of [join17(root, "package.json"), fileURLToPath3(import.meta.url)]) {
8569
8799
  const require2 = createRequire5(from);
8570
8800
  for (const name of ["vite", "prism-code-editor", "preact", "@preact/signals"]) {
8571
8801
  try {
@@ -8590,7 +8820,12 @@ function uiConfig(project, operations) {
8590
8820
  rolldownOptions: { transform: { jsx: { runtime: "automatic", importSource: "preact" } } }
8591
8821
  },
8592
8822
  plugins: [
8593
- 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
+ })
8594
8829
  ],
8595
8830
  server: {
8596
8831
  fs: {
@@ -8623,7 +8858,7 @@ async function devCommand(project, port = 5173) {
8623
8858
  }
8624
8859
  async function buildCommand(project, outDir, options = {}) {
8625
8860
  const out = resolve11(project.root, outDir);
8626
- const generated = join15(project.root, project.config.codegen?.outDir ?? ".generated");
8861
+ const generated = join17(project.root, project.config.codegen?.outDir ?? ".generated");
8627
8862
  const operations = options.minimal ? await usedOperationKeys(project, generated) : void 0;
8628
8863
  if (operations && operations.size === 0)
8629
8864
  throw new Error("Nothing to publish: no request or flow imports an operation (see --minimal)");
@@ -8645,25 +8880,25 @@ async function buildCommand(project, outDir, options = {}) {
8645
8880
  }
8646
8881
  }
8647
8882
  });
8648
- await copyMetadata(join15(generated, "specbook.json"), join15(out, "specbook.json"), operations);
8883
+ await copyMetadata(join17(generated, "specbook.json"), join17(out, "specbook.json"), operations);
8649
8884
  }
8650
8885
  async function copyMetadata(from, to, operations) {
8651
8886
  try {
8652
8887
  if (!operations) return await copyFile(from, to);
8653
- const meta = JSON.parse(await readFile16(from, "utf8"));
8888
+ const meta = JSON.parse(await readFile18(from, "utf8"));
8654
8889
  for (const [svc, s] of Object.entries(meta.services)) {
8655
8890
  s.operations = Object.fromEntries(
8656
8891
  Object.entries(s.operations).filter(([id]) => operations.has(`${svc}/${id}`))
8657
8892
  );
8658
8893
  if (Object.keys(s.operations).length === 0) delete meta.services[svc];
8659
8894
  }
8660
- await writeFile9(to, `${JSON.stringify(meta)}
8895
+ await writeFile10(to, `${JSON.stringify(meta)}
8661
8896
  `);
8662
8897
  } catch {
8663
8898
  }
8664
8899
  }
8665
8900
  async function copyViewer(project, outDir, viewerDir = resolve11(uiRoot(), "dist-viewer")) {
8666
- const has2 = await stat(join15(viewerDir, "index.html")).then(
8901
+ const has2 = await stat(join17(viewerDir, "index.html")).then(
8667
8902
  () => true,
8668
8903
  () => false
8669
8904
  );
@@ -8682,10 +8917,10 @@ var slug3 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/
8682
8917
  async function link(root, name) {
8683
8918
  try {
8684
8919
  let dir = dirname8(createRequire6(import.meta.url).resolve(name));
8685
- while (!existsSync10(join16(dir, "package.json")) || JSON.parse(readFileSync2(join16(dir, "package.json"), "utf8")).name !== name)
8920
+ while (!existsSync11(join18(dir, "package.json")) || JSON.parse(readFileSync2(join18(dir, "package.json"), "utf8")).name !== name)
8686
8921
  dir = dirname8(dir);
8687
- const dest = join16(root, "node_modules", ...name.split("/"));
8688
- await mkdir8(dirname8(dest), { recursive: true });
8922
+ const dest = join18(root, "node_modules", ...name.split("/"));
8923
+ await mkdir9(dirname8(dest), { recursive: true });
8689
8924
  await symlink(dir, dest, "dir");
8690
8925
  } catch {
8691
8926
  }
@@ -8705,7 +8940,7 @@ async function previewCommand(source, o = {}) {
8705
8940
  const { servers } = openApiServers(api, source);
8706
8941
  const where = servers.length === 0 ? "" : servers.length === 1 && servers[0]?.name === "live" ? `, live calls go to ${servers[0].url}` : `
8707
8942
  Environments: ${servers.map((s) => `${s.name} \u2192 ${s.url}`).join(", ")}`;
8708
- const root = await mkdtemp2(join16(tmpdir2(), "specbook-preview-"));
8943
+ const root = await mkdtemp2(join18(tmpdir2(), "specbook-preview-"));
8709
8944
  const cleanup = () => rm4(root, { recursive: true, force: true }).catch(() => {
8710
8945
  });
8711
8946
  process.once("SIGINT", () => cleanup().then(() => process.exit(0)));
@@ -8715,9 +8950,9 @@ async function previewCommand(source, o = {}) {
8715
8950
  const res = await fetch(source);
8716
8951
  if (!res.ok) throw new Error(`Could not fetch ${source} (${res.status})`);
8717
8952
  const ext = /ya?ml/.test(extname(new URL(source).pathname)) ? ".yaml" : ".json";
8718
- specFile = join16(root, `openapi/api${ext}`);
8719
- await mkdir8(dirname8(specFile), { recursive: true });
8720
- await writeFile10(specFile, await res.text());
8953
+ specFile = join18(root, `openapi/api${ext}`);
8954
+ await mkdir9(dirname8(specFile), { recursive: true });
8955
+ await writeFile11(specFile, await res.text());
8721
8956
  }
8722
8957
  const {
8723
8958
  auth,
@@ -8750,14 +8985,14 @@ async function previewCommand(source, o = {}) {
8750
8985
  }
8751
8986
  }
8752
8987
  };
8753
- const configFile = join16(root, ".specbook/specbook.config.ts");
8754
- await mkdir8(dirname8(configFile), { recursive: true });
8755
- await writeFile10(configFile, `export default ${JSON.stringify(config, null, 2)}
8988
+ const configFile = join18(root, ".specbook/specbook.config.ts");
8989
+ await mkdir9(dirname8(configFile), { recursive: true });
8990
+ await writeFile11(configFile, `export default ${JSON.stringify(config, null, 2)}
8756
8991
  `);
8757
8992
  await link(root, "@specbook/core");
8758
8993
  await link(root, "@faker-js/faker");
8759
8994
  console.log(`Generating "${title}" \u2026`);
8760
- const out = await generateToDisk({ spec: specFile, service, outDir: join16(root, ".generated") });
8995
+ const out = await generateToDisk({ spec: specFile, service, outDir: join18(root, ".generated") });
8761
8996
  console.log(`\u2713 ${out.operations.length} operations${where}`);
8762
8997
  const project = { root, configFile, config };
8763
8998
  const server = await devCommand(project, o.port ?? 5173);
@@ -8826,18 +9061,18 @@ import { resolve as resolve15 } from "path";
8826
9061
 
8827
9062
  // src/examples.ts
8828
9063
  import { File } from "buffer";
8829
- import { readFile as readFile17 } from "fs/promises";
8830
- import { basename as basename6, dirname as dirname9, join as join17, resolve as resolve14 } from "path";
8831
- import fg9 from "fast-glob";
8832
- 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/**"];
8833
9068
  function exampleLoadersFor(project) {
8834
9069
  const outDir = resolve14(project.root, project.config.codegen?.outDir ?? ".generated");
8835
9070
  const example = async (op, name) => {
8836
9071
  const rel2 = `${op.service}/${op.id}/${name}.example.json`;
8837
- const [custom] = await fg9(`**/${rel2}`, { cwd: project.root, absolute: true, ignore: IGNORE3 });
8838
- const file = custom ?? join17(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`);
8839
9074
  try {
8840
- const doc = JSON.parse(await readFile17(file, "utf8"));
9075
+ const doc = JSON.parse(await readFile19(file, "utf8"));
8841
9076
  return { ...doc, source: custom ? "project" : "generated", dir: dirname9(file) };
8842
9077
  } catch {
8843
9078
  return void 0;
@@ -8845,7 +9080,7 @@ function exampleLoadersFor(project) {
8845
9080
  };
8846
9081
  const read = async (path, dir) => {
8847
9082
  const abs = resolve14(dir ?? project.root, path);
8848
- return new File([await readFile17(abs)], basename6(abs));
9083
+ return new File([await readFile19(abs)], basename6(abs));
8849
9084
  };
8850
9085
  return { example, readFile: read };
8851
9086
  }
@@ -8876,7 +9111,8 @@ async function testCommand(project, loader, o) {
8876
9111
  readEnvironment: (env) => readEnvironment(project.root, env),
8877
9112
  examples: exampleLoadersFor(project),
8878
9113
  mock: mockLoaderFor(project),
8879
- 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,
8880
9116
  readSource: (file) => {
8881
9117
  try {
8882
9118
  return readFileSync3(file, "utf8");
@@ -8960,7 +9196,7 @@ var TEST_HELP = ` --env <name> environment (default: mock, for
8960
9196
  --input k=v flow input value (repeatable)
8961
9197
  --var k=v variable override (repeatable)
8962
9198
  --secrets-file <path> dotenv file with secrets
8963
- --locale <l> --seed <n> synthetic data locale / deterministic seed
9199
+ --locale <l> --seed <n> generated values: locale / seed for a repeatable run
8964
9200
  --notify <url> POST a summary to a webhook when something fails`;
8965
9201
  var parseValue = (v2) => {
8966
9202
  try {
@@ -9012,7 +9248,9 @@ function testOptions(values, patterns) {
9012
9248
  // src/bin.ts
9013
9249
  var HELP = `specbook <command>
9014
9250
 
9015
- init scaffold config, #api/* import and the environments folder
9251
+ init scaffold config, #api/* import, the environments folder and an agent guide
9252
+ --agents add a Specbook section to an existing AGENTS.md or CLAUDE.md (never edited otherwise)
9253
+ --no-agents skip the agent guide and AGENTS.md
9016
9254
  add <service> --spec <p|url> register an OpenAPI spec in the config and generate it; an address is downloaded to
9017
9255
  openapi/ and recorded in openapi/sources.json
9018
9256
  add --refresh [service...] download the recorded addresses again, say what changed and regenerate it
@@ -9028,7 +9266,7 @@ var HELP = `specbook <command>
9028
9266
  ${TEST_HELP}
9029
9267
  bundle [--out <file>] compile the tests into one .mjs that runs with just node: for CI or ops, no install
9030
9268
  --tag <t> only the requests and flows with this tag (repeatable)
9031
- --external <pkg> leave a package as an import (it must be installed where the file runs; repeatable)
9269
+ --external <pkg> leave a package as an import (installed where the file runs; a browser cannot load it; repeatable)
9032
9270
  check validate specs, generated code drift, imported operations, placeholders and environment files
9033
9271
  secrets [--env <name>] show where each declared secret resolves from (never values)
9034
9272
  --secrets-file <path> include a dotenv file, as test would
@@ -9046,6 +9284,8 @@ async function main() {
9046
9284
  out: { type: "string" },
9047
9285
  external: { type: "string", multiple: true },
9048
9286
  "no-open": { type: "boolean" },
9287
+ agents: { type: "boolean" },
9288
+ "no-agents": { type: "boolean" },
9049
9289
  viewer: { type: "boolean" },
9050
9290
  minimal: { type: "boolean" },
9051
9291
  help: { type: "boolean", short: "h" }
@@ -9070,7 +9310,13 @@ async function main() {
9070
9310
  });
9071
9311
  }
9072
9312
  if (cmd === "init") {
9073
- await initCommand(cwd);
9313
+ if (values.agents && values["no-agents"]) {
9314
+ console.error("init: pass --agents or --no-agents, not both\n");
9315
+ return 3;
9316
+ }
9317
+ await initCommand(cwd, {
9318
+ agents: values["no-agents"] ? false : values.agents ? true : void 0
9319
+ });
9074
9320
  return 0;
9075
9321
  }
9076
9322
  if (!["generate", "test", "bundle", "dev", "build", "add", "check", "secrets"].includes(cmd)) {