@redpanda-data/docs-extensions-and-macros 5.2.5 → 5.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,566 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Static (source-based) detection of Linux-only rpk commands.
5
+ *
6
+ * Works on any platform (including Linux CI runners, where the dynamic
7
+ * "build on Linux and Darwin and diff the trees" approach is impossible
8
+ * because a cross-compiled darwin rpk cannot be executed).
9
+ *
10
+ * How it works
11
+ * ------------
12
+ * rpk gates platform-specific commands with Go build constraints:
13
+ *
14
+ * 1. Whole-package gating: every file in a command's package carries a
15
+ * Linux constraint (explicit `//go:build linux` tag or a `_linux.go`
16
+ * filename suffix) and no darwin-buildable counterpart exists.
17
+ * Example: pkg/cli/iotune, pkg/cli/redpanda/tune.
18
+ *
19
+ * 2. Dual registration: a package provides two variants of the same
20
+ * command constructor, one per platform, registering different
21
+ * subcommand sets. Example: pkg/cli/redpanda/redpanda.go
22
+ * (`//go:build linux`, registers start/stop/check/mode/config/tune)
23
+ * vs pkg/cli/redpanda/redpanda_darwin.go (`//go:build darwin`,
24
+ * registers only the hidden admin command). Same pattern at the root:
25
+ * pkg/cli/root_linux.go vs pkg/cli/root_darwin.go.
26
+ *
27
+ * This module detects both patterns:
28
+ *
29
+ * - For every scanned package, it computes which files build on linux
30
+ * and which build on darwin (from `//go:build` / `// +build` tags AND
31
+ * filename-implied constraints like `_linux.go`).
32
+ * - Packages whose files all fail to build on darwin (but build on
33
+ * linux) and that define a cobra command constructor are Linux-only.
34
+ * - Packages containing platform-differential files get a registration
35
+ * diff: subcommand constructors referenced from linux-buildable files
36
+ * but not from darwin-buildable files are Linux-only. Deprecated and
37
+ * hidden constructors are ignored because `rpk --print-tree` (the
38
+ * basis of the generated docs) excludes hidden commands.
39
+ *
40
+ * Scope: built-in commands only. Plugin commands (rpk connect, rpk ai, ...)
41
+ * are not part of the rpk source tree, so their platform availability
42
+ * cannot be derived here. Plugins keep whatever platform behavior the
43
+ * caller's other detection paths provide (for example, dynamic detection
44
+ * marks plugin-only commands when plugins are only installed in the Linux
45
+ * build container).
46
+ */
47
+
48
+ const fs = require('fs')
49
+ const path = require('path')
50
+
51
+ /** GOOS values recognized in filename suffixes and build tags */
52
+ const KNOWN_GOOS = new Set([
53
+ 'aix', 'android', 'darwin', 'dragonfly', 'freebsd', 'hurd', 'illumos',
54
+ 'ios', 'js', 'linux', 'nacl', 'netbsd', 'openbsd', 'plan9', 'solaris',
55
+ 'wasip1', 'windows', 'zos'
56
+ ])
57
+
58
+ /** GOOS values that satisfy the `unix` build tag */
59
+ const UNIX_GOOS = new Set([
60
+ 'aix', 'android', 'darwin', 'dragonfly', 'freebsd', 'hurd', 'illumos',
61
+ 'ios', 'linux', 'netbsd', 'openbsd', 'solaris'
62
+ ])
63
+
64
+ /** GOARCH values recognized in filename suffixes */
65
+ const KNOWN_GOARCH = new Set([
66
+ '386', 'amd64', 'amd64p32', 'arm', 'arm64', 'arm64be', 'armbe', 'loong64',
67
+ 'mips', 'mips64', 'mips64le', 'mips64p32', 'mips64p32le', 'mipsle',
68
+ 'ppc', 'ppc64', 'ppc64le', 'riscv', 'riscv64', 's390', 's390x',
69
+ 'sparc', 'sparc64', 'wasm'
70
+ ])
71
+
72
+ /**
73
+ * Directories (relative to the rpk source root) scanned for command
74
+ * implementations. Modern rpk keeps commands in pkg/cli/<command>/;
75
+ * the other roots cover older source layouts.
76
+ */
77
+ const SCAN_ROOTS = ['pkg/cli', 'pkg/cli/cmd', 'cmd/rpk']
78
+
79
+ /** Directory names that never correspond to a command path segment */
80
+ const NON_COMMAND_SEGMENTS = new Set(['internal', 'common', 'testdata'])
81
+
82
+ /**
83
+ * Determine the GOOS constraint implied by a Go filename.
84
+ * Follows Go's rules: *_GOOS.go, *_GOOS_GOARCH.go constrain the OS;
85
+ * *_GOARCH.go alone does not.
86
+ * @param {string} fileName - e.g. 'redpanda_darwin.go', 'bundle_k8s_linux.go'
87
+ * @returns {string|null} GOOS name or null when unconstrained
88
+ */
89
+ function goosFromFileName(fileName) {
90
+ const base = fileName.replace(/\.go$/, '')
91
+ const parts = base.split('_')
92
+ if (parts.length < 2) return null
93
+
94
+ const last = parts[parts.length - 1]
95
+ if (KNOWN_GOOS.has(last)) return last
96
+ if (KNOWN_GOARCH.has(last) && parts.length >= 3) {
97
+ const secondLast = parts[parts.length - 2]
98
+ if (KNOWN_GOOS.has(secondLast)) return secondLast
99
+ }
100
+ return null
101
+ }
102
+
103
+ /**
104
+ * Evaluate a `//go:build` constraint expression for a target GOOS.
105
+ * Supports identifiers, !, &&, || and parentheses.
106
+ * Unknown identifiers (custom build tags) evaluate to false, matching a
107
+ * default `go build` with no -tags flag. GOARCH identifiers evaluate to
108
+ * true because we only care about OS-level availability.
109
+ * @param {string} expr - Expression after `//go:build`
110
+ * @param {string} goos - Target GOOS ('linux' or 'darwin')
111
+ * @returns {boolean}
112
+ */
113
+ function evaluateBuildExpr(expr, goos) {
114
+ const tokens = expr.match(/[A-Za-z0-9_.]+|&&|\|\||!|\(|\)/g) || []
115
+ let pos = 0
116
+
117
+ const peek = () => tokens[pos]
118
+ const next = () => tokens[pos++]
119
+
120
+ function evalIdent(ident) {
121
+ if (KNOWN_GOOS.has(ident)) return ident === goos
122
+ if (ident === 'unix') return UNIX_GOOS.has(goos)
123
+ if (KNOWN_GOARCH.has(ident)) return true
124
+ if (ident === 'cgo') return true
125
+ // Custom build tags (e.g. withasan, integration) are unset by default
126
+ return false
127
+ }
128
+
129
+ function parsePrimary() {
130
+ const tok = next()
131
+ if (tok === '(') {
132
+ const val = parseOr()
133
+ if (peek() === ')') next()
134
+ return val
135
+ }
136
+ if (tok === '!') return !parsePrimary()
137
+ if (tok === undefined) return false
138
+ return evalIdent(tok)
139
+ }
140
+
141
+ function parseAnd() {
142
+ let val = parsePrimary()
143
+ while (peek() === '&&') {
144
+ next()
145
+ val = parsePrimary() && val
146
+ }
147
+ return val
148
+ }
149
+
150
+ function parseOr() {
151
+ let val = parseAnd()
152
+ while (peek() === '||') {
153
+ next()
154
+ val = parseAnd() || val
155
+ }
156
+ return val
157
+ }
158
+
159
+ return parseOr()
160
+ }
161
+
162
+ /**
163
+ * Evaluate legacy `// +build` lines for a target GOOS.
164
+ * Multiple lines AND together; within a line, space-separated options OR
165
+ * and comma-separated terms AND.
166
+ * @param {string[]} lines - Contents after `// +build` (one per line)
167
+ * @param {string} goos - Target GOOS
168
+ * @returns {boolean}
169
+ */
170
+ function evaluatePlusBuildLines(lines, goos) {
171
+ const evalTerm = (term) => {
172
+ let negate = false
173
+ while (term.startsWith('!')) {
174
+ negate = !negate
175
+ term = term.slice(1)
176
+ }
177
+ let val
178
+ if (KNOWN_GOOS.has(term)) val = term === goos
179
+ else if (term === 'unix') val = UNIX_GOOS.has(goos)
180
+ else if (KNOWN_GOARCH.has(term) || term === 'cgo') val = true
181
+ else val = false
182
+ return negate ? !val : val
183
+ }
184
+
185
+ return lines.every(line => {
186
+ const options = line.trim().split(/\s+/).filter(Boolean)
187
+ if (options.length === 0) return true
188
+ return options.some(opt => opt.split(',').every(evalTerm))
189
+ })
190
+ }
191
+
192
+ /**
193
+ * Extract the build constraint (if any) from a Go file's header.
194
+ * Only lines before the `package` declaration are considered.
195
+ * @param {string} content - Go file content
196
+ * @returns {{goBuildExpr: string|null, plusBuildLines: string[]}}
197
+ */
198
+ function extractBuildConstraint(content) {
199
+ const header = content.split(/^package\s/m)[0]
200
+ const goBuildMatch = header.match(/^\/\/go:build\s+(.+)$/m)
201
+ const plusBuildLines = []
202
+ const plusBuildRe = /^\/\/\s*\+build\s+(.+)$/gm
203
+ let m
204
+ while ((m = plusBuildRe.exec(header)) !== null) {
205
+ plusBuildLines.push(m[1])
206
+ }
207
+ return {
208
+ goBuildExpr: goBuildMatch ? goBuildMatch[1].trim() : null,
209
+ plusBuildLines
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Determine whether a Go file builds for a target GOOS, taking both the
215
+ * filename-implied constraint and explicit build tags into account.
216
+ * @param {string} fileName - Base name of the file
217
+ * @param {string} content - File content
218
+ * @param {string} goos - Target GOOS ('linux' or 'darwin')
219
+ * @returns {boolean}
220
+ */
221
+ function fileBuildsOn(fileName, content, goos) {
222
+ const fileGoos = goosFromFileName(fileName)
223
+ if (fileGoos && fileGoos !== goos) return false
224
+
225
+ const { goBuildExpr, plusBuildLines } = extractBuildConstraint(content)
226
+ if (goBuildExpr) return evaluateBuildExpr(goBuildExpr, goos)
227
+ if (plusBuildLines.length > 0) return evaluatePlusBuildLines(plusBuildLines, goos)
228
+ return true
229
+ }
230
+
231
+ /**
232
+ * Derive a command name from a cobra constructor function name.
233
+ * NewStartCommand -> start, newTuneCommand -> tune, NewCommand -> null.
234
+ * @param {string} funcName
235
+ * @returns {string|null}
236
+ */
237
+ function commandNameFromConstructor(funcName) {
238
+ const m = funcName.match(/^(?:New|new)(\w+?)(?:Command|Cmd)$/)
239
+ if (m && m[1]) return m[1].toLowerCase()
240
+ return null
241
+ }
242
+
243
+ /**
244
+ * Parse cobra command constructors defined in a Go file.
245
+ * A constructor is any `func XxxYyy(...) *cobra.Command`. For each one we
246
+ * record the cobra `Use:` name (first token) and whether the command is
247
+ * deprecated or hidden (such commands never appear in `rpk --print-tree`).
248
+ * @param {string} content - Go file content
249
+ * @returns {Object<string, {useName: string|null, excluded: boolean}>}
250
+ */
251
+ function parseConstructors(content) {
252
+ const constructors = {}
253
+ const funcRe = /^func\s+(\w+)\s*\([^)]*\)\s*\*cobra\.Command\s*\{/gm
254
+ const matches = []
255
+ let m
256
+ while ((m = funcRe.exec(content)) !== null) {
257
+ matches.push({ name: m[1], start: m.index })
258
+ }
259
+
260
+ for (let i = 0; i < matches.length; i++) {
261
+ const bodyEnd = i + 1 < matches.length ? matches[i + 1].start : content.length
262
+ const body = content.slice(matches[i].start, bodyEnd)
263
+
264
+ const useMatch = body.match(/\bUse:\s*"([^"]+)"/)
265
+ const useName = useMatch ? useMatch[1].trim().split(/\s+/)[0] : null
266
+
267
+ // Deprecated or hidden commands are excluded from `rpk --print-tree`
268
+ const excluded = /\bHidden:\s*true\b/.test(body) ||
269
+ /\bDeprecated:\s*"/.test(body) ||
270
+ /\bDeprecateCmd\s*\(/.test(body)
271
+
272
+ constructors[matches[i].name] = { useName, excluded }
273
+ }
274
+
275
+ return constructors
276
+ }
277
+
278
+ /**
279
+ * Extract constructor references from all AddCommand(...) calls in a file.
280
+ * Handles both same-package (`NewStartCommand(...)`) and cross-package
281
+ * (`tune.NewCommand(...)`) references. Only functions starting with
282
+ * New/new are considered, which skips wrappers like cobraext.DeprecateCmd.
283
+ * @param {string} content - Go file content
284
+ * @returns {Array<{qualifier: string|null, funcName: string}>}
285
+ */
286
+ function parseAddCommandRefs(content) {
287
+ const refs = []
288
+ const addRe = /\bAddCommand\s*\(/g
289
+ let m
290
+ while ((m = addRe.exec(content)) !== null) {
291
+ // Find the span of this AddCommand(...) call by balancing parentheses
292
+ let depth = 1
293
+ let i = m.index + m[0].length
294
+ while (i < content.length && depth > 0) {
295
+ if (content[i] === '(') depth++
296
+ else if (content[i] === ')') depth--
297
+ i++
298
+ }
299
+ const span = content.slice(m.index + m[0].length, i - 1)
300
+
301
+ const callRe = /(?:(\w+)\.)?((?:New|new)\w*)\s*\(/g
302
+ let c
303
+ while ((c = callRe.exec(span)) !== null) {
304
+ refs.push({ qualifier: c[1] || null, funcName: c[2] })
305
+ }
306
+ }
307
+ return refs
308
+ }
309
+
310
+ /**
311
+ * Recursively collect non-test Go files under a directory.
312
+ * @param {string} dir - Absolute directory path
313
+ * @param {Array} out - Accumulator of {absPath, dir, name}
314
+ */
315
+ function collectGoFiles(dir, out) {
316
+ let entries
317
+ try {
318
+ entries = fs.readdirSync(dir, { withFileTypes: true })
319
+ } catch (err) {
320
+ return
321
+ }
322
+ for (const entry of entries) {
323
+ const fullPath = path.join(dir, entry.name)
324
+ if (entry.isDirectory()) {
325
+ if (entry.name === 'testdata' || entry.name === 'vendor') continue
326
+ collectGoFiles(fullPath, out)
327
+ } else if (entry.name.endsWith('.go') && !entry.name.endsWith('_test.go')) {
328
+ out.push({ absPath: fullPath, dir, name: entry.name })
329
+ }
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Build the command path prefix for a package directory.
335
+ * pkg/cli -> 'rpk'; pkg/cli/redpanda -> 'rpk redpanda';
336
+ * pkg/cli/cmd/redpanda (legacy) -> 'rpk redpanda'.
337
+ * @param {string} scanRoot - Absolute path of the scan root
338
+ * @param {string} dir - Absolute directory path
339
+ * @returns {string|null} Command path prefix, or null for non-command dirs
340
+ */
341
+ function commandPrefixForDir(scanRoot, dir) {
342
+ const rel = path.relative(scanRoot, dir)
343
+ let segments = rel === '' ? [] : rel.split(path.sep)
344
+ // Legacy layouts nested commands under an extra cmd/ level
345
+ if (segments[0] === 'cmd') segments = segments.slice(1)
346
+ if (segments.some(s => NON_COMMAND_SEGMENTS.has(s))) return null
347
+ return ['rpk', ...segments].join(' ')
348
+ }
349
+
350
+ /**
351
+ * Analyze one scan root and add detected Linux-only command paths.
352
+ * @param {string} scanRoot - Absolute path of the scan root
353
+ * @param {Set<string>} linuxOnlyCommands - Accumulator
354
+ */
355
+ function scanRootForLinuxOnly(scanRoot, linuxOnlyCommands) {
356
+ const files = []
357
+ collectGoFiles(scanRoot, files)
358
+ if (files.length === 0) return
359
+
360
+ // Group files (with parsed metadata) by directory
361
+ const dirs = new Map()
362
+ for (const file of files) {
363
+ let content
364
+ try {
365
+ content = fs.readFileSync(file.absPath, 'utf8')
366
+ } catch (err) {
367
+ continue
368
+ }
369
+ const info = {
370
+ name: file.name,
371
+ content,
372
+ buildsOnLinux: fileBuildsOn(file.name, content, 'linux'),
373
+ buildsOnDarwin: fileBuildsOn(file.name, content, 'darwin')
374
+ }
375
+ if (!dirs.has(file.dir)) dirs.set(file.dir, [])
376
+ dirs.get(file.dir).push(info)
377
+ }
378
+
379
+ // Per-directory constructor index: dir -> {funcName: {useName, excluded}}
380
+ const constructorIndex = new Map()
381
+ for (const [dir, dirFiles] of dirs) {
382
+ const index = {}
383
+ for (const f of dirFiles) {
384
+ Object.assign(index, parseConstructors(f.content))
385
+ }
386
+ constructorIndex.set(dir, index)
387
+ }
388
+
389
+ /**
390
+ * Resolve an AddCommand constructor reference to a visible command name.
391
+ * Returns null for deprecated/hidden/unresolvable commands.
392
+ */
393
+ const resolveRef = (dir, ref) => {
394
+ let target = null
395
+ let fallback = commandNameFromConstructor(ref.funcName)
396
+
397
+ if (ref.qualifier) {
398
+ const subDir = path.join(dir, ref.qualifier)
399
+ target = constructorIndex.get(subDir) || null
400
+ if (!fallback) fallback = ref.qualifier
401
+ } else {
402
+ target = constructorIndex.get(dir) || null
403
+ }
404
+
405
+ if (target && target[ref.funcName]) {
406
+ const ctor = target[ref.funcName]
407
+ if (ctor.excluded) return null
408
+ return ctor.useName || fallback
409
+ }
410
+
411
+ // Unresolvable reference: only trust it if the function name follows
412
+ // the cobra constructor convention (New[Xxx]Command). This skips
413
+ // helper arguments captured inside AddCommand spans, such as the
414
+ // rp.NewLauncher() argument in redpanda.NewCommand(fs, p, rp.NewLauncher()).
415
+ if (!/^(?:New|new)(?:\w*(?:Command|Cmd))?$/.test(ref.funcName)) return null
416
+ return fallback
417
+ }
418
+
419
+ for (const [dir, dirFiles] of dirs) {
420
+ const prefix = commandPrefixForDir(scanRoot, dir)
421
+ if (!prefix) continue
422
+
423
+ const hasDifferentialFile = dirFiles.some(f => f.buildsOnLinux !== f.buildsOnDarwin)
424
+
425
+ // Pattern 1: whole package is Linux-gated (and defines a command)
426
+ if (prefix !== 'rpk') {
427
+ const anyLinux = dirFiles.some(f => f.buildsOnLinux)
428
+ const anyDarwin = dirFiles.some(f => f.buildsOnDarwin)
429
+ const definesCommand = Object.keys(constructorIndex.get(dir)).length > 0
430
+ if (anyLinux && !anyDarwin && definesCommand) {
431
+ linuxOnlyCommands.add(prefix)
432
+ continue
433
+ }
434
+ }
435
+
436
+ // Pattern 2: dual registration - diff subcommands registered from
437
+ // linux-buildable files vs darwin-buildable files
438
+ if (!hasDifferentialFile) continue
439
+
440
+ const refsFor = (goos) => {
441
+ const names = new Set()
442
+ for (const f of dirFiles) {
443
+ const buildable = goos === 'linux' ? f.buildsOnLinux : f.buildsOnDarwin
444
+ if (!buildable) continue
445
+ for (const ref of parseAddCommandRefs(f.content)) {
446
+ const name = resolveRef(dir, ref)
447
+ if (name) names.add(name)
448
+ }
449
+ }
450
+ return names
451
+ }
452
+
453
+ const linuxRefs = refsFor('linux')
454
+ const darwinRefs = refsFor('darwin')
455
+
456
+ for (const name of linuxRefs) {
457
+ if (!darwinRefs.has(name)) {
458
+ linuxOnlyCommands.add(`${prefix} ${name}`)
459
+ }
460
+ }
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Detect Linux-only rpk commands by statically analyzing Go source.
466
+ * Works on any platform, including Linux CI runners.
467
+ * @param {string} sourcePath - Path to the rpk Go source root (src/go/rpk)
468
+ * @returns {Set<string>} Linux-only command paths (e.g. 'rpk iotune').
469
+ * Descendants of a returned path are implicitly Linux-only too.
470
+ */
471
+ function detectLinuxOnlyFromSource(sourcePath) {
472
+ const linuxOnlyCommands = new Set()
473
+
474
+ const scannedRoots = new Set()
475
+ for (const root of SCAN_ROOTS) {
476
+ const absRoot = path.join(sourcePath, root)
477
+ if (!fs.existsSync(absRoot)) continue
478
+ // pkg/cli/cmd is nested inside pkg/cli; avoid scanning it twice
479
+ if ([...scannedRoots].some(r => absRoot.startsWith(r + path.sep))) continue
480
+ scannedRoots.add(absRoot)
481
+ scanRootForLinuxOnly(absRoot, linuxOnlyCommands)
482
+ }
483
+
484
+ // Drop any detected path that is a descendant of another detected path;
485
+ // platform markers already propagate to descendants.
486
+ for (const cmd of [...linuxOnlyCommands]) {
487
+ if ([...linuxOnlyCommands].some(other => other !== cmd && cmd.startsWith(other + ' '))) {
488
+ linuxOnlyCommands.delete(cmd)
489
+ }
490
+ }
491
+
492
+ return linuxOnlyCommands
493
+ }
494
+
495
+ /**
496
+ * List Go files under the scanned roots that carry a Linux-only build
497
+ * constraint (explicit tag or filename suffix). Used as a tripwire: if
498
+ * detection returns an empty set while this list is non-empty, the scan
499
+ * is almost certainly broken (wrong directory layout, changed tag style).
500
+ * @param {string} sourcePath - Path to the rpk Go source root
501
+ * @returns {string[]} Relative paths of Linux-constrained files
502
+ */
503
+ function findLinuxConstrainedFiles(sourcePath) {
504
+ const results = []
505
+ for (const root of SCAN_ROOTS) {
506
+ const absRoot = path.join(sourcePath, root)
507
+ if (!fs.existsSync(absRoot)) continue
508
+ const files = []
509
+ collectGoFiles(absRoot, files)
510
+ for (const file of files) {
511
+ let content
512
+ try {
513
+ content = fs.readFileSync(file.absPath, 'utf8')
514
+ } catch (err) {
515
+ continue
516
+ }
517
+ if (fileBuildsOn(file.name, content, 'linux') && !fileBuildsOn(file.name, content, 'darwin')) {
518
+ results.push(path.relative(sourcePath, file.absPath))
519
+ }
520
+ }
521
+ }
522
+ return [...new Set(results)]
523
+ }
524
+
525
+ /**
526
+ * Emit a loud warning when platform detection came back empty even though
527
+ * the source demonstrably contains Linux-gated files. Never let a scan
528
+ * failure silently mark every command as cross-platform.
529
+ * @param {string} sourcePath - Path to the rpk Go source root
530
+ * @param {Set<string>} linuxOnlyCommands - Combined detection result
531
+ * @returns {boolean} True when the tripwire fired
532
+ */
533
+ function warnIfDetectionLooksBroken(sourcePath, linuxOnlyCommands) {
534
+ if (linuxOnlyCommands.size > 0) return false
535
+
536
+ const constrained = findLinuxConstrainedFiles(sourcePath)
537
+ if (constrained.length === 0) return false
538
+
539
+ console.warn('\n' + '!'.repeat(70))
540
+ console.warn('⚠ PLATFORM DETECTION TRIPWIRE: no Linux-only commands were detected,')
541
+ console.warn(`⚠ but the rpk source contains ${constrained.length} Linux-constrained file(s), e.g.:`)
542
+ for (const file of constrained.slice(0, 5)) {
543
+ console.warn(`⚠ - ${file}`)
544
+ }
545
+ console.warn('⚠ This usually means the static source scan failed (unexpected')
546
+ console.warn('⚠ directory layout or build-tag style) and every command is about')
547
+ console.warn('⚠ to be marked as available on both Linux and macOS, which is wrong.')
548
+ console.warn('⚠ Fix tools/rpk-docs/detect-platform-commands.js before publishing')
549
+ console.warn('⚠ these docs. See redpanda-data/docs#1831 for the impact of this.')
550
+ console.warn('!'.repeat(70) + '\n')
551
+ return true
552
+ }
553
+
554
+ module.exports = {
555
+ detectLinuxOnlyFromSource,
556
+ findLinuxConstrainedFiles,
557
+ warnIfDetectionLooksBroken,
558
+ // Exported for unit tests
559
+ fileBuildsOn,
560
+ goosFromFileName,
561
+ evaluateBuildExpr,
562
+ evaluatePlusBuildLines,
563
+ parseConstructors,
564
+ parseAddCommandRefs,
565
+ commandNameFromConstructor
566
+ }