@intentius/chant-lexicon-fly 0.124.31 → 0.124.32

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.
Files changed (83) hide show
  1. package/dist/codegen/docs-cli.js +6 -0
  2. package/dist/codegen/docs.js +143 -0
  3. package/dist/codegen/generate-cli.js +26 -0
  4. package/dist/codegen/generate-lexicon.js +38 -0
  5. package/dist/codegen/generate-typescript.js +45 -0
  6. package/dist/codegen/generate.js +82 -0
  7. package/dist/codegen/naming.js +33 -0
  8. package/dist/codegen/package.js +35 -0
  9. package/dist/components/capability-plugin.js +26 -0
  10. package/dist/components/fly-release.js +335 -0
  11. package/dist/components/index.js +10 -0
  12. package/dist/components/run-agent.js +133 -0
  13. package/dist/composites/catalog.js +176 -0
  14. package/dist/composites/fly-deploy.js +91 -0
  15. package/dist/composites/fly-otel-collector.js +66 -0
  16. package/dist/composites/fly-site.js +80 -0
  17. package/dist/coverage.js +29 -0
  18. package/dist/describe-resources.js +443 -0
  19. package/dist/detect.js +63 -0
  20. package/dist/export-resources.js +81 -0
  21. package/dist/generated/index.js +44 -0
  22. package/dist/generated/runtime.js +4 -0
  23. package/dist/import/generator.js +208 -0
  24. package/dist/import/live-export.js +81 -0
  25. package/dist/import/parser.js +157 -0
  26. package/dist/index.js +40 -0
  27. package/dist/integrity.json +3 -3
  28. package/dist/lint/audit-catalog.js +28 -0
  29. package/dist/lint/audit-lineage.js +5 -0
  30. package/dist/lint/post-synth/fly-helpers.js +24 -0
  31. package/dist/lint/post-synth/fly010-machine-requires-image.js +33 -0
  32. package/dist/lint/post-synth/fly011-mount-references-declared-volume.js +58 -0
  33. package/dist/lint/post-synth/index.js +6 -0
  34. package/dist/lint/rules/guest-sizing.js +92 -0
  35. package/dist/lint/rules/index.js +12 -0
  36. package/dist/lint/rules/no-secret-literals.js +58 -0
  37. package/dist/lint/rules/valid-region.js +52 -0
  38. package/dist/lsp/completions.js +12 -0
  39. package/dist/lsp/hover.js +12 -0
  40. package/dist/manifest.json +1 -1
  41. package/dist/mcp/context-tools.js +113 -0
  42. package/dist/okf/types/Machine.md +1 -1
  43. package/dist/op/activities/box-services.js +88 -0
  44. package/dist/op/activities/emulator-images.js +32 -0
  45. package/dist/op/activities/flaps.js +34 -0
  46. package/dist/op/activities/fly-apply.js +911 -0
  47. package/dist/op/activities/fly-release-step.js +21 -0
  48. package/dist/op/activities/fly-rollback-step.js +22 -0
  49. package/dist/op/activities/index.js +53 -0
  50. package/dist/op/activities/machine-release.js +236 -0
  51. package/dist/op/activities/machines-contract.js +81 -0
  52. package/dist/op/activities/machines-fake.js +203 -0
  53. package/dist/op/activities/machines-local-cli.js +85 -0
  54. package/dist/op/activities/machines-local.js +395 -0
  55. package/dist/op/activities/progress.js +10 -0
  56. package/dist/op/activities/sprite-config.js +409 -0
  57. package/dist/op/activities/sprite-fs.js +131 -0
  58. package/dist/op/activities/sprite-service-converge.js +224 -0
  59. package/dist/op/activities/sprite-services.js +174 -0
  60. package/dist/op/activities/sprite-tasks.js +57 -0
  61. package/dist/op/activities/sprites-contract.js +87 -0
  62. package/dist/op/activities/sprites-emulator.js +72 -0
  63. package/dist/op/activities/sprites-fake.js +547 -0
  64. package/dist/op/activities/sprites.js +515 -0
  65. package/dist/op/activity-contracts.js +82 -0
  66. package/dist/op/builders.js +157 -0
  67. package/dist/ownership.js +29 -0
  68. package/dist/package-cli.js +20 -0
  69. package/dist/plugin.js +298 -0
  70. package/dist/pseudo.js +26 -0
  71. package/dist/reference-catalog.js +37 -0
  72. package/dist/release-metadata.js +59 -0
  73. package/dist/release-store.js +46 -0
  74. package/dist/serializer.js +299 -0
  75. package/dist/spec/fetch.js +32 -0
  76. package/dist/spec/parse.js +260 -0
  77. package/dist/teardown.js +194 -0
  78. package/dist/types/index.d.ts +33 -2
  79. package/dist/validate-cli.js +4 -0
  80. package/dist/validate.js +33 -0
  81. package/package.json +11 -11
  82. package/src/codegen/package.ts +2 -2
  83. package/src/generated/index.d.ts +33 -2
@@ -0,0 +1,85 @@
1
+ /**
2
+ * A local Fly Machines API that runs its Machines (#2831), as a command:
3
+ *
4
+ * tsx node_modules/@intentius/chant-lexicon-fly/src/op/activities/machines-local-cli.ts \
5
+ * [--listen 4280] [--root <dir>] [--publish <port> | --publish <name>=<port> ...] [--log <file>]
6
+ *
7
+ * Point the release activities at it with `FLY_FLAPS_BASE_URL=http://127.0.0.1:4280`.
8
+ * `--publish 8080` publishes every Machine's service on 8080; `--publish web=8080`
9
+ * publishes the Machine named `web` there; with no `--publish` each Machine gets a free
10
+ * port, which the event lines say. SIGINT or SIGTERM stops every Machine's process
11
+ * and exits. See ./machines-local.ts.
12
+ */
13
+ import { realpathSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { serveLocalMachines } from "./machines-local.js";
16
+ export function parseArgs(argv) {
17
+ let port = 4280;
18
+ let root;
19
+ let log;
20
+ let all;
21
+ const byName = {};
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const flag = argv[i];
24
+ const value = argv[i + 1];
25
+ if (value === undefined)
26
+ throw new Error(`${flag} needs a value`);
27
+ if (flag === "--listen")
28
+ port = Number(value);
29
+ else if (flag === "--root")
30
+ root = value;
31
+ else if (flag === "--log")
32
+ log = value;
33
+ else if (flag === "--publish") {
34
+ const eq = value.lastIndexOf("=");
35
+ if (eq === -1)
36
+ all = Number(value);
37
+ else
38
+ byName[value.slice(0, eq)] = Number(value.slice(eq + 1));
39
+ }
40
+ else
41
+ throw new Error(`unknown flag ${flag}`);
42
+ i++;
43
+ }
44
+ for (const n of [port, all, ...Object.values(byName)])
45
+ if (n !== undefined && !Number.isInteger(n))
46
+ throw new Error(`not a port: ${n}`);
47
+ const named = Object.keys(byName).length > 0;
48
+ const publish = named ? (app, m) => byName[`${app}/${m.name}`] ?? byName[m.name] ?? all : all;
49
+ return { port, root, publish, log };
50
+ }
51
+ async function main() {
52
+ let args;
53
+ try {
54
+ args = parseArgs(process.argv.slice(2));
55
+ }
56
+ catch (error) {
57
+ console.error(`machines-local: ${error.message}`);
58
+ console.error("usage: machines-local-cli.ts [--listen <port>] [--root <dir>] [--publish <port> | --publish <name>=<port>]... [--log <file>]");
59
+ process.exit(2);
60
+ }
61
+ // A standalone server, not an Op activity: its stdout is its output. The flaps URL and the
62
+ // event lines (which name each Machine's published port) stay there for the person or
63
+ // script that started it; errors and usage go to stderr.
64
+ const served = await serveLocalMachines({ ...args, handleSignals: false, onEvent: (line) => console.log(`machines-local: ${line}`) });
65
+ console.log(`machines-local: flaps on ${served.url}${args.root ? `, guest paths under ${args.root}` : ""}`);
66
+ let closing = false;
67
+ const shutdown = () => {
68
+ if (closing)
69
+ return;
70
+ closing = true;
71
+ void served.close().then(() => process.exit(0));
72
+ };
73
+ process.on("SIGINT", shutdown);
74
+ process.on("SIGTERM", shutdown);
75
+ }
76
+ const invoked = (() => {
77
+ try {
78
+ return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ })();
84
+ if (invoked)
85
+ void main();
@@ -0,0 +1,395 @@
1
+ /**
2
+ * A local Fly Machines API that runs its Machines (#2831).
3
+ *
4
+ * ./machines-fake.ts answers the flaps endpoints from memory and runs nothing,
5
+ * and so does mudflaps. That is what unit tests want, and it stays the
6
+ * default. This module is the opt-in running mode: the same in-memory API,
7
+ * with each started Machine run as a process on this host, so a release that
8
+ * puts a source tree on a Machine (`config.files` plus `init.cmd`) can be
9
+ * checked over HTTP.
10
+ *
11
+ * For each Machine the fake holds in state `started`:
12
+ *
13
+ * - `config.files` are written at their `guest_path` (under `root` when one
14
+ * is given), and the files an earlier instance wrote are removed first, as
15
+ * a Machine's root filesystem starts again from its image;
16
+ * - `config.mounts` are directories that outlive instances (a Volume);
17
+ * - `init.exec`, or `init.entrypoint` then `init.cmd`, runs with
18
+ * `config.env`, and an `exec` (a migration) runs its command with the same
19
+ * env;
20
+ * - a service's `internal_port` is published on a host port: when the
21
+ * Machine's env sets `PORT` to an `internal_port`, the process gets the
22
+ * host port instead, since every Machine here shares the host's network.
23
+ *
24
+ * No image is pulled: the command runs on this host's binaries (a Node app
25
+ * runs on this host's Node).
26
+ *
27
+ * A create or an update (a new `instance_id`) starts the process again, a
28
+ * `restart` restarts it, a `stop` or a delete stops it, and a process that
29
+ * exits on its own leaves its Machine `stopped`. Each call answers only after
30
+ * that is done, and a started process is given until its published port
31
+ * accepts a connection, so the next call (a migration's exec, a health check)
32
+ * finds the files and the server.
33
+ *
34
+ * Processes run in their own process group. `close()` stops them all
35
+ * (SIGTERM, then SIGKILL); if the host process exits or is ended by SIGINT,
36
+ * SIGTERM or SIGHUP first, they are killed with it. Only a SIGKILL of the host
37
+ * process can leave them running.
38
+ */
39
+ import { spawn } from "node:child_process";
40
+ import { chmodSync, closeSync, mkdirSync, openSync, rmSync, writeFileSync } from "node:fs";
41
+ import { createServer as createHttpServer } from "node:http";
42
+ import { createConnection, createServer as createNetServer } from "node:net";
43
+ import { dirname, join } from "node:path";
44
+ import { createMachinesFake } from "./machines-fake.js";
45
+ const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
46
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+ /** Is `pid`'s process group still there? */
48
+ function groupAlive(pid) {
49
+ try {
50
+ process.kill(-pid, 0);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ function killGroup(child, signal) {
58
+ if (!child?.pid)
59
+ return;
60
+ try {
61
+ process.kill(-child.pid, signal);
62
+ }
63
+ catch {
64
+ // Already gone.
65
+ }
66
+ }
67
+ /**
68
+ * SIGKILL a process group until nothing in it is left to kill. One kill(-pgid)
69
+ * can miss a child that a member is forking at that moment: the child joins the
70
+ * group after the signal went out, and then outlives the rest (seen on macOS,
71
+ * #3202). Sending again once the forker is dead catches it. Synchronous, so it
72
+ * works in an "exit" handler; it stops as soon as the group is empty (or only
73
+ * zombies), and gives up after about 100ms.
74
+ */
75
+ function killGroupNow(child) {
76
+ if (!child?.pid)
77
+ return;
78
+ const pause = new Int32Array(new SharedArrayBuffer(4));
79
+ for (let i = 0; i < 20; i++) {
80
+ try {
81
+ process.kill(-child.pid, "SIGKILL");
82
+ }
83
+ catch {
84
+ return;
85
+ }
86
+ Atomics.wait(pause, 0, 0, 5);
87
+ }
88
+ }
89
+ async function freePort() {
90
+ const srv = createNetServer();
91
+ await new Promise((ok, fail) => srv.once("error", fail).listen(0, "127.0.0.1", ok));
92
+ const { port } = srv.address();
93
+ await new Promise((ok) => srv.close(() => ok()));
94
+ return port;
95
+ }
96
+ function connects(port) {
97
+ return new Promise((done) => {
98
+ const socket = createConnection({ port, host: "127.0.0.1" });
99
+ socket.once("connect", () => {
100
+ socket.destroy();
101
+ done(true);
102
+ });
103
+ socket.once("error", () => done(false));
104
+ });
105
+ }
106
+ /** An in-memory Machines API that runs each started Machine on this host. See the module header. */
107
+ export function createLocalMachines(options = {}) {
108
+ const root = options.root;
109
+ const readyTimeoutMs = options.readyTimeoutMs ?? 15000;
110
+ const stopTimeoutMs = options.stopTimeoutMs ?? 5000;
111
+ const say = options.onEvent ?? (() => undefined);
112
+ const running = new Map();
113
+ /** Host ports picked for Machines with no declared one, by `app/name`, so a new instance serves where the last did. */
114
+ const picked = new Map();
115
+ const host = (guest) => (root ? join(root, guest) : guest);
116
+ /** Rewrites guest paths named in a string to host paths (identity with no root). */
117
+ function mapperOf(config) {
118
+ if (!root)
119
+ return (s) => s;
120
+ const prefixes = new Set();
121
+ for (const m of config.mounts ?? [])
122
+ if (m.path)
123
+ prefixes.add(m.path.replace(/\/+$/, ""));
124
+ for (const f of config.files ?? [])
125
+ if (f.guest_path)
126
+ prefixes.add(dirname(f.guest_path));
127
+ prefixes.delete("");
128
+ prefixes.delete("/");
129
+ if (prefixes.size === 0)
130
+ return (s) => s;
131
+ const alts = [...prefixes].sort((a, b) => b.length - a.length).map(escape).join("|");
132
+ const re = new RegExp(`(^|[\\s=:'"(;&|])(${alts})(?=$|[/\\s'":;)&|])`, "g");
133
+ return (s) => s.replace(re, (_m, pre, path) => `${pre}${host(path)}`);
134
+ }
135
+ async function publishedPort(app, m) {
136
+ const env = m.config.env ?? {};
137
+ const internal = (m.config.services ?? []).map((s) => String(s.internal_port));
138
+ if (env.PORT === undefined || !internal.includes(String(env.PORT)))
139
+ return undefined;
140
+ const p = options.publish;
141
+ const declared = typeof p === "number" ? p : typeof p === "function" ? p(app, m) : p ? (p[`${app}/${m.name}`] ?? p[m.name]) : undefined;
142
+ if (declared !== undefined)
143
+ return declared;
144
+ const key = `${app}/${m.name}`;
145
+ if (!picked.has(key))
146
+ picked.set(key, await freePort());
147
+ return picked.get(key);
148
+ }
149
+ function envOf(config, port) {
150
+ const map = mapperOf(config);
151
+ const env = { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: process.env.HOME ?? "/" };
152
+ for (const [k, v] of Object.entries(config.env ?? {}))
153
+ env[k] = map(String(v));
154
+ if (port !== undefined)
155
+ env.PORT = String(port);
156
+ return env;
157
+ }
158
+ function argvOf(config) {
159
+ const init = (config.init ?? {});
160
+ const map = mapperOf(config);
161
+ if (init.exec?.length)
162
+ return init.exec.map(map);
163
+ const cmd = typeof init.cmd === "string" ? ["sh", "-c", init.cmd] : (init.cmd ?? []);
164
+ const argv = [...(init.entrypoint ?? []), ...cmd];
165
+ return argv.length ? argv.map(map) : undefined;
166
+ }
167
+ const stdio = () => {
168
+ if (!options.log)
169
+ return { out: 2, close: () => undefined };
170
+ mkdirSync(dirname(options.log), { recursive: true });
171
+ const fd = openSync(options.log, "a");
172
+ return { out: fd, close: () => closeSync(fd) };
173
+ };
174
+ async function stop(key) {
175
+ const r = running.get(key);
176
+ if (!r)
177
+ return;
178
+ running.delete(key);
179
+ r.stopping = true;
180
+ const child = r.child;
181
+ if (child?.pid && groupAlive(child.pid)) {
182
+ const exited = child.exitCode === null && child.signalCode === null ? new Promise((ok) => child.once("exit", ok)) : Promise.resolve();
183
+ killGroup(child, "SIGTERM");
184
+ const deadline = Date.now() + stopTimeoutMs;
185
+ // Wait for the leader to exit and the rest of its group to go.
186
+ await Promise.race([exited, new Promise((ok) => setTimeout(ok, stopTimeoutMs))]);
187
+ while (groupAlive(child.pid) && Date.now() < deadline)
188
+ await new Promise((ok) => setTimeout(ok, 25));
189
+ // Sent again each round: one group kill can miss a child being forked as it lands (see killGroupNow).
190
+ while (groupAlive(child.pid)) {
191
+ killGroup(child, "SIGKILL");
192
+ await new Promise((ok) => setTimeout(ok, 25));
193
+ }
194
+ }
195
+ for (const path of r.files)
196
+ rmSync(path, { force: true });
197
+ say(`stopped ${key} (instance ${r.instance})`);
198
+ }
199
+ async function start(app, m) {
200
+ const key = `${app}/${m.id}`;
201
+ await stop(key);
202
+ const { config } = m;
203
+ for (const mount of config.mounts ?? [])
204
+ if (mount.path)
205
+ mkdirSync(host(mount.path), { recursive: true });
206
+ const files = [];
207
+ for (const f of config.files ?? []) {
208
+ const path = host(f.guest_path);
209
+ mkdirSync(dirname(path), { recursive: true });
210
+ writeFileSync(path, Buffer.from(f.raw_value ?? "", "base64"));
211
+ if (f.mode)
212
+ chmodSync(path, f.mode);
213
+ files.push(path);
214
+ }
215
+ const argv = argvOf(config);
216
+ const port = argv ? await publishedPort(app, m) : undefined;
217
+ const r = { app, id: m.id, name: m.name, instance: m.instance_id, files, child: undefined, port, stopping: false, exited: false };
218
+ running.set(key, r);
219
+ if (!argv) {
220
+ say(`started ${key} instance ${m.instance_id}: ${files.length} files, no command`);
221
+ return;
222
+ }
223
+ const log = stdio();
224
+ const child = spawn(argv[0], argv.slice(1), { cwd: root ?? "/", env: envOf(config, port), stdio: ["ignore", log.out, log.out], detached: true });
225
+ log.close();
226
+ child.unref();
227
+ r.child = child;
228
+ child.once("error", (error) => say(`${key} instance ${m.instance_id} failed to start: ${error.message}`));
229
+ child.once("exit", (code, signal) => {
230
+ r.exited = true;
231
+ if (r.stopping)
232
+ return;
233
+ say(`${key} instance ${m.instance_id} exited ${signal ?? code}`);
234
+ // A process that ends on its own leaves its Machine stopped, and its group is killed with it.
235
+ killGroupNow(child);
236
+ if (m.instance_id === r.instance && m.state === "started")
237
+ m.state = "stopped";
238
+ });
239
+ say(`started ${key} instance ${m.instance_id}: ${files.length} files, ${argv.join(" ")}${port ? `, on 127.0.0.1:${port}` : ""}`);
240
+ if (port !== undefined) {
241
+ const deadline = Date.now() + readyTimeoutMs;
242
+ while (!r.exited && Date.now() < deadline && !(await connects(port)))
243
+ await new Promise((ok) => setTimeout(ok, 50));
244
+ if (!r.exited && Date.now() >= deadline)
245
+ say(`${key} instance ${m.instance_id} is not accepting connections on ${port} after ${readyTimeoutMs}ms`);
246
+ }
247
+ }
248
+ async function exec(app, m, command, timeoutSecs) {
249
+ const map = mapperOf(m.config);
250
+ const argv = command.map(map);
251
+ const port = running.get(`${app}/${m.id}`)?.port;
252
+ return new Promise((done) => {
253
+ const child = spawn(argv[0], argv.slice(1), { cwd: root ?? "/", env: envOf(m.config, port), stdio: ["ignore", "pipe", "pipe"], detached: true });
254
+ let stdout = "";
255
+ let stderr = "";
256
+ child.stdout.on("data", (d) => (stdout += d));
257
+ child.stderr.on("data", (d) => (stderr += d));
258
+ // killGroupNow: a child missed by one group kill would hold the output pipes, and "close" would wait for it.
259
+ const timer = setTimeout(() => killGroupNow(child), timeoutSecs * 1000);
260
+ child.once("error", (error) => {
261
+ clearTimeout(timer);
262
+ done({ exit_code: 127, stdout, stderr: `${stderr}${error.message}` });
263
+ });
264
+ child.once("close", (code, signal) => {
265
+ clearTimeout(timer);
266
+ const exit = code ?? (signal ? 128 : 1);
267
+ say(`exec on ${app}/${m.id}: ${command.join(" ")} exited ${signal ?? code}${exit === 0 ? "" : `: ${`${stderr}${stdout}`.trim().slice(-500)}`}`);
268
+ done({ exit_code: exit, stdout, stderr });
269
+ });
270
+ });
271
+ }
272
+ /** The exec timeout the last exec call asked for, read before the fake answers it. */
273
+ let execTimeout = 120;
274
+ const fake = createMachinesFake({
275
+ settle: options.settle,
276
+ exec: (app, m, command) => exec(app, m, command, execTimeout),
277
+ });
278
+ /** Make what runs match the fake's Machines after a call; `restarted` names a Machine (`app/id`) whose process starts again. */
279
+ async function reconcile(restarted) {
280
+ const live = new Set();
281
+ for (const [app, machines] of fake.machines) {
282
+ for (const m of machines) {
283
+ const key = `${app}/${m.id}`;
284
+ live.add(key);
285
+ const r = running.get(key);
286
+ if (m.state !== "started")
287
+ await stop(key);
288
+ else if (!r || r.exited || r.instance !== m.instance_id || key === restarted)
289
+ await start(app, m);
290
+ }
291
+ }
292
+ for (const key of [...running.keys()])
293
+ if (!live.has(key))
294
+ await stop(key);
295
+ }
296
+ // One call at a time, so a start never races the next call.
297
+ let queue = Promise.resolve();
298
+ const http = (method, url, body, headers, signal) => {
299
+ const next = queue.then(async () => {
300
+ const seg = new URL(url).pathname.split("/").filter(Boolean).map(decodeURIComponent);
301
+ if (method === "POST" && seg[3] === "machines" && seg[5] === "exec") {
302
+ const t = Number(body?.timeout);
303
+ execTimeout = Number.isFinite(t) && t > 0 ? t : 120;
304
+ }
305
+ const out = await fake.http(method, url, body, headers, signal);
306
+ if (method !== "GET" && !(seg[5] === "exec" || seg[5] === "lease" || seg[5] === "wait")) {
307
+ const restarted = method === "POST" && seg[3] === "machines" && seg[5] === "restart" && out.status < 300 ? `${seg[2]}/${seg[4]}` : undefined;
308
+ await reconcile(restarted);
309
+ }
310
+ return out;
311
+ });
312
+ queue = next.catch(() => undefined);
313
+ return next;
314
+ };
315
+ // Nothing is left running when this process ends: on exit, and on a signal (which is then raised again, so it still ends the process).
316
+ const killAll = () => {
317
+ for (const r of running.values()) {
318
+ r.stopping = true;
319
+ killGroupNow(r.child);
320
+ }
321
+ };
322
+ const onSignal = (sig) => {
323
+ killAll();
324
+ detach();
325
+ if (process.listenerCount(sig) === 0)
326
+ process.kill(process.pid, sig);
327
+ };
328
+ const detach = () => {
329
+ process.off("exit", killAll);
330
+ for (const s of SIGNALS)
331
+ process.off(s, onSignal);
332
+ };
333
+ process.on("exit", killAll);
334
+ if (options.handleSignals !== false)
335
+ for (const s of SIGNALS)
336
+ process.on(s, onSignal);
337
+ return {
338
+ ...fake,
339
+ http,
340
+ endpoint(app, name) {
341
+ const m = fake.machine(app, name);
342
+ const r = m ? running.get(`${app}/${m.id}`) : undefined;
343
+ const port = r && !r.exited ? r.port : undefined;
344
+ return port === undefined ? undefined : `http://127.0.0.1:${port}`;
345
+ },
346
+ processes: () => [...running.values()].filter((r) => !r.exited).map((r) => ({ app: r.app, id: r.id, name: r.name, instance: r.instance, pid: r.child?.pid, port: r.port })),
347
+ async close() {
348
+ await queue;
349
+ for (const key of [...running.keys()])
350
+ await stop(key);
351
+ detach();
352
+ },
353
+ };
354
+ }
355
+ /**
356
+ * Serve {@link createLocalMachines} over HTTP, where the release activities
357
+ * reach it through `FLY_FLAPS_BASE_URL`. `GET /_mudflaps/health` answers 200,
358
+ * as mudflaps' health check does.
359
+ */
360
+ export async function serveLocalMachines(options = {}) {
361
+ const machines = createLocalMachines(options);
362
+ const hostname = options.host ?? "127.0.0.1";
363
+ const server = createHttpServer((req, res) => {
364
+ const chunks = [];
365
+ req.on("data", (c) => chunks.push(c));
366
+ req.on("end", () => {
367
+ void (async () => {
368
+ let out;
369
+ try {
370
+ if (req.method === "GET" && req.url === "/_mudflaps/health")
371
+ out = { status: 200, text: JSON.stringify({ status: "ok" }) };
372
+ else {
373
+ const text = Buffer.concat(chunks).toString("utf-8");
374
+ out = await machines.http(req.method ?? "GET", `http://${hostname}${req.url}`, text ? JSON.parse(text) : undefined, {});
375
+ }
376
+ }
377
+ catch (error) {
378
+ out = { status: 500, text: JSON.stringify({ error: error.message }) };
379
+ }
380
+ res.writeHead(out.status, out.text ? { "content-type": "application/json" } : {});
381
+ res.end(out.text);
382
+ })();
383
+ });
384
+ });
385
+ await new Promise((ok, fail) => server.once("error", fail).listen(options.port ?? 4280, hostname, ok));
386
+ const { port } = server.address();
387
+ return {
388
+ url: `http://${hostname}:${port}`,
389
+ machines,
390
+ async close() {
391
+ await machines.close();
392
+ await new Promise((ok) => server.close(() => ok()));
393
+ },
394
+ };
395
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Progress output for the fly Op activities (#2516, #3200).
3
+ *
4
+ * An Op step's stdout is its output, so activities write their progress lines
5
+ * ("created: sprite/x", "pruned: ...") to stderr and keep stdout free for
6
+ * machine-readable results.
7
+ */
8
+ export function logProgress(line) {
9
+ process.stderr.write(`${line}\n`);
10
+ }