@clidoc/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/dist/completion.d.ts +10 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +255 -0
- package/dist/completion.js.map +1 -0
- package/dist/discovery.d.ts +11 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +42 -0
- package/dist/discovery.js.map +1 -0
- package/dist/docgen.d.ts +22 -0
- package/dist/docgen.d.ts.map +1 -0
- package/dist/docgen.js +44 -0
- package/dist/docgen.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +326 -0
- package/dist/index.js.map +1 -0
- package/dist/logical.d.ts +9 -0
- package/dist/logical.d.ts.map +1 -0
- package/dist/logical.js +112 -0
- package/dist/logical.js.map +1 -0
- package/dist/merge.d.ts +44 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +59 -0
- package/dist/merge.js.map +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +393 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/schema/spec.schema.json +537 -0
- package/src/completion.ts +285 -0
- package/src/discovery.ts +45 -0
- package/src/docgen.ts +61 -0
- package/src/index.ts +359 -0
- package/src/logical.ts +158 -0
- package/src/merge.ts +96 -0
- package/src/schema.ts +433 -0
- package/src/types.ts +105 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { validate } from './index.js';
|
|
3
|
+
import type { CommandItemObject, FlagItemObject, OpenCliDocument } from './types.js';
|
|
4
|
+
|
|
5
|
+
/** Shells supported by standalone completion scripts. */
|
|
6
|
+
export type CompletionShell = 'bash' | 'zsh' | 'fish';
|
|
7
|
+
export type CompletionOptions = { shell: CompletionShell; binary?: string };
|
|
8
|
+
// Tables use c=child transition, s=subcommands, o=options, f=flag values, a=arguments.
|
|
9
|
+
// Numeric command states let aliases share a subtree without duplicating scripts.
|
|
10
|
+
type Entry = { mode: string; values: string[] };
|
|
11
|
+
type Node = { command: CommandItemObject; children: Map<string, number> };
|
|
12
|
+
|
|
13
|
+
function compile(document: OpenCliDocument): Map<string, Entry> {
|
|
14
|
+
const binary = document.info.binary;
|
|
15
|
+
const nodes: Node[] = [{ command: {}, children: new Map() }];
|
|
16
|
+
const paths = new Map<string, number>([[binary, 0]]);
|
|
17
|
+
const commands = Object.entries(document.commands ?? {}).toSorted(([a], [b]) => a.localeCompare(b));
|
|
18
|
+
for (const [path, command] of commands) {
|
|
19
|
+
if (path !== binary && !path.startsWith(`${binary} `))
|
|
20
|
+
throw new Error(`Completion command must start with ${binary}: ${path}`);
|
|
21
|
+
let parent = 0;
|
|
22
|
+
let full = binary;
|
|
23
|
+
for (const part of path.slice(binary.length).trim().split(/\s+/).filter(Boolean)) {
|
|
24
|
+
full += ` ${part}`;
|
|
25
|
+
let id = paths.get(full);
|
|
26
|
+
if (id === undefined) {
|
|
27
|
+
id = nodes.length;
|
|
28
|
+
nodes.push({ command: {}, children: new Map() });
|
|
29
|
+
nodes[parent]!.children.set(part, id);
|
|
30
|
+
paths.set(full, id);
|
|
31
|
+
}
|
|
32
|
+
parent = id;
|
|
33
|
+
}
|
|
34
|
+
nodes[parent]!.command = command;
|
|
35
|
+
}
|
|
36
|
+
// Aliases identify a sibling command and share its complete descendant tree.
|
|
37
|
+
for (const node of nodes) {
|
|
38
|
+
for (const id of new Set(node.children.values())) {
|
|
39
|
+
for (const alias of nodes[id]!.command.aliases ?? []) {
|
|
40
|
+
if (!alias || /\s/.test(alias)) throw new Error('Completion command aliases must be single words');
|
|
41
|
+
const existing = node.children.get(alias);
|
|
42
|
+
if (existing !== undefined && existing !== id) throw new Error(`Ambiguous completion alias: ${alias}`);
|
|
43
|
+
node.children.set(alias, id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const table = new Map<string, Entry>();
|
|
48
|
+
const put = (key: string, mode: string, values: string[] = []) => table.set(key, { mode, values });
|
|
49
|
+
for (const [id, node] of nodes.entries()) {
|
|
50
|
+
const children = [...node.children].filter(([, child]) => !nodes[child]!.command.hidden);
|
|
51
|
+
put(
|
|
52
|
+
`s:${id}`,
|
|
53
|
+
'',
|
|
54
|
+
children.map(([name]) => name),
|
|
55
|
+
);
|
|
56
|
+
for (const [name, child] of children) put(`c:${id}:${name}`, String(child));
|
|
57
|
+
const flags = new Map<string, FlagItemObject>();
|
|
58
|
+
for (const flag of [...(document.global?.flags ?? []), ...(node.command.flags ?? [])]) {
|
|
59
|
+
flags.set(`--${flag.name}`, flag);
|
|
60
|
+
for (const alias of flag.aliases ?? []) flags.set(`${alias.length === 1 ? '-' : '--'}${alias}`, flag);
|
|
61
|
+
}
|
|
62
|
+
put(
|
|
63
|
+
`o:${id}`,
|
|
64
|
+
'',
|
|
65
|
+
[...flags].filter(([, flag]) => !flag.hidden).map(([name]) => name),
|
|
66
|
+
);
|
|
67
|
+
for (const [name, flag] of flags)
|
|
68
|
+
put(
|
|
69
|
+
`f:${id}:${name}`,
|
|
70
|
+
flag.type === 'boolean' ? 'boolean' : 'value',
|
|
71
|
+
(flag.choices ?? []).map((choice) => String(choice.value)),
|
|
72
|
+
);
|
|
73
|
+
for (const [position, arg] of (node.command.args ?? []).entries())
|
|
74
|
+
put(
|
|
75
|
+
`a:${id}:${position}`,
|
|
76
|
+
arg.variadic ? 'repeat' : 'argument',
|
|
77
|
+
(arg.choices ?? []).map((choice) => String(choice.value)),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return table;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Generate a validated, deterministic script with no runtime dependency on clidoc or the target CLI. */
|
|
84
|
+
export function generateCompletion(document: OpenCliDocument, options: CompletionOptions): string {
|
|
85
|
+
const result = validate(document);
|
|
86
|
+
if (!result.valid) throw new Error(`Invalid OpenCLI document: ${result.errors.join('; ')}`);
|
|
87
|
+
if (!['bash', 'zsh', 'fish'].includes(options.shell))
|
|
88
|
+
throw new Error(`Unsupported completion shell: ${options.shell}`);
|
|
89
|
+
const binary = options.binary ?? document.info.binary;
|
|
90
|
+
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_.+-]*$/.test(binary))
|
|
91
|
+
throw new Error('Completion binary must be a single executable name (letters, digits, _, ., +, -)');
|
|
92
|
+
const table = compile(document);
|
|
93
|
+
const text = [
|
|
94
|
+
...Object.keys(document.commands ?? {}),
|
|
95
|
+
...[...table].flatMap(([key, entry]) => [key, ...entry.values]),
|
|
96
|
+
];
|
|
97
|
+
// eslint-disable-next-line no-control-regex -- Shell completion data must not contain control characters.
|
|
98
|
+
if (text.some((value) => /[\x00-\x1f\x7f]/.test(value)))
|
|
99
|
+
throw new Error('Completion names and choices cannot contain control characters');
|
|
100
|
+
const name = `_clidoc_${createHash('sha256').update(binary).digest('hex').slice(0, 16)}`;
|
|
101
|
+
const shell = options.shell;
|
|
102
|
+
const fish = shell === 'fish';
|
|
103
|
+
const quote = (value: string) =>
|
|
104
|
+
`'${fish ? value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") : value.replace(/'/g, "'\\''")}'`;
|
|
105
|
+
const cases = [...table]
|
|
106
|
+
.map(([key, { mode, values }]) =>
|
|
107
|
+
fish
|
|
108
|
+
? ` case ${quote(key.replace(/[\\*?[\]]/g, '\\$&'))}\n set mode ${quote(mode)}\n set reply ${values.map(quote).join(' ')}`
|
|
109
|
+
: ` ${quote(key)}) mode=${quote(mode)}; reply=(${values.map(quote).join(' ')});;`,
|
|
110
|
+
)
|
|
111
|
+
.join('\n');
|
|
112
|
+
const header = '# Generated by clidoc. Regenerate after changing the CLI schema.\n';
|
|
113
|
+
return header + (fish ? fishScript(name, binary, cases) : shScript(name, binary, cases, shell));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function shScript(name: string, binary: string, cases: string, shell: 'bash' | 'zsh'): string {
|
|
117
|
+
const bash = shell === 'bash';
|
|
118
|
+
return `${name}_lookup() {
|
|
119
|
+
mode=''; reply=()
|
|
120
|
+
case "$1" in
|
|
121
|
+
${cases}
|
|
122
|
+
esac
|
|
123
|
+
}
|
|
124
|
+
${name}() {
|
|
125
|
+
${bash ? '' : 'emulate -L zsh'}
|
|
126
|
+
local state=0 pos=0 ended=0 pending='' cur word key mode prefix='' candidate i
|
|
127
|
+
local -a reply candidates tokens
|
|
128
|
+
${
|
|
129
|
+
bash
|
|
130
|
+
? `COMPREPLY=()
|
|
131
|
+
# Bash splits '=' at COMP_WORDBREAKS; reconstruct tokens up to the cursor.
|
|
132
|
+
for ((i=1; i<=COMP_CWORD; i++)); do
|
|
133
|
+
word="\${COMP_WORDS[i]}"
|
|
134
|
+
if [[ "$word" == '=' && \${#tokens[@]} -gt 0 ]]; then
|
|
135
|
+
tokens[\${#tokens[@]}-1]+='='
|
|
136
|
+
elif [[ \${#tokens[@]} -gt 0 && "\${tokens[\${#tokens[@]}-1]}" == *= ]]; then
|
|
137
|
+
tokens[\${#tokens[@]}-1]+="$word"
|
|
138
|
+
else
|
|
139
|
+
tokens+=("$word")
|
|
140
|
+
fi
|
|
141
|
+
done
|
|
142
|
+
cur="\${tokens[\${#tokens[@]}-1]}"
|
|
143
|
+
unset "tokens[\${#tokens[@]}-1]"`
|
|
144
|
+
: `tokens=("\${(@)words[2,CURRENT-1]}")
|
|
145
|
+
(( CURRENT > 2 )) || tokens=()
|
|
146
|
+
cur="\${words[CURRENT]}"`
|
|
147
|
+
}
|
|
148
|
+
for word in "\${tokens[@]}"; do
|
|
149
|
+
if [[ -n "$pending" ]]; then pending=''; continue; fi
|
|
150
|
+
if [[ "$ended" == 0 && "$word" == '--' ]]; then ended=1; continue; fi
|
|
151
|
+
if [[ "$ended" == 0 && "$word" == -* ]]; then
|
|
152
|
+
${name}_lookup "f:$state:\${word%%=*}"
|
|
153
|
+
if [[ "$mode" == value && "$word" != *=* ]]; then pending="f:$state:$word"; fi
|
|
154
|
+
continue
|
|
155
|
+
fi
|
|
156
|
+
if [[ "$ended" == 0 && "$pos" == 0 ]]; then
|
|
157
|
+
${name}_lookup "c:$state:$word"
|
|
158
|
+
if [[ -n "$mode" ]]; then state="$mode"; continue; fi
|
|
159
|
+
fi
|
|
160
|
+
${name}_lookup "a:$state:$pos"
|
|
161
|
+
[[ "$mode" == repeat ]] || pos=$((pos + 1))
|
|
162
|
+
done
|
|
163
|
+
if [[ -n "$pending" ]]; then
|
|
164
|
+
${name}_lookup "$pending"
|
|
165
|
+
candidates=("\${reply[@]}")
|
|
166
|
+
elif [[ "$ended" == 0 && "$cur" == --*=* ]]; then
|
|
167
|
+
key="\${cur%%=*}"
|
|
168
|
+
prefix="$key="
|
|
169
|
+
cur="\${cur#*=}"
|
|
170
|
+
${name}_lookup "f:$state:$key"
|
|
171
|
+
candidates=("\${reply[@]}")
|
|
172
|
+
elif [[ "$ended" == 0 && "$cur" == -* ]]; then
|
|
173
|
+
${name}_lookup "o:$state"
|
|
174
|
+
candidates=("\${reply[@]}")
|
|
175
|
+
else
|
|
176
|
+
${name}_lookup "a:$state:$pos"
|
|
177
|
+
candidates=("\${reply[@]}")
|
|
178
|
+
if [[ "$ended" == 0 && "$pos" == 0 ]]; then
|
|
179
|
+
${name}_lookup "s:$state"
|
|
180
|
+
candidates+=("\${reply[@]}")
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
${
|
|
184
|
+
bash
|
|
185
|
+
? `for candidate in "\${candidates[@]}"; do
|
|
186
|
+
[[ "$candidate" == "$cur"* ]] && COMPREPLY+=("$prefix$candidate")
|
|
187
|
+
done
|
|
188
|
+
if [[ \${#candidates[@]} == 0 && "$cur" != -* ]]; then
|
|
189
|
+
while IFS= read -r candidate; do COMPREPLY+=("$prefix$candidate"); done < <(compgen -f -- "$cur")
|
|
190
|
+
fi
|
|
191
|
+
# Readline replaces only the value when '=' is a word break.
|
|
192
|
+
if [[ -n "$prefix" && "$COMP_WORDBREAKS" == *'='* ]]; then
|
|
193
|
+
for ((i=0; i<\${#COMPREPLY[@]}; i++)); do COMPREPLY[i]="\${COMPREPLY[i]#*=}"; done
|
|
194
|
+
fi`
|
|
195
|
+
: `if (( \${#candidates[@]} )); then
|
|
196
|
+
[[ -z "$prefix" ]] || compset -P '*='
|
|
197
|
+
compadd -- "\${candidates[@]}"
|
|
198
|
+
elif [[ "$cur" != -* ]]; then
|
|
199
|
+
[[ -z "$prefix" ]] || compset -P '*='
|
|
200
|
+
_files
|
|
201
|
+
fi`
|
|
202
|
+
}
|
|
203
|
+
return 0
|
|
204
|
+
}
|
|
205
|
+
${bash ? `complete -o filenames -F ${name} -- '${binary}'` : `compdef ${name} '${binary}'`}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function fishScript(name: string, binary: string, cases: string): string {
|
|
210
|
+
// Fish's no-scope-shadowing allows the helper to update caller-local reply/mode.
|
|
211
|
+
const lookup = (key: string) => `${name}_lookup ${key}`;
|
|
212
|
+
return `function ${name}_lookup --no-scope-shadowing
|
|
213
|
+
set mode ''; set reply
|
|
214
|
+
switch "$argv[1]"
|
|
215
|
+
${cases}
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
function ${name}
|
|
219
|
+
set -l tokens (commandline -opc)
|
|
220
|
+
set -e tokens[1]
|
|
221
|
+
set -l cur (commandline -ct)
|
|
222
|
+
set -l state 0
|
|
223
|
+
set -l pos 0
|
|
224
|
+
set -l ended 0
|
|
225
|
+
set -l pending ''
|
|
226
|
+
set -l mode ''
|
|
227
|
+
set -l reply
|
|
228
|
+
set -l candidates
|
|
229
|
+
set -l prefix ''
|
|
230
|
+
for word in $tokens
|
|
231
|
+
if test -n "$pending"
|
|
232
|
+
set pending ''; continue
|
|
233
|
+
end
|
|
234
|
+
if test $ended = 0; and test "$word" = --
|
|
235
|
+
set ended 1; continue
|
|
236
|
+
end
|
|
237
|
+
if test $ended = 0; and string match -q -- '-*' "$word"
|
|
238
|
+
set -l flag (string split -m 1 '=' -- "$word")[1]
|
|
239
|
+
${lookup('"f:$state:$flag"')}
|
|
240
|
+
if test "$mode" = value; and not string match -q '*=*' -- "$word"
|
|
241
|
+
set pending "f:$state:$word"
|
|
242
|
+
end
|
|
243
|
+
continue
|
|
244
|
+
end
|
|
245
|
+
if test $ended = 0; and test $pos = 0
|
|
246
|
+
${lookup('"c:$state:$word"')}
|
|
247
|
+
if test -n "$mode"
|
|
248
|
+
set state $mode; continue
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
${lookup('"a:$state:$pos"')}
|
|
252
|
+
if test "$mode" != repeat; set pos (math $pos + 1); end
|
|
253
|
+
end
|
|
254
|
+
if test -n "$pending"
|
|
255
|
+
${lookup('"$pending"')}
|
|
256
|
+
set candidates $reply
|
|
257
|
+
else if test $ended = 0; and string match -q -- '--*=*' "$cur"
|
|
258
|
+
set -l pair (string split -m 1 '=' -- "$cur")
|
|
259
|
+
set prefix "$pair[1]="
|
|
260
|
+
set cur "$pair[2]"
|
|
261
|
+
${lookup('"f:$state:$pair[1]"')}
|
|
262
|
+
set candidates $reply
|
|
263
|
+
else if test $ended = 0; and string match -q -- '-*' "$cur"
|
|
264
|
+
${lookup('"o:$state"')}
|
|
265
|
+
set candidates $reply
|
|
266
|
+
else
|
|
267
|
+
${lookup('"a:$state:$pos"')}
|
|
268
|
+
set candidates $reply
|
|
269
|
+
if test $ended = 0; and test $pos = 0
|
|
270
|
+
${lookup('"s:$state"')}
|
|
271
|
+
set -a candidates $reply
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
for candidate in $candidates
|
|
275
|
+
printf '%s\\n' "$prefix$candidate"
|
|
276
|
+
end
|
|
277
|
+
if test (count $candidates) = 0; and not string match -q -- '-*' "$cur"
|
|
278
|
+
for candidate in (__fish_complete_path "$cur")
|
|
279
|
+
printf '%s\\n' "$prefix$candidate"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
complete -c '${binary}' -f -a '(${name})'
|
|
284
|
+
`;
|
|
285
|
+
}
|
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { writeOpenCliDocument } from './docgen.js';
|
|
2
|
+
import type { OpenCliDocument } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** Hidden subcommand upstream OpenCLI adapters use for machine discovery. */
|
|
5
|
+
export const OPENCLI_DISCOVERY_COMMAND = '__opencli' as const;
|
|
6
|
+
|
|
7
|
+
/** Sentinel returned by {@link parseOutArg} for argv this module doesn't recognize. */
|
|
8
|
+
const INVALID = Symbol('invalid');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse `-o <file>` / `--out <file>` / `--out=<file>` from the args following `__opencli`,
|
|
12
|
+
* matching upstream OpenCLI's `ocobra` adapter flag. Returns `undefined` for no args (write to
|
|
13
|
+
* stdout), the file path for a recognized flag, or {@link INVALID} for anything else.
|
|
14
|
+
*/
|
|
15
|
+
function parseOutArg(args: readonly string[]): string | undefined | typeof INVALID {
|
|
16
|
+
if (args.length === 0) return undefined;
|
|
17
|
+
if (args.length === 1) {
|
|
18
|
+
const match = /^(?:-o|--out)=(.+)$/.exec(args[0]!);
|
|
19
|
+
return match ? match[1]! : INVALID;
|
|
20
|
+
}
|
|
21
|
+
if (args.length === 2 && (args[0] === '-o' || args[0] === '--out')) return args[1];
|
|
22
|
+
return INVALID;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* If argv requests the OpenCLI document (`__opencli` subcommand, optionally with upstream's
|
|
27
|
+
* `-o`/`--out <file>` flag), write it — JSON, 2-space indented, single trailing newline — to that
|
|
28
|
+
* file or to stdout, and return true. Otherwise return false without writing anything, so the
|
|
29
|
+
* caller can continue parsing argv as usual.
|
|
30
|
+
*/
|
|
31
|
+
export async function handleOpenCliRequest(
|
|
32
|
+
argv: readonly string[],
|
|
33
|
+
document: () => OpenCliDocument,
|
|
34
|
+
write: (chunk: string) => void = (chunk) => process.stdout.write(chunk),
|
|
35
|
+
): Promise<boolean> {
|
|
36
|
+
if (argv[0] !== OPENCLI_DISCOVERY_COMMAND) return false;
|
|
37
|
+
const outFile = parseOutArg(argv.slice(1));
|
|
38
|
+
if (outFile === INVALID) return false;
|
|
39
|
+
if (outFile === undefined) {
|
|
40
|
+
write(`${JSON.stringify(document(), null, 2)}\n`);
|
|
41
|
+
} else {
|
|
42
|
+
await writeOpenCliDocument(document(), outFile);
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
package/src/docgen.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
3
|
+
import { renderMarkdown } from './index.js';
|
|
4
|
+
import type { InfoObject, OpenCliDocument } from './types.js';
|
|
5
|
+
|
|
6
|
+
/** The minimal shape of `package.json` used to derive an {@link InfoObject}. */
|
|
7
|
+
export type PackageJsonLike = {
|
|
8
|
+
name?: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
bin?: string | Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Strip a npm scope (`@scope/name` -> `name`) for use as a default binary name. */
|
|
15
|
+
function unscopedName(name: string): string {
|
|
16
|
+
return name.replace(/^@[^/]+\//, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Derive an {@link InfoObject} from a parsed `package.json`, so adapters don't each need their
|
|
21
|
+
* own `title`/`binary`/`version` bookkeeping. `overrides` wins over anything derived from `pkg`,
|
|
22
|
+
* and is required for whatever `pkg` cannot express (e.g. a package that exposes several binaries).
|
|
23
|
+
*/
|
|
24
|
+
export function infoFromPackageJson(pkg: PackageJsonLike, overrides: Partial<InfoObject> = {}): InfoObject {
|
|
25
|
+
const binary =
|
|
26
|
+
overrides.binary ??
|
|
27
|
+
(typeof pkg.bin === 'object' && pkg.bin !== null ? Object.keys(pkg.bin)[0] : undefined) ??
|
|
28
|
+
(pkg.name ? unscopedName(pkg.name) : undefined);
|
|
29
|
+
if (!binary) throw new Error('infoFromPackageJson: could not determine a binary name; pass overrides.binary');
|
|
30
|
+
const version = overrides.version ?? pkg.version;
|
|
31
|
+
if (!version) throw new Error('infoFromPackageJson: package.json has no version; pass overrides.version');
|
|
32
|
+
const title = overrides.title ?? pkg.name ?? binary;
|
|
33
|
+
const info: InfoObject = { ...overrides, title, binary, version };
|
|
34
|
+
if (info.summary === undefined && pkg.description !== undefined) info.summary = pkg.description;
|
|
35
|
+
return info;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Output format for {@link writeOpenCliDocument}. */
|
|
39
|
+
export type DocumentFormat = 'json' | 'yaml' | 'markdown';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Render `document` (JSON, YAML, or Markdown) and write it to `output`, or to stdout if `output`
|
|
43
|
+
* is omitted. Shared by every adapter's docgen command and by `handleOpenCliRequest`.
|
|
44
|
+
*/
|
|
45
|
+
export async function writeOpenCliDocument(
|
|
46
|
+
document: OpenCliDocument,
|
|
47
|
+
output?: string,
|
|
48
|
+
format: DocumentFormat = 'json',
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const content =
|
|
51
|
+
format === 'markdown'
|
|
52
|
+
? renderMarkdown(document)
|
|
53
|
+
: format === 'yaml'
|
|
54
|
+
? stringifyYaml(document)
|
|
55
|
+
: `${JSON.stringify(document, null, 2)}\n`;
|
|
56
|
+
if (output === undefined) {
|
|
57
|
+
process.stdout.write(content);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await writeFile(output, content);
|
|
61
|
+
}
|