@intentius/chant-lexicon-fly 0.124.31 → 0.124.33

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,92 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * Fly guest presets, per `cpu_kind`.
4
+ *
5
+ * `cpus` is the set of valid CPU counts; `memory_mb` must be a multiple of 256
6
+ * within [minPerCpu * cpus, maxPerCpu * cpus]. Values mirror Fly's published
7
+ * guest presets (shared-cpu-Nx / performance-Nx): shared allows 256–2048 MB per
8
+ * CPU, performance allows 2048–8192 MB per CPU. The 256 MB step is the loosest
9
+ * safe multiple, so a valid config is never flagged; only clearly-invalid combos
10
+ * are. fly-go was not vendored in this tree to cross-check against, so this table
11
+ * is a documented representative set (a limitation, widen if Fly changes presets).
12
+ */
13
+ const GUEST_PRESETS = {
14
+ shared: { cpus: new Set([1, 2, 4, 8]), minPerCpu: 256, maxPerCpu: 2048 },
15
+ performance: { cpus: new Set([1, 2, 4, 8, 16]), minPerCpu: 2048, maxPerCpu: 8192 },
16
+ };
17
+ const MEMORY_STEP_MB = 256;
18
+ /** Read a numeric property from an object literal, if present as a plain number literal. */
19
+ function numberProp(obj, key, sf) {
20
+ for (const prop of obj.properties) {
21
+ if (ts.isPropertyAssignment(prop) && prop.name.getText(sf) === key && ts.isNumericLiteral(prop.initializer)) {
22
+ return Number(prop.initializer.text);
23
+ }
24
+ }
25
+ return undefined;
26
+ }
27
+ /** Read a string property from an object literal, if present as a plain string literal. */
28
+ function stringProp(obj, key, sf) {
29
+ for (const prop of obj.properties) {
30
+ if (ts.isPropertyAssignment(prop) && prop.name.getText(sf) === key && ts.isStringLiteral(prop.initializer)) {
31
+ return prop.initializer.text;
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+ /**
37
+ * FLY002: Sane guest sizing
38
+ *
39
+ * A guest's `cpu_kind`, `cpus`, and `memory_mb` must be a valid combination per
40
+ * Fly's guest presets. An invalid combination is rejected at apply time.
41
+ */
42
+ export const guestSizingRule = {
43
+ id: "FLY002",
44
+ severity: "error",
45
+ category: "correctness",
46
+ description: "A guest's cpu_kind, cpus and memory_mb must be a valid Fly combination",
47
+ check(context) {
48
+ const { sourceFile } = context;
49
+ const diagnostics = [];
50
+ function report(node, message) {
51
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
52
+ diagnostics.push({
53
+ file: sourceFile.fileName,
54
+ line: line + 1,
55
+ column: character + 1,
56
+ ruleId: "FLY002",
57
+ severity: "error",
58
+ message,
59
+ });
60
+ }
61
+ function visit(node) {
62
+ // A guest is any object literal carrying a cpu_kind property (new MachineGuest({...})
63
+ // or an inline guest object).
64
+ if (ts.isObjectLiteralExpression(node)) {
65
+ const cpuKind = stringProp(node, "cpu_kind", sourceFile);
66
+ if (cpuKind !== undefined) {
67
+ const preset = GUEST_PRESETS[cpuKind];
68
+ if (!preset) {
69
+ report(node, `Invalid guest cpu_kind "${cpuKind}". Use "shared" or "performance".`);
70
+ }
71
+ else {
72
+ const cpus = numberProp(node, "cpus", sourceFile);
73
+ const memoryMb = numberProp(node, "memory_mb", sourceFile);
74
+ if (cpus !== undefined && !preset.cpus.has(cpus)) {
75
+ report(node, `Invalid guest sizing: cpu_kind "${cpuKind}" does not allow ${cpus} cpus (valid: ${[...preset.cpus].join(", ")}).`);
76
+ }
77
+ else if (cpus !== undefined && memoryMb !== undefined) {
78
+ const min = preset.minPerCpu * cpus;
79
+ const max = preset.maxPerCpu * cpus;
80
+ if (memoryMb % MEMORY_STEP_MB !== 0 || memoryMb < min || memoryMb > max) {
81
+ report(node, `Invalid guest sizing: ${cpuKind}/${cpus} cpu requires memory_mb between ${min} and ${max} in steps of ${MEMORY_STEP_MB}, got ${memoryMb}.`);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ts.forEachChild(node, visit);
88
+ }
89
+ visit(sourceFile);
90
+ return diagnostics;
91
+ },
92
+ };
@@ -0,0 +1,12 @@
1
+ import { validRegionRule } from "./valid-region.js";
2
+ import { guestSizingRule } from "./guest-sizing.js";
3
+ import { noSecretLiteralsRule } from "./no-secret-literals.js";
4
+ export { validRegionRule } from "./valid-region.js";
5
+ export { guestSizingRule } from "./guest-sizing.js";
6
+ export { noSecretLiteralsRule } from "./no-secret-literals.js";
7
+ /** All lint rules provided by the fly lexicon. */
8
+ export const rules = [
9
+ validRegionRule,
10
+ guestSizingRule,
11
+ noSecretLiteralsRule,
12
+ ];
@@ -0,0 +1,58 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * Property names that carry a credential. Kept conservative on purpose — an
4
+ * inline value under one of these keys is very likely a real secret.
5
+ */
6
+ const SECRET_KEY_PATTERN = /(?:^|[_-])(?:password|passwd|secret|token|api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|credential|auth[_-]?token)(?:$|[_-])/i;
7
+ /**
8
+ * Values that are references or placeholders, not literal secrets:
9
+ * shell/interpolation references ($FOO, ${FOO}), the OpenTelemetry
10
+ * Collector's environment substitution (${env:FOO}), and secret-manager
11
+ * style references. These are skipped so a `secrets` reference is never
12
+ * flagged.
13
+ */
14
+ const REFERENCE_VALUE_PATTERN = /^(?:\$\{?(?:env:)?[A-Za-z0-9_]+\}?|(?:secret|ref|env|vault):\S+)$/;
15
+ /**
16
+ * FLY004: No secret literals in machine config
17
+ *
18
+ * Flags secret values written inline in config (e.g. an env value under a
19
+ * credential-looking key). Secrets belong in `secrets` (apply-only) or a
20
+ * reference. The heuristic is conservative: it fires only when the property
21
+ * name looks like a credential AND the value is a plain string literal that is
22
+ * not a reference/placeholder, so a `secrets` reference (an identifier or
23
+ * `$FOO` placeholder) is not flagged.
24
+ */
25
+ export const noSecretLiteralsRule = {
26
+ id: "FLY004",
27
+ severity: "warning",
28
+ category: "security",
29
+ description: "Secret values must not be written inline — use secrets or a reference",
30
+ check(context) {
31
+ const { sourceFile } = context;
32
+ const diagnostics = [];
33
+ function visit(node) {
34
+ if (ts.isPropertyAssignment(node)) {
35
+ const rawName = node.name.getText(sourceFile).replace(/^["']|["']$/g, "");
36
+ const init = node.initializer;
37
+ const literal = ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init) ? init.text : undefined;
38
+ if (literal !== undefined &&
39
+ literal.length >= 4 &&
40
+ SECRET_KEY_PATTERN.test(rawName) &&
41
+ !REFERENCE_VALUE_PATTERN.test(literal.trim())) {
42
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(init.getStart(sourceFile));
43
+ diagnostics.push({
44
+ file: sourceFile.fileName,
45
+ line: line + 1,
46
+ column: character + 1,
47
+ ruleId: "FLY004",
48
+ severity: "warning",
49
+ message: `Possible inline secret under "${rawName}". Move the value to \`secrets\` (apply-only) or use a reference instead of an inline literal.`,
50
+ });
51
+ }
52
+ }
53
+ ts.forEachChild(node, visit);
54
+ }
55
+ visit(sourceFile);
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,52 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * Known Fly.io region codes.
4
+ *
5
+ * Sourced from the representative region list mudflaps serves at
6
+ * `GET /v1/platform/regions` (mudflaps `internal/server/regions.go`), which
7
+ * mirrors fly-go's `GetRegions` wire shape. The generated Fly types (#737)
8
+ * type `region` as a plain `string`, so there is no generated enum to import;
9
+ * this static set is the closest stable list a unit test can check without a
10
+ * live platform. It is representative, not exhaustive — add codes as Fly adds
11
+ * regions.
12
+ */
13
+ const KNOWN_REGIONS = new Set([
14
+ "ams", "atl", "bog", "bos", "cdg", "den", "dfw", "ewr", "fra", "gru",
15
+ "hkg", "iad", "jnb", "lax", "lhr", "mad", "mia", "nrt", "ord", "scl",
16
+ "sea", "sin", "sjc", "syd", "yyz",
17
+ ]);
18
+ /**
19
+ * FLY001: Valid Fly region
20
+ *
21
+ * A machine (or volume/IP) `region` must be a known Fly region code. An unknown
22
+ * region is rejected at apply time; catch it at build with a clearer message.
23
+ */
24
+ export const validRegionRule = {
25
+ id: "FLY001",
26
+ severity: "error",
27
+ category: "correctness",
28
+ description: "A region must be a known Fly region code",
29
+ check(context) {
30
+ const { sourceFile } = context;
31
+ const diagnostics = [];
32
+ function visit(node) {
33
+ if (ts.isPropertyAssignment(node) && node.name.getText(sourceFile) === "region") {
34
+ const init = node.initializer;
35
+ if (ts.isStringLiteral(init) && init.text.length > 0 && !KNOWN_REGIONS.has(init.text)) {
36
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(init.getStart(sourceFile));
37
+ diagnostics.push({
38
+ file: sourceFile.fileName,
39
+ line: line + 1,
40
+ column: character + 1,
41
+ ruleId: "FLY001",
42
+ severity: "error",
43
+ message: `Unknown Fly region "${init.text}". Use a known region code, e.g. iad, lhr, sjc, fra, syd.`,
44
+ });
45
+ }
46
+ }
47
+ ts.forEachChild(node, visit);
48
+ }
49
+ visit(sourceFile);
50
+ return diagnostics;
51
+ },
52
+ };
@@ -0,0 +1,12 @@
1
+ // import { LexiconIndex, lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
2
+ /**
3
+ * Provide LSP completions for fly resources.
4
+ *
5
+ * TODO: Build a LexiconIndex from your generated lexicon data
6
+ * and delegate to lexiconCompletions().
7
+ */
8
+ export function completions(ctx) {
9
+ // const index = new LexiconIndex(lexiconData);
10
+ // return lexiconCompletions(ctx, index, "fly resource");
11
+ return [];
12
+ }
@@ -0,0 +1,12 @@
1
+ // import { LexiconIndex, lexiconHover } from "@intentius/chant/lsp/lexicon-providers";
2
+ /**
3
+ * Provide LSP hover information for fly resources.
4
+ *
5
+ * TODO: Build a LexiconIndex from your generated lexicon data
6
+ * and delegate to lexiconHover().
7
+ */
8
+ export function hover(ctx) {
9
+ // const index = new LexiconIndex(lexiconData);
10
+ // return lexiconHover(ctx, index, myCustomHoverFormatter);
11
+ return undefined;
12
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fly",
3
- "version": "0.124.31",
3
+ "version": "0.124.33",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Fly"
6
6
  }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Read-only MCP tools that expose what `chant build` already computes about a
3
+ * Fly deploy — its apps and machines, the flaps create bodies, image pinning,
4
+ * and the lexicon's findings — so an agent can ask about a change *before it
5
+ * runs* (#804).
6
+ *
7
+ * Every tool builds from source and returns data. None touch a live Fly org,
8
+ * read run history, or write anything — the same context-producer boundary the
9
+ * AWS/GitLab context tools hold.
10
+ */
11
+ import { build } from "@intentius/chant/build";
12
+ import { runPostSynthChecks, getPrimaryOutput } from "@intentius/chant/lint/post-synth";
13
+ import { postSynthChecks } from "../lint/post-synth/index.js";
14
+ import { flySerializer } from "../serializer.js";
15
+ import { parsePlan, isAppRequest, isMachineRequest } from "../op/activities/fly-apply.js";
16
+ const PATH_INPUT = {
17
+ type: "object",
18
+ properties: {
19
+ path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
20
+ },
21
+ };
22
+ /** Build the project and parse the flaps plan the serializer emits. */
23
+ async function buildFly(path) {
24
+ const result = await build(path || ".", [flySerializer]);
25
+ const output = result.outputs.get("fly");
26
+ let plan = {};
27
+ if (output) {
28
+ try {
29
+ plan = parsePlan(getPrimaryOutput(output));
30
+ }
31
+ catch {
32
+ // not a parseable fly plan — leave empty
33
+ }
34
+ }
35
+ return { plan, result, output };
36
+ }
37
+ const machineImage = (req) => {
38
+ const config = req.body.config;
39
+ return typeof config?.image === "string" ? config.image : undefined;
40
+ };
41
+ /** A container image reference is pinned when it names a digest, not a tag. */
42
+ export function imagePinned(image) {
43
+ return image.includes("@sha256:");
44
+ }
45
+ export function flyContextTools() {
46
+ return [
47
+ {
48
+ name: "fly:app",
49
+ description: "Build the project and summarize the declared Fly apps and machines — regions, images, guest sizing — from source. Read-only; never touches a live Fly org.",
50
+ inputSchema: PATH_INPUT,
51
+ async handler(params) {
52
+ const { plan } = await buildFly(String(params.path ?? "."));
53
+ const apps = [];
54
+ const machines = [];
55
+ for (const [name, req] of Object.entries(plan)) {
56
+ if (isAppRequest(req)) {
57
+ apps.push({ name: String(req.body.app_name ?? name), org_slug: req.body.org_slug ?? null });
58
+ }
59
+ else if (isMachineRequest(req)) {
60
+ machines.push({
61
+ name: String(req.body.name ?? name),
62
+ region: req.body.region ?? null,
63
+ image: machineImage(req) ?? null,
64
+ guest: req.body.config?.guest ?? null,
65
+ });
66
+ }
67
+ }
68
+ return { apps, machines };
69
+ },
70
+ },
71
+ {
72
+ name: "fly:checks",
73
+ description: "Build the project and return the fly lexicon's post-synth findings (machine requires an image, a mount references a declared Volume) as JSON. Read-only.",
74
+ inputSchema: PATH_INPUT,
75
+ async handler(params) {
76
+ const { output, result } = await buildFly(String(params.path ?? "."));
77
+ if (!output)
78
+ return { findings: [], note: "no Fly plan produced from this project" };
79
+ const scoped = { ...result, outputs: new Map([["fly", output]]) };
80
+ const diags = runPostSynthChecks(postSynthChecks, scoped);
81
+ return {
82
+ findings: diags.map((d) => ({ id: d.checkId, severity: d.severity, entity: d.entity ?? null, message: d.message })),
83
+ };
84
+ },
85
+ },
86
+ {
87
+ name: "fly:references",
88
+ description: "Build the project and list each machine's container image and whether it is pinned to a digest (vs a floating tag). Read-only.",
89
+ inputSchema: PATH_INPUT,
90
+ async handler(params) {
91
+ const { plan } = await buildFly(String(params.path ?? "."));
92
+ const images = [];
93
+ for (const [name, req] of Object.entries(plan)) {
94
+ if (!isMachineRequest(req))
95
+ continue;
96
+ const image = machineImage(req);
97
+ if (image)
98
+ images.push({ machine: String(req.body.name ?? name), image, pinned: imagePinned(image) });
99
+ }
100
+ return images;
101
+ },
102
+ },
103
+ {
104
+ name: "fly:plan",
105
+ description: "Build the project and return the flaps create bodies the serializer emits — exactly what flyApply would POST to the Machines API. Read-only.",
106
+ inputSchema: PATH_INPUT,
107
+ async handler(params) {
108
+ const { plan } = await buildFly(String(params.path ?? "."));
109
+ return plan;
110
+ },
111
+ },
112
+ ];
113
+ }
@@ -12,7 +12,7 @@ resource_type: Fly::Machines::Machine
12
12
 
13
13
  - `config` (`MachineConfig`, optional): An object defining the Machine configuration
14
14
  - `lease_ttl` (`number`, optional)
15
- - `min_secrets_version` (`number`, optional)
15
+ - `min_secrets_version` (`number`, optional): The minimum version of the app's secrets the Machine must run with. Set it to the `version` returned by the endpoints that set or delete app secrets, so that the Machine is guaranteed to see those changes. This field belongs at the top level of the request body, not in `config`. If omitted, the app's current secrets version is looked up for you.
16
16
  - `name` (`string`, optional): Unique name for this Machine. If omitted, one is generated for you
17
17
  - `region` (`string`, optional): The target region. Omitting this param launches in the same region as your WireGuard peer connection (somewhere near you).
18
18
  - `skip_launch` (`boolean`, optional)
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A box's declared services (#2880), read for the activities that take
3
+ * `box: true`: `spriteServicesObserve`, `spriteServiceRestart` and
4
+ * `spriteApplyServices`.
5
+ *
6
+ * The list is the `services` of the box block of the workspace member whose
7
+ * directory holds the working directory, read with core's declaration reader
8
+ * (`@intentius/chant/workspace/box-services`), so it is checked the way
9
+ * `chant workspace check` checks it. The module is imported when a step
10
+ * runs, since a core older than the field has no such module.
11
+ */
12
+ /** The box block's services for the member whose directory holds `cwd`, in file order. */
13
+ export async function boxServices(cwd = process.cwd()) {
14
+ let mod;
15
+ try {
16
+ mod = await import("@intentius/chant/workspace/box-services");
17
+ }
18
+ catch (err) {
19
+ const why = err instanceof Error ? err.message.split("\n")[0] : String(err);
20
+ throw new Error(`box: true reads the box block through @intentius/chant 0.95.0 or newer, and the chant installed here has no workspace/box-services module (${why})`);
21
+ }
22
+ return mod.readBoxServices(cwd).services.map((s) => ({
23
+ name: s.name,
24
+ cmd: s.cmd,
25
+ needs: [...s.needs],
26
+ httpPort: s.httpPort,
27
+ duration: s.duration,
28
+ health: s.health,
29
+ optional: s.optional,
30
+ }));
31
+ }
32
+ /**
33
+ * A service's command with each `${VAR}` replaced from `env`. Throws naming
34
+ * the first variable `env` does not set, so no service is defined with an
35
+ * empty path in it. Pure.
36
+ */
37
+ export function expandServiceCommand(name, cmd, env = process.env) {
38
+ return cmd.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, v) => {
39
+ const value = env[v];
40
+ if (value === undefined)
41
+ throw new Error(`service ${name}'s cmd names \${${v}}, which is not set in this process's environment`);
42
+ return value;
43
+ });
44
+ }
45
+ /**
46
+ * A service's command as the supervisor runs it: the executable and its
47
+ * arguments. The declared `cmd` is split on whitespace, with no quoting or
48
+ * other shell syntax (a command that needs it belongs in a script), and each
49
+ * word is then expanded by {@link expandServiceCommand}, so a variable whose
50
+ * value has a space stays one word. sprite-env takes the arguments as one
51
+ * comma-separated `--args`, so an argument with a comma in it is refused
52
+ * here rather than split there. Pure.
53
+ */
54
+ export function serviceCommandArgv(name, cmd, env = process.env) {
55
+ const words = cmd.trim().split(/\s+/).filter((w) => w !== "");
56
+ if (words.length === 0)
57
+ throw new Error(`service ${name}'s cmd is empty`);
58
+ const argv = words.map((w) => expandServiceCommand(name, w, env));
59
+ const comma = argv.slice(1).find((a) => a.includes(","));
60
+ if (comma !== undefined) {
61
+ throw new Error(`service ${name}'s cmd has the argument ${JSON.stringify(comma)}, and sprite-env's --args is comma-separated, so it can't carry a comma; run the command from a script`);
62
+ }
63
+ return argv;
64
+ }
65
+ /**
66
+ * The services in an order each one's `needs` come before it, and in file
67
+ * order otherwise. The list is one the declaration reader accepted, so every
68
+ * `needs` is declared and none forms a cycle. Pure.
69
+ */
70
+ export function inStartOrder(services) {
71
+ const byName = new Map(services.map((s) => [s.name, s]));
72
+ const done = new Set();
73
+ const out = [];
74
+ const visit = (s) => {
75
+ if (done.has(s.name))
76
+ return;
77
+ done.add(s.name);
78
+ for (const n of s.needs) {
79
+ const dep = byName.get(n);
80
+ if (dep)
81
+ visit(dep);
82
+ }
83
+ out.push(s);
84
+ };
85
+ for (const s of services)
86
+ visit(s);
87
+ return out;
88
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Single source of truth for the Fly emulator image pins (#808 T2).
3
+ *
4
+ * mudflaps (Machines API) and spritzer (Sprites API) are the local fakes fly's
5
+ * activities are exercised against. These tags were previously duplicated across
6
+ * flaps.ts, sprites-emulator.ts, the fly-deploy composite, and tests — and had
7
+ * already drifted (the composite's docstring said mudflaps 0.3.0 while the
8
+ * activity pinned 0.3.1). Pin each here so a bump touches one line and the
9
+ * emulator-freshness check (#808 T2) has a single target to compare against the
10
+ * latest GHCR release.
11
+ *
12
+ * Bump policy (#808): move these only when a consuming test needs a newer
13
+ * emulator (a fidelity fix the activities exercise), not on every emulator
14
+ * release. For new upstream API surface the emulator leads, then fly bumps here.
15
+ */
16
+ /** Pinned mudflaps (Fly Machines / flaps emulator) image. */
17
+ export const MUDFLAPS_IMAGE = "ghcr.io/intentius/mudflaps:0.4.1";
18
+ /** Pinned spritzer (Fly Sprites emulator) image. */
19
+ export const SPRITZER_IMAGE = "ghcr.io/intentius/spritzer:0.4.1";
20
+ /**
21
+ * Pinned spritzer image for container exec mode (`SPRITZER_EXEC=container`,
22
+ * #2711 / INTENTIUS/spritzer#22) — the Services CRUD activities
23
+ * (`./sprite-services.ts`) and `spriteUrl` only exist in this mode (real
24
+ * processes, a real sprite URL); the default `SPRITZER_IMAGE` above stays on
25
+ * the interpreter-mode pin everything else is tested against. 0.6.0 is the
26
+ * first release with container mode at all, so this can't move to a shared
27
+ * pin until interpreter mode also needs 0.6.1 for something else (#808 bump
28
+ * policy). 0.6.1 fixes `POST /policy/network` to answer 204 with no body, as
29
+ * the real Sprites API and every official SDK expect (#2719,
30
+ * INTENTIUS/spritzer#26/#27) — a fidelity fix the activities exercise.
31
+ */
32
+ export const SPRITZER_CONTAINER_IMAGE = "ghcr.io/intentius/spritzer:0.6.1";
@@ -0,0 +1,34 @@
1
+ import { emulatorLifecycle } from "@intentius/chant/op";
2
+ import { MUDFLAPS_IMAGE } from "./emulator-images.js";
3
+ // mudflaps is a stateful fake of the Fly Machines API (flaps) — a plain 200 on
4
+ // its health endpoint means ready. The local target for flyApply; point it there
5
+ // with FLY_FLAPS_BASE_URL. Shared lifecycle: emulatorLifecycle (#746).
6
+ export const MUDFLAPS_SPEC = {
7
+ name: "chant-mudflaps",
8
+ image: MUDFLAPS_IMAGE,
9
+ containerPort: 4280,
10
+ healthPath: "/_mudflaps/health",
11
+ upstream: { repo: "intentius/mudflaps" },
12
+ };
13
+ /**
14
+ * The Machines half of fly's emulator capability (#1345).
15
+ *
16
+ * fly ships two emulators, and `LexiconPlugin.emulator` held exactly one until
17
+ * #1345 widened it — so neither was declared and `chant emulator` reported fly
18
+ * as having none, while the tutorials booted mudflaps through `chant run`.
19
+ */
20
+ export const MUDFLAPS_EMULATOR = {
21
+ spec: MUDFLAPS_SPEC,
22
+ env: (endpoint) => ({ FLY_FLAPS_BASE_URL: endpoint }),
23
+ };
24
+ const flaps = emulatorLifecycle(MUDFLAPS_SPEC);
25
+ export const flapsExistsCommand = flaps.existsCommand;
26
+ export const flapsRmCommand = flaps.rmCommand;
27
+ export const flapsHealthUrl = flaps.healthUrl;
28
+ /** The flaps endpoint URL (what `flyApply`'s `FLY_FLAPS_BASE_URL`/`endpoint` points at). */
29
+ export const flapsEndpoint = flaps.endpoint;
30
+ export const flapsRunCommand = (args = {}) => flaps.runCommand(args);
31
+ /** Boot a local mudflaps (Fly Machines API emulator) in Docker and return its endpoint. */
32
+ export const flapsUp = (args = {}, signal) => flaps.up(args, signal);
33
+ /** Stop and remove the local mudflaps container (no-op if already gone). */
34
+ export const flapsDown = (args = {}, signal) => flaps.down(args, signal);