@emulates/docker 0.0.0-stage → 1.0.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/cli.js ADDED
@@ -0,0 +1,805 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ serveTarget
4
+ } from "./chunk-PYYL7YKU.js";
5
+ import {
6
+ createClock,
7
+ parseDuration,
8
+ resolveAdminPrefix
9
+ } from "./chunk-U6P6W4BK.js";
10
+
11
+ // ../../adapters/node/dist/cli.js
12
+ import { readFile as readFile2 } from "node:fs/promises";
13
+ import { createRequire } from "node:module";
14
+ import { resolve } from "node:path";
15
+ import { pathToFileURL } from "node:url";
16
+ import { parseArgs } from "node:util";
17
+
18
+ // ../../adapters/node/dist/fleet.js
19
+ import { randomUUID } from "node:crypto";
20
+ import { chmod, readFile, rename, rm, writeFile } from "node:fs/promises";
21
+
22
+ // ../../adapters/node/dist/serve.js
23
+ import { createServer } from "node:http";
24
+ var serve = async (api, options = {}) => {
25
+ const server = createServer(async (req, res) => {
26
+ const chunks = [];
27
+ for await (const chunk of req) {
28
+ chunks.push(Buffer.from(chunk));
29
+ }
30
+ const body = Buffer.concat(chunks);
31
+ const address = server.address();
32
+ const port = typeof address === "object" && address !== null ? address.port : void 0;
33
+ const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`;
34
+ const raw = req.url ?? "/";
35
+ const url = new URL(raw.replace(/^\/+/, "/"), base);
36
+ const method = req.method ?? "GET";
37
+ const init = { method, headers: req.headers };
38
+ if (method !== "GET" && method !== "HEAD" && body.length > 0) {
39
+ init.body = body;
40
+ }
41
+ const aborted = new AbortController();
42
+ res.once("close", () => {
43
+ if (!res.writableFinished)
44
+ aborted.abort();
45
+ });
46
+ init.signal = aborted.signal;
47
+ const request = new Request(url, init);
48
+ let response;
49
+ try {
50
+ response = await api.fetch(request);
51
+ } catch (error) {
52
+ if (error.code === "EMULATES_DROP") {
53
+ req.socket.destroy();
54
+ return;
55
+ }
56
+ res.writeHead(500, { "content-type": "application/json" });
57
+ res.end(JSON.stringify({
58
+ error: {
59
+ type: "emulates_internal",
60
+ message: error instanceof Error ? error.message : String(error)
61
+ }
62
+ }));
63
+ return;
64
+ }
65
+ const headers = Object.fromEntries(response.headers);
66
+ const cookies = response.headers.getSetCookie();
67
+ if (cookies.length > 0)
68
+ headers["set-cookie"] = cookies;
69
+ if (!response.body) {
70
+ res.writeHead(response.status, headers);
71
+ res.end();
72
+ return;
73
+ }
74
+ res.writeHead(response.status, headers);
75
+ res.flushHeaders();
76
+ const reader = response.body.getReader();
77
+ try {
78
+ for (; ; ) {
79
+ const { done, value } = await reader.read();
80
+ if (done)
81
+ break;
82
+ if (!res.write(value))
83
+ await new Promise((resolve2) => res.once("drain", resolve2));
84
+ }
85
+ res.end();
86
+ } catch {
87
+ res.destroy();
88
+ } finally {
89
+ reader.releaseLock();
90
+ }
91
+ });
92
+ await new Promise((resolve2, reject) => {
93
+ server.once("error", reject);
94
+ server.listen(options.port ?? 0, options.host, resolve2);
95
+ });
96
+ return server;
97
+ };
98
+
99
+ // ../../adapters/node/dist/listen.js
100
+ var listen = async (api, options = {}) => {
101
+ const host = options.host ?? "127.0.0.1";
102
+ const server = await serve(api, { port: options.port ?? 0, host });
103
+ const address = server.address();
104
+ const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
105
+ const shown = host.includes(":") ? `[${host}]` : host;
106
+ return {
107
+ url: `http://${shown}:${port}`,
108
+ port,
109
+ host,
110
+ server,
111
+ close: () => new Promise((resolve2, reject) => {
112
+ server.close((error) => {
113
+ if (error && error.code !== "ERR_SERVER_NOT_RUNNING")
114
+ reject(error);
115
+ else
116
+ resolve2();
117
+ });
118
+ server.closeAllConnections?.();
119
+ })
120
+ };
121
+ };
122
+
123
+ // ../../adapters/node/dist/fleet.js
124
+ var EMPTY = {
125
+ activeRequests: 0,
126
+ activeConnections: 0,
127
+ blockedCommands: 0,
128
+ pendingJobs: 0,
129
+ pendingWebhooks: 0,
130
+ unmatchedRequests: 0
131
+ };
132
+ var NS = /^[A-Za-z0-9_.-]{1,64}$/;
133
+ var json = (status, value) => Response.json(value, { status });
134
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
135
+ var pending = (d) => d.activeRequests + d.blockedCommands + d.pendingJobs + d.pendingWebhooks;
136
+ var publicUrl = (raw) => {
137
+ const url = new URL(raw);
138
+ url.password = "";
139
+ return url.href;
140
+ };
141
+ var atomicWrite = async (path, value) => {
142
+ const temp = `${path}.${randomUUID()}.tmp`;
143
+ try {
144
+ await writeFile(temp, `${JSON.stringify(value, null, 2)}
145
+ `, { mode: 384, flag: "wx" });
146
+ await chmod(temp, 384);
147
+ await rename(temp, path);
148
+ } finally {
149
+ await rm(temp, { force: true });
150
+ }
151
+ };
152
+ var removeOwned = async (path, id) => {
153
+ if (!path)
154
+ return;
155
+ try {
156
+ const value = JSON.parse(await readFile(path, "utf8"));
157
+ if (isObject(value) && value.id === id)
158
+ await rm(path, { force: true });
159
+ } catch (error) {
160
+ if (error.code !== "ENOENT")
161
+ throw error;
162
+ }
163
+ };
164
+ var httpChild = async (target, entry, options) => {
165
+ const runtime = await target.create(entry.options ?? {}, {
166
+ adminPrefix: resolveAdminPrefix(entry.adminPrefix),
167
+ adminKey: entry.adminKey,
168
+ seed: entry.seed,
169
+ onLog: options.onLog
170
+ });
171
+ const adminPrefix = resolveAdminPrefix(entry.adminPrefix);
172
+ runtime.isolateNamespaces();
173
+ const locked = /* @__PURE__ */ new Set();
174
+ const active = /* @__PURE__ */ new Map();
175
+ const api = {
176
+ async fetch(request) {
177
+ const namespace = runtime.namespaceOf(request);
178
+ if (locked.has(namespace))
179
+ return json(409, { error: "fleet control in progress", namespace });
180
+ active.set(namespace, (active.get(namespace) ?? 0) + 1);
181
+ try {
182
+ return await runtime.fetch(request);
183
+ } finally {
184
+ active.set(namespace, (active.get(namespace) ?? 1) - 1);
185
+ }
186
+ }
187
+ };
188
+ let server;
189
+ try {
190
+ server = await listen(api, {
191
+ port: entry.port ?? target.defaultPort,
192
+ host: entry.host ?? "127.0.0.1"
193
+ });
194
+ target.listening?.(server);
195
+ } catch (error) {
196
+ runtime.stop?.();
197
+ await server?.close();
198
+ runtime.webhooks?.clear();
199
+ await runtime.webhooks?.idle();
200
+ throw error;
201
+ }
202
+ return {
203
+ protocol: "http",
204
+ url: server.url,
205
+ healthUrl: `${server.url}${adminPrefix}/health`,
206
+ adminUrl: `${server.url}${adminPrefix}`,
207
+ namespaces: { header: "x-emulates-namespace", path: `${adminPrefix}/ns/{name}` },
208
+ ready: async () => {
209
+ try {
210
+ return (await fetch(`${server.url}${adminPrefix}/health`, { signal: AbortSignal.timeout(2e3) })).ok;
211
+ } catch {
212
+ return false;
213
+ }
214
+ },
215
+ async close() {
216
+ runtime.stop?.();
217
+ runtime.webhooks?.clear();
218
+ await server.close();
219
+ await runtime.webhooks?.idle();
220
+ },
221
+ lock(namespace) {
222
+ if (locked.has(namespace))
223
+ throw new Error("namespace already locked");
224
+ locked.add(namespace);
225
+ return () => locked.delete(namespace);
226
+ },
227
+ diagnostics(namespace) {
228
+ return {
229
+ ...EMPTY,
230
+ activeRequests: active.get(namespace) ?? 0,
231
+ pendingWebhooks: runtime.webhooks?.pending(namespace) ?? 0,
232
+ unmatchedRequests: runtime.metrics.report(namespace).unmatched.reduce((n, r) => n + r.count, 0)
233
+ };
234
+ },
235
+ async checkpoint(namespace) {
236
+ return runtime.fleetSnapshot(namespace);
237
+ },
238
+ async restore(namespace, checkpoint) {
239
+ runtime.fleetRestore(checkpoint, namespace);
240
+ },
241
+ reset: (namespace) => runtime.reset(namespace),
242
+ clock: (namespace) => runtime.namespaceClock(namespace),
243
+ async settle(namespace) {
244
+ const instance = runtime.instance(namespace);
245
+ instance.tick?.();
246
+ await runtime.webhooks?.idle();
247
+ }
248
+ };
249
+ };
250
+ async function startFleet(config, options) {
251
+ const id = randomUUID();
252
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
253
+ const children = /* @__PURE__ */ new Map();
254
+ const clocks = /* @__PURE__ */ new Map();
255
+ const checkpoints = /* @__PURE__ */ new Map();
256
+ let supervisor;
257
+ let stopped = false;
258
+ let controlling = false;
259
+ const close = async () => {
260
+ if (stopped)
261
+ return;
262
+ stopped = true;
263
+ await Promise.allSettled([...children.values()].map((child) => child.close()));
264
+ await supervisor?.close();
265
+ await removeOwned(options.readyFile, id);
266
+ await removeOwned(options.connectionsFile, id);
267
+ };
268
+ if (options.readyFile)
269
+ await rm(options.readyFile, { force: true });
270
+ if (options.connectionsFile)
271
+ await rm(options.connectionsFile, { force: true });
272
+ const clock = (namespace) => {
273
+ let value = clocks.get(namespace);
274
+ if (!value) {
275
+ value = createClock();
276
+ clocks.set(namespace, value);
277
+ }
278
+ return value;
279
+ };
280
+ const select = (body) => {
281
+ if (body.services === void 0)
282
+ return children;
283
+ if (!Array.isArray(body.services) || body.services.length === 0)
284
+ throw new Error("services must be a nonempty list");
285
+ const selected = /* @__PURE__ */ new Map();
286
+ for (const name of body.services) {
287
+ if (typeof name !== "string" || !children.has(name))
288
+ throw new Error("unknown service in selection");
289
+ selected.set(name, children.get(name));
290
+ }
291
+ return selected;
292
+ };
293
+ const diagnostics = (namespace) => Object.fromEntries([...children].map(([name, child]) => [name, child.diagnostics(namespace)]));
294
+ const transaction = async (namespace, selected, run) => {
295
+ if (controlling)
296
+ return json(409, { error: "another fleet operation is in progress" });
297
+ controlling = true;
298
+ const release = [];
299
+ const before = /* @__PURE__ */ new Map();
300
+ const results = {};
301
+ let failure;
302
+ try {
303
+ for (const [name, child] of selected) {
304
+ failure = name;
305
+ release.push(child.lock(namespace));
306
+ }
307
+ const busy = Object.fromEntries([...selected].filter(([, child]) => {
308
+ const state = child.diagnostics(namespace);
309
+ return state.activeRequests + state.blockedCommands + state.pendingWebhooks + (state.activeJobs ?? 0) > 0;
310
+ }).map(([name, child]) => [name, child.diagnostics(namespace)]));
311
+ if (Object.keys(busy).length)
312
+ return json(409, { error: "namespace is not quiescent", services: busy });
313
+ for (const [name, child] of selected) {
314
+ failure = name;
315
+ before.set(name, await child.checkpoint(namespace));
316
+ }
317
+ for (const [name, child] of selected) {
318
+ failure = name;
319
+ results[name] = { status: "ok", result: await run(name, child) };
320
+ }
321
+ return json(200, { status: "ok", namespace, services: results });
322
+ } catch {
323
+ const rollback = {};
324
+ for (const [name, point] of before) {
325
+ try {
326
+ await selected.get(name)?.restore(namespace, point);
327
+ rollback[name] = "restored";
328
+ } catch {
329
+ rollback[name] = "failed";
330
+ }
331
+ }
332
+ return json(500, {
333
+ status: "failed",
334
+ namespace,
335
+ failedService: failure,
336
+ services: results,
337
+ rollback,
338
+ rolledBack: before.size > 0 && Object.values(rollback).every((value) => value === "restored")
339
+ });
340
+ } finally {
341
+ for (const unlock of release.reverse())
342
+ unlock();
343
+ controlling = false;
344
+ }
345
+ };
346
+ const api = {
347
+ async fetch(request) {
348
+ const url = new URL(request.url);
349
+ const adminPrefix = resolveAdminPrefix(config.adminPrefix);
350
+ const path = url.pathname.startsWith(`${adminPrefix}/`) ? url.pathname.slice(adminPrefix.length) : "";
351
+ if (path === "/health" && request.method === "GET") {
352
+ const states = await Promise.all([...children].map(async ([name, child]) => {
353
+ let protocolReady = false;
354
+ try {
355
+ protocolReady = await child.ready();
356
+ } catch {
357
+ }
358
+ return [
359
+ name,
360
+ {
361
+ processReady: !stopped,
362
+ protocolReady,
363
+ status: protocolReady ? "ready" : "failed",
364
+ url: publicUrl(child.url)
365
+ }
366
+ ];
367
+ }));
368
+ const ready = !stopped && states.every(([, state]) => state.protocolReady);
369
+ return json(ready ? 200 : 503, {
370
+ status: ready ? "ready" : "failed",
371
+ id,
372
+ services: Object.fromEntries(states)
373
+ });
374
+ }
375
+ if (!path.startsWith("/fleet/"))
376
+ return json(404, { error: "not found" });
377
+ const adminKey = config.adminKey ?? process.env.EMULATES_ADMIN_KEY;
378
+ if (adminKey !== void 0 && request.headers.get("x-emulates-admin-key") !== adminKey)
379
+ return json(401, { error: "admin key required" });
380
+ const route = /^\/fleet\/namespaces\/([^/]+)\/(reset|snapshots)(?:\/([^/]+)\/restore)?$/.exec(path);
381
+ const namespace = route ? decodeURIComponent(route[1]) : url.searchParams.get("namespace") ?? request.headers.get("x-emulates-namespace") ?? config.namespace ?? "default";
382
+ if (!NS.test(namespace))
383
+ return json(400, { error: "invalid namespace" });
384
+ if (request.method === "GET" && path === "/fleet/metrics")
385
+ return json(200, { namespace, services: diagnostics(namespace) });
386
+ if (request.method === "GET" && path === "/fleet/clock")
387
+ return json(200, {
388
+ namespace,
389
+ ...clock(namespace).state(),
390
+ services: Object.fromEntries([...children].map(([name, child]) => [name, child.clock(namespace).state()]))
391
+ });
392
+ if (request.method !== "POST")
393
+ return json(405, { error: "method not allowed" });
394
+ let body;
395
+ try {
396
+ const text = await request.text();
397
+ const value = text ? JSON.parse(text) : {};
398
+ if (!isObject(value))
399
+ throw new Error("expected object");
400
+ body = value;
401
+ } catch {
402
+ return json(400, { error: "expected JSON object" });
403
+ }
404
+ let selected;
405
+ try {
406
+ selected = select(body);
407
+ } catch (error) {
408
+ return json(400, { error: error instanceof Error ? error.message : "bad selection" });
409
+ }
410
+ if (path === "/fleet/wait-until-idle") {
411
+ const services = diagnostics(namespace);
412
+ const idle = Object.values(services).every((d) => pending(d) === 0);
413
+ return json(idle ? 200 : 409, { status: idle ? "idle" : "busy", namespace, services });
414
+ }
415
+ if (path === "/fleet/clock") {
416
+ const current = clock(namespace);
417
+ let target = current.now();
418
+ if (body.set !== void 0) {
419
+ target = typeof body.set === "number" ? body.set : typeof body.set === "string" ? Date.parse(body.set) : NaN;
420
+ if (!Number.isFinite(target))
421
+ return json(400, { error: "set must be epoch ms or ISO-8601" });
422
+ }
423
+ if (body.advance !== void 0) {
424
+ const delta = parseDuration(body.advance);
425
+ if (delta === void 0)
426
+ return json(400, { error: "invalid advance" });
427
+ target += delta;
428
+ }
429
+ const frozen = body.freeze === void 0 ? current.state().frozen : body.freeze;
430
+ if (typeof frozen !== "boolean")
431
+ return json(400, { error: "freeze must be boolean" });
432
+ const response2 = await transaction(namespace, selected, async (_name, child) => {
433
+ const childClock = child.clock(namespace);
434
+ childClock.freeze();
435
+ childClock.set(target);
436
+ if (!frozen)
437
+ childClock.unfreeze();
438
+ await child.settle?.(namespace);
439
+ return childClock.state();
440
+ });
441
+ if (response2.ok) {
442
+ current.freeze();
443
+ current.set(target);
444
+ if (!frozen)
445
+ current.unfreeze();
446
+ }
447
+ return response2;
448
+ }
449
+ if (!route)
450
+ return json(404, { error: "not found" });
451
+ if (route[2] === "reset") {
452
+ if (route[3])
453
+ return json(404, { error: "not found" });
454
+ const response2 = await transaction(namespace, selected, async (_name, child) => child.reset(namespace));
455
+ if (response2.ok && selected.size === children.size)
456
+ clock(namespace).reset();
457
+ return response2;
458
+ }
459
+ if (route[3]) {
460
+ const point = checkpoints.get(route[3]);
461
+ if (!point || point.namespace !== namespace)
462
+ return json(404, { error: "no snapshot in this namespace" });
463
+ const restored = new Map([...point.values].map(([name]) => [name, children.get(name)]));
464
+ const response2 = await transaction(namespace, restored, async (name, child) => child.restore(namespace, point.values.get(name)));
465
+ if (response2.ok) {
466
+ const current = clock(namespace);
467
+ current.freeze();
468
+ current.set(point.clock.now);
469
+ if (!point.clock.frozen)
470
+ current.unfreeze();
471
+ }
472
+ return response2;
473
+ }
474
+ const snapshotId = randomUUID();
475
+ const values = /* @__PURE__ */ new Map();
476
+ const response = await transaction(namespace, selected, async (name, child) => {
477
+ values.set(name, await child.checkpoint(namespace));
478
+ return { checkpoint: `${snapshotId}:${name}` };
479
+ });
480
+ if (!response.ok)
481
+ return response;
482
+ checkpoints.set(snapshotId, { namespace, values, clock: clock(namespace).state() });
483
+ return json(201, {
484
+ ...await response.json(),
485
+ id: snapshotId,
486
+ label: body.label
487
+ });
488
+ }
489
+ };
490
+ try {
491
+ const adminPrefix = resolveAdminPrefix(config.adminPrefix);
492
+ if (!isObject(config.services) || Object.keys(config.services).length === 0)
493
+ throw new Error("config must contain at least one service");
494
+ for (const [name, entry] of Object.entries(config.services)) {
495
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
496
+ throw new Error("invalid service name");
497
+ try {
498
+ const target = await options.load(name);
499
+ if (entry.protocol && entry.protocol !== ("protocol" in target ? target.protocol : "http"))
500
+ throw new Error("configured protocol does not match target");
501
+ const adminKey = entry.adminKey ?? config.adminKey ?? process.env.EMULATES_ADMIN_KEY;
502
+ const child = "start" in target ? await target.start(entry) : await httpChild(target, {
503
+ ...entry,
504
+ adminPrefix: resolveAdminPrefix(entry.adminPrefix ?? adminPrefix),
505
+ ...adminKey !== void 0 ? { adminKey } : {}
506
+ }, options);
507
+ children.set(name, child);
508
+ if (!await child.ready())
509
+ throw new Error("protocol readiness failed");
510
+ } catch {
511
+ throw new Error(`service ${name} failed to start; all started listeners were closed`);
512
+ }
513
+ }
514
+ supervisor = await listen(api, {
515
+ port: config.control?.port ?? 0,
516
+ host: config.control?.host ?? "127.0.0.1"
517
+ });
518
+ const manifest = {
519
+ version: 1,
520
+ state: "ready",
521
+ pid: process.pid,
522
+ id,
523
+ startedAt,
524
+ adminBase: `${supervisor.url}${adminPrefix}/fleet`,
525
+ healthUrl: `${supervisor.url}${adminPrefix}/health`,
526
+ services: Object.fromEntries([...children].map(([name, child]) => [
527
+ name,
528
+ {
529
+ protocol: child.protocol,
530
+ url: publicUrl(child.url),
531
+ healthUrl: child.healthUrl ?? `${supervisor?.url}${adminPrefix}/health`,
532
+ adminUrl: child.adminUrl ?? `${supervisor?.url}${adminPrefix}/fleet`,
533
+ namespaces: child.namespaces,
534
+ processReady: true,
535
+ protocolReady: true
536
+ }
537
+ ]))
538
+ };
539
+ if (options.connectionsFile)
540
+ await atomicWrite(options.connectionsFile, {
541
+ version: 1,
542
+ id,
543
+ pid: process.pid,
544
+ services: Object.fromEntries([...children].map(([name, child]) => [
545
+ name,
546
+ { url: child.connection ?? child.url, namespaces: child.connections }
547
+ ]))
548
+ });
549
+ if (options.readyFile)
550
+ await atomicWrite(options.readyFile, manifest);
551
+ options.onReady?.(manifest);
552
+ return { manifest, children, fetch: api.fetch, close };
553
+ } catch (error) {
554
+ await close();
555
+ throw error;
556
+ }
557
+ }
558
+
559
+ // ../../adapters/node/dist/cli.js
560
+ var optionHelp = (options) => Object.entries(options).map(([name, option]) => {
561
+ const flag = `--${name}${option.type === "string" ? ` ${option.value ?? "<value>"}` : ""}`;
562
+ const fallback = option.default !== void 0 ? ` (default: ${String(option.default)})` : "";
563
+ return ` ${flag.padEnd(30)} ${option.description}${fallback}`;
564
+ });
565
+ var help = (spec) => [
566
+ `${spec.bin} \u2014 ${spec.description}`,
567
+ "",
568
+ "Usage:",
569
+ ` ${spec.bin} <command> [options]`,
570
+ "",
571
+ "Commands:",
572
+ ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),
573
+ "",
574
+ `Run \`${spec.bin} <command> --help\` for a command's options.`
575
+ ].join("\n");
576
+ var commandHelp = (spec, name, command) => [
577
+ `${spec.bin} ${name} \u2014 ${command.summary}`,
578
+ "",
579
+ "Usage:",
580
+ ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,
581
+ ...command.options ? ["", "Options:", ...optionHelp(command.options)] : []
582
+ ].join("\n");
583
+ var runCli = async (spec, argv) => {
584
+ const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : void 0;
585
+ const words = pair !== void 0 && spec.commands[pair] ? 2 : 1;
586
+ const name = words === 2 ? pair : argv[0];
587
+ const rest = argv.slice(words);
588
+ if (name === void 0 || name === "--help" || name === "-h" || name === "help") {
589
+ console.log(help(spec));
590
+ return 0;
591
+ }
592
+ const command = spec.commands[name];
593
+ if (!command) {
594
+ console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}
595
+
596
+ ${help(spec)}`);
597
+ return 2;
598
+ }
599
+ if (rest.includes("--help") || rest.includes("-h")) {
600
+ console.log(commandHelp(spec, name, command));
601
+ return 0;
602
+ }
603
+ let parsed;
604
+ try {
605
+ parsed = parseArgs({
606
+ args: rest,
607
+ allowPositionals: true,
608
+ strict: true,
609
+ options: Object.fromEntries(Object.entries(command.options ?? {}).map(([key, option]) => [
610
+ key,
611
+ {
612
+ type: option.type,
613
+ ...option.default !== void 0 ? { default: option.default } : {}
614
+ }
615
+ ]))
616
+ });
617
+ } catch (error) {
618
+ console.error(`${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}
619
+
620
+ ${commandHelp(spec, name, command)}`);
621
+ return 2;
622
+ }
623
+ return command.run(parsed.values, parsed.positionals);
624
+ };
625
+ var COMMON_SERVE_OPTIONS = {
626
+ port: { type: "string", value: "<port>", description: "Port to listen on" },
627
+ host: { type: "string", value: "<host>", description: "Interface to bind", default: "127.0.0.1" },
628
+ "admin-prefix": {
629
+ type: "string",
630
+ value: "<path>",
631
+ description: "Internal API prefix (default /__admin; env EMULATES_ADMIN_PREFIX)"
632
+ },
633
+ "admin-key": {
634
+ type: "string",
635
+ value: "<key>",
636
+ description: "Require x-emulates-admin-key on /__admin/* (env EMULATES_ADMIN_KEY)"
637
+ },
638
+ seed: { type: "string", value: "<seed>", description: "Seed for every random choice" },
639
+ log: {
640
+ type: "string",
641
+ value: "<pretty|json|off>",
642
+ description: "Request log format",
643
+ default: "pretty"
644
+ },
645
+ "log-requests": {
646
+ type: "boolean",
647
+ description: "One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json"
648
+ },
649
+ config: {
650
+ type: "string",
651
+ value: "<file>",
652
+ description: "Serve every service in a emulates.json config instead"
653
+ },
654
+ "ready-file": {
655
+ type: "string",
656
+ description: "Atomically publish fleet endpoint JSON",
657
+ value: "<file>"
658
+ },
659
+ "connections-file": {
660
+ type: "string",
661
+ description: "Write private connection URLs with mode 0600",
662
+ value: "<file>"
663
+ },
664
+ "ready-json": {
665
+ type: "boolean",
666
+ description: "Emit one public JSON record after fleet readiness"
667
+ }
668
+ };
669
+ var formatLog = (format) => {
670
+ if (format === "off")
671
+ return void 0;
672
+ if (format === "json")
673
+ return (entry) => console.log(JSON.stringify(entry));
674
+ return (entry) => {
675
+ const op = entry.operationId ?? (entry.unmatched ? "UNMATCHED" : "-");
676
+ const ns = entry.namespace === "default" ? "" : ` [${entry.namespace}]`;
677
+ const fault = entry.faultId ? ` fault=${entry.faultId}` : "";
678
+ const adopted = entry.adopted ? " adopted" : "";
679
+ const received = entry.request ? ` received=${entry.request.bodyBytes === null ? "?" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? "-"}${entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : ""}` : "";
680
+ const issues = entry.issues?.length ? ` issues=${entry.issues.slice(0, 3).map((issue) => `${issue.path || "$"}: ${issue.message}`).join("; ")}${entry.issues.length > 3 ? ` (+${entry.issues.length - 3})` : ""}` : "";
681
+ console.log(`${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`);
682
+ };
683
+ };
684
+ var asString = (value) => typeof value === "string" ? value : void 0;
685
+ var loadTarget = async (name, own) => {
686
+ if (name === own.name)
687
+ return own;
688
+ const specifier = `@emulates/${name}/server`;
689
+ try {
690
+ let mod;
691
+ try {
692
+ mod = await import(specifier);
693
+ } catch {
694
+ const fromProject = createRequire(resolve(process.cwd(), "package.json"));
695
+ mod = await import(pathToFileURL(fromProject.resolve(specifier)).href);
696
+ }
697
+ if (!mod.serveTarget)
698
+ throw new Error(`${specifier} exports no serveTarget`);
699
+ return mod.serveTarget;
700
+ } catch (error) {
701
+ const reason = error instanceof Error ? error.message : String(error);
702
+ throw new Error(`cannot load service "${name}": ${reason}. Install @emulates/${name}.`);
703
+ }
704
+ };
705
+ var start = async (target, values, config) => {
706
+ const adminPrefix = resolveAdminPrefix(config.adminPrefix);
707
+ const runtime = await target.create(values, {
708
+ adminPrefix,
709
+ adminKey: config.adminKey,
710
+ seed: config.seed,
711
+ onLog: formatLog(config.log)
712
+ });
713
+ const listening = await listen(runtime, { port: config.port, host: config.host });
714
+ target.listening?.(listening);
715
+ console.log(`${target.name} emulator listening on ${listening.url}`);
716
+ console.log(`${target.name} health: GET ${listening.url}${adminPrefix}/health`);
717
+ console.log(`${target.name} admin: ${listening.url}${adminPrefix} (${config.adminKey ? "x-emulates-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
718
+ console.log(`${target.name} admin ui: ${listening.url}${adminPrefix}/ui`);
719
+ for (const line of target.banner?.(runtime) ?? [])
720
+ console.log(`${target.name} ${line.replaceAll("/__admin", adminPrefix)}`);
721
+ return listening;
722
+ };
723
+ var untilSignal = async (servers) => new Promise((resolve2) => {
724
+ const stop = async () => {
725
+ ;
726
+ process.removeListener("SIGINT", stop);
727
+ process.removeListener("SIGTERM", stop);
728
+ await Promise.allSettled(servers.map((s) => s.close()));
729
+ resolve2(0);
730
+ };
731
+ process.once("SIGINT", stop);
732
+ process.once("SIGTERM", stop);
733
+ });
734
+ var serveCommand = (target) => ({
735
+ summary: `Serve the ${target.name} emulator or a configured fleet`,
736
+ options: { ...COMMON_SERVE_OPTIONS, ..."options" in target ? target.options : {} },
737
+ async run(values) {
738
+ const log = values["log-requests"] === true ? "json" : asString(values.log) ?? "pretty";
739
+ if (!["pretty", "json", "off"].includes(log)) {
740
+ console.error(`--log must be pretty, json or off (got ${log})`);
741
+ return 2;
742
+ }
743
+ const configPath = asString(values.config);
744
+ if (configPath !== void 0) {
745
+ try {
746
+ const config = JSON.parse(await readFile2(configPath, "utf8"));
747
+ config.adminPrefix = resolveAdminPrefix(config.adminPrefix ?? asString(values["admin-prefix"]) ?? process.env.EMULATES_ADMIN_PREFIX);
748
+ const adminKey2 = config.adminKey ?? asString(values["admin-key"]) ?? process.env.EMULATES_ADMIN_KEY;
749
+ if (adminKey2 !== void 0)
750
+ config.adminKey = adminKey2;
751
+ const fleet = await startFleet(config, {
752
+ load: (name) => loadTarget(name, target),
753
+ onLog: formatLog(config.log ?? log),
754
+ ...asString(values["ready-file"]) ? { readyFile: asString(values["ready-file"]) } : {},
755
+ ...asString(values["connections-file"]) ? { connectionsFile: asString(values["connections-file"]) } : {},
756
+ onReady: (manifest) => {
757
+ if (values["ready-json"])
758
+ console.log(JSON.stringify(manifest));
759
+ else
760
+ for (const [name, endpoint] of Object.entries(manifest.services))
761
+ console.log(`${name} emulator listening on ${endpoint.url}`);
762
+ }
763
+ });
764
+ return untilSignal([fleet]);
765
+ } catch (error) {
766
+ console.error(error instanceof Error ? error.message : String(error));
767
+ return 1;
768
+ }
769
+ }
770
+ if ("start" in target) {
771
+ console.error("Use --config to supervise protocol services with this command");
772
+ return 2;
773
+ }
774
+ const port = asString(values.port);
775
+ const adminKey = asString(values["admin-key"]) ?? process.env.EMULATES_ADMIN_KEY;
776
+ const seed = asString(values.seed);
777
+ let listening;
778
+ try {
779
+ listening = await start(target, values, {
780
+ adminPrefix: resolveAdminPrefix(asString(values["admin-prefix"]) ?? process.env.EMULATES_ADMIN_PREFIX),
781
+ port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
782
+ host: asString(values.host) ?? "127.0.0.1",
783
+ ...adminKey !== void 0 ? { adminKey } : {},
784
+ ...seed !== void 0 ? { seed } : {},
785
+ log
786
+ });
787
+ } catch (error) {
788
+ console.error(error instanceof Error ? error.message : String(error));
789
+ return 1;
790
+ }
791
+ return untilSignal([listening]);
792
+ }
793
+ });
794
+
795
+ // src/cli.ts
796
+ var code = await runCli(
797
+ {
798
+ bin: "emulates-docker",
799
+ description: "Docker Engine API emulator (WIP: synthetic observations and shared controls)",
800
+ commands: { serve: serveCommand(serveTarget) }
801
+ },
802
+ process.argv.slice(2)
803
+ );
804
+ if (code !== 0) process.exitCode = code;
805
+ //# sourceMappingURL=cli.js.map