@intentius/chant-lexicon-gcp 0.1.8 → 0.1.10

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "31ad54417045b2707c880b9b6176dd0aa8a5feeb1dfbc645f9d1a20707c525f5",
4
+ "manifest.json": "11955fe00ec8df4b79f7b69dfb982bc45413f927c4ccd7d70ece434adffe18c9",
5
5
  "meta.json": "2bc3713e9e01e90832d18dedaf4bf544dd4d8fe32f1363f7e95dc711d3d76e9d",
6
6
  "types/index.d.ts": "0554f7e883c6216ba735cbe79a8534ccad762bea28cc4d4c4884f8760a2f1dd3",
7
7
  "rules/hardcoded-project.ts": "228631d3159e1ffcce2359c66073d4ae59bb0285f378e46e446f416aec50481c",
@@ -37,5 +37,5 @@
37
37
  "skills/chant-gcp-patterns.md": "a7ef31c1eb2f7244d3f73952c300472ef94c1eb09bd7a1003281b89299b6b704",
38
38
  "skills/chant-gcp-gke.md": "be277019da9a722c851e47cd2dfb9c9536668948c3535fb20db7697e934c4e2b"
39
39
  },
40
- "composite": "a1ac4dc45f5c0421e27da3284a16555e53820e14a09161ce3318d10cb75b5024"
40
+ "composite": "70dd59dee34491e8d1659ac7b17a12e492b505ceb6b11040a48a30aa4ef8209f"
41
41
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcp",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GCP",
6
6
  "intrinsics": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gcp",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Google Cloud lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -44,7 +44,8 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "fflate": "^0.8.2",
47
- "js-yaml": "^4.1.0"
47
+ "js-yaml": "^4.1.0",
48
+ "@types/js-yaml": "^4.0.9"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@intentius/chant": "*",
@@ -4,14 +4,19 @@ import { GcpGenerator } from "./generator";
4
4
  const generator = new GcpGenerator();
5
5
 
6
6
  function makeIR(resources: any[]) {
7
- return { resources, parameters: [], outputs: [] };
7
+ return { resources, parameters: [] };
8
+ }
9
+
10
+ function content(ir: ReturnType<typeof makeIR>): string {
11
+ const files = generator.generate(ir);
12
+ return files[0].content;
8
13
  }
9
14
 
10
15
  describe("GcpGenerator", () => {
11
16
  test("generates valid TypeScript from IR", () => {
12
17
  const ir = makeIR([
13
18
  {
14
- logicalName: "my-bucket",
19
+ logicalId: "my-bucket",
15
20
  type: "GCP::Storage::Bucket",
16
21
  properties: {
17
22
  metadata: { name: "my-bucket" },
@@ -19,7 +24,7 @@ describe("GcpGenerator", () => {
19
24
  },
20
25
  },
21
26
  ]);
22
- const result = generator.generate(ir);
27
+ const result = content(ir);
23
28
  expect(result).toContain("import");
24
29
  expect(result).toContain("Bucket");
25
30
  });
@@ -27,29 +32,28 @@ describe("GcpGenerator", () => {
27
32
  test("correct import source (@intentius/chant-lexicon-gcp)", () => {
28
33
  const ir = makeIR([
29
34
  {
30
- logicalName: "my-bucket",
35
+ logicalId: "my-bucket",
31
36
  type: "GCP::Storage::Bucket",
32
37
  properties: { location: "US" },
33
38
  },
34
39
  ]);
35
- const result = generator.generate(ir);
36
- expect(result).toContain('from "@intentius/chant-lexicon-gcp"');
40
+ expect(content(ir)).toContain('from "@intentius/chant-lexicon-gcp"');
37
41
  });
38
42
 
39
43
  test("multiple resources produce multiple exports", () => {
40
44
  const ir = makeIR([
41
45
  {
42
- logicalName: "my-bucket",
46
+ logicalId: "my-bucket",
43
47
  type: "GCP::Storage::Bucket",
44
48
  properties: { location: "US" },
45
49
  },
46
50
  {
47
- logicalName: "my-vm",
51
+ logicalId: "my-vm",
48
52
  type: "GCP::Compute::Instance",
49
53
  properties: { machineType: "e2-medium" },
50
54
  },
51
55
  ]);
52
- const result = generator.generate(ir);
56
+ const result = content(ir);
53
57
  expect(result).toContain("export const myBucket");
54
58
  expect(result).toContain("export const myVm");
55
59
  });
@@ -57,26 +61,25 @@ describe("GcpGenerator", () => {
57
61
  test("camelCase variable names from kebab-case logical names", () => {
58
62
  const ir = makeIR([
59
63
  {
60
- logicalName: "my-data-bucket",
64
+ logicalId: "my-data-bucket",
61
65
  type: "GCP::Storage::Bucket",
62
66
  properties: { location: "US" },
63
67
  },
64
68
  ]);
65
- const result = generator.generate(ir);
66
- expect(result).toContain("export const myDataBucket");
69
+ expect(content(ir)).toContain("export const myDataBucket");
67
70
  });
68
71
 
69
- test("empty IR produces minimal output", () => {
72
+ test("empty IR still produces a generated file", () => {
70
73
  const ir = makeIR([]);
71
- const result = generator.generate(ir);
72
- // Should still produce valid TypeScript, even if no resources
73
- expect(typeof result).toBe("string");
74
+ const files = generator.generate(ir);
75
+ expect(files).toHaveLength(1);
76
+ expect(typeof files[0].content).toBe("string");
74
77
  });
75
78
 
76
79
  test("nested object formatting", () => {
77
80
  const ir = makeIR([
78
81
  {
79
- logicalName: "my-bucket",
82
+ logicalId: "my-bucket",
80
83
  type: "GCP::Storage::Bucket",
81
84
  properties: {
82
85
  location: "US",
@@ -84,7 +87,7 @@ describe("GcpGenerator", () => {
84
87
  },
85
88
  },
86
89
  ]);
87
- const result = generator.generate(ir);
90
+ const result = content(ir);
88
91
  expect(result).toContain("versioning:");
89
92
  expect(result).toContain("enabled: true");
90
93
  });
@@ -92,29 +95,28 @@ describe("GcpGenerator", () => {
92
95
  test("uses new Constructor() syntax", () => {
93
96
  const ir = makeIR([
94
97
  {
95
- logicalName: "my-bucket",
98
+ logicalId: "my-bucket",
96
99
  type: "GCP::Storage::Bucket",
97
100
  properties: { location: "US" },
98
101
  },
99
102
  ]);
100
- const result = generator.generate(ir);
101
- expect(result).toContain("new Bucket(");
103
+ expect(content(ir)).toContain("new Bucket(");
102
104
  });
103
105
 
104
106
  test("sorts imports alphabetically", () => {
105
107
  const ir = makeIR([
106
108
  {
107
- logicalName: "vm",
109
+ logicalId: "vm",
108
110
  type: "GCP::Compute::Instance",
109
111
  properties: { machineType: "e2-medium" },
110
112
  },
111
113
  {
112
- logicalName: "bucket",
114
+ logicalId: "bucket",
113
115
  type: "GCP::Storage::Bucket",
114
116
  properties: { location: "US" },
115
117
  },
116
118
  ]);
117
- const result = generator.generate(ir);
119
+ const result = content(ir);
118
120
  const importLine = result.split("\n").find((l: string) => l.startsWith("import"));
119
121
  expect(importLine).toBeDefined();
120
122
  // Bucket should come before Instance alphabetically
@@ -4,10 +4,11 @@
4
4
  * Converts import IR from the parser into typed chant TypeScript code.
5
5
  */
6
6
 
7
- import type { TypeScriptGenerator, TemplateIR } from "@intentius/chant/import/generator";
7
+ import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
8
+ import type { TemplateIR } from "@intentius/chant/import/parser";
8
9
 
9
10
  export class GcpGenerator implements TypeScriptGenerator {
10
- generate(ir: TemplateIR): string {
11
+ generate(ir: TemplateIR): GeneratedFile[] {
11
12
  const lines: string[] = [];
12
13
  const imports = new Set<string>();
13
14
 
@@ -31,13 +32,13 @@ export class GcpGenerator implements TypeScriptGenerator {
31
32
  for (const resource of ir.resources) {
32
33
  const parts = resource.type.split("::");
33
34
  const className = parts.length >= 3 ? parts[2] : resource.type;
34
- const varName = camelCase(resource.logicalName);
35
+ const varName = camelCase(resource.logicalId);
35
36
 
36
37
  lines.push(`export const ${varName} = new ${className}(${formatProps(resource.properties, 0)});`);
37
38
  lines.push("");
38
39
  }
39
40
 
40
- return lines.join("\n");
41
+ return [{ path: "main.ts", content: lines.join("\n") + "\n" }];
41
42
  }
42
43
  }
43
44
 
@@ -16,7 +16,7 @@ spec:
16
16
  `;
17
17
  const ir = parser.parse(yaml);
18
18
  expect(ir.resources).toHaveLength(1);
19
- expect(ir.resources[0].logicalName).toBe("my-bucket");
19
+ expect(ir.resources[0].logicalId).toBe("my-bucket");
20
20
  expect(ir.resources[0].type).toContain("Bucket");
21
21
  });
22
22
 
@@ -40,8 +40,8 @@ spec:
40
40
  `;
41
41
  const ir = parser.parse(yaml);
42
42
  expect(ir.resources).toHaveLength(2);
43
- expect(ir.resources[0].logicalName).toBe("my-network");
44
- expect(ir.resources[1].logicalName).toBe("my-subnet");
43
+ expect(ir.resources[0].logicalId).toBe("my-network");
44
+ expect(ir.resources[1].logicalId).toBe("my-subnet");
45
45
  });
46
46
 
47
47
  test("skips non-Config-Connector resources", () => {
@@ -91,7 +91,7 @@ spec:
91
91
  `;
92
92
  const ir = parser.parse(yaml);
93
93
  const generator = new GcpGenerator();
94
- const ts = generator.generate(ir);
94
+ const ts = generator.generate(ir)[0].content;
95
95
  expect(ts).toContain("Bucket");
96
96
  expect(ts).toContain("import");
97
97
  });
@@ -88,7 +88,7 @@ spec:
88
88
  expect(ir.resources).toEqual([]);
89
89
  });
90
90
 
91
- test("metadata.name extracted as logicalName", () => {
91
+ test("metadata.name extracted as logicalId", () => {
92
92
  const yaml = `
93
93
  apiVersion: compute.cnrm.cloud.google.com/v1beta1
94
94
  kind: ComputeNetwork
@@ -98,7 +98,7 @@ spec:
98
98
  autoCreateSubnetworks: false
99
99
  `;
100
100
  const ir = parser.parse(yaml);
101
- expect((ir.resources[0] as any).logicalName).toBe("my-network");
101
+ expect((ir.resources[0] as any).logicalId).toBe("my-network");
102
102
  });
103
103
 
104
104
  test("properties include metadata and spec fields, exclude apiVersion/kind", () => {
@@ -51,7 +51,7 @@ export class GcpParser extends BaseValueParser implements TemplateParser {
51
51
  const metadata = doc.metadata as Record<string, unknown> | undefined;
52
52
  const spec = doc.spec as Record<string, unknown> | undefined;
53
53
 
54
- const logicalName = (metadata?.name as string) ?? kind;
54
+ const logicalId = (metadata?.name as string) ?? kind;
55
55
 
56
56
  // Build properties from spec
57
57
  const properties: Record<string, unknown> = {};
@@ -65,7 +65,7 @@ export class GcpParser extends BaseValueParser implements TemplateParser {
65
65
  }
66
66
 
67
67
  resources.push({
68
- logicalName,
68
+ logicalId,
69
69
  type: typeName,
70
70
  properties,
71
71
  });
@@ -74,7 +74,6 @@ export class GcpParser extends BaseValueParser implements TemplateParser {
74
74
  return {
75
75
  resources,
76
76
  parameters: [],
77
- outputs: [],
78
77
  };
79
78
  }
80
79
  }
@@ -18,7 +18,7 @@ describe("roundtrip: parse YAML → generate TypeScript", () => {
18
18
  test("StorageBucket roundtrip", () => {
19
19
  const yaml = readFileSync(join(testdataDir, "storage-bucket.yaml"), "utf-8");
20
20
  const ir = parser.parse(yaml);
21
- const ts = generator.generate(ir);
21
+ const ts = generator.generate(ir)[0].content;
22
22
 
23
23
  expect(ir.resources.length).toBe(1);
24
24
  expect(ts).toContain("new Bucket");
@@ -28,7 +28,7 @@ describe("roundtrip: parse YAML → generate TypeScript", () => {
28
28
  test("ComputeInstance roundtrip", () => {
29
29
  const yaml = readFileSync(join(testdataDir, "compute-instance.yaml"), "utf-8");
30
30
  const ir = parser.parse(yaml);
31
- const ts = generator.generate(ir);
31
+ const ts = generator.generate(ir)[0].content;
32
32
 
33
33
  expect(ir.resources.length).toBe(1);
34
34
  expect(ts).toContain("new Instance");
@@ -41,7 +41,7 @@ describe("roundtrip: parse YAML → generate TypeScript", () => {
41
41
  const ir = parser.parse(yaml);
42
42
  expect(ir.resources.length).toBe(3); // StorageBucket + IAMPolicyMember + ComputeNetwork
43
43
 
44
- const ts = generator.generate(ir);
44
+ const ts = generator.generate(ir)[0].content;
45
45
  expect(ts).toContain("Bucket");
46
46
  expect(ts).toContain("PolicyMember");
47
47
  expect(ts).toContain("Network");
@@ -58,7 +58,7 @@ spec:
58
58
  storageClass: NEARLINE
59
59
  `;
60
60
  const ir = parser.parse(yaml);
61
- const ts = generator.generate(ir);
61
+ const ts = generator.generate(ir)[0].content;
62
62
 
63
63
  expect(ts).toContain("new Bucket");
64
64
  expect(ts).toContain("export const");
package/src/lsp/hover.ts CHANGED
@@ -3,11 +3,19 @@ import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
3
3
  import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
4
  const require = createRequire(import.meta.url);
5
5
 
6
+ // GCP's generated lexicon JSON includes extra CRD-derived metadata (apiVersion,
7
+ // gvkKind) that the core LexiconEntry interface doesn't carry. Type the data
8
+ // locally as a GCP-specific extension.
9
+ type GcpLexiconEntry = LexiconEntry & {
10
+ apiVersion?: string;
11
+ gvkKind?: string;
12
+ };
13
+
6
14
  let cachedIndex: LexiconIndex | null = null;
7
15
 
8
16
  function getIndex(): LexiconIndex {
9
17
  if (cachedIndex) return cachedIndex;
10
- const data = require("../generated/lexicon-gcp.json") as Record<string, LexiconEntry>;
18
+ const data = require("../generated/lexicon-gcp.json") as Record<string, GcpLexiconEntry>;
11
19
  cachedIndex = new LexiconIndex(data);
12
20
  return cachedIndex;
13
21
  }
@@ -18,17 +26,18 @@ export function gcpHover(ctx: HoverContext): HoverInfo | undefined {
18
26
 
19
27
  function resourceHover(className: string, entry: LexiconEntry): HoverInfo | undefined {
20
28
  const lines: string[] = [];
29
+ const gcpEntry = entry as GcpLexiconEntry;
21
30
 
22
31
  lines.push(`**${className}**`);
23
32
  lines.push("");
24
33
  lines.push(`GCP Config Connector resource: \`${entry.resourceType}\``);
25
34
 
26
- if (entry.apiVersion) {
27
- lines.push(`API Version: \`${entry.apiVersion}\``);
35
+ if (gcpEntry.apiVersion) {
36
+ lines.push(`API Version: \`${gcpEntry.apiVersion}\``);
28
37
  }
29
38
 
30
- if (entry.gvkKind) {
31
- lines.push(`Kind: \`${entry.gvkKind}\``);
39
+ if (gcpEntry.gvkKind) {
40
+ lines.push(`Kind: \`${gcpEntry.gvkKind}\``);
32
41
  }
33
42
 
34
43
  const customAttrs = Object.entries(entry.attrs ?? {})
package/src/plugin.ts CHANGED
@@ -174,20 +174,19 @@ export const bucketReader = new IAMPolicyMember({
174
174
  },
175
175
 
176
176
  detectTemplate(data: unknown): boolean {
177
- if (typeof data !== "object" || data === null) return false;
177
+ // Handle raw string input (unparsed YAML)
178
+ if (typeof data === "string") {
179
+ return data.includes("cnrm.cloud.google.com");
180
+ }
178
181
 
179
182
  // Handle parsed YAML objects
183
+ if (typeof data !== "object" || data === null) return false;
180
184
  const obj = data as Record<string, unknown>;
181
185
  const apiVersion = obj.apiVersion;
182
186
  if (typeof apiVersion === "string" && apiVersion.includes("cnrm.cloud.google.com")) {
183
187
  return true;
184
188
  }
185
189
 
186
- // Handle string input
187
- if (typeof data === "string") {
188
- return data.includes("cnrm.cloud.google.com");
189
- }
190
-
191
190
  return false;
192
191
  },
193
192
 
@@ -207,124 +206,6 @@ export const bucketReader = new IAMPolicyMember({
207
206
  return gcpHover(ctx);
208
207
  },
209
208
 
210
- async describeResources(options: {
211
- environment: string;
212
- buildOutput: string;
213
- entityNames: string[];
214
- }): Promise<Record<string, ResourceMetadata>> {
215
- const { getRuntime } = await import("@intentius/chant/runtime-adapter");
216
- const rt = getRuntime();
217
- const resources: Record<string, ResourceMetadata> = {};
218
-
219
- // Convert TypeScript variable names to kebab-case manifest names
220
- // (mirrors serializer.ts:165 metadata.name assignment)
221
- function entityToManifestName(name: string): string {
222
- return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
223
- }
224
-
225
- // Detect namespace: prefer namespace from manifests, then kubectl context, fallback "default"
226
- let namespace = "default";
227
- try {
228
- // Check manifests for an explicit namespace
229
- const nsMatch = options.buildOutput.match(/^\s+namespace:\s*(.+)$/m);
230
- if (nsMatch) {
231
- namespace = nsMatch[1].trim();
232
- } else {
233
- // Try kubectl current context namespace
234
- const nsResult = await rt.spawn([
235
- "kubectl", "config", "view", "--minify", "-o", "jsonpath={..namespace}",
236
- ]);
237
- if (nsResult.exitCode === 0 && nsResult.stdout.trim()) {
238
- namespace = nsResult.stdout.trim();
239
- }
240
- }
241
- } catch (err) {
242
- console.error(`[gcp] describeResources: namespace detection failed, using "default": ${err instanceof Error ? err.message : String(err)}`);
243
- }
244
-
245
- // Parse build output to extract kind/name pairs
246
- let manifests: Array<{ kind: string; name: string; apiVersion: string; namespace?: string }> = [];
247
- try {
248
- const docs = options.buildOutput.split(/^---$/m).filter((d) => d.trim());
249
- for (const doc of docs) {
250
- const kindMatch = doc.match(/^kind:\s*(.+)$/m);
251
- const nameMatch = doc.match(/^\s+name:\s*(.+)$/m);
252
- const apiVersionMatch = doc.match(/^apiVersion:\s*(.+)$/m);
253
- if (kindMatch && nameMatch && apiVersionMatch) {
254
- const nsMatch = doc.match(/^\s+namespace:\s*(.+)$/m);
255
- manifests.push({
256
- kind: kindMatch[1].trim(),
257
- name: nameMatch[1].trim(),
258
- apiVersion: apiVersionMatch[1].trim(),
259
- ...(nsMatch && { namespace: nsMatch[1].trim() }),
260
- });
261
- }
262
- }
263
- } catch (err) {
264
- console.error(`[gcp] describeResources: failed to parse build output: ${err instanceof Error ? err.message : String(err)}`);
265
- }
266
-
267
- let resolved = 0;
268
-
269
- for (const entityName of options.entityNames) {
270
- const manifestName = entityToManifestName(entityName);
271
- const manifest = manifests.find((m) => m.name === manifestName);
272
- if (!manifest) {
273
- console.error(`[gcp] describeResources: no manifest found for entity "${entityName}" (expected manifest name "${manifestName}")`);
274
- continue;
275
- }
276
-
277
- const resourceNs = manifest.namespace ?? namespace;
278
- const resourceType = manifest.kind.toLowerCase();
279
- const getResult = await rt.spawn([
280
- "kubectl", "get", resourceType, manifest.name,
281
- "-n", resourceNs, "-o", "json",
282
- ]);
283
-
284
- if (getResult.exitCode !== 0) {
285
- console.error(`[gcp] describeResources: kubectl get ${resourceType} ${manifest.name} -n ${resourceNs} failed (exit ${getResult.exitCode}): ${getResult.stderr.trim()}`);
286
- continue;
287
- }
288
-
289
- try {
290
- const obj = JSON.parse(getResult.stdout) as {
291
- metadata: { name: string; uid: string; creationTimestamp: string };
292
- status?: {
293
- conditions?: Array<{ type: string; status: string }>;
294
- externalRef?: string;
295
- };
296
- };
297
-
298
- let status = "Unknown";
299
- if (obj.status?.conditions) {
300
- const ready = obj.status.conditions.find((c) => c.type === "Ready");
301
- status = ready?.status === "True" ? "Ready" : "NotReady";
302
- }
303
-
304
- const attributes: Record<string, unknown> = {
305
- uid: obj.metadata.uid,
306
- };
307
- if (obj.status?.externalRef) {
308
- attributes.externalRef = obj.status.externalRef;
309
- }
310
-
311
- resources[entityName] = {
312
- type: `${manifest.apiVersion}/${manifest.kind}`,
313
- physicalId: obj.status?.externalRef ?? obj.metadata.name,
314
- status,
315
- lastUpdated: obj.metadata.creationTimestamp,
316
- attributes,
317
- };
318
- resolved++;
319
- } catch (err) {
320
- console.error(`[gcp] describeResources: failed to parse kubectl output for "${entityName}": ${err instanceof Error ? err.message : String(err)}`);
321
- }
322
- }
323
-
324
- console.error(`[gcp] describeResources: ${resolved}/${options.entityNames.length} resources resolved`);
325
- return resources;
326
- },
327
-
328
209
  mcpTools() {
329
210
  return [createDiffTool(gcpSerializer, "Compare current build output against previous output for GCP Config Connector manifests", "gcp")];
330
211
  },