@intentius/chant-lexicon-k3s 0.124.30 → 0.124.32

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 (45) hide show
  1. package/dist/codegen/docs-cli.js +3 -0
  2. package/dist/codegen/docs.js +83 -0
  3. package/dist/codegen/generate-cli.js +7 -0
  4. package/dist/codegen/generate-lexicon.js +39 -0
  5. package/dist/codegen/generate-typescript.js +38 -0
  6. package/dist/codegen/generate.js +93 -0
  7. package/dist/codegen/naming.js +34 -0
  8. package/dist/codegen/package.js +23 -0
  9. package/dist/codegen/parse.js +313 -0
  10. package/dist/describe-resources.js +200 -0
  11. package/dist/generated/index.js +10 -0
  12. package/dist/generated/runtime.js +4 -0
  13. package/dist/index.js +14 -0
  14. package/dist/lint/audit-catalog.js +16 -0
  15. package/dist/lint/audit-lineage.js +5 -0
  16. package/dist/lint/post-synth/index.js +16 -0
  17. package/dist/lint/post-synth/k3s-helpers.js +24 -0
  18. package/dist/lint/post-synth/k3s101.js +48 -0
  19. package/dist/lint/post-synth/k3s102.js +52 -0
  20. package/dist/lint/post-synth/k3s103.js +30 -0
  21. package/dist/lint/post-synth/k3s104.js +37 -0
  22. package/dist/lint/post-synth/k3s105.js +44 -0
  23. package/dist/lint/post-synth/k3s106.js +47 -0
  24. package/dist/lint/post-synth/k3s107.js +58 -0
  25. package/dist/lint/rules/index.js +4 -0
  26. package/dist/lint/rules/token-literal.js +78 -0
  27. package/dist/lsp/completions.d.ts.map +1 -1
  28. package/dist/lsp/completions.js +19 -0
  29. package/dist/lsp/hover.d.ts.map +1 -1
  30. package/dist/lsp/hover.js +33 -0
  31. package/dist/op/activities/index.js +21 -0
  32. package/dist/op/activities/k3s.js +112 -0
  33. package/dist/op/builders.js +31 -0
  34. package/dist/package-cli.js +19 -0
  35. package/dist/plugin.d.ts.map +1 -1
  36. package/dist/plugin.js +119 -0
  37. package/dist/serializer.js +169 -0
  38. package/dist/spec/fetch.js +72 -0
  39. package/dist/validate-cli.js +4 -0
  40. package/dist/validate.js +35 -0
  41. package/package.json +7 -7
  42. package/src/codegen/package.ts +2 -2
  43. package/src/lsp/completions.ts +2 -1
  44. package/src/lsp/hover.ts +2 -1
  45. package/src/plugin.ts +2 -1
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env tsx
2
+ import { generateDocs } from "./docs.js";
3
+ await generateDocs({ verbose: true });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Documentation generation for the k3s lexicon.
3
+ *
4
+ * Generates Starlight MDX pages for k3s 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] : "K3s";
13
+ }
14
+ const overview = `The k3s lexicon declares the files k3s itself consumes: \`config.yaml\` for
15
+ \`k3s server\` / \`k3s agent\`, and \`registries.yaml\` for the embedded
16
+ containerd. The emitted files are exactly what the native tool accepts —
17
+ drop them at \`/etc/rancher/k3s/\` (or pass \`--config\`) and chant is nowhere
18
+ in sight. That walk-away property is the reason this is a lexicon at all.
19
+
20
+ \`\`\`bash
21
+ npm install --save-dev @intentius/chant @intentius/chant-lexicon-k3s
22
+ \`\`\`
23
+
24
+ \`\`\`typescript
25
+ // chant.config.ts
26
+ export default {
27
+ lexicons: ["k3s"],
28
+ };
29
+ \`\`\`
30
+
31
+ ## A two-node cluster
32
+
33
+ \`\`\`typescript
34
+ import { Agent, Server } from "@intentius/chant-lexicon-k3s";
35
+
36
+ export const controlPlane = new Server({
37
+ "cluster-init": true,
38
+ "tls-san": ["10.0.0.10", "cp.example.internal"],
39
+ "write-kubeconfig-mode": "0600",
40
+ });
41
+
42
+ export const worker = new Agent({
43
+ server: "https://cp.example.internal:6443",
44
+ "token-file": "/etc/rancher/k3s/agent-token",
45
+ });
46
+ \`\`\`
47
+
48
+ ## The token boundary
49
+
50
+ There is no \`token\` property, on purpose. The join secret reaches a node
51
+ as a file (\`token-file\`, \`agent-token-file\`) or as \`K3S_TOKEN\` /
52
+ \`K3S_TOKEN_FILE\` in the installer's environment — never as a value in
53
+ source. A literal that arrives through raw props anyway fails K3S001 at
54
+ lint and K3S101 at build; the same wall covers the etcd S3 snapshot
55
+ credentials (\`etcd-s3-config-secret\` is the reference form upstream
56
+ provides).
57
+
58
+ ## Relationship to k3d
59
+
60
+ The [k3d lexicon](/chant/lexicons/k3d/) declares a k3d-managed cluster —
61
+ k3s wrapped in Docker, config consumed by the \`k3d\` CLI. This lexicon
62
+ declares k3s on real hosts: Lima VMs, bare metal, cloud instances. The
63
+ acceptance test for both is the same idea — the native tool consuming the
64
+ emitted file is the proof.
65
+ `;
66
+ export async function generateDocs(opts) {
67
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
68
+ const config = {
69
+ name: "k3s",
70
+ displayName: "K3s",
71
+ description: "k3s host configuration (server/agent config.yaml and registries.yaml) as typed chant source",
72
+ distDir: join(pkgDir, "dist"),
73
+ outDir: join(pkgDir, "docs"),
74
+ basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/k3s/",
75
+ overview,
76
+ serviceFromType,
77
+ };
78
+ const result = docsPipeline(config);
79
+ writeDocsSite(config, result);
80
+ if (opts?.verbose) {
81
+ console.error(`Generated ${result.pages.size} documentation pages`);
82
+ }
83
+ }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env tsx
2
+ import { generate, writeGeneratedFiles } from "./generate.js";
3
+ import { dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
6
+ const result = await generate({ verbose: true });
7
+ writeGeneratedFiles(result, pkgDir);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Lexicon JSON generator — produces lexicon-k3s.json with metadata
3
+ * for all k3s entities.
4
+ */
5
+ import { k3sShortName } from "./parse.js";
6
+ import { buildRegistry, serializeRegistry, } from "@intentius/chant/codegen/generate-registry";
7
+ /**
8
+ * Generate the lexicon-k3s.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: k3sShortName,
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: "k3s",
25
+ ...(r?.resource.deprecatedProperties?.length && {
26
+ deprecatedProperties: r.resource.deprecatedProperties,
27
+ }),
28
+ ...(propConstraints &&
29
+ Object.keys(propConstraints).length > 0 && { constraints: propConstraints }),
30
+ };
31
+ },
32
+ buildPropertyEntry: (resourceType, propertyType) => ({
33
+ resourceType: `${resourceType}.${propertyType}`,
34
+ kind: "property",
35
+ lexicon: "k3s",
36
+ }),
37
+ });
38
+ return serializeRegistry(entries);
39
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * TypeScript declaration file generator — produces index.d.ts with all
3
+ * k3s entity classes.
4
+ */
5
+ import { writeResourceClass, } from "@intentius/chant/codegen/generate-typescript";
6
+ /**
7
+ * Generate the complete .d.ts file content.
8
+ */
9
+ export function generateTypeScriptDeclarations(results, naming) {
10
+ const lines = [];
11
+ lines.push("// Code generated by chant k3s generate. DO NOT EDIT.");
12
+ const resources = [];
13
+ for (const r of results) {
14
+ const typeName = r.resource.typeName;
15
+ const tsName = naming.resolve(typeName);
16
+ if (!tsName)
17
+ continue;
18
+ const dtsProps = r.resource.properties.map((p) => ({
19
+ name: p.name,
20
+ type: p.tsType,
21
+ required: p.required,
22
+ description: p.description,
23
+ }));
24
+ const dtsAttrs = r.resource.attributes.map((a) => ({
25
+ name: a.name,
26
+ type: a.tsType,
27
+ }));
28
+ resources.push({ tsName, properties: dtsProps, attributes: dtsAttrs, remap: new Map() });
29
+ }
30
+ resources.sort((a, b) => a.tsName.localeCompare(b.tsName));
31
+ lines.push("");
32
+ lines.push("// --- k3s Entity classes ---");
33
+ for (const re of resources) {
34
+ writeResourceClass(lines, re.tsName, re.properties, re.attributes, re.remap);
35
+ }
36
+ lines.push("");
37
+ return lines.join("\n");
38
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * k3s generation pipeline — uses core generatePipeline with k3s-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 { parseSpecFiles } from "./parse.js";
10
+ import { NamingStrategy } 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 k3s 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 = parseSpecFiles(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 spec`);
35
+ pendingResults = [];
36
+ }
37
+ for (const r of results) {
38
+ for (const w of r.warnings ?? [])
39
+ log(`warning: ${w}`);
40
+ }
41
+ const props = results.map((r) => `${r.resource.typeName}(${r.resource.properties.length})`);
42
+ log(`Total: ${results.length} k3s entities — ${props.join(", ")}`);
43
+ return { results };
44
+ },
45
+ generateRegistry: (results, naming) => {
46
+ return generateLexiconJSON(results, naming);
47
+ },
48
+ generateTypes: (results, naming) => {
49
+ return generateTypeScriptDeclarations(results, naming);
50
+ },
51
+ generateRuntimeIndex: (results, naming) => {
52
+ return generateRuntimeIndex(results, naming);
53
+ },
54
+ };
55
+ return generatePipeline(config, opts);
56
+ }
57
+ /**
58
+ * Write generated artifacts to disk.
59
+ */
60
+ export function writeGeneratedFiles(result, pkgDir) {
61
+ const baseDir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
62
+ writeGeneratedArtifacts({
63
+ baseDir,
64
+ files: {
65
+ "lexicon-k3s.json": result.lexiconJSON,
66
+ "index.d.ts": result.typesDTS,
67
+ "index.ts": result.indexTS,
68
+ "runtime.ts": `/**\n * Runtime factory constructors — re-exported from core.\n */\nexport { createResource, createProperty } from "@intentius/chant/runtime";\n`,
69
+ },
70
+ });
71
+ }
72
+ /**
73
+ * Generate the runtime index.ts with factory constructor exports.
74
+ */
75
+ function generateRuntimeIndex(results, naming) {
76
+ const resourceEntries = [];
77
+ const propertyEntries = [];
78
+ for (const r of results) {
79
+ const typeName = r.resource.typeName;
80
+ const tsName = naming.resolve(typeName);
81
+ if (!tsName)
82
+ continue;
83
+ if (r.isProperty) {
84
+ propertyEntries.push({ tsName, resourceType: typeName });
85
+ }
86
+ else {
87
+ resourceEntries.push({ tsName, resourceType: typeName, attrs: {} });
88
+ }
89
+ }
90
+ return coreGenerateRuntimeIndex(resourceEntries, propertyEntries, {
91
+ lexiconName: "k3s",
92
+ });
93
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * k3s naming strategy — flat namespace, one short name per entity.
3
+ */
4
+ import { NamingStrategy as CoreNamingStrategy, } from "@intentius/chant/codegen/naming";
5
+ import { k3sShortName, k3sServiceName } from "./parse.js";
6
+ export { propertyTypeName, extractDefName } from "@intentius/chant/codegen/naming";
7
+ const k3sNamingConfig = {
8
+ priorityNames: {
9
+ "K3s::Server": "Server",
10
+ "K3s::Agent": "Agent",
11
+ "K3s::Registries": "Registries",
12
+ "K3s::Mirror": "Mirror",
13
+ "K3s::RegistryConfig": "RegistryConfig",
14
+ "K3s::RegistryAuth": "RegistryAuth",
15
+ "K3s::RegistryTLS": "RegistryTLS",
16
+ },
17
+ priorityAliases: {},
18
+ priorityPropertyAliases: {},
19
+ serviceAbbreviations: {},
20
+ shortName: k3sShortName,
21
+ serviceName: k3sServiceName,
22
+ };
23
+ /**
24
+ * k3s naming strategy.
25
+ */
26
+ export class NamingStrategy extends CoreNamingStrategy {
27
+ constructor(results) {
28
+ const inputs = results.map((r) => ({
29
+ typeName: r.resource.typeName,
30
+ propertyTypes: r.propertyTypes,
31
+ }));
32
+ super(inputs, k3sNamingConfig);
33
+ }
34
+ }
@@ -0,0 +1,23 @@
1
+ import { packagePipeline, collectSkills } from "@intentius/chant/codegen/package";
2
+ import { k3sPlugin } from "../plugin.js";
3
+ import { generate } from "./generate.js";
4
+ import { moduleSourceDir } from "@intentius/chant/module-source";
5
+ /**
6
+ * Package the k3s lexicon for distribution.
7
+ */
8
+ export async function packageLexicon(options) {
9
+ const srcDir = moduleSourceDir(import.meta.url);
10
+ const { spec, stats } = await packagePipeline({
11
+ generate: (opts) => generate({ verbose: opts?.verbose, force: opts?.force }),
12
+ buildManifest: () => ({
13
+ name: "k3s",
14
+ version: "0.0.1",
15
+ chantVersion: ">=0.1.0",
16
+ namespace: "K3s",
17
+ }),
18
+ srcDir,
19
+ collectSkills: () => collectSkills(k3sPlugin.skills?.() ?? []),
20
+ });
21
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules`);
22
+ return { spec, stats };
23
+ }
@@ -0,0 +1,313 @@
1
+ /**
2
+ * k3s CLI flag-definition parser.
3
+ *
4
+ * The spec is the pinned Go source of pkg/cli/cmds (#1599): urfave/cli
5
+ * struct literals carrying Name, Usage, Value and Hidden. This parser
6
+ * extracts the server and agent flag lists, resolves shared flag-var
7
+ * references across the fetched files, and produces one entity per config
8
+ * document k3s consumes:
9
+ *
10
+ * K3s::Server — /etc/rancher/k3s/config.yaml for `k3s server`
11
+ * K3s::Agent — /etc/rancher/k3s/config.yaml for `k3s agent`
12
+ * K3s::Registries — /etc/rancher/k3s/registries.yaml
13
+ *
14
+ * registries.yaml has no flag surface; its shape is hand-typed from the
15
+ * documented structure (docs.k3s.io/installation/private-registry) and
16
+ * pinned by the same release constant.
17
+ *
18
+ * ## The token boundary (#1601)
19
+ *
20
+ * `token`, `agent-token` and the deprecated `cluster-secret` are excluded
21
+ * from the typed surface on purpose. A literal shared secret in an emitted
22
+ * config.yaml is the #1365 wall; the reference forms (`token-file`,
23
+ * `agent-token-file`, and the K3S_TOKEN/K3S_TOKEN_FILE env vars at install
24
+ * time) stay. K3SC001 catches a literal that arrives through a raw prop
25
+ * anyway.
26
+ */
27
+ /**
28
+ * Config keys that must not reach the typed surface.
29
+ *
30
+ * - `config`: the --config flag itself — a config file naming its own path
31
+ * is not a property.
32
+ * - `token` / `agent-token` / `cluster-secret`: literal shared secrets;
33
+ * see the module doc.
34
+ * - `help`: urfave built-in.
35
+ */
36
+ const SKIP_KEYS = new Set(["config", "token", "agent-token", "cluster-secret", "help"]);
37
+ /**
38
+ * Extract every `&cli.<T>Flag{...}` literal in a Go source text, keyed by
39
+ * the variable name it is assigned to when there is one.
40
+ */
41
+ export function extractFlagLiterals(text) {
42
+ const byVar = new Map();
43
+ const all = [];
44
+ const re = /(?:([A-Za-z_][A-Za-z0-9_]*)\s*=\s*)?&cli\.([A-Za-z0-9]+)Flag\{/g;
45
+ let m;
46
+ while ((m = re.exec(text)) !== null) {
47
+ const bodyStart = m.index + m[0].length;
48
+ const body = balancedBody(text, bodyStart);
49
+ if (body === null)
50
+ continue;
51
+ const def = parseFlagBody(m[2], body);
52
+ if (m[1]) {
53
+ def.varName = m[1];
54
+ byVar.set(m[1], def);
55
+ }
56
+ all.push(def);
57
+ re.lastIndex = bodyStart + body.length;
58
+ }
59
+ return { byVar, all };
60
+ }
61
+ /** Return the text between an opening brace's position and its matching `}` (exclusive). */
62
+ function balancedBody(text, start) {
63
+ let depth = 1;
64
+ let inString = false;
65
+ for (let i = start; i < text.length; i++) {
66
+ const c = text[i];
67
+ if (inString) {
68
+ if (c === "\\")
69
+ i++;
70
+ else if (c === '"')
71
+ inString = false;
72
+ continue;
73
+ }
74
+ if (c === '"')
75
+ inString = true;
76
+ else if (c === "{")
77
+ depth++;
78
+ else if (c === "}") {
79
+ depth--;
80
+ if (depth === 0)
81
+ return text.slice(start, i);
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ function parseFlagBody(flagType, body) {
87
+ const def = { flagType, hidden: /(^|\n)\s*Hidden:\s*true\s*,/.test(body) };
88
+ const name = body.match(/(^|\n)\s*Name:\s*"([^"]+)"/);
89
+ if (name)
90
+ def.name = name[2];
91
+ // Usage may be a multi-line concatenation of string literals; join every
92
+ // quoted segment between `Usage:` and the next field key at line start.
93
+ const usageStart = body.search(/(^|\n)\s*Usage:/);
94
+ if (usageStart >= 0) {
95
+ const rest = body.slice(usageStart);
96
+ const end = rest.slice(1).search(/\n\s*[A-Z][A-Za-z]*:\s/);
97
+ const segment = end >= 0 ? rest.slice(0, end + 1) : rest;
98
+ const parts = [...segment.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((s) => s[1]);
99
+ if (parts.length > 0)
100
+ def.usage = parts.join("").replace(/\\"/g, '"');
101
+ }
102
+ const value = body.match(/(^|\n)\s*Value:\s*([^,\n]+)/);
103
+ if (value) {
104
+ const raw = value[2].trim();
105
+ const str = raw.match(/^"([^"]*)"$/);
106
+ if (str)
107
+ def.value = str[1];
108
+ else if (/^[\d.]+$/.test(raw))
109
+ def.value = raw;
110
+ else if (/^(true|false)$/.test(raw))
111
+ def.value = raw;
112
+ // Composite defaults (cli.NewStringSlice(...), consts) are dropped —
113
+ // they document poorly as a bare expression.
114
+ }
115
+ return def;
116
+ }
117
+ /**
118
+ * Extract the ordered item list of a `[]cli.Flag{...}` block starting at
119
+ * `startIndex` (the index of the opening brace + 1). Items are either
120
+ * bare identifiers (shared flag vars) or inline `&cli.*Flag{...}` literals.
121
+ */
122
+ export function extractFlagList(text, marker) {
123
+ const idx = typeof marker === "string" ? text.indexOf(marker) : (text.match(marker)?.index ?? -1);
124
+ if (idx < 0)
125
+ return [];
126
+ const open = text.indexOf("{", idx + (typeof marker === "string" ? marker.length - 1 : 0));
127
+ if (open < 0)
128
+ return [];
129
+ const body = balancedBody(text, open + 1);
130
+ if (body === null)
131
+ return [];
132
+ const items = [];
133
+ const re = /(?:^|\n)\s*(?:([A-Za-z_][A-Za-z0-9_]*)\s*,|&cli\.([A-Za-z0-9]+)Flag\{)/g;
134
+ let m;
135
+ while ((m = re.exec(body)) !== null) {
136
+ if (m[1]) {
137
+ items.push(m[1]);
138
+ continue;
139
+ }
140
+ const bodyStart = m.index + m[0].length;
141
+ const flagBody = balancedBody(body, bodyStart);
142
+ if (flagBody === null)
143
+ continue;
144
+ items.push(parseFlagBody(m[2], flagBody));
145
+ re.lastIndex = bodyStart + flagBody.length;
146
+ }
147
+ return items;
148
+ }
149
+ // ── Flag → property mapping ────────────────────────────────────────
150
+ function tsTypeFor(flagType) {
151
+ switch (flagType) {
152
+ case "String":
153
+ return "string";
154
+ case "Int":
155
+ case "Int64":
156
+ case "Uint":
157
+ case "Float64":
158
+ return "number";
159
+ case "Bool":
160
+ return "boolean";
161
+ // config.yaml accepts a single value or a list for slice flags.
162
+ case "StringSlice":
163
+ return "string | string[]";
164
+ case "IntSlice":
165
+ return "number | number[]";
166
+ // Durations are Go duration strings ("12h", "5m") in YAML.
167
+ case "Duration":
168
+ return "string";
169
+ default:
170
+ return "string";
171
+ }
172
+ }
173
+ function toProperty(def) {
174
+ if (!def.name || def.hidden)
175
+ return null;
176
+ // urfave v2 keeps aliases in Aliases, not the Name — but keep the guard
177
+ // for a comma form arriving through an older definition.
178
+ const name = def.name.split(",")[0].trim();
179
+ if (SKIP_KEYS.has(name))
180
+ return null;
181
+ if (def.usage && /\(deprecated/i.test(def.usage))
182
+ return null;
183
+ let description = def.usage;
184
+ if (def.value !== undefined && def.value !== "" && description) {
185
+ description += ` (default: ${def.value})`;
186
+ }
187
+ return {
188
+ name,
189
+ tsType: tsTypeFor(def.flagType),
190
+ required: false,
191
+ description,
192
+ constraints: {},
193
+ };
194
+ }
195
+ function resolveList(items, byVar, warnings, context) {
196
+ const props = [];
197
+ const seen = new Set();
198
+ for (const item of items) {
199
+ let def;
200
+ if (typeof item === "string") {
201
+ def = byVar.get(item);
202
+ if (!def) {
203
+ warnings.push(`${context}: unresolved flag reference ${item}`);
204
+ continue;
205
+ }
206
+ }
207
+ else {
208
+ def = item;
209
+ }
210
+ const prop = toProperty(def);
211
+ if (!prop || seen.has(prop.name))
212
+ continue;
213
+ seen.add(prop.name);
214
+ props.push(prop);
215
+ }
216
+ return props;
217
+ }
218
+ // ── Hand-typed registries.yaml surface ─────────────────────────────
219
+ function prop(name, tsType, description, required = false) {
220
+ return { name, tsType, required, description, constraints: {} };
221
+ }
222
+ function registriesEntities() {
223
+ const mk = (typeName, description, properties, isProperty = true) => ({
224
+ resource: { typeName, description, properties, attributes: [], deprecatedProperties: [] },
225
+ propertyTypes: [],
226
+ enums: [],
227
+ isProperty,
228
+ });
229
+ return [
230
+ mk("K3s::Registries", "Private registry configuration (/etc/rancher/k3s/registries.yaml) for the embedded containerd", [
231
+ prop("mirrors", "Record<string, Mirror>", "Registry name (or wildcard) to mirror configuration"),
232
+ prop("configs", "Record<string, RegistryConfig>", "Registry endpoint to auth/TLS configuration"),
233
+ ], false),
234
+ mk("K3s::Mirror", "Endpoint rewrites for one registry", [
235
+ prop("endpoint", "string | string[]", "Mirror endpoint URLs, tried in order"),
236
+ prop("rewrite", "Record<string, string>", "Image name rewrite rules (regex to replacement)"),
237
+ ]),
238
+ mk("K3s::RegistryConfig", "Auth and TLS for one registry endpoint", [
239
+ prop("auth", "RegistryAuth", "Credentials for the registry"),
240
+ prop("tls", "RegistryTLS", "TLS settings for the registry"),
241
+ ]),
242
+ mk("K3s::RegistryAuth", "Registry credentials — prefer out-of-band credential helpers; K3SC002 flags literals", [
243
+ prop("username", "string", "Basic auth username"),
244
+ prop("password", "string", "Basic auth password. A literal here fails K3SC002"),
245
+ prop("token", "string", "Bearer token. A literal here fails K3SC002"),
246
+ ]),
247
+ mk("K3s::RegistryTLS", "Registry TLS settings", [
248
+ prop("cert_file", "string", "Client certificate path on the node"),
249
+ prop("key_file", "string", "Client key path on the node"),
250
+ prop("ca_file", "string", "CA bundle path on the node"),
251
+ prop("insecure_skip_verify", "boolean", "Skip TLS verification. K3SS002 flags this"),
252
+ ]),
253
+ ];
254
+ }
255
+ // ── Entry point ────────────────────────────────────────────────────
256
+ /**
257
+ * Parse the fetched spec envelope (filename → Go source) into the k3s
258
+ * entity results.
259
+ */
260
+ export function parseSpecFiles(data) {
261
+ const files = JSON.parse(typeof data === "string" ? data : data.toString("utf-8"));
262
+ const combined = Object.values(files).join("\n");
263
+ const warnings = [];
264
+ const { byVar } = extractFlagLiterals(combined);
265
+ const serverItems = extractFlagList(files["server.go"] ?? "", "var ServerFlags = []cli.Flag{");
266
+ const serverProps = resolveList(serverItems, byVar, warnings, "ServerFlags");
267
+ const agentItems = extractFlagList(files["agent.go"] ?? "", /Flags:\s*\[\]cli\.Flag\{/);
268
+ const agentProps = resolveList(agentItems, byVar, warnings, "AgentFlags");
269
+ // An agent is nothing without the server it joins. `token-file` stays
270
+ // optional because the join secret may arrive as K3S_TOKEN_FILE at
271
+ // install time instead (#1601).
272
+ for (const p of agentProps) {
273
+ if (p.name === "server")
274
+ p.required = true;
275
+ }
276
+ const results = [
277
+ {
278
+ resource: {
279
+ typeName: "K3s::Server",
280
+ description: `k3s server configuration (/etc/rancher/k3s/config.yaml) — every \`k3s server\` flag as a YAML key`,
281
+ properties: serverProps,
282
+ attributes: [],
283
+ deprecatedProperties: [],
284
+ },
285
+ propertyTypes: [],
286
+ enums: [],
287
+ warnings,
288
+ },
289
+ {
290
+ resource: {
291
+ typeName: "K3s::Agent",
292
+ description: `k3s agent configuration (/etc/rancher/k3s/config.yaml) — every \`k3s agent\` flag as a YAML key`,
293
+ properties: agentProps,
294
+ attributes: [],
295
+ deprecatedProperties: [],
296
+ },
297
+ propertyTypes: [],
298
+ enums: [],
299
+ },
300
+ ...registriesEntities(),
301
+ ];
302
+ return results;
303
+ }
304
+ // ── Helpers ────────────────────────────────────────────────────────
305
+ /** Extract short name: "K3s::Server" → "Server" */
306
+ export function k3sShortName(typeName) {
307
+ const parts = typeName.split("::");
308
+ return parts[parts.length - 1];
309
+ }
310
+ /** Extract service name: always "K3s" — the lexicon is a single flat service. */
311
+ export function k3sServiceName(_typeName) {
312
+ return "K3s";
313
+ }