@crvouga/mockingbird-service-slack 1.7.0 → 1.8.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/CHANGELOG.md CHANGED
@@ -1,14 +1,31 @@
1
1
  # Changelog — @crvouga/mockingbird-service-slack
2
2
 
3
- ## 1.7.0 (2026-10-01)
3
+ ## 1.8.0 (2026-10-02)
4
4
 
5
5
  ### Features
6
6
 
7
- - record SQL timelines and serve the admin explorer ([8ced17a](https://github.com/crvouga/mockingbird/commit/8ced17ac398e386ef4e9d7e01fc7744c34b95d07))
7
+ - coordinate HTTP and protocol mocks ([3168653](https://github.com/crvouga/mockingbird/commit/31686534c55a9823fdaa92afc9cc456cf741b809))
8
+
9
+ ### Fixes and improvements
8
10
 
9
- ### Dependencies
11
+ - decouple smoke types from built packages ([a4b31c6](https://github.com/crvouga/mockingbird/commit/a4b31c6205d92aa0c6f7c9de3518d5ae44d38829))
12
+ - protect child admin endpoints ([5410b94](https://github.com/crvouga/mockingbird/commit/5410b948a30dcedc21e9a70e44fd415c1ea25394))
13
+ - harden protocol startup and state ([6f5b557](https://github.com/crvouga/mockingbird/commit/6f5b557e87b48b31985949e04f3b5ef58f51d295))
14
+ - preserve checkpoints and lifecycle ([50e8c32](https://github.com/crvouga/mockingbird/commit/50e8c322feded2f7f45afce9d8cdef43c37ab9ab))
15
+ - clean branch state on reset ([3b714d4](https://github.com/crvouga/mockingbird/commit/3b714d4a5df8e75fee0bb31e98d7c7566e164381))
16
+ - type signal listener cleanup ([5279852](https://github.com/crvouga/mockingbird/commit/5279852e56ea60262ccf17aa6e46b3c31091a34a))
17
+
18
+ ## 1.7.1 (2026-10-01)
19
+
20
+ ### Fixes and improvements
10
21
 
11
- - `@crvouga/mockingbird-service-sqlite`
22
+ - freeze clock before setting time ([a054a1d](https://github.com/crvouga/mockingbird/commit/a054a1defad62539d719b1d31c15107afea623bd))
23
+
24
+ ## 1.7.0 (2026-09-30)
25
+
26
+ ### Features
27
+
28
+ - record SQL timelines and serve the admin explorer ([8ced17a](https://github.com/crvouga/mockingbird/commit/8ced17ac398e386ef4e9d7e01fc7744c34b95d07))
12
29
 
13
30
  ## 1.6.1 (2026-09-30)
14
31
 
@@ -1,16 +1,25 @@
1
1
  import {
2
2
  bindSocketTransport,
3
3
  claimSocketTicket,
4
+ createClock,
4
5
  createRuntime,
5
6
  markSocketClosed,
6
7
  openSocketCount,
8
+ parseDuration,
7
9
  unbindSocketTransport
8
- } from "./chunk-E47OMSFP.js";
10
+ } from "./chunk-RJIEOQZA.js";
9
11
 
10
12
  // ../../adapters/node/dist/cli.js
11
- import { readFile } from "node:fs/promises";
13
+ import { readFile as readFile2 } from "node:fs/promises";
14
+ import { createRequire } from "node:module";
15
+ import { resolve } from "node:path";
16
+ import { pathToFileURL } from "node:url";
12
17
  import { parseArgs } from "node:util";
13
18
 
19
+ // ../../adapters/node/dist/fleet.js
20
+ import { randomUUID } from "node:crypto";
21
+ import { chmod, readFile, rename, rm, writeFile } from "node:fs/promises";
22
+
14
23
  // ../../adapters/node/dist/serve.js
15
24
  import { createServer } from "node:http";
16
25
  var serve = async (api, options = {}) => {
@@ -72,7 +81,7 @@ var serve = async (api, options = {}) => {
72
81
  if (done)
73
82
  break;
74
83
  if (!res.write(value))
75
- await new Promise((resolve) => res.once("drain", resolve));
84
+ await new Promise((resolve2) => res.once("drain", resolve2));
76
85
  }
77
86
  res.end();
78
87
  } catch {
@@ -81,9 +90,9 @@ var serve = async (api, options = {}) => {
81
90
  reader.releaseLock();
82
91
  }
83
92
  });
84
- await new Promise((resolve, reject) => {
93
+ await new Promise((resolve2, reject) => {
85
94
  server.once("error", reject);
86
- server.listen(options.port ?? 0, options.host, resolve);
95
+ server.listen(options.port ?? 0, options.host, resolve2);
87
96
  });
88
97
  return server;
89
98
  };
@@ -100,18 +109,445 @@ var listen = async (api, options = {}) => {
100
109
  port,
101
110
  host,
102
111
  server,
103
- close: () => new Promise((resolve, reject) => {
112
+ close: () => new Promise((resolve2, reject) => {
104
113
  server.close((error) => {
105
114
  if (error && error.code !== "ERR_SERVER_NOT_RUNNING")
106
115
  reject(error);
107
116
  else
108
- resolve();
117
+ resolve2();
109
118
  });
110
119
  server.closeAllConnections?.();
111
120
  })
112
121
  };
113
122
  };
114
123
 
124
+ // ../../adapters/node/dist/fleet.js
125
+ var EMPTY = {
126
+ activeRequests: 0,
127
+ activeConnections: 0,
128
+ blockedCommands: 0,
129
+ pendingJobs: 0,
130
+ pendingWebhooks: 0,
131
+ unmatchedRequests: 0
132
+ };
133
+ var NS = /^[A-Za-z0-9_.-]{1,64}$/;
134
+ var json = (status, value) => Response.json(value, { status });
135
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
136
+ var pending = (d) => d.activeRequests + d.blockedCommands + d.pendingJobs + d.pendingWebhooks;
137
+ var publicUrl = (raw) => {
138
+ const url = new URL(raw);
139
+ url.password = "";
140
+ return url.href;
141
+ };
142
+ var atomicWrite = async (path, value) => {
143
+ const temp = `${path}.${randomUUID()}.tmp`;
144
+ try {
145
+ await writeFile(temp, `${JSON.stringify(value, null, 2)}
146
+ `, { mode: 384, flag: "wx" });
147
+ await chmod(temp, 384);
148
+ await rename(temp, path);
149
+ } finally {
150
+ await rm(temp, { force: true });
151
+ }
152
+ };
153
+ var removeOwned = async (path, id) => {
154
+ if (!path)
155
+ return;
156
+ try {
157
+ const value = JSON.parse(await readFile(path, "utf8"));
158
+ if (isObject(value) && value.id === id)
159
+ await rm(path, { force: true });
160
+ } catch (error) {
161
+ if (error.code !== "ENOENT")
162
+ throw error;
163
+ }
164
+ };
165
+ var httpChild = async (target, entry, options) => {
166
+ const runtime = await target.create(entry.options ?? {}, {
167
+ adminKey: entry.adminKey,
168
+ seed: entry.seed,
169
+ onLog: options.onLog
170
+ });
171
+ runtime.isolateNamespaces();
172
+ const locked = /* @__PURE__ */ new Set();
173
+ const active = /* @__PURE__ */ new Map();
174
+ const api = {
175
+ async fetch(request) {
176
+ const namespace = runtime.namespaceOf(request);
177
+ if (locked.has(namespace))
178
+ return json(409, { error: "fleet control in progress", namespace });
179
+ active.set(namespace, (active.get(namespace) ?? 0) + 1);
180
+ try {
181
+ return await runtime.fetch(request);
182
+ } finally {
183
+ active.set(namespace, (active.get(namespace) ?? 1) - 1);
184
+ }
185
+ }
186
+ };
187
+ let server;
188
+ try {
189
+ server = await listen(api, {
190
+ port: entry.port ?? target.defaultPort,
191
+ host: entry.host ?? "127.0.0.1"
192
+ });
193
+ target.listening?.(server);
194
+ } catch (error) {
195
+ runtime.stop?.();
196
+ await server?.close();
197
+ runtime.webhooks?.clear();
198
+ await runtime.webhooks?.idle();
199
+ throw error;
200
+ }
201
+ return {
202
+ protocol: "http",
203
+ url: server.url,
204
+ healthUrl: `${server.url}/health`,
205
+ adminUrl: `${server.url}/__admin`,
206
+ namespaces: { header: "x-mockingbird-namespace", path: "/ns/{name}" },
207
+ ready: async () => {
208
+ try {
209
+ return (await fetch(`${server.url}/health`, { signal: AbortSignal.timeout(2e3) })).ok;
210
+ } catch {
211
+ return false;
212
+ }
213
+ },
214
+ async close() {
215
+ runtime.stop?.();
216
+ runtime.webhooks?.clear();
217
+ await server.close();
218
+ await runtime.webhooks?.idle();
219
+ },
220
+ lock(namespace) {
221
+ if (locked.has(namespace))
222
+ throw new Error("namespace already locked");
223
+ locked.add(namespace);
224
+ return () => locked.delete(namespace);
225
+ },
226
+ diagnostics(namespace) {
227
+ return {
228
+ ...EMPTY,
229
+ activeRequests: active.get(namespace) ?? 0,
230
+ pendingWebhooks: runtime.webhooks?.pending(namespace) ?? 0,
231
+ unmatchedRequests: runtime.metrics.report(namespace).unmatched.reduce((n, r) => n + r.count, 0)
232
+ };
233
+ },
234
+ async checkpoint(namespace) {
235
+ return runtime.fleetSnapshot(namespace);
236
+ },
237
+ async restore(namespace, checkpoint) {
238
+ runtime.fleetRestore(checkpoint, namespace);
239
+ },
240
+ reset: (namespace) => runtime.reset(namespace),
241
+ clock: (namespace) => runtime.namespaceClock(namespace),
242
+ async settle(namespace) {
243
+ const instance = runtime.instance(namespace);
244
+ instance.tick?.();
245
+ await runtime.webhooks?.idle();
246
+ }
247
+ };
248
+ };
249
+ async function startFleet(config, options) {
250
+ const id = randomUUID();
251
+ const startedAt2 = (/* @__PURE__ */ new Date()).toISOString();
252
+ const children = /* @__PURE__ */ new Map();
253
+ const clocks = /* @__PURE__ */ new Map();
254
+ const checkpoints = /* @__PURE__ */ new Map();
255
+ let supervisor;
256
+ let stopped = false;
257
+ let controlling = false;
258
+ const close = async () => {
259
+ if (stopped)
260
+ return;
261
+ stopped = true;
262
+ await Promise.allSettled([...children.values()].map((child) => child.close()));
263
+ await supervisor?.close();
264
+ await removeOwned(options.readyFile, id);
265
+ await removeOwned(options.connectionsFile, id);
266
+ };
267
+ if (options.readyFile)
268
+ await rm(options.readyFile, { force: true });
269
+ if (options.connectionsFile)
270
+ await rm(options.connectionsFile, { force: true });
271
+ const clock = (namespace) => {
272
+ let value = clocks.get(namespace);
273
+ if (!value) {
274
+ value = createClock();
275
+ clocks.set(namespace, value);
276
+ }
277
+ return value;
278
+ };
279
+ const select = (body) => {
280
+ if (body.services === void 0)
281
+ return children;
282
+ if (!Array.isArray(body.services) || body.services.length === 0)
283
+ throw new Error("services must be a nonempty list");
284
+ const selected = /* @__PURE__ */ new Map();
285
+ for (const name of body.services) {
286
+ if (typeof name !== "string" || !children.has(name))
287
+ throw new Error("unknown service in selection");
288
+ selected.set(name, children.get(name));
289
+ }
290
+ return selected;
291
+ };
292
+ const diagnostics = (namespace) => Object.fromEntries([...children].map(([name, child]) => [name, child.diagnostics(namespace)]));
293
+ const transaction = async (namespace, selected, run) => {
294
+ if (controlling)
295
+ return json(409, { error: "another fleet operation is in progress" });
296
+ controlling = true;
297
+ const release = [];
298
+ const before = /* @__PURE__ */ new Map();
299
+ const results = {};
300
+ let failure;
301
+ try {
302
+ for (const [name, child] of selected) {
303
+ failure = name;
304
+ release.push(child.lock(namespace));
305
+ }
306
+ const busy = Object.fromEntries([...selected].filter(([, child]) => {
307
+ const state = child.diagnostics(namespace);
308
+ return state.activeRequests + state.blockedCommands + state.pendingWebhooks + (state.activeJobs ?? 0) > 0;
309
+ }).map(([name, child]) => [name, child.diagnostics(namespace)]));
310
+ if (Object.keys(busy).length)
311
+ return json(409, { error: "namespace is not quiescent", services: busy });
312
+ for (const [name, child] of selected) {
313
+ failure = name;
314
+ before.set(name, await child.checkpoint(namespace));
315
+ }
316
+ for (const [name, child] of selected) {
317
+ failure = name;
318
+ results[name] = { status: "ok", result: await run(name, child) };
319
+ }
320
+ return json(200, { status: "ok", namespace, services: results });
321
+ } catch {
322
+ const rollback = {};
323
+ for (const [name, point] of before) {
324
+ try {
325
+ await selected.get(name)?.restore(namespace, point);
326
+ rollback[name] = "restored";
327
+ } catch {
328
+ rollback[name] = "failed";
329
+ }
330
+ }
331
+ return json(500, {
332
+ status: "failed",
333
+ namespace,
334
+ failedService: failure,
335
+ services: results,
336
+ rollback,
337
+ rolledBack: before.size > 0 && Object.values(rollback).every((value) => value === "restored")
338
+ });
339
+ } finally {
340
+ for (const unlock of release.reverse())
341
+ unlock();
342
+ controlling = false;
343
+ }
344
+ };
345
+ const api = {
346
+ async fetch(request) {
347
+ const url = new URL(request.url);
348
+ if (url.pathname === "/health" && request.method === "GET") {
349
+ const states = await Promise.all([...children].map(async ([name, child]) => {
350
+ let protocolReady = false;
351
+ try {
352
+ protocolReady = await child.ready();
353
+ } catch {
354
+ }
355
+ return [
356
+ name,
357
+ {
358
+ processReady: !stopped,
359
+ protocolReady,
360
+ status: protocolReady ? "ready" : "failed",
361
+ url: publicUrl(child.url)
362
+ }
363
+ ];
364
+ }));
365
+ const ready = !stopped && states.every(([, state]) => state.protocolReady);
366
+ return json(ready ? 200 : 503, {
367
+ status: ready ? "ready" : "failed",
368
+ id,
369
+ services: Object.fromEntries(states)
370
+ });
371
+ }
372
+ if (!url.pathname.startsWith("/__fleet/"))
373
+ return json(404, { error: "not found" });
374
+ const adminKey = config.adminKey ?? process.env.MOCKINGBIRD_ADMIN_KEY;
375
+ if (adminKey !== void 0 && request.headers.get("x-mockingbird-admin-key") !== adminKey)
376
+ return json(401, { error: "admin key required" });
377
+ const route = /^\/__fleet\/namespaces\/([^/]+)\/(reset|snapshots)(?:\/([^/]+)\/restore)?$/.exec(url.pathname);
378
+ const namespace = route ? decodeURIComponent(route[1]) : url.searchParams.get("namespace") ?? request.headers.get("x-mockingbird-namespace") ?? config.namespace ?? "default";
379
+ if (!NS.test(namespace))
380
+ return json(400, { error: "invalid namespace" });
381
+ if (request.method === "GET" && url.pathname === "/__fleet/metrics")
382
+ return json(200, { namespace, services: diagnostics(namespace) });
383
+ if (request.method === "GET" && url.pathname === "/__fleet/clock")
384
+ return json(200, {
385
+ namespace,
386
+ ...clock(namespace).state(),
387
+ services: Object.fromEntries([...children].map(([name, child]) => [name, child.clock(namespace).state()]))
388
+ });
389
+ if (request.method !== "POST")
390
+ return json(405, { error: "method not allowed" });
391
+ let body;
392
+ try {
393
+ const text2 = await request.text();
394
+ const value = text2 ? JSON.parse(text2) : {};
395
+ if (!isObject(value))
396
+ throw new Error("expected object");
397
+ body = value;
398
+ } catch {
399
+ return json(400, { error: "expected JSON object" });
400
+ }
401
+ let selected;
402
+ try {
403
+ selected = select(body);
404
+ } catch (error) {
405
+ return json(400, { error: error instanceof Error ? error.message : "bad selection" });
406
+ }
407
+ if (url.pathname === "/__fleet/wait-until-idle") {
408
+ const services = diagnostics(namespace);
409
+ const idle = Object.values(services).every((d) => pending(d) === 0);
410
+ return json(idle ? 200 : 409, { status: idle ? "idle" : "busy", namespace, services });
411
+ }
412
+ if (url.pathname === "/__fleet/clock") {
413
+ const current = clock(namespace);
414
+ let target = current.now();
415
+ if (body.set !== void 0) {
416
+ target = typeof body.set === "number" ? body.set : typeof body.set === "string" ? Date.parse(body.set) : NaN;
417
+ if (!Number.isFinite(target))
418
+ return json(400, { error: "set must be epoch ms or ISO-8601" });
419
+ }
420
+ if (body.advance !== void 0) {
421
+ const delta = parseDuration(body.advance);
422
+ if (delta === void 0)
423
+ return json(400, { error: "invalid advance" });
424
+ target += delta;
425
+ }
426
+ const frozen = body.freeze === void 0 ? current.state().frozen : body.freeze;
427
+ if (typeof frozen !== "boolean")
428
+ return json(400, { error: "freeze must be boolean" });
429
+ const response2 = await transaction(namespace, selected, async (_name, child) => {
430
+ const childClock = child.clock(namespace);
431
+ childClock.freeze();
432
+ childClock.set(target);
433
+ if (!frozen)
434
+ childClock.unfreeze();
435
+ await child.settle?.(namespace);
436
+ return childClock.state();
437
+ });
438
+ if (response2.ok) {
439
+ current.freeze();
440
+ current.set(target);
441
+ if (!frozen)
442
+ current.unfreeze();
443
+ }
444
+ return response2;
445
+ }
446
+ if (!route)
447
+ return json(404, { error: "not found" });
448
+ if (route[2] === "reset") {
449
+ if (route[3])
450
+ return json(404, { error: "not found" });
451
+ const response2 = await transaction(namespace, selected, async (_name, child) => child.reset(namespace));
452
+ if (response2.ok && selected.size === children.size)
453
+ clock(namespace).reset();
454
+ return response2;
455
+ }
456
+ if (route[3]) {
457
+ const point = checkpoints.get(route[3]);
458
+ if (!point || point.namespace !== namespace)
459
+ return json(404, { error: "no snapshot in this namespace" });
460
+ const restored = new Map([...point.values].map(([name]) => [name, children.get(name)]));
461
+ const response2 = await transaction(namespace, restored, async (name, child) => child.restore(namespace, point.values.get(name)));
462
+ if (response2.ok) {
463
+ const current = clock(namespace);
464
+ current.freeze();
465
+ current.set(point.clock.now);
466
+ if (!point.clock.frozen)
467
+ current.unfreeze();
468
+ }
469
+ return response2;
470
+ }
471
+ const snapshotId = randomUUID();
472
+ const values = /* @__PURE__ */ new Map();
473
+ const response = await transaction(namespace, selected, async (name, child) => {
474
+ values.set(name, await child.checkpoint(namespace));
475
+ return { checkpoint: `${snapshotId}:${name}` };
476
+ });
477
+ if (!response.ok)
478
+ return response;
479
+ checkpoints.set(snapshotId, { namespace, values, clock: clock(namespace).state() });
480
+ return json(201, {
481
+ ...await response.json(),
482
+ id: snapshotId,
483
+ label: body.label
484
+ });
485
+ }
486
+ };
487
+ try {
488
+ if (!isObject(config.services) || Object.keys(config.services).length === 0)
489
+ throw new Error("config must contain at least one service");
490
+ for (const [name, entry] of Object.entries(config.services)) {
491
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
492
+ throw new Error("invalid service name");
493
+ try {
494
+ const target = await options.load(name);
495
+ if (entry.protocol && entry.protocol !== ("protocol" in target ? target.protocol : "http"))
496
+ throw new Error("configured protocol does not match target");
497
+ const adminKey = entry.adminKey ?? config.adminKey ?? process.env.MOCKINGBIRD_ADMIN_KEY;
498
+ const child = "start" in target ? await target.start(entry) : await httpChild(target, { ...entry, ...adminKey !== void 0 ? { adminKey } : {} }, options);
499
+ children.set(name, child);
500
+ if (!await child.ready())
501
+ throw new Error("protocol readiness failed");
502
+ } catch {
503
+ throw new Error(`service ${name} failed to start; all started listeners were closed`);
504
+ }
505
+ }
506
+ supervisor = await listen(api, {
507
+ port: config.control?.port ?? 0,
508
+ host: config.control?.host ?? "127.0.0.1"
509
+ });
510
+ const manifest = {
511
+ version: 1,
512
+ state: "ready",
513
+ pid: process.pid,
514
+ id,
515
+ startedAt: startedAt2,
516
+ adminBase: `${supervisor.url}/__fleet`,
517
+ healthUrl: `${supervisor.url}/health`,
518
+ services: Object.fromEntries([...children].map(([name, child]) => [
519
+ name,
520
+ {
521
+ protocol: child.protocol,
522
+ url: publicUrl(child.url),
523
+ healthUrl: child.healthUrl ?? `${supervisor?.url}/health`,
524
+ adminUrl: child.adminUrl ?? `${supervisor?.url}/__fleet`,
525
+ namespaces: child.namespaces,
526
+ processReady: true,
527
+ protocolReady: true
528
+ }
529
+ ]))
530
+ };
531
+ if (options.connectionsFile)
532
+ await atomicWrite(options.connectionsFile, {
533
+ version: 1,
534
+ id,
535
+ pid: process.pid,
536
+ services: Object.fromEntries([...children].map(([name, child]) => [
537
+ name,
538
+ { url: child.connection ?? child.url, namespaces: child.connections }
539
+ ]))
540
+ });
541
+ if (options.readyFile)
542
+ await atomicWrite(options.readyFile, manifest);
543
+ options.onReady?.(manifest);
544
+ return { manifest, children, fetch: api.fetch, close };
545
+ } catch (error) {
546
+ await close();
547
+ throw error;
548
+ }
549
+ }
550
+
115
551
  // ../../adapters/node/dist/cli.js
116
552
  var optionHelp = (options) => Object.entries(options).map(([name, option]) => {
117
553
  const flag = `--${name}${option.type === "string" ? ` ${option.value ?? "<value>"}` : ""}`;
@@ -201,6 +637,20 @@ var COMMON_SERVE_OPTIONS = {
201
637
  type: "string",
202
638
  value: "<file>",
203
639
  description: "Serve every service in a mockingbird.json config instead"
640
+ },
641
+ "ready-file": {
642
+ type: "string",
643
+ description: "Atomically publish fleet endpoint JSON",
644
+ value: "<file>"
645
+ },
646
+ "connections-file": {
647
+ type: "string",
648
+ description: "Write private connection URLs with mode 0600",
649
+ value: "<file>"
650
+ },
651
+ "ready-json": {
652
+ type: "boolean",
653
+ description: "Emit one public JSON record after fleet readiness"
204
654
  }
205
655
  };
206
656
  var formatLog = (format) => {
@@ -224,7 +674,13 @@ var loadTarget = async (name, own) => {
224
674
  return own;
225
675
  const specifier = `@crvouga/mockingbird-service-${name}/server`;
226
676
  try {
227
- const mod = await import(specifier);
677
+ let mod;
678
+ try {
679
+ mod = await import(specifier);
680
+ } catch {
681
+ const fromProject = createRequire(resolve(process.cwd(), "package.json"));
682
+ mod = await import(pathToFileURL(fromProject.resolve(specifier)).href);
683
+ }
228
684
  if (!mod.serveTarget)
229
685
  throw new Error(`${specifier} exports no serveTarget`);
230
686
  return mod.serveTarget;
@@ -249,17 +705,20 @@ var start = async (target, values, config) => {
249
705
  console.log(`${target.name} ${line}`);
250
706
  return listening;
251
707
  };
252
- var untilSignal = async (servers) => new Promise((resolve) => {
708
+ var untilSignal = async (servers) => new Promise((resolve2) => {
253
709
  const stop = async () => {
710
+ ;
711
+ process.removeListener("SIGINT", stop);
712
+ process.removeListener("SIGTERM", stop);
254
713
  await Promise.allSettled(servers.map((s) => s.close()));
255
- resolve(0);
714
+ resolve2(0);
256
715
  };
257
716
  process.once("SIGINT", stop);
258
717
  process.once("SIGTERM", stop);
259
718
  });
260
719
  var serveCommand = (target) => ({
261
- summary: `Serve the ${target.name} mock over HTTP`,
262
- options: { ...COMMON_SERVE_OPTIONS, ...target.options },
720
+ summary: `Serve the ${target.name} mock or a configured fleet`,
721
+ options: { ...COMMON_SERVE_OPTIONS, ..."options" in target ? target.options : {} },
263
722
  async run(values) {
264
723
  const log = values["log-requests"] === true ? "json" : asString(values.log) ?? "pretty";
265
724
  if (!["pretty", "json", "off"].includes(log)) {
@@ -268,25 +727,33 @@ var serveCommand = (target) => ({
268
727
  }
269
728
  const configPath = asString(values.config);
270
729
  if (configPath !== void 0) {
271
- const config = JSON.parse(await readFile(configPath, "utf8"));
272
- const servers = [];
273
730
  try {
274
- for (const [name, entry] of Object.entries(config.services ?? {})) {
275
- const each = await loadTarget(name, target);
276
- servers.push(await start(each, entry.options ?? {}, {
277
- port: entry.port ?? each.defaultPort,
278
- host: entry.host ?? "127.0.0.1",
279
- ...entry.adminKey !== void 0 ? { adminKey: entry.adminKey } : {},
280
- ...entry.seed !== void 0 ? { seed: entry.seed } : {},
281
- log: config.log ?? log
282
- }));
283
- }
731
+ const config = JSON.parse(await readFile2(configPath, "utf8"));
732
+ const adminKey2 = config.adminKey ?? asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
733
+ if (adminKey2 !== void 0)
734
+ config.adminKey = adminKey2;
735
+ const fleet = await startFleet(config, {
736
+ load: (name) => loadTarget(name, target),
737
+ onLog: formatLog(config.log ?? log),
738
+ ...asString(values["ready-file"]) ? { readyFile: asString(values["ready-file"]) } : {},
739
+ ...asString(values["connections-file"]) ? { connectionsFile: asString(values["connections-file"]) } : {},
740
+ onReady: (manifest) => {
741
+ if (values["ready-json"])
742
+ console.log(JSON.stringify(manifest));
743
+ else
744
+ for (const [name, endpoint] of Object.entries(manifest.services))
745
+ console.log(`${name} mock listening on ${endpoint.url}`);
746
+ }
747
+ });
748
+ return untilSignal([fleet]);
284
749
  } catch (error) {
285
- await Promise.allSettled(servers.map((s) => s.close()));
286
750
  console.error(error instanceof Error ? error.message : String(error));
287
751
  return 1;
288
752
  }
289
- return untilSignal(servers);
753
+ }
754
+ if ("start" in target) {
755
+ console.error("Use --config to supervise protocol services with this command");
756
+ return 2;
290
757
  }
291
758
  const port = asString(values.port);
292
759
  const adminKey = asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
@@ -650,4 +1117,4 @@ export {
650
1117
  createServer2 as createServer,
651
1118
  serveTarget
652
1119
  };
653
- //# sourceMappingURL=chunk-3RJHDGHW.js.map
1120
+ //# sourceMappingURL=chunk-BDX5J7DR.js.map