@intentius/chant-lexicon-fountain 0.33.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 (171) hide show
  1. package/README.md +63 -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 +16 -0
  5. package/dist/codegen/docs.d.ts.map +1 -0
  6. package/dist/codegen/generate-cli.d.ts +3 -0
  7. package/dist/codegen/generate-cli.d.ts.map +1 -0
  8. package/dist/codegen/generate-lexicon.d.ts +24 -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/concierge-stack.d.ts +44 -0
  19. package/dist/composites/concierge-stack.d.ts.map +1 -0
  20. package/dist/coverage.d.ts +56 -0
  21. package/dist/coverage.d.ts.map +1 -0
  22. package/dist/describe-resources.d.ts +45 -0
  23. package/dist/describe-resources.d.ts.map +1 -0
  24. package/dist/detect.d.ts +10 -0
  25. package/dist/detect.d.ts.map +1 -0
  26. package/dist/entity-props.d.ts +11 -0
  27. package/dist/entity-props.d.ts.map +1 -0
  28. package/dist/export-resources.d.ts +29 -0
  29. package/dist/export-resources.d.ts.map +1 -0
  30. package/dist/generated/index.d.ts +5 -0
  31. package/dist/generated/index.d.ts.map +1 -0
  32. package/dist/generated/runtime.d.ts +5 -0
  33. package/dist/generated/runtime.d.ts.map +1 -0
  34. package/dist/import/generator.d.ts +15 -0
  35. package/dist/import/generator.d.ts.map +1 -0
  36. package/dist/import/parser.d.ts +19 -0
  37. package/dist/import/parser.d.ts.map +1 -0
  38. package/dist/index.d.ts +8 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/init-templates.d.ts +13 -0
  41. package/dist/init-templates.d.ts.map +1 -0
  42. package/dist/integrity.json +21 -0
  43. package/dist/lint/audit-catalog.d.ts +15 -0
  44. package/dist/lint/audit-catalog.d.ts.map +1 -0
  45. package/dist/lint/post-synth/ftn010-networking-explicit.d.ts +11 -0
  46. package/dist/lint/post-synth/ftn010-networking-explicit.d.ts.map +1 -0
  47. package/dist/lint/post-synth/ftn011-no-unrestricted-networking.d.ts +11 -0
  48. package/dist/lint/post-synth/ftn011-no-unrestricted-networking.d.ts.map +1 -0
  49. package/dist/lint/post-synth/ftn012-no-cloud-credential-env.d.ts +3 -0
  50. package/dist/lint/post-synth/ftn012-no-cloud-credential-env.d.ts.map +1 -0
  51. package/dist/lint/post-synth/ftn013-substitution-resolvable.d.ts +3 -0
  52. package/dist/lint/post-synth/ftn013-substitution-resolvable.d.ts.map +1 -0
  53. package/dist/lint/post-synth/ftn014-vault-shadowing.d.ts +3 -0
  54. package/dist/lint/post-synth/ftn014-vault-shadowing.d.ts.map +1 -0
  55. package/dist/lint/post-synth/ftn015-mcp-secret-env-substitution.d.ts +3 -0
  56. package/dist/lint/post-synth/ftn015-mcp-secret-env-substitution.d.ts.map +1 -0
  57. package/dist/lint/post-synth/ftn016-runtime-model-valid.d.ts +3 -0
  58. package/dist/lint/post-synth/ftn016-runtime-model-valid.d.ts.map +1 -0
  59. package/dist/lint/post-synth/ftn017-unique-resource-names.d.ts +11 -0
  60. package/dist/lint/post-synth/ftn017-unique-resource-names.d.ts.map +1 -0
  61. package/dist/lint/post-synth/index.d.ts +3 -0
  62. package/dist/lint/post-synth/index.d.ts.map +1 -0
  63. package/dist/lint/rules/ftn001-no-secret-literals.d.ts +3 -0
  64. package/dist/lint/rules/ftn001-no-secret-literals.d.ts.map +1 -0
  65. package/dist/lint/rules/index.d.ts +5 -0
  66. package/dist/lint/rules/index.d.ts.map +1 -0
  67. package/dist/lsp/completions.d.ts +7 -0
  68. package/dist/lsp/completions.d.ts.map +1 -0
  69. package/dist/lsp/hover.d.ts +4 -0
  70. package/dist/lsp/hover.d.ts.map +1 -0
  71. package/dist/manifest.json +6 -0
  72. package/dist/meta.json +69 -0
  73. package/dist/op/activities/fountain-apply.d.ts +76 -0
  74. package/dist/op/activities/fountain-apply.d.ts.map +1 -0
  75. package/dist/op/activities/fountain-run.d.ts +36 -0
  76. package/dist/op/activities/fountain-run.d.ts.map +1 -0
  77. package/dist/op/activities/index.d.ts +11 -0
  78. package/dist/op/activities/index.d.ts.map +1 -0
  79. package/dist/package-cli.d.ts +3 -0
  80. package/dist/package-cli.d.ts.map +1 -0
  81. package/dist/plugin.d.ts +8 -0
  82. package/dist/plugin.d.ts.map +1 -0
  83. package/dist/reference-catalog.d.ts +16 -0
  84. package/dist/reference-catalog.d.ts.map +1 -0
  85. package/dist/rules/ftn001-no-secret-literals.ts +71 -0
  86. package/dist/rules/ftn010-networking-explicit.ts +37 -0
  87. package/dist/rules/ftn011-no-unrestricted-networking.ts +36 -0
  88. package/dist/rules/ftn012-no-cloud-credential-env.ts +58 -0
  89. package/dist/rules/ftn013-substitution-resolvable.ts +93 -0
  90. package/dist/rules/ftn014-vault-shadowing.ts +61 -0
  91. package/dist/rules/ftn015-mcp-secret-env-substitution.ts +49 -0
  92. package/dist/rules/ftn016-runtime-model-valid.ts +50 -0
  93. package/dist/rules/ftn017-unique-resource-names.ts +43 -0
  94. package/dist/serializer.d.ts +3 -0
  95. package/dist/serializer.d.ts.map +1 -0
  96. package/dist/skills/chant-fountain-locked-sandboxes.md +34 -0
  97. package/dist/skills/chant-fountain-secrets.md +29 -0
  98. package/dist/skills/chant-fountain.md +45 -0
  99. package/dist/spec/fetch.d.ts +28 -0
  100. package/dist/spec/fetch.d.ts.map +1 -0
  101. package/dist/spec/parse.d.ts +58 -0
  102. package/dist/spec/parse.d.ts.map +1 -0
  103. package/dist/types/index.d.ts +60 -0
  104. package/dist/validate-cli.d.ts +3 -0
  105. package/dist/validate-cli.d.ts.map +1 -0
  106. package/dist/validate.d.ts +15 -0
  107. package/dist/validate.d.ts.map +1 -0
  108. package/package.json +61 -0
  109. package/src/codegen/docs-cli.ts +7 -0
  110. package/src/codegen/docs.ts +278 -0
  111. package/src/codegen/generate-cli.ts +8 -0
  112. package/src/codegen/generate-lexicon.ts +58 -0
  113. package/src/codegen/generate-typescript.ts +69 -0
  114. package/src/codegen/generate.ts +109 -0
  115. package/src/codegen/naming.ts +45 -0
  116. package/src/codegen/package.ts +32 -0
  117. package/src/composites/composites.test.ts +35 -0
  118. package/src/composites/concierge-stack.ts +71 -0
  119. package/src/coverage.test.ts +63 -0
  120. package/src/coverage.ts +178 -0
  121. package/src/describe-resources.test.ts +203 -0
  122. package/src/describe-resources.ts +192 -0
  123. package/src/detect.ts +37 -0
  124. package/src/entity-props.ts +22 -0
  125. package/src/export-resources.ts +103 -0
  126. package/src/generated/.gitkeep +0 -0
  127. package/src/generated/index.d.ts +60 -0
  128. package/src/generated/index.ts +10 -0
  129. package/src/generated/lexicon-fountain.json +69 -0
  130. package/src/generated/runtime.ts +4 -0
  131. package/src/import/generator.ts +112 -0
  132. package/src/import/parser.ts +83 -0
  133. package/src/import/roundtrip.test.ts +113 -0
  134. package/src/index.ts +17 -0
  135. package/src/init-templates.ts +60 -0
  136. package/src/lint/audit-catalog.ts +107 -0
  137. package/src/lint/post-synth/ftn010-networking-explicit.ts +37 -0
  138. package/src/lint/post-synth/ftn011-no-unrestricted-networking.ts +36 -0
  139. package/src/lint/post-synth/ftn012-no-cloud-credential-env.ts +58 -0
  140. package/src/lint/post-synth/ftn013-substitution-resolvable.ts +93 -0
  141. package/src/lint/post-synth/ftn014-vault-shadowing.ts +61 -0
  142. package/src/lint/post-synth/ftn015-mcp-secret-env-substitution.ts +49 -0
  143. package/src/lint/post-synth/ftn016-runtime-model-valid.ts +50 -0
  144. package/src/lint/post-synth/ftn017-unique-resource-names.ts +43 -0
  145. package/src/lint/post-synth/index.ts +21 -0
  146. package/src/lint/post-synth/post-synth.test.ts +133 -0
  147. package/src/lint/rules/ftn001-no-secret-literals.ts +71 -0
  148. package/src/lint/rules/index.ts +7 -0
  149. package/src/lint/rules/rules.test.ts +126 -0
  150. package/src/lsp/completions.test.ts +51 -0
  151. package/src/lsp/completions.ts +21 -0
  152. package/src/lsp/hover.test.ts +47 -0
  153. package/src/lsp/hover.ts +70 -0
  154. package/src/op/activities/fountain-apply.test.ts +208 -0
  155. package/src/op/activities/fountain-apply.ts +249 -0
  156. package/src/op/activities/fountain-run.ts +95 -0
  157. package/src/op/activities/index.ts +28 -0
  158. package/src/package-cli.ts +26 -0
  159. package/src/plugin.test.ts +73 -0
  160. package/src/plugin.ts +170 -0
  161. package/src/reference-catalog.ts +27 -0
  162. package/src/serializer.test.ts +109 -0
  163. package/src/serializer.ts +143 -0
  164. package/src/skills/chant-fountain-locked-sandboxes.md +34 -0
  165. package/src/skills/chant-fountain-secrets.md +29 -0
  166. package/src/skills/chant-fountain.md +45 -0
  167. package/src/spec/fetch.ts +53 -0
  168. package/src/spec/fountain-openapi.snapshot.json +1 -0
  169. package/src/spec/parse.ts +309 -0
  170. package/src/validate-cli.ts +13 -0
  171. package/src/validate.ts +34 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Live introspection of fountain resources — the read-back seam for chant's
3
+ * plan/drift machinery.
4
+ *
5
+ * Drives core's observer harness (`observeEntities`, #1201) rather than
6
+ * re-deriving its control flow: bind-or-not-observe-all with a typed reason,
7
+ * per-entity tri-state routing, and a read throw degrading to `read-failed`
8
+ * rather than a silent absence that would classify as a spurious `create`.
9
+ * What this lexicon supplies is the adapter — the transport and the per-entity
10
+ * read.
11
+ *
12
+ * The read is a lookup, not a fetch: fountain has no per-resource-by-name
13
+ * endpoint, so the adapter lists each declared kind once
14
+ * (GET /api/environments|vaults|agents) and indexes it by name. The list
15
+ * promise is cached per kind, so concurrent reads share one request and a
16
+ * failed list marks only that kind's entities read-failed.
17
+ *
18
+ * Ownership comes from the `managed-by: chant` metadata marker (fountain#137
19
+ * gave all three kinds the channel). Endpoint + auth reuse the applier
20
+ * verbatim (FOUNTAIN_ENDPOINT / FOUNTAIN_TOKEN), so plan reads the same
21
+ * instance fountainApply writes. Secret *values* never appear anywhere on
22
+ * this path — the API is write-only for values; the secrets sub-resource is
23
+ * not read here at all.
24
+ */
25
+
26
+ import type { ObservationResult, ResourceMetadata } from "@intentius/chant/lexicon";
27
+ import {
28
+ observeEntities,
29
+ type DeclaredEntity,
30
+ type EntityObservation,
31
+ type ObserverAdapter,
32
+ } from "@intentius/chant/observation";
33
+ import {
34
+ resolveEndpoint,
35
+ defaultFountainHttp,
36
+ isChantOwned,
37
+ OWNERSHIP_KEY,
38
+ OWNERSHIP_VALUE,
39
+ type FountainHttp,
40
+ } from "./op/activities/fountain-apply";
41
+
42
+ const KIND_PATHS: Record<string, string> = {
43
+ "Fountain::V1::Environment": "environments",
44
+ "Fountain::V1::Vault": "vaults",
45
+ "Fountain::V1::Agent": "agents",
46
+ };
47
+
48
+ /** Thrown by bind() when there is no token to read with. */
49
+ class MissingTokenError extends Error {}
50
+
51
+ export interface DescribeResourcesOptions {
52
+ environment: string;
53
+ buildOutput: string;
54
+ entityNames: string[];
55
+ entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
56
+ /** Restrict to chant-owned resources. */
57
+ owned?: boolean;
58
+ /** Endpoint override (tests). Defaults to resolveEndpoint(). */
59
+ endpoint?: string;
60
+ }
61
+
62
+ interface LiveResource {
63
+ id: string;
64
+ name: string;
65
+ metadata?: Record<string, unknown>;
66
+ inserted_at?: string;
67
+ updated_at?: string;
68
+ /** Agents only — the reference edge the catalog reconstructs. */
69
+ environment_id?: string | null;
70
+ }
71
+
72
+ /** The transport plus its per-kind list cache. */
73
+ interface FountainClient {
74
+ http: FountainHttp;
75
+ /** entityType → in-flight or settled list, indexed by resource name. */
76
+ lists: Map<string, Promise<Map<string, LiveResource>>>;
77
+ }
78
+
79
+ function listKind(client: FountainClient, entityType: string): Promise<Map<string, LiveResource>> {
80
+ const cached = client.lists.get(entityType);
81
+ if (cached) return cached;
82
+
83
+ const path = KIND_PATHS[entityType];
84
+ const pending = (async () => {
85
+ const { status, json } = await client.http("GET", `/api/${path}`);
86
+ if (status !== 200) throw new Error(`list ${path} returned ${status}`);
87
+ const data = (json as { data?: LiveResource[] })?.data ?? [];
88
+ return new Map(data.map((r) => [r.name, r]));
89
+ })();
90
+
91
+ client.lists.set(entityType, pending);
92
+ return pending;
93
+ }
94
+
95
+ function present(entityType: string, found: LiveResource, owned: boolean): ResourceMetadata {
96
+ return {
97
+ type: entityType,
98
+ physicalId: found.id,
99
+ status: "PRESENT",
100
+ ...(found.updated_at ? { lastUpdated: found.updated_at } : {}),
101
+ attributes: {
102
+ id: found.id,
103
+ ...(found.inserted_at ? { inserted_at: found.inserted_at } : {}),
104
+ ...(found.updated_at ? { updated_at: found.updated_at } : {}),
105
+ ...(found.environment_id ? { environment_id: found.environment_id } : {}),
106
+ },
107
+ ownership: owned ? "owned" : "foreign",
108
+ };
109
+ }
110
+
111
+ function createAdapter(
112
+ options: DescribeResourcesOptions,
113
+ http?: FountainHttp,
114
+ ): ObserverAdapter<FountainClient> {
115
+ return {
116
+ async bind() {
117
+ if (http) return { http, lists: new Map() };
118
+
119
+ const token = process.env.FOUNTAIN_TOKEN;
120
+ if (!token) {
121
+ throw new MissingTokenError(
122
+ "FOUNTAIN_TOKEN is not set — cannot read live fountain state",
123
+ );
124
+ }
125
+ return {
126
+ http: defaultFountainHttp(resolveEndpoint({ endpoint: options.endpoint }), token),
127
+ lists: new Map(),
128
+ };
129
+ },
130
+
131
+ classifyBindFailure(err) {
132
+ // The only whole-lexicon failure fountain has: nothing to authenticate
133
+ // with. Anything else is a genuine fault and must stay loud.
134
+ if (err instanceof MissingTokenError) {
135
+ return { reason: "no-credentials", detail: err.message };
136
+ }
137
+ return "rethrow";
138
+ },
139
+
140
+ async read(client, entity): Promise<EntityObservation> {
141
+ if (!(entity.type in KIND_PATHS)) {
142
+ return {
143
+ unobserved: {
144
+ reason: "unsupported-kind",
145
+ detail: `no fountain read path for ${entity.type}`,
146
+ },
147
+ };
148
+ }
149
+
150
+ // A list failure throws — the harness records read-failed for this
151
+ // entity, and the cached rejected promise gives the same verdict to
152
+ // every other entity of the kind without a second request.
153
+ const live = await listKind(client, entity.type);
154
+
155
+ const resourceName =
156
+ typeof entity.props.name === "string" ? (entity.props.name as string) : entity.name;
157
+ const found = live.get(resourceName);
158
+
159
+ // Observed absent: we asked, fountain said no → eligible for `create`.
160
+ if (!found) return { absent: true };
161
+
162
+ const owned = isChantOwned(found);
163
+ if (options.owned && !owned) {
164
+ return {
165
+ unobserved: {
166
+ reason: "filtered",
167
+ detail: `"${resourceName}" exists but does not carry the ${OWNERSHIP_KEY}: ${OWNERSHIP_VALUE} marker`,
168
+ },
169
+ };
170
+ }
171
+
172
+ return { present: present(entity.type, found, owned) };
173
+ },
174
+ };
175
+ }
176
+
177
+ /**
178
+ * `http` is injectable for tests; the default reuses the applier's fetch
179
+ * client (bearer token from FOUNTAIN_TOKEN).
180
+ */
181
+ export async function describeResources(
182
+ options: DescribeResourcesOptions,
183
+ http?: FountainHttp,
184
+ ): Promise<ObservationResult> {
185
+ const declared: DeclaredEntity[] = [...options.entities].map(([name, entity]) => ({
186
+ name,
187
+ type: entity.entityType,
188
+ props: entity.props,
189
+ }));
190
+
191
+ return observeEntities(declared, createAdapter(options, http));
192
+ }
package/src/detect.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Detection helpers — is this input a fountain artifact?
3
+ */
4
+
5
+ const KINDS = new Set(["Environment", "Vault", "Agent"]);
6
+
7
+ /** A fountain manifest document (parsed): apiVersion fountain.dev/v1 + known kind. */
8
+ export function isFountainManifest(doc: unknown): boolean {
9
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return false;
10
+ const d = doc as Record<string, unknown>;
11
+ return d.apiVersion === "fountain.dev/v1" && typeof d.kind === "string" && KINDS.has(d.kind);
12
+ }
13
+
14
+ /** The serializer's fountain-plan.json shape: entity name → { kind, spec }. */
15
+ export function isFountainPlan(data: unknown): boolean {
16
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
17
+ const entries = Object.values(data as Record<string, unknown>);
18
+ if (entries.length === 0) return false;
19
+ return entries.every((e) => {
20
+ if (!e || typeof e !== "object") return false;
21
+ const entry = e as Record<string, unknown>;
22
+ return typeof entry.kind === "string" && KINDS.has(entry.kind) && typeof entry.spec === "object";
23
+ });
24
+ }
25
+
26
+ /** Template detection for the plugin: raw string input (YAML or JSON). */
27
+ export function detectFountainTemplate(data: unknown): boolean {
28
+ if (typeof data === "string") {
29
+ if (/apiVersion:\s*fountain\.dev\/v1/.test(data)) return true;
30
+ try {
31
+ return isFountainPlan(JSON.parse(data));
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ return isFountainManifest(data) || isFountainPlan(data);
37
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Read an entity's authored props regardless of representation.
3
+ *
4
+ * Real runtime instances (createResource/createProperty) keep authored
5
+ * props under the non-enumerable `.props` — enumerating the instance
6
+ * yields its AttrRefs, not its inputs. Plain objects (parsed data, test
7
+ * fixtures) carry props as own fields. Every fountain consumer of entity
8
+ * props goes through here so both shapes behave identically.
9
+ */
10
+ export function propsOf(entity: unknown): Record<string, unknown> {
11
+ if (!entity || typeof entity !== "object") return {};
12
+ const anyEntity = entity as Record<string, unknown>;
13
+ if (anyEntity.props && typeof anyEntity.props === "object" && !Array.isArray(anyEntity.props)) {
14
+ return anyEntity.props as Record<string, unknown>;
15
+ }
16
+ const out: Record<string, unknown> = {};
17
+ for (const [key, value] of Object.entries(anyEntity)) {
18
+ if (key === "entityType" || key === "lexicon" || key === "kind" || key === "attributes") continue;
19
+ out[key] = value;
20
+ }
21
+ return out;
22
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Live export — read fountain resources back as import IR for
3
+ * `chant import --from`. The adoption path: an org with UI-built
4
+ * environments/vaults/agents gets typed leaf files.
5
+ *
6
+ * Server-written fields are stripped to the authored shape; an agent's
7
+ * `environment_id` is resolved back to the exported environment's logical
8
+ * name so the generated code carries a reviewable reference. Secret
9
+ * *values* never leave fountain (write-only upstream) and secret *keys*
10
+ * cannot round-trip into typed code yet — the request schema has no
11
+ * secrets field (they are a sub-resource; fountain#148's reference model
12
+ * would change this) — so environments export without their secrets and
13
+ * the caller is warned per environment that carries any.
14
+ */
15
+
16
+ import type { ExportedTemplate, ResourceSelector } from "@intentius/chant/lexicon";
17
+ import type { ResourceIR } from "@intentius/chant/import/parser";
18
+ import {
19
+ resolveEndpoint,
20
+ defaultFountainHttp,
21
+ isChantOwned,
22
+ type FountainHttp,
23
+ } from "./op/activities/fountain-apply";
24
+ import { SERVER_FIELDS } from "./import/parser";
25
+
26
+ const KINDS: Array<{ type: string; path: string; kind: string }> = [
27
+ { type: "Fountain::V1::Environment", path: "environments", kind: "Environment" },
28
+ { type: "Fountain::V1::Vault", path: "vaults", kind: "Vault" },
29
+ { type: "Fountain::V1::Agent", path: "agents", kind: "Agent" },
30
+ ];
31
+
32
+ export interface ExportResourcesOptions {
33
+ environment: string;
34
+ selector?: ResourceSelector;
35
+ owned?: boolean;
36
+ verbatim?: boolean;
37
+ endpoint?: string;
38
+ /** Injectable for tests. */
39
+ http?: FountainHttp;
40
+ /** Warning sink (defaults to console.error). */
41
+ warn?: (msg: string) => void;
42
+ }
43
+
44
+ export async function exportResources(options: ExportResourcesOptions): Promise<ExportedTemplate> {
45
+ const warn = options.warn ?? ((msg: string) => console.error(`[fountain] ${msg}`));
46
+ const token = process.env.FOUNTAIN_TOKEN;
47
+ const http =
48
+ options.http ??
49
+ defaultFountainHttp(resolveEndpoint({ endpoint: options.endpoint }), token ?? "");
50
+ if (!options.http && !token) {
51
+ throw new Error("fountain export: FOUNTAIN_TOKEN is not set");
52
+ }
53
+
54
+ const resources: ResourceIR[] = [];
55
+ const envNameById = new Map<string, string>();
56
+
57
+ for (const { type, path, kind } of KINDS) {
58
+ if (options.selector?.type && options.selector.type !== type && options.selector.type !== kind) continue;
59
+
60
+ const { status, json } = await http("GET", `/api/${path}`);
61
+ if (status !== 200) throw new Error(`fountain export: list ${path} returned ${status}`);
62
+ const data = ((json as { data?: Array<Record<string, unknown>> })?.data ?? []);
63
+
64
+ for (const live of data) {
65
+ const name = typeof live.name === "string" ? live.name : "";
66
+ if (!name) continue;
67
+ if (options.selector?.name && options.selector.name !== name) continue;
68
+ if (options.owned && !isChantOwned(live)) continue;
69
+
70
+ const properties: Record<string, unknown> = {};
71
+ for (const [key, value] of Object.entries(live)) {
72
+ if (!options.verbatim && SERVER_FIELDS.includes(key)) continue;
73
+ if (value === null || value === undefined) continue;
74
+ properties[key] = value;
75
+ }
76
+
77
+ if (kind === "Environment") {
78
+ envNameById.set(String(live.id), name);
79
+ // Secrets are write-only and not expressible on the typed surface.
80
+ const { status: sStatus, json: sJson } = await http("GET", `/api/${path}/${live.id}/secrets`);
81
+ const secretCount = sStatus === 200 ? (((sJson as { data?: unknown[] })?.data ?? []).length) : 0;
82
+ if (secretCount > 0) {
83
+ warn(
84
+ `environment "${name}" carries ${secretCount} secret(s) — values are write-only and ` +
85
+ `keys are not part of the typed surface; re-declare them via your secret provider`,
86
+ );
87
+ }
88
+ }
89
+
90
+ if (kind === "Agent" && typeof properties.environment_id === "string") {
91
+ const envName = envNameById.get(properties.environment_id);
92
+ if (envName) {
93
+ delete properties.environment_id;
94
+ properties.environment = envName;
95
+ }
96
+ }
97
+
98
+ resources.push({ logicalId: name, type, properties });
99
+ }
100
+ }
101
+
102
+ return { resources, parameters: [] } as ExportedTemplate;
103
+ }
File without changes
@@ -0,0 +1,60 @@
1
+ // Code generated by chant fly generate. DO NOT EDIT.
2
+
3
+ // --- Fly Resource classes ---
4
+
5
+ export declare class Agent {
6
+ constructor(props: {
7
+ model: string;
8
+ name: string;
9
+ runtime: "claude" | "codex" | "gemini" | "opencode";
10
+ /** Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist. */
11
+ allowed_vault_ids?: string[];
12
+ description?: string;
13
+ environment_id?: string;
14
+ mcp_servers?: Record<string, unknown>;
15
+ metadata?: Record<string, unknown>;
16
+ /** Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry. */
17
+ skills?: Record<string, unknown>[];
18
+ system?: string;
19
+ });
20
+ readonly id: string;
21
+ readonly inserted_at: string;
22
+ readonly updated_at: string;
23
+ }
24
+
25
+ export declare class Environment {
26
+ constructor(props: {
27
+ name: string;
28
+ env_vars?: Record<string, string>;
29
+ metadata?: Record<string, unknown>;
30
+ /** Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all. */
31
+ networking_config?: Record<string, unknown>;
32
+ networking_type?: "limited" | "unrestricted";
33
+ packages?: Record<string, unknown>;
34
+ repositories?: Repository[];
35
+ setup_script?: string;
36
+ });
37
+ readonly id: string;
38
+ readonly inserted_at: string;
39
+ readonly updated_at: string;
40
+ }
41
+
42
+ export declare class Vault {
43
+ constructor(props: {
44
+ name: string;
45
+ description?: string;
46
+ metadata?: Record<string, unknown>;
47
+ });
48
+ readonly id: string;
49
+ readonly inserted_at: string;
50
+ readonly updated_at: string;
51
+ }
52
+
53
+ // --- Property type classes ---
54
+
55
+ export declare class Repository {
56
+ constructor(props: {
57
+ mount_path: string;
58
+ url: string;
59
+ });
60
+ }
@@ -0,0 +1,10 @@
1
+ // Code generated by chant generate. DO NOT EDIT.
2
+ import { createResource, createProperty } from "./runtime";
3
+
4
+ export const Agent = createResource("Fountain::V1::Agent", "fountain", {"id":"id","inserted_at":"inserted_at","updated_at":"updated_at"});
5
+ export const Environment = createResource("Fountain::V1::Environment", "fountain", {"id":"id","inserted_at":"inserted_at","updated_at":"updated_at"});
6
+ export const Vault = createResource("Fountain::V1::Vault", "fountain", {"id":"id","inserted_at":"inserted_at","updated_at":"updated_at"});
7
+
8
+ export const Repository = createProperty("Fountain::V1::Repository", "fountain");
9
+
10
+ // Re-exports for convenience
@@ -0,0 +1,69 @@
1
+ {
2
+ "Agent": {
3
+ "resourceType": "Fountain::V1::Agent",
4
+ "kind": "resource",
5
+ "lexicon": "fountain",
6
+ "propertyConstraints": {
7
+ "environment_id": {
8
+ "format": "uuid"
9
+ },
10
+ "model": {
11
+ "pattern": "^[a-z0-9_-]+/[a-z0-9._-]+$"
12
+ },
13
+ "name": {
14
+ "minLength": 1,
15
+ "maxLength": 200
16
+ },
17
+ "runtime": {
18
+ "enum": [
19
+ "claude",
20
+ "codex",
21
+ "gemini",
22
+ "opencode"
23
+ ]
24
+ }
25
+ }
26
+ },
27
+ "Environment": {
28
+ "resourceType": "Fountain::V1::Environment",
29
+ "kind": "resource",
30
+ "lexicon": "fountain",
31
+ "propertyConstraints": {
32
+ "name": {
33
+ "minLength": 1,
34
+ "maxLength": 200
35
+ },
36
+ "networking_type": {
37
+ "enum": [
38
+ "unrestricted",
39
+ "limited"
40
+ ]
41
+ }
42
+ }
43
+ },
44
+ "Repository": {
45
+ "resourceType": "Fountain::V1::Repository",
46
+ "kind": "property",
47
+ "lexicon": "fountain",
48
+ "propertyConstraints": {
49
+ "mount_path": {
50
+ "pattern": "^/"
51
+ },
52
+ "url": {
53
+ "pattern": "^https://",
54
+ "format": "uri"
55
+ }
56
+ }
57
+ },
58
+ "Vault": {
59
+ "resourceType": "Fountain::V1::Vault",
60
+ "kind": "resource",
61
+ "lexicon": "fountain",
62
+ "propertyConstraints": {
63
+ "name": {
64
+ "minLength": 1,
65
+ "maxLength": 200
66
+ }
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Runtime factory constructors — re-exported from core.
3
+ */
4
+ export { createResource, createProperty } from "@intentius/chant/runtime";
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Fountain TypeScript generator.
3
+ *
4
+ * Converts import IR from {@link FountainParser} into typed chant
5
+ * TypeScript: `import { Environment, ... } from
6
+ * "@intentius/chant-lexicon-fountain";` plus one `export const` per
7
+ * resource. `repositories` entries are wrapped in `new Repository({...})`
8
+ * — the one nested property declarable on the fountain surface.
9
+ */
10
+
11
+ import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
12
+ import type { TemplateIR } from "@intentius/chant/import/parser";
13
+
14
+ type Nested = { kind: "single" | "array"; cls: string };
15
+
16
+ const NESTED_DECLARABLES: Record<string, Record<string, Nested>> = {
17
+ Environment: {
18
+ repositories: { kind: "array", cls: "Repository" },
19
+ },
20
+ };
21
+
22
+ export class FountainGenerator implements TypeScriptGenerator {
23
+ generate(ir: TemplateIR): GeneratedFile[] {
24
+ const imports = new Set<string>();
25
+ const bodyLines: string[] = [];
26
+
27
+ for (const resource of ir.resources) {
28
+ const parts = resource.type.split("::");
29
+ const className = parts.length >= 3 ? parts[2] : resource.type;
30
+ imports.add(className);
31
+ const varName = camelCase(resource.logicalId);
32
+ const props = formatProps(resource.properties, 0, className, imports);
33
+ bodyLines.push(`export const ${varName} = new ${className}(${props});`);
34
+ bodyLines.push("");
35
+ }
36
+
37
+ const lines: string[] = [];
38
+ if (imports.size > 0) {
39
+ lines.push(`import { ${[...imports].sort().join(", ")} } from "@intentius/chant-lexicon-fountain";`);
40
+ lines.push("");
41
+ }
42
+ lines.push(...bodyLines);
43
+
44
+ return [{ path: "main.ts", content: lines.join("\n") + "\n" }];
45
+ }
46
+ }
47
+
48
+ function camelCase(str: string): string {
49
+ return str.replace(/[-_.](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toLowerCase());
50
+ }
51
+
52
+ function formatProps(props: Record<string, unknown>, indent: number, ownerClass: string, imports: Set<string>): string {
53
+ const entries = Object.entries(props);
54
+ if (entries.length === 0) return "{}";
55
+
56
+ const pad = " ".repeat(indent + 1);
57
+ const closePad = " ".repeat(indent);
58
+ const nested = NESTED_DECLARABLES[ownerClass];
59
+
60
+ const lines = entries.map(([key, value]) => {
61
+ const spec = nested?.[key];
62
+ const rendered = spec !== undefined ? formatNested(value, indent + 1, spec, imports) : formatValue(value, indent + 1);
63
+ return `${pad}${objectKey(key)}: ${rendered},`;
64
+ });
65
+
66
+ return `{\n${lines.join("\n")}\n${closePad}}`;
67
+ }
68
+
69
+ function formatNested(value: unknown, indent: number, spec: Nested, imports: Set<string>): string {
70
+ const construct = (obj: unknown, at: number): string => {
71
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return formatValue(obj, at);
72
+ imports.add(spec.cls);
73
+ return `new ${spec.cls}(${formatProps(obj as Record<string, unknown>, at, spec.cls, imports)})`;
74
+ };
75
+
76
+ if (spec.kind === "single") return construct(value, indent);
77
+
78
+ if (!Array.isArray(value) || value.length === 0) return "[]";
79
+ const pad = " ".repeat(indent + 1);
80
+ const closePad = " ".repeat(indent);
81
+ const items = value.map((v) => `${pad}${construct(v, indent + 1)},`);
82
+ return `[\n${items.join("\n")}\n${closePad}]`;
83
+ }
84
+
85
+ function formatValue(value: unknown, indent: number): string {
86
+ if (value === null || value === undefined) return "undefined";
87
+ if (typeof value === "string") return JSON.stringify(value);
88
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
89
+
90
+ if (Array.isArray(value)) {
91
+ if (value.length === 0) return "[]";
92
+ const pad = " ".repeat(indent + 1);
93
+ const closePad = " ".repeat(indent);
94
+ const items = value.map((v) => `${pad}${formatValue(v, indent + 1)},`);
95
+ return `[\n${items.join("\n")}\n${closePad}]`;
96
+ }
97
+
98
+ if (typeof value === "object") {
99
+ const entries = Object.entries(value as Record<string, unknown>);
100
+ if (entries.length === 0) return "{}";
101
+ const pad = " ".repeat(indent + 1);
102
+ const closePad = " ".repeat(indent);
103
+ const lines = entries.map(([key, v]) => `${pad}${objectKey(key)}: ${formatValue(v, indent + 1)},`);
104
+ return `{\n${lines.join("\n")}\n${closePad}}`;
105
+ }
106
+
107
+ return String(value);
108
+ }
109
+
110
+ function objectKey(key: string): string {
111
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
112
+ }