@intentius/chant-lexicon-fly 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +91 -0
  2. package/dist/codegen/docs-cli.d.ts +3 -0
  3. package/dist/codegen/docs-cli.d.ts.map +1 -0
  4. package/dist/codegen/docs.d.ts +27 -0
  5. package/dist/codegen/docs.d.ts.map +1 -0
  6. package/dist/codegen/generate-cli.d.ts +6 -0
  7. package/dist/codegen/generate-cli.d.ts.map +1 -0
  8. package/dist/codegen/generate-lexicon.d.ts +18 -0
  9. package/dist/codegen/generate-lexicon.d.ts.map +1 -0
  10. package/dist/codegen/generate-typescript.d.ts +11 -0
  11. package/dist/codegen/generate-typescript.d.ts.map +1 -0
  12. package/dist/codegen/generate.d.ts +15 -0
  13. package/dist/codegen/generate.d.ts.map +1 -0
  14. package/dist/codegen/naming.d.ts +14 -0
  15. package/dist/codegen/naming.d.ts.map +1 -0
  16. package/dist/codegen/package.d.ts +17 -0
  17. package/dist/codegen/package.d.ts.map +1 -0
  18. package/dist/composites/fly-deploy.d.ts +110 -0
  19. package/dist/composites/fly-deploy.d.ts.map +1 -0
  20. package/dist/describe-resources.d.ts +74 -0
  21. package/dist/describe-resources.d.ts.map +1 -0
  22. package/dist/detect.d.ts +22 -0
  23. package/dist/detect.d.ts.map +1 -0
  24. package/dist/export-resources.d.ts +25 -0
  25. package/dist/export-resources.d.ts.map +1 -0
  26. package/dist/generated/index.d.ts +42 -0
  27. package/dist/generated/index.d.ts.map +1 -0
  28. package/dist/generated/runtime.d.ts +5 -0
  29. package/dist/generated/runtime.d.ts.map +1 -0
  30. package/dist/import/generator.d.ts +21 -0
  31. package/dist/import/generator.d.ts.map +1 -0
  32. package/dist/import/live-export.d.ts +42 -0
  33. package/dist/import/live-export.d.ts.map +1 -0
  34. package/dist/import/parser.d.ts +40 -0
  35. package/dist/import/parser.d.ts.map +1 -0
  36. package/dist/index.d.ts +8 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/integrity.json +9 -0
  39. package/dist/lint/post-synth/fly-helpers.d.ts +15 -0
  40. package/dist/lint/post-synth/fly-helpers.d.ts.map +1 -0
  41. package/dist/lint/post-synth/fly010-machine-requires-image.d.ts +10 -0
  42. package/dist/lint/post-synth/fly010-machine-requires-image.d.ts.map +1 -0
  43. package/dist/lint/post-synth/fly011-mount-references-declared-volume.d.ts +12 -0
  44. package/dist/lint/post-synth/fly011-mount-references-declared-volume.d.ts.map +1 -0
  45. package/dist/lint/post-synth/index.d.ts +3 -0
  46. package/dist/lint/post-synth/index.d.ts.map +1 -0
  47. package/dist/lint/rules/guest-sizing.d.ts +9 -0
  48. package/dist/lint/rules/guest-sizing.d.ts.map +1 -0
  49. package/dist/lint/rules/index.d.ts +7 -0
  50. package/dist/lint/rules/index.d.ts.map +1 -0
  51. package/dist/lint/rules/no-secret-literals.d.ts +13 -0
  52. package/dist/lint/rules/no-secret-literals.d.ts.map +1 -0
  53. package/dist/lint/rules/valid-region.d.ts +9 -0
  54. package/dist/lint/rules/valid-region.d.ts.map +1 -0
  55. package/dist/lsp/completions.d.ts +9 -0
  56. package/dist/lsp/completions.d.ts.map +1 -0
  57. package/dist/lsp/hover.d.ts +9 -0
  58. package/dist/lsp/hover.d.ts.map +1 -0
  59. package/dist/manifest.json +4 -0
  60. package/dist/meta.json +285 -0
  61. package/dist/op/activities/flaps.d.ts +29 -0
  62. package/dist/op/activities/flaps.d.ts.map +1 -0
  63. package/dist/op/activities/fly-apply.d.ts +382 -0
  64. package/dist/op/activities/fly-apply.d.ts.map +1 -0
  65. package/dist/op/activities/index.d.ts +12 -0
  66. package/dist/op/activities/index.d.ts.map +1 -0
  67. package/dist/ownership.d.ts +27 -0
  68. package/dist/ownership.d.ts.map +1 -0
  69. package/dist/package-cli.d.ts +3 -0
  70. package/dist/package-cli.d.ts.map +1 -0
  71. package/dist/plugin.d.ts +8 -0
  72. package/dist/plugin.d.ts.map +1 -0
  73. package/dist/pseudo.d.ts +23 -0
  74. package/dist/pseudo.d.ts.map +1 -0
  75. package/dist/serializer.d.ts +36 -0
  76. package/dist/serializer.d.ts.map +1 -0
  77. package/dist/spec/fetch.d.ts +12 -0
  78. package/dist/spec/fetch.d.ts.map +1 -0
  79. package/dist/spec/parse.d.ts +58 -0
  80. package/dist/spec/parse.d.ts.map +1 -0
  81. package/dist/types/index.d.ts +590 -0
  82. package/dist/validate-cli.d.ts +3 -0
  83. package/dist/validate-cli.d.ts.map +1 -0
  84. package/dist/validate.d.ts +15 -0
  85. package/dist/validate.d.ts.map +1 -0
  86. package/package.json +76 -0
  87. package/src/codegen/docs-cli.ts +7 -0
  88. package/src/codegen/docs.ts +208 -0
  89. package/src/codegen/generate-cli.ts +36 -0
  90. package/src/codegen/generate-lexicon.ts +52 -0
  91. package/src/codegen/generate-typescript.ts +69 -0
  92. package/src/codegen/generate.ts +106 -0
  93. package/src/codegen/naming.ts +45 -0
  94. package/src/codegen/package.ts +25 -0
  95. package/src/composites/fly-deploy.ts +169 -0
  96. package/src/describe-resources.test.ts +188 -0
  97. package/src/describe-resources.ts +0 -0
  98. package/src/detect.ts +64 -0
  99. package/src/export-resources.ts +123 -0
  100. package/src/generated/.gitkeep +0 -0
  101. package/src/generated/index.d.ts +590 -0
  102. package/src/generated/index.ts +47 -0
  103. package/src/generated/lexicon-fly.json +285 -0
  104. package/src/generated/runtime.ts +4 -0
  105. package/src/import/generator.test.ts +123 -0
  106. package/src/import/generator.ts +242 -0
  107. package/src/import/live-export.test.ts +150 -0
  108. package/src/import/live-export.ts +107 -0
  109. package/src/import/parser.test.ts +132 -0
  110. package/src/import/parser.ts +185 -0
  111. package/src/import/roundtrip.test.ts +77 -0
  112. package/src/index.ts +23 -0
  113. package/src/lint/post-synth/fly-helpers.ts +25 -0
  114. package/src/lint/post-synth/fly010-machine-requires-image.test.ts +59 -0
  115. package/src/lint/post-synth/fly010-machine-requires-image.ts +39 -0
  116. package/src/lint/post-synth/fly011-mount-references-declared-volume.test.ts +92 -0
  117. package/src/lint/post-synth/fly011-mount-references-declared-volume.ts +61 -0
  118. package/src/lint/post-synth/index.ts +9 -0
  119. package/src/lint/rules/guest-sizing.ts +100 -0
  120. package/src/lint/rules/index.ts +15 -0
  121. package/src/lint/rules/no-secret-literals.ts +67 -0
  122. package/src/lint/rules/rules.test.ts +127 -0
  123. package/src/lint/rules/valid-region.ts +58 -0
  124. package/src/lsp/completions.test.ts +10 -0
  125. package/src/lsp/completions.ts +14 -0
  126. package/src/lsp/hover.test.ts +10 -0
  127. package/src/lsp/hover.ts +14 -0
  128. package/src/op/activities/describe-resources.integration.test.ts +105 -0
  129. package/src/op/activities/flaps.test.ts +29 -0
  130. package/src/op/activities/flaps.ts +44 -0
  131. package/src/op/activities/fly-apply.integration.test.ts +245 -0
  132. package/src/op/activities/fly-apply.test.ts +513 -0
  133. package/src/op/activities/fly-apply.ts +1014 -0
  134. package/src/op/activities/index.ts +48 -0
  135. package/src/ownership.ts +32 -0
  136. package/src/package-cli.ts +24 -0
  137. package/src/plugin.test.ts +17 -0
  138. package/src/plugin.ts +285 -0
  139. package/src/pseudo.test.ts +32 -0
  140. package/src/pseudo.ts +29 -0
  141. package/src/serializer.test.ts +232 -0
  142. package/src/serializer.ts +293 -0
  143. package/src/skills/chant-fly-ops.md +55 -0
  144. package/src/skills/chant-fly-patterns.md +71 -0
  145. package/src/skills/chant-fly.md +123 -0
  146. package/src/spec/fetch.ts +34 -0
  147. package/src/spec/parse.ts +323 -0
  148. package/src/validate-cli.ts +5 -0
  149. package/src/validate.ts +28 -0
@@ -0,0 +1,123 @@
1
+ ---
2
+ skill: chant-fly
3
+ description: Author, lint, and deploy Fly apps and machines from a chant project, applied straight to the Machines API
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Deploy to Fly Operational Playbook
8
+
9
+ ## How chant and Fly relate
10
+
11
+ chant is a synthesis compiler: it compiles TypeScript in `src/` into a plan of Fly Machines API ("flaps") create requests, then reconciles that plan against a Fly org. Unlike the AWS or GCP lexicons, there is no external CLI to hand off to. `flyApply` speaks the Machines API directly, so the same code that builds the plan also applies it. There is no `flyctl` shell-out and no state file to store, lock, or keep in sync.
12
+
13
+ The source of truth is the TypeScript in `src/`. The serialized plan (a JSON object keyed by entity name, each value a `{ endpoint, method, body }` flaps request) is an intermediate artifact.
14
+
15
+ Your job as an agent:
16
+
17
+ - Use `chant build` for synthesis and lint (region, guest sizing, mounts, secret literals).
18
+ - Use `flyApply` (via the deploy Op, `chant run`) to reconcile the plan against the Machines API: create and update machines, wait each to `started`, and optionally prune what chant owns.
19
+
20
+ ## The endpoint switch
21
+
22
+ One environment variable decides where the same code applies:
23
+
24
+ - `FLY_FLAPS_BASE_URL` unset, no token: point it at a local [mudflaps](https://github.com/intentius/mudflaps) emulator (offline, no Fly account, no bill). This is the loop CI runs.
25
+ - `FLY_FLAPS_BASE_URL` set to a real Fly org endpoint, plus `FLY_API_TOKEN`: the same plan deploys for real.
26
+
27
+ Resolution order for the endpoint is: an explicit `endpoint` arg, then `FLY_FLAPS_BASE_URL`, then the real-Fly default (`https://api.machines.dev`). The bearer token defaults to `FLY_API_TOKEN`; mudflaps ignores it.
28
+
29
+ Start from the runnable [`examples/local-fly`](../../examples/local-fly) loop:
30
+
31
+ ```bash
32
+ cd examples/local-fly
33
+ chant run fly # boots mudflaps, applies an App + Machine, waits for started, tears down
34
+ ```
35
+
36
+ That Op runs the phases boot, build, apply, verify, and teardown against a local mudflaps container (Docker required). To target a real org, drop the local endpoint override and set `FLY_API_TOKEN`.
37
+
38
+ ## Author an App and a Machine
39
+
40
+ Import resource types from `@intentius/chant-lexicon-fly`. They are generated from Fly's Machines API OpenAPI spec, so `MachineConfig` is typed all the way down through guest, services, mounts, and checks.
41
+
42
+ ```ts
43
+ import { App, Machine, MachineConfig, MachineGuest } from "@intentius/chant-lexicon-fly";
44
+
45
+ export const app = new App({ name: "my-app" });
46
+
47
+ export const web = new Machine({
48
+ name: "web",
49
+ region: "iad",
50
+ config: new MachineConfig({
51
+ image: "flyio/hellofly:latest",
52
+ guest: new MachineGuest({ cpu_kind: "shared", cpus: 1, memory_mb: 256 }),
53
+ }),
54
+ });
55
+ ```
56
+
57
+ A machine that names no app is bound to the stack's sole app at apply time. You do not stamp the ownership marker yourself: the serializer writes `managed-by: chant` into each machine's `config.metadata`, and the owned-only prune reads it back.
58
+
59
+ The full resource set is `App`, `Machine`, `Volume`, `IPAddress`, `Certificate`, and `Secret`. Volumes, mounts, IPs, certificates, and apply-only secrets are covered in `chant-fly-patterns`.
60
+
61
+ ## Build and lint
62
+
63
+ ```bash
64
+ chant build src/
65
+ ```
66
+
67
+ Build synthesizes the flaps plan and runs the lint rules before anything reaches the API:
68
+
69
+ | Rule | Catches |
70
+ |------|---------|
71
+ | FLY001 | `region` is not a real Fly region |
72
+ | FLY002 | Guest sizing (`cpu_kind` / `cpus` / `memory_mb`) is not a valid combination |
73
+ | FLY004 | A secret value written inline in machine config |
74
+ | FLY010 | A machine config with no `image` |
75
+ | FLY011 | A machine mount that references a `Volume` not declared in the stack (checked across files) |
76
+
77
+ Fix every reported violation before applying. Secret values belong in a `Secret` or a reference, never inline (FLY004).
78
+
79
+ ## Apply with flyApply
80
+
81
+ `flyApply` reads the serialized plan and applies it to flaps in dependency order: app, then volumes, then machines, then IPs, certificates, and secrets. Per machine it does a GET-then-create or update, then waits.
82
+
83
+ - Create or update: POST the machine, then poll `GET .../wait` until it reaches `started` at its new `instance_id`. flaps caps its own long-poll at 60 seconds and answers 408 on expiry, so the client re-polls until its deadline (default 300 seconds).
84
+ - No-op on no drift: a re-apply of an unchanged machine (config structurally equal to live) does nothing.
85
+ - Leases: mutating an existing machine goes through the Machines API lease protocol. `flyApply` acquires a lease, echoes the nonce in the `fly-machine-lease-nonce` header on the mutation, and re-acquires and retries once if the lease was lost. Concurrent operators stay out of each other's way.
86
+
87
+ ### Owned-only prune
88
+
89
+ Prune is off by default and destructive; turn it on to remove declared-then-removed resources.
90
+
91
+ - Machines prune owned-only: a machine is destroyed only if it carries the `managed-by: chant` marker and the plan no longer declares it. An unmarked (foreign) machine in the same app is never modified or deleted, so the applier is safe to point at an app that also holds resources you manage elsewhere.
92
+ - Volumes, IPs, certificates, and secrets have no metadata channel, so their ownership boundary is the app itself. See `chant-fly-patterns` for that app-boundary model before enabling prune on an app that mixes chant and non-chant resources.
93
+
94
+ ## The deploy Op
95
+
96
+ The lexicon ships `flyDeploy`, a composite Op that wraps the boot, build, apply, verify, and teardown phases so `chant run` drives the whole loop as modeled activities with no raw shell.
97
+
98
+ ```ts
99
+ // examples/local-fly/ops/fly.op.ts
100
+ import { flyDeploy } from "@intentius/chant-lexicon-fly";
101
+
102
+ export default flyDeploy({ app: "local-fly-demo" });
103
+ ```
104
+
105
+ `chant run fly` boots mudflaps, builds the plan, applies the App and Machine, waits for the machine to reach `started`, and tears the emulator down. To deploy the same Op to a real org, drop the local endpoint override and set `FLY_API_TOKEN`.
106
+
107
+ ## Teardown
108
+
109
+ `flyDelete` is the inverse of `flyApply`: it destroys the machines the plan declares (dependents first), then deletes the apps. It is idempotent, so an already-absent resource is a no-op.
110
+
111
+ ## Quick reference
112
+
113
+ | Command | Description |
114
+ |---------|-------------|
115
+ | `chant build src/` | Synthesize the flaps plan and run lint (FLY001/FLY002/FLY004/FLY010/FLY011) |
116
+ | `chant run fly` | Run the deploy Op (boot, build, apply, verify, teardown) |
117
+ | `FLY_FLAPS_BASE_URL=...` | Point the same code at mudflaps or a real Fly org |
118
+ | `FLY_API_TOKEN=...` | Bearer token for a real Fly org (mudflaps ignores it) |
119
+
120
+ ## Where to go next
121
+
122
+ - `chant-fly-patterns` covers volumes and mounts, IP assignments, certificates, apply-only secrets, and the app-boundary ownership model.
123
+ - `chant-fly-ops` covers operating a live app: waiting on stuck machines, lease conflicts, prune safety, and targeting a real org versus the emulator.
@@ -0,0 +1,34 @@
1
+ import { join } from "path";
2
+ import { homedir } from "os";
3
+ import { fetchWithCache } from "@intentius/chant/codegen/fetch";
4
+
5
+ /**
6
+ * Fly's published Machines API (flaps) OpenAPI 3.0.1 spec.
7
+ *
8
+ * This is an unversioned live endpoint: it always serves the current spec with
9
+ * no version or release tag to pin. There is nothing for the self-upgrade
10
+ * tooling (#685) to bump, so the plugin declares no `upstreamPin` — unlike gcp
11
+ * (`KCC_VERSION`) or k8s (`K8S_SCHEMA_VERSION`), which pin a released schema
12
+ * version. Re-running `generate` simply picks up whatever this URL currently
13
+ * serves.
14
+ */
15
+ const SCHEMA_URL = "https://docs.machines.dev/openapi.json";
16
+ /**
17
+ * Cache under `~/.chant` (like the k8s lexicon) so CI's `~/.chant` schema
18
+ * cache covers it — a `docs.machines.dev` hiccup then falls back to the cache
19
+ * instead of failing the generate step.
20
+ */
21
+ const CACHE_FILE = join(homedir(), ".chant", "fly-machines-openapi.json");
22
+
23
+ /**
24
+ * Fetch the flaps OpenAPI spec and return it as a Map<typeName, Buffer>
25
+ * compatible with the generatePipeline fetchSchemas callback.
26
+ *
27
+ * The flaps spec is a single document, so we return a single entry keyed by
28
+ * "Fly::OpenAPI" — the parse step splits it into the curated resources and
29
+ * their reachable property types.
30
+ */
31
+ export async function fetchSchemas(options?: { force?: boolean }): Promise<Map<string, Buffer>> {
32
+ const raw = await fetchWithCache({ url: SCHEMA_URL, cacheFile: CACHE_FILE }, options?.force);
33
+ return new Map([["Fly::OpenAPI", raw]]);
34
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Fly Machines API OpenAPI 3.0.1 parser.
3
+ *
4
+ * The flaps spec has no resource marker and is mostly request/response DTOs,
5
+ * so we generate a curated set of resources rather than every schema. Each
6
+ * curated resource pairs a request schema (writable authoring surface) with a
7
+ * response schema (read-only attributes). The property types reachable from the
8
+ * request schemas — notably the fly.MachineConfig graph — are emitted as
9
+ * standalone property-type classes so `config` is fully typed.
10
+ */
11
+
12
+ import {
13
+ extractConstraints as coreExtractConstraints,
14
+ primaryType,
15
+ isEnumDefinition,
16
+ type JsonSchemaProperty,
17
+ type PropertyConstraints,
18
+ } from "@intentius/chant/codegen/json-schema";
19
+
20
+ // ── Types ──────────────────────────────────────────────────────────
21
+
22
+ export type { PropertyConstraints };
23
+
24
+ export interface ParsedProperty {
25
+ name: string;
26
+ tsType: string;
27
+ required: boolean;
28
+ description?: string;
29
+ constraints: PropertyConstraints;
30
+ }
31
+
32
+ export interface ParsedResource {
33
+ typeName: string;
34
+ description?: string;
35
+ properties: ParsedProperty[];
36
+ attributes: Array<{ name: string; tsType: string }>;
37
+ }
38
+
39
+ export interface FlyParseResult {
40
+ resource: ParsedResource;
41
+ /** Always empty — fly emits property types as standalone results. */
42
+ propertyTypes: Array<{ name: string; defType: string }>;
43
+ /** Always empty — enums are inlined as string-literal unions. */
44
+ enums: Array<{ name: string; values: string[] }>;
45
+ /** Whether this entity is a property type (nested config shape). */
46
+ isProperty?: boolean;
47
+ }
48
+
49
+ // ── OpenAPI types ──────────────────────────────────────────────────
50
+
51
+ interface OpenAPISchema extends JsonSchemaProperty {
52
+ allOf?: OpenAPISchema[];
53
+ additionalProperties?: boolean | OpenAPISchema;
54
+ items?: OpenAPISchema;
55
+ properties?: Record<string, OpenAPISchema>;
56
+ }
57
+
58
+ interface OpenAPISpec {
59
+ components?: { schemas?: Record<string, OpenAPISchema> };
60
+ }
61
+
62
+ // ── Curated manifest ───────────────────────────────────────────────
63
+
64
+ /** The single service segment for all fly type names. */
65
+ const SERVICE = "Machines";
66
+
67
+ interface ResourceSpec {
68
+ typeName: string;
69
+ request: string;
70
+ response: string;
71
+ }
72
+
73
+ const RESOURCES: ResourceSpec[] = [
74
+ { typeName: `Fly::${SERVICE}::App`, request: "CreateAppRequest", response: "App" },
75
+ { typeName: `Fly::${SERVICE}::Machine`, request: "CreateMachineRequest", response: "Machine" },
76
+ { typeName: `Fly::${SERVICE}::Volume`, request: "CreateVolumeRequest", response: "Volume" },
77
+ { typeName: `Fly::${SERVICE}::IPAddress`, request: "assignIPRequest", response: "IPAssignment" },
78
+ { typeName: `Fly::${SERVICE}::Certificate`, request: "createAcmeCertificateRequest", response: "CertificateDetail" },
79
+ { typeName: `Fly::${SERVICE}::Secret`, request: "SetAppSecretRequest", response: "AppSecret" },
80
+ ];
81
+
82
+ const REF_PREFIX = "#/components/schemas/";
83
+
84
+ // ── Parser ─────────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * Parse the flaps OpenAPI spec into the curated resources and the property
88
+ * types reachable from their request schemas.
89
+ */
90
+ export function parseFlyOpenAPI(data: string | Buffer): FlyParseResult[] {
91
+ const spec: OpenAPISpec = JSON.parse(typeof data === "string" ? data : data.toString("utf-8"));
92
+ const schemas = spec.components?.schemas ?? {};
93
+
94
+ // Phase 1: discover the emitted property-type set — object schemas reachable
95
+ // (transitively) from the request schemas' properties. Enums are inlined, so
96
+ // they are never emitted as classes.
97
+ const emitted = collectPropertyTypes(schemas);
98
+ const className = (schemaName: string): string => schemaToClassName(schemaName);
99
+ const resolve = (prop: OpenAPISchema | undefined): string => resolveType(prop, schemas, emitted);
100
+
101
+ const results: FlyParseResult[] = [];
102
+
103
+ // Phase 2: resources.
104
+ for (const rspec of RESOURCES) {
105
+ const req = schemas[rspec.request];
106
+ const res = schemas[rspec.response];
107
+ const reqProps = req?.properties ?? {};
108
+ const requiredSet = new Set(req?.required ?? []);
109
+
110
+ const properties: ParsedProperty[] = [];
111
+ for (const [name, prop] of Object.entries(reqProps)) {
112
+ properties.push({
113
+ name,
114
+ tsType: resolve(prop),
115
+ required: requiredSet.has(name),
116
+ description: prop.description,
117
+ constraints: coreExtractConstraints(prop as JsonSchemaProperty),
118
+ });
119
+ }
120
+
121
+ // Attributes = response props not present in the request schema.
122
+ const attributes: Array<{ name: string; tsType: string }> = [];
123
+ for (const [name, prop] of Object.entries(res?.properties ?? {})) {
124
+ if (name in reqProps) continue;
125
+ attributes.push({ name, tsType: resolve(prop) });
126
+ }
127
+
128
+ results.push({
129
+ resource: { typeName: rspec.typeName, description: req?.description, properties, attributes },
130
+ propertyTypes: [],
131
+ enums: [],
132
+ });
133
+ }
134
+
135
+ // Phase 3: property-type classes.
136
+ for (const schemaName of emitted) {
137
+ const def = schemas[schemaName];
138
+ const requiredSet = new Set(def.required ?? []);
139
+ const properties: ParsedProperty[] = [];
140
+ for (const [name, prop] of Object.entries(def.properties ?? {})) {
141
+ properties.push({
142
+ name,
143
+ tsType: resolve(prop),
144
+ required: requiredSet.has(name),
145
+ description: prop.description,
146
+ constraints: coreExtractConstraints(prop as JsonSchemaProperty),
147
+ });
148
+ }
149
+ results.push({
150
+ resource: {
151
+ typeName: `Fly::${SERVICE}::${className(schemaName)}`,
152
+ description: def.description,
153
+ properties,
154
+ attributes: [],
155
+ },
156
+ propertyTypes: [],
157
+ enums: [],
158
+ isProperty: true,
159
+ });
160
+ }
161
+
162
+ return results;
163
+ }
164
+
165
+ /**
166
+ * Walk the request schemas and collect the set of object schemas reachable
167
+ * through their properties (following $ref, allOf, arrays, and maps). Returns
168
+ * the schema names in insertion order, deduplicated across resources.
169
+ */
170
+ function collectPropertyTypes(schemas: Record<string, OpenAPISchema>): string[] {
171
+ const emitted = new Set<string>();
172
+ const queue: string[] = [];
173
+
174
+ const seedFrom = (node: unknown) => {
175
+ for (const ref of collectRefs(node)) {
176
+ const target = schemas[ref];
177
+ if (!target) continue;
178
+ if (isObjectSchema(target) && !emitted.has(ref)) {
179
+ emitted.add(ref);
180
+ queue.push(ref);
181
+ }
182
+ }
183
+ };
184
+
185
+ for (const spec of RESOURCES) {
186
+ seedFrom(schemas[spec.request]?.properties);
187
+ }
188
+ while (queue.length > 0) {
189
+ const name = queue.shift()!;
190
+ seedFrom(schemas[name]?.properties);
191
+ }
192
+
193
+ return [...emitted];
194
+ }
195
+
196
+ /** Collect every `#/components/schemas/X` ref name nested anywhere in a node. */
197
+ function collectRefs(node: unknown, acc: Set<string> = new Set()): Set<string> {
198
+ if (!node || typeof node !== "object") return acc;
199
+ if (Array.isArray(node)) {
200
+ for (const item of node) collectRefs(item, acc);
201
+ return acc;
202
+ }
203
+ const obj = node as Record<string, unknown>;
204
+ const ref = obj.$ref;
205
+ if (typeof ref === "string" && ref.startsWith(REF_PREFIX)) acc.add(ref.slice(REF_PREFIX.length));
206
+ for (const [key, value] of Object.entries(obj)) {
207
+ if (key === "$ref") continue;
208
+ collectRefs(value, acc);
209
+ }
210
+ return acc;
211
+ }
212
+
213
+ /** An object schema with properties (not a pure enum). */
214
+ function isObjectSchema(def: OpenAPISchema): boolean {
215
+ return !!def.properties && Object.keys(def.properties).length > 0 && !isEnumDefinition(def);
216
+ }
217
+
218
+ // ── Type resolution ────────────────────────────────────────────────
219
+
220
+ /**
221
+ * Resolve an OpenAPI schema node to its TypeScript type string. Emitted object
222
+ * schemas resolve to their class name; enums inline as string-literal unions;
223
+ * everything else falls back to primitives or `Record<string, any>`.
224
+ */
225
+ function resolveType(
226
+ prop: OpenAPISchema | undefined,
227
+ schemas: Record<string, OpenAPISchema>,
228
+ emitted: string[],
229
+ ): string {
230
+ if (!prop) return "any";
231
+
232
+ if (prop.$ref) return resolveRefType(prop.$ref, schemas, emitted);
233
+
234
+ // allOf: [{ $ref }] — the OpenAPI idiom for "typed as this schema".
235
+ if (prop.allOf && prop.allOf.length > 0) {
236
+ const withRef = prop.allOf.find((s) => s.$ref);
237
+ if (withRef?.$ref) return resolveRefType(withRef.$ref, schemas, emitted);
238
+ }
239
+
240
+ if (prop.enum && prop.enum.length > 0) {
241
+ return [...prop.enum].sort().map((v) => JSON.stringify(v)).join(" | ");
242
+ }
243
+
244
+ const pt = primaryType(prop.type);
245
+ switch (pt) {
246
+ case "string":
247
+ return "string";
248
+ case "integer":
249
+ case "number":
250
+ return "number";
251
+ case "boolean":
252
+ return "boolean";
253
+ case "array": {
254
+ if (prop.items) {
255
+ const itemType = resolveType(prop.items, schemas, emitted);
256
+ return itemType.includes(" | ") ? `(${itemType})[]` : `${itemType}[]`;
257
+ }
258
+ return "any[]";
259
+ }
260
+ case "object":
261
+ if (prop.additionalProperties && typeof prop.additionalProperties === "object") {
262
+ return `Record<string, ${resolveType(prop.additionalProperties, schemas, emitted)}>`;
263
+ }
264
+ return "Record<string, any>";
265
+ default:
266
+ return "any";
267
+ }
268
+ }
269
+
270
+ /** Resolve a `#/components/schemas/X` ref to a TypeScript type. */
271
+ function resolveRefType(ref: string, schemas: Record<string, OpenAPISchema>, emitted: string[]): string {
272
+ if (!ref.startsWith(REF_PREFIX)) return "any";
273
+ const name = ref.slice(REF_PREFIX.length);
274
+ const def = schemas[name];
275
+ if (!def) return "any";
276
+
277
+ // Emitted object schemas reference their class by name.
278
+ if (emitted.includes(name)) return schemaToClassName(name);
279
+
280
+ // Enums inline as string-literal unions.
281
+ if (isEnumDefinition(def)) {
282
+ return [...(def.enum ?? [])].sort().map((v) => JSON.stringify(v)).join(" | ");
283
+ }
284
+
285
+ // Object schemas we did not emit (response-only shapes) loosen to a map.
286
+ if (def.properties) return "Record<string, any>";
287
+
288
+ const pt = primaryType(def.type);
289
+ switch (pt) {
290
+ case "string":
291
+ return "string";
292
+ case "integer":
293
+ case "number":
294
+ return "number";
295
+ case "boolean":
296
+ return "boolean";
297
+ default:
298
+ return "any";
299
+ }
300
+ }
301
+
302
+ // ── Naming helpers ─────────────────────────────────────────────────
303
+
304
+ /**
305
+ * Convert a schema name into a PascalCase class-name segment.
306
+ * "fly.MachineConfig" → "MachineConfig", "fly.dnsOption" → "DnsOption".
307
+ */
308
+ export function schemaToClassName(schemaName: string): string {
309
+ const base = schemaName.slice(schemaName.lastIndexOf(".") + 1);
310
+ return base.charAt(0).toUpperCase() + base.slice(1);
311
+ }
312
+
313
+ /** Extract short name: "Fly::Machines::Machine" → "Machine". */
314
+ export function flyShortName(typeName: string): string {
315
+ const parts = typeName.split("::");
316
+ return parts[parts.length - 1];
317
+ }
318
+
319
+ /** Extract service name: "Fly::Machines::Machine" → "Machines". */
320
+ export function flyServiceName(typeName: string): string {
321
+ const parts = typeName.split("::");
322
+ return parts.length >= 2 ? parts[1] : SERVICE;
323
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env tsx
2
+ import { validate } from "./validate";
3
+ import { printValidationResult } from "@intentius/chant/codegen/validate";
4
+
5
+ printValidationResult(await validate());
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Validate generated lexicon-fly artifacts.
3
+ *
4
+ * Thin wrapper around the core validation framework
5
+ * with fly-specific configuration.
6
+ */
7
+
8
+ import { dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { validateLexiconArtifacts, type ValidateResult } from "@intentius/chant/codegen/validate";
11
+
12
+ export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
13
+
14
+ // TODO: Add names of required entities for your lexicon
15
+ const REQUIRED_NAMES: string[] = [];
16
+
17
+ /**
18
+ * Validate the generated lexicon-fly artifacts.
19
+ */
20
+ export async function validate(opts?: { basePath?: string }): Promise<ValidateResult> {
21
+ const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
22
+
23
+ return validateLexiconArtifacts({
24
+ lexiconJsonFilename: "lexicon-fly.json",
25
+ requiredNames: REQUIRED_NAMES,
26
+ basePath,
27
+ });
28
+ }