@intentius/chant 0.15.2 → 0.16.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.
@@ -0,0 +1,129 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { safeHeartbeat, sleep } from "./activity-runtime";
4
+
5
+ const execAsync = promisify(exec);
6
+
7
+ /**
8
+ * A local emulator's fixed identity — everything that differs between the
9
+ * per-cloud Docker-lifecycle wrappers (Floci for AWS, floci-az/gcp, mudflaps for
10
+ * Fly). Only these five things vary; the boot/health-poll/teardown loop is shared.
11
+ */
12
+ export interface EmulatorSpec {
13
+ /** Default container name (e.g. `chant-floci`, `chant-mudflaps`). */
14
+ name: string;
15
+ /** Default image, ideally a pinned tag. */
16
+ image: string;
17
+ /** Port the emulator listens on inside the container (e.g. 4566, 4280). */
18
+ containerPort: number;
19
+ /** Health path on the host port (e.g. `/_localstack/health`, `/_mudflaps/health`). */
20
+ healthPath: string;
21
+ /** Readiness predicate over the health body. Default: any 200 response is ready. */
22
+ ready?: (healthBody: string) => boolean;
23
+ /** Extra `docker run` args inserted before the image (e.g. a socket mount). */
24
+ runArgs?: readonly string[];
25
+ }
26
+
27
+ /** Per-call overrides for {@link EmulatorLifecycle.up} / `runCommand`. */
28
+ export interface EmulatorUpArgs {
29
+ name?: string;
30
+ port?: number;
31
+ image?: string;
32
+ timeoutMs?: number;
33
+ intervalMs?: number;
34
+ /** Additional `docker run` args for this call (after `spec.runArgs`, before the image). */
35
+ extraArgs?: readonly string[];
36
+ }
37
+
38
+ /** The shared lifecycle a per-cloud wrapper adapts. */
39
+ export interface EmulatorLifecycle {
40
+ runCommand(args?: EmulatorUpArgs): string;
41
+ existsCommand(name: string): string;
42
+ rmCommand(name: string): string;
43
+ healthUrl(port: number): string;
44
+ endpoint(port: number): string;
45
+ up(args?: EmulatorUpArgs, signal?: AbortSignal): Promise<{ endpoint: string }>;
46
+ down(args?: { name?: string }, signal?: AbortSignal): Promise<void>;
47
+ }
48
+
49
+ /**
50
+ * Build a Docker-lifecycle for a local emulator: an idempotent `up` (reuse a
51
+ * running container, else `docker run`, then poll health until ready), a `down`
52
+ * (`docker rm -f`), and the pure command builders each per-cloud wrapper exposes
53
+ * for testing. Removes the near-duplication across the per-lexicon `floci*.ts`
54
+ * wrappers — a new cloud is a spec, not a copy.
55
+ */
56
+ export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
57
+ const ready = spec.ready ?? (() => true);
58
+
59
+ const runCommand = (args: EmulatorUpArgs = {}): string => {
60
+ const name = args.name ?? spec.name;
61
+ const port = args.port ?? spec.containerPort;
62
+ const image = args.image ?? spec.image;
63
+ return [
64
+ "docker", "run", "-d", "--rm", "--name", name, "-p", `${port}:${spec.containerPort}`,
65
+ ...(spec.runArgs ?? []), ...(args.extraArgs ?? []), image,
66
+ ].join(" ");
67
+ };
68
+ const existsCommand = (name: string): string => `docker ps -q -f name=${name}`;
69
+ const rmCommand = (name: string): string => `docker rm -f ${name}`;
70
+ const healthUrl = (port: number): string => `http://localhost:${port}${spec.healthPath}`;
71
+ const endpoint = (port: number): string => `http://localhost:${port}`;
72
+
73
+ async function up(args: EmulatorUpArgs = {}, signal?: AbortSignal): Promise<{ endpoint: string }> {
74
+ const name = args.name ?? spec.name;
75
+ const port = args.port ?? spec.containerPort;
76
+ const timeoutMs = args.timeoutMs ?? 60_000;
77
+ const intervalMs = args.intervalMs ?? 2_000;
78
+
79
+ let running = false;
80
+ try {
81
+ const { stdout } = await execAsync(existsCommand(name), { signal });
82
+ running = Boolean(stdout.trim());
83
+ } catch {
84
+ // `docker ps` failed — assume not running and try to start it.
85
+ }
86
+
87
+ if (running) {
88
+ console.log(`emulator container "${name}" already running — reusing`);
89
+ } else {
90
+ await execAsync(runCommand({ ...args, name, port }), { signal });
91
+ }
92
+
93
+ const url = healthUrl(port);
94
+ const deadline = Date.now() + timeoutMs;
95
+ let ok = false;
96
+ while (Date.now() < deadline) {
97
+ if (signal?.aborted) throw new Error(`emulator "${name}" wait aborted`);
98
+ safeHeartbeat({ step: "emulatorUp", container: name });
99
+ try {
100
+ const res = await fetch(url, { signal });
101
+ if (res.ok && ready(await res.text())) {
102
+ ok = true;
103
+ break;
104
+ }
105
+ } catch {
106
+ // Not up yet (connection refused / non-2xx) — retry.
107
+ }
108
+ await sleep(intervalMs, signal);
109
+ }
110
+ if (!ok) {
111
+ throw new Error(`emulator "${name}" did not become ready within ${timeoutMs}ms`);
112
+ }
113
+
114
+ const ep = endpoint(port);
115
+ console.log(`emulator "${name}" ready on ${ep}`);
116
+ return { endpoint: ep };
117
+ }
118
+
119
+ async function down(args: { name?: string } = {}, signal?: AbortSignal): Promise<void> {
120
+ const name = args.name ?? spec.name;
121
+ try {
122
+ await execAsync(rmCommand(name), { signal });
123
+ } catch {
124
+ // Already removed (`--rm` on exit, or never started) — treat as success.
125
+ }
126
+ }
127
+
128
+ return { runCommand, existsCommand, rmCommand, healthUrl, endpoint, up, down };
129
+ }
package/src/op/index.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  export { Op, phase, activity, gate, build, kubectlApply, helmInstall, waitForStack,
2
2
  gitlabPipeline, lifecycleSnapshot, shell, teardown, k3dUp, k3dDown, flociUp, flociDown,
3
3
  flociAzUp, flociAzDown, flociGcpUp, flociGcpDown, httpCheck,
4
- azGroupEnsure, azGroupDelete, azApply, azDelete, gcpApply, gcpDelete, policyGate } from "./builders";
4
+ azGroupEnsure, azGroupDelete, azApply, azDelete, awsApply, awsDelete, gcpApply, gcpDelete, policyGate,
5
+ spriteCreate, spriteExec, spriteCheckpoint, spriteRestore, listCheckpoints, spriteDestroy,
6
+ spritesUp, spritesDown } from "./builders";
5
7
  export { OpResource } from "./resource";
6
8
  export { safeHeartbeat, sleep } from "./activity-runtime";
9
+ export { emulatorLifecycle } from "./emulator-lifecycle";
10
+ export type { EmulatorSpec, EmulatorUpArgs, EmulatorLifecycle } from "./emulator-lifecycle";
7
11
  export type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep } from "./types";
8
12
  export { discoverOps } from "./discover";
9
13
  export type { DiscoveredOp, OpDiscoveryResult } from "./discover";