@intentius/chant-lexicon-helm 0.124.31 → 0.124.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/archive-render.js +76 -0
  2. package/dist/codegen/docs-cli.js +3 -0
  3. package/dist/codegen/docs.js +113 -0
  4. package/dist/codegen/generate-cli.js +19 -0
  5. package/dist/codegen/generate.js +210 -0
  6. package/dist/codegen/naming.js +29 -0
  7. package/dist/codegen/package.js +44 -0
  8. package/dist/codegen/values-type.js +87 -0
  9. package/dist/commands.js +413 -0
  10. package/dist/components/builders.js +9 -0
  11. package/dist/components/capability-plugin.js +17 -0
  12. package/dist/components/helm-upgrade.js +109 -0
  13. package/dist/components/index.js +11 -0
  14. package/dist/composites/catalog.js +1119 -0
  15. package/dist/composites/helm-batch-job.js +148 -0
  16. package/dist/composites/helm-crd-lifecycle.js +145 -0
  17. package/dist/composites/helm-cron-job.js +130 -0
  18. package/dist/composites/helm-daemon-set.js +125 -0
  19. package/dist/composites/helm-external-secret.js +63 -0
  20. package/dist/composites/helm-library.js +24 -0
  21. package/dist/composites/helm-microservice.js +260 -0
  22. package/dist/composites/helm-monitored-service.js +188 -0
  23. package/dist/composites/helm-namespace-env.js +116 -0
  24. package/dist/composites/helm-secure-ingress.js +86 -0
  25. package/dist/composites/helm-stateful-service.js +160 -0
  26. package/dist/composites/helm-web-app.js +201 -0
  27. package/dist/composites/helm-worker.js +164 -0
  28. package/dist/composites/index.js +13 -0
  29. package/dist/config.js +161 -0
  30. package/dist/coverage.js +37 -0
  31. package/dist/deep-observe-hooks.js +19 -0
  32. package/dist/deep-observe.js +55 -0
  33. package/dist/describe-resources.js +119 -0
  34. package/dist/describe-stack-status.js +66 -0
  35. package/dist/detect.js +11 -0
  36. package/dist/generated/index.js +11 -0
  37. package/dist/helpers.js +84 -0
  38. package/dist/import/generator.js +292 -0
  39. package/dist/import/parser.js +141 -0
  40. package/dist/import/template-stripper.js +86 -0
  41. package/dist/index.js +55 -0
  42. package/dist/integrity.json +2 -2
  43. package/dist/intrinsics.js +482 -0
  44. package/dist/lint/audit-catalog.js +33 -0
  45. package/dist/lint/audit-lineage.js +53 -0
  46. package/dist/lint/post-synth/helm-helpers.js +60 -0
  47. package/dist/lint/post-synth/index.js +48 -0
  48. package/dist/lint/post-synth/whm005-no-empty-wrapper.js +47 -0
  49. package/dist/lint/post-synth/whm101.js +38 -0
  50. package/dist/lint/post-synth/whm102.js +26 -0
  51. package/dist/lint/post-synth/whm103.js +51 -0
  52. package/dist/lint/post-synth/whm104.js +29 -0
  53. package/dist/lint/post-synth/whm105.js +23 -0
  54. package/dist/lint/post-synth/whm201.js +29 -0
  55. package/dist/lint/post-synth/whm202.js +42 -0
  56. package/dist/lint/post-synth/whm203.js +32 -0
  57. package/dist/lint/post-synth/whm204.js +51 -0
  58. package/dist/lint/post-synth/whm301.js +31 -0
  59. package/dist/lint/post-synth/whm302.js +33 -0
  60. package/dist/lint/post-synth/whm401.js +52 -0
  61. package/dist/lint/post-synth/whm402.js +38 -0
  62. package/dist/lint/post-synth/whm403.js +38 -0
  63. package/dist/lint/post-synth/whm404.js +30 -0
  64. package/dist/lint/post-synth/whm405.js +47 -0
  65. package/dist/lint/post-synth/whm406.js +27 -0
  66. package/dist/lint/post-synth/whm407.js +72 -0
  67. package/dist/lint/post-synth/whm501.js +89 -0
  68. package/dist/lint/post-synth/whm502.js +85 -0
  69. package/dist/lint/post-synth/whm503.js +82 -0
  70. package/dist/lint/post-synth/whm504.js +47 -0
  71. package/dist/lint/rules/chart-metadata.js +53 -0
  72. package/dist/lint/rules/no-hardcoded-image.js +50 -0
  73. package/dist/lint/rules/values-no-helm-tpl.js +78 -0
  74. package/dist/lint/rules/values-no-secrets.js +65 -0
  75. package/dist/list-artifacts.js +134 -0
  76. package/dist/lsp/completions.d.ts.map +1 -1
  77. package/dist/lsp/completions.js +18 -0
  78. package/dist/lsp/hover.d.ts.map +1 -1
  79. package/dist/lsp/hover.js +40 -0
  80. package/dist/manifest.json +1 -1
  81. package/dist/op/activities/cluster-probe.js +116 -0
  82. package/dist/op/activities/helm.js +449 -0
  83. package/dist/op/activities/index.js +10 -0
  84. package/dist/op/builders.js +45 -0
  85. package/dist/package-cli.js +21 -0
  86. package/dist/pinnability/actions.js +207 -0
  87. package/dist/pinnability/classify.js +416 -0
  88. package/dist/pinnability/conditions.js +0 -0
  89. package/dist/pinnability/index.js +16 -0
  90. package/dist/pinnability/localize.js +518 -0
  91. package/dist/pinnability/render-stream.js +53 -0
  92. package/dist/pinnability/values.js +151 -0
  93. package/dist/plugin.js +458 -0
  94. package/dist/project-codegen.js +215 -0
  95. package/dist/release-observe.js +287 -0
  96. package/dist/render-diff.js +279 -0
  97. package/dist/render-digest.js +0 -0
  98. package/dist/render-records.js +29 -0
  99. package/dist/render-store.js +401 -0
  100. package/dist/render-wrapper.js +164 -0
  101. package/dist/render.js +394 -0
  102. package/dist/resources.js +109 -0
  103. package/dist/serializer.js +903 -0
  104. package/dist/validate-cli.js +17 -0
  105. package/dist/validate.js +30 -0
  106. package/dist/values-probe.js +504 -0
  107. package/dist/wrapper-chart.js +95 -0
  108. package/package.json +11 -11
  109. package/src/codegen/package.ts +2 -2
  110. package/src/lsp/completions.ts +2 -1
  111. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,903 @@
1
+ /**
2
+ * Helm chart serializer.
3
+ *
4
+ * Converts Chant declarables into a complete Helm chart directory structure
5
+ * returned as a SerializerResult with a files map:
6
+ *
7
+ * Chart.yaml, values.yaml, values.schema.json, .helmignore,
8
+ * templates/_helpers.tpl, templates/<resource>.yaml, templates/NOTES.txt,
9
+ * templates/tests/test-connection.yaml
10
+ *
11
+ * The serializer detects `__helm_tpl` markers in walked values and emits
12
+ * raw Go template expressions instead of YAML-quoting them. It also
13
+ * detects `__helm_if` markers to wrap entire resources in conditionals.
14
+ */
15
+ import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
16
+ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
17
+ import { walkValue } from "@intentius/chant/serializer-walker";
18
+ import { HELM_TPL_KEY, HELM_IF_KEY, HELM_RANGE_KEY, HELM_WITH_KEY, RUNTIME_SLOT_KEY } from "./intrinsics.js";
19
+ import { generateHelpers } from "./helpers.js";
20
+ // ── GVK resolution for K8s resources ──────────────────────
21
+ /**
22
+ * Well-known K8s API group → apiVersion mappings.
23
+ * Used to resolve K8s entity types to apiVersion/kind for YAML emission.
24
+ */
25
+ const API_GROUP_VERSIONS = {
26
+ Core: "v1",
27
+ Apps: "apps/v1",
28
+ Batch: "batch/v1",
29
+ Networking: "networking.k8s.io/v1",
30
+ Policy: "policy/v1",
31
+ Rbac: "rbac.authorization.k8s.io/v1",
32
+ Storage: "storage.k8s.io/v1",
33
+ Autoscaling: "autoscaling/v2",
34
+ Admissionregistration: "admissionregistration.k8s.io/v1",
35
+ };
36
+ function resolveK8sGVK(entityType) {
37
+ const parts = entityType.split("::");
38
+ if (parts.length !== 3 || parts[0] !== "K8s")
39
+ return null;
40
+ const group = parts[1];
41
+ const kind = parts[2];
42
+ const apiVersion = API_GROUP_VERSIONS[group];
43
+ if (!apiVersion)
44
+ return null;
45
+ return { apiVersion, kind };
46
+ }
47
+ // ── Helm visitor ──────────────────────────────────────────
48
+ function helmVisitor() {
49
+ return {
50
+ attrRef: (name, _attr) => name,
51
+ resourceRef: (name) => name,
52
+ propertyDeclarable: (entity, walk) => {
53
+ if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
54
+ return undefined;
55
+ }
56
+ const props = entity.props;
57
+ const result = {};
58
+ for (const [key, value] of Object.entries(props)) {
59
+ if (value !== undefined) {
60
+ result[key] = walk(value);
61
+ }
62
+ }
63
+ return Object.keys(result).length > 0 ? result : undefined;
64
+ },
65
+ };
66
+ }
67
+ // ── YAML emission with Helm template support ──────────────
68
+ /**
69
+ * Emit an else chain, detecting nested `__helm_if` markers to produce
70
+ * `{{- else if <cond> }}` instead of `{{- else }}\n{{- if <cond> }}`.
71
+ */
72
+ function emitElseChain(elseBody, indent) {
73
+ if (typeof elseBody === "object" && elseBody !== null && HELM_IF_KEY in elseBody) {
74
+ const nested = elseBody;
75
+ const nestedCond = nested[HELM_IF_KEY];
76
+ const nestedBody = nested.body;
77
+ const nestedElse = nested.else;
78
+ let result = `\n{{- else if ${nestedCond} }}\n${emitHelmYAML(nestedBody, indent)}`;
79
+ if (nestedElse !== undefined) {
80
+ result += emitElseChain(nestedElse, indent);
81
+ }
82
+ return result;
83
+ }
84
+ return `\n{{- else }}\n${emitHelmYAML(elseBody, indent)}`;
85
+ }
86
+ /**
87
+ * Emit a YAML value, detecting __helm_tpl markers and emitting them
88
+ * as raw Go template expressions.
89
+ *
90
+ * @param valuesContext - When true, HelmTpl intrinsics are emitted as empty
91
+ * string placeholders instead of raw Go template expressions. values.yaml
92
+ * is not processed as a Go template by Helm, so {{ .Values.x }} is invalid
93
+ * there. Actual values are provided via -f override files at deploy time.
94
+ */
95
+ function emitHelmYAML(value, indent, valuesContext = false) {
96
+ const prefix = " ".repeat(indent);
97
+ if (value === null || value === undefined)
98
+ return "null";
99
+ if (typeof value === "boolean")
100
+ return value ? "true" : "false";
101
+ if (typeof value === "number")
102
+ return String(value);
103
+ // Detect HelmTpl / Intrinsic objects via INTRINSIC_MARKER before string check
104
+ if (typeof value === "object" && value !== null && INTRINSIC_MARKER in value) {
105
+ const tplObj = value;
106
+ if (valuesContext) {
107
+ // In values.yaml context: emit empty placeholder — actual value comes from override files
108
+ return "''";
109
+ }
110
+ const json = tplObj.toJSON();
111
+ if (typeof json === "object" && json !== null && HELM_TPL_KEY in json) {
112
+ return json[HELM_TPL_KEY];
113
+ }
114
+ return "''";
115
+ }
116
+ if (typeof value === "string") {
117
+ // Check if this is a raw template expression that was already inlined
118
+ if (value.startsWith("{{") && value.endsWith("}}")) {
119
+ return value;
120
+ }
121
+ if (value === "" || value === "true" || value === "false" || value === "null" ||
122
+ value === "yes" || value === "no" ||
123
+ value.includes(": ") || value.includes("#") ||
124
+ value.startsWith("*") || value.startsWith("&") || value.startsWith("!") ||
125
+ value.startsWith("{") || value.startsWith("[") ||
126
+ value.startsWith("'") || value.startsWith('"') || value.startsWith("$") ||
127
+ /^\d/.test(value)) {
128
+ return `'${value.replace(/'/g, "''")}'`;
129
+ }
130
+ return value;
131
+ }
132
+ if (Array.isArray(value)) {
133
+ if (value.length === 0)
134
+ return "[]";
135
+ const lines = [];
136
+ for (const item of value) {
137
+ if (typeof item === "object" && item !== null && !Array.isArray(item)) {
138
+ const entries = Object.entries(item);
139
+ if (entries.length > 0) {
140
+ const [firstKey, firstVal] = entries[0];
141
+ const firstEmitted = emitHelmYAML(firstVal, indent + 2, valuesContext);
142
+ if (firstEmitted.startsWith("\n")) {
143
+ lines.push(`${prefix}- ${firstKey}:${firstEmitted}`);
144
+ }
145
+ else {
146
+ lines.push(`${prefix}- ${firstKey}: ${firstEmitted}`);
147
+ }
148
+ for (let i = 1; i < entries.length; i++) {
149
+ const [key, val] = entries[i];
150
+ const emitted = emitHelmYAML(val, indent + 2, valuesContext);
151
+ if (emitted.startsWith("\n")) {
152
+ lines.push(`${prefix} ${key}:${emitted}`);
153
+ }
154
+ else {
155
+ lines.push(`${prefix} ${key}: ${emitted}`);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ else {
161
+ lines.push(`${prefix}- ${emitHelmYAML(item, indent + 1, valuesContext).trimStart()}`);
162
+ }
163
+ }
164
+ return "\n" + lines.join("\n");
165
+ }
166
+ if (typeof value === "object") {
167
+ const obj = value;
168
+ // Detect __helm_tpl marker → emit raw template expression (not valid in valuesContext)
169
+ if (HELM_TPL_KEY in obj && typeof obj[HELM_TPL_KEY] === "string") {
170
+ if (valuesContext)
171
+ return "''";
172
+ return obj[HELM_TPL_KEY];
173
+ }
174
+ // Detect __helm_if marker → emit conditional block
175
+ if (HELM_IF_KEY in obj) {
176
+ const condition = obj[HELM_IF_KEY];
177
+ const body = obj.body;
178
+ const elseBody = obj.else;
179
+ let result = `{{- if ${condition} }}\n${emitHelmYAML(body, indent, valuesContext)}`;
180
+ if (elseBody !== undefined) {
181
+ result += emitElseChain(elseBody, indent);
182
+ }
183
+ result += "\n{{- end }}";
184
+ return result;
185
+ }
186
+ // Detect __helm_range marker → emit range loop
187
+ if (HELM_RANGE_KEY in obj) {
188
+ const list = obj[HELM_RANGE_KEY];
189
+ const body = obj.body;
190
+ return `{{- range ${list} }}\n${emitHelmYAML(body, indent, valuesContext)}\n{{- end }}`;
191
+ }
192
+ // Detect __helm_with marker → emit with scope
193
+ if (HELM_WITH_KEY in obj) {
194
+ const scope = obj[HELM_WITH_KEY];
195
+ const body = obj.body;
196
+ return `{{- with ${scope} }}\n${emitHelmYAML(body, indent, valuesContext)}\n{{- end }}`;
197
+ }
198
+ const entries = Object.entries(obj);
199
+ if (entries.length === 0)
200
+ return "{}";
201
+ const lines = [];
202
+ for (const [key, val] of entries) {
203
+ const emitted = emitHelmYAML(val, indent + 1, valuesContext);
204
+ if (emitted.startsWith("\n")) {
205
+ lines.push(`${prefix}${key}:${emitted}`);
206
+ }
207
+ else {
208
+ lines.push(`${prefix}${key}: ${emitted}`);
209
+ }
210
+ }
211
+ return "\n" + lines.join("\n");
212
+ }
213
+ return String(value);
214
+ }
215
+ /**
216
+ * Emit a top-level key-value pair in Helm YAML.
217
+ */
218
+ function emitKeyValue(key, value, valuesContext = false) {
219
+ const yamlStr = emitHelmYAML(value, 1, valuesContext);
220
+ if (yamlStr.startsWith("\n")) {
221
+ return `${key}:${yamlStr}`;
222
+ }
223
+ return `${key}: ${yamlStr}`;
224
+ }
225
+ // ── Serializer ────────────────────────────────────────────
226
+ /**
227
+ * Specless K8s types whose properties live directly on the manifest.
228
+ */
229
+ const SPECLESS_TYPES = new Set([
230
+ "ConfigMap", "Secret", "Namespace", "ServiceAccount",
231
+ ]);
232
+ /**
233
+ * Convert a logical name to a kebab-case filename stem.
234
+ */
235
+ function toFileName(name) {
236
+ return name
237
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
238
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
239
+ .toLowerCase();
240
+ }
241
+ /**
242
+ * Generate Chart.yaml content from Helm::Chart entity props.
243
+ */
244
+ function emitChartYaml(props) {
245
+ const orderedKeys = [
246
+ "apiVersion", "name", "version", "kubeVersion", "description", "type",
247
+ "keywords", "home", "sources", "icon", "maintainers", "deprecated",
248
+ "annotations", "condition", "tags", "appVersion",
249
+ ];
250
+ const lines = [];
251
+ for (const key of orderedKeys) {
252
+ if (props[key] !== undefined) {
253
+ lines.push(emitKeyValue(key, props[key]));
254
+ }
255
+ }
256
+ for (const [key, val] of Object.entries(props)) {
257
+ if (!orderedKeys.includes(key) && val !== undefined) {
258
+ lines.push(emitKeyValue(key, val));
259
+ }
260
+ }
261
+ return lines.join("\n") + "\n";
262
+ }
263
+ /**
264
+ * Generate values.yaml content from Helm::Values entity props.
265
+ * HelmTpl intrinsics are emitted as empty placeholders since values.yaml
266
+ * is not processed as a Go template by Helm.
267
+ */
268
+ function emitValuesYaml(props) {
269
+ if (Object.keys(props).length === 0)
270
+ return "{}\n";
271
+ const lines = [];
272
+ for (const [key, val] of Object.entries(props)) {
273
+ lines.push(emitKeyValue(key, val, true));
274
+ }
275
+ return lines.join("\n") + "\n";
276
+ }
277
+ /**
278
+ * Collect all RuntimeSlot instances from a props tree.
279
+ * Returns the path (as array of keys) and description for each slot.
280
+ */
281
+ function collectRuntimeSlots(props, path) {
282
+ const result = [];
283
+ if (props === null || props === undefined)
284
+ return result;
285
+ if (typeof props === "object" && INTRINSIC_MARKER in props) {
286
+ const json = props.toJSON();
287
+ if (typeof json === "object" && json !== null && RUNTIME_SLOT_KEY in json) {
288
+ result.push({ path, description: json[RUNTIME_SLOT_KEY] });
289
+ }
290
+ return result;
291
+ }
292
+ if (typeof props === "object" && !Array.isArray(props)) {
293
+ for (const [key, value] of Object.entries(props)) {
294
+ result.push(...collectRuntimeSlots(value, [...path, key]));
295
+ }
296
+ }
297
+ return result;
298
+ }
299
+ /**
300
+ * Build a nested tree from flat path+description pairs.
301
+ */
302
+ function buildSlotTree(slots) {
303
+ const tree = {};
304
+ for (const { path, description } of slots) {
305
+ let node = tree;
306
+ for (let i = 0; i < path.length - 1; i++) {
307
+ const key = path[i];
308
+ if (typeof node[key] !== "object") {
309
+ node[key] = {};
310
+ }
311
+ node = node[key];
312
+ }
313
+ node[path[path.length - 1]] = description;
314
+ }
315
+ return tree;
316
+ }
317
+ /**
318
+ * Emit a slot tree as YAML lines with description comments before leaves.
319
+ */
320
+ function emitSlotTreeYaml(tree, indent) {
321
+ const prefix = " ".repeat(indent);
322
+ const lines = [];
323
+ for (const [key, value] of Object.entries(tree)) {
324
+ if (typeof value === "string") {
325
+ if (value) {
326
+ lines.push(`${prefix}# ${value}`);
327
+ }
328
+ lines.push(`${prefix}${key}: ''`);
329
+ }
330
+ else {
331
+ lines.push(`${prefix}${key}:`);
332
+ const nested = emitSlotTreeYaml(value, indent + 1);
333
+ if (nested)
334
+ lines.push(nested);
335
+ }
336
+ }
337
+ return lines.join("\n");
338
+ }
339
+ /**
340
+ * Generate values-runtime-slots.yaml from collected RuntimeSlot instances.
341
+ * Returns empty string if no slots found.
342
+ */
343
+ function emitRuntimeSlotsYaml(slots) {
344
+ if (slots.length === 0)
345
+ return "";
346
+ const tree = buildSlotTree(slots);
347
+ return "# Generated by chant — fill these in before running helm upgrade\n" +
348
+ emitSlotTreeYaml(tree, 0) + "\n";
349
+ }
350
+ /**
351
+ * Description inference — static map from common Helm value key names.
352
+ */
353
+ const KEY_DESCRIPTIONS = {
354
+ replicaCount: "Number of pod replicas",
355
+ image: "Container image configuration",
356
+ repository: "Image repository",
357
+ tag: "Image tag (empty defaults to Chart.appVersion)",
358
+ pullPolicy: "Image pull policy",
359
+ port: "Service port number",
360
+ enabled: "Whether this feature is enabled",
361
+ resources: "Container resource requests and limits",
362
+ service: "Kubernetes Service configuration",
363
+ type: "Service type",
364
+ ingress: "Ingress configuration",
365
+ className: "Ingress class name",
366
+ hosts: "Ingress host rules",
367
+ tls: "Ingress TLS configuration",
368
+ autoscaling: "Horizontal pod autoscaling configuration",
369
+ minReplicas: "Minimum number of replicas",
370
+ maxReplicas: "Maximum number of replicas",
371
+ targetCPUUtilizationPercentage: "Target CPU utilization for autoscaling",
372
+ targetMemoryUtilizationPercentage: "Target memory utilization for autoscaling",
373
+ serviceAccount: "Service account configuration",
374
+ create: "Whether to create the resource",
375
+ name: "Resource name override",
376
+ annotations: "Additional annotations",
377
+ nodeSelector: "Node selector constraints",
378
+ tolerations: "Pod tolerations",
379
+ affinity: "Pod affinity rules",
380
+ podSecurityContext: "Pod-level security context",
381
+ securityContext: "Container-level security context",
382
+ livenessProbe: "Liveness probe configuration",
383
+ readinessProbe: "Readiness probe configuration",
384
+ persistence: "Persistent storage configuration",
385
+ size: "Storage size",
386
+ storageClass: "Storage class name",
387
+ config: "Application configuration",
388
+ schedule: "Cron schedule expression",
389
+ fullnameOverride: "Override the full release name",
390
+ nameOverride: "Override the chart name",
391
+ };
392
+ /**
393
+ * Enum detection — known string enums keyed by `parentKey.key` or just `key`.
394
+ */
395
+ const KEY_ENUMS = {
396
+ pullPolicy: ["Always", "IfNotPresent", "Never"],
397
+ "service.type": ["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"],
398
+ "ingress.pathType": ["Prefix", "Exact", "ImplementationSpecific"],
399
+ restartPolicy: ["Always", "OnFailure", "Never"],
400
+ "updateStrategy.type": ["RollingUpdate", "Recreate"],
401
+ };
402
+ /**
403
+ * Numeric constraints keyed by key name.
404
+ */
405
+ const KEY_NUMERIC_CONSTRAINTS = {
406
+ replicaCount: { minimum: 0 },
407
+ port: { minimum: 1, maximum: 65535 },
408
+ containerPort: { minimum: 1, maximum: 65535 },
409
+ minReplicas: { minimum: 1 },
410
+ maxReplicas: { minimum: 1 },
411
+ targetCPUUtilizationPercentage: { minimum: 1, maximum: 100 },
412
+ targetMemoryUtilizationPercentage: { minimum: 1, maximum: 100 },
413
+ };
414
+ /**
415
+ * Generate values.schema.json from values defaults.
416
+ */
417
+ function generateValuesSchema(props) {
418
+ function inferType(value, includeDefault = true, keyName, parentKeyName) {
419
+ if (value === null || value === undefined)
420
+ return { type: "null" };
421
+ if (typeof value === "boolean") {
422
+ const schema = { type: "boolean" };
423
+ if (includeDefault)
424
+ schema.default = value;
425
+ if (keyName && KEY_DESCRIPTIONS[keyName])
426
+ schema.description = KEY_DESCRIPTIONS[keyName];
427
+ return schema;
428
+ }
429
+ if (typeof value === "number") {
430
+ const schema = {
431
+ type: Number.isInteger(value) ? "integer" : "number",
432
+ };
433
+ if (includeDefault)
434
+ schema.default = value;
435
+ if (keyName && KEY_DESCRIPTIONS[keyName])
436
+ schema.description = KEY_DESCRIPTIONS[keyName];
437
+ if (keyName && KEY_NUMERIC_CONSTRAINTS[keyName]) {
438
+ const constraints = KEY_NUMERIC_CONSTRAINTS[keyName];
439
+ if (constraints.minimum !== undefined)
440
+ schema.minimum = constraints.minimum;
441
+ if (constraints.maximum !== undefined)
442
+ schema.maximum = constraints.maximum;
443
+ }
444
+ return schema;
445
+ }
446
+ if (typeof value === "string") {
447
+ const schema = { type: "string" };
448
+ if (includeDefault && value !== "")
449
+ schema.default = value;
450
+ if (keyName && KEY_DESCRIPTIONS[keyName])
451
+ schema.description = KEY_DESCRIPTIONS[keyName];
452
+ // Check enum — qualified key first, then bare key
453
+ const qualifiedKey = parentKeyName ? `${parentKeyName}.${keyName}` : undefined;
454
+ const enumValues = (qualifiedKey && KEY_ENUMS[qualifiedKey]) || (keyName && KEY_ENUMS[keyName]);
455
+ if (enumValues)
456
+ schema.enum = enumValues;
457
+ return schema;
458
+ }
459
+ if (Array.isArray(value)) {
460
+ const schema = {
461
+ type: "array",
462
+ items: value.length > 0 ? inferType(value[0], false) : {},
463
+ };
464
+ if (includeDefault && value.length > 0)
465
+ schema.default = value;
466
+ if (keyName && KEY_DESCRIPTIONS[keyName])
467
+ schema.description = KEY_DESCRIPTIONS[keyName];
468
+ return schema;
469
+ }
470
+ if (typeof value === "object" && value !== null && INTRINSIC_MARKER in value) {
471
+ // HelmTpl / Intrinsic — actual value provided via -f override; accept any type
472
+ return {};
473
+ }
474
+ if (typeof value === "object") {
475
+ const obj = value;
476
+ const properties = {};
477
+ const requiredFields = [];
478
+ for (const [k, v] of Object.entries(obj)) {
479
+ properties[k] = inferType(v, true, k, keyName);
480
+ // Non-null, non-empty-string defaults suggest the field is expected
481
+ if (v !== null && v !== undefined && v !== "" && v !== false) {
482
+ requiredFields.push(k);
483
+ }
484
+ }
485
+ const schema = {
486
+ type: "object",
487
+ properties,
488
+ };
489
+ if (keyName && KEY_DESCRIPTIONS[keyName])
490
+ schema.description = KEY_DESCRIPTIONS[keyName];
491
+ if (requiredFields.length > 0) {
492
+ schema.required = requiredFields;
493
+ }
494
+ return schema;
495
+ }
496
+ return {};
497
+ }
498
+ const topLevel = inferType(props, false, undefined, undefined);
499
+ return JSON.stringify({ $schema: "http://json-schema.org/draft-07/schema#", ...topLevel }, null, 2) + "\n";
500
+ }
501
+ /**
502
+ * Emit a K8s resource as a Helm template YAML file.
503
+ */
504
+ function emitK8sTemplate(name, entityType, props, entityNames, hookAnnotations) {
505
+ let gvk = resolveK8sGVK(entityType);
506
+ // Fallback: extract apiVersion/kind from props for CRD-based resources
507
+ if (!gvk && props.apiVersion && props.kind) {
508
+ gvk = {
509
+ apiVersion: props.apiVersion,
510
+ kind: props.kind,
511
+ };
512
+ }
513
+ if (!gvk)
514
+ return "";
515
+ const walked = walkValue(props, entityNames, helmVisitor());
516
+ if (!walked)
517
+ return "";
518
+ const manifest = {
519
+ apiVersion: gvk.apiVersion,
520
+ kind: gvk.kind,
521
+ };
522
+ // Build metadata
523
+ const metadata = walked.metadata ?? {};
524
+ if (!metadata.name) {
525
+ metadata.name = toFileName(name);
526
+ }
527
+ // Add hook annotations
528
+ if (hookAnnotations) {
529
+ const existing = (metadata.annotations ?? {});
530
+ metadata.annotations = { ...existing, ...hookAnnotations };
531
+ }
532
+ manifest.metadata = metadata;
533
+ // Build spec / specless body
534
+ if (SPECLESS_TYPES.has(gvk.kind)) {
535
+ for (const [key, value] of Object.entries(walked)) {
536
+ if (key !== "metadata")
537
+ manifest[key] = value;
538
+ }
539
+ }
540
+ else if (walked.spec !== undefined) {
541
+ manifest.spec = walked.spec;
542
+ for (const [key, value] of Object.entries(walked)) {
543
+ if (key !== "metadata" && key !== "spec")
544
+ manifest[key] = value;
545
+ }
546
+ }
547
+ else {
548
+ const spec = {};
549
+ for (const [key, value] of Object.entries(walked)) {
550
+ if (key !== "metadata")
551
+ spec[key] = value;
552
+ }
553
+ if (Object.keys(spec).length > 0)
554
+ manifest.spec = spec;
555
+ }
556
+ // Emit as YAML
557
+ const orderedKeys = ["apiVersion", "kind", "metadata", "spec"];
558
+ const lines = [];
559
+ for (const key of orderedKeys) {
560
+ if (manifest[key] !== undefined) {
561
+ lines.push(emitKeyValue(key, manifest[key]));
562
+ }
563
+ }
564
+ for (const [key, value] of Object.entries(manifest)) {
565
+ if (!orderedKeys.includes(key) && value !== undefined) {
566
+ lines.push(emitKeyValue(key, value));
567
+ }
568
+ }
569
+ return lines.join("\n") + "\n";
570
+ }
571
+ /**
572
+ * Generate .helmignore content.
573
+ */
574
+ function emitHelmignore() {
575
+ return `# Patterns to ignore when building packages.
576
+ .DS_Store
577
+ .git
578
+ .gitignore
579
+ .bzr
580
+ .bzrignore
581
+ .hg
582
+ .hgignore
583
+ .svn
584
+ *.swp
585
+ *.bak
586
+ *.tmp
587
+ *.orig
588
+ *~
589
+ .project
590
+ .idea
591
+ *.tmproj
592
+ .vscode
593
+ `;
594
+ }
595
+ // ── Conditional / intrinsic detection ──────────────────────
596
+ /**
597
+ * Check if a value is a HelmConditional (If wrapper).
598
+ */
599
+ function isHelmConditional(value) {
600
+ if (typeof value !== "object" || value === null)
601
+ return false;
602
+ return INTRINSIC_MARKER in value && "condition" in value && "body" in value;
603
+ }
604
+ /**
605
+ * Check if a value is a Declarable with an entityType.
606
+ */
607
+ function hasEntityType(value) {
608
+ return typeof value === "object" && value !== null && "entityType" in value &&
609
+ typeof value.entityType === "string";
610
+ }
611
+ /**
612
+ * Emit a Chart.yaml dependencies block from HelmDependency property entities.
613
+ */
614
+ /**
615
+ * Key mapping for dependency props that differ between JS and YAML.
616
+ */
617
+ const DEP_KEY_MAP = {
618
+ importValues: "import-values",
619
+ };
620
+ function emitDependencies(deps) {
621
+ const lines = ["dependencies:"];
622
+ for (const dep of deps) {
623
+ const orderedKeys = ["name", "version", "repository", "condition", "tags", "enabled", "importValues", "alias"];
624
+ const entries = [];
625
+ for (const key of orderedKeys) {
626
+ if (dep[key] !== undefined)
627
+ entries.push([DEP_KEY_MAP[key] ?? key, dep[key]]);
628
+ }
629
+ for (const [key, val] of Object.entries(dep)) {
630
+ if (!orderedKeys.includes(key) && val !== undefined)
631
+ entries.push([key, val]);
632
+ }
633
+ if (entries.length > 0) {
634
+ const [firstKey, firstVal] = entries[0];
635
+ lines.push(` - ${firstKey}: ${emitHelmYAML(firstVal, 2).trimStart()}`);
636
+ for (let i = 1; i < entries.length; i++) {
637
+ const [key, val] = entries[i];
638
+ const emitted = emitHelmYAML(val, 2);
639
+ if (emitted.startsWith("\n")) {
640
+ lines.push(` ${key}:${emitted}`);
641
+ }
642
+ else {
643
+ lines.push(` ${key}: ${emitted}`);
644
+ }
645
+ }
646
+ }
647
+ }
648
+ return lines.join("\n") + "\n";
649
+ }
650
+ /**
651
+ * Emit a Chart.yaml maintainers block from HelmMaintainer property entities.
652
+ */
653
+ function emitMaintainers(maintainers) {
654
+ const lines = ["maintainers:"];
655
+ for (const m of maintainers) {
656
+ const orderedKeys = ["name", "email", "url"];
657
+ const entries = [];
658
+ for (const key of orderedKeys) {
659
+ if (m[key] !== undefined)
660
+ entries.push([key, m[key]]);
661
+ }
662
+ for (const [key, val] of Object.entries(m)) {
663
+ if (!orderedKeys.includes(key) && val !== undefined)
664
+ entries.push([key, val]);
665
+ }
666
+ if (entries.length > 0) {
667
+ const [firstKey, firstVal] = entries[0];
668
+ lines.push(` - ${firstKey}: ${emitHelmYAML(firstVal, 2).trimStart()}`);
669
+ for (let i = 1; i < entries.length; i++) {
670
+ const [key, val] = entries[i];
671
+ const emitted = emitHelmYAML(val, 2);
672
+ if (emitted.startsWith("\n")) {
673
+ lines.push(` ${key}:${emitted}`);
674
+ }
675
+ else {
676
+ lines.push(` ${key}: ${emitted}`);
677
+ }
678
+ }
679
+ }
680
+ }
681
+ return lines.join("\n") + "\n";
682
+ }
683
+ /**
684
+ * Helm chart serializer implementation.
685
+ */
686
+ export const helmSerializer = {
687
+ name: "helm",
688
+ rulePrefix: "WHM",
689
+ serialize(entities, _outputs) {
690
+ const entityNames = new Map();
691
+ for (const [name, entity] of entities) {
692
+ entityNames.set(entity, name);
693
+ }
694
+ const files = {};
695
+ let chartName = "my-chart";
696
+ let chartProps = {};
697
+ let valuesProps = {};
698
+ let hasValues = false;
699
+ let notesContent;
700
+ const dependencies = [];
701
+ const maintainers = [];
702
+ const valuesOverrides = [];
703
+ // First pass: extract Helm-specific resources and collect metadata
704
+ for (const [_name, entity] of entities) {
705
+ if (!hasEntityType(entity))
706
+ continue;
707
+ const entityType = entity.entityType;
708
+ if (entityType === "Helm::Chart") {
709
+ const props = entity.props;
710
+ chartProps = props ?? {};
711
+ chartName = chartProps.name ?? chartName;
712
+ }
713
+ else if (entityType === "Helm::Values") {
714
+ const props = entity.props;
715
+ valuesProps = props ?? {};
716
+ hasValues = true;
717
+ }
718
+ else if (entityType === "Helm::Notes") {
719
+ const props = entity.props;
720
+ notesContent = props?.content ?? "";
721
+ }
722
+ else if (entityType === "Helm::Dependency") {
723
+ const props = entity.props;
724
+ if (props)
725
+ dependencies.push(props);
726
+ }
727
+ else if (entityType === "Helm::Maintainer") {
728
+ const props = entity.props;
729
+ if (props)
730
+ maintainers.push(props);
731
+ }
732
+ else if (entityType === "Helm::CRD") {
733
+ const props = entity.props;
734
+ if (props?.content) {
735
+ const filename = props.filename ?? `${toFileName(_name)}.yaml`;
736
+ files[`crds/${filename}`] = props.content;
737
+ }
738
+ }
739
+ else if (entityType === "Helm::ValuesOverride") {
740
+ const props = entity.props;
741
+ if (props?.filename && props?.values) {
742
+ valuesOverrides.push({
743
+ filename: props.filename,
744
+ values: props.values,
745
+ });
746
+ }
747
+ }
748
+ }
749
+ // Emit Chart.yaml
750
+ if (!chartProps.apiVersion)
751
+ chartProps.apiVersion = "v2";
752
+ if (!chartProps.name)
753
+ chartProps.name = chartName;
754
+ if (!chartProps.version)
755
+ chartProps.version = "0.1.0";
756
+ if (!chartProps.type)
757
+ chartProps.type = "application";
758
+ // Inject collected maintainers into chart props for ordered emission
759
+ if (maintainers.length > 0) {
760
+ chartProps.maintainers = maintainers;
761
+ }
762
+ let chartYaml = emitChartYaml(chartProps);
763
+ if (dependencies.length > 0) {
764
+ chartYaml += emitDependencies(dependencies);
765
+ }
766
+ files["Chart.yaml"] = chartYaml;
767
+ // Emit values.yaml
768
+ files["values.yaml"] = emitValuesYaml(valuesProps);
769
+ // Emit values-runtime-slots.yaml if any RuntimeSlot instances found
770
+ if (hasValues) {
771
+ const slots = collectRuntimeSlots(valuesProps, []);
772
+ if (slots.length > 0) {
773
+ files["values-runtime-slots.yaml"] = emitRuntimeSlotsYaml(slots);
774
+ }
775
+ }
776
+ // Emit ValuesOverride files
777
+ for (const override of valuesOverrides) {
778
+ files[`${override.filename}.yaml`] = emitValuesYaml(override.values);
779
+ }
780
+ // Emit values.schema.json if we have values
781
+ if (hasValues && Object.keys(valuesProps).length > 0) {
782
+ files["values.schema.json"] = generateValuesSchema(valuesProps);
783
+ }
784
+ // Emit .helmignore
785
+ files[".helmignore"] = emitHelmignore();
786
+ // Emit _helpers.tpl
787
+ files["templates/_helpers.tpl"] = generateHelpers({ chartName });
788
+ // Second pass: emit K8s resources as templates
789
+ for (const [name, entity] of entities) {
790
+ const raw = entity;
791
+ // Handle resource-level If(condition, resource) — HelmConditional wrapping a Declarable
792
+ if (isHelmConditional(raw)) {
793
+ const conditional = raw;
794
+ const innerEntity = conditional.body;
795
+ if (hasEntityType(innerEntity) && innerEntity.entityType.startsWith("K8s::")) {
796
+ const entityType = innerEntity.entityType;
797
+ const props = innerEntity.props;
798
+ if (props) {
799
+ const fileName = toFileName(name);
800
+ const templateContent = emitK8sTemplate(name, entityType, props, entityNames);
801
+ if (templateContent) {
802
+ files[`templates/${fileName}.yaml`] =
803
+ `{{- if ${conditional.condition} }}\n${templateContent}{{- end }}\n`;
804
+ }
805
+ }
806
+ }
807
+ continue;
808
+ }
809
+ if (!hasEntityType(raw))
810
+ continue;
811
+ const entityType = raw.entityType;
812
+ if (isPropertyDeclarable(entity))
813
+ continue;
814
+ // Skip Helm-specific resources (already handled)
815
+ if (entityType.startsWith("Helm::"))
816
+ continue;
817
+ // Handle K8s resources
818
+ if (entityType.startsWith("K8s::")) {
819
+ const props = raw.props;
820
+ if (!props)
821
+ continue;
822
+ const fileName = toFileName(name);
823
+ const templateContent = emitK8sTemplate(name, entityType, props, entityNames);
824
+ if (templateContent) {
825
+ files[`templates/${fileName}.yaml`] = templateContent;
826
+ }
827
+ }
828
+ }
829
+ // Handle Helm::Test resources
830
+ for (const [name, entity] of entities) {
831
+ if (!hasEntityType(entity))
832
+ continue;
833
+ const entityType = entity.entityType;
834
+ if (entityType === "Helm::Test") {
835
+ const props = entity.props;
836
+ if (!props)
837
+ continue;
838
+ const fileName = toFileName(name);
839
+ // If the test has a nested K8s Declarable as `resource`, use it
840
+ const resource = props.resource;
841
+ if (hasEntityType(resource) && resource.entityType.startsWith("K8s::")) {
842
+ const resType = resource.entityType;
843
+ const resProps = resource.props;
844
+ if (resProps) {
845
+ const templateContent = emitK8sTemplate(name, resType, resProps, entityNames, { "helm.sh/hook": "test" });
846
+ if (templateContent) {
847
+ files[`templates/tests/${fileName}.yaml`] = templateContent;
848
+ }
849
+ }
850
+ }
851
+ else {
852
+ // Treat props directly as Pod spec
853
+ const templateContent = emitK8sTemplate(name, "K8s::Core::Pod", props, entityNames, { "helm.sh/hook": "test" });
854
+ if (templateContent) {
855
+ files[`templates/tests/${fileName}.yaml`] = templateContent;
856
+ }
857
+ }
858
+ }
859
+ }
860
+ // Handle Helm::Hook property wrappers (property kind, but we iterate all)
861
+ for (const [name, entity] of entities) {
862
+ if (!hasEntityType(entity))
863
+ continue;
864
+ const entityType = entity.entityType;
865
+ if (entityType === "Helm::Hook") {
866
+ const props = entity.props;
867
+ if (!props)
868
+ continue;
869
+ const hook = props.hook;
870
+ const weight = props.weight;
871
+ const deletePolicy = props.deletePolicy;
872
+ const resource = props.resource;
873
+ if (resource && hasEntityType(resource)) {
874
+ const resType = resource.entityType;
875
+ const resProps = resource.props;
876
+ if (resProps && resType) {
877
+ const hookAnnotations = {
878
+ "helm.sh/hook": hook,
879
+ };
880
+ if (weight !== undefined)
881
+ hookAnnotations["helm.sh/hook-weight"] = String(weight);
882
+ if (deletePolicy)
883
+ hookAnnotations["helm.sh/hook-delete-policy"] = deletePolicy;
884
+ const fileName = toFileName(name);
885
+ const templateContent = emitK8sTemplate(name, resType, resProps, entityNames, hookAnnotations);
886
+ if (templateContent) {
887
+ files[`templates/${fileName}.yaml`] = templateContent;
888
+ }
889
+ }
890
+ }
891
+ }
892
+ }
893
+ // Emit NOTES.txt
894
+ if (notesContent) {
895
+ files["templates/NOTES.txt"] = notesContent;
896
+ }
897
+ // Primary content is Chart.yaml (used as the build output identifier)
898
+ return {
899
+ primary: files["Chart.yaml"],
900
+ files,
901
+ };
902
+ },
903
+ };