@redpanda-data/docs-extensions-and-macros 4.4.2 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.adoc +0 -163
  2. package/bin/doc-tools.js +808 -151
  3. package/cli-utils/antora-utils.js +127 -0
  4. package/cli-utils/convert-doc-links.js +58 -0
  5. package/cli-utils/generate-cluster-docs.sh +1 -1
  6. package/cli-utils/self-managed-docs-branch.js +91 -0
  7. package/cli-utils/start-cluster.sh +12 -2
  8. package/docker-compose/25.1/bootstrap.yml +67 -0
  9. package/docker-compose/25.1/docker-compose.yml +414 -0
  10. package/docker-compose/25.1/generate-profiles.yaml +77 -0
  11. package/docker-compose/25.1/rpk-profile.yaml +24 -0
  12. package/docker-compose/25.1/transactions-schema.json +37 -0
  13. package/docker-compose/25.1/transactions.md +46 -0
  14. package/docker-compose/25.1/transform/README.adoc +73 -0
  15. package/docker-compose/25.1/transform/go.mod +5 -0
  16. package/docker-compose/25.1/transform/go.sum +2 -0
  17. package/docker-compose/25.1/transform/regex.wasm +0 -0
  18. package/docker-compose/25.1/transform/transform.go +122 -0
  19. package/docker-compose/25.1/transform/transform.yaml +33 -0
  20. package/docker-compose/bootstrap.yml +0 -12
  21. package/package.json +6 -5
  22. package/tools/fetch-from-github.js +2 -2
  23. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +205 -0
  24. package/tools/redpanda-connect/helpers/advancedConfig.js +17 -0
  25. package/tools/redpanda-connect/helpers/buildConfigYaml.js +53 -0
  26. package/tools/redpanda-connect/helpers/commonConfig.js +31 -0
  27. package/tools/redpanda-connect/helpers/eq.js +10 -0
  28. package/tools/redpanda-connect/helpers/index.js +19 -0
  29. package/tools/redpanda-connect/helpers/isObject.js +1 -0
  30. package/tools/redpanda-connect/helpers/join.js +6 -0
  31. package/tools/redpanda-connect/helpers/ne.js +10 -0
  32. package/tools/redpanda-connect/helpers/or.js +4 -0
  33. package/tools/redpanda-connect/helpers/renderConnectExamples.js +37 -0
  34. package/tools/redpanda-connect/helpers/renderConnectFields.js +146 -0
  35. package/tools/redpanda-connect/helpers/renderLeafField.js +64 -0
  36. package/tools/redpanda-connect/helpers/renderObjectField.js +41 -0
  37. package/tools/redpanda-connect/helpers/renderYamlList.js +24 -0
  38. package/tools/redpanda-connect/helpers/toYaml.js +11 -0
  39. package/tools/redpanda-connect/helpers/uppercase.js +9 -0
  40. package/tools/redpanda-connect/parse-csv-connectors.js +63 -0
  41. package/tools/redpanda-connect/report-delta.js +152 -0
  42. package/tools/redpanda-connect/templates/connector.hbs +20 -0
  43. package/tools/redpanda-connect/templates/examples-partials.hbs +7 -0
  44. package/tools/redpanda-connect/templates/fields-partials.hbs +13 -0
  45. package/tools/redpanda-connect/templates/intro.hbs +35 -0
  46. package/macros/data-template.js +0 -591
package/bin/doc-tools.js CHANGED
@@ -1,16 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { execSync, spawnSync } = require('child_process');
4
+ const os = require('os');
4
5
  const { Command } = require('commander');
5
6
  const path = require('path');
7
+ const yaml = require('yaml');
6
8
  const fs = require('fs');
9
+ const handlebars = require('handlebars');
10
+ const { determineDocsBranch } = require('../cli-utils/self-managed-docs-branch.js');
11
+ const fetchFromGithub = require('../tools/fetch-from-github.js');
12
+ const { urlToXref } = require('../cli-utils/convert-doc-links.js');
13
+ const { generateRpcnConnectorDocs } = require('../tools/redpanda-connect/generate-rpcn-connector-docs.js');
14
+ const parseCSVConnectors = require('../tools/redpanda-connect/parse-csv-connectors.js');
15
+ const { getAntoraValue, setAntoraValue } = require('../cli-utils/antora-utils');
16
+ const {
17
+ getRpkConnectVersion,
18
+ printDeltaReport
19
+ } = require('../tools/redpanda-connect/report-delta');
7
20
 
21
+ /**
22
+ * Searches upward from a starting directory to locate the repository root.
23
+ *
24
+ * 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.
25
+ *
26
+ * @param {string} [start] - The directory to begin the search from. Defaults to the current working directory.
27
+ * @returns {string} The absolute path to the repository root directory.
28
+ */
8
29
  function findRepoRoot(start = process.cwd()) {
9
30
  let dir = start;
10
31
  while (dir !== path.parse(dir).root) {
11
- // marker could be a .git folder or package.json or anything you choose
12
- if (fs.existsSync(path.join(dir, '.git')) ||
13
- fs.existsSync(path.join(dir, 'package.json'))) {
32
+ if (
33
+ fs.existsSync(path.join(dir, '.git')) ||
34
+ fs.existsSync(path.join(dir, 'package.json'))
35
+ ) {
14
36
  return dir;
15
37
  }
16
38
  dir = path.dirname(dir);
@@ -21,107 +43,233 @@ function findRepoRoot(start = process.cwd()) {
21
43
 
22
44
  // --------------------------------------------------------------------
23
45
  // Dependency check functions
24
- // --------------------------------------------------------------------
25
- function checkDependency(command, versionArg, name, helpURL) {
26
- try {
27
- execSync(`${command} ${versionArg}`, { stdio: 'ignore' });
28
- } catch (error) {
29
- console.error(`Error: ${name} is required but not found or not working properly.
30
- Please install ${name} and try again.
31
- For more info, see: ${helpURL}`);
32
- process.exit(1);
33
- }
46
+
47
+ /**
48
+ * Prints an error message to stderr and exits the process with a non-zero status.
49
+ *
50
+ * @param {string} msg - The error message to display before exiting.
51
+ */
52
+ function fail(msg) {
53
+ console.error(`❌ ${msg}`);
54
+ process.exit(1);
34
55
  }
35
56
 
36
- function checkCommandExists(command) {
57
+ /**
58
+ * Ensures that a specified command-line tool is installed and operational.
59
+ *
60
+ * 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.
61
+ *
62
+ * @param {string} cmd - The name of the tool to check (e.g., 'docker', 'helm-docs').
63
+ * @param {object} [opts] - Optional settings.
64
+ * @param {string} [opts.versionFlag='--version'] - The flag used to test the tool's execution.
65
+ * @param {string} [opts.help] - An optional hint or installation instruction shown on failure.
66
+ */
67
+ function requireTool(cmd, { versionFlag = '--version', help = '' } = {}) {
37
68
  try {
38
- execSync(`which ${command}`, { stdio: 'ignore' });
39
- return true;
40
- } catch (error) {
41
- console.error(`Error: \`${command}\` is required but not found. Please install \`${command}\` and try again.`);
42
- return false;
69
+ execSync(`${cmd} ${versionFlag}`, { stdio: 'ignore' });
70
+ } catch {
71
+ const hint = help ? `\n→ ${help}` : '';
72
+ fail(`'${cmd}' is required but not found.${hint}`);
43
73
  }
44
74
  }
45
75
 
46
- function checkMake() {
47
- if (!checkCommandExists('make')) {
48
- console.error('Error: `make` is required but not found. Please install `make` to use the automation Makefile. For help, see: https://www.google.com/search?q=how+to+install+make');
49
- process.exit(1);
50
- }
76
+ /**
77
+ * Ensures that a command-line tool is installed by checking if it responds to a specified flag.
78
+ *
79
+ * @param {string} cmd - The name of the command-line tool to check.
80
+ * @param {string} [help] - Optional help text to display if the tool is not found.
81
+ * @param {string} [versionFlag='--version'] - The flag to use when checking if the tool is installed.
82
+ *
83
+ * @throws {Error} If the specified command is not found or does not respond to the specified flag.
84
+ */
85
+ function requireCmd(cmd, help, versionFlag = '--version') {
86
+ requireTool(cmd, { versionFlag, help });
51
87
  }
52
88
 
53
- function checkPython() {
54
- const candidates = ['python3', 'python'];
55
- let found = false;
89
+ // --------------------------------------------------------------------
90
+ // Special validators
56
91
 
57
- for (const cmd of candidates) {
92
+ /**
93
+ * Ensures that Python with a minimum required version is installed and available in the system PATH.
94
+ *
95
+ * 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.
96
+ *
97
+ * @param {number} [minMajor=3] - Minimum required major version of Python.
98
+ * @param {number} [minMinor=10] - Minimum required minor version of Python.
99
+ */
100
+ function requirePython(minMajor = 3, minMinor = 10) {
101
+ const candidates = ['python3', 'python'];
102
+ for (const p of candidates) {
58
103
  try {
59
- const versionOutput = execSync(`${cmd} --version`, {
60
- encoding: 'utf8',
61
- stdio: ['pipe', 'pipe', 'ignore']
62
- }).trim();
63
- // versionOutput looks like "Python 3.x.y"
64
- const versionString = versionOutput.split(' ')[1];
65
- const [major, minor] = versionString.split('.').map(Number);
66
- if (major > 3 || (major === 3 && minor >= 10)) {
67
- found = true;
68
- break;
69
- } else {
70
- console.error(`Error: Python 3.10 or higher is required. Detected version: ${versionString}`);
71
- process.exit(1);
104
+ const out = execSync(`${p} --version`, { encoding: 'utf8' }).trim();
105
+ const [maj, min] = out.split(' ')[1].split('.').map(Number);
106
+ if (maj > minMajor || (maj === minMajor && min >= minMinor)) {
107
+ return; // success
72
108
  }
73
109
  } catch {
74
- // this candidate didn’t exist or errored—try the next one
110
+ /* ignore & try next */
75
111
  }
76
112
  }
77
- if (!found) {
78
- console.error('Error: Python 3.10 or higher is required but not found.\nPlease install Python and ensure `python3 --version` or `python --version` returns at least 3.10: https://www.geeksforgeeks.org/how-to-install-python-on-mac/');
79
- process.exit(1);
80
- }
113
+ fail(
114
+ `Python ${minMajor}.${minMinor}+ not found or too old.
115
+ → Install from your package manager or https://python.org`
116
+ );
81
117
  }
82
118
 
83
- function checkCompiler() {
84
- const gccInstalled = checkCommandExists('gcc');
85
- const clangInstalled = checkCommandExists('clang');
86
- if (!gccInstalled && !clangInstalled) {
87
- console.error('Error: A C++ compiler (such as gcc or clang) is required but not found. Please install one: https://osxdaily.com/2023/05/02/how-install-gcc-mac/');
88
- process.exit(1);
89
- }
90
- }
91
-
92
- function checkDocker() {
93
- checkDependency('docker', '--version', 'Docker', 'https://docs.docker.com/get-docker/');
119
+ /**
120
+ * Ensures that the Docker CLI is installed and the Docker daemon is running.
121
+ *
122
+ * @throws {Error} If Docker is not installed or the Docker daemon is not running.
123
+ */
124
+ function requireDockerDaemon() {
125
+ requireTool('docker', { help: 'https://docs.docker.com/get-docker/' });
94
126
  try {
95
127
  execSync('docker info', { stdio: 'ignore' });
96
- } catch (error) {
97
- console.error('Error: Docker daemon appears to be not running. Please start Docker.');
98
- process.exit(1);
128
+ } catch {
129
+ fail('Docker daemon does not appear to be running. Please start Docker.');
99
130
  }
100
131
  }
101
132
 
133
+ // --------------------------------------------------------------------
134
+ // Grouped checks
135
+
136
+ /**
137
+ * Ensures that required dependencies for generating CRD documentation are installed.
138
+ *
139
+ * 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.
140
+ */
141
+ function verifyCrdDependencies() {
142
+ requireCmd('git', 'Install Git: https://git-scm.com/downloads');
143
+ requireCmd(
144
+ 'crd-ref-docs',
145
+ `
146
+ The 'crd-ref-docs' command is required but was not found.
147
+
148
+ To install it, follow these steps (for macOS):
149
+
150
+ 1. Determine your architecture:
151
+ Run: \`uname -m\`
152
+
153
+ 2. Download and install:
154
+
155
+ - For Apple Silicon (M1/M2/M3):
156
+ curl -fLO https://github.com/elastic/crd-ref-docs/releases/download/v0.1.0/crd-ref-docs_0.1.0_Darwin_arm64.tar.gz
157
+ tar -xzf crd-ref-docs_0.1.0_Darwin_arm64.tar.gz
158
+ chmod +x crd-ref-docs
159
+ sudo mv crd-ref-docs /usr/local/bin/
160
+
161
+ - For Intel (x86_64):
162
+ 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
163
+ tar -xzf crd-ref-docs_0.1.0_Darwin_x86_64.tar.gz
164
+ chmod +x crd-ref-docs
165
+ sudo mv crd-ref-docs /usr/local/bin/
166
+
167
+ For more details, visit: https://github.com/elastic/crd-ref-docs
168
+ `.trim()
169
+ );
170
+ requireCmd(
171
+ 'go',
172
+ `
173
+ The 'go' command (Golang) is required but was not found.
174
+
175
+ To install it on macOS:
176
+
177
+ Option 1: Install via Homebrew (recommended):
178
+ brew install go
179
+
180
+ Option 2: Download directly from the official site:
181
+ 1. Visit: https://go.dev/dl/
182
+ 2. Download the appropriate installer for macOS.
183
+ 3. Run the installer and follow the instructions.
184
+
185
+ After installation, verify it works:
186
+ go version
187
+
188
+ For more details, see: https://go.dev/doc/install
189
+ `.trim(),
190
+ 'version'
191
+ );
192
+ }
193
+
194
+ /**
195
+ * Ensures that all required tools for Helm documentation generation are installed.
196
+ *
197
+ * Checks for the presence of `helm-docs`, `pandoc`, and `git`, exiting the process with an error if any are missing.
198
+ */
199
+ function verifyHelmDependencies() {
200
+ requireCmd(
201
+ 'helm-docs',
202
+ `
203
+ The 'helm-docs' command is required but was not found.
204
+
205
+ To install it, follow these steps (for macOS):
206
+
207
+ 1. Determine your architecture:
208
+ Run: \`uname -m\`
209
+
210
+ 2. Download and install:
211
+
212
+ - For Apple Silicon (M1/M2/M3):
213
+ curl -fLO https://github.com/norwoodj/helm-docs/releases/download/v1.11.0/helm-docs_1.11.0_Darwin_arm64.tar.gz
214
+ tar -xzf helm-docs_1.11.0_Darwin_arm64.tar.gz
215
+ chmod +x helm-docs
216
+ sudo mv helm-docs /usr/local/bin/
217
+
218
+ - For Intel (x86_64):
219
+ curl -fLO https://github.com/norwoodj/helm-docs/releases/download/v1.11.0/helm-docs_1.11.0_Darwin_x86_64.tar.gz
220
+ tar -xzf helm-docs_1.11.0_Darwin_x86_64.tar.gz
221
+ chmod +x helm-docs
222
+ sudo mv helm-docs /usr/local/bin/
223
+
224
+ Alternatively, if you use Homebrew:
225
+ brew install norwoodj/tap/helm-docs
226
+
227
+ For more details, visit: https://github.com/norwoodj/helm-docs
228
+ `.trim()
229
+ );
230
+ requireCmd('pandoc', 'brew install pandoc or https://pandoc.org');
231
+ requireCmd('git', 'Install Git: https://git-scm.com/downloads');
232
+ }
233
+
234
+ /**
235
+ * Ensures all dependencies required for generating property documentation are installed.
236
+ *
237
+ * Checks for the presence of `make`, Python 3.10 or newer, and at least one C++ compiler (`gcc` or `clang`). Exits the process with an error message if any dependency is missing.
238
+ */
102
239
  function verifyPropertyDependencies() {
103
- checkMake();
104
- checkPython();
105
- checkCompiler();
240
+ requireCmd('make', 'Your OS package manager');
241
+ requirePython();
242
+ try {
243
+ execSync('gcc --version', { stdio: 'ignore' });
244
+ } catch {
245
+ try {
246
+ execSync('clang --version', { stdio: 'ignore' });
247
+ } catch {
248
+ fail('A C++ compiler (gcc or clang) is required.');
249
+ }
250
+ }
106
251
  }
107
252
 
253
+ /**
254
+ * Ensures all required dependencies for generating Redpanda metrics documentation are installed.
255
+ *
256
+ * Verifies that Python 3.10+, `curl`, and `tar` are available, and that the Docker daemon is running.
257
+ *
258
+ * @throws {Error} If any required dependency is missing or the Docker daemon is not running.
259
+ */
108
260
  function verifyMetricsDependencies() {
109
- checkPython();
110
- if (!checkCommandExists('curl') || !checkCommandExists('tar')) {
111
- // `checkCommandExists` already prints a helpful message.
112
- process.exit(1);
113
- }
114
- checkDocker();
261
+ requirePython();
262
+ requireCmd('curl');
263
+ requireCmd('tar');
264
+ requireDockerDaemon();
115
265
  }
266
+
116
267
  // --------------------------------------------------------------------
117
268
  // Main CLI Definition
118
269
  // --------------------------------------------------------------------
119
270
  const programCli = new Command();
120
271
 
121
- programCli
122
- .name('doc-tools')
123
- .description('Redpanda Document Automation CLI')
124
- .version('1.0.1');
272
+ programCli.name('doc-tools').description('Redpanda Document Automation CLI').version('1.1.0');
125
273
 
126
274
  // Top-level commands.
127
275
  programCli
@@ -142,7 +290,7 @@ programCli
142
290
  try {
143
291
  await require('../tools/get-redpanda-version.js')(options);
144
292
  } catch (err) {
145
- console.error(err);
293
+ console.error(`❌ ${err.message}`);
146
294
  process.exit(1);
147
295
  }
148
296
  });
@@ -156,41 +304,103 @@ programCli
156
304
  try {
157
305
  await require('../tools/get-console-version.js')(options);
158
306
  } catch (err) {
159
- console.error(err);
307
+ console.error(`❌ ${err.message}`);
308
+ process.exit(1);
309
+ }
310
+ });
311
+
312
+ programCli
313
+ .command('link-readme')
314
+ .description('Symlink a README.adoc into docs/modules/<module>/pages/')
315
+ .requiredOption('-s, --subdir <subdir>', 'Relative path to the lab project subdirectory')
316
+ .requiredOption('-t, --target <filename>', 'Name of the target AsciiDoc file in pages/')
317
+ .action((options) => {
318
+ const repoRoot = findRepoRoot();
319
+ const normalized = options.subdir.replace(/\/+$/, '');
320
+ const moduleName = normalized.split('/')[0];
321
+
322
+ const projectDir = path.join(repoRoot, normalized);
323
+ const pagesDir = path.join(repoRoot, 'docs', 'modules', moduleName, 'pages');
324
+ const sourceFile = path.join(projectDir, 'README.adoc');
325
+ const destLink = path.join(pagesDir, options.target);
326
+
327
+ if (!fs.existsSync(projectDir)) {
328
+ console.error(`❌ Project directory not found: ${projectDir}`);
329
+ process.exit(1);
330
+ }
331
+ if (!fs.existsSync(sourceFile)) {
332
+ console.error(`❌ README.adoc not found in ${projectDir}`);
333
+ process.exit(1);
334
+ }
335
+
336
+ fs.mkdirSync(pagesDir, { recursive: true });
337
+ const relPath = path.relative(pagesDir, sourceFile);
338
+
339
+ try {
340
+ if (fs.existsSync(destLink)) {
341
+ const stat = fs.lstatSync(destLink);
342
+ if (stat.isSymbolicLink()) fs.unlinkSync(destLink);
343
+ else fail(`Destination already exists and is not a symlink: ${destLink}`);
344
+ }
345
+ fs.symlinkSync(relPath, destLink);
346
+ console.log(`✅ Linked ${relPath} → ${destLink}`);
347
+ } catch (err) {
348
+ fail(`Failed to create symlink: ${err.message}`);
349
+ }
350
+ });
351
+
352
+ programCli
353
+ .command('fetch')
354
+ .description('Fetch a file or directory from GitHub and save it locally')
355
+ .requiredOption('-o, --owner <owner>', 'GitHub repo owner or org')
356
+ .requiredOption('-r, --repo <repo>', 'GitHub repo name')
357
+ .requiredOption('-p, --remote-path <path>', 'Path in the repo to fetch')
358
+ .requiredOption('-d, --save-dir <dir>', 'Local directory to save into')
359
+ .option('-f, --filename <name>', 'Custom filename to save as')
360
+ .action(async (options) => {
361
+ try {
362
+ await fetchFromGithub(
363
+ options.owner,
364
+ options.repo,
365
+ options.remotePath,
366
+ options.saveDir,
367
+ options.filename
368
+ );
369
+ console.log(`✅ Fetched to ${options.saveDir}`);
370
+ } catch (err) {
371
+ console.error(`❌ ${err.message}`);
160
372
  process.exit(1);
161
373
  }
162
374
  });
163
375
 
164
376
  // Create an "automation" subcommand group.
165
- const automation = new Command('generate')
166
- .description('Run docs automations (properties, metrics, and rpk docs generation)');
377
+ const automation = new Command('generate').description('Run docs automations');
167
378
 
168
379
  // --------------------------------------------------------------------
169
- // Automation Subcommands: Delegate to a unified Bash script internally.
380
+ // Automation subcommands
170
381
  // --------------------------------------------------------------------
171
382
 
172
383
  // Common options for both automation tasks.
173
384
  const commonOptions = {
174
- tag: 'latest',
175
385
  dockerRepo: 'redpanda',
176
386
  consoleTag: 'latest',
177
- consoleDockerRepo: 'console'
387
+ consoleDockerRepo: 'console',
178
388
  };
179
389
 
180
390
  function runClusterDocs(mode, tag, options) {
181
391
  const script = path.join(__dirname, '../cli-utils/generate-cluster-docs.sh');
182
- const args = [ mode, tag, options.dockerRepo, options.consoleTag, options.consoleDockerRepo ];
183
- console.log(`Running ${script} with arguments: ${args.join(' ')}`);
184
- const r = spawnSync('bash', [ script, ...args ], { stdio: 'inherit', shell: true });
392
+ const args = [mode, tag, options.dockerRepo, options.consoleTag, options.consoleDockerRepo];
393
+ console.log(`⏳ Running ${script} with arguments: ${args.join(' ')}`);
394
+ const r = spawnSync('bash', [script, ...args], { stdio: 'inherit', shell: true });
185
395
  if (r.status !== 0) process.exit(r.status);
186
396
  }
187
397
 
188
398
  // helper to diff two autogenerated directories
189
399
  function diffDirs(kind, oldTag, newTag) {
190
- const oldDir = path.join('autogenerated', oldTag, kind);
191
- const newDir = path.join('autogenerated', newTag, kind);
400
+ const oldDir = path.join('autogenerated', oldTag, kind);
401
+ const newDir = path.join('autogenerated', newTag, kind);
192
402
  const diffDir = path.join('autogenerated', 'diffs', kind, `${oldTag}_to_${newTag}`);
193
- const patch = path.join(diffDir, 'changes.patch');
403
+ const patch = path.join(diffDir, 'changes.patch');
194
404
 
195
405
  if (!fs.existsSync(oldDir)) {
196
406
  console.error(`❌ Cannot diff: missing ${oldDir}`);
@@ -215,11 +425,23 @@ function diffDirs(kind, oldTag, newTag) {
215
425
 
216
426
  automation
217
427
  .command('metrics-docs')
218
- .description('Extract Redpanda metrics and generate JSON/AsciiDoc docs')
219
- .option('--tag <tag>', 'Redpanda tag (default: latest)', commonOptions.tag)
220
- .option('--docker-repo <repo>', '...', commonOptions.dockerRepo)
221
- .option('--console-tag <tag>', '...', commonOptions.consoleTag)
222
- .option('--console-docker-repo <repo>', '...', commonOptions.consoleDockerRepo)
428
+ .description('Generate JSON and AsciiDoc documentation for Redpanda metrics')
429
+ .requiredOption('-t, --tag <tag>', 'Redpanda version to use when starting Redpanda in Docker')
430
+ .option(
431
+ '--docker-repo <repo>',
432
+ 'Docker repository to use when starting Redpanda in Docker',
433
+ commonOptions.dockerRepo
434
+ )
435
+ .option(
436
+ '--console-tag <tag>',
437
+ 'Redpanda Console version to use when starting Redpanda Console in Docker',
438
+ commonOptions.consoleTag
439
+ )
440
+ .option(
441
+ '--console-docker-repo <repo>',
442
+ 'Docker repository to use when starting Redpanda Console in Docker',
443
+ commonOptions.consoleDockerRepo
444
+ )
223
445
  .option('--diff <oldTag>', 'Also diff autogenerated metrics from <oldTag> → <tag>')
224
446
  .action((options) => {
225
447
  verifyMetricsDependencies();
@@ -245,21 +467,216 @@ automation
245
467
  process.exit(0);
246
468
  });
247
469
 
470
+ automation
471
+ .command('rpcn-connector-docs')
472
+ .description('Generate RPCN connector docs and diff changes since the last version')
473
+ .option('-d, --data-dir <path>', 'Directory where versioned connect JSON files live', path.resolve(process.cwd(), 'docs-data'))
474
+ .option('--old-data <path>', 'Optional override for old data file (for diff)')
475
+ .option('-f, --fetch-connectors', 'Fetch latest connector data using rpk')
476
+ .option('-m, --draft-missing', 'Generate full-doc drafts for connectors missing in output')
477
+ .option('--csv <path>', 'Path to connector metadata CSV file', 'internal/plugins/info.csv')
478
+ .option('--template-main <path>', 'Main Handlebars template', path.resolve(__dirname, '../tools/redpanda-connect/templates/connector.hbs'))
479
+ .option('--template-intro <path>', 'Intro section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/intro.hbs'))
480
+ .option('--template-fields <path>', 'Fields section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/fields-partials.hbs'))
481
+ .option('--template-examples <path>', 'Examples section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/examples-partials.hbs'))
482
+ .option('--overrides <path>', 'Optional JSON file with overrides')
483
+ .action(async (options) => {
484
+ let success = true;
485
+ const dataDir = path.resolve(process.cwd(), options.dataDir);
486
+ fs.mkdirSync(dataDir, { recursive: true });
487
+
488
+ const timestamp = new Date().toISOString();
489
+
490
+ let newVersion;
491
+ let dataFile;
492
+ if (options.fetchConnectors) {
493
+ try {
494
+ execSync('rpk --version', { stdio: 'ignore' });
495
+ newVersion = getRpkConnectVersion();
496
+ const tmpFile = path.join(dataDir, `connect-${newVersion}.tmp.json`);
497
+ const finalFile = path.join(dataDir, `connect-${newVersion}.json`);
498
+
499
+ const fd = fs.openSync(tmpFile, 'w');
500
+ const r = spawnSync('rpk', ['connect', 'list', '--format', 'json-full'], { stdio: ['ignore', fd, 'inherit'] });
501
+ fs.closeSync(fd);
502
+
503
+ const rawJson = fs.readFileSync(tmpFile, 'utf8');
504
+ const parsed = JSON.parse(rawJson);
505
+ fs.writeFileSync(finalFile, JSON.stringify(parsed, null, 2));
506
+ fs.unlinkSync(tmpFile);
507
+ dataFile = finalFile;
508
+ console.log(`✅ Fetched and saved: ${finalFile}`);
509
+ } catch (err) {
510
+ console.error(`❌ Failed to fetch connectors: ${err.message}`);
511
+ success = false;
512
+ }
513
+ } else {
514
+ const candidates = fs.readdirSync(dataDir).filter(f => /^connect-\d+\.\d+\.\d+\.json$/.test(f));
515
+ if (candidates.length === 0) {
516
+ console.error('❌ No connect-<version>.json found. Use --fetch-connectors.');
517
+ process.exit(1);
518
+ }
519
+ candidates.sort();
520
+ dataFile = path.join(dataDir, candidates[candidates.length - 1]);
521
+ newVersion = candidates[candidates.length - 1].match(/connect-(\d+\.\d+\.\d+)\.json/)[1];
522
+ }
523
+
524
+ console.log('⏳ Generating connector partials...');
525
+ let partialsWritten, partialFiles, draftsWritten, draftFiles;
526
+
527
+ try {
528
+ const result = await generateRpcnConnectorDocs({
529
+ data: dataFile,
530
+ overrides: options.overrides,
531
+ template: options.templateMain,
532
+ templateIntro: options.templateIntro,
533
+ templateFields: options.templateFields,
534
+ templateExamples: options.templateExamples,
535
+ writeFullDrafts: false
536
+ });
537
+ partialsWritten = result.partialsWritten;
538
+ partialFiles = result.partialFiles;
539
+ } catch (err) {
540
+ console.error(`❌ Failed to generate partials: ${err.message}`);
541
+ success = false;
542
+ }
543
+
544
+ if (options.draftMissing) {
545
+ console.log('⏳ Drafting missing connectors...');
546
+ try {
547
+ const connectorList = await parseCSVConnectors(options.csv, console);
548
+ const validConnectors = connectorList.filter(row => row.name && row.type);
549
+
550
+ const pagesRoot = path.resolve(process.cwd(), 'modules/components/pages');
551
+ const allMissing = validConnectors.filter(({ name, type }) => {
552
+ if (!name || !type) {
553
+ console.warn(`⚠️ Skipping invalid connector entry:`, { name, type });
554
+ return false;
555
+ }
556
+ const expected = path.join(pagesRoot, `${type}s`, `${name}.adoc`);
557
+ return !fs.existsSync(expected);
558
+ });
559
+
560
+ const missingConnectors = allMissing.filter(({ name }) => !name.includes('sql_driver'));
561
+
562
+ if (missingConnectors.length === 0) {
563
+ console.log('✅ All connectors (excluding sql_drivers) already have docs—nothing to draft.');
564
+ } else {
565
+ console.log(`⏳ Docs missing for ${missingConnectors.length} connectors:`);
566
+ missingConnectors.forEach(({ name, type }) => {
567
+ console.log(` • ${type}/${name}`);
568
+ });
569
+ console.log('');
570
+
571
+ const rawData = fs.readFileSync(dataFile, 'utf8');
572
+ const dataObj = JSON.parse(rawData);
573
+
574
+ const filteredDataObj = {};
575
+ for (const [key, arr] of Object.entries(dataObj)) {
576
+ if (!Array.isArray(arr)) {
577
+ filteredDataObj[key] = arr;
578
+ continue;
579
+ }
580
+ filteredDataObj[key] = arr.filter(component =>
581
+ missingConnectors.some(m => m.name === component.name && `${m.type}s` === key)
582
+ );
583
+ }
584
+
585
+ const tempDataPath = path.join(dataDir, '._filtered_connect_data.json');
586
+ fs.writeFileSync(tempDataPath, JSON.stringify(filteredDataObj, null, 2), 'utf8');
587
+
588
+ const draftResult = await generateRpcnConnectorDocs({
589
+ data: tempDataPath,
590
+ overrides: options.overrides,
591
+ template: options.templateMain,
592
+ templateFields: options.templateFields,
593
+ templateExamples: options.templateExamples,
594
+ templateIntro: options.templateIntro,
595
+ writeFullDrafts: true
596
+ });
597
+
598
+ fs.unlinkSync(tempDataPath);
599
+ draftsWritten = draftResult.draftsWritten;
600
+ draftFiles = draftResult.draftFiles;
601
+ }
602
+ } catch (err) {
603
+ console.error(`❌ Could not draft missing: ${err.message}`);
604
+ success = false;
605
+ }
606
+ }
607
+
608
+ let oldIndex = {};
609
+ if (options.oldData && fs.existsSync(options.oldData)) {
610
+ oldIndex = JSON.parse(fs.readFileSync(options.oldData, 'utf8'));
611
+ } else {
612
+ const oldVersion = getAntoraValue('asciidoc.attributes.latest-connect-version');
613
+ if (oldVersion) {
614
+ const oldPath = path.join(dataDir, `connect-${oldVersion}.json`);
615
+ if (fs.existsSync(oldPath)) {
616
+ oldIndex = JSON.parse(fs.readFileSync(oldPath, 'utf8'));
617
+ }
618
+ }
619
+ }
620
+
621
+ const newIndex = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
622
+ printDeltaReport(oldIndex, newIndex);
623
+
624
+ function logCollapsed(label, filesArray, maxToShow = 10) {
625
+ console.log(` • ${label}: ${filesArray.length} total`);
626
+ const sample = filesArray.slice(0, maxToShow);
627
+ sample.forEach(fp => console.log(` – ${fp}`));
628
+ const remaining = filesArray.length - sample.length;
629
+ if (remaining > 0) {
630
+ console.log(` … plus ${remaining} more`);
631
+ }
632
+ console.log('');
633
+ }
634
+
635
+ const wrote = setAntoraValue('asciidoc.attributes.latest-connect-version', newVersion);
636
+ if (wrote) {
637
+ console.log(`✅ Updated Antora version: ${newVersion}`);
638
+ }
639
+
640
+ console.log('📊 Generation Report:');
641
+ console.log(` • Partial files: ${partialsWritten}`);
642
+ // Split “partials” into fields vs examples by checking the path substring.
643
+ const fieldsPartials = partialFiles.filter(fp => fp.includes('/fields/'));
644
+ const examplesPartials = partialFiles.filter(fp => fp.includes('/examples/'));
645
+
646
+ // Show only up to 10 of each
647
+ logCollapsed('Fields partials', fieldsPartials, 10);
648
+ logCollapsed('Examples partials', examplesPartials, 10);
649
+
650
+ if (options.draftMissing) {
651
+ console.log(` • Full drafts: ${draftsWritten}`);
652
+ logCollapsed('Draft files', draftFiles, 5);
653
+ }
654
+
655
+ console.log('\n📄 Summary:');
656
+ console.log(` • Run time: ${timestamp}`);
657
+ console.log(` • Version used: ${newVersion}`);
658
+
659
+ process.exit(success ? 0 : 1);
660
+ });
661
+
248
662
  automation
249
663
  .command('property-docs')
250
- .description('Extract properties from Redpanda source')
251
- .option('--tag <tag>', 'Git tag or branch to extract from (default: dev)', 'dev')
664
+ .description('Generate JSON and AsciiDoc documentation for Redpanda configuration properties')
665
+ .option('--tag <tag>', 'Git tag or branch to extract from', 'dev')
252
666
  .option('--diff <oldTag>', 'Also diff autogenerated properties from <oldTag> → <tag>')
253
667
  .action((options) => {
254
668
  verifyPropertyDependencies();
255
669
 
256
670
  const newTag = options.tag;
257
671
  const oldTag = options.diff;
258
- const cwd = path.resolve(__dirname, '../tools/property-extractor');
259
- const make = (tag) => {
672
+ const cwd = path.resolve(__dirname, '../tools/property-extractor');
673
+ const make = (tag) => {
260
674
  console.log(`⏳ Building property docs for ${tag}…`);
261
675
  const r = spawnSync('make', ['build', `TAG=${tag}`], { cwd, stdio: 'inherit' });
262
- if (r.error ) { console.error(r.error); process.exit(1); }
676
+ if (r.error) {
677
+ console.error(`❌ ${r.error.message}`);
678
+ process.exit(1);
679
+ }
263
680
  if (r.status !== 0) process.exit(r.status);
264
681
  };
265
682
 
@@ -279,11 +696,23 @@ automation
279
696
 
280
697
  automation
281
698
  .command('rpk-docs')
282
- .description('Generate documentation for rpk commands')
283
- .option('--tag <tag>', 'Redpanda tag (default: latest)', commonOptions.tag)
284
- .option('--docker-repo <repo>', '...', commonOptions.dockerRepo)
285
- .option('--console-tag <tag>', '...', commonOptions.consoleTag)
286
- .option('--console-docker-repo <repo>', '...', commonOptions.consoleDockerRepo)
699
+ .description('Generate AsciiDoc documentation for rpk CLI commands')
700
+ .requiredOption('-t, --tag <tag>', 'Redpanda version to use when starting Redpanda in Docker')
701
+ .option(
702
+ '--docker-repo <repo>',
703
+ 'Docker repository to use when starting Redpanda in Docker',
704
+ commonOptions.dockerRepo
705
+ )
706
+ .option(
707
+ '--console-tag <tag>',
708
+ 'Redpanda Console version to use when starting Redpanda Console in Docker',
709
+ commonOptions.consoleTag
710
+ )
711
+ .option(
712
+ '--console-docker-repo <repo>',
713
+ 'Docker repository to use when starting Redpanda Console in Docker',
714
+ commonOptions.consoleDockerRepo
715
+ )
287
716
  .option('--diff <oldTag>', 'Also diff autogenerated rpk docs from <oldTag> → <tag>')
288
717
  .action((options) => {
289
718
  verifyMetricsDependencies();
@@ -309,70 +738,298 @@ automation
309
738
  process.exit(0);
310
739
  });
311
740
 
312
- programCli
313
- .command('link-readme')
314
- .description('Symlink a README.adoc into docs/modules/<module>/pages/')
315
- .requiredOption('-s, --subdir <subdir>', 'Relative path to the lab project subdirectory')
316
- .requiredOption('-t, --target <filename>', 'Name of the target AsciiDoc file in pages/')
317
- .action((options) => {
318
- const repoRoot = findRepoRoot();
319
- const normalized = options.subdir.replace(/\/+$/, '');
320
- const moduleName = normalized.split('/')[0];
741
+ automation
742
+ .command('helm-spec')
743
+ .description(
744
+ `Generate AsciiDoc documentation for one or more Helm charts (supports local dirs or GitHub URLs)`
745
+ )
746
+ .option(
747
+ '--chart-dir <dir|url>',
748
+ 'Chart directory (contains Chart.yaml) or a root containing multiple charts, or a GitHub URL',
749
+ 'https://github.com/redpanda-data/redpanda-operator/charts'
750
+ )
751
+ .requiredOption('-t, --tag <tag>', 'Branch or tag to clone when using a GitHub URL for the chart-dir')
752
+ .option('--readme <file>', 'Relative README.md path inside each chart dir', 'README.md')
753
+ .option('--output-dir <dir>', 'Where to write all generated AsciiDoc files', 'modules/reference/pages')
754
+ .option('--output-suffix <suffix>', 'Suffix to append to each chart name (including extension)', '-helm-spec.adoc')
755
+ .action((opts) => {
756
+ verifyHelmDependencies();
321
757
 
322
- const projectDir = path.join(repoRoot, normalized);
323
- const pagesDir = path.join(repoRoot, 'docs', 'modules', moduleName, 'pages');
324
- const sourceFile = path.join(projectDir, 'README.adoc');
325
- const destLink = path.join(pagesDir, options.target);
758
+ // Prepare chart-root (local or GitHub)
759
+ let root = opts.chartDir;
760
+ let tmpClone = null;
326
761
 
327
- if (!fs.existsSync(projectDir)) {
328
- console.error(`❌ Project directory not found: ${projectDir}`);
762
+ if (/^https?:\/\/github\.com\//.test(root)) {
763
+ if (!opts.tag) {
764
+ console.error('❌ When using a GitHub URL you must pass --tag');
765
+ process.exit(1);
766
+ }
767
+ const u = new URL(root);
768
+ const parts = u.pathname.replace(/\.git$/, '').split('/').filter(Boolean);
769
+ if (parts.length < 2) {
770
+ console.error(`❌ Invalid GitHub URL: ${root}`);
771
+ process.exit(1);
772
+ }
773
+ const [owner, repo, ...sub] = parts;
774
+ const repoUrl = `https://${u.host}/${owner}/${repo}.git`;
775
+ const ref = opts.tag;
776
+
777
+ console.log(`⏳ Verifying ${repoUrl}@${ref}…`);
778
+ const ok =
779
+ spawnSync(
780
+ 'git',
781
+ ['ls-remote', '--exit-code', repoUrl, `refs/heads/${ref}`, `refs/tags/${ref}`],
782
+ { stdio: 'ignore' }
783
+ ).status === 0;
784
+ if (!ok) {
785
+ console.error(`❌ ${ref} not found on ${repoUrl}`);
786
+ process.exit(1);
787
+ }
788
+
789
+ tmpClone = fs.mkdtempSync(path.join(os.tmpdir(), 'helm-'));
790
+ console.log(`⏳ Cloning ${repoUrl}@${ref} → ${tmpClone}`);
791
+ if (
792
+ spawnSync('git', ['clone', '--depth', '1', '--branch', ref, repoUrl, tmpClone], {
793
+ stdio: 'inherit',
794
+ }).status !== 0
795
+ ) {
796
+ console.error('❌ git clone failed');
797
+ process.exit(1);
798
+ }
799
+ root = sub.length ? path.join(tmpClone, sub.join('/')) : tmpClone;
800
+ }
801
+
802
+ // Discover charts
803
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
804
+ console.error(`❌ Chart root not found: ${root}`);
329
805
  process.exit(1);
330
806
  }
331
- if (!fs.existsSync(sourceFile)) {
332
- console.error(`❌ README.adoc not found in ${projectDir}`);
807
+ let charts = [];
808
+ if (fs.existsSync(path.join(root, 'Chart.yaml'))) {
809
+ charts = [root];
810
+ } else {
811
+ charts = fs
812
+ .readdirSync(root)
813
+ .map((n) => path.join(root, n))
814
+ .filter((p) => fs.existsSync(path.join(p, 'Chart.yaml')));
815
+ }
816
+ if (charts.length === 0) {
817
+ console.error(`❌ No charts found under: ${root}`);
333
818
  process.exit(1);
334
819
  }
335
820
 
336
- fs.mkdirSync(pagesDir, { recursive: true });
337
- const relPath = path.relative(pagesDir, sourceFile);
821
+ // Ensure output-dir exists
822
+ const outDir = path.resolve(opts.outputDir);
823
+ fs.mkdirSync(outDir, { recursive: true });
338
824
 
339
- try {
340
- fs.symlinkSync(relPath, destLink);
341
- console.log(`✔️ Linked ${relPath} ${destLink}`);
342
- } catch (err) {
343
- console.error(`❌ Failed to create symlink: ${err.message}`);
344
- process.exit(1);
825
+ // Process each chart
826
+ for (const chartPath of charts) {
827
+ const name = path.basename(chartPath);
828
+ console.log(`⏳ Processing chart "${name}"…`);
829
+
830
+ // Regenerate README.md
831
+ console.log(`⏳ helm-docs in ${chartPath}`);
832
+ let r = spawnSync('helm-docs', { cwd: chartPath, stdio: 'inherit' });
833
+ if (r.status !== 0) process.exit(r.status);
834
+
835
+ // Convert Markdown → AsciiDoc
836
+ const md = path.join(chartPath, opts.readme);
837
+ if (!fs.existsSync(md)) {
838
+ console.error(`❌ README not found: ${md}`);
839
+ process.exit(1);
840
+ }
841
+ const outFile = path.join(outDir, `k-${name}${opts.outputSuffix}`);
842
+ console.log(`⏳ pandoc ${md} → ${outFile}`);
843
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
844
+ r = spawnSync('pandoc', [md, '-t', 'asciidoc', '-o', outFile], { stdio: 'inherit' });
845
+ if (r.status !== 0) process.exit(r.status);
846
+
847
+ // Post-process tweaks
848
+ let doc = fs.readFileSync(outFile, 'utf8');
849
+ const xrefRe = /https:\/\/docs\.redpanda\.com[^\s\]\[\)"]+(?:\[[^\]]*\])?/g;
850
+ doc = doc
851
+ .replace(/(\[\d+\])\]\./g, '$1\\].')
852
+ .replace(/^== # (.*)$/gm, '= $1')
853
+ .replace(/^== description: (.*)$/gm, ':description: $1')
854
+ .replace(xrefRe, (match) => {
855
+ let urlPart = match;
856
+ let bracketPart = '';
857
+ const m = match.match(/^([^\[]+)(\[[^\]]*\])$/);
858
+ if (m) {
859
+ urlPart = m[1];
860
+ bracketPart = m[2];
861
+ }
862
+ if (urlPart.endsWith('#')) {
863
+ return match;
864
+ }
865
+ try {
866
+ const xref = urlToXref(urlPart);
867
+ return bracketPart ? `${xref}${bracketPart}` : `${xref}[]`;
868
+ } catch (err) {
869
+ console.warn(`⚠️ urlToXref failed on ${urlPart}: ${err.message}`);
870
+ return match;
871
+ }
872
+ });
873
+ fs.writeFileSync(outFile, doc, 'utf8');
874
+
875
+ console.log(`✅ Wrote ${outFile}`);
345
876
  }
877
+
878
+ // Cleanup
879
+ if (tmpClone) fs.rmSync(tmpClone, { recursive: true, force: true });
346
880
  });
347
881
 
348
- programCli
349
- .command('fetch')
350
- .description('Fetch a file or directory from GitHub and save locally')
351
- .requiredOption('-o, --owner <owner>', 'GitHub repo owner or org')
352
- .requiredOption('-r, --repo <repo>', 'GitHub repo name')
353
- .requiredOption('-p, --remote-path <path>', 'Path in the repo to fetch')
354
- .requiredOption('-d, --save-dir <dir>', 'Local directory to save into')
355
- .option('-f, --filename <name>', 'Custom filename to save as')
356
- .action(async (options) => {
357
- try {
358
- const fetchFromGithub = await require('../tools/fetch-from-github.js');
359
- // options.owner, options.repo, options.remotePath, options.saveDir, options.filename
882
+ automation
883
+ .command('crd-spec')
884
+ .description('Generate Asciidoc documentation for Kubernetes CRD references')
885
+ .requiredOption('-t, --tag <operatorTag>', 'Operator release tag or branch, such as operator/v25.1.2')
886
+ .option(
887
+ '-s, --source-path <src>',
888
+ 'CRD Go types dir or GitHub URL',
889
+ 'https://github.com/redpanda-data/redpanda-operator/operator/api/redpanda/v1alpha2'
890
+ )
891
+ .option('-d, --depth <n>', 'How many levels deep', '10')
892
+ .option('--templates-dir <dir>', 'Asciidoctor templates dir', '.github/crd-config/templates/asciidoctor/operator')
893
+ .option('--output <file>', 'Where to write the generated AsciiDoc file', 'modules/reference/pages/k-crd.adoc')
894
+ .action(async (opts) => {
895
+ verifyCrdDependencies();
896
+
897
+ // Fetch upstream config
898
+ const configTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'crd-config-'));
899
+ console.log(`⏳ Fetching crd-ref-docs-config.yaml from redpanda-operator@main…`);
360
900
  await fetchFromGithub(
361
- options.owner,
362
- options.repo,
363
- options.remotePath,
364
- options.saveDir,
365
- options.filename
901
+ 'redpanda-data',
902
+ 'redpanda-operator',
903
+ 'operator/crd-ref-docs-config.yaml',
904
+ configTmp,
905
+ 'crd-ref-docs-config.yaml'
366
906
  );
367
- } catch (err) {
368
- console.error('❌', err.message);
369
- process.exit(1);
370
- }
371
- });
907
+ const configPath = path.join(configTmp, 'crd-ref-docs-config.yaml');
372
908
 
909
+ // Detect docs repo context
910
+ const repoRoot = findRepoRoot();
911
+ const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
912
+ const inDocs =
913
+ pkg.name === 'redpanda-docs-playbook' ||
914
+ (pkg.repository && pkg.repository.url.includes('redpanda-data/docs'));
915
+ let docsBranch = null;
373
916
 
374
- // Attach the automation group to the main program.
375
- programCli.addCommand(automation);
917
+ if (!inDocs) {
918
+ console.warn('⚠️ Not inside redpanda-data/docs; skipping branch suggestion.');
919
+ } else {
920
+ try {
921
+ docsBranch = await determineDocsBranch(opts.tag);
922
+ console.log(`✅ Detected docs repo; you should commit to branch '${docsBranch}'.`);
923
+ } catch (err) {
924
+ console.error(`❌ Unable to determine docs branch: ${err.message}`);
925
+ process.exit(1);
926
+ }
927
+ }
376
928
 
377
- programCli.parse(process.argv);
929
+ // Validate templates
930
+ if (!fs.existsSync(opts.templatesDir)) {
931
+ console.error(`❌ Templates directory not found: ${opts.templatesDir}`);
932
+ process.exit(1);
933
+ }
934
+
935
+ // Prepare source (local folder or GitHub URL)
936
+ let localSrc = opts.sourcePath;
937
+ let tmpSrc;
938
+ if (/^https?:\/\/github\.com\//.test(opts.sourcePath)) {
939
+ const u = new URL(opts.sourcePath);
940
+ const parts = u.pathname.split('/').filter(Boolean);
941
+ if (parts.length < 2) {
942
+ console.error(`❌ Invalid GitHub URL: ${opts.sourcePath}`);
943
+ process.exit(1);
944
+ }
945
+ const [owner, repo, ...subpathParts] = parts;
946
+ const repoUrl = `https://${u.host}/${owner}/${repo}`;
947
+ const subpath = subpathParts.join('/');
948
+ console.log(`⏳ Verifying "${opts.tag}" in ${repoUrl}…`);
949
+ const ok =
950
+ spawnSync('git', ['ls-remote', '--exit-code', repoUrl, `refs/tags/${opts.tag}`, `refs/heads/${opts.tag}`], {
951
+ stdio: 'ignore',
952
+ }).status === 0;
953
+ if (!ok) {
954
+ console.error(`❌ Tag or branch "${opts.tag}" not found on ${repoUrl}`);
955
+ process.exit(1);
956
+ }
957
+ tmpSrc = fs.mkdtempSync(path.join(os.tmpdir(), 'crd-src-'));
958
+ console.log(`⏳ Cloning ${repoUrl}@${opts.tag} → ${tmpSrc}`);
959
+ if (
960
+ spawnSync('git', ['clone', '--depth', '1', '--branch', opts.tag, repoUrl, tmpSrc], {
961
+ stdio: 'inherit',
962
+ }).status !== 0
963
+ ) {
964
+ console.error(`❌ git clone failed`);
965
+ process.exit(1);
966
+ }
967
+ localSrc = subpath ? path.join(tmpSrc, subpath) : tmpSrc;
968
+ if (!fs.existsSync(localSrc)) {
969
+ console.error(`❌ Subdirectory not found in repo: ${subpath}`);
970
+ process.exit(1);
971
+ }
972
+ }
973
+
974
+ // Ensure output directory exists
975
+ const outputDir = path.dirname(opts.output);
976
+ if (!fs.existsSync(outputDir)) {
977
+ fs.mkdirSync(outputDir, { recursive: true });
978
+ }
378
979
 
980
+ // Run crd-ref-docs
981
+ const args = [
982
+ '--source-path',
983
+ localSrc,
984
+ '--max-depth',
985
+ opts.depth,
986
+ '--templates-dir',
987
+ opts.templatesDir,
988
+ '--config',
989
+ configPath,
990
+ '--renderer',
991
+ 'asciidoctor',
992
+ '--output-path',
993
+ opts.output,
994
+ ];
995
+ console.log(`⏳ Running crd-ref-docs ${args.join(' ')}`);
996
+ if (spawnSync('crd-ref-docs', args, { stdio: 'inherit' }).status !== 0) {
997
+ console.error(`❌ crd-ref-docs failed`);
998
+ process.exit(1);
999
+ }
1000
+
1001
+ let doc = fs.readFileSync(opts.output, 'utf8');
1002
+ const xrefRe = /https:\/\/docs\.redpanda\.com[^\s\]\[\)"]+(?:\[[^\]]*\])?/g;
1003
+ doc = doc.replace(xrefRe, (match) => {
1004
+ let urlPart = match;
1005
+ let bracketPart = '';
1006
+ const m = match.match(/^([^\[]+)(\[[^\]]*\])$/);
1007
+ if (m) {
1008
+ urlPart = m[1];
1009
+ bracketPart = m[2];
1010
+ }
1011
+ if (urlPart.endsWith('#')) {
1012
+ return match;
1013
+ }
1014
+ try {
1015
+ const xref = urlToXref(urlPart);
1016
+ return bracketPart ? `${xref}${bracketPart}` : `${xref}[]`;
1017
+ } catch (err) {
1018
+ console.warn(`⚠️ urlToXref failed on ${urlPart}: ${err.message}`);
1019
+ return match;
1020
+ }
1021
+ });
1022
+ fs.writeFileSync(opts.output, doc, 'utf8');
1023
+
1024
+ // Cleanup
1025
+ if (tmpSrc) fs.rmSync(tmpSrc, { recursive: true, force: true });
1026
+ fs.rmSync(configTmp, { recursive: true, force: true });
1027
+
1028
+ console.log(`✅ CRD docs generated at ${opts.output}`);
1029
+ if (inDocs) {
1030
+ console.log(`ℹ️ Don't forget to commit your changes on branch '${docsBranch}'.`);
1031
+ }
1032
+ });
1033
+
1034
+ programCli.addCommand(automation);
1035
+ programCli.parse(process.argv);