@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.
- 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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flaps TypeScript generator.
|
|
3
|
+
*
|
|
4
|
+
* Converts import IR from {@link FlyParser} into typed chant TypeScript:
|
|
5
|
+
* `import { App, Machine, ... } from "@intentius/chant-lexicon-fly";` followed by
|
|
6
|
+
* `export const <var> = new <Class>({ ... });`.
|
|
7
|
+
*
|
|
8
|
+
* fly's nested config types (`MachineConfig`, `MachineGuest`, `MachineService`,
|
|
9
|
+
* ...) are property declarables — classes with an empty instance shape — so a
|
|
10
|
+
* bare object literal would trip TypeScript's excess-property check against the
|
|
11
|
+
* declared parameter type. Wherever a field's declared type is one of those
|
|
12
|
+
* classes, the value is wrapped in `new <Class>({ ... })` (recursively), which is
|
|
13
|
+
* the same authoring surface `chant init` scaffolds and the serializer walks back
|
|
14
|
+
* to the identical flaps body.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* `<OwnerClass> → { <field>: { kind, cls } }` for every field whose declared
|
|
18
|
+
* type is a fly property declarable (a class, an array of one, or a
|
|
19
|
+
* `Record<string, one>`). Mirrors the constructor parameter types in the
|
|
20
|
+
* generated resource classes (src/generated). Regenerate this map if the pinned
|
|
21
|
+
* flaps spec changes the config surface.
|
|
22
|
+
*/
|
|
23
|
+
const NESTED_DECLARABLES = {
|
|
24
|
+
Machine: {
|
|
25
|
+
config: { kind: "single", cls: "MachineConfig" },
|
|
26
|
+
},
|
|
27
|
+
Volume: {
|
|
28
|
+
compute: { kind: "single", cls: "MachineGuest" },
|
|
29
|
+
},
|
|
30
|
+
ContainerConfig: {
|
|
31
|
+
depends_on: { kind: "array", cls: "ContainerDependency" },
|
|
32
|
+
env_from: { kind: "array", cls: "EnvFrom" },
|
|
33
|
+
files: { kind: "array", cls: "File" },
|
|
34
|
+
healthchecks: { kind: "array", cls: "ContainerHealthcheck" },
|
|
35
|
+
restart: { kind: "single", cls: "MachineRestart" },
|
|
36
|
+
secrets: { kind: "array", cls: "MachineSecret" },
|
|
37
|
+
stop: { kind: "single", cls: "StopConfig" },
|
|
38
|
+
},
|
|
39
|
+
ContainerHealthcheck: {
|
|
40
|
+
exec: { kind: "single", cls: "ExecHealthcheck" },
|
|
41
|
+
http: { kind: "single", cls: "HTTPHealthcheck" },
|
|
42
|
+
tcp: { kind: "single", cls: "TCPHealthcheck" },
|
|
43
|
+
},
|
|
44
|
+
DNSConfig: {
|
|
45
|
+
dns_forward_rules: { kind: "array", cls: "DnsForwardRule" },
|
|
46
|
+
options: { kind: "array", cls: "DnsOption" },
|
|
47
|
+
},
|
|
48
|
+
HTTPHealthcheck: {
|
|
49
|
+
headers: { kind: "array", cls: "MachineHTTPHeader" },
|
|
50
|
+
},
|
|
51
|
+
HTTPOptions: {
|
|
52
|
+
replay_cache: { kind: "array", cls: "ReplayCache" },
|
|
53
|
+
response: { kind: "single", cls: "HTTPResponseOptions" },
|
|
54
|
+
},
|
|
55
|
+
MachineCheck: {
|
|
56
|
+
headers: { kind: "array", cls: "MachineHTTPHeader" },
|
|
57
|
+
},
|
|
58
|
+
MachineConfig: {
|
|
59
|
+
cache_drive: { kind: "single", cls: "MachineCacheDrive" },
|
|
60
|
+
checks: { kind: "record", cls: "MachineCheck" },
|
|
61
|
+
containers: { kind: "array", cls: "ContainerConfig" },
|
|
62
|
+
dns: { kind: "single", cls: "DNSConfig" },
|
|
63
|
+
files: { kind: "array", cls: "File" },
|
|
64
|
+
guest: { kind: "single", cls: "MachineGuest" },
|
|
65
|
+
init: { kind: "single", cls: "MachineInit" },
|
|
66
|
+
metrics: { kind: "single", cls: "MachineMetrics" },
|
|
67
|
+
mounts: { kind: "array", cls: "MachineMount" },
|
|
68
|
+
processes: { kind: "array", cls: "MachineProcess" },
|
|
69
|
+
restart: { kind: "single", cls: "MachineRestart" },
|
|
70
|
+
rootfs: { kind: "single", cls: "MachineRootfs" },
|
|
71
|
+
services: { kind: "array", cls: "MachineService" },
|
|
72
|
+
spot: { kind: "single", cls: "MachineSpot" },
|
|
73
|
+
statics: { kind: "array", cls: "Static" },
|
|
74
|
+
stop_config: { kind: "single", cls: "StopConfig" },
|
|
75
|
+
},
|
|
76
|
+
MachinePort: {
|
|
77
|
+
http_options: { kind: "single", cls: "HTTPOptions" },
|
|
78
|
+
proxy_proto_options: { kind: "single", cls: "ProxyProtoOptions" },
|
|
79
|
+
tls_options: { kind: "single", cls: "TLSOptions" },
|
|
80
|
+
},
|
|
81
|
+
MachineProcess: {
|
|
82
|
+
env_from: { kind: "array", cls: "EnvFrom" },
|
|
83
|
+
secrets: { kind: "array", cls: "MachineSecret" },
|
|
84
|
+
},
|
|
85
|
+
MachineService: {
|
|
86
|
+
checks: { kind: "array", cls: "MachineServiceCheck" },
|
|
87
|
+
concurrency: { kind: "single", cls: "MachineServiceConcurrency" },
|
|
88
|
+
ports: { kind: "array", cls: "MachinePort" },
|
|
89
|
+
},
|
|
90
|
+
MachineServiceCheck: {
|
|
91
|
+
headers: { kind: "array", cls: "MachineHTTPHeader" },
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
export class FlyGenerator {
|
|
95
|
+
generate(ir) {
|
|
96
|
+
const imports = new Set();
|
|
97
|
+
const bodyLines = [];
|
|
98
|
+
for (const resource of ir.resources) {
|
|
99
|
+
const parts = resource.type.split("::");
|
|
100
|
+
const className = parts.length >= 3 ? parts[2] : resource.type;
|
|
101
|
+
imports.add(className);
|
|
102
|
+
const varName = camelCase(resource.logicalId);
|
|
103
|
+
const props = formatProps(resource.properties, 0, className, imports);
|
|
104
|
+
bodyLines.push(`export const ${varName} = new ${className}(${props});`);
|
|
105
|
+
bodyLines.push("");
|
|
106
|
+
}
|
|
107
|
+
const lines = [];
|
|
108
|
+
if (imports.size > 0) {
|
|
109
|
+
lines.push(`import { ${[...imports].sort().join(", ")} } from "@intentius/chant-lexicon-fly";`);
|
|
110
|
+
lines.push("");
|
|
111
|
+
}
|
|
112
|
+
lines.push(...bodyLines);
|
|
113
|
+
return [{ path: "main.ts", content: lines.join("\n") + "\n" }];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function camelCase(str) {
|
|
117
|
+
return str
|
|
118
|
+
.replace(/[-_.](.)/g, (_, c) => c.toUpperCase())
|
|
119
|
+
.replace(/^(.)/, (_, c) => c.toLowerCase());
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Format an object literal for the props of `ownerClass`. Fields whose declared
|
|
123
|
+
* type is a nested declarable are wrapped in their constructor; everything else
|
|
124
|
+
* is emitted as a plain literal.
|
|
125
|
+
*/
|
|
126
|
+
function formatProps(props, indent, ownerClass, imports) {
|
|
127
|
+
const entries = Object.entries(props);
|
|
128
|
+
if (entries.length === 0)
|
|
129
|
+
return "{}";
|
|
130
|
+
const pad = " ".repeat(indent + 1);
|
|
131
|
+
const closePad = " ".repeat(indent);
|
|
132
|
+
const nested = NESTED_DECLARABLES[ownerClass];
|
|
133
|
+
const lines = entries.map(([key, value]) => {
|
|
134
|
+
const spec = nested?.[key];
|
|
135
|
+
const rendered = spec !== undefined
|
|
136
|
+
? formatNested(value, indent + 1, spec, imports)
|
|
137
|
+
: formatValue(value, indent + 1);
|
|
138
|
+
return `${pad}${key}: ${rendered},`;
|
|
139
|
+
});
|
|
140
|
+
return `{\n${lines.join("\n")}\n${closePad}}`;
|
|
141
|
+
}
|
|
142
|
+
/** Wrap a nested-declarable value in its constructor per the field's kind. */
|
|
143
|
+
function formatNested(value, indent, spec, imports) {
|
|
144
|
+
const construct = (obj, at) => {
|
|
145
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
146
|
+
return formatValue(obj, at);
|
|
147
|
+
}
|
|
148
|
+
imports.add(spec.cls);
|
|
149
|
+
return `new ${spec.cls}(${formatProps(obj, at, spec.cls, imports)})`;
|
|
150
|
+
};
|
|
151
|
+
if (spec.kind === "single") {
|
|
152
|
+
return construct(value, indent);
|
|
153
|
+
}
|
|
154
|
+
if (spec.kind === "array") {
|
|
155
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
156
|
+
return "[]";
|
|
157
|
+
const pad = " ".repeat(indent + 1);
|
|
158
|
+
const closePad = " ".repeat(indent);
|
|
159
|
+
const items = value.map((v) => `${pad}${construct(v, indent + 1)},`);
|
|
160
|
+
return `[\n${items.join("\n")}\n${closePad}]`;
|
|
161
|
+
}
|
|
162
|
+
// record: a plain object whose values are wrapped in the constructor.
|
|
163
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
164
|
+
return formatValue(value, indent);
|
|
165
|
+
}
|
|
166
|
+
const record = value;
|
|
167
|
+
const recKeys = Object.entries(record);
|
|
168
|
+
if (recKeys.length === 0)
|
|
169
|
+
return "{}";
|
|
170
|
+
const pad = " ".repeat(indent + 1);
|
|
171
|
+
const closePad = " ".repeat(indent);
|
|
172
|
+
const items = recKeys.map(([k, v]) => `${pad}${JSON.stringify(k)}: ${construct(v, indent + 1)},`);
|
|
173
|
+
return `{\n${items.join("\n")}\n${closePad}}`;
|
|
174
|
+
}
|
|
175
|
+
function formatValue(value, indent) {
|
|
176
|
+
if (value === null || value === undefined)
|
|
177
|
+
return "undefined";
|
|
178
|
+
if (typeof value === "string")
|
|
179
|
+
return JSON.stringify(value);
|
|
180
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
181
|
+
return String(value);
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
if (value.length === 0)
|
|
184
|
+
return "[]";
|
|
185
|
+
const pad = " ".repeat(indent + 1);
|
|
186
|
+
const closePad = " ".repeat(indent);
|
|
187
|
+
const items = value.map((v) => `${pad}${formatValue(v, indent + 1)},`);
|
|
188
|
+
return `[\n${items.join("\n")}\n${closePad}]`;
|
|
189
|
+
}
|
|
190
|
+
if (typeof value === "object") {
|
|
191
|
+
return formatPlainObject(value, indent);
|
|
192
|
+
}
|
|
193
|
+
return String(value);
|
|
194
|
+
}
|
|
195
|
+
/** A plain object literal (no constructor wrapping) — for scalar-valued maps like env/metadata. */
|
|
196
|
+
function formatPlainObject(obj, indent) {
|
|
197
|
+
const entries = Object.entries(obj);
|
|
198
|
+
if (entries.length === 0)
|
|
199
|
+
return "{}";
|
|
200
|
+
const pad = " ".repeat(indent + 1);
|
|
201
|
+
const closePad = " ".repeat(indent);
|
|
202
|
+
const lines = entries.map(([key, value]) => `${pad}${objectKey(key)}: ${formatValue(value, indent + 1)},`);
|
|
203
|
+
return `{\n${lines.join("\n")}\n${closePad}}`;
|
|
204
|
+
}
|
|
205
|
+
/** Emit a bare identifier key when it is a valid one, else a quoted key. */
|
|
206
|
+
function objectKey(key) {
|
|
207
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
208
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure export helpers for the fly lexicon — strip live flaps state to the
|
|
3
|
+
* authored shape and build import IR from it. All flaps I/O stays in the caller
|
|
4
|
+
* (`../export-resources`); this module is deterministic and unit-testable.
|
|
5
|
+
*
|
|
6
|
+
* The strip mirrors the read-only field lists the parser drops, so a live
|
|
7
|
+
* machine/app reaches the same declared shape a user would have written. Machine
|
|
8
|
+
* ownership is a per-resource marker (`managed-by: chant` in `config.metadata`,
|
|
9
|
+
* read via `isChantOwned`); the app carries no marker, so under the `owned`
|
|
10
|
+
* filter its ownership is inferred at the app boundary (an app is chant-managed
|
|
11
|
+
* when one of its machines is), and the limitation is logged rather than
|
|
12
|
+
* silently returning everything — the seam contract, matching gcp/aws.
|
|
13
|
+
*
|
|
14
|
+
* `verbatim` is inert for fly (the core contract permits this for targets whose
|
|
15
|
+
* importable surface is already the declared shape): the App/Machine
|
|
16
|
+
* constructors accept only the writable surface, so server-written fields are
|
|
17
|
+
* not constructor parameters and cannot round-trip into the generated code. The
|
|
18
|
+
* option is accepted for signature parity.
|
|
19
|
+
*/
|
|
20
|
+
import { isChantOwned } from "../op/activities/fly-apply.js";
|
|
21
|
+
import { FlyParser, MACHINE_SERVER_FIELDS, APP_SERVER_FIELDS, } from "./parser.js";
|
|
22
|
+
function isRecord(v) {
|
|
23
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
24
|
+
}
|
|
25
|
+
/** Drop server-written read-only machine fields to reach the authored shape. Returns a new object. */
|
|
26
|
+
export function stripMachineServerFields(machine) {
|
|
27
|
+
const clone = JSON.parse(JSON.stringify(machine));
|
|
28
|
+
for (const f of MACHINE_SERVER_FIELDS)
|
|
29
|
+
delete clone[f];
|
|
30
|
+
return clone;
|
|
31
|
+
}
|
|
32
|
+
/** Drop server-written read-only app fields, keeping `name`/`org_slug`. Returns a new object. */
|
|
33
|
+
export function stripAppServerFields(app) {
|
|
34
|
+
const clone = JSON.parse(JSON.stringify(app));
|
|
35
|
+
// Carry the org slug down before dropping the organization object it rides on.
|
|
36
|
+
if (clone.org_slug === undefined && isRecord(clone.organization) && typeof clone.organization.slug === "string") {
|
|
37
|
+
clone.org_slug = clone.organization.slug;
|
|
38
|
+
}
|
|
39
|
+
for (const f of APP_SERVER_FIELDS)
|
|
40
|
+
delete clone[f];
|
|
41
|
+
return clone;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build export IR from one app's live flaps state. Reuses {@link FlyParser} as
|
|
45
|
+
* the single wire→authoring mapping authority by feeding it a cleaned
|
|
46
|
+
* app-with-machines bundle.
|
|
47
|
+
*
|
|
48
|
+
* `opts.owned` keeps only machines carrying the chant marker, and includes the
|
|
49
|
+
* app only when it is chant-managed (a machine is). `opts.verbatim` keeps server
|
|
50
|
+
* fields. Pure: logging is delegated to `opts.onBoundaryInference`, which the
|
|
51
|
+
* I/O caller wires to a single warn.
|
|
52
|
+
*/
|
|
53
|
+
export function buildExportFromApp(app, machines, opts = {}) {
|
|
54
|
+
const appManaged = machines.some((m) => isChantOwned(m.config?.metadata));
|
|
55
|
+
const keptMachines = opts.owned
|
|
56
|
+
? machines.filter((m) => isChantOwned(m.config?.metadata))
|
|
57
|
+
: machines;
|
|
58
|
+
// The app has no per-resource marker; under `owned` it rides the app boundary.
|
|
59
|
+
let keptApp = app;
|
|
60
|
+
if (opts.owned && app) {
|
|
61
|
+
opts.onBoundaryInference?.();
|
|
62
|
+
if (!appManaged)
|
|
63
|
+
keptApp = undefined;
|
|
64
|
+
}
|
|
65
|
+
// The parser maps to the writable surface regardless; the explicit strip here
|
|
66
|
+
// keeps the pure cleaning layer visible and testable. `verbatim` is inert (see
|
|
67
|
+
// module header) — server fields are not App/Machine constructor parameters.
|
|
68
|
+
const cleanMachines = keptMachines.map((m) => stripMachineServerFields(m));
|
|
69
|
+
const cleanApp = keptApp ? stripAppServerFields(keptApp) : keptApp;
|
|
70
|
+
const bundle = { ...(cleanApp ?? {}), machines: cleanMachines };
|
|
71
|
+
const ir = new FlyParser().parse(JSON.stringify(bundle));
|
|
72
|
+
const selector = opts.selector;
|
|
73
|
+
if (!selector || (selector.type === undefined && selector.name === undefined)) {
|
|
74
|
+
return ir;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
...ir,
|
|
78
|
+
resources: ir.resources.filter((r) => (selector.type === undefined || r.type === selector.type) &&
|
|
79
|
+
(selector.name === undefined || r.logicalId === selector.name)),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flaps JSON template parser.
|
|
3
|
+
*
|
|
4
|
+
* Turns Fly Machines API ("flaps") JSON into the import IR for conversion to
|
|
5
|
+
* chant TypeScript. Three input shapes are accepted, all mapped to the authoring
|
|
6
|
+
* surface (the writable fields a user would have declared):
|
|
7
|
+
*
|
|
8
|
+
* 1. The #738 serializer plan — `{ <entity>: { endpoint, method, body } }`.
|
|
9
|
+
* `/v1/apps` bodies become `App`s; `.../machines` bodies become `Machine`s.
|
|
10
|
+
* 2. A machines listing (an array) or an app-with-machines bundle
|
|
11
|
+
* (`{ ...appFields, machines: [...] }`) — an `App` plus its `Machine`s.
|
|
12
|
+
* 3. A single machine (`{ name, region, config }`) or a single app
|
|
13
|
+
* (`{ app_name }` / the `GET /v1/apps/{app}` shape).
|
|
14
|
+
*
|
|
15
|
+
* flaps wire field names are mapped back to the authoring surface: an app's
|
|
16
|
+
* `app_name` becomes the `App`'s `name`, and server-written read-only machine
|
|
17
|
+
* fields (id, state, instance_id, ...) are dropped so the IR is the declared
|
|
18
|
+
* shape. `logicalId` is the app/machine name (the generator camelCases it into a
|
|
19
|
+
* variable).
|
|
20
|
+
*/
|
|
21
|
+
import { BaseValueParser } from "@intentius/chant/import/base-parser";
|
|
22
|
+
import { isSerializerPlan } from "../detect.js";
|
|
23
|
+
export const APP_TYPE = "Fly::Machines::App";
|
|
24
|
+
export const MACHINE_TYPE = "Fly::Machines::Machine";
|
|
25
|
+
/** Server-written read-only machine fields — never part of the authored shape. */
|
|
26
|
+
export const MACHINE_SERVER_FIELDS = [
|
|
27
|
+
"id",
|
|
28
|
+
"state",
|
|
29
|
+
"created_at",
|
|
30
|
+
"updated_at",
|
|
31
|
+
"private_ip",
|
|
32
|
+
"instance_id",
|
|
33
|
+
"nonce",
|
|
34
|
+
"checks",
|
|
35
|
+
"events",
|
|
36
|
+
"host_status",
|
|
37
|
+
"image_ref",
|
|
38
|
+
"incomplete_config",
|
|
39
|
+
];
|
|
40
|
+
/** Server-written read-only app fields — never part of the authored shape. */
|
|
41
|
+
export const APP_SERVER_FIELDS = [
|
|
42
|
+
"id",
|
|
43
|
+
"status",
|
|
44
|
+
"machine_count",
|
|
45
|
+
"volume_count",
|
|
46
|
+
"internal_numeric_id",
|
|
47
|
+
"organization",
|
|
48
|
+
];
|
|
49
|
+
function isRecord(v) {
|
|
50
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
51
|
+
}
|
|
52
|
+
export class FlyParser extends BaseValueParser {
|
|
53
|
+
dispatchIntrinsic(_key, _value, _obj) {
|
|
54
|
+
// flaps JSON has no intrinsic functions.
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
parse(input) {
|
|
58
|
+
const trimmed = input.trim();
|
|
59
|
+
if (!trimmed)
|
|
60
|
+
return { resources: [], parameters: [] };
|
|
61
|
+
const data = JSON.parse(trimmed);
|
|
62
|
+
const resources = [];
|
|
63
|
+
if (isSerializerPlan(data)) {
|
|
64
|
+
for (const [entityName, req] of Object.entries(data)) {
|
|
65
|
+
const r = this.resourceFromRequest(entityName, req);
|
|
66
|
+
if (r)
|
|
67
|
+
resources.push(r);
|
|
68
|
+
}
|
|
69
|
+
return { resources, parameters: [] };
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(data)) {
|
|
72
|
+
for (const m of data) {
|
|
73
|
+
if (isRecord(m))
|
|
74
|
+
resources.push(this.machineResource(m));
|
|
75
|
+
}
|
|
76
|
+
return { resources, parameters: [] };
|
|
77
|
+
}
|
|
78
|
+
if (isRecord(data)) {
|
|
79
|
+
// App-with-machines bundle: top-level app fields plus a `machines` array.
|
|
80
|
+
if (Array.isArray(data.machines)) {
|
|
81
|
+
const { machines, ...appFields } = data;
|
|
82
|
+
if (this.looksLikeApp(appFields))
|
|
83
|
+
resources.push(this.appResource(appFields));
|
|
84
|
+
for (const m of machines) {
|
|
85
|
+
if (isRecord(m))
|
|
86
|
+
resources.push(this.machineResource(m));
|
|
87
|
+
}
|
|
88
|
+
return { resources, parameters: [] };
|
|
89
|
+
}
|
|
90
|
+
// A single machine (has a config) wins over an app read, since a machine
|
|
91
|
+
// GET also carries a `name`.
|
|
92
|
+
if (isRecord(data.config)) {
|
|
93
|
+
resources.push(this.machineResource(data));
|
|
94
|
+
return { resources, parameters: [] };
|
|
95
|
+
}
|
|
96
|
+
if (this.looksLikeApp(data)) {
|
|
97
|
+
resources.push(this.appResource(data));
|
|
98
|
+
return { resources, parameters: [] };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { resources, parameters: [] };
|
|
102
|
+
}
|
|
103
|
+
/** Map one serializer-plan request to a resource by its endpoint. */
|
|
104
|
+
resourceFromRequest(entityName, req) {
|
|
105
|
+
const endpoint = typeof req.endpoint === "string" ? req.endpoint : "";
|
|
106
|
+
const body = isRecord(req.body) ? req.body : {};
|
|
107
|
+
if (/^\/v1\/apps\/?$/.test(endpoint)) {
|
|
108
|
+
return this.appResource(body, entityName);
|
|
109
|
+
}
|
|
110
|
+
if (/^\/v1\/apps\/[^/]+\/machines\/?$/.test(endpoint)) {
|
|
111
|
+
return this.machineResource(body, entityName);
|
|
112
|
+
}
|
|
113
|
+
// Other endpoints (volumes, ip_assignments, certificates, secrets) are not
|
|
114
|
+
// part of the App/Machine import surface.
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
looksLikeApp(obj) {
|
|
118
|
+
return typeof obj.app_name === "string" || typeof obj.name === "string";
|
|
119
|
+
}
|
|
120
|
+
/** An `App` resource. flaps `app_name` (create body) or `name` (GET) → `name`. */
|
|
121
|
+
appResource(obj, entityName) {
|
|
122
|
+
const name = (typeof obj.app_name === "string" && obj.app_name) ||
|
|
123
|
+
(typeof obj.name === "string" && obj.name) ||
|
|
124
|
+
entityName ||
|
|
125
|
+
"app";
|
|
126
|
+
const properties = { name };
|
|
127
|
+
// org_slug either sits on the create body directly, or is carried on the GET
|
|
128
|
+
// shape under `organization.slug`.
|
|
129
|
+
if (obj.org_slug !== undefined) {
|
|
130
|
+
properties.org_slug = this.parseValue(obj.org_slug);
|
|
131
|
+
}
|
|
132
|
+
else if (isRecord(obj.organization) && typeof obj.organization.slug === "string") {
|
|
133
|
+
properties.org_slug = obj.organization.slug;
|
|
134
|
+
}
|
|
135
|
+
if (obj.network !== undefined)
|
|
136
|
+
properties.network = this.parseValue(obj.network);
|
|
137
|
+
if (obj.enable_subdomains !== undefined) {
|
|
138
|
+
properties.enable_subdomains = this.parseValue(obj.enable_subdomains);
|
|
139
|
+
}
|
|
140
|
+
return { logicalId: name, type: APP_TYPE, properties };
|
|
141
|
+
}
|
|
142
|
+
/** A `Machine` resource, stripped of server-written read-only fields. */
|
|
143
|
+
machineResource(obj, entityName) {
|
|
144
|
+
const name = (typeof obj.name === "string" && obj.name) || entityName || "machine";
|
|
145
|
+
const properties = {};
|
|
146
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
147
|
+
if (MACHINE_SERVER_FIELDS.includes(key))
|
|
148
|
+
continue;
|
|
149
|
+
if (value === undefined)
|
|
150
|
+
continue;
|
|
151
|
+
properties[key] = this.parseValue(value);
|
|
152
|
+
}
|
|
153
|
+
// Always carry a name so the round-trip keeps the machine's identity.
|
|
154
|
+
properties.name = name;
|
|
155
|
+
return { logicalId: name, type: MACHINE_TYPE, properties };
|
|
156
|
+
}
|
|
157
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Plugin
|
|
2
|
+
export { flyPlugin } from "./plugin.js";
|
|
3
|
+
// Component/release capabilities — run-agent, the sprite-lifecycle leaf
|
|
4
|
+
// contributed to core's capability-plugin seam (#1942). Core loads
|
|
5
|
+
// `flyCapabilityPlugin` when a project's chant.config lists this lexicon.
|
|
6
|
+
export { flyCapabilityPlugin, FLY_VERB_FAMILIES } from "./components/capability-plugin.js";
|
|
7
|
+
// Serializer
|
|
8
|
+
export { flySerializer } from "./serializer.js";
|
|
9
|
+
// Pseudo-parameters — environment-resolved values (`Fly.Region`, `Fly.OrgSlug`,
|
|
10
|
+
// `Fly.AppName`) usable in place of hard-coded strings.
|
|
11
|
+
export { Fly, Region, OrgSlug, AppName, PseudoParameter } from "./pseudo.js";
|
|
12
|
+
// Ownership marker convention (machine config.metadata keys)
|
|
13
|
+
export { FLY_METADATA_OWNERSHIP_KEYS } from "./ownership.js";
|
|
14
|
+
// The release a Machine serves, in its metadata (#2736): read by
|
|
15
|
+
// describeResources and compared with the release ledger by
|
|
16
|
+
// `chant components status --live`.
|
|
17
|
+
export { RELEASE_METADATA_KEYS, readMachineRelease, withReleaseMetadata } from "./release-metadata.js";
|
|
18
|
+
// Deploy Op composite + typed step builders (#744). `flyDeploy` returns a
|
|
19
|
+
// `boot → build → flyApply → wait → teardown` Op; the step builders wrap the
|
|
20
|
+
// generic `activity()` so the fly activities resolve by name without a core change.
|
|
21
|
+
export { flyDeploy, flapsUp, flapsDown, flyApplyStep, LOCAL_FLAPS_ENDPOINT } from "./composites/fly-deploy.js";
|
|
22
|
+
// OpenTelemetry Collector on a Machine, its config declared with the otel lexicon (#2613).
|
|
23
|
+
export { FlyOtelCollector } from "./composites/fly-otel-collector.js";
|
|
24
|
+
// A Fly app that serves one app's releases: App, Machine, and its Volume, IP and Secrets (#2809, ws-056).
|
|
25
|
+
export { FlySite } from "./composites/fly-site.js";
|
|
26
|
+
// Sprite Op step builders. chant #1288 Stage 2: these author
|
|
27
|
+
// `activity("spriteCreate", ...)` steps with authoring-time types derived
|
|
28
|
+
// from this lexicon's own `Sprite*Args` interfaces (`./op/builders.ts`),
|
|
29
|
+
// replacing the hand-restated inline types core's same-named builders used
|
|
30
|
+
// to carry — same names, same import path, so an existing
|
|
31
|
+
// `import { spriteCreate } from "@intentius/chant-lexicon-fly"` call site
|
|
32
|
+
// gains real derived types with no change. `loadActivities(["fly"])` binds
|
|
33
|
+
// the `fn` strings to the implementations in `./op/activities/sprites.ts`
|
|
34
|
+
// etc. The `spritesUp`/`spritesDown` builders boot/tear down the spritzer
|
|
35
|
+
// emulator as modeled steps.
|
|
36
|
+
export { spriteCreate, spriteExec, spriteCheckpoint, spriteRestore, listCheckpoints, spriteDestroy, spriteDelete, spriteUrl, spriteWriteFile, spriteReadFile, spriteListDir, spriteRemove, spriteApplyNetworkPolicy, spriteApplyServices, spriteServiceCreate, spriteServiceGet, spriteServiceList, spriteServiceStart, spriteServiceStop, spriteServiceDelete, spriteServiceLogs, spriteServicesObserve, spriteServiceRestart, spriteTaskCreate, spriteTaskRefresh, spriteTaskRelease, spritesUp, spritesDown, flyMachineRelease, flyMachineExec, flyMachineRestart, flyMachineStop, flyMachineVerify, flyMachineRestore, flyRelease, flyRollback, } from "./op/builders.js";
|
|
37
|
+
// Generated resources — export everything from generated index.
|
|
38
|
+
// Provides `App`, `Machine`, `Volume`, and the property types
|
|
39
|
+
// (`MachineConfig`, `MachineGuest`, `MachineService`, ...) for authoring.
|
|
40
|
+
export * from "./generated/index.js";
|
package/dist/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "4c0b65513cadb008353bf569e94010aabbd988d57482ded015f6e0cce8526054",
|
|
5
5
|
"meta.json": "273f2a523b66f08ca91642dba2592c44765ab59ddf7329998884a0b08c6c3f24",
|
|
6
|
-
"types/index.d.ts": "
|
|
6
|
+
"types/index.d.ts": "ca9b4c777e58e7ce2132f5f024e7a1b3d3f2ba4305d935aab85f6523469aed1e",
|
|
7
7
|
"rules/guest-sizing.ts": "8dd2db79a47ceab3d488f84f560aed1b1e4a66b4e36bf11f3baf6a8544d6e126",
|
|
8
8
|
"rules/no-secret-literals.ts": "3854307bd0e8d7a1bf6c23051b460b90f3212baa8bb2f7d4324d0e4418a69982",
|
|
9
9
|
"rules/valid-region.ts": "238afb252ffb59ef131d070a705a49a1ac87b98f6a86c029a69dc82200e1aabf",
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
"skills/chant-fly-ops.md": "44102f1fdf611b9262c04457258990e44026e83f9e5b19ae86b69322f3a37620",
|
|
16
16
|
"skills/chant-fly-sprites.md": "3d100680422151b58d082c3a7b30a286edfc52faa4887995c9ad5553873bab95"
|
|
17
17
|
},
|
|
18
|
-
"composite": "
|
|
18
|
+
"composite": "bbbdd32cead5547f480ccd70200d0dfca6541e2643baddd2f237400b6bc0ab22"
|
|
19
19
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fly lexicon's chant audit catalog — metadata for the FLY post-synth
|
|
3
|
+
* checks, contributed via `flyPlugin.auditCatalog()` (#687).
|
|
4
|
+
*
|
|
5
|
+
* Both entries carry `yamlBased: false`: each check reads the chant model
|
|
6
|
+
* (`ctx.entities`) rather than the emitted manifest, because what they need —
|
|
7
|
+
* whether a mount's volume is declared in the same stack — lives in the typed
|
|
8
|
+
* graph and is gone by the time `fly.toml` exists. So they fire on
|
|
9
|
+
* `chant build` and cannot fire on an audit of standalone Fly config.
|
|
10
|
+
* `auditRule()` hardcodes `yamlBased: true`, so these are constructed directly
|
|
11
|
+
* (the same reasoning as fountain's catalog).
|
|
12
|
+
*
|
|
13
|
+
* Without this the two checks reached `chant audit` with no title, tier, fix
|
|
14
|
+
* kind, or category at all — `resolveAuditCatalog` contributes nothing for a
|
|
15
|
+
* lexicon that omits the method, silently (#1346).
|
|
16
|
+
*/
|
|
17
|
+
import { flyAuditLineage } from "./audit-lineage.js";
|
|
18
|
+
import { applyLineage } from "@intentius/chant/audit/catalog";
|
|
19
|
+
/** Entity-based rule: everything fly ships. */
|
|
20
|
+
function rule(id, tier, category, title, remediation) {
|
|
21
|
+
return { id, tier, fixKind: "guidance", category, title, remediation, yamlBased: false };
|
|
22
|
+
}
|
|
23
|
+
export const flyAuditCatalog = {
|
|
24
|
+
FLY010: rule("FLY010", "merge-worthy", "correctness", "Machine config has no image", "Set `config.image` on the Machine — a Machine cannot boot without one."),
|
|
25
|
+
FLY011: rule("FLY011", "merge-worthy", "correctness", "Machine mount references an undeclared volume", "Declare the Volume in the same stack, or point the mount at one that is."),
|
|
26
|
+
};
|
|
27
|
+
// Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
|
|
28
|
+
applyLineage(flyAuditCatalog, flyAuditLineage);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the fly post-synth checks. Not a check itself, so the
|
|
3
|
+
* generated barrel skips it (the scanner only picks up exported PostSynthChecks).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Read an entity or property's constructor props. Nested Declarables
|
|
7
|
+
* (MachineConfig, MachineMount, Volume) stash their args under a non-enumerable
|
|
8
|
+
* `props`; a plain inline object carries them directly. Handle both so a check
|
|
9
|
+
* behaves the same whether the user authored `config: { image }` or
|
|
10
|
+
* `config: new MachineConfig({ image })`.
|
|
11
|
+
*/
|
|
12
|
+
export function readProps(value) {
|
|
13
|
+
if (value && typeof value === "object") {
|
|
14
|
+
const nested = value.props;
|
|
15
|
+
if (nested && typeof nested === "object")
|
|
16
|
+
return nested;
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
/** The entityType of a declarable, or undefined. */
|
|
22
|
+
export function entityTypeOf(value) {
|
|
23
|
+
return value?.entityType;
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FLY010: Machine config must specify an image.
|
|
3
|
+
*
|
|
4
|
+
* A Machine cannot boot without a container image. The Machines API rejects a
|
|
5
|
+
* create request whose config omits `image`, so catch it at synth time with a
|
|
6
|
+
* clearer message.
|
|
7
|
+
*/
|
|
8
|
+
import { readProps, entityTypeOf } from "./fly-helpers.js";
|
|
9
|
+
export const fly010 = {
|
|
10
|
+
id: "FLY010",
|
|
11
|
+
description: "Machine config must specify an image — a Machine cannot boot without one",
|
|
12
|
+
check(ctx) {
|
|
13
|
+
const diagnostics = [];
|
|
14
|
+
for (const [name, entity] of ctx.entities) {
|
|
15
|
+
if (entityTypeOf(entity) !== "Fly::Machines::Machine")
|
|
16
|
+
continue;
|
|
17
|
+
const config = readProps(entity).config;
|
|
18
|
+
if (!config)
|
|
19
|
+
continue; // no config authored — a different concern
|
|
20
|
+
const image = readProps(config).image;
|
|
21
|
+
if (typeof image !== "string" || image.length === 0) {
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
checkId: "FLY010",
|
|
24
|
+
severity: "error",
|
|
25
|
+
message: `Machine "${name}" config has no image — set config.image, e.g. image: "flyio/fastify-functions"`,
|
|
26
|
+
entity: name,
|
|
27
|
+
lexicon: "fly",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return diagnostics;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FLY011: A machine mount must reference a declared volume.
|
|
3
|
+
*
|
|
4
|
+
* Every machine `config.mounts[].volume` must resolve to a `Volume` declared in
|
|
5
|
+
* the stack. A mount pointing at a volume that does not exist is rejected at
|
|
6
|
+
* apply time, so catch it at synth. This is a whole-stack check: it reads all
|
|
7
|
+
* entities in the build (`ctx.entities`), so the mount and the Volume can be
|
|
8
|
+
* declared in different files.
|
|
9
|
+
*/
|
|
10
|
+
import { readProps, entityTypeOf } from "./fly-helpers.js";
|
|
11
|
+
const MACHINE_ENTITY_TYPE = "Fly::Machines::Machine";
|
|
12
|
+
const VOLUME_ENTITY_TYPE = "Fly::Machines::Volume";
|
|
13
|
+
export const fly011 = {
|
|
14
|
+
id: "FLY011",
|
|
15
|
+
description: "A machine mount's volume must resolve to a Volume declared in the stack",
|
|
16
|
+
check(ctx) {
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
// A mount can name a declared volume by the Volume's `name` or by its
|
|
19
|
+
// logical (declaration) name — collect both.
|
|
20
|
+
const declaredVolumes = new Set();
|
|
21
|
+
for (const [logicalName, entity] of ctx.entities) {
|
|
22
|
+
if (entityTypeOf(entity) !== VOLUME_ENTITY_TYPE)
|
|
23
|
+
continue;
|
|
24
|
+
declaredVolumes.add(logicalName);
|
|
25
|
+
const name = readProps(entity).name;
|
|
26
|
+
if (typeof name === "string" && name.length > 0)
|
|
27
|
+
declaredVolumes.add(name);
|
|
28
|
+
}
|
|
29
|
+
for (const [machineName, entity] of ctx.entities) {
|
|
30
|
+
if (entityTypeOf(entity) !== MACHINE_ENTITY_TYPE)
|
|
31
|
+
continue;
|
|
32
|
+
const config = readProps(entity).config;
|
|
33
|
+
if (!config)
|
|
34
|
+
continue;
|
|
35
|
+
const mounts = readProps(config).mounts;
|
|
36
|
+
if (!Array.isArray(mounts))
|
|
37
|
+
continue;
|
|
38
|
+
for (const mount of mounts) {
|
|
39
|
+
const volume = readProps(mount).volume;
|
|
40
|
+
// Only a string names a volume by value. A Declarable/AttrRef is a live
|
|
41
|
+
// reference to a Volume that exists in the stack by construction, so it
|
|
42
|
+
// never dangles — skip it to avoid false positives.
|
|
43
|
+
if (typeof volume !== "string" || volume.length === 0)
|
|
44
|
+
continue;
|
|
45
|
+
if (!declaredVolumes.has(volume)) {
|
|
46
|
+
diagnostics.push({
|
|
47
|
+
checkId: "FLY011",
|
|
48
|
+
severity: "error",
|
|
49
|
+
message: `Machine "${machineName}" mounts volume "${volume}", which is not declared as a Volume in the stack.`,
|
|
50
|
+
entity: machineName,
|
|
51
|
+
lexicon: "fly",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return diagnostics;
|
|
57
|
+
},
|
|
58
|
+
};
|