@redpanda-data/docs-extensions-and-macros 4.13.1 → 4.13.2

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 (33) hide show
  1. package/bin/doc-tools-mcp.js +15 -3
  2. package/bin/doc-tools.js +767 -2088
  3. package/bin/mcp-tools/property-docs.js +18 -0
  4. package/bin/mcp-tools/rpcn-docs.js +28 -3
  5. package/cli-utils/antora-utils.js +53 -2
  6. package/cli-utils/dependencies.js +313 -0
  7. package/cli-utils/diff-utils.js +273 -0
  8. package/cli-utils/doc-tools-utils.js +54 -0
  9. package/extensions/algolia-indexer/generate-index.js +134 -102
  10. package/extensions/algolia-indexer/index.js +70 -38
  11. package/extensions/collect-bloblang-samples.js +2 -1
  12. package/extensions/generate-rp-connect-categories.js +126 -67
  13. package/extensions/generate-rp-connect-info.js +291 -137
  14. package/macros/rp-connect-components.js +34 -5
  15. package/package.json +4 -3
  16. package/tools/add-commercial-names.js +207 -0
  17. package/tools/generate-cli-docs.js +6 -2
  18. package/tools/get-console-version.js +5 -0
  19. package/tools/get-redpanda-version.js +5 -0
  20. package/tools/property-extractor/compare-properties.js +3 -3
  21. package/tools/property-extractor/generate-handlebars-docs.js +14 -14
  22. package/tools/property-extractor/generate-pr-summary.js +46 -0
  23. package/tools/property-extractor/pr-summary-formatter.js +375 -0
  24. package/tools/redpanda-connect/README.adoc +403 -38
  25. package/tools/redpanda-connect/connector-binary-analyzer.js +588 -0
  26. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +97 -34
  27. package/tools/redpanda-connect/parse-csv-connectors.js +1 -1
  28. package/tools/redpanda-connect/pr-summary-formatter.js +601 -0
  29. package/tools/redpanda-connect/report-delta.js +69 -2
  30. package/tools/redpanda-connect/rpcn-connector-docs-handler.js +1180 -0
  31. package/tools/redpanda-connect/templates/connector.hbs +38 -0
  32. package/tools/redpanda-connect/templates/intro.hbs +0 -20
  33. package/tools/redpanda-connect/update-nav.js +205 -0
package/bin/doc-tools.js CHANGED
@@ -1,382 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync, spawnSync } = require('child_process');
4
- const os = require('os');
5
- const { Command, Option } = require('commander');
6
- const path = require('path');
7
- const fs = require('fs');
8
- const { determineDocsBranch } = require('../cli-utils/self-managed-docs-branch.js');
9
- const fetchFromGithub = require('../tools/fetch-from-github.js');
10
- const { urlToXref } = require('../cli-utils/convert-doc-links.js');
11
- const { generateRpcnConnectorDocs } = require('../tools/redpanda-connect/generate-rpcn-connector-docs.js');
12
- const parseCSVConnectors = require('../tools/redpanda-connect/parse-csv-connectors.js');
13
- const { getAntoraValue, setAntoraValue } = require('../cli-utils/antora-utils');
14
- const {
15
- getRpkConnectVersion,
16
- printDeltaReport
17
- } = require('../tools/redpanda-connect/report-delta');
18
-
19
- /**
20
- * Searches upward from a starting directory to locate the repository root.
21
- *
22
- * Traverses parent directories from the specified start path, returning the first directory containing either a `.git` folder or a `package.json` file. Exits the process with an error if no such directory is found.
23
- *
24
- * @param {string} [start] - The directory to begin the search from. Defaults to the current working directory.
25
- * @returns {string} The absolute path to the repository root directory.
26
- */
27
- function findRepoRoot(start = process.cwd()) {
28
- let dir = start;
29
- while (dir !== path.parse(dir).root) {
30
- if (
31
- fs.existsSync(path.join(dir, '.git')) ||
32
- fs.existsSync(path.join(dir, 'package.json'))
33
- ) {
34
- return dir;
35
- }
36
- dir = path.dirname(dir);
37
- }
38
- console.error('❌ Could not find repo root (no .git or package.json in any parent)');
39
- process.exit(1);
40
- }
41
-
42
- // --------------------------------------------------------------------
43
- // Dependency check functions
3
+ 'use strict'
44
4
 
45
- /**
46
- * Prints an error message to stderr and exits the process with a non-zero status.
47
- *
48
- * @param {string} msg - The error message to display before exiting.
49
- */
50
- function fail(msg) {
51
- console.error(`❌ ${msg}`);
52
- process.exit(1);
53
- }
5
+ const { spawnSync } = require('child_process')
6
+ const os = require('os')
7
+ const { Command, Option } = require('commander')
8
+ const path = require('path')
9
+ const fs = require('fs')
54
10
 
55
- /**
56
- * Ensures that a specified command-line tool is installed and operational.
57
- *
58
- * Attempts to execute the tool with a version flag to verify its presence. If the tool is missing or fails to run, the process exits with an error message and optional installation hint.
59
- *
60
- * @param {string} cmd - The name of the tool to check (for example, 'docker', 'helm-docs').
61
- * @param {object} [opts] - Optional settings.
62
- * @param {string} [opts.versionFlag='--version'] - The flag used to test the tool's execution.
63
- * @param {string} [opts.help] - An optional hint or installation instruction shown on failure.
64
- */
65
- function requireTool(cmd, { versionFlag = '--version', help = '' } = {}) {
66
- try {
67
- execSync(`${cmd} ${versionFlag}`, { stdio: 'ignore' });
68
- } catch {
69
- const hint = help ? `\n→ ${help}` : '';
70
- fail(`'${cmd}' is required but not found.${hint}`);
71
- }
72
- }
73
-
74
- /**
75
- * Ensures that a command-line tool is installed by checking if it responds to a specified flag.
76
- *
77
- * @param {string} cmd - The name of the command-line tool to check.
78
- * @param {string} [help] - Optional help text to display if the tool is not found.
79
- * @param {string} [versionFlag='--version'] - The flag to use when checking if the tool is installed.
80
- *
81
- * @throws {Error} If the specified command is not found or does not respond to the specified flag.
82
- */
83
- function requireCmd(cmd, help, versionFlag = '--version') {
84
- requireTool(cmd, { versionFlag, help });
85
- }
86
-
87
- // --------------------------------------------------------------------
88
- // Special validators
89
-
90
- /**
91
- * Ensures that Python with a minimum required version is installed and available in the system PATH.
92
- *
93
- * Checks for either `python3` or `python` executables and verifies that the version is at least the specified minimum (default: 3.10). Exits the process with an error message if the requirement is not met.
94
- *
95
- * @param {number} [minMajor=3] - Minimum required major version of Python.
96
- * @param {number} [minMinor=10] - Minimum required minor version of Python.
97
- */
98
- function requirePython(minMajor = 3, minMinor = 10) {
99
- const candidates = ['python3', 'python', 'python3.12', 'python3.11', 'python3.10'];
100
- for (const p of candidates) {
101
- try {
102
- const out = execSync(`${p} --version`, { encoding: 'utf8' }).trim();
103
- const [maj, min] = out.split(' ')[1].split('.').map(Number);
104
- if (maj > minMajor || (maj === minMajor && min >= minMinor)) {
105
- return; // success
106
- }
107
- } catch {
108
- /* ignore & try next */
109
- }
110
- }
111
- fail(
112
- `Python ${minMajor}.${minMinor}+ not found or too old.
113
-
114
- **Quick Install (Recommended):**
115
- Run the automated installer to set up all dependencies:
116
- npm run install-test-dependencies
117
-
118
- Or install manually from your package manager or https://python.org`
119
- );
120
- }
121
-
122
- /**
123
- * Ensures that the Docker CLI is installed and the Docker daemon is running.
124
- *
125
- * @throws {Error} If Docker is not installed or the Docker daemon is not running.
126
- */
127
- function requireDockerDaemon() {
128
- requireTool('docker', { help: 'https://docs.docker.com/get-docker/' });
129
- try {
130
- execSync('docker info', { stdio: 'ignore' });
131
- } catch {
132
- fail(`Docker daemon does not appear to be running.
133
-
134
- **Quick Install (Recommended):**
135
- Run the automated installer to set up all dependencies:
136
- npm run install-test-dependencies
137
-
138
- Or install and start Docker manually: https://docs.docker.com/get-docker/`);
139
- }
140
- }
141
-
142
- // --------------------------------------------------------------------
143
- // Grouped checks
144
-
145
- /**
146
- * Ensures that required dependencies for generating CRD documentation are installed.
147
- *
148
- * Verifies the presence of the {@link git} and {@link crd-ref-docs} command-line tools, exiting the process with an error message if either is missing.
149
- */
150
- function verifyCrdDependencies() {
151
- requireCmd('git', 'Install Git: https://git-scm.com/downloads');
152
- requireCmd(
153
- 'crd-ref-docs',
154
- `
155
- The 'crd-ref-docs' command is required but was not found.
156
-
157
- **Quick Install (Recommended):**
158
- Run the automated installer to set up all dependencies:
159
- npm run install-test-dependencies
160
-
161
- Or install manually (for macOS):
162
-
163
- 1. Determine your architecture:
164
- Run: \`uname -m\`
165
-
166
- 2. Download and install:
167
-
168
- - For Apple Silicon (M1/M2/M3):
169
- curl -fLO https://github.com/elastic/crd-ref-docs/releases/download/v0.1.0/crd-ref-docs_0.1.0_Darwin_arm64.tar.gz
170
- tar -xzf crd-ref-docs_0.1.0_Darwin_arm64.tar.gz
171
- chmod +x crd-ref-docs
172
- sudo mv crd-ref-docs /usr/local/bin/
173
-
174
- - For Intel (x86_64):
175
- curl -fLO https://github.com/elastic/crd-ref-docs/releases/download/v0.1.0/crd-ref-docs_0.1.0_Darwin_x86_64.tar.gz
176
- tar -xzf crd-ref-docs_0.1.0_Darwin_x86_64.tar.gz
177
- chmod +x crd-ref-docs
178
- sudo mv crd-ref-docs /usr/local/bin/
179
-
180
- For more details, visit: https://github.com/elastic/crd-ref-docs
181
- `.trim()
182
- );
183
- requireCmd(
184
- 'go',
185
- `
186
- The 'go' command (Golang) is required but was not found.
187
-
188
- **Quick Install (Recommended):**
189
- Run the automated installer to set up all dependencies:
190
- npm run install-test-dependencies
191
-
192
- Or install manually on macOS:
193
-
194
- Option 1: Install via Homebrew (recommended):
195
- brew install go
196
-
197
- Option 2: Download directly from the official site:
198
- 1. Visit: https://go.dev/dl/
199
- 2. Download the appropriate installer for macOS.
200
- 3. Run the installer and follow the instructions.
201
-
202
- After installation, verify it works:
203
- go version
204
-
205
- For more details, see: https://go.dev/doc/install
206
- `.trim(),
207
- 'version'
208
- );
209
- }
210
-
211
- /**
212
- * Ensures that all required tools for Helm documentation generation are installed.
213
- *
214
- * Checks for the presence of `helm-docs`, `pandoc`, and `git`, exiting the process with an error if any are missing.
215
- */
216
- function verifyHelmDependencies() {
217
- requireCmd(
218
- 'helm-docs',
219
- `
220
- The 'helm-docs' command is required but was not found.
221
-
222
- **Quick Install (Recommended):**
223
- Run the automated installer to set up all dependencies:
224
- npm run install-test-dependencies
225
-
226
- Or install manually (for macOS):
227
-
228
- 1. Determine your architecture:
229
- Run: \`uname -m\`
230
-
231
- 2. Download and install:
232
-
233
- - For Apple Silicon (M1/M2/M3):
234
- curl -fLO https://github.com/norwoodj/helm-docs/releases/download/v1.11.0/helm-docs_1.11.0_Darwin_arm64.tar.gz
235
- tar -xzf helm-docs_1.11.0_Darwin_arm64.tar.gz
236
- chmod +x helm-docs
237
- sudo mv helm-docs /usr/local/bin/
238
-
239
- - For Intel (x86_64):
240
- curl -fLO https://github.com/norwoodj/helm-docs/releases/download/v1.11.0/helm-docs_1.11.0_Darwin_x86_64.tar.gz
241
- tar -xzf helm-docs_1.11.0_Darwin_x86_64.tar.gz
242
- chmod +x helm-docs
243
- sudo mv helm-docs /usr/local/bin/
244
-
245
- Alternatively, if you use Homebrew:
246
- brew install norwoodj/tap/helm-docs
247
-
248
- For more details, visit: https://github.com/norwoodj/helm-docs
249
- `.trim()
250
- );
251
- requireCmd('pandoc', 'brew install pandoc or https://pandoc.org');
252
- requireCmd('git', 'Install Git: https://git-scm.com/downloads');
253
- }
254
-
255
- /**
256
- * Ensures all dependencies required for generating property documentation are installed.
257
- *
258
- * Checks for the presence of `make`, Python 3.10 or newer, Node.js, C++ compiler, and C++ standard library headers.
259
- * Exits the process with an error message if any dependency is missing.
260
- */
261
- function verifyPropertyDependencies() {
262
- requireCmd('make', 'Your OS package manager');
263
- requirePython();
264
-
265
- // Check for Node.js (required for Handlebars templates)
266
- requireCmd('node', 'https://nodejs.org/en/download/ or use your package manager (for example, brew install node)');
267
- requireCmd('npm', 'Usually installed with Node.js');
268
-
269
- // Check for C++ compiler
270
- let cppCompiler = null;
271
- try {
272
- execSync('gcc --version', { stdio: 'ignore' });
273
- cppCompiler = 'gcc';
274
- } catch {
275
- try {
276
- execSync('clang --version', { stdio: 'ignore' });
277
- cppCompiler = 'clang';
278
- } catch {
279
- fail(`A C++ compiler (gcc or clang) is required for tree-sitter compilation.
280
-
281
- **Quick Install (Recommended):**
282
- Run the automated installer to set up all dependencies:
283
- npm run install-test-dependencies
284
-
285
- Or install manually:
286
-
287
- On macOS, install Xcode Command Line Tools:
288
- xcode-select --install
289
-
290
- On Linux (Ubuntu/Debian):
291
- sudo apt update && sudo apt install build-essential
292
-
293
- On Linux (CentOS/RHEL/Fedora):
294
- sudo yum groupinstall "Development Tools"
295
- # or on newer versions:
296
- sudo dnf groupinstall "Development Tools"
297
-
298
- After installation, verify with:
299
- gcc --version
300
- # or
301
- clang --version`);
302
- }
303
- }
304
-
305
- // Check for C++ standard library headers (critical for tree-sitter compilation)
306
- let tempDir = null;
307
- let compileCmd = null;
308
- try {
309
- const testProgram = '#include <functional>\nint main() { return 0; }';
310
- tempDir = require('fs').mkdtempSync(require('path').join(require('os').tmpdir(), 'cpp-test-'));
311
- const tempFile = require('path').join(tempDir, 'test.cpp');
312
- require('fs').writeFileSync(tempFile, testProgram);
313
-
314
- compileCmd = cppCompiler === 'gcc' ? 'gcc' : 'clang++';
315
- execSync(`${compileCmd} -x c++ -fsyntax-only "${tempFile}"`, { stdio: 'ignore' });
316
- require('fs').rmSync(tempDir, { recursive: true, force: true });
317
- } catch {
318
- // Clean up temp directory if it was created
319
- if (tempDir) {
320
- try {
321
- require('fs').rmSync(tempDir, { recursive: true, force: true });
322
- } catch {
323
- // Ignore cleanup errors
324
- }
325
- }
326
- fail(`C++ standard library headers are missing or incomplete.
327
-
328
- **Quick Install (Recommended):**
329
- Run the automated installer to set up all dependencies:
330
- npm run install-test-dependencies
331
-
332
- Or fix manually:
333
-
334
- 1. **Test if the issue exists**:
335
- echo '#include <functional>' | ${compileCmd} -x c++ -fsyntax-only -
336
-
337
- 2. **If the test fails, try these fixes in order**:
338
- **Fix 1**: Reset developer path
339
- sudo xcode-select --reset
340
-
341
- **Fix 2**: Force reinstall Command Line Tools
342
- sudo rm -rf /Library/Developer/CommandLineTools
343
- xcode-select --install
344
-
345
- Complete the GUI installation dialog that appears.
346
-
347
- 3. **Verify the fix**:
348
- echo '#include <functional>' | ${compileCmd} -x c++ -fsyntax-only -
349
- If successful, you should see no output and the command should exit with code 0.
350
- `);
351
- }
352
- }
353
-
354
- /**
355
- * Ensures all required dependencies for generating Redpanda metrics documentation are installed.
356
- *
357
- * Verifies that Python 3.10+, `curl`, and `tar` are available, and that the Docker daemon is running.
358
- *
359
- * @throws {Error} If any required dependency is missing or the Docker daemon is not running.
360
- */
361
- function verifyMetricsDependencies() {
362
- requirePython();
363
- requireCmd('curl');
364
- requireCmd('tar');
365
- requireDockerDaemon();
366
- }
11
+ // Import extracted utility modules
12
+ const { findRepoRoot, fail, commonOptions } = require('../cli-utils/doc-tools-utils')
13
+ const {
14
+ requireTool,
15
+ requireCmd,
16
+ verifyCrdDependencies,
17
+ verifyHelmDependencies,
18
+ verifyPropertyDependencies,
19
+ verifyMetricsDependencies
20
+ } = require('../cli-utils/dependencies')
21
+ const {
22
+ runClusterDocs,
23
+ diffDirs,
24
+ generatePropertyComparisonReport,
25
+ updatePropertyOverridesWithVersion,
26
+ cleanupOldDiffs
27
+ } = require('../cli-utils/diff-utils')
28
+
29
+ // Import other utilities
30
+ const { determineDocsBranch } = require('../cli-utils/self-managed-docs-branch.js')
31
+ const fetchFromGithub = require('../tools/fetch-from-github.js')
32
+ const { urlToXref } = require('../cli-utils/convert-doc-links.js')
33
+ const { getAntoraValue, setAntoraValue } = require('../cli-utils/antora-utils')
367
34
 
368
35
  // --------------------------------------------------------------------
369
36
  // Main CLI Definition
370
37
  // --------------------------------------------------------------------
371
- const programCli = new Command();
38
+ const programCli = new Command()
372
39
 
373
- const pkg = require('../package.json');
40
+ const pkg = require('../package.json')
374
41
  programCli
375
42
  .name('doc-tools')
376
43
  .description('Redpanda Document Automation CLI')
377
- .version(pkg.version);
44
+ .version(pkg.version)
378
45
 
379
- // Top-level commands.
46
+ // ====================================================================
47
+ // TOP-LEVEL COMMANDS
48
+ // ====================================================================
380
49
 
381
50
  /**
382
51
  * install-test-dependencies
@@ -407,10 +76,10 @@ programCli
407
76
  .command('install-test-dependencies')
408
77
  .description('Install packages for doc test workflows')
409
78
  .action(() => {
410
- const scriptPath = path.join(__dirname, '../cli-utils/install-test-dependencies.sh');
411
- const result = spawnSync(scriptPath, { stdio: 'inherit', shell: true });
412
- process.exit(result.status);
413
- });
79
+ const scriptPath = path.join(__dirname, '../cli-utils/install-test-dependencies.sh')
80
+ const result = spawnSync(scriptPath, { stdio: 'inherit', shell: true })
81
+ process.exit(result.status)
82
+ })
414
83
 
415
84
  /**
416
85
  * get-redpanda-version
@@ -454,12 +123,12 @@ programCli
454
123
  .option('--from-antora', 'Read prerelease flag from local antora.yml')
455
124
  .action(async (options) => {
456
125
  try {
457
- await require('../tools/get-redpanda-version.js')(options);
126
+ await require('../tools/get-redpanda-version.js')(options)
458
127
  } catch (err) {
459
- console.error(`❌ ${err.message}`);
460
- process.exit(1);
128
+ console.error(`Error: ${err.message}`)
129
+ process.exit(1)
461
130
  }
462
- });
131
+ })
463
132
 
464
133
  /**
465
134
  * get-console-version
@@ -502,12 +171,12 @@ programCli
502
171
  .option('--from-antora', 'Read prerelease flag from local antora.yml')
503
172
  .action(async (options) => {
504
173
  try {
505
- await require('../tools/get-console-version.js')(options);
174
+ await require('../tools/get-console-version.js')(options)
506
175
  } catch (err) {
507
- console.error(`❌ ${err.message}`);
508
- process.exit(1);
176
+ console.error(`Error: ${err.message}`)
177
+ process.exit(1)
509
178
  }
510
- });
179
+ })
511
180
 
512
181
  /**
513
182
  * link-readme
@@ -527,8 +196,8 @@ programCli
527
196
  *
528
197
  * @example
529
198
  * # Link a lab project README into documentation
530
- * npx doc-tools link-readme \\
531
- * --subdir labs/docker-compose \\
199
+ * npx doc-tools link-readme \
200
+ * --subdir labs/docker-compose \
532
201
  * --target docker-compose-lab.adoc
533
202
  *
534
203
  * # Link multiple lab READMEs
@@ -549,39 +218,39 @@ programCli
549
218
  .requiredOption('-s, --subdir <subdir>', 'Relative path to the lab project subdirectory')
550
219
  .requiredOption('-t, --target <filename>', 'Name of the target AsciiDoc file in pages/')
551
220
  .action((options) => {
552
- const repoRoot = findRepoRoot();
553
- const normalized = options.subdir.replace(/\/+$/, '');
554
- const moduleName = normalized.split('/')[0];
221
+ const repoRoot = findRepoRoot()
222
+ const normalized = options.subdir.replace(/\/+$/, '')
223
+ const moduleName = normalized.split('/')[0]
555
224
 
556
- const projectDir = path.join(repoRoot, normalized);
557
- const pagesDir = path.join(repoRoot, 'docs', 'modules', moduleName, 'pages');
558
- const sourceFile = path.join(projectDir, 'README.adoc');
559
- const destLink = path.join(pagesDir, options.target);
225
+ const projectDir = path.join(repoRoot, normalized)
226
+ const pagesDir = path.join(repoRoot, 'docs', 'modules', moduleName, 'pages')
227
+ const sourceFile = path.join(projectDir, 'README.adoc')
228
+ const destLink = path.join(pagesDir, options.target)
560
229
 
561
230
  if (!fs.existsSync(projectDir)) {
562
- console.error(`❌ Project directory not found: ${projectDir}`);
563
- process.exit(1);
231
+ console.error(`Error: Project directory not found: ${projectDir}`)
232
+ process.exit(1)
564
233
  }
565
234
  if (!fs.existsSync(sourceFile)) {
566
- console.error(`❌ README.adoc not found in ${projectDir}`);
567
- process.exit(1);
235
+ console.error(`Error: README.adoc not found in ${projectDir}`)
236
+ process.exit(1)
568
237
  }
569
238
 
570
- fs.mkdirSync(pagesDir, { recursive: true });
571
- const relPath = path.relative(pagesDir, sourceFile);
239
+ fs.mkdirSync(pagesDir, { recursive: true })
240
+ const relPath = path.relative(pagesDir, sourceFile)
572
241
 
573
242
  try {
574
243
  if (fs.existsSync(destLink)) {
575
- const stat = fs.lstatSync(destLink);
576
- if (stat.isSymbolicLink()) fs.unlinkSync(destLink);
577
- else fail(`Destination already exists and is not a symlink: ${destLink}`);
244
+ const stat = fs.lstatSync(destLink)
245
+ if (stat.isSymbolicLink()) fs.unlinkSync(destLink)
246
+ else fail(`Destination already exists and is not a symlink: ${destLink}`)
578
247
  }
579
- fs.symlinkSync(relPath, destLink);
580
- console.log(`✅ Linked ${relPath} → ${destLink}`);
248
+ fs.symlinkSync(relPath, destLink)
249
+ console.log(`Done: Linked ${relPath} → ${destLink}`)
581
250
  } catch (err) {
582
- fail(`Failed to create symlink: ${err.message}`);
251
+ fail(`Failed to create symlink: ${err.message}`)
583
252
  }
584
- });
253
+ })
585
254
 
586
255
  /**
587
256
  * fetch
@@ -600,25 +269,25 @@ programCli
600
269
  *
601
270
  * @example
602
271
  * # Fetch a specific configuration file
603
- * npx doc-tools fetch \\
604
- * --owner redpanda-data \\
605
- * --repo redpanda \\
606
- * --remote-path docker/docker-compose.yml \\
272
+ * npx doc-tools fetch \
273
+ * --owner redpanda-data \
274
+ * --repo redpanda \
275
+ * --remote-path docker/docker-compose.yml \
607
276
  * --save-dir examples/
608
277
  *
609
278
  * # Fetch an entire directory of examples
610
- * npx doc-tools fetch \\
611
- * -o redpanda-data \\
612
- * -r connect-examples \\
613
- * -p pipelines/mongodb \\
279
+ * npx doc-tools fetch \
280
+ * -o redpanda-data \
281
+ * -r connect-examples \
282
+ * -p pipelines/mongodb \
614
283
  * -d docs/modules/examples/attachments/
615
284
  *
616
285
  * # Fetch with custom filename
617
- * npx doc-tools fetch \\
618
- * -o redpanda-data \\
619
- * -r helm-charts \\
620
- * -p charts/redpanda/values.yaml \\
621
- * -d examples/ \\
286
+ * npx doc-tools fetch \
287
+ * -o redpanda-data \
288
+ * -r helm-charts \
289
+ * -p charts/redpanda/values.yaml \
290
+ * -d examples/ \
622
291
  * --filename redpanda-values-example.yaml
623
292
  *
624
293
  * @requirements
@@ -642,13 +311,13 @@ programCli
642
311
  options.remotePath,
643
312
  options.saveDir,
644
313
  options.filename
645
- );
646
- console.log(`✅ Fetched to ${options.saveDir}`);
314
+ )
315
+ console.log(`Done: Fetched to ${options.saveDir}`)
647
316
  } catch (err) {
648
- console.error(`❌ ${err.message}`);
649
- process.exit(1);
317
+ console.error(`Error: ${err.message}`)
318
+ process.exit(1)
650
319
  }
651
- });
320
+ })
652
321
 
653
322
  /**
654
323
  * setup-mcp
@@ -701,250 +370,287 @@ programCli
701
370
  .option('--status', 'Show current MCP server configuration status', false)
702
371
  .action(async (options) => {
703
372
  try {
704
- const { setupMCP, showStatus, printNextSteps } = require('../cli-utils/setup-mcp.js');
373
+ const { setupMCP, showStatus, printNextSteps } = require('../cli-utils/setup-mcp.js')
705
374
 
706
375
  if (options.status) {
707
- showStatus();
708
- return;
376
+ showStatus()
377
+ return
709
378
  }
710
379
 
711
380
  const result = await setupMCP({
712
381
  force: options.force,
713
382
  target: options.target,
714
383
  local: options.local
715
- });
384
+ })
716
385
 
717
386
  if (result.success) {
718
- printNextSteps(result);
719
- process.exit(0);
387
+ printNextSteps(result)
388
+ process.exit(0)
720
389
  } else {
721
- console.error(`❌ Setup failed: ${result.error}`);
722
- process.exit(1);
390
+ console.error(`Error: Setup failed: ${result.error}`)
391
+ process.exit(1)
723
392
  }
724
393
  } catch (err) {
725
- console.error(`❌ ${err.message}`);
726
- process.exit(1);
394
+ console.error(`Error: ${err.message}`)
395
+ process.exit(1)
727
396
  }
728
- });
729
-
730
- // Create an "automation" subcommand group.
731
- const automation = new Command('generate').description('Run docs automations');
732
-
733
- // --------------------------------------------------------------------
734
- // Automation subcommands
735
- // --------------------------------------------------------------------
736
-
737
- // Common options for both automation tasks.
738
- const commonOptions = {
739
- dockerRepo: 'redpanda',
740
- consoleTag: 'latest',
741
- consoleDockerRepo: 'console',
742
- };
397
+ })
743
398
 
744
399
  /**
745
- * Run the cluster documentation generator script for a specific release/tag.
746
- *
747
- * Invokes the external `generate-cluster-docs.sh` script with the provided mode, tag,
748
- * and Docker-related options. The script's stdout/stderr are forwarded to the current
749
- * process; if the script exits with a non-zero status, this function will terminate
750
- * the Node.js process with that status code.
751
- *
752
- * @param {string} mode - Operation mode passed to the script (for example, "generate" or "clean").
753
- * @param {string} tag - Release tag or version to generate docs for.
754
- * @param {Object} options - Runtime options.
755
- * @param {string} options.dockerRepo - Docker repository used by the script.
756
- * @param {string} options.consoleTag - Console image tag passed to the script.
757
- * @param {string} options.consoleDockerRepo - Console Docker repository used by the script.
400
+ * @description Validate the MCP server configuration including prompts, resources,
401
+ * and metadata. Reports any issues with prompt definitions or missing files.
402
+ * @why Use this command to verify that all MCP prompts and resources are properly
403
+ * configured before deploying or after making changes to prompt files.
404
+ * @example
405
+ * # Validate MCP configuration
406
+ * npx doc-tools validate-mcp
407
+ * @requirements None.
758
408
  */
759
- function runClusterDocs(mode, tag, options) {
760
- const script = path.join(__dirname, '../cli-utils/generate-cluster-docs.sh');
761
- const args = [mode, tag, options.dockerRepo, options.consoleTag, options.consoleDockerRepo];
762
- console.log(`⏳ Running ${script} with arguments: ${args.join(' ')}`);
763
- const r = spawnSync('bash', [script, ...args], { stdio: 'inherit' });
764
- if (r.status !== 0) process.exit(r.status);
765
- }
409
+ programCli
410
+ .command('validate-mcp')
411
+ .description('Validate MCP server configuration (prompts, resources, metadata)')
412
+ .action(() => {
413
+ const {
414
+ PromptCache,
415
+ loadAllPrompts
416
+ } = require('./mcp-tools/prompt-discovery')
417
+ const {
418
+ validateMcpConfiguration,
419
+ formatValidationResults
420
+ } = require('./mcp-tools/mcp-validation')
766
421
 
767
- /**
768
- * Cleanup old diff files, keeping only the 2 most recent.
769
- *
770
- * @param {string} diffDir - Directory containing diff files
771
- */
772
- function cleanupOldDiffs(diffDir) {
773
- try {
774
- console.log('Cleaning up old diff JSON files (keeping only 2 most recent)…');
422
+ const baseDir = findRepoRoot()
423
+ const promptCache = new PromptCache()
775
424
 
776
- const absoluteDiffDir = path.resolve(diffDir);
777
- if (!fs.existsSync(absoluteDiffDir)) {
778
- return;
425
+ const resources = [
426
+ {
427
+ uri: 'redpanda://style-guide',
428
+ name: 'Redpanda Documentation Style Guide',
429
+ description: 'Complete style guide based on Google Developer Documentation Style Guide with Redpanda-specific guidelines',
430
+ mimeType: 'text/markdown',
431
+ version: '1.0.0',
432
+ lastUpdated: '2025-12-11'
433
+ }
434
+ ]
435
+
436
+ const resourceMap = {
437
+ 'redpanda://style-guide': { file: 'style-guide.md', mimeType: 'text/markdown' }
779
438
  }
780
439
 
781
- // Get all diff files sorted by modification time (newest first)
782
- const files = fs.readdirSync(absoluteDiffDir)
783
- .filter(file => file.startsWith('redpanda-property-changes-') && file.endsWith('.json'))
784
- .map(file => ({
785
- name: file,
786
- path: path.join(absoluteDiffDir, file),
787
- time: fs.statSync(path.join(absoluteDiffDir, file)).mtime.getTime()
788
- }))
789
- .sort((a, b) => b.time - a.time);
790
-
791
- // Delete all but the 2 most recent
792
- if (files.length > 2) {
793
- files.slice(2).forEach(file => {
794
- console.log(` Removing old file: ${file.name}`);
795
- fs.unlinkSync(file.path);
796
- });
440
+ try {
441
+ console.log('Loading prompts...')
442
+ const prompts = loadAllPrompts(baseDir, promptCache)
443
+ console.log(`Found ${prompts.length} prompts`)
444
+
445
+ console.log('\nValidating configuration...')
446
+ const validation = validateMcpConfiguration({
447
+ resources,
448
+ resourceMap,
449
+ prompts,
450
+ baseDir
451
+ })
452
+
453
+ const output = formatValidationResults(validation, { resources, prompts })
454
+ console.log('\n' + output)
455
+
456
+ if (!validation.valid) {
457
+ process.exit(1)
458
+ }
459
+ } catch (err) {
460
+ console.error(`Error: Validation failed: ${err.message}`)
461
+ process.exit(1)
797
462
  }
798
- } catch (error) {
799
- console.error(` Failed to cleanup old diff files: ${error.message}`);
800
- }
801
- }
463
+ })
802
464
 
803
465
  /**
804
- * Generate a detailed JSON report describing property changes between two releases.
805
- *
806
- * Looks for `<oldTag>-properties.json` and `<newTag>-properties.json` in
807
- * `modules/reference/examples`. If both files exist, invokes the external
808
- * property comparison tool to produce `property-changes-<oldTag>-to-<newTag>.json`
809
- * in the provided output directory.
810
- *
811
- * If either input JSON is missing the function logs a message and returns without
812
- * error. Any errors from the comparison tool are logged; the function does not
813
- * throw.
466
+ * @description Preview an MCP prompt with sample arguments to see the final rendered
467
+ * output. Useful for testing and debugging prompt templates.
468
+ * @why Use this command when developing or modifying MCP prompts to verify the
469
+ * output format and argument substitution works correctly.
470
+ * @example
471
+ * # Preview a prompt with content argument
472
+ * npx doc-tools preview-prompt review-content --content "Sample text"
814
473
  *
815
- * @param {string} oldTag - Release tag or identifier for the "old" properties set.
816
- * @param {string} newTag - Release tag or identifier for the "new" properties set.
817
- * @param {string} outputDir - Directory where the comparison report will be written.
474
+ * # Preview a prompt with topic and audience
475
+ * npx doc-tools preview-prompt write-docs --topic "Kafka" --audience "developers"
476
+ * @requirements None.
818
477
  */
819
- function generatePropertyComparisonReport(oldTag, newTag, outputDir) {
820
- try {
821
- console.log(`\n📊 Generating detailed property comparison report...`);
822
-
823
- // Look for the property JSON files in the standard location (modules/reference/attachments)
824
- // regardless of where we're saving the diff output
825
- const repoRoot = findRepoRoot();
826
- const attachmentsDir = path.join(repoRoot, 'modules/reference/attachments');
827
- const oldJsonPath = path.join(attachmentsDir, `redpanda-properties-${oldTag}.json`);
828
- const newJsonPath = path.join(attachmentsDir, `redpanda-properties-${newTag}.json`);
829
-
830
- // Check if JSON files exist
831
- if (!fs.existsSync(oldJsonPath)) {
832
- console.log(`⚠️ Old properties JSON not found at: ${oldJsonPath}`);
833
- console.log(` Skipping detailed property comparison.`);
834
- return;
835
- }
836
-
837
- if (!fs.existsSync(newJsonPath)) {
838
- console.log(`⚠️ New properties JSON not found at: ${newJsonPath}`);
839
- console.log(` Skipping detailed property comparison.`);
840
- return;
841
- }
842
-
843
- // Ensure output directory exists (use absolute path)
844
- const absoluteOutputDir = path.resolve(outputDir);
845
- fs.mkdirSync(absoluteOutputDir, { recursive: true });
846
-
847
- // Run the property comparison tool with descriptive filename
848
- const propertyExtractorDir = path.resolve(__dirname, '../tools/property-extractor');
849
- const compareScript = path.join(propertyExtractorDir, 'compare-properties.js');
850
- const reportFilename = `redpanda-property-changes-${oldTag}-to-${newTag}.json`;
851
- const reportPath = path.join(absoluteOutputDir, reportFilename);
852
- const args = [compareScript, oldJsonPath, newJsonPath, oldTag, newTag, absoluteOutputDir, reportFilename];
853
-
854
- const result = spawnSync('node', args, {
855
- stdio: 'inherit',
856
- cwd: propertyExtractorDir
857
- });
858
-
859
- if (result.error) {
860
- console.error(`❌ Property comparison failed: ${result.error.message}`);
861
- } else if (result.status !== 0) {
862
- console.error(`❌ Property comparison exited with code: ${result.status}`);
863
- } else {
864
- console.log(`✅ Property comparison report saved to: ${reportPath}`);
478
+ programCli
479
+ .command('preview-prompt')
480
+ .description('Preview a prompt with arguments to see the final output')
481
+ .argument('<prompt-name>', 'Name of the prompt to preview')
482
+ .option('--content <text>', 'Content argument (for review/check prompts)')
483
+ .option('--topic <text>', 'Topic argument (for write prompts)')
484
+ .option('--audience <text>', 'Audience argument (for write prompts)')
485
+ .action((promptName, options) => {
486
+ const {
487
+ PromptCache,
488
+ loadAllPrompts,
489
+ buildPromptWithArguments
490
+ } = require('./mcp-tools/prompt-discovery')
491
+
492
+ const baseDir = findRepoRoot()
493
+ const promptCache = new PromptCache()
494
+
495
+ try {
496
+ loadAllPrompts(baseDir, promptCache)
497
+
498
+ const prompt = promptCache.get(promptName)
499
+ if (!prompt) {
500
+ console.error(`Error: Prompt not found: ${promptName}`)
501
+ console.error(`\nAvailable prompts: ${promptCache.getNames().join(', ')}`)
502
+ process.exit(1)
503
+ }
504
+
505
+ const args = {}
506
+ if (options.content) args.content = options.content
507
+ if (options.topic) args.topic = options.topic
508
+ if (options.audience) args.audience = options.audience
509
+
510
+ const promptText = buildPromptWithArguments(prompt, args)
511
+
512
+ console.log('='.repeat(70))
513
+ console.log(`PROMPT PREVIEW: ${promptName}`)
514
+ console.log('='.repeat(70))
515
+ console.log(`Description: ${prompt.description}`)
516
+ console.log(`Version: ${prompt.version}`)
517
+ if (prompt.arguments.length > 0) {
518
+ console.log(`Arguments: ${prompt.arguments.map(a => a.name).join(', ')}`)
519
+ }
520
+ console.log('='.repeat(70))
521
+ console.log('\n' + promptText)
522
+ console.log('\n' + '='.repeat(70))
523
+ } catch (err) {
524
+ console.error(`Error: Preview failed: ${err.message}`)
525
+ process.exit(1)
865
526
  }
866
- } catch (error) {
867
- console.error(`❌ Error generating property comparison: ${error.message}`);
868
- }
869
- }
527
+ })
870
528
 
871
529
  /**
872
- * Create a unified diff patch between two temporary directories and clean them up.
873
- *
874
- * Ensures both source directories exist, writes a recursive unified diff
875
- * (changes.patch) to tmp/diffs/<kind>/<oldTag>_to_<newTag>/, and removes the
876
- * provided temporary directories. On missing inputs or if the diff subprocess
877
- * fails to spawn, the process exits with a non-zero status.
878
- *
879
- * @param {string} kind - Logical category for the diff (for example, "metrics" or "rpk"); used in the output path.
880
- * @param {string} oldTag - Identifier for the "old" version (used in the output path).
881
- * @param {string} newTag - Identifier for the "new" version (used in the output path).
882
- * @param {string} oldTempDir - Path to the existing temporary directory containing the old output; must exist.
883
- * @param {string} newTempDir - Path to the existing temporary directory containing the new output; must exist.
530
+ * @description Show MCP server version information including available prompts,
531
+ * resources, and optionally usage statistics from previous sessions.
532
+ * @why Use this command to see what MCP capabilities are available and to review
533
+ * usage patterns for optimization.
534
+ * @example
535
+ * # Show version and capabilities
536
+ * npx doc-tools mcp-version
537
+ *
538
+ * # Show with usage statistics
539
+ * npx doc-tools mcp-version --stats
540
+ * @requirements None.
884
541
  */
885
- function diffDirs(kind, oldTag, newTag, oldTempDir, newTempDir) {
886
- // Backwards compatibility: if temp directories not provided, use autogenerated paths
887
- if (!oldTempDir) {
888
- oldTempDir = path.join('autogenerated', oldTag, kind);
889
- }
890
- if (!newTempDir) {
891
- newTempDir = path.join('autogenerated', newTag, kind);
892
- }
893
-
894
- const diffDir = path.join('tmp', 'diffs', kind, `${oldTag}_to_${newTag}`);
895
-
896
- if (!fs.existsSync(oldTempDir)) {
897
- console.error(`❌ Cannot diff: missing ${oldTempDir}`);
898
- process.exit(1);
899
- }
900
- if (!fs.existsSync(newTempDir)) {
901
- console.error(`❌ Cannot diff: missing ${newTempDir}`);
902
- process.exit(1);
903
- }
904
-
905
- fs.mkdirSync(diffDir, { recursive: true });
906
-
907
- // Generate traditional patch for metrics and rpk
908
- const patch = path.join(diffDir, 'changes.patch');
909
- const cmd = `diff -ru "${oldTempDir}" "${newTempDir}" > "${patch}" || true`;
910
- const res = spawnSync(cmd, { stdio: 'inherit', shell: true });
911
-
912
- if (res.error) {
913
- console.error(`❌ diff failed: ${res.error.message}`);
914
- process.exit(1);
915
- }
916
- console.log(`✅ Wrote patch: ${patch}`);
917
-
918
- // Safety guard: only clean up directories that are explicitly passed as temp directories
919
- // For backwards compatibility with autogenerated paths, don't clean up automatically
920
- const tmpRoot = path.resolve('tmp') + path.sep;
921
- const workspaceRoot = path.resolve('.') + path.sep;
922
-
923
- // Only clean up if directories were explicitly provided as temp directories
924
- // (indicated by having all 5 parameters) and they're in the tmp/ directory
925
- const explicitTempDirs = arguments.length >= 5;
926
-
927
- if (explicitTempDirs) {
928
- [oldTempDir, newTempDir].forEach(dirPath => {
929
- const resolvedPath = path.resolve(dirPath) + path.sep;
930
- const isInTmp = resolvedPath.startsWith(tmpRoot);
931
- const isInWorkspace = resolvedPath.startsWith(workspaceRoot);
932
-
933
- if (isInWorkspace && isInTmp) {
934
- try {
935
- fs.rmSync(dirPath, { recursive: true, force: true });
936
- console.log(`🧹 Cleaned up temporary directory: ${dirPath}`);
937
- } catch (err) {
938
- console.warn(`⚠️ Warning: Could not clean up directory ${dirPath}: ${err.message}`);
542
+ programCli
543
+ .command('mcp-version')
544
+ .description('Show MCP server version and configuration information')
545
+ .option('--stats', 'Show usage statistics if available', false)
546
+ .action((options) => {
547
+ const packageJson = require('../package.json')
548
+ const {
549
+ PromptCache,
550
+ loadAllPrompts
551
+ } = require('./mcp-tools/prompt-discovery')
552
+
553
+ const baseDir = findRepoRoot()
554
+ const promptCache = new PromptCache()
555
+
556
+ try {
557
+ const prompts = loadAllPrompts(baseDir, promptCache)
558
+
559
+ const resources = [
560
+ {
561
+ uri: 'redpanda://style-guide',
562
+ name: 'Redpanda Documentation Style Guide',
563
+ version: '1.0.0',
564
+ lastUpdated: '2025-12-11'
565
+ }
566
+ ]
567
+
568
+ console.log('Redpanda Doc Tools MCP Server')
569
+ console.log('='.repeat(60))
570
+ console.log(`Server version: ${packageJson.version}`)
571
+ console.log(`Base directory: ${baseDir}`)
572
+ console.log('')
573
+
574
+ console.log(`Prompts (${prompts.length} available):`)
575
+ prompts.forEach(prompt => {
576
+ const args = prompt.arguments.length > 0
577
+ ? ` [${prompt.arguments.map(a => a.name).join(', ')}]`
578
+ : ''
579
+ console.log(` - ${prompt.name} (v${prompt.version})${args}`)
580
+ console.log(` ${prompt.description}`)
581
+ })
582
+ console.log('')
583
+
584
+ console.log(`Resources (${resources.length} available):`)
585
+ resources.forEach(resource => {
586
+ console.log(` - ${resource.name} (v${resource.version})`)
587
+ console.log(` URI: ${resource.uri}`)
588
+ console.log(` Last updated: ${resource.lastUpdated}`)
589
+ })
590
+ console.log('')
591
+
592
+ if (options.stats) {
593
+ const statsPath = path.join(os.tmpdir(), 'mcp-usage-stats.json')
594
+ if (fs.existsSync(statsPath)) {
595
+ try {
596
+ const stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'))
597
+ console.log('Usage Statistics:')
598
+ console.log('='.repeat(60))
599
+
600
+ if (stats.tools && Object.keys(stats.tools).length > 0) {
601
+ console.log('\nTool Usage:')
602
+ Object.entries(stats.tools)
603
+ .sort(([, a], [, b]) => b.count - a.count)
604
+ .forEach(([name, data]) => {
605
+ console.log(` ${name}:`)
606
+ console.log(` Invocations: ${data.count}`)
607
+ if (data.errors > 0) {
608
+ console.log(` Errors: ${data.errors}`)
609
+ }
610
+ })
611
+ }
612
+
613
+ if (stats.prompts && Object.keys(stats.prompts).length > 0) {
614
+ console.log('\nPrompt Usage:')
615
+ Object.entries(stats.prompts)
616
+ .sort(([, a], [, b]) => b - a)
617
+ .forEach(([name, count]) => {
618
+ console.log(` ${name}: ${count} invocations`)
619
+ })
620
+ }
621
+
622
+ if (stats.resources && Object.keys(stats.resources).length > 0) {
623
+ console.log('\nResource Access:')
624
+ Object.entries(stats.resources)
625
+ .sort(([, a], [, b]) => b - a)
626
+ .forEach(([uri, count]) => {
627
+ console.log(` ${uri}: ${count} reads`)
628
+ })
629
+ }
630
+ } catch (err) {
631
+ console.error('Failed to parse usage statistics:', err.message)
632
+ }
633
+ } else {
634
+ console.log('No usage statistics available yet.')
635
+ console.log('Statistics are exported when the MCP server shuts down.')
939
636
  }
940
- } else {
941
- console.log(`ℹ️ Skipping cleanup of directory outside tmp/: ${dirPath}`);
942
637
  }
943
- });
944
- } else {
945
- console.log(`ℹ️ Using autogenerated directories - skipping cleanup for safety`);
946
- }
947
- }
638
+
639
+ console.log('')
640
+ console.log('For more information, see:')
641
+ console.log(' mcp/WRITER_EXTENSION_GUIDE.adoc')
642
+ console.log(' mcp/AI_CONSISTENCY_ARCHITECTURE.adoc')
643
+ } catch (err) {
644
+ console.error(`Error: Failed to retrieve version information: ${err.message}`)
645
+ process.exit(1)
646
+ }
647
+ })
648
+
649
+ // ====================================================================
650
+ // GENERATE SUBCOMMAND GROUP
651
+ // ====================================================================
652
+
653
+ const automation = new Command('generate').description('Run docs automations')
948
654
 
949
655
  /**
950
656
  * generate metrics-docs
@@ -968,13 +674,13 @@ function diffDirs(kind, oldTag, newTag, oldTempDir, newTempDir) {
968
674
  * npx doc-tools generate metrics-docs --tag v25.3.1
969
675
  *
970
676
  * # Compare metrics between versions to see what changed
971
- * npx doc-tools generate metrics-docs \\
972
- * --tag v25.3.1 \\
677
+ * npx doc-tools generate metrics-docs \
678
+ * --tag v25.3.1 \
973
679
  * --diff v25.2.1
974
680
  *
975
681
  * # Use custom Docker repository
976
- * npx doc-tools generate metrics-docs \\
977
- * --tag v25.3.1 \\
682
+ * npx doc-tools generate metrics-docs \
683
+ * --tag v25.3.1 \
978
684
  * --docker-repo docker.redpanda.com/redpandadata/redpanda
979
685
  *
980
686
  * # Full workflow: document new release
@@ -992,66 +698,52 @@ automation
992
698
  .description('Generate JSON and AsciiDoc documentation for Redpanda metrics. Defaults to branch "dev" if neither --tag nor --branch is specified.')
993
699
  .option('-t, --tag <tag>', 'Git tag for released content (GA/beta)')
994
700
  .option('-b, --branch <branch>', 'Branch name for in-progress content')
995
- .option(
996
- '--docker-repo <repo>',
997
- 'Docker repository to use when starting Redpanda in Docker',
998
- commonOptions.dockerRepo
999
- )
1000
- .option(
1001
- '--console-tag <tag>',
1002
- 'Redpanda Console version to use when starting Redpanda Console in Docker',
1003
- commonOptions.consoleTag
1004
- )
1005
- .option(
1006
- '--console-docker-repo <repo>',
1007
- 'Docker repository to use when starting Redpanda Console in Docker',
1008
- commonOptions.consoleDockerRepo
1009
- )
701
+ .option('--docker-repo <repo>', 'Docker repository to use', commonOptions.dockerRepo)
702
+ .option('--console-tag <tag>', 'Redpanda Console version to use', commonOptions.consoleTag)
703
+ .option('--console-docker-repo <repo>', 'Docker repository for Console', commonOptions.consoleDockerRepo)
1010
704
  .option('--diff <oldTag>', 'Also diff autogenerated metrics from <oldTag> → <tag>')
1011
705
  .action((options) => {
1012
- verifyMetricsDependencies();
706
+ verifyMetricsDependencies()
1013
707
 
1014
- // Validate that tag and branch are mutually exclusive
1015
708
  if (options.tag && options.branch) {
1016
- console.error('Error: Cannot specify both --tag and --branch');
1017
- process.exit(1);
709
+ console.error('Error: Cannot specify both --tag and --branch')
710
+ process.exit(1)
1018
711
  }
1019
712
 
1020
- // Default to 'dev' branch if neither specified
1021
- const newTag = options.tag || options.branch || 'dev';
1022
- const oldTag = options.diff;
713
+ const newTag = options.tag || options.branch || 'dev'
714
+ const oldTag = options.diff
1023
715
 
1024
716
  if (oldTag) {
1025
- const oldDir = path.join('autogenerated', oldTag, 'metrics');
717
+ const oldDir = path.join('autogenerated', oldTag, 'metrics')
1026
718
  if (!fs.existsSync(oldDir)) {
1027
- console.log(`⏳ Generating metrics docs for old tag ${oldTag}…`);
1028
- runClusterDocs('metrics', oldTag, options);
719
+ console.log(`Generating metrics docs for old tag ${oldTag}…`)
720
+ runClusterDocs('metrics', oldTag, options)
1029
721
  }
1030
722
  }
1031
723
 
1032
- console.log(`⏳ Generating metrics docs for new tag ${newTag}…`);
1033
- runClusterDocs('metrics', newTag, options);
724
+ console.log(`Generating metrics docs for new tag ${newTag}…`)
725
+ runClusterDocs('metrics', newTag, options)
1034
726
 
1035
727
  if (oldTag) {
1036
- diffDirs('metrics', oldTag, newTag);
728
+ diffDirs('metrics', oldTag, newTag)
1037
729
  }
1038
730
 
1039
- process.exit(0);
1040
- });
731
+ process.exit(0)
732
+ })
1041
733
 
1042
734
  /**
1043
735
  * generate rpcn-connector-docs
1044
736
  *
1045
737
  * @description
1046
738
  * Generates complete reference documentation for Redpanda Connect (formerly Benthos) connectors,
1047
- * processors, and components. Clones the Redpanda Connect repository, parses component templates
1048
- * and configuration schemas embedded in Go code, reads connector metadata from CSV, and generates
1049
- * AsciiDoc documentation for each component. Supports diffing changes between versions and
1050
- * automatically updating what's new documentation. Can also generate Bloblang function documentation.
739
+ * processors, and components. Parses component templates and configuration schemas, reads
740
+ * connector metadata from CSV, and generates AsciiDoc documentation for each component. Supports
741
+ * diffing changes between versions and automatically updating what's new documentation. Can also
742
+ * generate Bloblang function documentation.
1051
743
  *
1052
744
  * @why
1053
745
  * Redpanda Connect has hundreds of connectors (inputs, outputs, processors) with complex
1054
- * configuration schemas. Each component's documentation lives in its Go source code as struct
746
+ * configuration schemas. Each component's documentation lives in its source code as struct
1055
747
  * tags and comments. Manual documentation is impossible to maintain. This automation extracts
1056
748
  * documentation directly from code, ensuring accuracy and completeness. The diff capability
1057
749
  * automatically identifies new connectors and changed configurations for release notes.
@@ -1066,30 +758,28 @@ automation
1066
758
  * # Include Bloblang function documentation
1067
759
  * npx doc-tools generate rpcn-connector-docs --include-bloblang
1068
760
  *
1069
- * # Generate with custom metadata CSV
1070
- * npx doc-tools generate rpcn-connector-docs \\
1071
- * --csv custom/connector-metadata.csv
761
+ * # Fetch latest connector data using rpk
762
+ * npx doc-tools generate rpcn-connector-docs --fetch-connectors
1072
763
  *
1073
764
  * # Full workflow with diff and what's new update
1074
- * npx doc-tools generate rpcn-connector-docs \\
1075
- * --update-whats-new \\
765
+ * npx doc-tools generate rpcn-connector-docs \
766
+ * --update-whats-new \
1076
767
  * --include-bloblang
1077
768
  *
1078
769
  * @requirements
1079
- * - Git to clone Redpanda Connect repository
1080
- * - Internet connection to clone repository
770
+ * - rpk and rpk connect must be installed
771
+ * - Internet connection for fetching connector data
1081
772
  * - Node.js for parsing and generation
1082
- * - Sufficient disk space for repository clone (~500MB)
1083
773
  */
1084
- automation
774
+ automation
1085
775
  .command('rpcn-connector-docs')
1086
776
  .description('Generate RPCN connector docs and diff changes since the last version')
1087
777
  .option('-d, --data-dir <path>', 'Directory where versioned connect JSON files live', path.resolve(process.cwd(), 'docs-data'))
1088
778
  .option('--old-data <path>', 'Optional override for old data file (for diff)')
1089
779
  .option('--update-whats-new', 'Update whats-new.adoc with new section from diff JSON')
1090
780
  .option('-f, --fetch-connectors', 'Fetch latest connector data using rpk')
781
+ .option('--connect-version <version>', 'Connect version to fetch (requires --fetch-connectors)')
1091
782
  .option('-m, --draft-missing', 'Generate full-doc drafts for connectors missing in output')
1092
- .option('--csv <path>', 'Path to connector metadata CSV file', 'internal/plugins/info.csv')
1093
783
  .option('--template-main <path>', 'Main Handlebars template', path.resolve(__dirname, '../tools/redpanda-connect/templates/connector.hbs'))
1094
784
  .option('--template-intro <path>', 'Intro section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/intro.hbs'))
1095
785
  .option('--template-fields <path>', 'Fields section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/fields-partials.hbs'))
@@ -1097,760 +787,22 @@ automation
1097
787
  .option('--template-bloblang <path>', 'Custom Handlebars template for bloblang function/method partials')
1098
788
  .option('--overrides <path>', 'Optional JSON file with overrides', 'docs-data/overrides.json')
1099
789
  .option('--include-bloblang', 'Include Bloblang functions and methods in generation')
790
+ .option('--cloud-version <version>', 'Cloud binary version (default: auto-detect latest)')
791
+ .option('--cgo-version <version>', 'cgo binary version (default: same as cloud-version)')
1100
792
  .action(async (options) => {
1101
793
  requireTool('rpk', {
1102
794
  versionFlag: '--version',
1103
795
  help: 'rpk is not installed. Install rpk: https://docs.redpanda.com/current/get-started/rpk-install/'
1104
- });
796
+ })
1105
797
 
1106
798
  requireTool('rpk connect', {
1107
799
  versionFlag: '--version',
1108
800
  help: 'rpk connect is not installed. Run rpk connect install before continuing.'
1109
- });
1110
-
1111
- const dataDir = path.resolve(process.cwd(), options.dataDir);
1112
- fs.mkdirSync(dataDir, { recursive: true });
1113
-
1114
- const timestamp = new Date().toISOString();
1115
-
1116
- let newVersion;
1117
- let dataFile;
1118
- if (options.fetchConnectors) {
1119
- try {
1120
- newVersion = getRpkConnectVersion();
1121
- const tmpFile = path.join(dataDir, `connect-${newVersion}.tmp.json`);
1122
- const finalFile = path.join(dataDir, `connect-${newVersion}.json`);
1123
-
1124
- const fd = fs.openSync(tmpFile, 'w');
1125
- const r = spawnSync('rpk', ['connect', 'list', '--format', 'json-full'], { stdio: ['ignore', fd, 'inherit'] });
1126
- fs.closeSync(fd);
1127
-
1128
- const rawJson = fs.readFileSync(tmpFile, 'utf8');
1129
- const parsed = JSON.parse(rawJson);
1130
- fs.writeFileSync(finalFile, JSON.stringify(parsed, null, 2));
1131
- fs.unlinkSync(tmpFile);
1132
- dataFile = finalFile;
1133
- console.log(`✅ Fetched and saved: ${finalFile}`);
1134
-
1135
- // Keep only 2 most recent versions in docs-data
1136
- const dataFiles = fs.readdirSync(dataDir)
1137
- .filter(f => /^connect-\d+\.\d+\.\d+\.json$/.test(f))
1138
- .sort();
1139
-
1140
- while (dataFiles.length > 2) {
1141
- const oldestFile = dataFiles.shift();
1142
- const oldestPath = path.join(dataDir, oldestFile);
1143
- fs.unlinkSync(oldestPath);
1144
- console.log(`🧹 Deleted old version from docs-data: ${oldestFile}`);
1145
- }
1146
- } catch (err) {
1147
- console.error(`❌ Failed to fetch connectors: ${err.message}`);
1148
- process.exit(1);
1149
- }
1150
- } else {
1151
- const candidates = fs.readdirSync(dataDir).filter(f => /^connect-\d+\.\d+\.\d+\.json$/.test(f));
1152
- if (candidates.length === 0) {
1153
- console.error('❌ No connect-<version>.json found. Use --fetch-connectors.');
1154
- process.exit(1);
1155
- }
1156
- candidates.sort();
1157
- dataFile = path.join(dataDir, candidates[candidates.length - 1]);
1158
- newVersion = candidates[candidates.length - 1].match(/connect-(\d+\.\d+\.\d+)\.json/)[1];
1159
- }
1160
-
1161
- console.log('⏳ Generating connector partials...');
1162
- let partialsWritten, partialFiles, draftsWritten, draftFiles;
1163
-
1164
- try {
1165
- const result = await generateRpcnConnectorDocs({
1166
- data: dataFile,
1167
- overrides: options.overrides,
1168
- template: options.templateMain,
1169
- templateIntro: options.templateIntro,
1170
- templateFields: options.templateFields,
1171
- templateExamples: options.templateExamples,
1172
- templateBloblang: options.templateBloblang,
1173
- writeFullDrafts: false,
1174
- includeBloblang: !!options.includeBloblang
1175
- });
1176
- partialsWritten = result.partialsWritten;
1177
- partialFiles = result.partialFiles;
1178
- } catch (err) {
1179
- console.error(`❌ Failed to generate partials: ${err.message}`);
1180
- process.exit(1);
1181
- }
1182
-
1183
- if (options.draftMissing) {
1184
- console.log('⏳ Drafting missing connectors…');
1185
- try {
1186
- const connectorList = await parseCSVConnectors(options.csv, console);
1187
- const validConnectors = connectorList.filter(r => r.name && r.type);
1188
-
1189
- const roots = {
1190
- pages: path.resolve(process.cwd(), 'modules/components/pages'),
1191
- partials:path.resolve(process.cwd(), 'modules/components/partials/components'),
1192
- };
1193
-
1194
- // find any connector that has NO .adoc under pages/TYPEs or partials/TYPEs
1195
- const allMissing = validConnectors.filter(({ name, type }) => {
1196
- const relPath = path.join(`${type}s`, `${name}.adoc`);
1197
- const existsInAny = Object.values(roots).some(root =>
1198
- fs.existsSync(path.join(root, relPath))
1199
- );
1200
- return !existsInAny;
1201
- });
1202
-
1203
- // still skip sql_driver
1204
- const missingConnectors = allMissing.filter(c => !c.name.includes('sql_driver'));
1205
-
1206
- if (missingConnectors.length === 0) {
1207
- console.log('✅ All connectors (excluding sql_drivers) already have docs—nothing to draft.');
1208
- } else {
1209
- console.log(`⏳ Docs missing for ${missingConnectors.length} connectors:`);
1210
- missingConnectors.forEach(({ name, type }) => {
1211
- console.log(` • ${type}/${name}`);
1212
- });
1213
- console.log('');
1214
-
1215
- // build your filtered JSON as before…
1216
- const rawData = fs.readFileSync(dataFile, 'utf8');
1217
- const dataObj = JSON.parse(rawData);
1218
- const filteredDataObj = {};
1219
-
1220
- for (const [key, arr] of Object.entries(dataObj)) {
1221
- if (!Array.isArray(arr)) {
1222
- filteredDataObj[key] = arr;
1223
- continue;
1224
- }
1225
- filteredDataObj[key] = arr.filter(component =>
1226
- missingConnectors.some(
1227
- m => m.name === component.name && `${m.type}s` === key
1228
- )
1229
- );
1230
- }
1231
-
1232
- const tempDataPath = path.join(dataDir, '._filtered_connect_data.json');
1233
- fs.writeFileSync(tempDataPath, JSON.stringify(filteredDataObj, null, 2), 'utf8');
1234
-
1235
- const draftResult = await generateRpcnConnectorDocs({
1236
- data: tempDataPath,
1237
- overrides: options.overrides,
1238
- template: options.templateMain,
1239
- templateFields: options.templateFields,
1240
- templateExamples:options.templateExamples,
1241
- templateIntro: options.templateIntro,
1242
- writeFullDrafts: true
1243
- });
1244
-
1245
- fs.unlinkSync(tempDataPath);
1246
- draftsWritten = draftResult.draftsWritten;
1247
- draftFiles = draftResult.draftFiles;
1248
- }
1249
- } catch (err) {
1250
- console.error(`❌ Could not draft missing: ${err.message}`);
1251
- process.exit(1);
1252
- }
1253
- }
1254
-
1255
- let oldIndex = {};
1256
- let oldVersion = null;
1257
- if (options.oldData && fs.existsSync(options.oldData)) {
1258
- oldIndex = JSON.parse(fs.readFileSync(options.oldData, 'utf8'));
1259
- const m = options.oldData.match(/connect-([\d.]+)\.json$/);
1260
- if (m) oldVersion = m[1];
1261
- } else {
1262
- oldVersion = getAntoraValue('asciidoc.attributes.latest-connect-version');
1263
- if (oldVersion) {
1264
- const oldPath = path.join(dataDir, `connect-${oldVersion}.json`);
1265
- if (fs.existsSync(oldPath)) {
1266
- oldIndex = JSON.parse(fs.readFileSync(oldPath, 'utf8'));
1267
- }
1268
- }
1269
- }
801
+ })
1270
802
 
1271
- const newIndex = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
1272
-
1273
- // Check if versions match - skip diff and updates if so
1274
- if (oldVersion && newVersion && oldVersion === newVersion) {
1275
- console.log(`\n✓ Already at version ${newVersion}`);
1276
- console.log(' No diff or version updates needed.\n');
1277
-
1278
- console.log('📊 Generation Report:');
1279
- console.log(` • Partial files: ${partialsWritten}`);
1280
- const fieldsPartials = partialFiles.filter(fp => fp.includes('/fields/'));
1281
- const examplesPartials = partialFiles.filter(fp => fp.includes('/examples/'));
1282
-
1283
- console.log(` • Fields partials: ${fieldsPartials.length}`);
1284
- console.log(` • Examples partials: ${examplesPartials.length}`);
1285
-
1286
- if (options.draftMissing && draftsWritten) {
1287
- console.log(` • Draft files: ${draftsWritten}`);
1288
- }
1289
-
1290
- process.exit(0);
1291
- }
1292
-
1293
- // Publish merged version with overrides to modules/components/attachments
1294
- if (options.overrides && fs.existsSync(options.overrides)) {
1295
- try {
1296
- const { mergeOverrides, resolveReferences } = require('../tools/redpanda-connect/generate-rpcn-connector-docs.js');
1297
-
1298
- // Create a copy of newIndex to merge overrides into
1299
- const mergedData = JSON.parse(JSON.stringify(newIndex));
1300
-
1301
- // Read and apply overrides
1302
- const ovRaw = fs.readFileSync(options.overrides, 'utf8');
1303
- const ovObj = JSON.parse(ovRaw);
1304
- const resolvedOverrides = resolveReferences(ovObj, ovObj);
1305
- mergeOverrides(mergedData, resolvedOverrides);
1306
-
1307
- // Publish to modules/components/attachments
1308
- const attachmentsRoot = path.resolve(process.cwd(), 'modules/components/attachments');
1309
- fs.mkdirSync(attachmentsRoot, { recursive: true });
1310
-
1311
- // Delete older versions from modules/components/attachments
1312
- const existingFiles = fs.readdirSync(attachmentsRoot)
1313
- .filter(f => /^connect-\d+\.\d+\.\d+\.json$/.test(f))
1314
- .sort();
1315
-
1316
- for (const oldFile of existingFiles) {
1317
- const oldFilePath = path.join(attachmentsRoot, oldFile);
1318
- fs.unlinkSync(oldFilePath);
1319
- console.log(`🧹 Deleted old version: ${oldFile}`);
1320
- }
1321
-
1322
- // Save merged version to modules/components/attachments
1323
- const destFile = path.join(attachmentsRoot, `connect-${newVersion}.json`);
1324
- fs.writeFileSync(destFile, JSON.stringify(mergedData, null, 2), 'utf8');
1325
- console.log(`✅ Published merged version to: ${path.relative(process.cwd(), destFile)}`);
1326
- } catch (err) {
1327
- console.error(`❌ Failed to publish merged version: ${err.message}`);
1328
- }
1329
- }
1330
-
1331
- printDeltaReport(oldIndex, newIndex);
1332
-
1333
- // Generate JSON diff file for whats-new.adoc
1334
- const { generateConnectorDiffJson } = require('../tools/redpanda-connect/report-delta.js');
1335
- const diffJson = generateConnectorDiffJson(
1336
- oldIndex,
1337
- newIndex,
1338
- {
1339
- oldVersion: oldVersion || '',
1340
- newVersion,
1341
- timestamp
1342
- }
1343
- );
1344
- const diffPath = path.join(dataDir, `connect-diff-${(oldVersion || 'unknown')}_to_${newVersion}.json`);
1345
- fs.writeFileSync(diffPath, JSON.stringify(diffJson, null, 2), 'utf8');
1346
- console.log(`✅ Connector diff JSON written to: ${diffPath}`);
1347
-
1348
- function logCollapsed(label, filesArray, maxToShow = 10) {
1349
- console.log(` • ${label}: ${filesArray.length} total`);
1350
- const sample = filesArray.slice(0, maxToShow);
1351
- sample.forEach(fp => console.log(` – ${fp}`));
1352
- const remaining = filesArray.length - sample.length;
1353
- if (remaining > 0) {
1354
- console.log(` … plus ${remaining} more`);
1355
- }
1356
- console.log('');
1357
- }
1358
-
1359
- const wrote = setAntoraValue('asciidoc.attributes.latest-connect-version', newVersion);
1360
- if (wrote) {
1361
- console.log(`✅ Updated Antora version: ${newVersion}`);
1362
- }
1363
-
1364
- console.log('📊 Generation Report:');
1365
- console.log(` • Partial files: ${partialsWritten}`);
1366
- // Split “partials” into fields vs examples by checking the path substring.
1367
- const fieldsPartials = partialFiles.filter(fp => fp.includes('/fields/'));
1368
- const examplesPartials = partialFiles.filter(fp => fp.includes('/examples/'));
1369
-
1370
- // Show only up to 10 of each
1371
- logCollapsed('Fields partials', fieldsPartials, 10);
1372
- logCollapsed('Examples partials', examplesPartials, 10);
1373
-
1374
- if (options.draftMissing) {
1375
- console.log(` • Full drafts: ${draftsWritten}`);
1376
- logCollapsed('Draft files', draftFiles, 5);
1377
- }
1378
-
1379
- // Optionally update whats-new.adoc
1380
- if (options.updateWhatsNew) {
1381
- // Helper function to cap description to two sentences
1382
- const capToTwoSentences = (description) => {
1383
- if (!description) return '';
1384
-
1385
- // Helper to check if text contains problematic content
1386
- const hasProblematicContent = (text) => {
1387
- return /```[\s\S]*?```/.test(text) || // code blocks
1388
- /`[^`]+`/.test(text) || // inline code
1389
- /^[=#]+\s+.+$/m.test(text) || // headings
1390
- /\n/.test(text); // newlines
1391
- };
1392
-
1393
- // Step 1: Replace common abbreviations and ellipses with placeholders
1394
- const abbreviations = [
1395
- /\bv\d+\.\d+(?:\.\d+)?/gi, // version numbers like v4.12 or v4.12.0 (must come before decimal)
1396
- /\d+\.\d+/g, // decimal numbers
1397
- /\be\.g\./gi, // e.g.
1398
- /\bi\.e\./gi, // i.e.
1399
- /\betc\./gi, // etc.
1400
- /\bvs\./gi, // vs.
1401
- /\bDr\./gi, // Dr.
1402
- /\bMr\./gi, // Mr.
1403
- /\bMs\./gi, // Ms.
1404
- /\bMrs\./gi, // Mrs.
1405
- /\bSt\./gi, // St.
1406
- /\bNo\./gi // No.
1407
- ];
1408
-
1409
- let normalized = description;
1410
- const placeholders = [];
1411
-
1412
- // Replace abbreviations with placeholders
1413
- abbreviations.forEach((abbrevRegex, idx) => {
1414
- normalized = normalized.replace(abbrevRegex, (match) => {
1415
- const placeholder = `__ABBREV${idx}_${placeholders.length}__`;
1416
- placeholders.push({ placeholder, original: match });
1417
- return placeholder;
1418
- });
1419
- });
1420
-
1421
- // Replace ellipses (three or more dots) with placeholder
1422
- normalized = normalized.replace(/\.{3,}/g, (match) => {
1423
- const placeholder = `__ELLIPSIS_${placeholders.length}__`;
1424
- placeholders.push({ placeholder, original: match });
1425
- return placeholder;
1426
- });
1427
-
1428
- // Step 2: Split sentences using the regex
1429
- const sentenceRegex = /[^.!?]+[.!?]+(?:\s|$)/g;
1430
- const sentences = normalized.match(sentenceRegex);
1431
-
1432
- if (!sentences || sentences.length === 0) {
1433
- // Restore placeholders and return original
1434
- let result = normalized;
1435
- placeholders.forEach(({ placeholder, original }) => {
1436
- result = result.replace(placeholder, original);
1437
- });
1438
- return result;
1439
- }
1440
-
1441
- // Step 3: Determine how many sentences to include
1442
- let maxSentences = 2;
1443
-
1444
- // If we have at least 2 sentences, check if the second one has problematic content
1445
- if (sentences.length >= 2) {
1446
- // Restore placeholders in second sentence to check original content
1447
- let secondSentence = sentences[1];
1448
- placeholders.forEach(({ placeholder, original }) => {
1449
- secondSentence = secondSentence.replace(new RegExp(placeholder, 'g'), original);
1450
- });
1451
-
1452
- // If second sentence has problematic content, only take first sentence
1453
- if (hasProblematicContent(secondSentence)) {
1454
- maxSentences = 1;
1455
- }
1456
- }
1457
-
1458
- let result = sentences.slice(0, maxSentences).join('');
1459
-
1460
- // Step 4: Restore placeholders back to original text
1461
- placeholders.forEach(({ placeholder, original }) => {
1462
- result = result.replace(new RegExp(placeholder, 'g'), original);
1463
- });
1464
-
1465
- return result.trim();
1466
- };
1467
-
1468
- try {
1469
- const whatsNewPath = path.join(findRepoRoot(), 'modules/get-started/pages/whats-new.adoc');
1470
- if (!fs.existsSync(whatsNewPath)) {
1471
- console.error(`❌ Unable to update release notes: 'whats-new.adoc' was not found at: ${whatsNewPath}\nPlease ensure this file exists and is tracked in your repository.`);
1472
- return;
1473
- }
1474
- // Find the diff JSON file we just wrote
1475
- const diffPath = path.join(dataDir, `connect-diff-${(oldVersion || 'unknown')}_to_${newVersion}.json`);
1476
- if (!fs.existsSync(diffPath)) {
1477
- console.error(`❌ Unable to update release notes: The connector diff JSON was not found at: ${diffPath}\nPlease ensure the diff was generated successfully before updating release notes.`);
1478
- return;
1479
- }
1480
- let diff;
1481
- try {
1482
- diff = JSON.parse(fs.readFileSync(diffPath, 'utf8'));
1483
- } catch (jsonErr) {
1484
- console.error(`❌ Unable to parse connector diff JSON at ${diffPath}: ${jsonErr.message}\nPlease check the file for syntax errors or corruption.`);
1485
- return;
1486
- }
1487
- let whatsNewContent;
1488
- try {
1489
- whatsNewContent = fs.readFileSync(whatsNewPath, 'utf8');
1490
- } catch (readErr) {
1491
- console.error(`❌ Unable to read whats-new.adoc at ${whatsNewPath}: ${readErr.message}\nPlease check file permissions and try again.`);
1492
- return;
1493
- }
1494
- const whatsNew = whatsNewContent;
1495
- // Regex to find section for this version
1496
- const versionTitle = `== Version ${diff.comparison.newVersion}`;
1497
- const versionRe = new RegExp(`^== Version ${diff.comparison.newVersion.replace(/[-.]/g, '\\$&')}(?:\\r?\\n|$)`, 'm');
1498
- const match = versionRe.exec(whatsNew);
1499
- let startIdx = match ? match.index : -1;
1500
- let endIdx = -1;
1501
- if (startIdx !== -1) {
1502
- // Find the start of the next version section
1503
- const rest = whatsNew.slice(startIdx + 1);
1504
- const nextMatch = /^== Version /m.exec(rest);
1505
- endIdx = nextMatch ? startIdx + 1 + nextMatch.index : whatsNew.length;
1506
- }
1507
- // Compose new section
1508
- // Add link to full release notes for this connector version after version heading
1509
- let releaseNotesLink = '';
1510
- if (diff.comparison && diff.comparison.newVersion) {
1511
- releaseNotesLink = `link:https://github.com/redpanda-data/connect/releases/tag/v${diff.comparison.newVersion}[See the full release notes^].\n\n`;
1512
- }
1513
- let section = `\n== Version ${diff.comparison.newVersion}\n\n${releaseNotesLink}`;
1514
-
1515
- // Separate Bloblang components from regular components
1516
- const bloblangComponents = [];
1517
- const regularComponents = [];
1518
-
1519
- if (diff.details.newComponents && diff.details.newComponents.length) {
1520
- for (const comp of diff.details.newComponents) {
1521
- if (comp.type === 'bloblang-functions' || comp.type === 'bloblang-methods') {
1522
- bloblangComponents.push(comp);
1523
- } else {
1524
- regularComponents.push(comp);
1525
- }
1526
- }
1527
- }
1528
-
1529
- // Bloblang updates section
1530
- if (bloblangComponents.length > 0) {
1531
- section += '=== Bloblang updates\n\n';
1532
- section += 'This release adds the following new Bloblang capabilities:\n\n';
1533
-
1534
- // Group by type (functions vs methods)
1535
- const byType = {};
1536
- for (const comp of bloblangComponents) {
1537
- if (!byType[comp.type]) byType[comp.type] = [];
1538
- byType[comp.type].push(comp);
1539
- }
1540
-
1541
- for (const [type, comps] of Object.entries(byType)) {
1542
- if (type === 'bloblang-functions') {
1543
- section += '* Functions:\n';
1544
- for (const comp of comps) {
1545
- section += `** xref:guides:bloblang/functions.adoc#${comp.name}[\`${comp.name}\`]`;
1546
- if (comp.status && comp.status !== 'stable') section += ` (${comp.status})`;
1547
- if (comp.description) section += `: ${capToTwoSentences(comp.description)}`;
1548
- section += '\n';
1549
- }
1550
- } else if (type === 'bloblang-methods') {
1551
- section += '* Methods:\n';
1552
- for (const comp of comps) {
1553
- section += `** xref:guides:bloblang/methods.adoc#${comp.name}[\`${comp.name}\`]`;
1554
- if (comp.status && comp.status !== 'stable') section += ` (${comp.status})`;
1555
- if (comp.description) section += `: ${capToTwoSentences(comp.description)}`;
1556
- section += '\n';
1557
- }
1558
- }
1559
- }
1560
- section += '\n';
1561
- }
1562
-
1563
- // Regular component updates section
1564
- if (regularComponents.length > 0) {
1565
- section += '=== Component updates\n\n';
1566
- section += 'This release adds the following new components:\n\n';
1567
-
1568
- section += '[cols="1m,1,1,3"]\n';
1569
- section += '|===\n';
1570
- section += '|Component |Type |Status |Description\n\n';
1571
-
1572
- for (const comp of regularComponents) {
1573
- const typeLabel = comp.type.charAt(0).toUpperCase() + comp.type.slice(1);
1574
- const statusLabel = comp.status || '-';
1575
- const desc = comp.description ? capToTwoSentences(comp.description) : '-';
1576
-
1577
- section += `|xref:components:${comp.type}/${comp.name}.adoc[${comp.name}]\n`;
1578
- section += `|${typeLabel}\n`;
1579
- section += `|${statusLabel}\n`;
1580
- section += `|${desc}\n\n`;
1581
- }
1582
-
1583
- section += '|===\n\n';
1584
- }
1585
-
1586
- // New fields (exclude Bloblang functions/methods)
1587
- if (diff.details.newFields && diff.details.newFields.length) {
1588
- // Filter out Bloblang components
1589
- const regularFields = diff.details.newFields.filter(field => {
1590
- const [type] = field.component.split(':');
1591
- return type !== 'bloblang-functions' && type !== 'bloblang-methods';
1592
- });
1593
-
1594
- if (regularFields.length > 0) {
1595
- section += '\n=== New field support\n\n';
1596
- section += 'This release adds support for the following new fields:\n\n';
1597
-
1598
- // Group by field name
1599
- const byField = {};
1600
- for (const field of regularFields) {
1601
- const [type, compName] = field.component.split(':');
1602
- if (!byField[field.field]) {
1603
- byField[field.field] = {
1604
- description: field.description,
1605
- components: []
1606
- };
1607
- }
1608
- byField[field.field].components.push({ type, name: compName });
1609
- }
1610
-
1611
- section += '[cols="1m,3,2a"]\n';
1612
- section += '|===\n';
1613
- section += '|Field |Description |Affected components\n\n';
1614
-
1615
- for (const [fieldName, info] of Object.entries(byField)) {
1616
- // Format component list - group by type
1617
- const byType = {};
1618
- for (const comp of info.components) {
1619
- if (!byType[comp.type]) byType[comp.type] = [];
1620
- byType[comp.type].push(comp.name);
1621
- }
1622
-
1623
- let componentList = '';
1624
- for (const [type, names] of Object.entries(byType)) {
1625
- if (componentList) componentList += '\n\n';
1626
-
1627
- // Smart pluralization: don't add 's' if already plural
1628
- const typeLabel = names.length === 1
1629
- ? type.charAt(0).toUpperCase() + type.slice(1)
1630
- : type.charAt(0).toUpperCase() + type.slice(1) + (type.endsWith('s') ? '' : 's');
1631
-
1632
- componentList += `*${typeLabel}:*\n\n`;
1633
- names.forEach(name => {
1634
- componentList += `* xref:components:${type}/${name}.adoc#${fieldName}[${name}]\n`;
1635
- });
1636
- }
1637
-
1638
- const desc = info.description ? capToTwoSentences(info.description) : '-';
1639
-
1640
- section += `|${fieldName}\n`;
1641
- section += `|${desc}\n`;
1642
- section += `|${componentList}\n\n`;
1643
- }
1644
-
1645
- section += '|===\n\n';
1646
- }
1647
- }
1648
-
1649
- // Deprecated components
1650
- if (diff.details.deprecatedComponents && diff.details.deprecatedComponents.length) {
1651
- section += '\n=== Deprecations\n\n';
1652
- section += 'The following components are now deprecated:\n\n';
1653
-
1654
- section += '[cols="1m,1,3"]\n';
1655
- section += '|===\n';
1656
- section += '|Component |Type |Description\n\n';
1657
-
1658
- for (const comp of diff.details.deprecatedComponents) {
1659
- const typeLabel = comp.type.charAt(0).toUpperCase() + comp.type.slice(1);
1660
- const desc = comp.description ? capToTwoSentences(comp.description) : '-';
1661
-
1662
- if (comp.type === 'bloblang-functions') {
1663
- section += `|xref:guides:bloblang/functions.adoc#${comp.name}[${comp.name}]\n`;
1664
- } else if (comp.type === 'bloblang-methods') {
1665
- section += `|xref:guides:bloblang/methods.adoc#${comp.name}[${comp.name}]\n`;
1666
- } else {
1667
- section += `|xref:components:${comp.type}/${comp.name}.adoc[${comp.name}]\n`;
1668
- }
1669
- section += `|${typeLabel}\n`;
1670
- section += `|${desc}\n\n`;
1671
- }
1672
-
1673
- section += '|===\n\n';
1674
- }
1675
-
1676
- // Deprecated fields (exclude Bloblang functions/methods)
1677
- if (diff.details.deprecatedFields && diff.details.deprecatedFields.length) {
1678
- // Filter out Bloblang components
1679
- const regularDeprecatedFields = diff.details.deprecatedFields.filter(field => {
1680
- const [type] = field.component.split(':');
1681
- return type !== 'bloblang-functions' && type !== 'bloblang-methods';
1682
- });
1683
-
1684
- if (regularDeprecatedFields.length > 0) {
1685
- if (!diff.details.deprecatedComponents || diff.details.deprecatedComponents.length === 0) {
1686
- section += '\n=== Deprecations\n\n';
1687
- } else {
1688
- section += '\n';
1689
- }
1690
- section += 'The following fields are now deprecated:\n\n';
1691
-
1692
- // Group by field name
1693
- const byField = {};
1694
- for (const field of regularDeprecatedFields) {
1695
- const [type, compName] = field.component.split(':');
1696
- if (!byField[field.field]) {
1697
- byField[field.field] = {
1698
- description: field.description,
1699
- components: []
1700
- };
1701
- }
1702
- byField[field.field].components.push({ type, name: compName });
1703
- }
1704
-
1705
- section += '[cols="1m,3,2a"]\n';
1706
- section += '|===\n';
1707
- section += '|Field |Description |Affected components\n\n';
1708
-
1709
- for (const [fieldName, info] of Object.entries(byField)) {
1710
- // Format component list - group by type
1711
- const byType = {};
1712
- for (const comp of info.components) {
1713
- if (!byType[comp.type]) byType[comp.type] = [];
1714
- byType[comp.type].push(comp.name);
1715
- }
1716
-
1717
- let componentList = '';
1718
- for (const [type, names] of Object.entries(byType)) {
1719
- if (componentList) componentList += '\n\n';
1720
-
1721
- // Smart pluralization: don't add 's' if already plural
1722
- const typeLabel = names.length === 1
1723
- ? type.charAt(0).toUpperCase() + type.slice(1)
1724
- : type.charAt(0).toUpperCase() + type.slice(1) + (type.endsWith('s') ? '' : 's');
1725
-
1726
- componentList += `*${typeLabel}:*\n\n`;
1727
- names.forEach(name => {
1728
- componentList += `* xref:components:${type}/${name}.adoc#${fieldName}[${name}]\n`;
1729
- });
1730
- }
1731
-
1732
- const desc = info.description ? capToTwoSentences(info.description) : '-';
1733
-
1734
- section += `|${fieldName}\n`;
1735
- section += `|${desc}\n`;
1736
- section += `|${componentList}\n\n`;
1737
- }
1738
-
1739
- section += '|===\n\n';
1740
- }
1741
- }
1742
-
1743
- // Changed defaults (exclude Bloblang functions/methods)
1744
- if (diff.details.changedDefaults && diff.details.changedDefaults.length) {
1745
- // Filter out Bloblang components
1746
- const regularChangedDefaults = diff.details.changedDefaults.filter(change => {
1747
- const [type] = change.component.split(':');
1748
- return type !== 'bloblang-functions' && type !== 'bloblang-methods';
1749
- });
1750
-
1751
- if (regularChangedDefaults.length > 0) {
1752
- section += '\n=== Default value changes\n\n';
1753
- section += 'This release includes the following default value changes:\n\n';
1754
-
1755
- // Group by field name and default values to avoid overwriting different default changes
1756
- const byFieldAndDefaults = {};
1757
- for (const change of regularChangedDefaults) {
1758
- const [type, compName] = change.component.split(':');
1759
- const compositeKey = `${change.field}|${String(change.oldDefault)}|${String(change.newDefault)}`;
1760
- if (!byFieldAndDefaults[compositeKey]) {
1761
- byFieldAndDefaults[compositeKey] = {
1762
- field: change.field,
1763
- oldDefault: change.oldDefault,
1764
- newDefault: change.newDefault,
1765
- description: change.description,
1766
- components: []
1767
- };
1768
- }
1769
- byFieldAndDefaults[compositeKey].components.push({
1770
- type,
1771
- name: compName
1772
- });
1773
- }
1774
-
1775
- // Create table
1776
- section += '[cols="1m,1,1,3,2a"]\n';
1777
- section += '|===\n';
1778
- section += '|Field |Old default |New default |Description |Affected components\n\n';
1779
-
1780
- for (const [compositeKey, info] of Object.entries(byFieldAndDefaults)) {
1781
- // Format old and new defaults
1782
- const formatDefault = (val) => {
1783
- if (val === undefined || val === null) return 'none';
1784
- if (typeof val === 'string') return val;
1785
- if (typeof val === 'number' || typeof val === 'boolean') return String(val);
1786
- return JSON.stringify(val);
1787
- };
1788
-
1789
- const oldVal = formatDefault(info.oldDefault);
1790
- const newVal = formatDefault(info.newDefault);
1791
-
1792
- // Get description
1793
- const desc = info.description ? capToTwoSentences(info.description) : '-';
1794
-
1795
- // Format component references - group by type
1796
- const byType = {};
1797
- for (const comp of info.components) {
1798
- if (!byType[comp.type]) byType[comp.type] = [];
1799
- byType[comp.type].push(comp.name);
1800
- }
1801
-
1802
- let componentList = '';
1803
- for (const [type, names] of Object.entries(byType)) {
1804
- if (componentList) componentList += '\n\n';
1805
-
1806
- // Smart pluralization: don't add 's' if already plural
1807
- const typeLabel = names.length === 1
1808
- ? type.charAt(0).toUpperCase() + type.slice(1)
1809
- : type.charAt(0).toUpperCase() + type.slice(1) + (type.endsWith('s') ? '' : 's');
1810
-
1811
- componentList += `*${typeLabel}:*\n\n`;
1812
-
1813
- // List components, with links to the field anchor
1814
- names.forEach(name => {
1815
- componentList += `* xref:components:${type}/${name}.adoc#${info.field}[${name}]\n`;
1816
- });
1817
- }
1818
-
1819
- section += `|${info.field}\n`;
1820
- section += `|${oldVal}\n`;
1821
- section += `|${newVal}\n`;
1822
- section += `|${desc}\n`;
1823
- section += `|${componentList}\n\n`;
1824
- }
1825
-
1826
- section += '|===\n\n';
1827
- }
1828
- }
1829
-
1830
- let updated;
1831
- if (startIdx !== -1) {
1832
- // Replace the existing section
1833
- updated = whatsNew.slice(0, startIdx) + section + '\n' + whatsNew.slice(endIdx);
1834
- console.log(`♻️ whats-new.adoc: replaced section for Version ${diff.comparison.newVersion}`);
1835
- } else {
1836
- // Insert above first version heading
1837
- const versionHeading = /^== Version /m;
1838
- const firstMatch = versionHeading.exec(whatsNew);
1839
- let insertIdx = firstMatch ? firstMatch.index : 0;
1840
- updated = whatsNew.slice(0, insertIdx) + section + '\n' + whatsNew.slice(insertIdx);
1841
- console.log(`✅ whats-new.adoc updated with Version ${diff.comparison.newVersion}`);
1842
- }
1843
- fs.writeFileSync(whatsNewPath, updated, 'utf8');
1844
- } catch (err) {
1845
- console.error(`❌ Failed to update whats-new.adoc: ${err.message}`);
1846
- }
1847
- }
1848
-
1849
- console.log('\n📄 Summary:');
1850
- console.log(` • Run time: ${timestamp}`);
1851
- console.log(` • Version used: ${newVersion}`);
1852
- process.exit(0);
1853
- });
803
+ const { handleRpcnConnectorDocs } = require('../tools/redpanda-connect/rpcn-connector-docs-handler.js')
804
+ await handleRpcnConnectorDocs(options)
805
+ })
1854
806
 
1855
807
  /**
1856
808
  * generate property-docs
@@ -1878,26 +830,26 @@ automation
1878
830
  *
1879
831
  * # Include Cloud support tags (requires GitHub token)
1880
832
  * export GITHUB_TOKEN=ghp_xxx
1881
- * npx doc-tools generate property-docs \\
1882
- * --tag v25.3.1 \\
1883
- * --generate-partials \\
833
+ * npx doc-tools generate property-docs \
834
+ * --tag v25.3.1 \
835
+ * --generate-partials \
1884
836
  * --cloud-support
1885
837
  *
1886
838
  * # Compare properties between versions
1887
- * npx doc-tools generate property-docs \\
1888
- * --tag v25.3.1 \\
839
+ * npx doc-tools generate property-docs \
840
+ * --tag v25.3.1 \
1889
841
  * --diff v25.2.1
1890
842
  *
1891
843
  * # Use custom output directory
1892
- * npx doc-tools generate property-docs \\
1893
- * --tag v25.3.1 \\
844
+ * npx doc-tools generate property-docs \
845
+ * --tag v25.3.1 \
1894
846
  * --output-dir docs/modules/reference
1895
847
  *
1896
848
  * # Full workflow: document new release
1897
849
  * VERSION=$(npx doc-tools get-redpanda-version)
1898
- * npx doc-tools generate property-docs \\
1899
- * --tag $VERSION \\
1900
- * --generate-partials \\
850
+ * npx doc-tools generate property-docs \
851
+ * --tag $VERSION \
852
+ * --generate-partials \
1901
853
  * --cloud-support
1902
854
  *
1903
855
  * @requirements
@@ -1911,144 +863,148 @@ automation
1911
863
  .command('property-docs')
1912
864
  .description(
1913
865
  'Generate JSON and consolidated AsciiDoc partials for Redpanda configuration properties. ' +
1914
- 'By default, only extracts properties to JSON. Use --generate-partials to create consolidated ' +
1915
- 'AsciiDoc partials (including deprecated properties). Defaults to branch "dev" if neither --tag nor --branch is specified.'
866
+ 'Defaults to branch "dev" if neither --tag nor --branch is specified.'
1916
867
  )
1917
868
  .option('-t, --tag <tag>', 'Git tag for released content (GA/beta)')
1918
869
  .option('-b, --branch <branch>', 'Branch name for in-progress content')
1919
870
  .option('--diff <oldTag>', 'Also diff autogenerated properties from <oldTag> to current tag/branch')
1920
871
  .option('--overrides <path>', 'Optional JSON file with property description overrides', 'docs-data/property-overrides.json')
1921
872
  .option('--output-dir <dir>', 'Where to write all generated files', 'modules/reference')
1922
- .option('--cloud-support', 'Add AsciiDoc tags to generated property docs to indicate which ones are supported in Redpanda Cloud. This data is fetched from the cloudv2 repository so requires a GitHub token with repo permissions. Set the token as an environment variable using GITHUB_TOKEN, GH_TOKEN, or REDPANDA_GITHUB_TOKEN')
873
+ .option('--cloud-support', 'Add AsciiDoc tags to generated property docs to indicate which ones are supported in Redpanda Cloud. This data is fetched from the cloudv2 repository so requires a GitHub token with repo permissions. Set the token as an environment variable using GITHUB_TOKEN, GH_TOKEN, or REDPANDA_GITHUB_TOKEN', true)
1923
874
  .option('--template-property <path>', 'Custom Handlebars template for individual property sections')
1924
- .option('--template-topic-property <path>', 'Custom Handlebars template for individual topic property sections')
875
+ .option('--template-topic-property <path>', 'Custom Handlebars template for topic property sections')
1925
876
  .option('--template-topic-property-mappings <path>', 'Custom Handlebars template for topic property mappings table')
1926
877
  .option('--template-deprecated <path>', 'Custom Handlebars template for deprecated properties page')
1927
878
  .option('--template-deprecated-property <path>', 'Custom Handlebars template for individual deprecated property sections')
1928
- .option('--generate-partials', 'Generate consolidated property partials (cluster-properties.adoc, topic-properties.adoc, etc.) in the partials directory')
879
+ .option('--generate-partials', 'Generate consolidated property partials')
1929
880
  .option('--partials-dir <path>', 'Directory for property partials (relative to output-dir)', 'partials')
1930
881
  .action((options) => {
1931
- verifyPropertyDependencies();
882
+ verifyPropertyDependencies()
1932
883
 
1933
- // Validate that tag and branch are mutually exclusive
1934
884
  if (options.tag && options.branch) {
1935
- console.error('Error: Cannot specify both --tag and --branch');
1936
- process.exit(1);
885
+ console.error('Error: Cannot specify both --tag and --branch')
886
+ process.exit(1)
1937
887
  }
1938
888
 
1939
- // Default to 'dev' branch if neither specified
1940
- const newTag = options.tag || options.branch || 'dev';
889
+ const newTag = options.tag || options.branch || 'dev'
1941
890
 
1942
- // Validate cloud support dependencies if requested
1943
891
  if (options.cloudSupport) {
1944
- console.log('🔍 Validating cloud support dependencies...');
1945
- const { getGitHubToken } = require('../cli-utils/github-token');
1946
- const token = getGitHubToken();
892
+ console.log('Validating cloud support dependencies...')
893
+ const { getGitHubToken } = require('../cli-utils/github-token')
894
+ const token = getGitHubToken()
1947
895
  if (!token) {
1948
- console.error(' Cloud support requires a GitHub token');
1949
- console.error(' Set up GitHub token:');
1950
- console.error(' 1. Go to https://github.com/settings/tokens');
1951
- console.error(' 2. Generate token with "repo" scope');
1952
- console.error(' 3. Set: export GITHUB_TOKEN=your_token_here');
1953
- console.error(' Or: export GH_TOKEN=your_token_here');
1954
- console.error(' Or: export REDPANDA_GITHUB_TOKEN=your_token_here');
1955
- process.exit(1);
896
+ console.error('Error: Cloud support requires a GitHub token')
897
+ console.error(' Set: export GITHUB_TOKEN=your_token_here')
898
+ console.error(' Or disable cloud support with: --no-cloud-support')
899
+ process.exit(1)
1956
900
  }
1957
- console.log('📦 Cloud support enabled - Python dependencies will be validated during execution');
1958
- if (process.env.VIRTUAL_ENV) {
1959
- console.log(` Using virtual environment: ${process.env.VIRTUAL_ENV}`);
1960
- }
1961
- console.log(' Required packages: pyyaml, requests');
1962
- console.log('✅ GitHub token validated');
901
+ console.log('Done: GitHub token validated')
1963
902
  }
1964
- let oldTag = options.diff;
1965
- const overridesPath = options.overrides;
1966
- const outputDir = options.outputDir;
1967
- const cwd = path.resolve(__dirname, '../tools/property-extractor');
1968
903
 
1969
- // If --diff is not provided, try to get the latest-redpanda-tag from Antora attributes
904
+ let oldTag = options.diff
905
+
1970
906
  if (!oldTag) {
1971
- oldTag = getAntoraValue('asciidoc.attributes.latest-redpanda-tag');
907
+ oldTag = getAntoraValue('asciidoc.attributes.latest-redpanda-tag')
1972
908
  if (oldTag) {
1973
- console.log(`Using latest-redpanda-tag from Antora attributes for --diff: ${oldTag}`);
1974
- } else {
1975
- console.log('No --diff provided and no latest-redpanda-tag found in Antora attributes. Skipping diff.');
909
+ console.log(`Using latest-redpanda-tag from Antora attributes for --diff: ${oldTag}`)
1976
910
  }
1977
911
  }
1978
912
 
913
+ const overridesPath = options.overrides
914
+ const outputDir = options.outputDir
915
+ const cwd = path.resolve(__dirname, '../tools/property-extractor')
916
+
1979
917
  const make = (tag, overrides, templates = {}, outDir = 'modules/reference/') => {
1980
- console.log(`⏳ Building property docs for ${tag}…`);
1981
- const args = ['build', `TAG=${tag}`];
1982
- const env = { ...process.env };
1983
- if (overrides) {
1984
- env.OVERRIDES = path.resolve(overrides);
1985
- }
1986
- if (options.cloudSupport) {
1987
- env.CLOUD_SUPPORT = '1';
1988
- }
1989
- if (templates.property) {
1990
- env.TEMPLATE_PROPERTY = path.resolve(templates.property);
1991
- }
1992
- if (templates.topicProperty) {
1993
- env.TEMPLATE_TOPIC_PROPERTY = path.resolve(templates.topicProperty);
1994
- }
1995
- if (templates.topicPropertyMappings) {
1996
- env.TEMPLATE_TOPIC_PROPERTY_MAPPINGS = path.resolve(templates.topicPropertyMappings);
1997
- }
1998
- if (templates.deprecated) {
1999
- env.TEMPLATE_DEPRECATED = path.resolve(templates.deprecated);
2000
- }
2001
- if (templates.deprecatedProperty) {
2002
- env.TEMPLATE_DEPRECATED_PROPERTY = path.resolve(templates.deprecatedProperty);
2003
- }
2004
- env.OUTPUT_JSON_DIR = path.resolve(outDir, 'attachments');
2005
- env.OUTPUT_AUTOGENERATED_DIR = path.resolve(outDir);
918
+ console.log(`Building property docs for ${tag}…`)
919
+ const args = ['build', `TAG=${tag}`]
920
+ const env = { ...process.env }
921
+ if (overrides) env.OVERRIDES = path.resolve(overrides)
922
+ if (options.cloudSupport) env.CLOUD_SUPPORT = '1'
923
+ if (templates.property) env.TEMPLATE_PROPERTY = path.resolve(templates.property)
924
+ if (templates.topicProperty) env.TEMPLATE_TOPIC_PROPERTY = path.resolve(templates.topicProperty)
925
+ if (templates.topicPropertyMappings) env.TEMPLATE_TOPIC_PROPERTY_MAPPINGS = path.resolve(templates.topicPropertyMappings)
926
+ if (templates.deprecated) env.TEMPLATE_DEPRECATED = path.resolve(templates.deprecated)
927
+ if (templates.deprecatedProperty) env.TEMPLATE_DEPRECATED_PROPERTY = path.resolve(templates.deprecatedProperty)
928
+ env.OUTPUT_JSON_DIR = path.resolve(outDir, 'attachments')
929
+ env.OUTPUT_AUTOGENERATED_DIR = path.resolve(outDir)
2006
930
  if (options.generatePartials) {
2007
- env.GENERATE_PARTIALS = '1';
2008
- env.OUTPUT_PARTIALS_DIR = path.resolve(outDir, options.partialsDir || 'partials');
931
+ env.GENERATE_PARTIALS = '1'
932
+ env.OUTPUT_PARTIALS_DIR = path.resolve(outDir, options.partialsDir || 'partials')
2009
933
  }
2010
- const r = spawnSync('make', args, { cwd, stdio: 'inherit', env });
934
+ const r = spawnSync('make', args, { cwd, stdio: 'inherit', env })
2011
935
  if (r.error) {
2012
- console.error(`❌ ${r.error.message}`);
2013
- process.exit(1);
936
+ console.error(`Error: ${r.error.message}`)
937
+ process.exit(1)
2014
938
  }
2015
- if (r.status !== 0) process.exit(r.status);
2016
- };
939
+ if (r.status !== 0) process.exit(r.status)
940
+ }
2017
941
 
2018
942
  const templates = {
2019
943
  property: options.templateProperty,
2020
944
  topicProperty: options.templateTopicProperty,
2021
945
  topicPropertyMappings: options.templateTopicPropertyMappings,
2022
946
  deprecated: options.templateDeprecated,
2023
- deprecatedProperty: options.templateDeprecatedProperty,
2024
- };
947
+ deprecatedProperty: options.templateDeprecatedProperty
948
+ }
2025
949
 
2026
- const tagsAreSame = oldTag && newTag && oldTag === newTag;
950
+ const tagsAreSame = oldTag && newTag && oldTag === newTag
2027
951
  if (oldTag && !tagsAreSame) {
2028
- make(oldTag, overridesPath, templates, outputDir);
952
+ make(oldTag, overridesPath, templates, outputDir)
2029
953
  }
2030
- make(newTag, overridesPath, templates, outputDir);
954
+ make(newTag, overridesPath, templates, outputDir)
2031
955
  if (oldTag && !tagsAreSame) {
2032
- // Save diff to overrides directory if OVERRIDES is specified, otherwise to outputDir
2033
- const diffOutputDir = overridesPath ? path.dirname(path.resolve(overridesPath)) : outputDir;
2034
- generatePropertyComparisonReport(oldTag, newTag, diffOutputDir);
2035
-
2036
- // Cleanup old diff files (keep only 2 most recent)
2037
- cleanupOldDiffs(diffOutputDir);
2038
- } else if (tagsAreSame) {
2039
- console.log('--diff and --tag are the same. Skipping diff and Antora config update.');
956
+ const diffOutputDir = overridesPath ? path.dirname(path.resolve(overridesPath)) : outputDir
957
+ generatePropertyComparisonReport(oldTag, newTag, diffOutputDir)
958
+
959
+ try {
960
+ const diffReportPath = path.join(diffOutputDir, `redpanda-property-changes-${oldTag}-to-${newTag}.json`)
961
+ if (fs.existsSync(diffReportPath)) {
962
+ const diffData = JSON.parse(fs.readFileSync(diffReportPath, 'utf8'))
963
+ const { printPRSummary } = require('../tools/property-extractor/pr-summary-formatter')
964
+ printPRSummary(diffData)
965
+
966
+ if (overridesPath && fs.existsSync(overridesPath)) {
967
+ updatePropertyOverridesWithVersion(overridesPath, diffData, newTag)
968
+ }
969
+ }
970
+ } catch (err) {
971
+ console.warn(`Warning: Failed to generate PR summary: ${err.message}`)
972
+ }
973
+
974
+ cleanupOldDiffs(diffOutputDir)
2040
975
  }
2041
976
 
2042
- // If we used Antora's latest-redpanda-tag for diff, update it to the new tag
2043
977
  if (!options.diff && !tagsAreSame) {
2044
- const success = setAntoraValue('asciidoc.attributes.latest-redpanda-tag', newTag);
2045
- if (success) {
2046
- console.log(`✅ Updated Antora latest-redpanda-tag to: ${newTag}`);
978
+ const tagSuccess = setAntoraValue('asciidoc.attributes.latest-redpanda-tag', newTag)
979
+ if (tagSuccess) console.log(`Done: Updated Antora latest-redpanda-tag to: ${newTag}`)
980
+
981
+ const versionWithoutV = newTag.startsWith('v') ? newTag.slice(1) : newTag
982
+ const versionSuccess = setAntoraValue('asciidoc.attributes.full-version', versionWithoutV)
983
+ if (versionSuccess) console.log(`Done: Updated Antora full-version to: ${versionWithoutV}`)
984
+
985
+ try {
986
+ const jsonDir = path.resolve(outputDir, 'attachments')
987
+ const propertyFiles = fs.readdirSync(jsonDir)
988
+ .filter(f => /^redpanda-properties-v[\d.]+\.json$/.test(f))
989
+ .sort()
990
+
991
+ const keepFile = `redpanda-properties-${newTag}.json`
992
+ const filesToDelete = propertyFiles.filter(f => f !== keepFile)
993
+
994
+ if (filesToDelete.length > 0) {
995
+ console.log('🧹 Cleaning up old property JSON files...')
996
+ filesToDelete.forEach(file => {
997
+ fs.unlinkSync(path.join(jsonDir, file))
998
+ console.log(` Deleted: ${file}`)
999
+ })
1000
+ }
1001
+ } catch (err) {
1002
+ console.warn(`Warning: Failed to cleanup old property JSON files: ${err.message}`)
2047
1003
  }
2048
1004
  }
2049
1005
 
2050
- process.exit(0);
2051
- });
1006
+ process.exit(0)
1007
+ })
2052
1008
 
2053
1009
  /**
2054
1010
  * generate rpk-docs
@@ -2072,13 +1028,13 @@ automation
2072
1028
  * npx doc-tools generate rpk-docs --tag v25.3.1
2073
1029
  *
2074
1030
  * # Compare RPK commands between versions
2075
- * npx doc-tools generate rpk-docs \\
2076
- * --tag v25.3.1 \\
1031
+ * npx doc-tools generate rpk-docs \
1032
+ * --tag v25.3.1 \
2077
1033
  * --diff v25.2.1
2078
1034
  *
2079
1035
  * # Use custom Docker repository
2080
- * npx doc-tools generate rpk-docs \\
2081
- * --tag v25.3.1 \\
1036
+ * npx doc-tools generate rpk-docs \
1037
+ * --tag v25.3.1 \
2082
1038
  * --docker-repo docker.redpanda.com/redpandadata/redpanda
2083
1039
  *
2084
1040
  * # Full workflow: document new release
@@ -2095,52 +1051,38 @@ automation
2095
1051
  .description('Generate AsciiDoc documentation for rpk CLI commands. Defaults to branch "dev" if neither --tag nor --branch is specified.')
2096
1052
  .option('-t, --tag <tag>', 'Git tag for released content (GA/beta)')
2097
1053
  .option('-b, --branch <branch>', 'Branch name for in-progress content')
2098
- .option(
2099
- '--docker-repo <repo>',
2100
- 'Docker repository to use when starting Redpanda in Docker',
2101
- commonOptions.dockerRepo
2102
- )
2103
- .option(
2104
- '--console-tag <tag>',
2105
- 'Redpanda Console version to use when starting Redpanda Console in Docker',
2106
- commonOptions.consoleTag
2107
- )
2108
- .option(
2109
- '--console-docker-repo <repo>',
2110
- 'Docker repository to use when starting Redpanda Console in Docker',
2111
- commonOptions.consoleDockerRepo
2112
- )
1054
+ .option('--docker-repo <repo>', 'Docker repository to use', commonOptions.dockerRepo)
1055
+ .option('--console-tag <tag>', 'Redpanda Console version to use', commonOptions.consoleTag)
1056
+ .option('--console-docker-repo <repo>', 'Docker repository for Console', commonOptions.consoleDockerRepo)
2113
1057
  .option('--diff <oldTag>', 'Also diff autogenerated rpk docs from <oldTag> → <tag>')
2114
1058
  .action((options) => {
2115
- verifyMetricsDependencies();
1059
+ verifyMetricsDependencies()
2116
1060
 
2117
- // Validate that tag and branch are mutually exclusive
2118
1061
  if (options.tag && options.branch) {
2119
- console.error('Error: Cannot specify both --tag and --branch');
2120
- process.exit(1);
1062
+ console.error('Error: Cannot specify both --tag and --branch')
1063
+ process.exit(1)
2121
1064
  }
2122
1065
 
2123
- // Default to 'dev' branch if neither specified
2124
- const newTag = options.tag || options.branch || 'dev';
2125
- const oldTag = options.diff;
1066
+ const newTag = options.tag || options.branch || 'dev'
1067
+ const oldTag = options.diff
2126
1068
 
2127
1069
  if (oldTag) {
2128
- const oldDir = path.join('autogenerated', oldTag, 'rpk');
1070
+ const oldDir = path.join('autogenerated', oldTag, 'rpk')
2129
1071
  if (!fs.existsSync(oldDir)) {
2130
- console.log(`⏳ Generating rpk docs for old tag ${oldTag}…`);
2131
- runClusterDocs('rpk', oldTag, options);
1072
+ console.log(`Generating rpk docs for old tag ${oldTag}…`)
1073
+ runClusterDocs('rpk', oldTag, options)
2132
1074
  }
2133
1075
  }
2134
1076
 
2135
- console.log(`⏳ Generating rpk docs for new tag ${newTag}…`);
2136
- runClusterDocs('rpk', newTag, options);
1077
+ console.log(`Generating rpk docs for new tag ${newTag}…`)
1078
+ runClusterDocs('rpk', newTag, options)
2137
1079
 
2138
1080
  if (oldTag) {
2139
- diffDirs('rpk', oldTag, newTag);
1081
+ diffDirs('rpk', oldTag, newTag)
2140
1082
  }
2141
1083
 
2142
- process.exit(0);
2143
- });
1084
+ process.exit(0)
1085
+ })
2144
1086
 
2145
1087
  /**
2146
1088
  * generate helm-spec
@@ -2161,170 +1103,148 @@ automation
2161
1103
  *
2162
1104
  * @example
2163
1105
  * # Generate docs from GitHub repository
2164
- * npx doc-tools generate helm-spec \\
2165
- * --chart-dir https://github.com/redpanda-data/helm-charts \\
2166
- * --tag v5.9.0 \\
1106
+ * npx doc-tools generate helm-spec \
1107
+ * --chart-dir https://github.com/redpanda-data/helm-charts \
1108
+ * --tag v5.9.0 \
2167
1109
  * --output-dir modules/deploy/pages
2168
1110
  *
2169
1111
  * # Generate docs from local chart directory
2170
- * npx doc-tools generate helm-spec \\
2171
- * --chart-dir ./charts/redpanda \\
1112
+ * npx doc-tools generate helm-spec \
1113
+ * --chart-dir ./charts/redpanda \
2172
1114
  * --output-dir docs/modules/deploy/pages
2173
1115
  *
2174
1116
  * # Use custom README and output suffix
2175
- * npx doc-tools generate helm-spec \\
2176
- * --chart-dir https://github.com/redpanda-data/helm-charts \\
2177
- * --tag v5.9.0 \\
2178
- * --readme docs/README.md \\
1117
+ * npx doc-tools generate helm-spec \
1118
+ * --chart-dir https://github.com/redpanda-data/helm-charts \
1119
+ * --tag v5.9.0 \
1120
+ * --readme docs/README.md \
2179
1121
  * --output-suffix -values.adoc
2180
1122
  *
2181
1123
  * @requirements
2182
1124
  * - For GitHub URLs: Git and internet connection
2183
1125
  * - For local charts: Chart directory must contain Chart.yaml
2184
1126
  * - README.md file in chart directory (optional but recommended)
1127
+ * - helm-docs and pandoc must be installed
2185
1128
  */
2186
1129
  automation
2187
1130
  .command('helm-spec')
2188
- .description(
2189
- `Generate AsciiDoc documentation for one or more Helm charts (supports local dirs or GitHub URLs). When using GitHub URLs, requires either --tag or --branch to be specified.`
2190
- )
2191
- .option(
2192
- '--chart-dir <dir|url>',
2193
- 'Chart directory (contains Chart.yaml) or a root containing multiple charts, or a GitHub URL',
2194
- 'https://github.com/redpanda-data/redpanda-operator/charts'
2195
- )
2196
- .option('-t, --tag <tag>', 'Git tag for released content when using GitHub URL (auto-prepends "operator/" for redpanda-operator repository)')
2197
- .option('-b, --branch <branch>', 'Branch name for in-progress content when using GitHub URL')
1131
+ .description('Generate AsciiDoc documentation for Helm charts. Requires either --tag or --branch for GitHub URLs.')
1132
+ .option('--chart-dir <dir|url>', 'Chart directory or GitHub URL', 'https://github.com/redpanda-data/redpanda-operator/charts')
1133
+ .option('-t, --tag <tag>', 'Git tag for released content')
1134
+ .option('-b, --branch <branch>', 'Branch name for in-progress content')
2198
1135
  .option('--readme <file>', 'Relative README.md path inside each chart dir', 'README.md')
2199
- .option('--output-dir <dir>', 'Where to write all generated AsciiDoc files', 'modules/reference/pages')
2200
- .option('--output-suffix <suffix>', 'Suffix to append to each chart name (including extension)', '-helm-spec.adoc')
1136
+ .option('--output-dir <dir>', 'Where to write generated AsciiDoc files', 'modules/reference/pages')
1137
+ .option('--output-suffix <suffix>', 'Suffix to append to each chart name', '-helm-spec.adoc')
2201
1138
  .action((opts) => {
2202
- verifyHelmDependencies();
1139
+ verifyHelmDependencies()
2203
1140
 
2204
- // Prepare chart-root (local or GitHub)
2205
- let root = opts.chartDir;
2206
- let tmpClone = null;
1141
+ let root = opts.chartDir
1142
+ let tmpClone = null
2207
1143
 
2208
1144
  if (/^https?:\/\/github\.com\//.test(root)) {
2209
- // Validate tag/branch for GitHub URLs
2210
1145
  if (!opts.tag && !opts.branch) {
2211
- console.error(' When using a GitHub URL you must pass either --tag or --branch');
2212
- process.exit(1);
1146
+ console.error('Error: When using a GitHub URL you must pass either --tag or --branch')
1147
+ process.exit(1)
2213
1148
  }
2214
1149
  if (opts.tag && opts.branch) {
2215
- console.error(' Cannot specify both --tag and --branch');
2216
- process.exit(1);
1150
+ console.error('Error: Cannot specify both --tag and --branch')
1151
+ process.exit(1)
2217
1152
  }
2218
1153
 
2219
- let gitRef = opts.tag || opts.branch;
1154
+ let gitRef = opts.tag || opts.branch
2220
1155
 
2221
- // Normalize tag: add 'v' prefix if not present for tags
2222
1156
  if (opts.tag && !gitRef.startsWith('v')) {
2223
- gitRef = `v${gitRef}`;
2224
- console.log(`ℹ️ Auto-prepending "v" to tag: ${gitRef}`);
1157
+ gitRef = `v${gitRef}`
1158
+ console.log(`ℹ️ Auto-prepending "v" to tag: ${gitRef}`)
2225
1159
  }
2226
1160
 
2227
- const u = new URL(root);
2228
- const parts = u.pathname.replace(/\.git$/, '').split('/').filter(Boolean);
1161
+ const u = new URL(root)
1162
+ const parts = u.pathname.replace(/\.git$/, '').split('/').filter(Boolean)
2229
1163
  if (parts.length < 2) {
2230
- console.error(`❌ Invalid GitHub URL: ${root}`);
2231
- process.exit(1);
1164
+ console.error(`Error: Invalid GitHub URL: ${root}`)
1165
+ process.exit(1)
2232
1166
  }
2233
- const [owner, repo, ...sub] = parts;
2234
- const repoUrl = `https://${u.host}/${owner}/${repo}.git`;
1167
+ const [owner, repo, ...sub] = parts
1168
+ const repoUrl = `https://${u.host}/${owner}/${repo}.git`
2235
1169
 
2236
- // Auto-prepend "operator/" for tags in redpanda-operator repository
2237
1170
  if (opts.tag && owner === 'redpanda-data' && repo === 'redpanda-operator') {
2238
1171
  if (!gitRef.startsWith('operator/')) {
2239
- gitRef = `operator/${gitRef}`;
2240
- console.log(`ℹ️ Auto-prepending "operator/" to tag: ${gitRef}`);
1172
+ gitRef = `operator/${gitRef}`
1173
+ console.log(`ℹ️ Auto-prepending "operator/" to tag: ${gitRef}`)
2241
1174
  }
2242
1175
  }
2243
1176
 
2244
- console.log(`⏳ Verifying ${repoUrl}@${gitRef}…`);
2245
- const ok =
2246
- spawnSync(
2247
- 'git',
2248
- ['ls-remote', '--exit-code', repoUrl, `refs/heads/${gitRef}`, `refs/tags/${gitRef}`],
2249
- { stdio: 'ignore' }
2250
- ).status === 0;
1177
+ console.log(`Verifying ${repoUrl}@${gitRef}…`)
1178
+ const ok = spawnSync(
1179
+ 'git',
1180
+ ['ls-remote', '--exit-code', repoUrl, `refs/heads/${gitRef}`, `refs/tags/${gitRef}`],
1181
+ { stdio: 'ignore' }
1182
+ ).status === 0
2251
1183
  if (!ok) {
2252
- console.error(`❌ ${gitRef} not found on ${repoUrl}`);
2253
- process.exit(1);
1184
+ console.error(`Error: ${gitRef} not found on ${repoUrl}`)
1185
+ process.exit(1)
2254
1186
  }
2255
1187
 
2256
- const { getAuthenticatedGitHubUrl, hasGitHubToken } = require('../cli-utils/github-token');
1188
+ const { getAuthenticatedGitHubUrl, hasGitHubToken } = require('../cli-utils/github-token')
2257
1189
 
2258
- tmpClone = fs.mkdtempSync(path.join(os.tmpdir(), 'helm-'));
1190
+ tmpClone = fs.mkdtempSync(path.join(os.tmpdir(), 'helm-'))
2259
1191
 
2260
- // Use token if available for GitHub repos
2261
- let cloneUrl = repoUrl;
1192
+ let cloneUrl = repoUrl
2262
1193
  if (hasGitHubToken() && repoUrl.includes('github.com')) {
2263
- cloneUrl = getAuthenticatedGitHubUrl(repoUrl);
2264
- console.log(`⏳ Cloning ${repoUrl}@${gitRef} → ${tmpClone} (authenticated)`);
1194
+ cloneUrl = getAuthenticatedGitHubUrl(repoUrl)
1195
+ console.log(`Cloning ${repoUrl}@${gitRef} → ${tmpClone} (authenticated)`)
2265
1196
  } else {
2266
- console.log(`⏳ Cloning ${repoUrl}@${gitRef} → ${tmpClone}`);
1197
+ console.log(`Cloning ${repoUrl}@${gitRef} → ${tmpClone}`)
2267
1198
  }
2268
1199
 
2269
- if (
2270
- spawnSync('git', ['clone', '--depth', '1', '--branch', gitRef, cloneUrl, tmpClone], {
2271
- stdio: 'inherit',
2272
- }).status !== 0
2273
- ) {
2274
- console.error('❌ git clone failed');
2275
- process.exit(1);
1200
+ if (spawnSync('git', ['clone', '--depth', '1', '--branch', gitRef, cloneUrl, tmpClone], { stdio: 'inherit' }).status !== 0) {
1201
+ console.error('Error: git clone failed')
1202
+ process.exit(1)
2276
1203
  }
2277
- root = sub.length ? path.join(tmpClone, sub.join('/')) : tmpClone;
1204
+ root = sub.length ? path.join(tmpClone, sub.join('/')) : tmpClone
2278
1205
  }
2279
1206
 
2280
- // Discover charts
2281
1207
  if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
2282
- console.error(`❌ Chart root not found: ${root}`);
2283
- process.exit(1);
1208
+ console.error(`Error: Chart root not found: ${root}`)
1209
+ process.exit(1)
2284
1210
  }
2285
- let charts = [];
1211
+ let charts = []
2286
1212
  if (fs.existsSync(path.join(root, 'Chart.yaml'))) {
2287
- charts = [root];
1213
+ charts = [root]
2288
1214
  } else {
2289
- charts = fs
2290
- .readdirSync(root)
1215
+ charts = fs.readdirSync(root)
2291
1216
  .map((n) => path.join(root, n))
2292
- .filter((p) => fs.existsSync(path.join(p, 'Chart.yaml')));
1217
+ .filter((p) => fs.existsSync(path.join(p, 'Chart.yaml')))
2293
1218
  }
2294
1219
  if (charts.length === 0) {
2295
- console.error(`❌ No charts found under: ${root}`);
2296
- process.exit(1);
1220
+ console.error(`Error: No charts found under: ${root}`)
1221
+ process.exit(1)
2297
1222
  }
2298
1223
 
2299
- // Ensure output-dir exists
2300
- const outDir = path.resolve(opts.outputDir);
2301
- fs.mkdirSync(outDir, { recursive: true });
1224
+ const outDir = path.resolve(opts.outputDir)
1225
+ fs.mkdirSync(outDir, { recursive: true })
2302
1226
 
2303
- // Process each chart
2304
1227
  for (const chartPath of charts) {
2305
- const name = path.basename(chartPath);
2306
- console.log(`⏳ Processing chart "${name}"…`);
1228
+ const name = path.basename(chartPath)
1229
+ console.log(`Processing chart "${name}"…`)
2307
1230
 
2308
- // Regenerate README.md
2309
- console.log(`⏳ helm-docs in ${chartPath}`);
2310
- let r = spawnSync('helm-docs', { cwd: chartPath, stdio: 'inherit' });
2311
- if (r.status !== 0) process.exit(r.status);
1231
+ console.log(`helm-docs in ${chartPath}`)
1232
+ let r = spawnSync('helm-docs', { cwd: chartPath, stdio: 'inherit' })
1233
+ if (r.status !== 0) process.exit(r.status)
2312
1234
 
2313
- // Convert Markdown AsciiDoc
2314
- const md = path.join(chartPath, opts.readme);
1235
+ const md = path.join(chartPath, opts.readme)
2315
1236
  if (!fs.existsSync(md)) {
2316
- console.error(`❌ README not found: ${md}`);
2317
- process.exit(1);
1237
+ console.error(`Error: README not found: ${md}`)
1238
+ process.exit(1)
2318
1239
  }
2319
- const outFile = path.join(outDir, `k-${name}${opts.outputSuffix}`);
2320
- console.log(`⏳ pandoc ${md} → ${outFile}`);
2321
- fs.mkdirSync(path.dirname(outFile), { recursive: true });
2322
- r = spawnSync('pandoc', [md, '-t', 'asciidoc', '-o', outFile], { stdio: 'inherit' });
2323
- if (r.status !== 0) process.exit(r.status);
2324
-
2325
- // Post-process tweaks
2326
- let doc = fs.readFileSync(outFile, 'utf8');
2327
- const xrefRe = /https:\/\/docs\.redpanda\.com[^\s\]\[\)"]+(?:\[[^\]]*\])?/g;
1240
+ const outFile = path.join(outDir, `k-${name}${opts.outputSuffix}`)
1241
+ console.log(`pandoc ${md} → ${outFile}`)
1242
+ fs.mkdirSync(path.dirname(outFile), { recursive: true })
1243
+ r = spawnSync('pandoc', [md, '-t', 'asciidoc', '-o', outFile], { stdio: 'inherit' })
1244
+ if (r.status !== 0) process.exit(r.status)
1245
+
1246
+ let doc = fs.readFileSync(outFile, 'utf8')
1247
+ const xrefRe = /https:\/\/docs\.redpanda\.com[^\s\]\[\)"]+(?:\[[^\]]*\])?/g
2328
1248
  doc = doc
2329
1249
  .replace(/(\[\d+\])\]\./g, '$1\\].')
2330
1250
  .replace(/(\[\d+\])\]\]/g, '$1\\]\\]')
@@ -2332,36 +1252,30 @@ automation
2332
1252
  .replace(/^== # (.*)$/gm, '= $1')
2333
1253
  .replace(/^== description: (.*)$/gm, ':description: $1')
2334
1254
  .replace(xrefRe, (match) => {
2335
- let urlPart = match;
2336
- let bracketPart = '';
2337
- const m = match.match(/^([^\[]+)(\[[^\]]*\])$/);
1255
+ let urlPart = match
1256
+ let bracketPart = ''
1257
+ const m = match.match(/^([^\[]+)(\[[^\]]*\])$/)
2338
1258
  if (m) {
2339
- urlPart = m[1];
2340
- bracketPart = m[2];
2341
- }
2342
- if (urlPart.endsWith('#')) {
2343
- return match;
1259
+ urlPart = m[1]
1260
+ bracketPart = m[2]
2344
1261
  }
1262
+ if (urlPart.endsWith('#')) return match
2345
1263
  try {
2346
- const xref = urlToXref(urlPart);
2347
- return bracketPart ? `${xref}${bracketPart}` : `${xref}[]`;
1264
+ const xref = urlToXref(urlPart)
1265
+ return bracketPart ? `${xref}${bracketPart}` : `${xref}[]`
2348
1266
  } catch (err) {
2349
- console.warn(`⚠️ urlToXref failed on ${urlPart}: ${err.message}`);
2350
- return match;
1267
+ console.warn(`⚠️ urlToXref failed on ${urlPart}: ${err.message}`)
1268
+ return match
2351
1269
  }
2352
- });
2353
- fs.writeFileSync(outFile, doc, 'utf8');
1270
+ })
1271
+ fs.writeFileSync(outFile, doc, 'utf8')
2354
1272
 
2355
- console.log(`✅ Wrote ${outFile}`);
1273
+ console.log(`Done: Wrote ${outFile}`)
2356
1274
  }
2357
1275
 
2358
- // Cleanup
2359
- if (tmpClone) fs.rmSync(tmpClone, { recursive: true, force: true });
2360
- });
1276
+ if (tmpClone) fs.rmSync(tmpClone, { recursive: true, force: true })
1277
+ })
2361
1278
 
2362
- /**
2363
- * Generate Markdown table of cloud regions and tiers from master-data.yaml
2364
- */
2365
1279
  /**
2366
1280
  * generate cloud-regions
2367
1281
  *
@@ -2394,7 +1308,7 @@ automation
2394
1308
  *
2395
1309
  * # Use custom output file
2396
1310
  * export GITHUB_TOKEN=ghp_xxx
2397
- * npx doc-tools generate cloud-regions \\
1311
+ * npx doc-tools generate cloud-regions \
2398
1312
  * --output custom/path/regions.md
2399
1313
  *
2400
1314
  * # Use different branch for testing
@@ -2418,21 +1332,21 @@ automation
2418
1332
  .option('--template <path>', 'Path to custom Handlebars template (relative to repo root)')
2419
1333
  .option('--dry-run', 'Print output to stdout instead of writing file')
2420
1334
  .action(async (options) => {
2421
- const { generateCloudRegions } = require('../tools/cloud-regions/generate-cloud-regions.js');
2422
- const { getGitHubToken } = require('../cli-utils/github-token');
1335
+ const { generateCloudRegions } = require('../tools/cloud-regions/generate-cloud-regions.js')
1336
+ const { getGitHubToken } = require('../cli-utils/github-token')
2423
1337
 
2424
1338
  try {
2425
- const token = getGitHubToken();
1339
+ const token = getGitHubToken()
2426
1340
  if (!token) {
2427
- throw new Error('GitHub token is required to fetch from private cloudv2-infra repo. Set GITHUB_TOKEN, GH_TOKEN, or REDPANDA_GITHUB_TOKEN.');
1341
+ throw new Error('GitHub token is required to fetch from private cloudv2-infra repo.')
2428
1342
  }
2429
- const fmt = (options.format || 'md').toLowerCase();
2430
- let templatePath = undefined;
1343
+ const fmt = (options.format || 'md').toLowerCase()
1344
+ let templatePath
2431
1345
  if (options.template) {
2432
- const repoRoot = findRepoRoot();
2433
- templatePath = path.resolve(repoRoot, options.template);
1346
+ const repoRoot = findRepoRoot()
1347
+ templatePath = path.resolve(repoRoot, options.template)
2434
1348
  if (!fs.existsSync(templatePath)) {
2435
- throw new Error(`Custom template not found: ${templatePath}`);
1349
+ throw new Error(`Custom template not found: ${templatePath}`)
2436
1350
  }
2437
1351
  }
2438
1352
  const out = await generateCloudRegions({
@@ -2442,24 +1356,23 @@ automation
2442
1356
  ref: options.ref,
2443
1357
  format: fmt,
2444
1358
  token,
2445
- template: templatePath,
2446
- });
1359
+ template: templatePath
1360
+ })
2447
1361
  if (options.dryRun) {
2448
- process.stdout.write(out);
2449
- console.log(`\n✅ (dry-run) ${fmt === 'adoc' ? 'AsciiDoc' : 'Markdown'} output printed to stdout.`);
1362
+ process.stdout.write(out)
1363
+ console.log(`\nDone: (dry-run) ${fmt === 'adoc' ? 'AsciiDoc' : 'Markdown'} output printed to stdout.`)
2450
1364
  } else {
2451
- // Always resolve output relative to repo root
2452
- const repoRoot = findRepoRoot();
2453
- const absOutput = path.resolve(repoRoot, options.output);
2454
- fs.mkdirSync(path.dirname(absOutput), { recursive: true });
2455
- fs.writeFileSync(absOutput, out, 'utf8');
2456
- console.log(`✅ Wrote ${absOutput}`);
1365
+ const repoRoot = findRepoRoot()
1366
+ const absOutput = path.resolve(repoRoot, options.output)
1367
+ fs.mkdirSync(path.dirname(absOutput), { recursive: true })
1368
+ fs.writeFileSync(absOutput, out, 'utf8')
1369
+ console.log(`Done: Wrote ${absOutput}`)
2457
1370
  }
2458
1371
  } catch (err) {
2459
- console.error(`❌ Failed to generate cloud regions: ${err.message}`);
2460
- process.exit(1);
1372
+ console.error(`Error: Failed to generate cloud regions: ${err.message}`)
1373
+ process.exit(1)
2461
1374
  }
2462
- });
1375
+ })
2463
1376
 
2464
1377
  /**
2465
1378
  * generate crd-spec
@@ -2500,9 +1413,9 @@ automation
2500
1413
  * npx doc-tools generate crd-spec --branch dev
2501
1414
  *
2502
1415
  * # Use custom templates and output location
2503
- * npx doc-tools generate crd-spec \\
2504
- * --tag operator/v2.2.6-25.3.1 \\
2505
- * --templates-dir custom/templates \\
1416
+ * npx doc-tools generate crd-spec \
1417
+ * --tag operator/v2.2.6-25.3.1 \
1418
+ * --templates-dir custom/templates \
2506
1419
  * --output modules/reference/pages/operator-crd.adoc
2507
1420
  *
2508
1421
  * @requirements
@@ -2512,42 +1425,29 @@ automation
2512
1425
  */
2513
1426
  automation
2514
1427
  .command('crd-spec')
2515
- .description('Generate Asciidoc documentation for Kubernetes CRD references. Requires either --tag or --branch to be specified.')
2516
- .option('-t, --tag <operatorTag>', 'Operator release tag for GA/beta content (for example operator/v2.2.6-25.3.1 or v25.1.2). Auto-prepends "operator/" if not present.')
2517
- .option('-b, --branch <branch>', 'Branch name for in-progress content (for example release/v2.2.x, main, dev)')
2518
- .option(
2519
- '-s, --source-path <src>',
2520
- 'CRD Go types dir or GitHub URL',
2521
- 'https://github.com/redpanda-data/redpanda-operator/operator/api/redpanda/v1alpha2'
2522
- )
1428
+ .description('Generate Asciidoc documentation for Kubernetes CRD references. Requires either --tag or --branch.')
1429
+ .option('-t, --tag <operatorTag>', 'Operator release tag for GA/beta content')
1430
+ .option('-b, --branch <branch>', 'Branch name for in-progress content')
1431
+ .option('-s, --source-path <src>', 'CRD Go types dir or GitHub URL', 'https://github.com/redpanda-data/redpanda-operator/operator/api/redpanda/v1alpha2')
2523
1432
  .option('-d, --depth <n>', 'How many levels deep', '10')
2524
1433
  .option('--templates-dir <dir>', 'Asciidoctor templates dir', '.github/crd-config/templates/asciidoctor/operator')
2525
1434
  .option('--output <file>', 'Where to write the generated AsciiDoc file', 'modules/reference/pages/k-crd.adoc')
2526
1435
  .action(async (opts) => {
2527
- verifyCrdDependencies();
1436
+ verifyCrdDependencies()
2528
1437
 
2529
- // Validate that either --tag or --branch is provided (but not both)
2530
1438
  if (!opts.tag && !opts.branch) {
2531
- console.error('Error: Either --tag or --branch must be specified');
2532
- process.exit(1);
1439
+ console.error('Error: Either --tag or --branch must be specified')
1440
+ process.exit(1)
2533
1441
  }
2534
1442
  if (opts.tag && opts.branch) {
2535
- console.error('Error: Cannot specify both --tag and --branch');
2536
- process.exit(1);
1443
+ console.error('Error: Cannot specify both --tag and --branch')
1444
+ process.exit(1)
2537
1445
  }
2538
1446
 
2539
- // Determine the git ref to use
2540
- let configRef;
2541
- if (opts.branch) {
2542
- // Branch - use as-is
2543
- configRef = opts.branch;
2544
- } else {
2545
- // Tag - auto-prepend operator/ if needed
2546
- configRef = opts.tag.startsWith('operator/') ? opts.tag : `operator/${opts.tag}`;
2547
- }
1447
+ let configRef = opts.branch || (opts.tag.startsWith('operator/') ? opts.tag : `operator/${opts.tag}`)
2548
1448
 
2549
- const configTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'crd-config-'));
2550
- console.log(`⏳ Fetching crd-ref-docs-config.yaml from redpanda-operator@${configRef}…`);
1449
+ const configTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'crd-config-'))
1450
+ console.log(`Fetching crd-ref-docs-config.yaml from redpanda-operator@${configRef}…`)
2551
1451
  await fetchFromGithub(
2552
1452
  'redpanda-data',
2553
1453
  'redpanda-operator',
@@ -2555,144 +1455,120 @@ automation
2555
1455
  configTmp,
2556
1456
  'crd-ref-docs-config.yaml',
2557
1457
  configRef
2558
- );
2559
- const configPath = path.join(configTmp, 'crd-ref-docs-config.yaml');
1458
+ )
1459
+ const configPath = path.join(configTmp, 'crd-ref-docs-config.yaml')
2560
1460
 
2561
- // Detect docs repo context
2562
- const repoRoot = findRepoRoot();
2563
- const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
2564
- const inDocs =
2565
- pkg.name === 'redpanda-docs-playbook' ||
2566
- (pkg.repository && pkg.repository.url.includes('redpanda-data/docs'));
2567
- let docsBranch = null;
1461
+ const repoRoot = findRepoRoot()
1462
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'))
1463
+ const inDocs = pkgJson.name === 'redpanda-docs-playbook' || (pkgJson.repository && pkgJson.repository.url.includes('redpanda-data/docs'))
1464
+ let docsBranch = null
2568
1465
 
2569
1466
  if (!inDocs) {
2570
- console.warn('⚠️ Not inside redpanda-data/docs; skipping branch suggestion.');
1467
+ console.warn('⚠️ Not inside redpanda-data/docs; skipping branch suggestion.')
2571
1468
  } else {
2572
1469
  try {
2573
- docsBranch = await determineDocsBranch(configRef);
2574
- console.log(`✅ Detected docs repo; you should commit to branch '${docsBranch}'.`);
1470
+ docsBranch = await determineDocsBranch(configRef)
1471
+ console.log(`Done: Detected docs repo; you should commit to branch '${docsBranch}'.`)
2575
1472
  } catch (err) {
2576
- console.error(`❌ Unable to determine docs branch: ${err.message}`);
2577
- process.exit(1);
1473
+ console.error(`Error: Unable to determine docs branch: ${err.message}`)
1474
+ process.exit(1)
2578
1475
  }
2579
1476
  }
2580
1477
 
2581
- // Validate templates
2582
1478
  if (!fs.existsSync(opts.templatesDir)) {
2583
- console.error(`❌ Templates directory not found: ${opts.templatesDir}`);
2584
- process.exit(1);
1479
+ console.error(`Error: Templates directory not found: ${opts.templatesDir}`)
1480
+ process.exit(1)
2585
1481
  }
2586
1482
 
2587
- // Prepare source (local folder or GitHub URL)
2588
- let localSrc = opts.sourcePath;
2589
- let tmpSrc;
1483
+ let localSrc = opts.sourcePath
1484
+ let tmpSrc
2590
1485
  if (/^https?:\/\/github\.com\//.test(opts.sourcePath)) {
2591
- const u = new URL(opts.sourcePath);
2592
- const parts = u.pathname.split('/').filter(Boolean);
1486
+ const u = new URL(opts.sourcePath)
1487
+ const parts = u.pathname.split('/').filter(Boolean)
2593
1488
  if (parts.length < 2) {
2594
- console.error(`❌ Invalid GitHub URL: ${opts.sourcePath}`);
2595
- process.exit(1);
1489
+ console.error(`Error: Invalid GitHub URL: ${opts.sourcePath}`)
1490
+ process.exit(1)
2596
1491
  }
2597
- const [owner, repo, ...subpathParts] = parts;
2598
- const repoUrl = `https://${u.host}/${owner}/${repo}`;
2599
- const subpath = subpathParts.join('/');
2600
- console.log(`⏳ Verifying "${configRef}" in ${repoUrl}…`);
2601
- const ok =
2602
- spawnSync('git', ['ls-remote', '--exit-code', repoUrl, `refs/tags/${configRef}`, `refs/heads/${configRef}`], {
2603
- stdio: 'ignore',
2604
- }).status === 0;
1492
+ const [owner, repo, ...subpathParts] = parts
1493
+ const repoUrl = `https://${u.host}/${owner}/${repo}`
1494
+ const subpath = subpathParts.join('/')
1495
+ console.log(`Verifying "${configRef}" in ${repoUrl}…`)
1496
+ const ok = spawnSync('git', ['ls-remote', '--exit-code', repoUrl, `refs/tags/${configRef}`, `refs/heads/${configRef}`], { stdio: 'ignore' }).status === 0
2605
1497
  if (!ok) {
2606
- console.error(`❌ Tag or branch "${configRef}" not found on ${repoUrl}`);
2607
- process.exit(1);
1498
+ console.error(`Error: Tag or branch "${configRef}" not found on ${repoUrl}`)
1499
+ process.exit(1)
2608
1500
  }
2609
- const { getAuthenticatedGitHubUrl, hasGitHubToken } = require('../cli-utils/github-token');
1501
+ const { getAuthenticatedGitHubUrl, hasGitHubToken } = require('../cli-utils/github-token')
2610
1502
 
2611
- tmpSrc = fs.mkdtempSync(path.join(os.tmpdir(), 'crd-src-'));
1503
+ tmpSrc = fs.mkdtempSync(path.join(os.tmpdir(), 'crd-src-'))
2612
1504
 
2613
- // Use token if available for GitHub repos
2614
- let cloneUrl = repoUrl;
1505
+ let cloneUrl = repoUrl
2615
1506
  if (hasGitHubToken() && repoUrl.includes('github.com')) {
2616
- cloneUrl = getAuthenticatedGitHubUrl(repoUrl);
2617
- console.log(`⏳ Cloning ${repoUrl}@${configRef} → ${tmpSrc} (authenticated)`);
1507
+ cloneUrl = getAuthenticatedGitHubUrl(repoUrl)
1508
+ console.log(`Cloning ${repoUrl}@${configRef} → ${tmpSrc} (authenticated)`)
2618
1509
  } else {
2619
- console.log(`⏳ Cloning ${repoUrl}@${configRef} → ${tmpSrc}`);
1510
+ console.log(`Cloning ${repoUrl}@${configRef} → ${tmpSrc}`)
2620
1511
  }
2621
1512
 
2622
- if (
2623
- spawnSync('git', ['clone', '--depth', '1', '--branch', configRef, cloneUrl, tmpSrc], {
2624
- stdio: 'inherit',
2625
- }).status !== 0
2626
- ) {
2627
- console.error(`❌ git clone failed`);
2628
- process.exit(1);
1513
+ if (spawnSync('git', ['clone', '--depth', '1', '--branch', configRef, cloneUrl, tmpSrc], { stdio: 'inherit' }).status !== 0) {
1514
+ console.error('Error: git clone failed')
1515
+ process.exit(1)
2629
1516
  }
2630
- localSrc = subpath ? path.join(tmpSrc, subpath) : tmpSrc;
1517
+ localSrc = subpath ? path.join(tmpSrc, subpath) : tmpSrc
2631
1518
  if (!fs.existsSync(localSrc)) {
2632
- console.error(`❌ Subdirectory not found in repo: ${subpath}`);
2633
- process.exit(1);
1519
+ console.error(`Error: Subdirectory not found in repo: ${subpath}`)
1520
+ process.exit(1)
2634
1521
  }
2635
1522
  }
2636
1523
 
2637
- // Ensure output directory exists
2638
- const outputDir = path.dirname(opts.output);
1524
+ const outputDir = path.dirname(opts.output)
2639
1525
  if (!fs.existsSync(outputDir)) {
2640
- fs.mkdirSync(outputDir, { recursive: true });
1526
+ fs.mkdirSync(outputDir, { recursive: true })
2641
1527
  }
2642
1528
 
2643
- // Run crd-ref-docs
2644
1529
  const args = [
2645
- '--source-path',
2646
- localSrc,
2647
- '--max-depth',
2648
- opts.depth,
2649
- '--templates-dir',
2650
- opts.templatesDir,
2651
- '--config',
2652
- configPath,
2653
- '--renderer',
2654
- 'asciidoctor',
2655
- '--output-path',
2656
- opts.output,
2657
- ];
2658
- console.log(`⏳ Running crd-ref-docs ${args.join(' ')}`);
1530
+ '--source-path', localSrc,
1531
+ '--max-depth', opts.depth,
1532
+ '--templates-dir', opts.templatesDir,
1533
+ '--config', configPath,
1534
+ '--renderer', 'asciidoctor',
1535
+ '--output-path', opts.output
1536
+ ]
1537
+ console.log(`Running crd-ref-docs ${args.join(' ')}`)
2659
1538
  if (spawnSync('crd-ref-docs', args, { stdio: 'inherit' }).status !== 0) {
2660
- console.error(`❌ crd-ref-docs failed`);
2661
- process.exit(1);
1539
+ console.error('Error: crd-ref-docs failed')
1540
+ process.exit(1)
2662
1541
  }
2663
1542
 
2664
- let doc = fs.readFileSync(opts.output, 'utf8');
2665
- const xrefRe = /https:\/\/docs\.redpanda\.com[^\s\]\[\)"]+(?:\[[^\]]*\])?/g;
1543
+ let doc = fs.readFileSync(opts.output, 'utf8')
1544
+ const xrefRe = /https:\/\/docs\.redpanda\.com[^\s\]\[\)"]+(?:\[[^\]]*\])?/g
2666
1545
  doc = doc.replace(xrefRe, (match) => {
2667
- let urlPart = match;
2668
- let bracketPart = '';
2669
- const m = match.match(/^([^\[]+)(\[[^\]]*\])$/);
1546
+ let urlPart = match
1547
+ let bracketPart = ''
1548
+ const m = match.match(/^([^\[]+)(\[[^\]]*\])$/)
2670
1549
  if (m) {
2671
- urlPart = m[1];
2672
- bracketPart = m[2];
2673
- }
2674
- if (urlPart.endsWith('#')) {
2675
- return match;
1550
+ urlPart = m[1]
1551
+ bracketPart = m[2]
2676
1552
  }
1553
+ if (urlPart.endsWith('#')) return match
2677
1554
  try {
2678
- const xref = urlToXref(urlPart);
2679
- return bracketPart ? `${xref}${bracketPart}` : `${xref}[]`;
1555
+ const xref = urlToXref(urlPart)
1556
+ return bracketPart ? `${xref}${bracketPart}` : `${xref}[]`
2680
1557
  } catch (err) {
2681
- console.warn(`⚠️ urlToXref failed on ${urlPart}: ${err.message}`);
2682
- return match;
1558
+ console.warn(`⚠️ urlToXref failed on ${urlPart}: ${err.message}`)
1559
+ return match
2683
1560
  }
2684
- });
2685
- fs.writeFileSync(opts.output, doc, 'utf8');
1561
+ })
1562
+ fs.writeFileSync(opts.output, doc, 'utf8')
2686
1563
 
2687
- // Cleanup
2688
- if (tmpSrc) fs.rmSync(tmpSrc, { recursive: true, force: true });
2689
- fs.rmSync(configTmp, { recursive: true, force: true });
1564
+ if (tmpSrc) fs.rmSync(tmpSrc, { recursive: true, force: true })
1565
+ fs.rmSync(configTmp, { recursive: true, force: true })
2690
1566
 
2691
- console.log(`✅ CRD docs generated at ${opts.output}`);
1567
+ console.log(`Done: CRD docs generated at ${opts.output}`)
2692
1568
  if (inDocs) {
2693
- console.log(`ℹ️ Don't forget to commit your changes on branch '${docsBranch}'.`);
1569
+ console.log(`ℹ️ Don't forget to commit your changes on branch '${docsBranch}'.`)
2694
1570
  }
2695
- });
1571
+ })
2696
1572
 
2697
1573
  /**
2698
1574
  * generate bundle-openapi
@@ -2715,26 +1591,26 @@ automation
2715
1591
  *
2716
1592
  * @example
2717
1593
  * # Bundle both Admin and Connect APIs
2718
- * npx doc-tools generate bundle-openapi \\
2719
- * --tag v25.3.1 \\
1594
+ * npx doc-tools generate bundle-openapi \
1595
+ * --tag v25.3.1 \
2720
1596
  * --surface both
2721
1597
  *
2722
1598
  * # Bundle only Admin API
2723
- * npx doc-tools generate bundle-openapi \\
2724
- * --tag v25.3.1 \\
1599
+ * npx doc-tools generate bundle-openapi \
1600
+ * --tag v25.3.1 \
2725
1601
  * --surface admin
2726
1602
  *
2727
1603
  * # Use custom output paths
2728
- * npx doc-tools generate bundle-openapi \\
2729
- * --tag v25.3.1 \\
2730
- * --surface both \\
2731
- * --out-admin api/admin-api.yaml \\
1604
+ * npx doc-tools generate bundle-openapi \
1605
+ * --tag v25.3.1 \
1606
+ * --surface both \
1607
+ * --out-admin api/admin-api.yaml \
2732
1608
  * --out-connect api/connect-api.yaml
2733
1609
  *
2734
1610
  * # Use major version for Admin API version field
2735
- * npx doc-tools generate bundle-openapi \\
2736
- * --tag v25.3.1 \\
2737
- * --surface admin \\
1611
+ * npx doc-tools generate bundle-openapi \
1612
+ * --tag v25.3.1 \
1613
+ * --surface admin \
2738
1614
  * --use-admin-major-version
2739
1615
  *
2740
1616
  * # Full workflow: generate API specs for new release
@@ -2750,9 +1626,9 @@ automation
2750
1626
  */
2751
1627
  automation
2752
1628
  .command('bundle-openapi')
2753
- .description('Bundle Redpanda OpenAPI fragments for admin and connect APIs into complete OpenAPI 3.1 documents. Requires either --tag or --branch to be specified.')
2754
- .option('-t, --tag <tag>', 'Git tag for released content (for example, v24.3.2 or 24.3.2)')
2755
- .option('-b, --branch <branch>', 'Branch name for in-progress content (for example, dev, main)')
1629
+ .description('Bundle Redpanda OpenAPI fragments for admin and connect APIs. Requires either --tag or --branch.')
1630
+ .option('-t, --tag <tag>', 'Git tag for released content')
1631
+ .option('-b, --branch <branch>', 'Branch name for in-progress content')
2756
1632
  .option('--repo <url>', 'Repository URL', 'https://github.com/redpanda-data/redpanda.git')
2757
1633
  .addOption(new Option('-s, --surface <surface>', 'Which API surface(s) to bundle').choices(['admin', 'connect', 'both']).makeOptionMandatory())
2758
1634
  .option('--out-admin <path>', 'Output path for admin API', 'admin/redpanda-admin-api.yaml')
@@ -2761,32 +1637,28 @@ automation
2761
1637
  .option('--use-admin-major-version', 'Use admin major version for info.version instead of git tag', false)
2762
1638
  .option('--quiet', 'Suppress logs', false)
2763
1639
  .action(async (options) => {
2764
- // Validate that either tag or branch is provided (but not both)
2765
1640
  if (!options.tag && !options.branch) {
2766
- console.error('Error: Either --tag or --branch must be specified');
2767
- process.exit(1);
1641
+ console.error('Error: Either --tag or --branch must be specified')
1642
+ process.exit(1)
2768
1643
  }
2769
1644
  if (options.tag && options.branch) {
2770
- console.error('Error: Cannot specify both --tag and --branch');
2771
- process.exit(1);
1645
+ console.error('Error: Cannot specify both --tag and --branch')
1646
+ process.exit(1)
2772
1647
  }
2773
1648
 
2774
- // Determine the git ref to use
2775
- const gitRef = options.tag || options.branch;
2776
- // Verify dependencies
2777
- requireCmd('git', 'Install Git: https://git-scm.com/downloads');
2778
- requireCmd('buf', 'buf should be automatically available after npm install');
2779
-
2780
- // Check for OpenAPI bundler using the existing detectBundler function
1649
+ const gitRef = options.tag || options.branch
1650
+ requireCmd('git', 'Install Git: https://git-scm.com/downloads')
1651
+ requireCmd('buf', 'buf should be automatically available after npm install')
1652
+
2781
1653
  try {
2782
- const { detectBundler } = require('../tools/bundle-openapi.js');
2783
- detectBundler(true); // quiet mode to avoid duplicate output
1654
+ const { detectBundler } = require('../tools/bundle-openapi.js')
1655
+ detectBundler(true)
2784
1656
  } catch (err) {
2785
- fail(err.message);
1657
+ fail(err.message)
2786
1658
  }
2787
1659
 
2788
1660
  try {
2789
- const { bundleOpenAPI } = require('../tools/bundle-openapi.js');
1661
+ const { bundleOpenAPI } = require('../tools/bundle-openapi.js')
2790
1662
  await bundleOpenAPI({
2791
1663
  tag: gitRef,
2792
1664
  repo: options.repo,
@@ -2796,256 +1668,63 @@ automation
2796
1668
  adminMajor: options.adminMajor,
2797
1669
  useAdminMajorVersion: options.useAdminMajorVersion,
2798
1670
  quiet: options.quiet
2799
- });
1671
+ })
2800
1672
  } catch (err) {
2801
- console.error(`❌ ${err.message}`);
2802
- process.exit(err.message.includes('Validation failed') ? 2 : 1);
1673
+ console.error(`Error: ${err.message}`)
1674
+ process.exit(err.message.includes('Validation failed') ? 2 : 1)
2803
1675
  }
2804
- });
1676
+ })
2805
1677
 
2806
1678
  /**
2807
- * Validate MCP configuration
2808
- *
2809
- * Validates that all prompts and resources are properly configured and accessible.
2810
- * Checks for:
2811
- * - Valid frontmatter in all prompt files
2812
- * - Accessible resource files
2813
- * - Proper metadata and descriptions
2814
- * - No naming conflicts
2815
- *
1679
+ * @description Update the Redpanda Connect version attribute in antora.yml by fetching
1680
+ * the latest release tag from GitHub or using a specified version.
1681
+ * @why Use this command before generating Connect connector docs to ensure the version
1682
+ * attribute is current. It updates the latest-connect-version attribute automatically.
2816
1683
  * @example
2817
- * npx doc-tools validate-mcp
2818
- */
2819
- programCli
2820
- .command('validate-mcp')
2821
- .description('Validate MCP server configuration (prompts, resources, metadata)')
2822
- .action(() => {
2823
- const {
2824
- PromptCache,
2825
- loadAllPrompts
2826
- } = require('./mcp-tools/prompt-discovery');
2827
- const {
2828
- validateMcpConfiguration,
2829
- formatValidationResults
2830
- } = require('./mcp-tools/mcp-validation');
2831
-
2832
- const baseDir = findRepoRoot();
2833
- const promptCache = new PromptCache();
2834
-
2835
- // Resources configuration
2836
- const resources = [
2837
- {
2838
- uri: 'redpanda://style-guide',
2839
- name: 'Redpanda Documentation Style Guide',
2840
- description: 'Complete style guide based on Google Developer Documentation Style Guide with Redpanda-specific guidelines',
2841
- mimeType: 'text/markdown',
2842
- version: '1.0.0',
2843
- lastUpdated: '2025-12-11'
2844
- }
2845
- ];
2846
-
2847
- const resourceMap = {
2848
- 'redpanda://style-guide': { file: 'style-guide.md', mimeType: 'text/markdown' }
2849
- };
2850
-
2851
- try {
2852
- // Load prompts
2853
- console.log('Loading prompts...');
2854
- const prompts = loadAllPrompts(baseDir, promptCache);
2855
- console.log(`Found ${prompts.length} prompts`);
2856
-
2857
- // Validate configuration
2858
- console.log('\nValidating configuration...');
2859
- const validation = validateMcpConfiguration({
2860
- resources,
2861
- resourceMap,
2862
- prompts,
2863
- baseDir
2864
- });
2865
-
2866
- // Format and display results
2867
- const output = formatValidationResults(validation, { resources, prompts });
2868
- console.log('\n' + output);
2869
-
2870
- if (!validation.valid) {
2871
- process.exit(1);
2872
- }
2873
- } catch (err) {
2874
- console.error(`❌ Validation failed: ${err.message}`);
2875
- process.exit(1);
2876
- }
2877
- });
2878
-
2879
- /**
2880
- * Preview a prompt with arguments
1684
+ * # Update to the latest version from GitHub
1685
+ * npx doc-tools generate update-connect-version
2881
1686
  *
2882
- * Loads a prompt and shows how it will appear when used with specified arguments.
2883
- * Useful for testing prompts before using them in Claude Code.
2884
- *
2885
- * @example
2886
- * npx doc-tools preview-prompt review-for-style --content "Sample content"
2887
- * npx doc-tools preview-prompt write-new-guide --topic "Deploy Redpanda"
1687
+ * # Update to a specific version
1688
+ * npx doc-tools generate update-connect-version --connect-version 4.50.0
1689
+ * @requirements None (uses GitHub API).
2888
1690
  */
2889
- programCli
2890
- .command('preview-prompt')
2891
- .description('Preview a prompt with arguments to see the final output')
2892
- .argument('<prompt-name>', 'Name of the prompt to preview')
2893
- .option('--content <text>', 'Content argument (for review/check prompts)')
2894
- .option('--topic <text>', 'Topic argument (for write prompts)')
2895
- .option('--audience <text>', 'Audience argument (for write prompts)')
2896
- .action((promptName, options) => {
2897
- const {
2898
- PromptCache,
2899
- loadAllPrompts,
2900
- buildPromptWithArguments
2901
- } = require('./mcp-tools/prompt-discovery');
2902
-
2903
- const baseDir = findRepoRoot();
2904
- const promptCache = new PromptCache();
1691
+ automation
1692
+ .command('update-connect-version')
1693
+ .description('Update the Redpanda Connect version in antora.yml')
1694
+ .option('-v, --connect-version <version>', 'Specific Connect version (default: fetch latest from GitHub)')
1695
+ .action(async (options) => {
1696
+ const GetLatestConnectTag = require('../extensions/version-fetcher/get-latest-connect')
2905
1697
 
2906
1698
  try {
2907
- // Load prompts
2908
- loadAllPrompts(baseDir, promptCache);
2909
-
2910
- // Get the requested prompt
2911
- const prompt = promptCache.get(promptName);
2912
- if (!prompt) {
2913
- console.error(`❌ Prompt not found: ${promptName}`);
2914
- console.error(`\nAvailable prompts: ${promptCache.getNames().join(', ')}`);
2915
- process.exit(1);
2916
- }
1699
+ let version
2917
1700
 
2918
- // Build arguments object from options
2919
- const args = {};
2920
- if (options.content) args.content = options.content;
2921
- if (options.topic) args.topic = options.topic;
2922
- if (options.audience) args.audience = options.audience;
2923
-
2924
- // Build final prompt text
2925
- const promptText = buildPromptWithArguments(prompt, args);
2926
-
2927
- // Display preview
2928
- console.log('='.repeat(70));
2929
- console.log(`PROMPT PREVIEW: ${promptName}`);
2930
- console.log('='.repeat(70));
2931
- console.log(`Description: ${prompt.description}`);
2932
- console.log(`Version: ${prompt.version}`);
2933
- if (prompt.arguments.length > 0) {
2934
- console.log(`Arguments: ${prompt.arguments.map(a => a.name).join(', ')}`);
1701
+ if (options.connectVersion) {
1702
+ version = options.connectVersion.replace(/^v/, '')
1703
+ console.log(`Updating to specified Connect version: ${version}`)
1704
+ } else {
1705
+ console.log('Fetching latest Connect version from GitHub...')
1706
+ version = await GetLatestConnectTag()
1707
+ console.log(`Latest Connect version: ${version}`)
2935
1708
  }
2936
- console.log('='.repeat(70));
2937
- console.log('\n' + promptText);
2938
- console.log('\n' + '='.repeat(70));
2939
- } catch (err) {
2940
- console.error(`❌ Preview failed: ${err.message}`);
2941
- process.exit(1);
2942
- }
2943
- });
2944
-
2945
- /**
2946
- * Show MCP server version and statistics
2947
- *
2948
- * Displays version information for the MCP server, including:
2949
- * - Server version
2950
- * - Available prompts and their versions
2951
- * - Available resources and their versions
2952
- * - Usage statistics (if available)
2953
- *
2954
- * @example
2955
- * npx doc-tools mcp-version
2956
- * npx doc-tools mcp-version --stats # Show usage statistics if available
2957
- */
2958
- programCli
2959
- .command('mcp-version')
2960
- .description('Show MCP server version and configuration information')
2961
- .option('--stats', 'Show usage statistics if available', false)
2962
- .action((options) => {
2963
- const packageJson = require('../package.json');
2964
- const {
2965
- PromptCache,
2966
- loadAllPrompts
2967
- } = require('./mcp-tools/prompt-discovery');
2968
-
2969
- const baseDir = findRepoRoot();
2970
- const promptCache = new PromptCache();
2971
-
2972
- try {
2973
- // Load prompts
2974
- const prompts = loadAllPrompts(baseDir, promptCache);
2975
-
2976
- // Resources configuration
2977
- const resources = [
2978
- {
2979
- uri: 'redpanda://style-guide',
2980
- name: 'Redpanda Documentation Style Guide',
2981
- version: '1.0.0',
2982
- lastUpdated: '2025-12-11'
2983
- }
2984
- ];
2985
1709
 
2986
- // Display version information
2987
- console.log('Redpanda Doc Tools MCP Server');
2988
- console.log('='.repeat(60));
2989
- console.log(`Server version: ${packageJson.version}`);
2990
- console.log(`Base directory: ${baseDir}`);
2991
- console.log('');
1710
+ const currentVersion = getAntoraValue('asciidoc.attributes.latest-connect-version')
2992
1711
 
2993
- // Prompts
2994
- console.log(`Prompts (${prompts.length} available):`);
2995
- prompts.forEach(prompt => {
2996
- const args = prompt.arguments.length > 0
2997
- ? ` [${prompt.arguments.map(a => a.name).join(', ')}]`
2998
- : '';
2999
- console.log(` - ${prompt.name} (v${prompt.version})${args}`);
3000
- console.log(` ${prompt.description}`);
3001
- });
3002
- console.log('');
3003
-
3004
- // Resources
3005
- console.log(`Resources (${resources.length} available):`);
3006
- resources.forEach(resource => {
3007
- console.log(` - ${resource.uri}`);
3008
- console.log(` ${resource.name} (v${resource.version})`);
3009
- console.log(` Last updated: ${resource.lastUpdated}`);
3010
- });
3011
- console.log('');
3012
-
3013
- // Check for usage statistics
3014
- if (options.stats) {
3015
- const statsPath = path.join(baseDir, 'mcp-usage-stats.json');
3016
- if (fs.existsSync(statsPath)) {
3017
- console.log('Usage Statistics:');
3018
- console.log('='.repeat(60));
3019
- const stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
3020
- console.log(`Exported at: ${stats.exportedAt}`);
3021
- console.log(`Total prompt calls: ${stats.totalPromptCalls}`);
3022
- console.log(`Total resource calls: ${stats.totalResourceCalls}`);
3023
- console.log(`Total tool calls: ${stats.totalToolCalls}`);
3024
-
3025
- if (stats.totalPromptCalls > 0) {
3026
- console.log('\nMost used prompts:');
3027
- Object.entries(stats.prompts)
3028
- .sort((a, b) => b[1] - a[1])
3029
- .slice(0, 5)
3030
- .forEach(([name, count]) => {
3031
- console.log(` ${name}: ${count} calls`);
3032
- });
3033
- }
3034
- } else {
3035
- console.log('No usage statistics available yet.');
3036
- console.log('Statistics are exported when the MCP server shuts down.');
3037
- }
1712
+ if (currentVersion === version) {
1713
+ console.log(`✓ Already at version ${version}`)
1714
+ return
3038
1715
  }
3039
1716
 
3040
- console.log('');
3041
- console.log('For more information, see:');
3042
- console.log(' mcp/WRITER_EXTENSION_GUIDE.adoc');
3043
- console.log(' mcp/AI_CONSISTENCY_ARCHITECTURE.adoc');
1717
+ setAntoraValue('asciidoc.attributes.latest-connect-version', version)
1718
+ console.log(`Done: Updated latest-connect-version from ${currentVersion} to ${version}`)
1719
+ console.log('')
1720
+ console.log('Next steps:')
1721
+ console.log(' 1. Run: npx doc-tools generate rpcn-connector-docs --fetch-connectors')
1722
+ console.log(' 2. Review and commit the changes')
3044
1723
  } catch (err) {
3045
- console.error(`❌ Failed to retrieve version information: ${err.message}`);
3046
- process.exit(1);
1724
+ console.error(`Error: Failed to update Connect version: ${err.message}`)
1725
+ process.exit(1)
3047
1726
  }
3048
- });
1727
+ })
3049
1728
 
3050
- programCli.addCommand(automation);
3051
- programCli.parse(process.argv);
1729
+ programCli.addCommand(automation)
1730
+ programCli.parse(process.argv)