@redpanda-data/docs-extensions-and-macros 5.13.5 → 5.14.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.
package/bin/doc-tools.js CHANGED
@@ -529,6 +529,103 @@ programCli
529
529
  }
530
530
  })
531
531
 
532
+ /**
533
+ * @description Copy this package's docs-data/*.schema.json files into a
534
+ * content repo's own docs-data/ (the file that documents docs-data/x.json
535
+ * lives alongside x.json itself, so a writer can change both in one PR
536
+ * without waiting on a package release first). Run this after bumping
537
+ * @redpanda-data/docs-extensions-and-macros so the local copy picks up
538
+ * whatever changed upstream.
539
+ * @why Nothing keeps a content repo's schema copy in sync with this
540
+ * package's copy today, in either direction. A content repo's copy can
541
+ * legitimately be ahead of this package's (confirmed happening in
542
+ * practice: the docs repo's rpk-overrides.schema.json documents a real,
543
+ * working `asPartial` override field that this package's own copy is
544
+ * missing) — overwriting it in that case would delete real, correct
545
+ * documentation, not fix drift. This only overwrites when this package's
546
+ * copy is a strict superset of the destination; otherwise it reports which
547
+ * keys only the destination has and leaves the file alone (status
548
+ * 'diverged') unless --force is passed. --check never writes, for a CI
549
+ * gate.
550
+ * @example
551
+ * # Sync into ./docs-data (writes any missing or out-of-date schema)
552
+ * npx doc-tools sync-schemas
553
+ *
554
+ * # Report drift without writing -- exits 1 if anything is out of sync
555
+ * npx doc-tools sync-schemas --check
556
+ *
557
+ * # Sync into a non-default location
558
+ * npx doc-tools sync-schemas --dest path/to/docs-data
559
+ *
560
+ * # Overwrite even a destination that has diverged (rarely correct --
561
+ * # read the 'diverged' output first; it names what would be lost)
562
+ * npx doc-tools sync-schemas --force
563
+ * @requirements None.
564
+ */
565
+ programCli
566
+ .command('sync-schemas')
567
+ .description("Sync this package's docs-data/*.schema.json into a content repo's docs-data/")
568
+ .option('--dest <path>', 'Destination docs-data directory', 'docs-data')
569
+ .option('--check', 'Report drift without writing; exit 1 if any schema is missing, out of date, or diverged')
570
+ .option('--force', 'Overwrite even a destination that has diverged (has content this package lacks)')
571
+ .action((options) => {
572
+ const { syncSchemas } = require('../cli-utils/sync-schemas')
573
+
574
+ try {
575
+ const { destDir, results, drift } = syncSchemas({
576
+ destDir: options.dest,
577
+ check: Boolean(options.check),
578
+ force: Boolean(options.force),
579
+ })
580
+
581
+ if (results.length === 0) {
582
+ console.log('No *.schema.json files found in this package to sync.')
583
+ process.exit(0)
584
+ }
585
+
586
+ console.log(`${options.check ? 'Checking' : 'Syncing'} against: ${destDir}`)
587
+ console.log('')
588
+ let hasUnresolvedDivergence = false
589
+ for (const { name, status, destOnlyPaths } of results) {
590
+ const label = {
591
+ unchanged: '✓ up to date',
592
+ created: '+ created',
593
+ updated: '↻ updated',
594
+ diverged: options.force ? '↻ updated (forced)' : '⚠ diverged, left alone',
595
+ }[status]
596
+ console.log(` ${label} ${name}`)
597
+ if (status === 'diverged' && !options.force) {
598
+ hasUnresolvedDivergence = true
599
+ for (const p of destOnlyPaths) console.log(` only in the destination: ${p}`)
600
+ }
601
+ }
602
+ console.log('')
603
+
604
+ if (options.check) {
605
+ if (drift) {
606
+ const hint = hasUnresolvedDivergence
607
+ ? 'A diverged file needs a human decision (see paths above), not just a re-run.'
608
+ : 'Run `npx doc-tools sync-schemas` to fix.'
609
+ console.log(`✗ One or more schemas are out of date. ${hint}`)
610
+ process.exit(1)
611
+ }
612
+ console.log('✓ All schemas are in sync.')
613
+ process.exit(0)
614
+ }
615
+
616
+ if (hasUnresolvedDivergence) {
617
+ console.log('⚠ Synced what was safe to sync. One or more files diverged and were left alone -- see paths above. Re-run with --force only after confirming the destination-only content should be lost.')
618
+ process.exit(1)
619
+ }
620
+
621
+ console.log(drift ? '✓ Synced.' : '✓ Already in sync -- nothing to do.')
622
+ process.exit(0)
623
+ } catch (err) {
624
+ console.error(`Error: ${err.message}`)
625
+ process.exit(1)
626
+ }
627
+ })
628
+
532
629
  /**
533
630
  * @description Show MCP server version information including available tools
534
631
  * and optionally usage statistics from previous sessions.
@@ -0,0 +1,144 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { isDeepStrictEqual } = require('util')
6
+
7
+ /**
8
+ * Schemas that document docs-data/*.json files (rpk-overrides.schema.json,
9
+ * property-overrides.schema.json, ...) are meant to live inside each content
10
+ * repo's own docs-data/, alongside the file they document — a writer can
11
+ * change both the override data and its schema in one PR, without waiting on
12
+ * a new @redpanda-data/docs-extensions-and-macros release first.
13
+ *
14
+ * That's exactly why this package's copy is not automatically authoritative:
15
+ * a content repo's copy can legitimately be AHEAD of this package's, when a
16
+ * writer documented a real override field here before anyone remembered to
17
+ * update the schema in this package. Confirmed happening in practice —
18
+ * docs-data/rpk-overrides.schema.json in this package is missing `asPartial`,
19
+ * a real field the generator (tools/rpk-docs/generate-rpk-docs.js) already
20
+ * reads, that the docs repo's copy documents correctly. A naive "package
21
+ * always wins" sync would have silently deleted that the first time someone
22
+ * ran it. So syncSchemas() only ever overwrites a destination file when this
23
+ * package's copy is a superset of it (every object key the destination has,
24
+ * the source also has) — otherwise it reports the destination-only keys and
25
+ * refuses, same as `check` mode, unless the caller passes `force: true`.
26
+ */
27
+
28
+ const PACKAGE_SCHEMA_DIR = path.resolve(__dirname, '..', 'docs-data')
29
+
30
+ /**
31
+ * List the *.schema.json files this package ships.
32
+ * @returns {Array<{name: string, sourcePath: string}>}
33
+ */
34
+ function listPackageSchemas () {
35
+ if (!fs.existsSync(PACKAGE_SCHEMA_DIR)) return []
36
+ return fs.readdirSync(PACKAGE_SCHEMA_DIR)
37
+ .filter((name) => name.endsWith('.schema.json'))
38
+ .sort()
39
+ .map((name) => ({ name, sourcePath: path.join(PACKAGE_SCHEMA_DIR, name) }))
40
+ }
41
+
42
+ /**
43
+ * Find object keys present in `dest` but absent from the same path in
44
+ * `source` — the shape of "the destination knows something the source
45
+ * doesn't". Arrays are compared as opaque leaves (JSON Schema's own
46
+ * structure keeps named, distinguishable content in objects — `properties`,
47
+ * `$defs` — not in arrays like `required`/`enum`, so this is where a
48
+ * destination-only capability would actually show up).
49
+ *
50
+ * @param {*} source
51
+ * @param {*} dest
52
+ * @param {string} [pathPrefix]
53
+ * @returns {string[]} Dotted paths that exist in dest but not source.
54
+ */
55
+ function findDestOnlyPaths (source, dest, pathPrefix = '') {
56
+ if (
57
+ dest === null || typeof dest !== 'object' || Array.isArray(dest) ||
58
+ source === null || typeof source !== 'object' || Array.isArray(source)
59
+ ) {
60
+ return []
61
+ }
62
+
63
+ const onlyInDest = []
64
+ for (const [key, destValue] of Object.entries(dest)) {
65
+ const keyPath = pathPrefix ? `${pathPrefix}.${key}` : key
66
+ if (!Object.hasOwn(source, key)) {
67
+ onlyInDest.push(keyPath)
68
+ continue
69
+ }
70
+ onlyInDest.push(...findDestOnlyPaths(source[key], destValue, keyPath))
71
+ }
72
+ return onlyInDest
73
+ }
74
+
75
+ /**
76
+ * Compare this package's schemas against the copies in a destination
77
+ * directory (a content repo's docs-data/), and optionally write updates.
78
+ *
79
+ * @param {object} [opts]
80
+ * @param {string} [opts.destDir] - Directory to sync into. Defaults to
81
+ * docs-data/ under the current repo root.
82
+ * @param {boolean} [opts.check=false] - When true, never writes — only
83
+ * reports what's missing or out of date. Use this in CI.
84
+ * @param {boolean} [opts.force=false] - Overwrite a destination file even
85
+ * when it has content this package's copy lacks. Rarely correct — see the
86
+ * module doc comment. Ignored when `check` is true.
87
+ * @returns {{
88
+ * destDir: string,
89
+ * results: Array<{
90
+ * name: string,
91
+ * status: ('created'|'updated'|'unchanged'|'diverged'),
92
+ * sourcePath: string,
93
+ * destPath: string,
94
+ * destOnlyPaths?: string[]
95
+ * }>,
96
+ * drift: boolean
97
+ * }} drift is true when any schema is missing, differs, or has diverged.
98
+ * status 'diverged' means the destination has content this package's copy
99
+ * doesn't — nothing was written for that file unless `force` was set.
100
+ */
101
+ function syncSchemas ({ destDir, check = false, force = false } = {}) {
102
+ const resolvedDest = path.resolve(destDir || 'docs-data')
103
+ const schemas = listPackageSchemas()
104
+
105
+ const results = schemas.map(({ name, sourcePath }) => {
106
+ const destPath = path.join(resolvedDest, name)
107
+ const sourceContent = fs.readFileSync(sourcePath, 'utf8')
108
+ const destExists = fs.existsSync(destPath)
109
+ const destContent = destExists ? fs.readFileSync(destPath, 'utf8') : null
110
+
111
+ let status
112
+ let destOnlyPaths
113
+ if (!destExists) {
114
+ status = 'created'
115
+ } else {
116
+ // Compare parsed content, not raw text — a destination reformatted by a
117
+ // content repo's own prettier/editorconfig (different indent, key
118
+ // order, trailing newline) with no real data change must not read as
119
+ // drift.
120
+ const sourceParsed = JSON.parse(sourceContent)
121
+ const destParsed = JSON.parse(destContent)
122
+ if (isDeepStrictEqual(sourceParsed, destParsed)) {
123
+ status = 'unchanged'
124
+ } else {
125
+ destOnlyPaths = findDestOnlyPaths(sourceParsed, destParsed)
126
+ status = destOnlyPaths.length > 0 ? 'diverged' : 'updated'
127
+ }
128
+ }
129
+
130
+ const shouldWrite = !check && (status === 'created' || status === 'updated' || (status === 'diverged' && force))
131
+ if (shouldWrite) {
132
+ fs.mkdirSync(resolvedDest, { recursive: true })
133
+ fs.writeFileSync(destPath, sourceContent)
134
+ }
135
+
136
+ return { name, status, sourcePath, destPath, ...(destOnlyPaths ? { destOnlyPaths } : {}) }
137
+ })
138
+
139
+ const drift = results.some((r) => r.status !== 'unchanged')
140
+
141
+ return { destDir: resolvedDest, results, drift }
142
+ }
143
+
144
+ module.exports = { listPackageSchemas, syncSchemas, findDestOnlyPaths, PACKAGE_SCHEMA_DIR }
@@ -137,6 +137,10 @@
137
137
  "type": "boolean",
138
138
  "description": "If true, exclude this command from documentation entirely. Use for internal or hidden commands that shouldn't be documented."
139
139
  },
140
+ "asPartial": {
141
+ "type": "boolean",
142
+ "description": "If true, write this command's .adoc file to the partials directory instead of the pages directory. Inherited by child commands — set on a parent command (e.g., 'rpk ai') to route the entire subtree to partials."
143
+ },
140
144
  "excludeFlags": {
141
145
  "type": "array",
142
146
  "description": "List of flag names to exclude from documentation (without dashes)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.13.5",
3
+ "version": "5.14.0",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",