-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathautomd.config.ts
More file actions
243 lines (205 loc) · 6.11 KB
/
Copy pathautomd.config.ts
File metadata and controls
243 lines (205 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { defineGenerator } from 'automd'
import { bold, code, codeblock, h2, h3, type MarkdownEntry, p, tsMarkdown, ul } from 'ts-markdown'
type Option = {
preferredName: string
nameSet: string[]
definition: string
description: string
required: boolean
}
type Command = {
path: string
usage: string
description: string
details: string
examples: Array<string[]>
options: Option[]
}
const execFileP = promisify(execFile)
/**
* Runs the CLI binary with `--clipanion=definitions` flag to get command definitions
*/
async function runDefinitions(binPath: string) {
try {
const { stdout } = await execFileP('node', [binPath, '--clipanion=definitions'], {
maxBuffer: 10 * 1024 * 1024,
})
return stdout
} catch (err) {
throw new Error(`Failed to run CLI binary: ${err instanceof Error ? err.message : String(err)}`)
}
}
/**
* Safely parses JSON text with error recovery
*/
function safeParseJson(text: string): Command[] {
const trimmed = text.trim()
if (!trimmed) return []
try {
return JSON.parse(trimmed)
} catch {
throw new Error('Failed to parse JSON output from CLI')
}
}
/**
* Formats option names from nameSet or fallback values
*/
function formatOptionNames(option: Option) {
if (Array.isArray(option.nameSet) && option.nameSet.length) {
return option.nameSet.join(', ')
}
return option.preferredName || option.definition || ''
}
/**
* Normalizes description text by removing extra whitespace
*/
function normalizeDescription(description: string) {
return description ? description.replace(/\s+/g, ' ').trim() : ''
}
/**
* Safely trims a string, returning empty string if falsy
*/
function safeTrim(text: string | undefined | null): string {
return text ? text.trim() : ''
}
/**
* Creates a code block entry using ts-markdown
*/
function createCodeBlockEntry(content: string, language = 'sh') {
return codeblock(content.split('\n'), {
fenced: '`',
language,
})
}
/**
* Safely iterates over an array-like property and applies a transform function
*/
function renderArraySection<T>(
items: T[] | undefined,
title: string,
renderItem: (item: T) => MarkdownEntry | MarkdownEntry[]
): MarkdownEntry[] {
if (!Array.isArray(items) || !items.length) return []
const entries: MarkdownEntry[] = [p(bold(title))]
for (const item of items) {
const rendered = renderItem(item)
if (Array.isArray(rendered)) {
entries.push(...rendered)
} else {
entries.push(rendered)
}
}
return entries
}
/**
* Renders the usage section for a command
*/
function renderUsageSection(command: Command) {
const usage = safeTrim(command.usage)
if (!usage) return []
return [createCodeBlockEntry(usage)]
}
/**
* Renders the description sections (description and details) for a command
*/
function renderDescriptionSections(command: Command) {
const entries: MarkdownEntry[] = []
const description = safeTrim(command.description)
if (description) entries.push(p(description))
const details = safeTrim(command.details)
if (details) entries.push(p(details))
return entries
}
/**
* Renders a single example entry
*/
function renderExample(example: string[] | string) {
if (Array.isArray(example) && example.length >= 2) {
const description = safeTrim(String(example[0]))
const command = safeTrim(String(example[1]))
const commandEntry = createCodeBlockEntry(command)
const entries: MarkdownEntry[] = []
if (description) {
entries.push(ul([[description, commandEntry]]))
} else {
entries.push(commandEntry)
}
return entries
}
if (typeof example === 'string') {
const command = safeTrim(example)
return [createCodeBlockEntry(command)]
}
return []
}
/**
* Renders the examples section for a command
*/
function renderExamplesSection(command: Command) {
return renderArraySection(command.examples, 'Examples', renderExample)
}
/**
* Renders the options section for a command
*/
function renderOptionsSection(command: Command) {
if (!Array.isArray(command.options) || !command.options.length) return []
const entries = [p(bold('Options'))]
for (const option of command.options) {
const names = formatOptionNames(option)
const description = normalizeDescription(option.description)
const required = option.required ? 'Yes' : 'No'
if (names) {
const content = `- \`${names}\`${description ? ` — ${description}` : ''} Required: ${required}`
entries.push(p(content))
}
}
return entries
}
/**
* Renders a complete command definition using ts-markdown
*/
function renderCommand(command: Command) {
const entries: MarkdownEntry[] = []
// Command header
entries.push(h3(code(command.path)))
// Add all sections
entries.push(...renderUsageSection(command))
entries.push(...renderDescriptionSections(command))
entries.push(...renderExamplesSection(command))
entries.push(...renderOptionsSection(command))
return entries
}
/**
* Generates the complete Markdown using ts-markdown
*/
function generateMarkdown(commands: Command[]) {
const entries: MarkdownEntry[] = [h2('Commands')]
for (let i = 0; i < commands.length; i++) {
entries.push(...renderCommand(commands[i]))
if (i < commands.length - 1) {
entries.push('---')
}
}
return tsMarkdown(entries)
}
const cliCommands = defineGenerator({
name: 'cliCommands',
async generate({ args }) {
if (!args.bin) throw new Error('bin is required')
const rawOutput = await runDefinitions(args.bin)
const commands = safeParseJson(rawOutput)
const contents = generateMarkdown(commands)
return {
contents,
}
},
})
/** @type {import("automd").Config} */
export default {
file: 'README.md',
generators: {
cliCommands,
},
}