@intentius/chant-lexicon-k3d 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.
package/dist/plugin.js ADDED
@@ -0,0 +1,88 @@
1
+ import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
2
+ import { k3dSerializer } from "./serializer.js";
3
+ import { rules } from "./lint/rules/index.js";
4
+ import { postSynthChecks } from "./lint/post-synth/index.js";
5
+ import { k3dAuditCatalog } from "./lint/audit-catalog.js";
6
+ import { completions } from "./lsp/completions.js";
7
+ import { hover } from "./lsp/hover.js";
8
+ /**
9
+ * k3d lexicon plugin.
10
+ *
11
+ * Implements all required LexiconPlugin lifecycle methods.
12
+ */
13
+ export const k3dPlugin = {
14
+ name: "k3d",
15
+ serializer: k3dSerializer,
16
+ /**
17
+ * The marker is stamped as Docker labels on every node via
18
+ * `options.runtime.labels` (the serializer does this when a build carries
19
+ * ownership) and read back through `docker inspect` — k3d's own list
20
+ * output does not expose custom labels (#1412, verified live). Labels
21
+ * survive `k3d cluster stop`/`start`.
22
+ */
23
+ ownershipChannel: {
24
+ keys: LABEL_OWNERSHIP_KEYS,
25
+ reads: ["describeResources"],
26
+ },
27
+ async describeResources(options) {
28
+ const { describeResources } = await import("./describe-resources.js");
29
+ return describeResources(options);
30
+ },
31
+ // ── Required lifecycle methods ────────────────────────────────
32
+ async generate(options) {
33
+ const { generate } = await import("./codegen/generate.js");
34
+ await generate(options);
35
+ },
36
+ async validate(options) {
37
+ const { validate } = await import("./validate.js");
38
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
39
+ const result = await validate();
40
+ printValidationResult(result);
41
+ },
42
+ async coverage(options) {
43
+ // TODO: Implement coverage analysis
44
+ console.error("Coverage analysis not yet implemented");
45
+ },
46
+ async package(options) {
47
+ const { packageLexicon } = await import("./codegen/package.js");
48
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
49
+ const { join, dirname } = await import("path");
50
+ const { fileURLToPath } = await import("url");
51
+ const { spec, stats } = await packageLexicon(options);
52
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
53
+ writeBundleSpec(spec, join(pkgDir, "dist"));
54
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
55
+ },
56
+ // ── Optional extensions ────────────────────────────────────
57
+ lintRules() {
58
+ return rules;
59
+ },
60
+ postSynthChecks() {
61
+ return postSynthChecks;
62
+ },
63
+ auditCatalog() {
64
+ return k3dAuditCatalog;
65
+ },
66
+ skills() {
67
+ return []; // TODO: Add skills
68
+ },
69
+ mcpTools() {
70
+ return []; // TODO: Implement MCP tools
71
+ },
72
+ mcpResources() {
73
+ return []; // TODO: Implement MCP resources
74
+ },
75
+ detectTemplate(data) {
76
+ return false; // TODO: Detect if a template belongs to this lexicon
77
+ },
78
+ completionProvider(ctx) {
79
+ return completions(ctx);
80
+ },
81
+ hoverProvider(ctx) {
82
+ return hover(ctx);
83
+ },
84
+ async docs(options) {
85
+ const { generateDocs } = await import("./codegen/docs.js");
86
+ return generateDocs(options);
87
+ },
88
+ };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * k3d SimpleConfig YAML serializer.
3
+ *
4
+ * Emits the config file `k3d cluster create --config` consumes verbatim —
5
+ * the walk-away artifact that justifies a lexicon here at all. One YAML
6
+ * document per declared Cluster: the first is the primary output, any
7
+ * further clusters land in files[] keyed `<name>.k3d.yaml`.
8
+ *
9
+ * Three things are non-negotiable in the output (#1408):
10
+ *
11
+ * - `apiVersion` and `kind` are literal schema properties, not derived
12
+ * from a type name. They default when undeclared and are never dropped.
13
+ * - Booleans stay booleans and zero stays zero. `agents: 0` is the normal
14
+ * local shape and `updateDefaultKubeconfig: "false"` is truthy.
15
+ * - `nodeFilters` pass through untouched — they are k3d's own syntax
16
+ * (`server:0`, `agent:*`, `loadbalancer`) and rewriting them here would
17
+ * be inventing a dialect.
18
+ *
19
+ * ## The kubeconfig default is chant's, not upstream's
20
+ *
21
+ * When a declaration says nothing about `options.kubeconfig`, the emitted
22
+ * config pins `updateDefaultKubeconfig: false` and `switchCurrentContext:
23
+ * false` — the opposite of k3d's own defaults, chosen deliberately (#1411):
24
+ * a tool that reconciles infrastructure must not rewrite ~/.kube/config or
25
+ * repoint the caller's shell as a side effect. Injecting the values into the
26
+ * artifact rather than relying on activity flags means the emitted YAML says
27
+ * on its face what creating the cluster will do, and `k3d --config` applied
28
+ * by hand behaves identically. A declaration that sets `options.kubeconfig`
29
+ * itself — either key — is passed through exactly as written; upstream
30
+ * defaults then apply to whatever it left unset, because a partially
31
+ * explicit block is the author taking the wheel.
32
+ */
33
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
34
+ import { walkValue } from "@intentius/chant/serializer-walker";
35
+ import { emitYAMLEntry } from "@intentius/chant/yaml";
36
+ import { ownershipEntries, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
37
+ const CLUSTER_TYPE = "K3d::Cluster";
38
+ const API_VERSION = "k3d.io/v1alpha5";
39
+ const KIND = "Simple";
40
+ /**
41
+ * Top-level key order, matching the schema's own property order so a chant
42
+ * emission diffs cleanly against upstream's documented examples.
43
+ */
44
+ const KEY_ORDER = [
45
+ "apiVersion",
46
+ "kind",
47
+ "metadata",
48
+ "servers",
49
+ "agents",
50
+ "kubeAPI",
51
+ "image",
52
+ "network",
53
+ "subnet",
54
+ "token",
55
+ "volumes",
56
+ "ports",
57
+ "files",
58
+ "env",
59
+ "registries",
60
+ "hostAliases",
61
+ "options",
62
+ ];
63
+ const visitor = {
64
+ attrRef: (name) => name,
65
+ resourceRef: (name) => name,
66
+ propertyDeclarable: (entity, walk) => {
67
+ if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
68
+ return undefined;
69
+ }
70
+ const props = entity.props;
71
+ const result = {};
72
+ for (const [key, value] of Object.entries(props)) {
73
+ if (value !== undefined)
74
+ result[key] = walk(value);
75
+ }
76
+ return result;
77
+ },
78
+ };
79
+ /**
80
+ * Stamp the ownership marker as Docker labels on every node, via
81
+ * `options.runtime.labels` (#1412). The labels survive `k3d cluster stop`/
82
+ * `start` and are readable back through `docker inspect` — verified live;
83
+ * `k3d cluster list -o json` does not expose custom labels, so the read
84
+ * side goes to Docker. An author's own label with the same key wins.
85
+ */
86
+ function stampOwnership(options, ownership) {
87
+ const runtime = { ...(options.runtime ?? {}) };
88
+ const labels = Array.isArray(runtime.labels) ? [...runtime.labels] : [];
89
+ const present = new Set(labels
90
+ .map((l) => (typeof l === "object" && l !== null ? l.label : undefined))
91
+ .filter((l) => typeof l === "string")
92
+ .map((l) => l.split("=")[0]));
93
+ for (const [key, value] of Object.entries(ownershipEntries(LABEL_OWNERSHIP_KEYS, ownership))) {
94
+ if (present.has(key))
95
+ continue;
96
+ labels.push({ label: `${key}=${value}`, nodeFilters: ["all"] });
97
+ }
98
+ runtime.labels = labels;
99
+ options.runtime = runtime;
100
+ }
101
+ function clusterDocument(name, cluster, entityNames, context) {
102
+ const raw = (isResourceDeclarable(cluster) ? cluster.props : {});
103
+ const props = (walkValue(raw, entityNames, visitor) ?? {});
104
+ const doc = {};
105
+ doc.apiVersion = props.apiVersion ?? API_VERSION;
106
+ doc.kind = props.kind ?? KIND;
107
+ // metadata.name defaults from the entity's declared name — the schema wants
108
+ // a hostname there and the export name is the one identity the author has
109
+ // already chosen. An explicit metadata wins untouched.
110
+ const metadata = props.metadata;
111
+ doc.metadata = metadata?.name !== undefined ? metadata : { ...(metadata ?? {}), name: name };
112
+ for (const key of KEY_ORDER) {
113
+ if (key === "apiVersion" || key === "kind" || key === "metadata" || key === "options")
114
+ continue;
115
+ if (props[key] !== undefined)
116
+ doc[key] = props[key];
117
+ }
118
+ const options = { ...(props.options ?? {}) };
119
+ if (options.kubeconfig === undefined) {
120
+ // chant's deliberate inversion of upstream's true/true — see module doc.
121
+ options.kubeconfig = { updateDefaultKubeconfig: false, switchCurrentContext: false };
122
+ }
123
+ if (context?.ownership)
124
+ stampOwnership(options, context.ownership);
125
+ doc.options = options;
126
+ // Anything declared outside the known order still emits — the schema is
127
+ // additionalProperties: false, so this only ever carries what a future
128
+ // schema version adds before KEY_ORDER learns about it.
129
+ for (const [key, value] of Object.entries(props)) {
130
+ if (KEY_ORDER.includes(key) || value === undefined)
131
+ continue;
132
+ doc[key] = value;
133
+ }
134
+ return doc;
135
+ }
136
+ function emitDocument(doc) {
137
+ const sections = [];
138
+ for (const [key, value] of Object.entries(doc)) {
139
+ if (value === undefined)
140
+ continue;
141
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
142
+ sections.push(`${key}: ${scalar(value)}`);
143
+ }
144
+ else {
145
+ sections.push(emitYAMLEntry(key, value));
146
+ }
147
+ }
148
+ return sections.join("\n") + "\n";
149
+ }
150
+ function scalar(value) {
151
+ if (typeof value === "boolean")
152
+ return value ? "true" : "false";
153
+ if (typeof value === "number")
154
+ return String(value);
155
+ if (/[:#\[\]{}|>&*!%@`'"]|^[\s\d]|\s$|^$|^(true|false|null|yes|no|on|off)$/i.test(value)) {
156
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
157
+ }
158
+ return value;
159
+ }
160
+ export const k3dSerializer = {
161
+ name: "k3d",
162
+ rulePrefix: "K3D",
163
+ serialize(entities, _outputs, context) {
164
+ const entityNames = new Map();
165
+ for (const [name, entity] of entities)
166
+ entityNames.set(entity, name);
167
+ const clusters = [...entities].filter(([, e]) => e.entityType === CLUSTER_TYPE);
168
+ let primary = "";
169
+ const files = {};
170
+ clusters.forEach(([name, cluster], i) => {
171
+ const content = emitDocument(clusterDocument(name, cluster, entityNames, context));
172
+ if (i === 0)
173
+ primary = content;
174
+ else
175
+ files[`${name}.k3d.yaml`] = content;
176
+ });
177
+ return Object.keys(files).length > 0 ? { primary, files } : primary;
178
+ },
179
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * k3d SimpleConfig schema fetching — downloads the JSON Schema and
3
+ * caches it locally.
4
+ */
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { fetchWithCache, clearCacheFile } from "@intentius/chant/codegen/fetch";
8
+ /**
9
+ * Pinned upstream k3d release.
10
+ *
11
+ * The schema is fetched from the tagged source tree, not a moving branch:
12
+ * https://raw.githubusercontent.com/k3d-io/k3d/v5.9.0/pkg/config/v1alpha5/schema.json
13
+ *
14
+ * v1alpha5 is the config apiVersion this lexicon models. Older config
15
+ * versions (v1alpha2..v1alpha4) each ship their own migrate.go upstream —
16
+ * k3d migrates old configs itself, so modelling superseded versions here
17
+ * would duplicate upstream's job. Bump this constant (and re-generate)
18
+ * when adopting a newer k3d release.
19
+ */
20
+ export const K3D_VERSION = "v5.9.0";
21
+ const SCHEMA_URL = `https://raw.githubusercontent.com/k3d-io/k3d/${K3D_VERSION}/pkg/config/v1alpha5/schema.json`;
22
+ /**
23
+ * Get the cache file path for the k3d schema.
24
+ */
25
+ export function getCachePath() {
26
+ return join(homedir(), ".chant", `k3d-schema-${K3D_VERSION}.json`);
27
+ }
28
+ /**
29
+ * Fetch the k3d SimpleConfig JSON Schema.
30
+ * Uses local file caching with 24-hour TTL.
31
+ */
32
+ export async function fetchConfigSchema(force) {
33
+ return fetchWithCache({
34
+ url: SCHEMA_URL,
35
+ cacheFile: getCachePath(),
36
+ }, force);
37
+ }
38
+ /**
39
+ * Fetch the config schema as a Map<typeName, Buffer> compatible with the
40
+ * generatePipeline fetchSchemas callback.
41
+ *
42
+ * Single document keyed by "K3d::Cluster" — the parse step splits it into
43
+ * multiple entities.
44
+ */
45
+ export async function fetchSchemas(force) {
46
+ const data = await fetchConfigSchema(force);
47
+ const schemas = new Map();
48
+ schemas.set("K3d::Cluster", data);
49
+ return schemas;
50
+ }
51
+ /**
52
+ * Clear the cached schema file.
53
+ */
54
+ export function clearCache() {
55
+ clearCacheFile(getCachePath());
56
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Test fixture loader for the k3d lexicon.
3
+ *
4
+ * The fixture is a verbatim copy of the pinned upstream schema
5
+ * (see src/spec/fetch.ts for the pin), committed so codegen tests
6
+ * run offline.
7
+ */
8
+ import { readFileSync } from "fs";
9
+ import { join } from "path";
10
+ /**
11
+ * Load the pinned SimpleConfig schema fixture for offline testing.
12
+ */
13
+ export function loadSchemaFixture() {
14
+ const fixturePath = join(import.meta.dirname, "k3d-schema-fixture.json");
15
+ return Buffer.from(readFileSync(fixturePath));
16
+ }
17
+ /**
18
+ * Load the schema fixture as a Map compatible with generatePipeline.
19
+ */
20
+ export function loadSchemaFixtureMap() {
21
+ const schemas = new Map();
22
+ schemas.set("K3d::Cluster", loadSchemaFixture());
23
+ return schemas;
24
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env tsx
2
+ import { validate } from "./validate.js";
3
+ // `validate` takes an optional { basePath }; defaults to the lexicon root.
4
+ await validate();
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Validate generated lexicon-k3d artifacts.
3
+ *
4
+ * Thin wrapper around the core validation framework
5
+ * with k3d-specific configuration.
6
+ */
7
+ import { dirname } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { validateLexiconArtifacts } from "@intentius/chant/codegen/validate";
10
+ // TODO: Add names of required entities for your lexicon
11
+ const REQUIRED_NAMES = [];
12
+ /**
13
+ * Validate the generated lexicon-k3d artifacts.
14
+ */
15
+ export async function validate(opts) {
16
+ const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
17
+ return validateLexiconArtifacts({
18
+ lexiconJsonFilename: "lexicon-k3d.json",
19
+ requiredNames: REQUIRED_NAMES,
20
+ basePath,
21
+ });
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k3d",
3
- "version": "0.124.31",
3
+ "version": "0.124.33",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/",
@@ -10,22 +10,22 @@
10
10
  ".": {
11
11
  "development": "./src/index.ts",
12
12
  "types": "./dist/index.d.ts",
13
- "default": "./src/index.ts"
13
+ "default": "./dist/index.js"
14
14
  },
15
15
  "./*": {
16
16
  "development": "./src/*.ts",
17
17
  "types": "./dist/*.d.ts",
18
- "default": "./src/*.ts"
18
+ "default": "./dist/*.js"
19
19
  },
20
20
  "./op/activities": {
21
21
  "development": "./src/op/activities/index.ts",
22
22
  "types": "./dist/op/activities/index.d.ts",
23
- "default": "./src/op/activities/index.ts"
23
+ "default": "./dist/op/activities/index.js"
24
24
  },
25
25
  "./lint/post-synth": {
26
26
  "development": "./src/lint/post-synth/index.ts",
27
27
  "types": "./dist/lint/post-synth/index.d.ts",
28
- "default": "./src/lint/post-synth/index.ts"
28
+ "default": "./dist/lint/post-synth/index.js"
29
29
  },
30
30
  "./manifest": "./dist/manifest.json",
31
31
  "./meta": "./dist/meta.json",
@@ -35,7 +35,7 @@
35
35
  "generate": "npx tsx src/codegen/generate-cli.ts",
36
36
  "validate": "npx tsx src/validate-cli.ts",
37
37
  "docs": "npx tsx src/codegen/docs-cli.ts",
38
- "build": "../../scripts/tsc7.sh -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && find dist -type f \\( -name \"*.js\" -o -name \"*.js.map\" \\) -delete",
38
+ "build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json",
39
39
  "prepack": "npm run generate && npm run bundle && npm run validate && npm run build",
40
40
  "bundle": "tsx src/package-cli.ts"
41
41
  },
@@ -52,7 +52,7 @@
52
52
  "access": "public"
53
53
  },
54
54
  "peerDependencies": {
55
- "@intentius/chant": "^0.124.31"
55
+ "@intentius/chant": "^0.124.33"
56
56
  },
57
57
  "description": "k3d lexicon for chant — a local Kubernetes cluster as declarable data",
58
58
  "license": "Apache-2.0",
@@ -2,13 +2,13 @@ import { packagePipeline } from "@intentius/chant/codegen/package";
2
2
  import type { PackagePipelineConfig } from "@intentius/chant/codegen/package";
3
3
  import { generate } from "./generate";
4
4
  import { dirname } from "path";
5
- import { fileURLToPath } from "url";
5
+ import { moduleSourceDir } from "@intentius/chant/module-source";
6
6
 
7
7
  /**
8
8
  * Package the k3d lexicon for distribution.
9
9
  */
10
10
  export async function packageLexicon(options?: { verbose?: boolean; force?: boolean }) {
11
- const srcDir = dirname(fileURLToPath(import.meta.url));
11
+ const srcDir = moduleSourceDir(import.meta.url);
12
12
 
13
13
  const { spec, stats } = await packagePipeline({
14
14
  generate: (opts) => generate({ verbose: opts?.verbose, force: opts?.force }),
@@ -1,7 +1,8 @@
1
1
  import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
2
3
  import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
3
4
  import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
- const require = createRequire(import.meta.url);
5
+ const require = createRequire(moduleSourceUrl(import.meta.url));
5
6
 
6
7
  let cachedIndex: LexiconIndex | null = null;
7
8
 
package/src/lsp/hover.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
2
3
  import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
3
4
  import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
- const require = createRequire(import.meta.url);
5
+ const require = createRequire(moduleSourceUrl(import.meta.url));
5
6
 
6
7
  let cachedIndex: LexiconIndex | null = null;
7
8