@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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env tsx
2
+ import { generateDocs } from "./docs.js";
3
+ await generateDocs({ verbose: true });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Documentation generation for the k3d lexicon.
3
+ *
4
+ * Generates Starlight MDX pages for k3d entities using the core docs pipeline.
5
+ * The overview prose lives here; authored pages live under docs/pages/.
6
+ */
7
+ import { dirname, join } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
10
+ function serviceFromType(resourceType) {
11
+ const parts = resourceType.split("::");
12
+ return parts.length >= 2 ? parts[1] : "K3d";
13
+ }
14
+ const overview = `The k3d lexicon types [k3d](https://k3d.io)'s own declarative config —
15
+ \`k3d.io/v1alpha5\` SimpleConfig, generated from upstream's published JSON
16
+ Schema, pinned at **k3d v5.9.0**. \`chant build\` emits a YAML file that
17
+ \`k3d cluster create --config\` consumes verbatim, so the walk-away cost is
18
+ zero: the artifact is a file the native tool accepts with chant nowhere in
19
+ sight.
20
+
21
+ \`\`\`ts
22
+ import { Cluster, K3dOptions, Options } from "@intentius/chant-lexicon-k3d";
23
+
24
+ export const devCluster = new Cluster({
25
+ metadata: { name: "chant-dev" },
26
+ servers: 1,
27
+ agents: 0,
28
+ options: new Options({
29
+ k3d: new K3dOptions({ disableLoadbalancer: true }),
30
+ }),
31
+ });
32
+ \`\`\`
33
+
34
+ ## The kubeconfig default is chant's, not upstream's
35
+
36
+ k3d's own defaults rewrite \`~/.kube/config\` and switch your active context
37
+ on every cluster create. When a declaration says nothing about
38
+ \`options.kubeconfig\`, the emitted config pins both off:
39
+
40
+ \`\`\`yaml
41
+ options:
42
+ kubeconfig:
43
+ updateDefaultKubeconfig: false
44
+ switchCurrentContext: false
45
+ \`\`\`
46
+
47
+ This is deliberate: a tool that reconciles infrastructure must not repoint
48
+ your shell as a side effect — an unrelated \`k3d cluster create\` switching
49
+ the ambient context mid-run produces convincingly false failures. Declare
50
+ \`options.kubeconfig\` yourself to opt back into upstream behaviour; what you
51
+ write is emitted exactly as written. The \`k3dUp\` activity reports the
52
+ context name and kubeconfig path it produced either way, so whatever
53
+ applies manifests afterwards knows what to talk to.
54
+
55
+ ## Lifecycle is an Op activity pair
56
+
57
+ Cluster creation is procedural, not desired-state — nobody expects
58
+ \`chant lifecycle diff\` to reconcile a laptop. \`k3dUp\` / \`k3dDown\` shell out
59
+ to k3d with the emitted config and are idempotent; add \`"k3d"\` to your
60
+ project's \`lexicons\` so \`loadActivities\` finds them.
61
+
62
+ ## Ownership and observation
63
+
64
+ With ownership configured, the serializer stamps chant's marker as Docker
65
+ labels on every node via \`options.runtime.labels\`; the labels survive
66
+ \`k3d cluster stop\`/\`start\`. \`chant lifecycle diff --live\` reports each
67
+ declared cluster as present, absent, or not-observed-with-a-reason — Docker
68
+ being down reads as *not observed*, never as *absent*, so a stopped daemon
69
+ cannot masquerade as a missing cluster. There is no property-level drift on
70
+ purpose: the config is an input to creation, not a spec k3d reconciles
71
+ against.
72
+
73
+ ## Checks
74
+
75
+ - **K3D001** (lint, error) — a literal \`registries.create.proxy.password\`
76
+ in source. A committed cluster config is a leaked registry credential.
77
+ - **K3D101** (post-synth, error) — a \`nodeFilter\` that matches no node.
78
+ k3d applies an unmatched filter to nothing and says nothing.
79
+ - **K3D102** (post-synth, error) — a registry proxy password that reached
80
+ the emitted config, whatever route it took through the source.
81
+ `;
82
+ export async function generateDocs(opts) {
83
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
84
+ const config = {
85
+ name: "k3d",
86
+ displayName: "K3d",
87
+ description: "Declare a local k3d cluster as data; the config is the artifact",
88
+ distDir: join(pkgDir, "dist"),
89
+ outDir: join(pkgDir, "docs"),
90
+ basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/k3d/",
91
+ overview,
92
+ serviceFromType,
93
+ };
94
+ const result = docsPipeline(config);
95
+ writeDocsSite(config, result);
96
+ if (opts?.verbose) {
97
+ console.error(`Generated ${result.pages.size} documentation pages`);
98
+ }
99
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry point for k3d lexicon generation.
4
+ */
5
+ import { dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { generate, writeGeneratedFiles } from "./generate.js";
8
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
9
+ async function main() {
10
+ const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
11
+ const force = process.argv.includes("--force") || process.argv.includes("-f");
12
+ console.error("Generating k3d lexicon...");
13
+ const result = await generate({ verbose, force });
14
+ writeGeneratedFiles(result, pkgDir);
15
+ console.error(`Generated ${result.resources} entities, ${result.properties} property types, ${result.enums} enums`);
16
+ if (result.warnings.length > 0) {
17
+ console.error(`${result.warnings.length} warnings:`);
18
+ for (const w of result.warnings) {
19
+ console.error(` ${w.file}: ${w.error}`);
20
+ }
21
+ }
22
+ }
23
+ main().catch((err) => {
24
+ console.error("Generation failed:", err);
25
+ process.exit(1);
26
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Lexicon JSON generator — produces lexicon-k3d.json with metadata
3
+ * for all k3d entities.
4
+ */
5
+ import { k3dShortName } from "./parse.js";
6
+ import { buildRegistry, serializeRegistry, } from "@intentius/chant/codegen/generate-registry";
7
+ /**
8
+ * Generate the lexicon-k3d.json content.
9
+ */
10
+ export function generateLexiconJSON(results, naming) {
11
+ const registryResources = results.map((r) => ({
12
+ typeName: r.resource.typeName,
13
+ attributes: r.resource.attributes,
14
+ properties: r.resource.properties,
15
+ propertyTypes: r.propertyTypes.map((pt) => ({ name: pt.name, specType: pt.defType })),
16
+ }));
17
+ const entries = buildRegistry(registryResources, naming, {
18
+ shortName: k3dShortName,
19
+ buildEntry: (resource, _tsName, _attrs, propConstraints) => {
20
+ const r = results.find((res) => res.resource.typeName === resource.typeName);
21
+ return {
22
+ resourceType: resource.typeName,
23
+ kind: (r?.isProperty ? "property" : "resource"),
24
+ lexicon: "k3d",
25
+ ...(r?.resource.deprecatedProperties?.length && { deprecatedProperties: r.resource.deprecatedProperties }),
26
+ ...(propConstraints && Object.keys(propConstraints).length > 0 && { constraints: propConstraints }),
27
+ };
28
+ },
29
+ buildPropertyEntry: (resourceType, propertyType) => ({
30
+ resourceType: `${resourceType}.${propertyType}`,
31
+ kind: "property",
32
+ lexicon: "k3d",
33
+ }),
34
+ });
35
+ return serializeRegistry(entries);
36
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * TypeScript declaration file generator — produces index.d.ts with all
3
+ * k3d entity classes.
4
+ */
5
+ import { k3dShortName } from "./parse.js";
6
+ import { propertyTypeName, extractDefName } from "./naming.js";
7
+ import { writeResourceClass, writeEnumType, } from "@intentius/chant/codegen/generate-typescript";
8
+ /**
9
+ * Generate the complete .d.ts file content.
10
+ */
11
+ export function generateTypeScriptDeclarations(results, naming) {
12
+ const lines = [];
13
+ lines.push("// Code generated by chant k3d generate. DO NOT EDIT.");
14
+ const resources = [];
15
+ const enums = [];
16
+ for (const r of results) {
17
+ const typeName = r.resource.typeName;
18
+ const tsName = naming.resolve(typeName);
19
+ if (!tsName)
20
+ continue;
21
+ const shortName = k3dShortName(typeName);
22
+ const remap = new Map();
23
+ for (const pt of r.propertyTypes) {
24
+ const defName = extractDefName(pt.name, shortName);
25
+ remap.set(pt.name, propertyTypeName(tsName, defName));
26
+ }
27
+ for (const e of r.enums) {
28
+ const defName = extractDefName(e.name, shortName);
29
+ remap.set(e.name, propertyTypeName(tsName, defName));
30
+ }
31
+ const dtsProps = r.resource.properties.map((p) => ({
32
+ name: p.name,
33
+ type: p.tsType,
34
+ required: p.required,
35
+ description: p.description,
36
+ }));
37
+ const dtsAttrs = r.resource.attributes.map((a) => ({
38
+ name: a.name,
39
+ type: a.tsType,
40
+ }));
41
+ resources.push({ tsName, properties: dtsProps, attributes: dtsAttrs, remap });
42
+ for (const alias of naming.aliases(typeName)) {
43
+ resources.push({ tsName: alias, properties: dtsProps, attributes: dtsAttrs, remap });
44
+ }
45
+ for (const e of r.enums) {
46
+ const defName = extractDefName(e.name, shortName);
47
+ const eName = propertyTypeName(tsName, defName);
48
+ enums.push({ tsName: eName, values: e.values });
49
+ }
50
+ }
51
+ resources.sort((a, b) => a.tsName.localeCompare(b.tsName));
52
+ enums.sort((a, b) => a.tsName.localeCompare(b.tsName));
53
+ lines.push("");
54
+ lines.push("// --- k3d Entity classes ---");
55
+ for (const re of resources) {
56
+ writeResourceClass(lines, re.tsName, re.properties, re.attributes, re.remap);
57
+ }
58
+ if (enums.length > 0) {
59
+ lines.push("");
60
+ lines.push("// --- Enum types ---");
61
+ for (const ee of enums) {
62
+ writeEnumType(lines, ee.tsName, ee.values);
63
+ }
64
+ }
65
+ lines.push("");
66
+ return lines.join("\n");
67
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * k3d generation pipeline — uses core generatePipeline with k3d-specific
3
+ * fetch, parse, naming, and generation callbacks.
4
+ */
5
+ import { generatePipeline, writeGeneratedArtifacts, } from "@intentius/chant/codegen/generate";
6
+ import { dirname } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { fetchSchemas } from "../spec/fetch.js";
9
+ import { parseConfigSchema, k3dShortName } from "./parse.js";
10
+ import { NamingStrategy, propertyTypeName, extractDefName } from "./naming.js";
11
+ import { generateLexiconJSON } from "./generate-lexicon.js";
12
+ import { generateTypeScriptDeclarations } from "./generate-typescript.js";
13
+ import { generateRuntimeIndex as coreGenerateRuntimeIndex, } from "@intentius/chant/codegen/generate-runtime-index";
14
+ /**
15
+ * Run the full k3d generation pipeline.
16
+ */
17
+ export async function generate(opts = {}) {
18
+ let pendingResults = [];
19
+ const config = {
20
+ fetchSchemas: async (fetchOpts) => {
21
+ return fetchSchemas(fetchOpts.force);
22
+ },
23
+ parseSchema: (_typeName, data) => {
24
+ const results = parseConfigSchema(data);
25
+ if (results.length === 0)
26
+ return null;
27
+ pendingResults = results.slice(1);
28
+ return results[0];
29
+ },
30
+ createNaming: (results) => new NamingStrategy(results),
31
+ augmentResults: (results, _opts, log) => {
32
+ if (pendingResults.length > 0) {
33
+ results.push(...pendingResults);
34
+ log(`Added ${pendingResults.length} additional entities from single schema`);
35
+ pendingResults = [];
36
+ }
37
+ log(`Total: ${results.length} k3d entity schemas`);
38
+ return { results };
39
+ },
40
+ generateRegistry: (results, naming) => {
41
+ return generateLexiconJSON(results, naming);
42
+ },
43
+ generateTypes: (results, naming) => {
44
+ return generateTypeScriptDeclarations(results, naming);
45
+ },
46
+ generateRuntimeIndex: (results, naming) => {
47
+ return generateRuntimeIndex(results, naming);
48
+ },
49
+ };
50
+ return generatePipeline(config, opts);
51
+ }
52
+ /**
53
+ * Write generated artifacts to disk.
54
+ */
55
+ export function writeGeneratedFiles(result, pkgDir) {
56
+ const baseDir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
57
+ writeGeneratedArtifacts({
58
+ baseDir,
59
+ files: {
60
+ "lexicon-k3d.json": result.lexiconJSON,
61
+ "index.d.ts": result.typesDTS,
62
+ "index.ts": result.indexTS,
63
+ "runtime.ts": `/**\n * Runtime factory constructors — re-exported from core.\n */\nexport { createResource, createProperty } from "@intentius/chant/runtime";\n`,
64
+ },
65
+ });
66
+ }
67
+ /**
68
+ * Generate the runtime index.ts with factory constructor exports.
69
+ */
70
+ function generateRuntimeIndex(results, naming) {
71
+ const resourceEntries = [];
72
+ const propertyEntries = [];
73
+ for (const r of results) {
74
+ const typeName = r.resource.typeName;
75
+ const tsName = naming.resolve(typeName);
76
+ if (!tsName)
77
+ continue;
78
+ const attrs = {};
79
+ if (r.isProperty) {
80
+ propertyEntries.push({ tsName, resourceType: typeName });
81
+ for (const alias of naming.aliases(typeName)) {
82
+ propertyEntries.push({ tsName: alias, resourceType: typeName });
83
+ }
84
+ }
85
+ else {
86
+ resourceEntries.push({ tsName, resourceType: typeName, attrs });
87
+ for (const alias of naming.aliases(typeName)) {
88
+ resourceEntries.push({ tsName: alias, resourceType: typeName, attrs });
89
+ }
90
+ }
91
+ const shortName = k3dShortName(typeName);
92
+ const ptAliases = naming.propertyTypeAliases(typeName);
93
+ for (const pt of r.propertyTypes) {
94
+ const defName = extractDefName(pt.name, shortName);
95
+ const ptName = propertyTypeName(tsName, defName);
96
+ const ptType = `${typeName}.${pt.defType}`;
97
+ propertyEntries.push({ tsName: ptName, resourceType: ptType });
98
+ if (ptAliases) {
99
+ const aliasName = ptAliases.get(defName);
100
+ if (aliasName) {
101
+ propertyEntries.push({ tsName: aliasName, resourceType: ptType });
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return coreGenerateRuntimeIndex(resourceEntries, propertyEntries, {
107
+ lexiconName: "k3d",
108
+ });
109
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * k3d naming strategy — flat namespace, one short name per entity.
3
+ */
4
+ import { NamingStrategy as CoreNamingStrategy, } from "@intentius/chant/codegen/naming";
5
+ import { k3dShortName, k3dServiceName } from "./parse.js";
6
+ export { propertyTypeName, extractDefName } from "@intentius/chant/codegen/naming";
7
+ const k3dNamingConfig = {
8
+ priorityNames: {
9
+ "K3d::Cluster": "Cluster",
10
+ "K3d::Metadata": "Metadata",
11
+ "K3d::KubeAPI": "KubeAPI",
12
+ "K3d::Volume": "Volume",
13
+ "K3d::Port": "Port",
14
+ "K3d::File": "File",
15
+ "K3d::EnvVar": "EnvVar",
16
+ "K3d::HostAlias": "HostAlias",
17
+ "K3d::Registries": "Registries",
18
+ "K3d::RegistryCreate": "RegistryCreate",
19
+ "K3d::RegistryProxy": "RegistryProxy",
20
+ "K3d::Options": "Options",
21
+ "K3d::K3dOptions": "K3dOptions",
22
+ "K3d::LoadbalancerOptions": "LoadbalancerOptions",
23
+ "K3d::K3sOptions": "K3sOptions",
24
+ "K3d::K3sExtraArg": "K3sExtraArg",
25
+ "K3d::NodeLabel": "NodeLabel",
26
+ "K3d::KubeconfigOptions": "KubeconfigOptions",
27
+ "K3d::RuntimeOptions": "RuntimeOptions",
28
+ "K3d::RuntimeLabel": "RuntimeLabel",
29
+ "K3d::Ulimit": "Ulimit",
30
+ },
31
+ priorityAliases: {},
32
+ priorityPropertyAliases: {},
33
+ serviceAbbreviations: {},
34
+ shortName: k3dShortName,
35
+ serviceName: k3dServiceName,
36
+ };
37
+ /**
38
+ * k3d naming strategy.
39
+ */
40
+ export class NamingStrategy extends CoreNamingStrategy {
41
+ constructor(results) {
42
+ const inputs = results.map((r) => ({
43
+ typeName: r.resource.typeName,
44
+ propertyTypes: r.propertyTypes,
45
+ }));
46
+ super(inputs, k3dNamingConfig);
47
+ }
48
+ }
@@ -0,0 +1,22 @@
1
+ import { packagePipeline } from "@intentius/chant/codegen/package";
2
+ import { generate } from "./generate.js";
3
+ import { moduleSourceDir } from "@intentius/chant/module-source";
4
+ /**
5
+ * Package the k3d lexicon for distribution.
6
+ */
7
+ export async function packageLexicon(options) {
8
+ const srcDir = moduleSourceDir(import.meta.url);
9
+ const { spec, stats } = await packagePipeline({
10
+ generate: (opts) => generate({ verbose: opts?.verbose, force: opts?.force }),
11
+ buildManifest: () => ({
12
+ name: "k3d",
13
+ version: "0.0.1",
14
+ chantVersion: ">=0.1.0",
15
+ namespace: "K3d",
16
+ }),
17
+ srcDir,
18
+ collectSkills: () => new Map(),
19
+ });
20
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules`);
21
+ return { spec, stats };
22
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * k3d SimpleConfig JSON Schema parser.
3
+ *
4
+ * Parses the single v1alpha5 schema into multiple entity results — the
5
+ * Cluster resource plus one property entity per nested config object
6
+ * (Metadata, Volume, Registries, Options, etc.).
7
+ */
8
+ import { extractConstraints as coreExtractConstraints, constraintsIsEmpty as coreConstraintsIsEmpty, primaryType, } from "@intentius/chant/codegen/json-schema";
9
+ export { coreConstraintsIsEmpty as constraintsIsEmpty };
10
+ // ── Entity extraction mapping ──────────────────────────────────────
11
+ const RESOURCE_ENTITIES = [
12
+ {
13
+ typeName: "K3d::Cluster",
14
+ source: "root",
15
+ description: "A k3d cluster (SimpleConfig, apiVersion k3d.io/v1alpha5)",
16
+ },
17
+ ];
18
+ const PROPERTY_ENTITIES = [
19
+ { typeName: "K3d::Metadata", source: "metadata", description: "Cluster metadata (name)" },
20
+ { typeName: "K3d::KubeAPI", source: "kubeAPI", description: "Kubernetes API server exposure settings" },
21
+ { typeName: "K3d::Volume", source: "volumes:item", description: "A volume mount with node filters" },
22
+ { typeName: "K3d::Port", source: "ports:item", description: "A port mapping with node filters" },
23
+ { typeName: "K3d::File", source: "files:item", description: "A file to copy into nodes" },
24
+ { typeName: "K3d::EnvVar", source: "env:item", description: "An environment variable with node filters" },
25
+ { typeName: "K3d::HostAlias", source: "hostAliases:item", description: "An IP to hostnames mapping" },
26
+ { typeName: "K3d::Registries", source: "registries", description: "Registry configuration" },
27
+ { typeName: "K3d::RegistryCreate", source: "registries.create", description: "Create a new container image registry alongside the cluster" },
28
+ { typeName: "K3d::RegistryProxy", source: "registries.create.proxy", description: "Pull-through proxy settings for a created registry" },
29
+ { typeName: "K3d::Options", source: "options", description: "Cluster options (k3d, k3s, kubeconfig, runtime)" },
30
+ { typeName: "K3d::K3dOptions", source: "options.k3d", description: "k3d-specific options" },
31
+ { typeName: "K3d::LoadbalancerOptions", source: "options.k3d.loadbalancer", description: "Loadbalancer options" },
32
+ { typeName: "K3d::K3sOptions", source: "options.k3s", description: "k3s-specific options" },
33
+ { typeName: "K3d::K3sExtraArg", source: "options.k3s.extraArgs:item", description: "An extra k3s server/agent argument with node filters" },
34
+ { typeName: "K3d::NodeLabel", source: "options.k3s.nodeLabels:item", description: "A Kubernetes node label with node filters" },
35
+ { typeName: "K3d::KubeconfigOptions", source: "options.kubeconfig", description: "Kubeconfig update behaviour" },
36
+ { typeName: "K3d::RuntimeOptions", source: "options.runtime", description: "Container runtime options" },
37
+ { typeName: "K3d::RuntimeLabel", source: "options.runtime.labels:item", description: "A container runtime label with node filters" },
38
+ { typeName: "K3d::Ulimit", source: "options.runtime.ulimits:item", description: "A ulimit setting for nodes" },
39
+ ];
40
+ // ── Parser ─────────────────────────────────────────────────────────
41
+ /**
42
+ * Parse the k3d SimpleConfig JSON Schema into multiple entity results.
43
+ */
44
+ export function parseConfigSchema(data) {
45
+ const schema = JSON.parse(typeof data === "string" ? data : data.toString("utf-8"));
46
+ const results = [];
47
+ for (const entity of RESOURCE_ENTITIES) {
48
+ const result = extractEntity(schema, entity);
49
+ if (result)
50
+ results.push(result);
51
+ }
52
+ for (const entity of PROPERTY_ENTITIES) {
53
+ const result = extractEntity(schema, entity);
54
+ if (result) {
55
+ result.isProperty = true;
56
+ results.push(result);
57
+ }
58
+ }
59
+ return results;
60
+ }
61
+ function extractEntity(schema, entity) {
62
+ const def = resolveSource(schema, entity.source);
63
+ if (!def?.properties)
64
+ return null;
65
+ const properties = parseProperties(def.properties, new Set(def.required ?? []), schema);
66
+ const overrides = PROPERTY_OVERRIDES[entity.typeName];
67
+ if (overrides) {
68
+ for (const prop of properties) {
69
+ if (overrides[prop.name]) {
70
+ prop.tsType = overrides[prop.name];
71
+ }
72
+ }
73
+ }
74
+ return {
75
+ resource: {
76
+ typeName: entity.typeName,
77
+ description: entity.description ?? def.description,
78
+ properties,
79
+ attributes: [],
80
+ deprecatedProperties: [],
81
+ },
82
+ propertyTypes: [],
83
+ enums: [],
84
+ };
85
+ }
86
+ // ── Source resolution ──────────────────────────────────────────────
87
+ /**
88
+ * Resolve an extraction source to a schema definition.
89
+ *
90
+ * Sources are:
91
+ * - "root" — the schema document itself (K3d::Cluster)
92
+ * - a dotted path of property names from the root, e.g. "options.k3d"
93
+ * - a dotted path ending in ":item" for array item schemas,
94
+ * e.g. "volumes:item", "options.k3s.extraArgs:item"
95
+ */
96
+ function resolveSource(schema, source) {
97
+ if (source === "root") {
98
+ return { properties: schema.properties, required: schema.required };
99
+ }
100
+ const wantsItem = source.endsWith(":item");
101
+ const path = wantsItem ? source.slice(0, -":item".length) : source;
102
+ let current = { properties: schema.properties };
103
+ for (const seg of path.split(".")) {
104
+ const next = current?.properties?.[seg];
105
+ if (!next || typeof next === "boolean")
106
+ return null;
107
+ current = next;
108
+ }
109
+ if (!current)
110
+ return null;
111
+ if (wantsItem) {
112
+ return current.items ?? null;
113
+ }
114
+ return current;
115
+ }
116
+ function resolveRef(ref, schema) {
117
+ const prefix = "#/definitions/";
118
+ if (!ref.startsWith(prefix))
119
+ return null;
120
+ const defName = ref.slice(prefix.length);
121
+ return schema.definitions?.[defName] ?? null;
122
+ }
123
+ // ── Property parsing ──────────────────────────────────────────────
124
+ function parseProperties(properties, requiredSet, schema) {
125
+ const result = [];
126
+ for (const [name, prop] of Object.entries(properties)) {
127
+ // Upstream schema bug (present in v5.9.0): inside `registries.properties`
128
+ // the key `"additionalProperties": false` is nested INSIDE the properties
129
+ // block instead of sitting beside it, so a naive walk would emit a config
130
+ // property literally named "additionalProperties" whose schema is the
131
+ // bare boolean `false`. Drop it — it is a misplaced schema keyword, not a
132
+ // property. Keep this guard on regeneration until upstream fixes the
133
+ // schema, or the bogus property comes back.
134
+ if (name === "additionalProperties" && typeof prop === "boolean")
135
+ continue;
136
+ if (typeof prop === "boolean")
137
+ continue;
138
+ const tsType = resolvePropertyType(prop, schema);
139
+ result.push({
140
+ name,
141
+ tsType,
142
+ required: requiredSet.has(name),
143
+ description: prop.description,
144
+ enum: prop.enum,
145
+ constraints: coreExtractConstraints(prop),
146
+ });
147
+ }
148
+ return result;
149
+ }
150
+ function resolvePropertyType(prop, schema) {
151
+ if (!prop)
152
+ return "any";
153
+ if (prop.$ref) {
154
+ const def = resolveRef(prop.$ref, schema);
155
+ // The only definition in the v1alpha5 schema is nodeFilters
156
+ // (array of strings); resolve through it structurally.
157
+ if (def)
158
+ return resolvePropertyType(def, schema);
159
+ return "any";
160
+ }
161
+ if (prop.enum && prop.enum.length > 0) {
162
+ return prop.enum.map((v) => JSON.stringify(v)).join(" | ");
163
+ }
164
+ const pt = primaryType(prop.type);
165
+ switch (pt) {
166
+ case "string": return "string";
167
+ case "integer":
168
+ case "number": return "number";
169
+ case "boolean": return "boolean";
170
+ case "array":
171
+ if (prop.items) {
172
+ const itemType = resolvePropertyType(prop.items, schema);
173
+ if (itemType.includes(" | "))
174
+ return `(${itemType})[]`;
175
+ return `${itemType}[]`;
176
+ }
177
+ return "any[]";
178
+ case "object": return "Record<string, any>";
179
+ default:
180
+ // Upstream schema quirk (v5.9.0): `options.k3d.timeout` declares
181
+ // `examples` ("60s", "1m", "1m30s") but no `type` at all. It is a Go
182
+ // time.Duration string upstream, so when a type-less schema carries
183
+ // string examples we treat it as a string (duration) rather than `any`.
184
+ if (Array.isArray(prop.examples) && prop.examples.length > 0 && prop.examples.every((e) => typeof e === "string")) {
185
+ return "string";
186
+ }
187
+ return "any";
188
+ }
189
+ }
190
+ // ── Property overrides ────────────────────────────────────────────
191
+ /**
192
+ * Cross-reference nested objects to their extracted property entity
193
+ * classes so authors get typed constructors instead of Record<string, any>.
194
+ */
195
+ const PROPERTY_OVERRIDES = {
196
+ "K3d::Cluster": {
197
+ metadata: "Metadata",
198
+ kubeAPI: "KubeAPI",
199
+ volumes: "Volume[]",
200
+ ports: "Port[]",
201
+ files: "File[]",
202
+ env: "EnvVar[]",
203
+ hostAliases: "HostAlias[]",
204
+ registries: "Registries",
205
+ options: "Options",
206
+ },
207
+ "K3d::Registries": {
208
+ create: "RegistryCreate",
209
+ },
210
+ "K3d::RegistryCreate": {
211
+ proxy: "RegistryProxy",
212
+ },
213
+ "K3d::Options": {
214
+ k3d: "K3dOptions",
215
+ k3s: "K3sOptions",
216
+ kubeconfig: "KubeconfigOptions",
217
+ runtime: "RuntimeOptions",
218
+ },
219
+ "K3d::K3dOptions": {
220
+ loadbalancer: "LoadbalancerOptions",
221
+ },
222
+ "K3d::K3sOptions": {
223
+ extraArgs: "K3sExtraArg[]",
224
+ nodeLabels: "NodeLabel[]",
225
+ },
226
+ "K3d::RuntimeOptions": {
227
+ labels: "RuntimeLabel[]",
228
+ ulimits: "Ulimit[]",
229
+ },
230
+ };
231
+ // ── Helpers ───────────────────────────────────────────────────────
232
+ /**
233
+ * Extract short name: "K3d::Cluster" → "Cluster"
234
+ */
235
+ export function k3dShortName(typeName) {
236
+ const parts = typeName.split("::");
237
+ return parts[parts.length - 1];
238
+ }
239
+ /**
240
+ * Extract service name: always "K3d" — the lexicon is a single flat service.
241
+ */
242
+ export function k3dServiceName(_typeName) {
243
+ return "K3d";
244
+ }