@intentius/chant-lexicon-fly 0.124.30 → 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.
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +143 -0
- package/dist/codegen/generate-cli.js +26 -0
- package/dist/codegen/generate-lexicon.js +38 -0
- package/dist/codegen/generate-typescript.js +45 -0
- package/dist/codegen/generate.js +82 -0
- package/dist/codegen/naming.js +33 -0
- package/dist/codegen/package.js +35 -0
- package/dist/components/capability-plugin.js +26 -0
- package/dist/components/fly-release.js +335 -0
- package/dist/components/index.js +10 -0
- package/dist/components/run-agent.js +133 -0
- package/dist/composites/catalog.js +176 -0
- package/dist/composites/fly-deploy.js +91 -0
- package/dist/composites/fly-otel-collector.js +66 -0
- package/dist/composites/fly-site.js +80 -0
- package/dist/coverage.js +29 -0
- package/dist/describe-resources.js +443 -0
- package/dist/detect.js +63 -0
- package/dist/export-resources.js +81 -0
- package/dist/generated/index.js +44 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/import/generator.js +208 -0
- package/dist/import/live-export.js +81 -0
- package/dist/import/parser.js +157 -0
- package/dist/index.js +40 -0
- package/dist/integrity.json +3 -3
- package/dist/lint/audit-catalog.js +28 -0
- package/dist/lint/audit-lineage.js +5 -0
- package/dist/lint/post-synth/fly-helpers.js +24 -0
- package/dist/lint/post-synth/fly010-machine-requires-image.js +33 -0
- package/dist/lint/post-synth/fly011-mount-references-declared-volume.js +58 -0
- package/dist/lint/post-synth/index.js +6 -0
- package/dist/lint/rules/guest-sizing.js +92 -0
- package/dist/lint/rules/index.js +12 -0
- package/dist/lint/rules/no-secret-literals.js +58 -0
- package/dist/lint/rules/valid-region.js +52 -0
- package/dist/lsp/completions.js +12 -0
- package/dist/lsp/hover.js +12 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/context-tools.js +113 -0
- package/dist/okf/types/Machine.md +1 -1
- package/dist/op/activities/box-services.js +88 -0
- package/dist/op/activities/emulator-images.js +32 -0
- package/dist/op/activities/flaps.js +34 -0
- package/dist/op/activities/fly-apply.js +911 -0
- package/dist/op/activities/fly-release-step.js +21 -0
- package/dist/op/activities/fly-rollback-step.js +22 -0
- package/dist/op/activities/index.js +53 -0
- package/dist/op/activities/machine-release.js +236 -0
- package/dist/op/activities/machines-contract.js +81 -0
- package/dist/op/activities/machines-fake.js +203 -0
- package/dist/op/activities/machines-local-cli.js +85 -0
- package/dist/op/activities/machines-local.js +395 -0
- package/dist/op/activities/progress.js +10 -0
- package/dist/op/activities/sprite-config.js +409 -0
- package/dist/op/activities/sprite-fs.js +131 -0
- package/dist/op/activities/sprite-service-converge.js +224 -0
- package/dist/op/activities/sprite-services.js +174 -0
- package/dist/op/activities/sprite-tasks.js +57 -0
- package/dist/op/activities/sprites-contract.js +87 -0
- package/dist/op/activities/sprites-emulator.js +72 -0
- package/dist/op/activities/sprites-fake.js +547 -0
- package/dist/op/activities/sprites.js +515 -0
- package/dist/op/activity-contracts.js +82 -0
- package/dist/op/builders.js +157 -0
- package/dist/ownership.js +29 -0
- package/dist/package-cli.js +20 -0
- package/dist/plugin.js +298 -0
- package/dist/pseudo.js +26 -0
- package/dist/reference-catalog.js +37 -0
- package/dist/release-metadata.js +59 -0
- package/dist/release-store.js +46 -0
- package/dist/serializer.js +299 -0
- package/dist/spec/fetch.js +32 -0
- package/dist/spec/parse.js +260 -0
- package/dist/teardown.js +194 -0
- package/dist/types/index.d.ts +33 -2
- package/dist/validate-cli.js +4 -0
- package/dist/validate.js +33 -0
- package/package.json +11 -11
- package/src/codegen/package.ts +2 -2
- package/src/generated/index.d.ts +33 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sprite's services as resources a `ConvergeOp` observes and converges
|
|
3
|
+
* (#2778).
|
|
4
|
+
*
|
|
5
|
+
* A box runs its app, hud, door and site as services a supervisor keeps
|
|
6
|
+
* running: sprite-env on a sprite, and a stand-in with the same command line
|
|
7
|
+
* elsewhere. No lexicon declares them, so a ConvergeOp can't observe them
|
|
8
|
+
* through an environment. It can through an observer step:
|
|
9
|
+
*
|
|
10
|
+
* - `spriteServicesObserve` reads the supervisor's list and probes each
|
|
11
|
+
* declared service's health URL, and returns one verdict per service,
|
|
12
|
+
* `in-sync`, `drifted` or `unknown`: the `ResourceObservation` a
|
|
13
|
+
* `ConvergeOp({ observe })` evaluates its rules against, once per service.
|
|
14
|
+
* - `spriteServiceRestart` restarts one service through the supervisor and
|
|
15
|
+
* waits for its health URL. It is the step of the Op a rule dispatches; the
|
|
16
|
+
* service is the one the rule fired for, which the dispatched run reads
|
|
17
|
+
* from `CHANT_CONVERGE_RESOURCE`.
|
|
18
|
+
*
|
|
19
|
+
* Both work in one of two ways. With no sprite `id`, they run inside the
|
|
20
|
+
* sprite and call `sprite-env services` (found on PATH or in /.sprite/bin,
|
|
21
|
+
* or at `spriteEnv`). With an `id`, they go through the Sprites API
|
|
22
|
+
* (`spriteServiceList`, `spriteServiceStop` and `spriteServiceStart`).
|
|
23
|
+
*
|
|
24
|
+
* The declared services come from `services`, from a `servicesFile`, or,
|
|
25
|
+
* with `box: true`, from the `services` of the box block of the workspace
|
|
26
|
+
* member whose directory holds the working directory (#2880), the list
|
|
27
|
+
* `chant workspace status --json` prints as `members[].box.services`.
|
|
28
|
+
*/
|
|
29
|
+
import { execFile } from "node:child_process";
|
|
30
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
31
|
+
import { join, resolve } from "node:path";
|
|
32
|
+
import { promisify } from "node:util";
|
|
33
|
+
import { boxServices } from "./box-services.js";
|
|
34
|
+
import { spriteServiceList, spriteServiceStart, spriteServiceStop } from "./sprite-services.js";
|
|
35
|
+
import { defaultSpritesHttp } from "./sprites.js";
|
|
36
|
+
import { logProgress } from "./progress.js";
|
|
37
|
+
const execFileAsync = promisify(execFile);
|
|
38
|
+
/**
|
|
39
|
+
* The variable a ConvergeOp's dispatched run reads its resource from
|
|
40
|
+
* (core's `CONVERGE_RESOURCE_ENV`). Spelled out here so the lexicon runs
|
|
41
|
+
* against a core that predates the export.
|
|
42
|
+
*/
|
|
43
|
+
const CONVERGE_RESOURCE_ENV = "CHANT_CONVERGE_RESOURCE";
|
|
44
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
45
|
+
/** The services declared inline and in `servicesFile`, inline first. */
|
|
46
|
+
export function declaredServices(args, cwd = process.cwd()) {
|
|
47
|
+
const out = [...(args.services ?? [])];
|
|
48
|
+
if (args.servicesFile) {
|
|
49
|
+
const parsed = JSON.parse(readFileSync(resolve(cwd, args.servicesFile), "utf8"));
|
|
50
|
+
if (!Array.isArray(parsed.services))
|
|
51
|
+
throw new Error(`${args.servicesFile} has no "services" array`);
|
|
52
|
+
for (const s of parsed.services) {
|
|
53
|
+
if (!s || typeof s.name !== "string")
|
|
54
|
+
throw new Error(`${args.servicesFile}: a service has no name`);
|
|
55
|
+
out.push(s);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The declared services: with `box: true`, the box block's (#2880), and
|
|
62
|
+
* otherwise those of {@link declaredServices}. `box` in place of the other
|
|
63
|
+
* two, never beside them.
|
|
64
|
+
*/
|
|
65
|
+
export async function resolveDeclaredServices(args, cwd = process.cwd()) {
|
|
66
|
+
if (!args.box)
|
|
67
|
+
return declaredServices(args, cwd);
|
|
68
|
+
if (args.services || args.servicesFile)
|
|
69
|
+
throw new Error("box: true reads the services from the box block, so it takes no services or servicesFile beside it");
|
|
70
|
+
return (await boxServices(cwd)).map((s) => ({ name: s.name, ...(s.health ? { health: s.health } : {}), ...(s.optional ? { optional: true } : {}) }));
|
|
71
|
+
}
|
|
72
|
+
/** The sprite-env binary, or null when there is none. */
|
|
73
|
+
export function findSpriteEnv(explicit, env = process.env) {
|
|
74
|
+
if (explicit)
|
|
75
|
+
return explicit;
|
|
76
|
+
for (const dir of (env.PATH ?? "").split(":").concat("/.sprite/bin")) {
|
|
77
|
+
if (dir && existsSync(join(dir, "sprite-env")))
|
|
78
|
+
return join(dir, "sprite-env");
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `sprite-env services list` as name to state. A sprite's sprite-env prints
|
|
84
|
+
* JSON (an array of services with `state.status`); a stand-in may print a
|
|
85
|
+
* tab-separated table whose second column is the state. Pure.
|
|
86
|
+
*/
|
|
87
|
+
export function parseServicesList(text) {
|
|
88
|
+
const out = new Map();
|
|
89
|
+
const trimmed = text.trim();
|
|
90
|
+
if (!trimmed)
|
|
91
|
+
return out;
|
|
92
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
93
|
+
const parsed = JSON.parse(trimmed);
|
|
94
|
+
const list = Array.isArray(parsed) ? parsed : (parsed.services ?? []);
|
|
95
|
+
for (const s of list) {
|
|
96
|
+
if (typeof s?.name !== "string")
|
|
97
|
+
continue;
|
|
98
|
+
const status = typeof s.state?.status === "string" ? s.state.status : typeof s.status === "string" ? s.status : null;
|
|
99
|
+
out.set(s.name, status);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
for (const line of trimmed.split("\n")) {
|
|
104
|
+
const cols = line.split("\t");
|
|
105
|
+
if (!cols[0])
|
|
106
|
+
continue;
|
|
107
|
+
out.set(cols[0].trim(), cols[1]?.trim() || null);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
async function listServices(via, signal, http) {
|
|
112
|
+
if (via.id) {
|
|
113
|
+
const list = await spriteServiceList({ id: via.id, endpoint: via.endpoint, token: via.token }, signal, http ?? defaultSpritesHttp(via.token));
|
|
114
|
+
return new Map(list.map((s) => [s.name, s.state?.status ?? null]));
|
|
115
|
+
}
|
|
116
|
+
const bin = findSpriteEnv(via.spriteEnv);
|
|
117
|
+
if (!bin)
|
|
118
|
+
throw new Error("no sprite-env on PATH or in /.sprite/bin, and no sprite id to reach the Sprites API with");
|
|
119
|
+
const { stdout } = await execFileAsync(bin, ["services", "list"], { signal, timeout: 60000, maxBuffer: 16 * 1024 * 1024 });
|
|
120
|
+
return parseServicesList(stdout);
|
|
121
|
+
}
|
|
122
|
+
/** One GET of a health URL: true on 200. */
|
|
123
|
+
export async function probeHealth(url, signal) {
|
|
124
|
+
try {
|
|
125
|
+
const timeout = AbortSignal.timeout(3000);
|
|
126
|
+
const res = await fetch(url, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout, redirect: "manual" });
|
|
127
|
+
await res.arrayBuffer().catch(() => undefined);
|
|
128
|
+
return res.status === 200;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const DOWN = new Set(["stopped", "failed", "stopping"]);
|
|
135
|
+
/**
|
|
136
|
+
* Observe the declared services: one verdict each, for a ConvergeOp's rules.
|
|
137
|
+
* A service the supervisor doesn't have, or has stopped, is drifted; one
|
|
138
|
+
* with a health URL is in sync when the URL answers 200 within `probes`
|
|
139
|
+
* tries and drifted when it doesn't; one without is in sync while it runs.
|
|
140
|
+
* When the supervisor can't be read, every service is unknown, and unknown
|
|
141
|
+
* never remediates.
|
|
142
|
+
*/
|
|
143
|
+
export async function spriteServicesObserve(args, signal, http) {
|
|
144
|
+
const declared = await resolveDeclaredServices(args);
|
|
145
|
+
const probes = Math.max(1, args.probes ?? 3);
|
|
146
|
+
const interval = args.probeIntervalMs ?? 1000;
|
|
147
|
+
let states;
|
|
148
|
+
try {
|
|
149
|
+
states = await listServices(args, signal, http);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
const why = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
153
|
+
return {
|
|
154
|
+
resources: declared.filter((s) => !s.optional).map((s) => ({ name: s.name, status: "unknown", detail: `the supervisor could not be read: ${why}` })),
|
|
155
|
+
skipped: declared.filter((s) => s.optional).map((s) => s.name),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const resources = [];
|
|
159
|
+
const skipped = [];
|
|
160
|
+
for (const service of declared) {
|
|
161
|
+
if (!states.has(service.name)) {
|
|
162
|
+
if (service.optional)
|
|
163
|
+
skipped.push(service.name);
|
|
164
|
+
else
|
|
165
|
+
resources.push({ name: service.name, status: "drifted", detail: "declared, but the supervisor has no such service" });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const state = states.get(service.name) ?? null;
|
|
169
|
+
if (state !== null && DOWN.has(state)) {
|
|
170
|
+
resources.push({ name: service.name, status: "drifted", detail: state });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (service.health) {
|
|
174
|
+
let ok = false;
|
|
175
|
+
for (let i = 0; i < probes && !ok; i++) {
|
|
176
|
+
if (i > 0)
|
|
177
|
+
await sleep(interval);
|
|
178
|
+
ok = await probeHealth(service.health, signal);
|
|
179
|
+
}
|
|
180
|
+
resources.push(ok
|
|
181
|
+
? { name: service.name, status: "in-sync", detail: `${state ?? "listed"}, ${service.health} answers 200` }
|
|
182
|
+
: { name: service.name, status: "drifted", detail: `${state ?? "listed"}, ${service.health} does not answer 200 after ${probes} tries` });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
resources.push(state === "running"
|
|
186
|
+
? { name: service.name, status: "in-sync", detail: "running" }
|
|
187
|
+
: { name: service.name, status: "unknown", detail: `${state ?? "no state"}, and no health URL to probe` });
|
|
188
|
+
}
|
|
189
|
+
return { resources, skipped };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Restart one service through its supervisor and wait for its health URL.
|
|
193
|
+
* The service is `name`, or the resource the ConvergeOp's rule fired for.
|
|
194
|
+
* Throws when neither names one, when the supervisor refuses, or when the
|
|
195
|
+
* service does not answer its health URL within `waitMs`.
|
|
196
|
+
*/
|
|
197
|
+
export async function spriteServiceRestart(args, signal, http) {
|
|
198
|
+
const name = args.name ?? process.env[CONVERGE_RESOURCE_ENV];
|
|
199
|
+
if (!name) {
|
|
200
|
+
throw new Error(`spriteServiceRestart: no service named: pass \`name\`, or run the Op from a ConvergeOp rule, which sets ${CONVERGE_RESOURCE_ENV}`);
|
|
201
|
+
}
|
|
202
|
+
const health = args.health ?? (args.services || args.servicesFile || args.box ? (await resolveDeclaredServices(args)).find((s) => s.name === name)?.health : undefined);
|
|
203
|
+
if (args.id) {
|
|
204
|
+
const client = http ?? defaultSpritesHttp(args.token);
|
|
205
|
+
await spriteServiceStop({ id: args.id, name, endpoint: args.endpoint, token: args.token }, signal, client);
|
|
206
|
+
await spriteServiceStart({ id: args.id, name, endpoint: args.endpoint, token: args.token }, signal, client);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const bin = findSpriteEnv(args.spriteEnv);
|
|
210
|
+
if (!bin)
|
|
211
|
+
throw new Error("no sprite-env on PATH or in /.sprite/bin, and no sprite id to reach the Sprites API with");
|
|
212
|
+
await execFileAsync(bin, ["services", "restart", name], { signal, timeout: 120000 });
|
|
213
|
+
}
|
|
214
|
+
logProgress(`restarted: ${name}`);
|
|
215
|
+
if (!health)
|
|
216
|
+
return { name, healthy: null };
|
|
217
|
+
const until = Date.now() + (args.waitMs ?? 60000);
|
|
218
|
+
do {
|
|
219
|
+
if (await probeHealth(health, signal))
|
|
220
|
+
return { name, healthy: true };
|
|
221
|
+
await sleep(500);
|
|
222
|
+
} while (Date.now() < until && !signal?.aborted);
|
|
223
|
+
throw new Error(`${name} was restarted and does not answer ${health}`);
|
|
224
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprite Services activities (#2711) — imperative create/get/list/start/stop/
|
|
3
|
+
* delete/logs over a Sprite's background services, the surface a box's door
|
|
4
|
+
* and hud run as long-lived services through (`sprite-env services`
|
|
5
|
+
* inside the sprite, `/v1/sprites/{id}/services/...` outside). Shared by the
|
|
6
|
+
* Sprites API, wisp (arugula-salad/wisp) and spritzer 0.6.0's container mode
|
|
7
|
+
* (INTENTIUS/spritzer#22).
|
|
8
|
+
*
|
|
9
|
+
* This is the imperative twin of `sprite-config.ts`'s `spriteApplyServices` —
|
|
10
|
+
* that one reconciles a whole desired service *set* (additive create-or-update,
|
|
11
|
+
* optionally started, no delete). These activities are the single-service
|
|
12
|
+
* primitives underneath it: create-and-start one service and watch it come
|
|
13
|
+
* up, inspect or list what's running, stop/start one by name, delete it, and
|
|
14
|
+
* read its log tail. An Op that provisions a box one service at a time (or
|
|
15
|
+
* needs delete, which `spriteApplyServices` doesn't expose) uses these
|
|
16
|
+
* directly; `spriteApplyServices` stays the batch-reconcile convenience on
|
|
17
|
+
* top of the same wire shapes.
|
|
18
|
+
*
|
|
19
|
+
* `spriteServiceCreate` is a `PUT` that both defines the service and starts
|
|
20
|
+
* it, streaming NDJSON progress (`started`, `stdout`, `stderr`, `exit`,
|
|
21
|
+
* `complete`) for a bounded `?duration` window (default 500ms — enough to
|
|
22
|
+
* surface an immediate failure, e.g. a missing binary, without holding the
|
|
23
|
+
* step open for spritzer's own 5s default). The stream is read to completion
|
|
24
|
+
* either way; the definitive result comes from a follow-up `GET` of the
|
|
25
|
+
* service (`spriteServiceGet`'s own logic), which throws if its `state.status`
|
|
26
|
+
* is `"failed"`. `spriteServiceStart` follows the same create-then-GET shape
|
|
27
|
+
* (`PUT` already started it; `start` is for a stopped service).
|
|
28
|
+
*/
|
|
29
|
+
import { resolveSpritesEndpoint, defaultSpritesHttp } from "./sprites.js";
|
|
30
|
+
import { logProgress } from "./progress.js";
|
|
31
|
+
// ── URL building (pure) ─────────────────────────────────────────────────────
|
|
32
|
+
const servicesUrl = (base, id) => `${base}/v1/sprites/${encodeURIComponent(id)}/services`;
|
|
33
|
+
const serviceUrl = (base, id, name) => `${servicesUrl(base, id)}/${encodeURIComponent(name)}`;
|
|
34
|
+
const serviceLogsUrl = (base, id, name) => `${serviceUrl(base, id, name)}/logs`;
|
|
35
|
+
function safeJson(text) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Build the `PUT` create-or-update body. Pure. */
|
|
44
|
+
export function spriteServiceCreateBody(args) {
|
|
45
|
+
return {
|
|
46
|
+
cmd: args.cmd,
|
|
47
|
+
...(args.args !== undefined ? { args: args.args } : {}),
|
|
48
|
+
...(args.env !== undefined ? { env: args.env } : {}),
|
|
49
|
+
...(args.dir !== undefined ? { dir: args.dir } : {}),
|
|
50
|
+
...(args.needs !== undefined ? { needs: args.needs } : {}),
|
|
51
|
+
...(args.http_port !== undefined ? { http_port: args.http_port } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse a service log NDJSON body (`{type, data, timestamp, ...}` per line,
|
|
56
|
+
* terminated by a `complete` event) into a plain line tail — the `data` text
|
|
57
|
+
* of every non-`complete` event, in stream order. Pure.
|
|
58
|
+
*/
|
|
59
|
+
export function parseServiceLogNdjson(text) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (const raw of text.split("\n")) {
|
|
62
|
+
const t = raw.trim();
|
|
63
|
+
if (!t)
|
|
64
|
+
continue;
|
|
65
|
+
const obj = safeJson(t);
|
|
66
|
+
if (!obj || obj.type === "complete")
|
|
67
|
+
continue;
|
|
68
|
+
if (typeof obj.data === "string")
|
|
69
|
+
lines.push(obj.data);
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
// ── Activities ───────────────────────────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* Create (or replace) and start a service. `PUT /v1/sprites/{id}/services/{name}`,
|
|
76
|
+
* streamed as NDJSON for `?duration` (mapped from `durationMs`). The stream is
|
|
77
|
+
* read to completion, then a `GET` fetches the definitive state; a `"failed"`
|
|
78
|
+
* status throws with the agent's own error text.
|
|
79
|
+
*/
|
|
80
|
+
export async function spriteServiceCreate(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
81
|
+
const base = resolveSpritesEndpoint(args);
|
|
82
|
+
const durationMs = args.durationMs ?? 500;
|
|
83
|
+
const url = `${serviceUrl(base, args.id, args.name)}?duration=${durationMs}ms`;
|
|
84
|
+
const res = await http("PUT", url, spriteServiceCreateBody(args), undefined, signal);
|
|
85
|
+
if (res.status >= 300) {
|
|
86
|
+
throw new Error(`sprite ${args.id} service ${args.name} create failed (${res.status}): ${res.text}`);
|
|
87
|
+
}
|
|
88
|
+
const svc = await spriteServiceGet({ id: args.id, name: args.name, endpoint: args.endpoint, token: args.token }, signal, http);
|
|
89
|
+
if (svc.state.status === "failed") {
|
|
90
|
+
throw new Error(`sprite ${args.id} service ${args.name} failed to start: ${svc.state.error ?? "unknown error"}`);
|
|
91
|
+
}
|
|
92
|
+
logProgress(`service: sprite/${args.id}/${args.name} ${svc.state.status} (${base})`);
|
|
93
|
+
return svc;
|
|
94
|
+
}
|
|
95
|
+
/** Get one service. `GET /v1/sprites/{id}/services/{name}`. */
|
|
96
|
+
export async function spriteServiceGet(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
97
|
+
const base = resolveSpritesEndpoint(args);
|
|
98
|
+
const res = await http("GET", serviceUrl(base, args.id, args.name), undefined, undefined, signal);
|
|
99
|
+
if (res.status >= 300) {
|
|
100
|
+
throw new Error(`sprite ${args.id} service ${args.name} get failed (${res.status}): ${res.text}`);
|
|
101
|
+
}
|
|
102
|
+
return safeJson(res.text) ?? { name: args.name, cmd: "", state: { name: args.name, status: "stopped" } };
|
|
103
|
+
}
|
|
104
|
+
/** List every service on the sprite. `GET /v1/sprites/{id}/services`. */
|
|
105
|
+
export async function spriteServiceList(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
106
|
+
const base = resolveSpritesEndpoint(args);
|
|
107
|
+
const res = await http("GET", servicesUrl(base, args.id), undefined, undefined, signal);
|
|
108
|
+
if (res.status >= 300)
|
|
109
|
+
throw new Error(`sprite ${args.id} service list failed (${res.status}): ${res.text}`);
|
|
110
|
+
const parsed = safeJson(res.text);
|
|
111
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Start a stopped service. `POST /v1/sprites/{id}/services/{name}/start`,
|
|
115
|
+
* streamed as NDJSON for `?duration`, then a `GET` for the definitive state
|
|
116
|
+
* (same shape as `spriteServiceCreate`).
|
|
117
|
+
*/
|
|
118
|
+
export async function spriteServiceStart(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
119
|
+
const base = resolveSpritesEndpoint(args);
|
|
120
|
+
const durationMs = args.durationMs ?? 500;
|
|
121
|
+
const url = `${serviceUrl(base, args.id, args.name)}/start?duration=${durationMs}ms`;
|
|
122
|
+
const res = await http("POST", url, undefined, undefined, signal);
|
|
123
|
+
if (res.status >= 300) {
|
|
124
|
+
throw new Error(`sprite ${args.id} service ${args.name} start failed (${res.status}): ${res.text}`);
|
|
125
|
+
}
|
|
126
|
+
const svc = await spriteServiceGet({ id: args.id, name: args.name, endpoint: args.endpoint, token: args.token }, signal, http);
|
|
127
|
+
if (svc.state.status === "failed") {
|
|
128
|
+
throw new Error(`sprite ${args.id} service ${args.name} failed to start: ${svc.state.error ?? "unknown error"}`);
|
|
129
|
+
}
|
|
130
|
+
logProgress(`service: sprite/${args.id}/${args.name} ${svc.state.status} (${base})`);
|
|
131
|
+
return svc;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Stop a running service (SIGTERM, then SIGKILL after the grace period).
|
|
135
|
+
* `POST /v1/sprites/{id}/services/{name}/stop`.
|
|
136
|
+
*/
|
|
137
|
+
export async function spriteServiceStop(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
138
|
+
const base = resolveSpritesEndpoint(args);
|
|
139
|
+
const qs = args.timeoutMs !== undefined ? `?timeout=${args.timeoutMs}ms` : "";
|
|
140
|
+
const url = `${serviceUrl(base, args.id, args.name)}/stop${qs}`;
|
|
141
|
+
const res = await http("POST", url, undefined, undefined, signal);
|
|
142
|
+
if (res.status >= 300) {
|
|
143
|
+
throw new Error(`sprite ${args.id} service ${args.name} stop failed (${res.status}): ${res.text}`);
|
|
144
|
+
}
|
|
145
|
+
const svc = await spriteServiceGet({ id: args.id, name: args.name, endpoint: args.endpoint, token: args.token }, signal, http);
|
|
146
|
+
logProgress(`service: sprite/${args.id}/${args.name} ${svc.state.status} (${base})`);
|
|
147
|
+
return svc;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Delete a service, stopping it first (idempotent; a 404 means it is already
|
|
151
|
+
* gone). `DELETE /v1/sprites/{id}/services/{name}`.
|
|
152
|
+
*/
|
|
153
|
+
export async function spriteServiceDelete(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
154
|
+
const base = resolveSpritesEndpoint(args);
|
|
155
|
+
const res = await http("DELETE", serviceUrl(base, args.id, args.name), undefined, undefined, signal);
|
|
156
|
+
if (res.status >= 300 && res.status !== 404) {
|
|
157
|
+
throw new Error(`sprite ${args.id} service ${args.name} delete failed (${res.status}): ${res.text}`);
|
|
158
|
+
}
|
|
159
|
+
logProgress(`deleted: sprite/${args.id}/${args.name}`);
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Read a service's log tail. `GET /v1/sprites/{id}/services/{name}/logs`,
|
|
164
|
+
* parsed from NDJSON to a plain line array (`parseServiceLogNdjson`).
|
|
165
|
+
*/
|
|
166
|
+
export async function spriteServiceLogs(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
167
|
+
const base = resolveSpritesEndpoint(args);
|
|
168
|
+
const qs = args.lines !== undefined ? `?lines=${args.lines}` : "";
|
|
169
|
+
const res = await http("GET", `${serviceLogsUrl(base, args.id, args.name)}${qs}`, undefined, undefined, signal);
|
|
170
|
+
if (res.status >= 300) {
|
|
171
|
+
throw new Error(`sprite ${args.id} service ${args.name} logs failed (${res.status}): ${res.text}`);
|
|
172
|
+
}
|
|
173
|
+
return { lines: parseServiceLogNdjson(res.text) };
|
|
174
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprite keep-alive Tasks activities (#847) — a hold that prevents a Sprite from
|
|
3
|
+
* pausing while a session runs. While at least one task exists the Sprite stays
|
|
4
|
+
* active; the hold is refreshed on an interval and released on exit.
|
|
5
|
+
*
|
|
6
|
+
* Per the Sprites docs a task carries an `expire` (seconds or a duration string
|
|
7
|
+
* like `"5m"`/`"1h"`) with a **1-hour max per task**, so a session longer than
|
|
8
|
+
* that must refresh. The recommended shape is a short expiry refreshed on a
|
|
9
|
+
* shorter interval — 5-minute expiry / 60-second refresh — released on exit.
|
|
10
|
+
*
|
|
11
|
+
* These are the three primitives, not a magic looping hold: a phased Op creates
|
|
12
|
+
* the task around a session (`spriteTaskCreate`) and releases it after (in the
|
|
13
|
+
* happy path and in `onFailure`). A worker whose session can outlast the 1-hour
|
|
14
|
+
* cap wraps its own run in a `spriteTaskRefresh` loop (that ambient loop lives in
|
|
15
|
+
* the long-running caller, not a single serializable activity — a phased Op has
|
|
16
|
+
* no single step to hang it on). `spriteTaskRelease` is idempotent so a crash
|
|
17
|
+
* still frees the Sprite (and the task auto-expires if the release never lands).
|
|
18
|
+
*
|
|
19
|
+
* The task REST path is provisional (S6, #766); it mirrors the other endpoints'
|
|
20
|
+
* `/v1/sprites/{id}/...` shape. URL building is a pure helper so the path can
|
|
21
|
+
* move without touching callers.
|
|
22
|
+
*/
|
|
23
|
+
import { resolveSpritesEndpoint, defaultSpritesHttp } from "./sprites.js";
|
|
24
|
+
/** The task REST base for a sprite. Pure. */
|
|
25
|
+
export function spriteTasksUrl(base, id) {
|
|
26
|
+
return `${base}/v1/sprites/${encodeURIComponent(id)}/tasks`;
|
|
27
|
+
}
|
|
28
|
+
export function spriteTaskUrl(base, id, name) {
|
|
29
|
+
return `${spriteTasksUrl(base, id)}/${encodeURIComponent(name)}`;
|
|
30
|
+
}
|
|
31
|
+
/** Create a keep-alive task. `POST /v1/sprites/{id}/tasks`. */
|
|
32
|
+
export async function spriteTaskCreate(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
33
|
+
const base = resolveSpritesEndpoint(args);
|
|
34
|
+
const body = { name: args.name, ...(args.expire !== undefined ? { expire: args.expire } : {}) };
|
|
35
|
+
const res = await http("POST", spriteTasksUrl(base, args.id), body, undefined, signal);
|
|
36
|
+
if (res.status >= 300)
|
|
37
|
+
throw new Error(`sprite ${args.id} task create failed (${res.status}): ${res.text}`);
|
|
38
|
+
return { name: args.name };
|
|
39
|
+
}
|
|
40
|
+
/** Refresh a task's expiry. `PUT /v1/sprites/{id}/tasks/{name}`. */
|
|
41
|
+
export async function spriteTaskRefresh(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
42
|
+
const base = resolveSpritesEndpoint(args);
|
|
43
|
+
const body = args.expire !== undefined ? { expire: args.expire } : undefined;
|
|
44
|
+
const res = await http("PUT", spriteTaskUrl(base, args.id, args.name), body, undefined, signal);
|
|
45
|
+
if (res.status >= 300)
|
|
46
|
+
throw new Error(`sprite ${args.id} task refresh failed (${res.status}): ${res.text}`);
|
|
47
|
+
return { name: args.name };
|
|
48
|
+
}
|
|
49
|
+
/** Release a task (idempotent; a 404 means already gone). `DELETE /v1/sprites/{id}/tasks/{name}`. */
|
|
50
|
+
export async function spriteTaskRelease(args, signal, http = defaultSpritesHttp(args.token)) {
|
|
51
|
+
const base = resolveSpritesEndpoint(args);
|
|
52
|
+
const res = await http("DELETE", spriteTaskUrl(base, args.id, args.name), undefined, undefined, signal);
|
|
53
|
+
if (res.status >= 300 && res.status !== 404) {
|
|
54
|
+
throw new Error(`sprite ${args.id} task release failed (${res.status}): ${res.text}`);
|
|
55
|
+
}
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-authored Fly Sprites API contract (#808 T3).
|
|
3
|
+
*
|
|
4
|
+
* The Fly Machines API ships a machine-readable OpenAPI (docs.machines.dev) that
|
|
5
|
+
* the fly resource surface generates + drift-checks against. The Sprites API
|
|
6
|
+
* (api.sprites.dev) ships no such spec at any conventional path, so there is
|
|
7
|
+
* nothing to diff automatically. This module is the manual stand-in: the exact
|
|
8
|
+
* endpoint set the fly sprite activities (lifecycle, filesystem, config, tasks)
|
|
9
|
+
* depend on, maintained by hand from https://docs.sprites.dev.
|
|
10
|
+
*
|
|
11
|
+
* It anchors two fidelity checks:
|
|
12
|
+
* 1. Coverage — every endpoint here must be served by the pinned spritzer
|
|
13
|
+
* emulator (its `/_spritzer/health` enumerates implemented paths). If an
|
|
14
|
+
* activity ever calls something spritzer doesn't model, the docker-gated
|
|
15
|
+
* contract test fails instead of silently passing against a partial fake.
|
|
16
|
+
* 2. Drift anchor — when the Sprites API changes, a human updates this file;
|
|
17
|
+
* reviewers see exactly which activity/endpoint moved.
|
|
18
|
+
*
|
|
19
|
+
* Path params are written with names matching ./sprites.ts (`{id}`, `{cp}`);
|
|
20
|
+
* the coverage check normalizes param names before comparing, since spritzer
|
|
21
|
+
* spells the checkpoint id `{cid}`.
|
|
22
|
+
*
|
|
23
|
+
* #2711 adds the imperative Services activities (`./sprite-services.ts`).
|
|
24
|
+
* Several of them share a physical endpoint with `spriteApplyServices`'s own
|
|
25
|
+
* calls (one `PUT`/one `GET .../services`/one `.../start` — the reconcile
|
|
26
|
+
* activity and the single-service primitives are two callers of the same
|
|
27
|
+
* REST surface), so one row lists every activity that calls it, comma
|
|
28
|
+
* separated, rather than one row per activity — `contractKeys()` stays one
|
|
29
|
+
* key per physical endpoint. `spriteServiceDelete` and `spriteServiceLogs`
|
|
30
|
+
* are deliberately absent: spritzer's interpreter mode (the coverage test's
|
|
31
|
+
* oracle) doesn't implement them, only its container-mode agent does, which
|
|
32
|
+
* has no per-verb entry in `/_spritzer/health` (real.go reports the whole
|
|
33
|
+
* `/v1/sprites/{id}/services/...` proxy as one wildcard line — nothing to
|
|
34
|
+
* normalize-compare against a specific verb). They're exercised instead by
|
|
35
|
+
* `sprite-services.docker.integration.e2e.test.ts` against real container-mode
|
|
36
|
+
* spritzer. `spriteUrl` is absent for the same reason, plus its target
|
|
37
|
+
* (`/s/{name}/...`) isn't under `/v1/sprites` at all.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* The Sprites endpoints the fly sprite activities depend on, across the lifecycle
|
|
41
|
+
* (./sprites.ts), filesystem (./sprite-fs.ts), config reconcile
|
|
42
|
+
* (./sprite-config.ts), and keep-alive tasks (./sprite-tasks.ts) modules. Keep in
|
|
43
|
+
* sync with the activity implementations — the unit test asserts every sprite
|
|
44
|
+
* activity is represented, and the coverage test asserts the pinned spritzer
|
|
45
|
+
* serves each one.
|
|
46
|
+
*/
|
|
47
|
+
export const SPRITES_CONTRACT = [
|
|
48
|
+
// Lifecycle (./sprites.ts)
|
|
49
|
+
{ method: "POST", path: "/v1/sprites", activity: "spriteCreate" },
|
|
50
|
+
{ method: "WS", path: "/v1/sprites/{id}/exec", activity: "spriteExec" },
|
|
51
|
+
{ method: "POST", path: "/v1/sprites/{id}/checkpoint", activity: "spriteCheckpoint" },
|
|
52
|
+
{ method: "GET", path: "/v1/sprites/{id}/checkpoints", activity: "listCheckpoints" },
|
|
53
|
+
{ method: "POST", path: "/v1/sprites/{id}/checkpoints/{cp}/restore", activity: "spriteRestore" },
|
|
54
|
+
{ method: "DELETE", path: "/v1/sprites/{id}", activity: "spriteDestroy, spriteDelete" },
|
|
55
|
+
// Filesystem (./sprite-fs.ts)
|
|
56
|
+
{ method: "PUT", path: "/v1/sprites/{id}/fs/write", activity: "spriteWriteFile" },
|
|
57
|
+
{ method: "GET", path: "/v1/sprites/{id}/fs/read", activity: "spriteReadFile" },
|
|
58
|
+
{ method: "GET", path: "/v1/sprites/{id}/fs/list", activity: "spriteListDir" },
|
|
59
|
+
{ method: "DELETE", path: "/v1/sprites/{id}/fs/delete", activity: "spriteRemove" },
|
|
60
|
+
// Network policy (./sprite-config.ts)
|
|
61
|
+
{ method: "GET", path: "/v1/sprites/{id}/policy/network", activity: "spriteApplyNetworkPolicy" },
|
|
62
|
+
{ method: "POST", path: "/v1/sprites/{id}/policy/network", activity: "spriteApplyNetworkPolicy" },
|
|
63
|
+
// Services (./sprite-config.ts, ./sprite-services.ts)
|
|
64
|
+
{ method: "GET", path: "/v1/sprites/{id}/services", activity: "spriteApplyServices, spriteServiceList" },
|
|
65
|
+
{ method: "GET", path: "/v1/sprites/{id}/services/{svc}", activity: "spriteServiceGet" },
|
|
66
|
+
{ method: "PUT", path: "/v1/sprites/{id}/services/{svc}", activity: "spriteApplyServices, spriteServiceCreate" },
|
|
67
|
+
{ method: "POST", path: "/v1/sprites/{id}/services/{svc}/start", activity: "spriteApplyServices, spriteServiceStart" },
|
|
68
|
+
{ method: "POST", path: "/v1/sprites/{id}/services/{svc}/stop", activity: "spriteServiceStop" },
|
|
69
|
+
// Keep-alive tasks (./sprite-tasks.ts)
|
|
70
|
+
{ method: "POST", path: "/v1/sprites/{id}/tasks", activity: "spriteTaskCreate" },
|
|
71
|
+
{ method: "PUT", path: "/v1/sprites/{id}/tasks/{name}", activity: "spriteTaskRefresh" },
|
|
72
|
+
{ method: "DELETE", path: "/v1/sprites/{id}/tasks/{name}", activity: "spriteTaskRelease" },
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* Normalize a `METHOD path` key for comparison across sources: collapse every
|
|
76
|
+
* `{param}` to `{}` (so `{cp}` and `{cid}` match) and treat the WebSocket exec
|
|
77
|
+
* channel as a `GET` (spritzer registers it as `GET .../exec`).
|
|
78
|
+
*/
|
|
79
|
+
export function normalizeEndpoint(method, path) {
|
|
80
|
+
const m = method === "WS" ? "GET" : method.toUpperCase();
|
|
81
|
+
const p = path.replace(/\{[^}]+\}/g, "{}");
|
|
82
|
+
return `${m} ${p}`;
|
|
83
|
+
}
|
|
84
|
+
/** The contract as a set of normalized `METHOD path` keys. */
|
|
85
|
+
export function contractKeys() {
|
|
86
|
+
return new Set(SPRITES_CONTRACT.map((e) => normalizeEndpoint(e.method, e.path)));
|
|
87
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { emulatorLifecycle } from "@intentius/chant/op";
|
|
2
|
+
import { SPRITZER_IMAGE, SPRITZER_CONTAINER_IMAGE } from "./emulator-images.js";
|
|
3
|
+
// spritzer is a stateful fake of the Fly Sprites API — a plain 200 on its health
|
|
4
|
+
// endpoint means ready. The local target for the sprite activities; point them
|
|
5
|
+
// there with SPRITES_BASE_URL. Shared lifecycle: emulatorLifecycle (the same
|
|
6
|
+
// helper that boots mudflaps for fly).
|
|
7
|
+
export const SPRITZER_SPEC = {
|
|
8
|
+
name: "chant-spritzer",
|
|
9
|
+
image: SPRITZER_IMAGE,
|
|
10
|
+
containerPort: 4290,
|
|
11
|
+
healthPath: "/_spritzer/health",
|
|
12
|
+
upstream: { repo: "intentius/spritzer" },
|
|
13
|
+
};
|
|
14
|
+
/** The Sprites half of fly's emulator capability (#1345). */
|
|
15
|
+
export const SPRITZER_EMULATOR = {
|
|
16
|
+
spec: SPRITZER_SPEC,
|
|
17
|
+
env: (endpoint) => ({ SPRITES_BASE_URL: endpoint }),
|
|
18
|
+
};
|
|
19
|
+
const spritzer = emulatorLifecycle(SPRITZER_SPEC);
|
|
20
|
+
export const spritesExistsCommand = spritzer.existsCommand;
|
|
21
|
+
export const spritesRmCommand = spritzer.rmCommand;
|
|
22
|
+
export const spritesHealthUrl = spritzer.healthUrl;
|
|
23
|
+
/** The Sprites endpoint URL (what the sprite activities' `SPRITES_BASE_URL`/`endpoint` points at). */
|
|
24
|
+
export const spritesEndpoint = spritzer.endpoint;
|
|
25
|
+
export const spritesRunCommand = (args = {}) => spritzer.runCommand(args);
|
|
26
|
+
/** Boot a local spritzer (Fly Sprites API emulator) in Docker and return its endpoint. */
|
|
27
|
+
export const spritesUp = (args = {}, signal) => spritzer.up(args, signal);
|
|
28
|
+
/** Stop and remove the local spritzer container (no-op if already gone). */
|
|
29
|
+
export const spritesDown = (args = {}, signal) => spritzer.down(args, signal);
|
|
30
|
+
// ── Container exec mode (#2711, INTENTIUS/spritzer#22) ─────────────────────────
|
|
31
|
+
//
|
|
32
|
+
// A second, distinct spritzer lifecycle: `SPRITZER_EXEC=container` with the
|
|
33
|
+
// Docker runtime, so each sprite it makes is a real container — the mode the
|
|
34
|
+
// Services CRUD activities (`./sprite-services.ts`) and `spriteUrl` need (real
|
|
35
|
+
// processes, a real sprite URL), and that `SPRITZER_SPEC` above (interpreter
|
|
36
|
+
// mode, the default everything else tests against) does not run. The Docker
|
|
37
|
+
// socket is mounted read-write so spritzer can create/exec/delete sibling
|
|
38
|
+
// sprite containers through it (docker-outside-of-docker); no port publishing
|
|
39
|
+
// is needed for the sprites themselves — spritzer reaches them by exec, and
|
|
40
|
+
// proxies their URL (`/s/{name}/...`) through the one published port.
|
|
41
|
+
/** Container exec mode spritzer, pinned to {@link SPRITZER_CONTAINER_IMAGE} (0.6.0, the first release with this mode). */
|
|
42
|
+
export const SPRITZER_CONTAINER_SPEC = {
|
|
43
|
+
name: "chant-spritzer-container",
|
|
44
|
+
image: SPRITZER_CONTAINER_IMAGE,
|
|
45
|
+
containerPort: 4290,
|
|
46
|
+
healthPath: "/_spritzer/health",
|
|
47
|
+
runArgs: [
|
|
48
|
+
// The image runs as `nonroot` by default, which cannot open a
|
|
49
|
+
// group/other-unwritable host socket — root inside the container is
|
|
50
|
+
// still an unprivileged Linux user account relative to the host/Docker
|
|
51
|
+
// Desktop VM, no different from any other `docker run` that mounts the
|
|
52
|
+
// socket.
|
|
53
|
+
"--user",
|
|
54
|
+
"root",
|
|
55
|
+
"-e",
|
|
56
|
+
"SPRITZER_EXEC=container",
|
|
57
|
+
"-e",
|
|
58
|
+
"SPRITZER_RUNTIME=docker",
|
|
59
|
+
"-v",
|
|
60
|
+
"/var/run/docker.sock:/var/run/docker.sock",
|
|
61
|
+
],
|
|
62
|
+
upstream: { repo: "intentius/spritzer" },
|
|
63
|
+
};
|
|
64
|
+
const spritzerContainer = emulatorLifecycle(SPRITZER_CONTAINER_SPEC);
|
|
65
|
+
export const spritesContainerRunCommand = (args = {}) => spritzerContainer.runCommand(args);
|
|
66
|
+
export const spritesContainerExistsCommand = spritzerContainer.existsCommand;
|
|
67
|
+
export const spritesContainerRmCommand = spritzerContainer.rmCommand;
|
|
68
|
+
export const spritesContainerHealthUrl = spritzerContainer.healthUrl;
|
|
69
|
+
/** Boot a local spritzer in container exec mode (Docker runtime) and return its endpoint. */
|
|
70
|
+
export const spritesContainerUp = (args = {}, signal) => spritzerContainer.up(args, signal);
|
|
71
|
+
/** Stop and remove the local container-exec-mode spritzer (no-op if already gone). Sprite containers it made are the caller's to clean up (see the integration test). */
|
|
72
|
+
export const spritesContainerDown = (args = {}, signal) => spritzerContainer.down(args, signal);
|