@shepherdjerred/helm-types 0.0.0-dev.706

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/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
+
19
+ // src/index.ts
20
+ var HELM_TYPES_PACKAGE_VERSION = "1.1.0";
21
+ export {
22
+ HELM_TYPES_PACKAGE_VERSION
23
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@shepherdjerred/helm-types",
3
+ "version": "0.0.0-dev.706",
4
+ "description": "Generate TypeScript types from Helm chart values.yaml and values.schema.json",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "helm-types": "dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "!src/**/*.test.ts",
21
+ "!src/__snapshots__"
22
+ ],
23
+ "scripts": {
24
+ "build": "bun build src/index.ts src/cli.ts --outdir dist --target node",
25
+ "generate": "bun run src/cli.ts",
26
+ "test": "bun test",
27
+ "test:helm-types": "bun test src/helm-types.test.ts",
28
+ "test:cli": "bun test src/cli.test.ts",
29
+ "lint": "bunx eslint .",
30
+ "lint:fix": "bunx eslint . --fix",
31
+ "typecheck": "bunx tsc --noEmit",
32
+ "prepublishOnly": "bun run build"
33
+ },
34
+ "keywords": [
35
+ "helm",
36
+ "kubernetes",
37
+ "typescript",
38
+ "types",
39
+ "codegen",
40
+ "k8s"
41
+ ],
42
+ "author": "Jerred Shepherd",
43
+ "license": "GPL-3.0",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/shepherdjerred/homelab.git",
47
+ "directory": "src/helm-types"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/shepherdjerred/homelab/issues"
51
+ },
52
+ "homepage": "https://github.com/shepherdjerred/homelab/tree/main/src/helm-types#readme",
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "engines": {
57
+ "node": ">=18"
58
+ },
59
+ "dependencies": {
60
+ "yaml": "^2.8.1",
61
+ "zod": "^4.1.11"
62
+ },
63
+ "devDependencies": {
64
+ "jiti": "^2.6.1"
65
+ }
66
+ }
@@ -0,0 +1,171 @@
1
+ // Using Bun.$ for path operations instead of node:path
2
+ import { parse as yamlParse } from "yaml";
3
+ import type { ChartInfo, JSONSchemaProperty } from "./types.ts";
4
+ import { HelmValueSchema, RecordSchema, ErrorSchema } from "./schemas.ts";
5
+ import type { HelmValue } from "./schemas.ts";
6
+ import { parseYAMLComments } from "./yaml-comments.ts";
7
+
8
+ /**
9
+ * Load JSON schema if it exists in the chart
10
+ */
11
+ async function loadJSONSchema(
12
+ chartPath: string,
13
+ ): Promise<JSONSchemaProperty | null> {
14
+ try {
15
+ const schemaPath = `${chartPath}/values.schema.json`;
16
+ const schemaContent = await Bun.file(schemaPath).text();
17
+ const parsed: unknown = JSON.parse(schemaContent);
18
+ // Validate that parsed is an object
19
+ const recordCheck = RecordSchema.safeParse(parsed);
20
+ if (!recordCheck.success) {
21
+ return null;
22
+ }
23
+ // Note: JSONSchemaProperty is a structural type
24
+ const schema: JSONSchemaProperty = recordCheck.data;
25
+ console.log(` 📋 Loaded values.schema.json`);
26
+ return schema;
27
+ } catch {
28
+ // Schema doesn't exist or couldn't be parsed - that's okay
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Run a command and return its output using Bun
35
+ */
36
+ async function runCommand(command: string, args: string[]): Promise<string> {
37
+ try {
38
+ const proc = Bun.spawn([command, ...args], {
39
+ stdout: "pipe",
40
+ stderr: "inherit",
41
+ });
42
+
43
+ const output = await new Response(proc.stdout).text();
44
+ const exitCode = await proc.exited;
45
+
46
+ if (exitCode === 0) {
47
+ return output;
48
+ } else {
49
+ throw new Error(
50
+ `Command "${command} ${args.join(" ")}" failed with code ${exitCode.toString()}`,
51
+ );
52
+ }
53
+ } catch (error) {
54
+ const parseResult = ErrorSchema.safeParse(error);
55
+ const errorMessage = parseResult.success
56
+ ? parseResult.data.message
57
+ : String(error);
58
+ throw new Error(
59
+ `Failed to spawn command "${command} ${args.join(" ")}": ${errorMessage}`,
60
+ );
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Fetch a Helm chart and extract its values.yaml and optional schema
66
+ */
67
+ export async function fetchHelmChart(chart: ChartInfo): Promise<{
68
+ values: HelmValue;
69
+ schema: JSONSchemaProperty | null;
70
+ yamlComments: Map<string, string>;
71
+ }> {
72
+ const pwd = Bun.env["PWD"] ?? process.cwd();
73
+ const tempDir = `${pwd}/temp/helm-${chart.name}`;
74
+ const repoName = `temp-repo-${chart.name}-${String(Date.now())}`;
75
+
76
+ try {
77
+ // Ensure temp directory exists
78
+ await Bun.$`mkdir -p ${tempDir}`.quiet();
79
+
80
+ console.log(` 📦 Adding Helm repo: ${chart.repoUrl}`);
81
+ // Add the helm repo
82
+ await runCommand("helm", ["repo", "add", repoName, chart.repoUrl]);
83
+
84
+ console.log(` 🔄 Updating Helm repos...`);
85
+ // Update repo
86
+ await runCommand("helm", ["repo", "update"]);
87
+
88
+ console.log(` ⬇️ Pulling chart ${chart.chartName}:${chart.version}...`);
89
+ // Pull the chart
90
+ await runCommand("helm", [
91
+ "pull",
92
+ `${repoName}/${chart.chartName}`,
93
+ "--version",
94
+ chart.version,
95
+ "--destination",
96
+ tempDir,
97
+ "--untar",
98
+ ]);
99
+
100
+ // Read values.yaml
101
+ const valuesPath = `${tempDir}/${chart.chartName}/values.yaml`;
102
+ console.log(` 📖 Reading values.yaml from ${valuesPath}`);
103
+
104
+ try {
105
+ const valuesContent = await Bun.file(valuesPath).text();
106
+
107
+ // Parse YAML comments
108
+ const yamlComments = parseYAMLComments(valuesContent);
109
+ console.log(
110
+ ` 💬 Extracted ${String(yamlComments.size)} comments from values.yaml`,
111
+ );
112
+
113
+ // Parse YAML using yaml package
114
+ const parsedValues = yamlParse(valuesContent) as unknown;
115
+ console.log(` ✅ Successfully parsed values.yaml`);
116
+ const recordParseResult = RecordSchema.safeParse(parsedValues);
117
+ if (recordParseResult.success) {
118
+ console.log(
119
+ ` 🔍 Parsed values keys: ${Object.keys(recordParseResult.data)
120
+ .slice(0, 10)
121
+ .join(
122
+ ", ",
123
+ )}${Object.keys(recordParseResult.data).length > 10 ? "..." : ""}`,
124
+ );
125
+ }
126
+
127
+ // Check if parsedValues is a valid object using Zod before validation
128
+ if (!recordParseResult.success) {
129
+ console.warn(
130
+ ` ⚠️ Parsed values is not a valid record object: ${String(parsedValues)}`,
131
+ );
132
+ return { values: {}, schema: null, yamlComments: new Map() };
133
+ }
134
+
135
+ // Validate and parse with Zod for runtime type safety
136
+ const parseResult = HelmValueSchema.safeParse(recordParseResult.data);
137
+
138
+ // Try to load JSON schema
139
+ const chartPath = `${tempDir}/${chart.chartName}`;
140
+ const schema = await loadJSONSchema(chartPath);
141
+
142
+ if (parseResult.success) {
143
+ console.log(` ✅ Zod validation successful`);
144
+ return { values: parseResult.data, schema, yamlComments };
145
+ } else {
146
+ console.warn(` ⚠️ Zod validation failed for ${chart.name}:`);
147
+ console.warn(
148
+ ` First few errors:`,
149
+ parseResult.error.issues.slice(0, 3),
150
+ );
151
+ console.warn(
152
+ ` ⚠️ Falling back to unvalidated object for type generation`,
153
+ );
154
+ // Return the validated record data from the successful parse result
155
+ return { values: recordParseResult.data, schema, yamlComments };
156
+ }
157
+ } catch (error) {
158
+ console.warn(` ⚠️ Failed to read/parse values.yaml: ${String(error)}`);
159
+ return { values: {}, schema: null, yamlComments: new Map() };
160
+ }
161
+ } finally {
162
+ // Cleanup
163
+ try {
164
+ console.log(` 🧹 Cleaning up...`);
165
+ await runCommand("helm", ["repo", "remove", repoName]);
166
+ await Bun.$`rm -rf ${tempDir}`.quiet();
167
+ } catch (cleanupError) {
168
+ console.warn(`Cleanup failed for ${chart.name}:`, String(cleanupError));
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,72 @@
1
+ import type { ChartInfo } from "./types.ts";
2
+
3
+ /**
4
+ * Parse chart information from versions.ts comments and values
5
+ */
6
+ export async function parseChartInfoFromVersions(
7
+ versionsPath = "src/versions.ts",
8
+ ): Promise<ChartInfo[]> {
9
+ const content = await Bun.file(versionsPath).text();
10
+ const lines = content.split("\n");
11
+ const charts: ChartInfo[] = [];
12
+
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const line = lines[i];
15
+ const nextLine = lines[i + 1];
16
+
17
+ // Look for renovate comments that indicate Helm charts
18
+ if (
19
+ line == null ||
20
+ !line.includes("renovate: datasource=helm") ||
21
+ nextLine == null ||
22
+ nextLine === ""
23
+ ) {
24
+ continue;
25
+ }
26
+
27
+ const repoUrlMatch = /registryUrl=(\S+)/.exec(line);
28
+ const versionKeyMatch = /^\s*"?([^":\s]+)"?:/.exec(nextLine);
29
+ if (!repoUrlMatch || !versionKeyMatch) {
30
+ continue;
31
+ }
32
+
33
+ const repoUrl = repoUrlMatch[1];
34
+ const versionKey = versionKeyMatch[1];
35
+ if (
36
+ repoUrl == null ||
37
+ repoUrl === "" ||
38
+ versionKey == null ||
39
+ versionKey === ""
40
+ ) {
41
+ continue;
42
+ }
43
+
44
+ // Extract version value
45
+ const versionMatch = /:\s*"([^"]+)"/.exec(nextLine);
46
+ if (!versionMatch) {
47
+ continue;
48
+ }
49
+
50
+ const version = versionMatch[1];
51
+ if (version == null || version === "") {
52
+ continue;
53
+ }
54
+
55
+ // Try to determine chart name from the version key or URL
56
+ let chartName = versionKey;
57
+
58
+ // Handle special cases like "argo-cd" vs "argocd"
59
+ if (versionKey === "argo-cd") {
60
+ chartName = "argo-cd";
61
+ }
62
+
63
+ charts.push({
64
+ name: versionKey,
65
+ repoUrl: repoUrl.replace(/\/$/, ""), // Remove trailing slash
66
+ version,
67
+ chartName,
68
+ });
69
+ }
70
+
71
+ return charts;
72
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * CLI for @homelab/helm-types
4
+ *
5
+ * Generate TypeScript types from Helm charts
6
+ */
7
+ import { z } from "zod";
8
+ import { fetchHelmChart } from "./chart-fetcher.ts";
9
+ import { convertToTypeScriptInterface } from "./type-converter.ts";
10
+ import { generateTypeScriptCode } from "./interface-generator.ts";
11
+ import type { ChartInfo } from "./types.ts";
12
+
13
+ const ErrorSchema = z.object({
14
+ message: z.string(),
15
+ });
16
+
17
+ const HELP_TEXT = String.raw`
18
+ helm-types - Generate TypeScript types from Helm charts
19
+
20
+ USAGE:
21
+ bunx @homelab/helm-types [options]
22
+
23
+ OPTIONS:
24
+ --name, -n Unique identifier for the chart (required)
25
+ --chart, -c Chart name in the repository (defaults to --name)
26
+ --repo, -r Helm repository URL (required)
27
+ --version, -v Chart version (required)
28
+ --output, -o Output file path (defaults to stdout)
29
+ --interface, -i Interface name (auto-generated from chart name if not provided)
30
+ --help, -h Show this help message
31
+
32
+ EXAMPLES:
33
+ # Generate types for ArgoCD and print to stdout
34
+ bunx @homelab/helm-types \
35
+ --name argo-cd \
36
+ --repo https://argoproj.github.io/argo-helm \
37
+ --version 8.3.1
38
+
39
+ # Generate types with custom output file
40
+ bunx @homelab/helm-types \
41
+ --name argo-cd \
42
+ --repo https://argoproj.github.io/argo-helm \
43
+ --version 8.3.1 \
44
+ --output argo-cd.types.ts
45
+
46
+ # Generate types with custom chart name and interface name
47
+ bunx @homelab/helm-types \
48
+ --name argocd \
49
+ --chart argo-cd \
50
+ --repo https://argoproj.github.io/argo-helm \
51
+ --version 8.3.1 \
52
+ --interface ArgocdHelmValues \
53
+ --output argocd.types.ts
54
+ `;
55
+
56
+ type CliArgs = {
57
+ name?: string;
58
+ chart?: string;
59
+ repo?: string;
60
+ version?: string;
61
+ output?: string;
62
+ interface?: string;
63
+ help?: boolean;
64
+ };
65
+
66
+ /** String-valued flag names (excludes boolean 'help') */
67
+ type StringCliArgsKey = Exclude<keyof CliArgs, "help">;
68
+
69
+ /** Map from flag name to CliArgs key */
70
+ const FLAG_MAP: Record<string, StringCliArgsKey> = {
71
+ "--name": "name",
72
+ "-n": "name",
73
+ "--chart": "chart",
74
+ "-c": "chart",
75
+ "--repo": "repo",
76
+ "-r": "repo",
77
+ "--version": "version",
78
+ "-v": "version",
79
+ "--output": "output",
80
+ "-o": "output",
81
+ "--interface": "interface",
82
+ "-i": "interface",
83
+ };
84
+
85
+ const HELP_FLAGS = new Set(["--help", "-h"]);
86
+
87
+ /**
88
+ * Simple argument parser for Bun CLI
89
+ */
90
+ function parseCliArgs(args: string[]): CliArgs {
91
+ const result: CliArgs = {};
92
+
93
+ for (let i = 0; i < args.length; i++) {
94
+ const arg = args[i];
95
+ if (arg == null || arg === "") {
96
+ continue;
97
+ }
98
+
99
+ if (HELP_FLAGS.has(arg)) {
100
+ result.help = true;
101
+ continue;
102
+ }
103
+
104
+ const key = FLAG_MAP[arg];
105
+ if (key != null) {
106
+ const value = args[i + 1];
107
+ if (value != null && value !== "") {
108
+ result[key] = value;
109
+ i += 1;
110
+ }
111
+ continue;
112
+ }
113
+
114
+ if (arg.startsWith("-")) {
115
+ throw new Error(`Unknown argument: ${arg}`);
116
+ }
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ async function main() {
123
+ try {
124
+ const args = parseCliArgs(Bun.argv.slice(2));
125
+
126
+ // Show help
127
+ if (args.help === true) {
128
+ console.log(HELP_TEXT);
129
+ process.exit(0);
130
+ }
131
+
132
+ // Validate required arguments
133
+ if (
134
+ args.name == null ||
135
+ args.name === "" ||
136
+ args.repo == null ||
137
+ args.repo === "" ||
138
+ args.version == null ||
139
+ args.version === ""
140
+ ) {
141
+ console.error("Error: Missing required arguments");
142
+ console.error("Required: --name, --repo, --version");
143
+ console.error("\nRun with --help for usage information");
144
+ process.exit(1);
145
+ }
146
+
147
+ // Build chart info
148
+ const chartInfo: ChartInfo = {
149
+ name: args.name,
150
+ chartName: args.chart ?? args.name,
151
+ repoUrl: args.repo,
152
+ version: args.version,
153
+ };
154
+
155
+ // Generate interface name from chart name if not provided
156
+ const interfaceName =
157
+ args.interface ?? `${toPascalCase(args.name)}HelmValues`;
158
+
159
+ console.error(
160
+ `Fetching chart: ${chartInfo.chartName}@${chartInfo.version}`,
161
+ );
162
+ console.error(`Repository: ${chartInfo.repoUrl}`);
163
+ console.error("");
164
+
165
+ // Fetch chart
166
+ const { values, schema, yamlComments } = await fetchHelmChart(chartInfo);
167
+
168
+ console.error("");
169
+ console.error(`Converting to TypeScript interface: ${interfaceName}`);
170
+
171
+ // Convert to TypeScript interface
172
+ const tsInterface = convertToTypeScriptInterface({
173
+ values,
174
+ interfaceName,
175
+ schema,
176
+ yamlComments,
177
+ chartName: args.name,
178
+ });
179
+
180
+ // Generate TypeScript code
181
+ const code = generateTypeScriptCode(tsInterface, args.name);
182
+
183
+ // Write to file or stdout
184
+ if (args.output != null && args.output !== "") {
185
+ await Bun.write(args.output, code);
186
+ console.error("");
187
+ console.error(`✅ Types written to: ${args.output}`);
188
+ } else {
189
+ // Write to stdout (so it can be piped)
190
+ console.log(code);
191
+ }
192
+
193
+ process.exit(0);
194
+ } catch (error) {
195
+ const parseResult = ErrorSchema.safeParse(error);
196
+ if (parseResult.success) {
197
+ console.error(`Error: ${parseResult.data.message}`);
198
+ } else {
199
+ console.error(`Error: ${String(error)}`);
200
+ }
201
+ process.exit(1);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Convert a string to PascalCase
207
+ */
208
+ function toPascalCase(str: string): string {
209
+ return str
210
+ .split(/[-_\s]+/)
211
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
212
+ .join("");
213
+ }
214
+
215
+ void main();