@shepherdjerred/helm-types 1.1.0

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,799 @@
1
+ import { parseDocument } from "yaml";
2
+ import { z } from "zod";
3
+
4
+ /**
5
+ * Metadata about how a comment was extracted
6
+ */
7
+ export type CommentMetadata = {
8
+ source: "AST" | "REGEX";
9
+ rawComment?: string;
10
+ indent?: number;
11
+ debugInfo?: string;
12
+ };
13
+
14
+ /**
15
+ * Comment with metadata for debugging
16
+ */
17
+ export type CommentWithMetadata = {
18
+ text: string;
19
+ metadata: CommentMetadata;
20
+ };
21
+
22
+ /**
23
+ * Helper: Check if a line looks like a YAML key (e.g., "key: value" or "key:")
24
+ * Exported for testing purposes
25
+ */
26
+ export function isYAMLKey(line: string): boolean {
27
+ return /^[\w.-]+:\s*(\||$)/.test(line);
28
+ }
29
+
30
+ /**
31
+ * Helper: Check if a line looks like a simple YAML value assignment
32
+ * Exported for testing purposes
33
+ */
34
+ export function isSimpleYAMLValue(line: string): boolean {
35
+ const hasURL = line.includes("http://") || line.includes("https://");
36
+ const isRef = /^ref:/i.test(line);
37
+ return /^[\w.-]+:\s+[^:]+$/.test(line) && !hasURL && !isRef;
38
+ }
39
+
40
+ /**
41
+ * Helper: Check if a line is a section header (short line followed by YAML config)
42
+ * Exported for testing purposes
43
+ */
44
+ export function isSectionHeader(line: string, nextLine: string | undefined): boolean {
45
+ if (!nextLine) return false;
46
+
47
+ const isFollowedByYAMLKey =
48
+ /^[\w.-]+:\s*\|/.test(nextLine) || /^[\w.-]+:\s*$/.test(nextLine) || /^[\w.-]+:\s+/.test(nextLine);
49
+
50
+ if (!isFollowedByYAMLKey) return false;
51
+
52
+ const wordCount = line.split(/\s+/).length;
53
+ const hasConfigKeywords = /\b(configuration|config|example|setup|settings?|options?|alternative)\b/i.test(line);
54
+ const endsWithPunctuation = /[.!?]$/.test(line);
55
+ const hasURL = line.includes("http://") || line.includes("https://");
56
+ const startsWithArticle = /^(This|The|A|An)\s/i.test(line);
57
+ const startsWithCommonWord = /^(This|The|A|An|It|For|To|If|When|You|We|Use|Configure)\s/i.test(line);
58
+
59
+ return (
60
+ ((wordCount === 2 && !startsWithCommonWord) || (hasConfigKeywords && !startsWithCommonWord)) &&
61
+ !endsWithPunctuation &&
62
+ !hasURL &&
63
+ !/^ref:/i.test(line) &&
64
+ !startsWithArticle
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Helper: Check if a line looks like code/YAML example
70
+ * Exported for testing purposes
71
+ */
72
+ export function isCodeExample(line: string, wordCount: number): boolean {
73
+ const looksLikeYAMLKey = isYAMLKey(line);
74
+ const looksLikeSimpleYAMLValue = isSimpleYAMLValue(line);
75
+ const looksLikeYAMLList = line.startsWith("-") && (line.includes(":") || /^-\s+\|/.test(line));
76
+ const looksLikePolicyRule = /^[pg],\s*/.test(line);
77
+ const hasIndentation = /^\s{2,}/.test(line);
78
+ const looksLikeCommand = /^echo\s+/.test(line) || line.includes("$ARGOCD_") || line.includes("$KUBE_");
79
+ const isSeparator =
80
+ /^-{3,}/.test(line) || /^BEGIN .*(KEY|CERTIFICATE)/.test(line) || /^END .*(KEY|CERTIFICATE)/.test(line);
81
+
82
+ return (
83
+ isSeparator ||
84
+ looksLikeYAMLKey ||
85
+ (looksLikeSimpleYAMLValue && wordCount <= 4) ||
86
+ looksLikeYAMLList ||
87
+ looksLikePolicyRule ||
88
+ hasIndentation ||
89
+ looksLikeCommand ||
90
+ line.startsWith("|")
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Helper: Check if a line looks like prose (real documentation)
96
+ * Exported for testing purposes
97
+ */
98
+ export function looksLikeProse(line: string, wordCount: number): boolean {
99
+ const hasURL = line.includes("http://") || line.includes("https://");
100
+ const startsWithCapital = /^[A-Z]/.test(line);
101
+ const hasEndPunctuation = /[.!?:]$/.test(line);
102
+ const notYamlKey = !(isYAMLKey(line) && !hasURL && !/^ref:/i.test(line));
103
+ const reasonableLength = line.length > 10;
104
+ const hasMultipleWords = wordCount >= 3;
105
+ const startsWithArticle = /^(This|The|A|An)\s/i.test(line);
106
+
107
+ // Lines starting with markers like ^, ->, etc. are documentation references
108
+ const isReferenceMarker = /^(\^|->|→)\s/.test(line);
109
+
110
+ return (
111
+ (startsWithCapital || isReferenceMarker) &&
112
+ (hasEndPunctuation || hasURL || startsWithArticle || hasMultipleWords || isReferenceMarker) &&
113
+ notYamlKey &&
114
+ reasonableLength
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Helper: Normalize a comment line by removing markers
120
+ * Exported for testing purposes
121
+ */
122
+ export function normalizeCommentLine(line: string): string {
123
+ let normalized = line.trim();
124
+ normalized = normalized.replace(/^#+\s*/, ""); // Remove leading # symbols
125
+ normalized = normalized.replace(/^--\s*/, ""); // Remove Helm's -- marker
126
+ normalized = normalized.replace(/^@param\s+[\w.-]+\s+/, ""); // Remove Bitnami's @param prefix
127
+ normalized = normalized.replace(/^@section\s+/, ""); // Remove Bitnami's @section prefix
128
+ return normalized.trim();
129
+ }
130
+
131
+ /**
132
+ * Clean up YAML comment text for use in JSDoc
133
+ * Removes Helm-specific markers and filters out code examples and section headers
134
+ */
135
+ export function cleanYAMLComment(comment: string): string {
136
+ if (!comment) return "";
137
+
138
+ // Normalize all lines
139
+ const lines = comment.split("\n").map(normalizeCommentLine);
140
+
141
+ // Filter out code examples and section headers, keep documentation
142
+ const cleaned: string[] = [];
143
+ let inCodeBlock = false;
144
+ let inExample = false; // Track if we're in an "Example:" section (keep these!)
145
+
146
+ for (let i = 0; i < lines.length; i++) {
147
+ const line = lines[i] ?? "";
148
+
149
+ // Empty lines end code blocks (but not examples)
150
+ if (!line) {
151
+ if (inCodeBlock && !inExample) inCodeBlock = false;
152
+ continue;
153
+ }
154
+
155
+ // Skip @default lines (we'll generate our own)
156
+ if (line.startsWith("@default")) continue;
157
+
158
+ // Check if this line starts an Example section (preserve these!)
159
+ if (/^Example:?$/i.test(line.trim())) {
160
+ inExample = true;
161
+ inCodeBlock = true; // Treat examples as special code blocks
162
+ cleaned.push(line);
163
+ continue;
164
+ }
165
+
166
+ const nextLine = lines[i + 1];
167
+ const wordCount = line.split(/\s+/).length;
168
+
169
+ // Skip section headers
170
+ if (isSectionHeader(line, nextLine)) {
171
+ continue;
172
+ }
173
+
174
+ // If we're in an example section, keep all lines (including code)
175
+ if (inExample) {
176
+ cleaned.push(line);
177
+ // Check if we're exiting the example section
178
+ if (line.startsWith("For more information") || line.startsWith("Ref:")) {
179
+ inExample = false;
180
+ inCodeBlock = false;
181
+ }
182
+ continue;
183
+ }
184
+
185
+ // Check if this line is a code example
186
+ if (isCodeExample(line, wordCount)) {
187
+ inCodeBlock = true;
188
+ continue;
189
+ }
190
+
191
+ // Resume prose when we hit a proper sentence
192
+ if (inCodeBlock) {
193
+ if (looksLikeProse(line, wordCount)) {
194
+ inCodeBlock = false;
195
+ } else {
196
+ continue;
197
+ }
198
+ }
199
+
200
+ cleaned.push(line);
201
+ }
202
+
203
+ return cleaned.join("\n").trim();
204
+ }
205
+
206
+ /**
207
+ * Helper: Check if a line is part of a commented YAML block
208
+ */
209
+ function isPartOfYAMLBlock(line: string, trimmed: string): boolean {
210
+ return line.startsWith(" ") || line.startsWith("\t") || trimmed.startsWith("-") || trimmed.startsWith("#");
211
+ }
212
+
213
+ /**
214
+ * Helper: Check if this is a section header followed by a YAML key
215
+ */
216
+ function isSectionHeaderForCommentedBlock(nextLine: string | undefined): boolean {
217
+ if (!nextLine) return false;
218
+ const nextTrimmed = nextLine.trim();
219
+ return /^[\w.-]+:\s*(\||$)/.test(nextTrimmed);
220
+ }
221
+
222
+ /**
223
+ * Helper: Check if line indicates start of real documentation
224
+ */
225
+ function isRealDocumentation(trimmed: string): boolean {
226
+ return trimmed.startsWith("--") || trimmed.startsWith("#");
227
+ }
228
+
229
+ /**
230
+ * Filter out commented-out YAML blocks from the START of a comment string
231
+ * The YAML AST gives us ALL comments, including commented-out config sections
232
+ * We only remove these if they appear BEFORE the real documentation starts
233
+ */
234
+ function filterCommentedOutYAML(comment: string): string {
235
+ const lines = comment.split("\n");
236
+ let startIndex = 0;
237
+ let inCommentedBlock = false;
238
+ let hasSeenRealDoc = false;
239
+
240
+ // First pass: find where real documentation starts
241
+ for (let i = 0; i < lines.length; i++) {
242
+ const line = lines[i] ?? "";
243
+ const trimmed = line.trim();
244
+
245
+ // Blank line could be end of commented block
246
+ if (trimmed === "") {
247
+ if (inCommentedBlock) {
248
+ inCommentedBlock = false;
249
+ }
250
+ continue;
251
+ }
252
+
253
+ // Check if this looks like a commented-out YAML key
254
+ const looksLikeYAMLKey = isYAMLKey(trimmed);
255
+
256
+ if (looksLikeYAMLKey && !hasSeenRealDoc) {
257
+ // This starts a commented-out YAML block
258
+ inCommentedBlock = true;
259
+ continue;
260
+ }
261
+
262
+ // If we're in a commented block, check if this line is part of it
263
+ if (inCommentedBlock) {
264
+ if (isPartOfYAMLBlock(line, trimmed)) {
265
+ continue;
266
+ } else {
267
+ // This line doesn't look like YAML content, we're out of the block
268
+ inCommentedBlock = false;
269
+ hasSeenRealDoc = true;
270
+ startIndex = i;
271
+ }
272
+ } else if (!hasSeenRealDoc) {
273
+ const nextLine = lines[i + 1];
274
+
275
+ // Check if this is a section header for a commented-out block
276
+ if (isSectionHeaderForCommentedBlock(nextLine)) {
277
+ continue;
278
+ }
279
+
280
+ // Check if this line indicates real documentation
281
+ if (isRealDocumentation(trimmed)) {
282
+ hasSeenRealDoc = true;
283
+ startIndex = i;
284
+ } else {
285
+ // First real prose line
286
+ hasSeenRealDoc = true;
287
+ startIndex = i;
288
+ }
289
+ }
290
+ }
291
+
292
+ // Return everything from where real documentation starts
293
+ return lines
294
+ .slice(startIndex)
295
+ .map((l) => l.trim())
296
+ .join("\n");
297
+ }
298
+
299
+ /**
300
+ * Pre-process YAML to uncomment commented-out keys
301
+ * In Helm charts, commented-out keys are documentation of available options
302
+ * e.g., "## key: value" or "# key: value"
303
+ *
304
+ * This allows us to parse them as real keys and associate their comments
305
+ *
306
+ * Only uncomments keys that are:
307
+ * - At root level or similar indentation to real keys
308
+ * - Not part of "Example:" blocks
309
+ * - Not part of documentation prose (have their own dedicated comment block)
310
+ * - Not deeply nested (which would indicate example YAML)
311
+ *
312
+ * Exported for testing purposes
313
+ */
314
+ export function preprocessYAMLComments(yamlContent: string): string {
315
+ const lines = yamlContent.split("\n");
316
+ const processedLines: string[] = [];
317
+ let inExampleBlock = false;
318
+ let inBlockScalar = false; // Track if we're in a block scalar (| or >)
319
+ let lastRealKeyIndent = -1;
320
+ let consecutiveCommentedKeys = 0;
321
+
322
+ for (let i = 0; i < lines.length; i++) {
323
+ const line = lines[i] ?? "";
324
+ const trimmed = line.trim();
325
+
326
+ // Detect "Example:" markers (case-insensitive)
327
+ if (/^##?\s*Example:?$/i.test(trimmed)) {
328
+ inExampleBlock = true;
329
+ processedLines.push(line);
330
+ continue;
331
+ }
332
+
333
+ // Exit example block on blank line or "For more information"
334
+ if (inExampleBlock && (!trimmed || trimmed.startsWith("For more information") || trimmed.startsWith("Ref:"))) {
335
+ inExampleBlock = false;
336
+ }
337
+
338
+ // Detect if previous commented line had a block scalar indicator (| or >)
339
+ if (i > 0) {
340
+ const prevLine = lines[i - 1];
341
+ const prevTrimmed = prevLine?.trim() ?? "";
342
+ // Check if previous line is a commented key with block scalar
343
+ if (/^#+\s*[\w.-]+:\s*[|>]\s*$/.test(prevTrimmed)) {
344
+ inBlockScalar = true;
345
+ }
346
+ }
347
+
348
+ // Exit block scalar on non-indented line or blank line
349
+ if (inBlockScalar) {
350
+ const isIndented = trimmed && (line.startsWith(" ") || line.startsWith("\t") || /^#\s{2,}/.test(line));
351
+ if (!trimmed || !isIndented) {
352
+ inBlockScalar = false;
353
+ }
354
+ }
355
+
356
+ // Track indentation of real (uncommented) keys
357
+ if (!trimmed.startsWith("#") && /^[\w.-]+:/.test(trimmed)) {
358
+ lastRealKeyIndent = line.search(/\S/);
359
+ consecutiveCommentedKeys = 0;
360
+ }
361
+
362
+ // Don't uncomment if we're in an example block or block scalar
363
+ if (inExampleBlock || inBlockScalar) {
364
+ processedLines.push(line);
365
+ consecutiveCommentedKeys = 0;
366
+ continue;
367
+ }
368
+
369
+ // Check if this is a commented-out YAML key
370
+ // Pattern: one or more # followed by optional whitespace, then key: value
371
+ const commentedKeyMatch = /^([ \t]*)(#+)\s*([\w.-]+:\s*.*)$/.exec(line);
372
+
373
+ if (commentedKeyMatch) {
374
+ const [, indent, , keyValue] = commentedKeyMatch;
375
+
376
+ if (!keyValue || !indent) {
377
+ processedLines.push(line);
378
+ continue;
379
+ }
380
+
381
+ // Check if the key part looks like a valid YAML key (not prose)
382
+ const keyPart = keyValue.split(":")[0]?.trim() ?? "";
383
+ const isValidKey = /^[\w.-]+$/.test(keyPart);
384
+
385
+ // Don't uncomment documentation references like "ref: https://..."
386
+ const isDocReference = /^ref:/i.test(keyValue) && (keyValue.includes("http://") || keyValue.includes("https://"));
387
+
388
+ // Don't uncomment URLs (they might be continuation lines in multi-line Ref comments)
389
+ const isURL = keyValue.trim().startsWith("http://") || keyValue.trim().startsWith("https://");
390
+
391
+ if (isValidKey && !isDocReference && !isURL) {
392
+ const keyIndent = indent.length;
393
+
394
+ // Check the context: look at previous lines to see if this is part of prose
395
+ // If the previous line is prose (not a commented key or blank), this is likely an example
396
+ const prevLine = i > 0 ? lines[i - 1] : "";
397
+ const prevTrimmed = prevLine?.trim() ?? "";
398
+ const prevIsCommentedKey = /^#+\s*[\w.-]+:\s/.test(prevTrimmed);
399
+ const prevIsBlank = !prevTrimmed;
400
+ const prevIsListItem = prevTrimmed.startsWith("#") && prevTrimmed.substring(1).trim().startsWith("-");
401
+
402
+ // If previous line is prose or a list item, this is likely a YAML example
403
+ const likelyExample = (!prevIsBlank && !prevIsCommentedKey && prevTrimmed.startsWith("#")) || prevIsListItem;
404
+
405
+ // Also check if we're in a sequence of commented keys (good sign of commented-out config)
406
+ if (prevIsCommentedKey) {
407
+ consecutiveCommentedKeys++;
408
+ } else if (!prevIsBlank) {
409
+ consecutiveCommentedKeys = 0;
410
+ }
411
+
412
+ // Only uncomment if:
413
+ // 1. It's not likely an example (based on context)
414
+ // 2. AND (we haven't seen any real keys yet OR this key is at similar indent level)
415
+ // 3. OR we've seen multiple consecutive commented keys (likely a commented-out config block)
416
+ const shouldUncomment =
417
+ !likelyExample &&
418
+ (lastRealKeyIndent === -1 || Math.abs(keyIndent - lastRealKeyIndent) <= 4 || consecutiveCommentedKeys >= 2);
419
+
420
+ if (shouldUncomment) {
421
+ processedLines.push(`${indent}${keyValue}`);
422
+ continue;
423
+ }
424
+ }
425
+ }
426
+
427
+ // Reset consecutive count if not a commented key
428
+ if (!commentedKeyMatch) {
429
+ consecutiveCommentedKeys = 0;
430
+ }
431
+
432
+ // Keep the line as-is
433
+ processedLines.push(line);
434
+ }
435
+
436
+ return processedLines.join("\n");
437
+ }
438
+
439
+ /**
440
+ * Parse Bitnami-style @param directives from a comment
441
+ * Format: @param key.path Description
442
+ * Returns: Map of extracted params and remaining non-param lines
443
+ * Exported for testing purposes
444
+ */
445
+ export function parseBitnamiParams(comment: string): {
446
+ params: Map<string, string>;
447
+ remainingLines: string[];
448
+ } {
449
+ const lines = comment.split("\n");
450
+ const params = new Map<string, string>();
451
+ const remainingLines: string[] = [];
452
+
453
+ for (const line of lines) {
454
+ const trimmedLine = line
455
+ .trim()
456
+ .replace(/^#+\s*/, "")
457
+ .replace(/^--\s*/, "");
458
+
459
+ const paramMatch = /^@param\s+([\w.-]+)\s+(.+)$/.exec(trimmedLine);
460
+ if (paramMatch) {
461
+ const [, paramKey, description] = paramMatch;
462
+ if (paramKey && description) {
463
+ params.set(paramKey, description);
464
+ }
465
+ } else if (trimmedLine) {
466
+ remainingLines.push(trimmedLine);
467
+ }
468
+ }
469
+
470
+ return { params, remainingLines };
471
+ }
472
+
473
+ /**
474
+ * Regex-based fallback parser for comments that the YAML AST loses
475
+ * This handles cases with commented-out YAML keys and inconsistent indentation
476
+ * Returns comments with metadata for debugging
477
+ */
478
+ function parseCommentsWithRegex(yamlContent: string): Map<string, CommentWithMetadata> {
479
+ const comments = new Map<string, CommentWithMetadata>();
480
+ const lines = yamlContent.split("\n");
481
+ let pendingComment: string[] = [];
482
+ let pendingCommentIndent = -1;
483
+ let pendingDebugInfo: string[] = [];
484
+
485
+ lines.forEach((line, lineNum) => {
486
+ const trimmed = line.trim();
487
+
488
+ // Skip empty lines - blank lines reset pending comments
489
+ // This ensures comments for commented-out keys don't leak to next real key
490
+ if (!trimmed) {
491
+ // Only reset if we had a commented-out key (pendingCommentIndent === -1)
492
+ // This allows multi-line comments for real keys to work
493
+ if (pendingComment.length > 0 && pendingCommentIndent === -1) {
494
+ pendingDebugInfo.push(`Line ${String(lineNum)}: Blank line after commented-out key, resetting pending`);
495
+ pendingComment = [];
496
+ pendingDebugInfo = [];
497
+ }
498
+ return;
499
+ }
500
+
501
+ // Check if this is a comment line
502
+ if (trimmed.startsWith("#")) {
503
+ // Extract comment text (handle multiple # characters)
504
+ let commentText = trimmed;
505
+ while (commentText.startsWith("#")) {
506
+ commentText = commentText.substring(1);
507
+ }
508
+ commentText = commentText.trim();
509
+
510
+ // Skip commented-out YAML keys (these are not documentation)
511
+ // Match patterns like "key: value" or "key:" but NOT prose-like text
512
+ // This includes quoted values like: key: "value"
513
+ const looksLikeYAMLKey = /^[\w.-]+:\s*(\||$|[\w.-]+$|"[^"]*"$|'[^']*'$|\[|\{)/.test(commentText);
514
+
515
+ if (!looksLikeYAMLKey) {
516
+ const commentIndent = line.search(/\S/);
517
+
518
+ // If we just discarded comments (indent === -1), this is a fresh start
519
+ if (pendingCommentIndent === -1) {
520
+ pendingComment = [commentText];
521
+ pendingCommentIndent = commentIndent;
522
+ pendingDebugInfo = [
523
+ `Line ${String(lineNum)}: Fresh start (indent=${String(commentIndent)}): "${commentText}"`,
524
+ ];
525
+ } else if (commentIndent === pendingCommentIndent || Math.abs(commentIndent - pendingCommentIndent) <= 2) {
526
+ // Same indent level, add to pending
527
+ pendingComment.push(commentText);
528
+ pendingDebugInfo.push(
529
+ `Line ${String(lineNum)}: Continuing (indent=${String(commentIndent)}): "${commentText}"`,
530
+ );
531
+ } else {
532
+ // Different indent, reset
533
+ pendingDebugInfo.push(
534
+ `Line ${String(lineNum)}: Different indent (${String(commentIndent)} vs ${String(pendingCommentIndent)}), resetting`,
535
+ );
536
+ pendingComment = [commentText];
537
+ pendingCommentIndent = commentIndent;
538
+ pendingDebugInfo.push(`Line ${String(lineNum)}: New start: "${commentText}"`);
539
+ }
540
+ } else {
541
+ // This is a commented-out YAML key, which means the pending comments
542
+ // were describing this commented-out section, not a future real key
543
+ // So we should discard them
544
+ pendingDebugInfo.push(
545
+ `Line ${String(lineNum)}: Commented-out YAML key detected: "${commentText}", discarding pending`,
546
+ );
547
+ pendingComment = [];
548
+ pendingCommentIndent = -1;
549
+ pendingDebugInfo = [];
550
+ }
551
+ return;
552
+ }
553
+
554
+ // Check if this is a YAML key line
555
+ const keyMatchRegex = /^([\w.-]+):\s/;
556
+ const keyMatch = keyMatchRegex.exec(trimmed);
557
+ if (keyMatch && pendingComment.length > 0) {
558
+ const key = keyMatch[1];
559
+ if (key) {
560
+ const keyIndent = line.search(/\S/);
561
+
562
+ // Only associate comment if indentation matches closely
563
+ // Allow 2 space difference (for comment being indented slightly different)
564
+ if (pendingCommentIndent === -1 || Math.abs(keyIndent - pendingCommentIndent) <= 2) {
565
+ const commentText = pendingComment.join("\n");
566
+ const debugInfo = [...pendingDebugInfo, `Line ${String(lineNum)}: Associating with key "${key}"`].join("\n");
567
+
568
+ comments.set(key, {
569
+ text: commentText,
570
+ metadata: {
571
+ source: "REGEX",
572
+ rawComment: commentText,
573
+ indent: keyIndent,
574
+ debugInfo,
575
+ },
576
+ });
577
+ } else {
578
+ pendingDebugInfo.push(
579
+ `Line ${String(lineNum)}: Skipping key "${key}" due to indent mismatch (${String(keyIndent)} vs ${String(pendingCommentIndent)})`,
580
+ );
581
+ }
582
+
583
+ // Reset for next key
584
+ pendingComment = [];
585
+ pendingCommentIndent = -1;
586
+ pendingDebugInfo = [];
587
+ }
588
+ } else if (!trimmed.startsWith("#")) {
589
+ // Non-comment, non-key line - reset pending comment
590
+ if (pendingComment.length > 0) {
591
+ pendingDebugInfo.push(`Line ${String(lineNum)}: Non-comment/non-key line, resetting pending`);
592
+ }
593
+ pendingComment = [];
594
+ pendingCommentIndent = -1;
595
+ pendingDebugInfo = [];
596
+ }
597
+ });
598
+
599
+ return comments;
600
+ }
601
+
602
+ /**
603
+ * Parse YAML comments with metadata for debugging
604
+ * Returns comments with information about how they were extracted
605
+ * Exported for testing purposes
606
+ */
607
+ export function parseYAMLCommentsWithMetadata(yamlContent: string): Map<string, CommentWithMetadata> {
608
+ const comments = new Map<string, CommentWithMetadata>();
609
+
610
+ try {
611
+ // Pre-process to uncomment commented-out keys
612
+ // This allows Helm chart commented-out options to be parsed as real keys
613
+ const preprocessedYaml = preprocessYAMLComments(yamlContent);
614
+ const doc = parseDocument(preprocessedYaml);
615
+
616
+ // Build a regex-based fallback map for cases where YAML parser loses comments
617
+ // Use preprocessed YAML so commented-out keys are treated as real keys
618
+ const regexComments = parseCommentsWithRegex(preprocessedYaml);
619
+
620
+ // Recursively walk the YAML AST and extract comments
621
+ function visitNode(node: unknown, keyPath: string[] = [], inheritedComment = ""): void {
622
+ if (!node) return;
623
+
624
+ // Handle map/object nodes - check if node has items array
625
+ const mapNodeCheck = z
626
+ .object({ items: z.array(z.unknown()), commentBefore: z.unknown().optional() })
627
+ .safeParse(node);
628
+ if (mapNodeCheck.success) {
629
+ // Extract the map's own comment (to be inherited by first child if needed)
630
+ let mapComment = inheritedComment;
631
+ const mapCommentCheck = z.string().safeParse(mapNodeCheck.data.commentBefore);
632
+ if (mapCommentCheck.success) {
633
+ mapComment = mapCommentCheck.data;
634
+ }
635
+
636
+ for (let i = 0; i < mapNodeCheck.data.items.length; i++) {
637
+ const item = mapNodeCheck.data.items[i];
638
+ const itemCheck = z.object({ key: z.unknown(), value: z.unknown() }).safeParse(item);
639
+ if (!itemCheck.success) continue;
640
+
641
+ // Get the key - validate it has a value property that's a string
642
+ const keyNodeCheck = z.object({ value: z.string() }).safeParse(itemCheck.data.key);
643
+ if (!keyNodeCheck.success) continue;
644
+
645
+ const key = keyNodeCheck.data.value;
646
+ const newPath = [...keyPath, key];
647
+ const fullKey = newPath.join(".");
648
+
649
+ // Extract comment from the key's commentBefore
650
+ let comment = "";
651
+ const keyCommentCheck = z.object({ commentBefore: z.unknown() }).safeParse(itemCheck.data.key);
652
+ if (keyCommentCheck.success) {
653
+ const commentCheck = z.string().safeParse(keyCommentCheck.data.commentBefore);
654
+ comment = commentCheck.success ? commentCheck.data : "";
655
+ }
656
+
657
+ // Also check the pair itself for comments
658
+ const pairCommentCheck = z.object({ commentBefore: z.unknown() }).safeParse(item);
659
+ if (pairCommentCheck.success) {
660
+ const pairCommentValue = z.string().safeParse(pairCommentCheck.data.commentBefore);
661
+ const pairComment = pairCommentValue.success ? pairCommentValue.data : "";
662
+ if (pairComment) {
663
+ comment = comment ? `${pairComment}\n${comment}` : pairComment;
664
+ }
665
+ }
666
+
667
+ // Check for inline comments on the value
668
+ const valueCommentCheck = z.object({ comment: z.unknown() }).safeParse(itemCheck.data.value);
669
+ if (valueCommentCheck.success) {
670
+ const inlineComment = z.string().safeParse(valueCommentCheck.data.comment);
671
+ if (inlineComment.success && inlineComment.data) {
672
+ comment = comment ? `${comment}\n${inlineComment.data}` : inlineComment.data;
673
+ }
674
+ }
675
+
676
+ // If this is the first item and has no comment, inherit from map
677
+ if (i === 0 && !comment && mapComment) {
678
+ comment = mapComment;
679
+ }
680
+
681
+ // Filter out commented-out YAML blocks before cleaning
682
+ if (comment) {
683
+ comment = filterCommentedOutYAML(comment);
684
+ }
685
+
686
+ // Check if this is a Bitnami-style @param comment
687
+ // These need special handling before cleaning
688
+ const hasParamDirective = comment.includes("@param ");
689
+ if (hasParamDirective) {
690
+ const { params, remainingLines } = parseBitnamiParams(comment);
691
+
692
+ // Store each param comment with its specific key
693
+ for (const [paramKey, description] of params.entries()) {
694
+ comments.set(paramKey, {
695
+ text: description,
696
+ metadata: {
697
+ source: "AST",
698
+ rawComment: comment,
699
+ debugInfo: `Bitnami @param directive for ${paramKey}`,
700
+ },
701
+ });
702
+ }
703
+
704
+ // Store remaining non-param lines with the current key if any
705
+ if (remainingLines.length > 0) {
706
+ const cleaned = cleanYAMLComment(remainingLines.join("\n"));
707
+ if (cleaned) {
708
+ comments.set(fullKey, {
709
+ text: cleaned,
710
+ metadata: {
711
+ source: "AST",
712
+ rawComment: comment,
713
+ debugInfo: `AST comment after extracting @param directives`,
714
+ },
715
+ });
716
+ }
717
+ }
718
+ } else {
719
+ // Clean and store the comment normally
720
+ if (comment) {
721
+ const cleaned = cleanYAMLComment(comment);
722
+ if (cleaned) {
723
+ comments.set(fullKey, {
724
+ text: cleaned,
725
+ metadata: {
726
+ source: "AST",
727
+ rawComment: comment,
728
+ debugInfo: `Direct AST comment for ${fullKey}`,
729
+ },
730
+ });
731
+ }
732
+ }
733
+ }
734
+
735
+ // Check if the value has a commentBefore (for nested structures)
736
+ let valueInheritedComment = "";
737
+ const valueCommentBeforeCheck = z.object({ commentBefore: z.unknown() }).safeParse(itemCheck.data.value);
738
+ if (valueCommentBeforeCheck.success) {
739
+ const valueCommentBefore = z.string().safeParse(valueCommentBeforeCheck.data.commentBefore);
740
+ if (valueCommentBefore.success) {
741
+ valueInheritedComment = valueCommentBefore.data;
742
+ }
743
+ }
744
+
745
+ // Recurse into nested structures
746
+ if (itemCheck.data.value) {
747
+ visitNode(itemCheck.data.value, newPath, valueInheritedComment);
748
+ }
749
+ }
750
+ }
751
+ }
752
+
753
+ // Start with the document contents
754
+ if (doc.contents) {
755
+ visitNode(doc.contents, []);
756
+ }
757
+
758
+ // Merge regex comments as fallback - only use them if AST parsing didn't find a comment
759
+ // This handles cases where YAML parser loses comments due to:
760
+ // - Inconsistent indentation
761
+ // - Commented-out YAML keys mixed with documentation
762
+ // - Other edge cases
763
+ for (const [key, commentWithMeta] of regexComments.entries()) {
764
+ if (!comments.has(key)) {
765
+ const cleaned = cleanYAMLComment(commentWithMeta.text);
766
+ if (cleaned) {
767
+ comments.set(key, {
768
+ text: cleaned,
769
+ metadata: {
770
+ ...commentWithMeta.metadata,
771
+ debugInfo: `${commentWithMeta.metadata.debugInfo ?? ""}\nCleaned from: "${commentWithMeta.text}"`,
772
+ },
773
+ });
774
+ }
775
+ }
776
+ }
777
+ } catch (error) {
778
+ // If YAML parsing fails, fall back to empty map
779
+ console.warn("Failed to parse YAML comments:", error);
780
+ }
781
+
782
+ return comments;
783
+ }
784
+
785
+ /**
786
+ * Parse YAML comments and associate them with keys
787
+ * Returns a simple Map<string, string> for backward compatibility
788
+ * Exported for testing purposes
789
+ */
790
+ export function parseYAMLComments(yamlContent: string): Map<string, string> {
791
+ const commentsWithMetadata = parseYAMLCommentsWithMetadata(yamlContent);
792
+ const simpleComments = new Map<string, string>();
793
+
794
+ for (const [key, commentWithMeta] of commentsWithMetadata.entries()) {
795
+ simpleComments.set(key, commentWithMeta.text);
796
+ }
797
+
798
+ return simpleComments;
799
+ }