@intentius/chant-lexicon-helm 0.0.18 → 0.0.24

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 (55) hide show
  1. package/dist/integrity.json +5 -5
  2. package/dist/manifest.json +1 -1
  3. package/dist/skills/chant-helm.md +447 -0
  4. package/package.json +21 -3
  5. package/src/codegen/docs.test.ts +14 -0
  6. package/src/codegen/generate.test.ts +71 -0
  7. package/src/codegen/package.test.ts +36 -0
  8. package/src/composites/composites.test.ts +116 -110
  9. package/src/composites/helm-batch-job.ts +33 -19
  10. package/src/composites/helm-crd-lifecycle.ts +37 -24
  11. package/src/composites/helm-cron-job.ts +25 -13
  12. package/src/composites/helm-daemon-set.ts +26 -14
  13. package/src/composites/helm-external-secret.ts +21 -12
  14. package/src/composites/helm-library.ts +21 -7
  15. package/src/composites/helm-microservice.ts +46 -29
  16. package/src/composites/helm-monitored-service.ts +75 -51
  17. package/src/composites/helm-namespace-env.ts +80 -52
  18. package/src/composites/helm-secure-ingress.ts +66 -50
  19. package/src/composites/helm-stateful-service.ts +29 -16
  20. package/src/composites/helm-web-app.ts +37 -22
  21. package/src/composites/helm-worker.ts +34 -20
  22. package/src/import/roundtrip.test.ts +144 -0
  23. package/src/lint/post-synth/whm101.test.ts +69 -0
  24. package/src/lint/post-synth/whm102.test.ts +57 -0
  25. package/src/lint/post-synth/whm103.test.ts +58 -0
  26. package/src/lint/post-synth/whm104.test.ts +57 -0
  27. package/src/lint/post-synth/whm105.test.ts +41 -0
  28. package/src/lint/post-synth/whm201.test.ts +59 -0
  29. package/src/lint/post-synth/whm202.test.ts +62 -0
  30. package/src/lint/post-synth/whm203.test.ts +58 -0
  31. package/src/lint/post-synth/whm204.test.ts +56 -0
  32. package/src/lint/post-synth/whm301.test.ts +49 -0
  33. package/src/lint/post-synth/whm302.test.ts +58 -0
  34. package/src/lint/post-synth/whm401.test.ts +59 -0
  35. package/src/lint/post-synth/whm402.test.ts +58 -0
  36. package/src/lint/post-synth/whm403.test.ts +50 -0
  37. package/src/lint/post-synth/whm404.test.ts +50 -0
  38. package/src/lint/post-synth/whm405.test.ts +60 -0
  39. package/src/lint/post-synth/whm406.test.ts +43 -0
  40. package/src/lint/post-synth/whm407.test.ts +60 -0
  41. package/src/lint/post-synth/whm501.test.ts +70 -0
  42. package/src/lint/post-synth/whm502.test.ts +72 -0
  43. package/src/lint/rules/chart-metadata.test.ts +45 -0
  44. package/src/lint/rules/no-hardcoded-image.test.ts +41 -0
  45. package/src/lint/rules/values-no-secrets.test.ts +48 -0
  46. package/src/plugin.test.ts +3 -3
  47. package/src/plugin.ts +190 -19
  48. package/src/resources.ts +29 -0
  49. package/src/skills/chant-helm.md +447 -0
  50. package/dist/skills/chant-helm-create-chart.md +0 -211
  51. package/src/skills/create-chart.md +0 -211
  52. /package/dist/skills/{chant-helm-chart-patterns.md → chant-helm-patterns.md} +0 -0
  53. /package/dist/skills/{chant-helm-chart-security-patterns.md → chant-helm-security.md} +0 -0
  54. /package/src/skills/{chart-patterns.md → chant-helm-patterns.md} +0 -0
  55. /package/src/skills/{chart-security-patterns.md → chant-helm-security.md} +0 -0
@@ -0,0 +1,45 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import type { LintContext } from "@intentius/chant/lint/rule";
4
+ import { chartMetadataRule } from "./chart-metadata";
5
+
6
+ function makeContext(code: string): LintContext {
7
+ const sourceFile = ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath: "test.ts" };
9
+ }
10
+
11
+ describe("WHM001: chartMetadataRule", () => {
12
+ test("passes when all required fields present", () => {
13
+ const ctx = makeContext(`new Chart({ apiVersion: "v2", name: "my-app", version: "0.1.0" });`);
14
+ expect(chartMetadataRule.check(ctx)).toHaveLength(0);
15
+ });
16
+
17
+ test("fails when name is missing", () => {
18
+ const ctx = makeContext(`new Chart({ apiVersion: "v2", version: "0.1.0" });`);
19
+ const diags = chartMetadataRule.check(ctx);
20
+ expect(diags).toHaveLength(1);
21
+ expect(diags[0].ruleId).toBe("WHM001");
22
+ expect(diags[0].message).toContain("name");
23
+ });
24
+
25
+ test("fails when all fields are missing", () => {
26
+ const ctx = makeContext(`new Chart({});`);
27
+ const diags = chartMetadataRule.check(ctx);
28
+ expect(diags).toHaveLength(1);
29
+ expect(diags[0].message).toContain("name");
30
+ expect(diags[0].message).toContain("version");
31
+ expect(diags[0].message).toContain("apiVersion");
32
+ });
33
+
34
+ test("ignores non-Chart constructors", () => {
35
+ const ctx = makeContext(`new Deployment({ name: "test" });`);
36
+ expect(chartMetadataRule.check(ctx)).toHaveLength(0);
37
+ });
38
+
39
+ test("fails when version is missing", () => {
40
+ const ctx = makeContext(`new Chart({ apiVersion: "v2", name: "my-app" });`);
41
+ const diags = chartMetadataRule.check(ctx);
42
+ expect(diags).toHaveLength(1);
43
+ expect(diags[0].message).toContain("version");
44
+ });
45
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import type { LintContext } from "@intentius/chant/lint/rule";
4
+ import { noHardcodedImageRule } from "./no-hardcoded-image";
5
+
6
+ function makeContext(code: string): LintContext {
7
+ const sourceFile = ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath: "test.ts" };
9
+ }
10
+
11
+ describe("WHM003: noHardcodedImageRule", () => {
12
+ test("warns on hardcoded image with tag", () => {
13
+ const ctx = makeContext(`new Deployment({ spec: { containers: [{ image: "nginx:1.19" }] } });`);
14
+ const diags = noHardcodedImageRule.check(ctx);
15
+ expect(diags).toHaveLength(1);
16
+ expect(diags[0].ruleId).toBe("WHM003");
17
+ expect(diags[0].message).toContain("nginx:1.19");
18
+ });
19
+
20
+ test("warns on registry/image:tag format", () => {
21
+ const ctx = makeContext(`({ image: "registry.io/app:latest" })`);
22
+ const diags = noHardcodedImageRule.check(ctx);
23
+ expect(diags).toHaveLength(1);
24
+ });
25
+
26
+ test("passes when image is not a string literal (intrinsic)", () => {
27
+ const ctx = makeContext(`({ image: printf("%s:%s", values.image.repo, values.image.tag) })`);
28
+ expect(noHardcodedImageRule.check(ctx)).toHaveLength(0);
29
+ });
30
+
31
+ test("passes for image key without colon-tag pattern", () => {
32
+ const ctx = makeContext(`({ image: "just-a-name" })`);
33
+ expect(noHardcodedImageRule.check(ctx)).toHaveLength(0);
34
+ });
35
+
36
+ test("warns on multi-segment registry path", () => {
37
+ const ctx = makeContext(`({ image: "gcr.io/my-project/my-app:v1.0" })`);
38
+ const diags = noHardcodedImageRule.check(ctx);
39
+ expect(diags).toHaveLength(1);
40
+ });
41
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import type { LintContext } from "@intentius/chant/lint/rule";
4
+ import { valuesNoSecretsRule } from "./values-no-secrets";
5
+
6
+ function makeContext(code: string): LintContext {
7
+ const sourceFile = ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath: "test.ts" };
9
+ }
10
+
11
+ describe("WHM002: valuesNoSecretsRule", () => {
12
+ test("passes with empty values", () => {
13
+ const ctx = makeContext(`new Values({});`);
14
+ expect(valuesNoSecretsRule.check(ctx)).toHaveLength(0);
15
+ });
16
+
17
+ test("warns on hardcoded password", () => {
18
+ const ctx = makeContext(`new Values({ password: "hunter2" });`);
19
+ const diags = valuesNoSecretsRule.check(ctx);
20
+ expect(diags).toHaveLength(1);
21
+ expect(diags[0].ruleId).toBe("WHM002");
22
+ expect(diags[0].message).toContain("password");
23
+ });
24
+
25
+ test("warns on hardcoded secret in nested object", () => {
26
+ const ctx = makeContext(`new Values({ db: { secret: "s3cret" } });`);
27
+ const diags = valuesNoSecretsRule.check(ctx);
28
+ expect(diags).toHaveLength(1);
29
+ expect(diags[0].message).toContain("secret");
30
+ });
31
+
32
+ test("passes when secret value is empty", () => {
33
+ const ctx = makeContext(`new Values({ password: "" });`);
34
+ expect(valuesNoSecretsRule.check(ctx)).toHaveLength(0);
35
+ });
36
+
37
+ test("passes for non-sensitive keys", () => {
38
+ const ctx = makeContext(`new Values({ replicaCount: 3, name: "test" });`);
39
+ expect(valuesNoSecretsRule.check(ctx)).toHaveLength(0);
40
+ });
41
+
42
+ test("warns on hardcoded token", () => {
43
+ const ctx = makeContext(`new Values({ token: "abc123" });`);
44
+ const diags = valuesNoSecretsRule.check(ctx);
45
+ expect(diags).toHaveLength(1);
46
+ expect(diags[0].message).toContain("token");
47
+ });
48
+ });
@@ -64,8 +64,8 @@ describe("helmPlugin", () => {
64
64
  expect(Array.isArray(skills)).toBe(true);
65
65
  expect(skills.length).toBeGreaterThanOrEqual(3);
66
66
  const names = skills.map((s) => s.name);
67
- expect(names).toContain("chant-helm-create-chart");
68
- expect(names).toContain("chant-helm-chart-patterns");
69
- expect(names).toContain("chant-helm-chart-security-patterns");
67
+ expect(names).toContain("chant-helm");
68
+ expect(names).toContain("chant-helm-patterns");
69
+ expect(names).toContain("chant-helm-security");
70
70
  });
71
71
  });
package/src/plugin.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  * Helm-specific intrinsics, lint rules, and post-synth checks.
6
6
  */
7
7
 
8
- import type { LexiconPlugin, IntrinsicDef, InitTemplateSet, SkillDefinition } from "@intentius/chant/lexicon";
8
+ import type { LexiconPlugin, IntrinsicDef, InitTemplateSet } from "@intentius/chant/lexicon";
9
9
  import { discoverLintRules, discoverPostSynthChecks } from "@intentius/chant/lint/discover";
10
- import { readFileSync, readdirSync } from "fs";
10
+ import { createSkillsLoader, createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
11
11
  import { join, dirname } from "path";
12
12
  import { fileURLToPath } from "url";
13
13
  import { helmSerializer } from "./serializer";
@@ -64,7 +64,190 @@ export const helmPlugin: LexiconPlugin = {
64
64
  return false;
65
65
  },
66
66
 
67
+ mcpTools() {
68
+ return [createDiffTool(helmSerializer, "Compare current Helm chart build output against previous output")];
69
+ },
70
+
71
+ mcpResources() {
72
+ return [
73
+ {
74
+ uri: "resource-catalog",
75
+ name: "Helm Chart Resource Catalog",
76
+ description: "JSON list of all supported Helm chart resource types",
77
+ mimeType: "application/json",
78
+ async handler(): Promise<string> {
79
+ const { readFileSync } = await import("fs");
80
+ const { join, dirname } = await import("path");
81
+ const { fileURLToPath } = await import("url");
82
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
83
+ try {
84
+ const lexicon = JSON.parse(readFileSync(join(pkgDir, "dist", "meta.json"), "utf-8")) as Record<string, { resourceType: string; kind: string }>;
85
+ const entries = Object.entries(lexicon).map(([className, entry]) => ({
86
+ className,
87
+ resourceType: entry.resourceType,
88
+ kind: entry.kind,
89
+ }));
90
+ return JSON.stringify(entries);
91
+ } catch {
92
+ return JSON.stringify([{ className: "Chart", kind: "resource" }, { className: "Values", kind: "resource" }, { className: "HelmNotes", kind: "resource" }]);
93
+ }
94
+ },
95
+ },
96
+ {
97
+ uri: "examples/web-app",
98
+ name: "Web App Helm Chart Example",
99
+ description: "A basic web application Helm chart with Deployment, Service, and Ingress",
100
+ mimeType: "text/typescript",
101
+ async handler(): Promise<string> {
102
+ return `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
103
+ import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
104
+ import { Deployment, Service, Ingress } from "@intentius/chant-lexicon-k8s";
105
+
106
+ export const chart = new Chart({
107
+ apiVersion: "v2",
108
+ name: "web-app",
109
+ version: "0.1.0",
110
+ appVersion: "1.0.0",
111
+ type: "application",
112
+ description: "A web application chart",
113
+ });
114
+
115
+ export const valuesSchema = new Values({
116
+ replicaCount: 2,
117
+ image: { repository: "nginx", tag: "latest", pullPolicy: "IfNotPresent" },
118
+ service: { type: "ClusterIP", port: 80 },
119
+ ingress: { enabled: false, host: "example.com" },
120
+ });
121
+
122
+ export const deployment = new Deployment({
123
+ metadata: { name: include("web-app.fullname"), labels: include("web-app.labels") },
124
+ spec: {
125
+ replicas: values.replicaCount,
126
+ selector: { matchLabels: include("web-app.selectorLabels") },
127
+ template: {
128
+ metadata: { labels: include("web-app.selectorLabels") },
129
+ spec: {
130
+ containers: [{
131
+ name: "web-app",
132
+ image: printf("%s:%s", values.image.repository, values.image.tag),
133
+ ports: [{ containerPort: values.service.port, name: "http" }],
134
+ }],
135
+ },
136
+ },
137
+ },
138
+ });
139
+
140
+ export const service = new Service({
141
+ metadata: { name: include("web-app.fullname"), labels: include("web-app.labels") },
142
+ spec: {
143
+ type: values.service.type,
144
+ ports: [{ port: values.service.port, targetPort: "http", protocol: "TCP", name: "http" }],
145
+ selector: include("web-app.selectorLabels"),
146
+ },
147
+ });
148
+ `;
149
+ },
150
+ },
151
+ ];
152
+ },
153
+
67
154
  initTemplates(_template?: string): InitTemplateSet {
155
+ if (_template === "stateful-service") {
156
+ return {
157
+ src: {
158
+ "chart.ts": `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
159
+ import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
160
+ import { StatefulSet, Service, PersistentVolumeClaim, ConfigMap } from "@intentius/chant-lexicon-k8s";
161
+
162
+ export const chart = new Chart({
163
+ apiVersion: "v2",
164
+ name: "my-stateful-app",
165
+ version: "0.1.0",
166
+ appVersion: "1.0.0",
167
+ type: "application",
168
+ description: "A Helm chart for a stateful application",
169
+ });
170
+
171
+ export const valuesSchema = new Values({
172
+ replicaCount: 3,
173
+ image: {
174
+ repository: "postgres",
175
+ tag: "16",
176
+ pullPolicy: "IfNotPresent",
177
+ },
178
+ service: {
179
+ port: 5432,
180
+ },
181
+ storage: {
182
+ size: "10Gi",
183
+ storageClass: "",
184
+ },
185
+ config: {
186
+ maxConnections: "100",
187
+ sharedBuffers: "256MB",
188
+ },
189
+ });
190
+
191
+ export const configMap = new ConfigMap({
192
+ metadata: {
193
+ name: include("my-stateful-app.fullname"),
194
+ labels: include("my-stateful-app.labels"),
195
+ },
196
+ data: {
197
+ "max_connections": values.config.maxConnections,
198
+ "shared_buffers": values.config.sharedBuffers,
199
+ },
200
+ });
201
+
202
+ export const headlessService = new Service({
203
+ metadata: {
204
+ name: printf("%s-headless", include("my-stateful-app.fullname")),
205
+ labels: include("my-stateful-app.labels"),
206
+ },
207
+ spec: {
208
+ type: "ClusterIP",
209
+ clusterIP: "None",
210
+ ports: [{ port: values.service.port, targetPort: "db", protocol: "TCP", name: "db" }],
211
+ selector: include("my-stateful-app.selectorLabels"),
212
+ },
213
+ });
214
+
215
+ export const statefulSet = new StatefulSet({
216
+ metadata: {
217
+ name: include("my-stateful-app.fullname"),
218
+ labels: include("my-stateful-app.labels"),
219
+ },
220
+ spec: {
221
+ serviceName: printf("%s-headless", include("my-stateful-app.fullname")),
222
+ replicas: values.replicaCount,
223
+ selector: { matchLabels: include("my-stateful-app.selectorLabels") },
224
+ template: {
225
+ metadata: { labels: include("my-stateful-app.selectorLabels") },
226
+ spec: {
227
+ containers: [{
228
+ name: "db",
229
+ image: printf("%s:%s", values.image.repository, values.image.tag),
230
+ ports: [{ containerPort: values.service.port, name: "db" }],
231
+ volumeMounts: [{ name: "data", mountPath: "/var/lib/postgresql/data" }],
232
+ }],
233
+ },
234
+ },
235
+ volumeClaimTemplates: [
236
+ new PersistentVolumeClaim({
237
+ metadata: { name: "data" },
238
+ spec: {
239
+ accessModes: ["ReadWriteOnce"],
240
+ resources: { requests: { storage: values.storage.size } },
241
+ },
242
+ }),
243
+ ],
244
+ },
245
+ });
246
+ `,
247
+ },
248
+ };
249
+ }
250
+
68
251
  return {
69
252
  src: {
70
253
  "chart.ts": `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
@@ -139,23 +322,11 @@ export const service = new Service({
139
322
  };
140
323
  },
141
324
 
142
- skills(): SkillDefinition[] {
143
- const skillsDir = join(dirname(fileURLToPath(import.meta.url)), "skills");
144
- const skills: SkillDefinition[] = [];
145
- try {
146
- for (const file of readdirSync(skillsDir)) {
147
- if (!file.endsWith(".md")) continue;
148
- const content = readFileSync(join(skillsDir, file), "utf-8");
149
- const name = file.replace(/\.md$/, "");
150
- // Derive a readable description from the filename
151
- const desc = name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
152
- skills.push({ name: `chant-helm-${name}`, description: desc, content });
153
- }
154
- } catch {
155
- // No skills directory
156
- }
157
- return skills;
158
- },
325
+ skills: createSkillsLoader(import.meta.url, [
326
+ { file: "chant-helm.md", name: "chant-helm", description: "Build, validate, and package Helm charts from a chant project" },
327
+ { file: "chant-helm-patterns.md", name: "chant-helm-patterns", description: "Common Helm chart patterns and best practices using chant" },
328
+ { file: "chant-helm-security.md", name: "chant-helm-security", description: "Security best practices for Helm charts built with chant" },
329
+ ]),
159
330
 
160
331
  async docs(options?: { verbose?: boolean }): Promise<void> {
161
332
  const { generateDocs } = await import("./codegen/docs");
package/src/resources.ts CHANGED
@@ -75,3 +75,32 @@ export const HelmMaintainer = createProperty("Helm::Maintainer", LEXICON);
75
75
  * Props: { content: string, filename?: string }
76
76
  */
77
77
  export const HelmCRD = createResource("Helm::CRD", LEXICON, {});
78
+
79
+ // ── K8s resource types used by composites ────────────────
80
+ // These are thin wrappers so composite members are Declarable.
81
+
82
+ const K8S = "k8s";
83
+
84
+ export const Deployment = createResource("K8s::Apps::Deployment", K8S, {});
85
+ export const StatefulSet = createResource("K8s::Apps::StatefulSet", K8S, {});
86
+ export const DaemonSet = createResource("K8s::Apps::DaemonSet", K8S, {});
87
+ export const Service = createResource("K8s::Core::Service", K8S, {});
88
+ export const ServiceAccount = createResource("K8s::Core::ServiceAccount", K8S, {});
89
+ export const ConfigMap = createResource("K8s::Core::ConfigMap", K8S, {});
90
+ export const Namespace = createResource("K8s::Core::Namespace", K8S, {});
91
+ export const Job = createResource("K8s::Batch::Job", K8S, {});
92
+ export const CronJob = createResource("K8s::Batch::CronJob", K8S, {});
93
+ export const Ingress = createResource("K8s::Networking::Ingress", K8S, {});
94
+ export const NetworkPolicy = createResource("K8s::Networking::NetworkPolicy", K8S, {});
95
+ export const HPA = createResource("K8s::Autoscaling::HorizontalPodAutoscaler", K8S, {});
96
+ export const PDB = createResource("K8s::Policy::PodDisruptionBudget", K8S, {});
97
+ export const ResourceQuota = createResource("K8s::Core::ResourceQuota", K8S, {});
98
+ export const LimitRange = createResource("K8s::Core::LimitRange", K8S, {});
99
+ export const ClusterRole = createResource("K8s::Rbac::ClusterRole", K8S, {});
100
+ export const ClusterRoleBinding = createResource("K8s::Rbac::ClusterRoleBinding", K8S, {});
101
+ export const Role = createResource("K8s::Rbac::Role", K8S, {});
102
+ export const RoleBinding = createResource("K8s::Rbac::RoleBinding", K8S, {});
103
+ export const ExternalSecret = createResource("K8s::ExternalSecrets::ExternalSecret", K8S, {});
104
+ export const ServiceMonitor = createResource("K8s::Monitoring::ServiceMonitor", K8S, {});
105
+ export const PrometheusRule = createResource("K8s::Monitoring::PrometheusRule", K8S, {});
106
+ export const Certificate = createResource("K8s::CertManager::Certificate", K8S, {});