politty 0.8.0 → 0.9.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/dist/cli.js +1 -1
- package/dist/docs/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{runner-D43SkHt5.js → runner-APRZYXUS.js} +74 -3
- package/package.json +20 -65
- package/dist/arg-registry-DDJpsUea.d.cts +0 -942
- package/dist/arg-registry-DDJpsUea.d.cts.map +0 -1
- package/dist/arg-registry-DDJpsUea.d.ts.map +0 -1
- package/dist/augment.cjs +0 -0
- package/dist/augment.d.cts +0 -17
- package/dist/augment.d.cts.map +0 -1
- package/dist/augment.d.ts.map +0 -1
- package/dist/cli.cjs +0 -54
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/completion/index.cjs +0 -23
- package/dist/completion/index.d.cts +0 -3
- package/dist/completion-CLHO3Xaz.cjs +0 -5769
- package/dist/completion-CLHO3Xaz.cjs.map +0 -1
- package/dist/completion-DHnVx9Zk.js.map +0 -1
- package/dist/docs/index.cjs +0 -3127
- package/dist/docs/index.cjs.map +0 -1
- package/dist/docs/index.d.cts +0 -752
- package/dist/docs/index.d.cts.map +0 -1
- package/dist/docs/index.d.ts.map +0 -1
- package/dist/docs/index.js.map +0 -1
- package/dist/index-DKGn3lIl.d.ts.map +0 -1
- package/dist/index-WyViqW59.d.cts +0 -663
- package/dist/index-WyViqW59.d.cts.map +0 -1
- package/dist/index.cjs +0 -45
- package/dist/index.d.cts +0 -685
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/log-collector-DK32-73m.js.map +0 -1
- package/dist/log-collector-DUqC427m.cjs +0 -185
- package/dist/log-collector-DUqC427m.cjs.map +0 -1
- package/dist/prompt/clack/index.cjs +0 -33
- package/dist/prompt/clack/index.cjs.map +0 -1
- package/dist/prompt/clack/index.d.cts +0 -18
- package/dist/prompt/clack/index.d.cts.map +0 -1
- package/dist/prompt/clack/index.d.ts.map +0 -1
- package/dist/prompt/clack/index.js.map +0 -1
- package/dist/prompt/index.cjs +0 -7
- package/dist/prompt/index.d.cts +0 -108
- package/dist/prompt/index.d.cts.map +0 -1
- package/dist/prompt/index.d.ts.map +0 -1
- package/dist/prompt/inquirer/index.cjs +0 -48
- package/dist/prompt/inquirer/index.cjs.map +0 -1
- package/dist/prompt/inquirer/index.d.cts +0 -18
- package/dist/prompt/inquirer/index.d.cts.map +0 -1
- package/dist/prompt/inquirer/index.d.ts.map +0 -1
- package/dist/prompt/inquirer/index.js.map +0 -1
- package/dist/prompt-Bs9e-Em3.cjs +0 -196
- package/dist/prompt-Bs9e-Em3.cjs.map +0 -1
- package/dist/prompt-Cc8Tfmdv.js.map +0 -1
- package/dist/runner-D43SkHt5.js.map +0 -1
- package/dist/runner-DvFvokV6.cjs +0 -2865
- package/dist/runner-DvFvokV6.cjs.map +0 -1
- package/dist/schema-extractor-BxSRwLrx.cjs +0 -710
- package/dist/schema-extractor-BxSRwLrx.cjs.map +0 -1
- package/dist/schema-extractor-Dqe7_kyQ.js.map +0 -1
package/dist/docs/index.cjs
DELETED
|
@@ -1,3127 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_log_collector = require('../log-collector-DUqC427m.cjs');
|
|
3
|
-
const require_schema_extractor = require('../schema-extractor-BxSRwLrx.cjs');
|
|
4
|
-
let zod = require("zod");
|
|
5
|
-
let node_fs = require("node:fs");
|
|
6
|
-
node_fs = require_log_collector.__toESM(node_fs, 1);
|
|
7
|
-
let node_path = require("node:path");
|
|
8
|
-
node_path = require_log_collector.__toESM(node_path, 1);
|
|
9
|
-
let node_util = require("node:util");
|
|
10
|
-
|
|
11
|
-
//#region src/docs/types.ts
|
|
12
|
-
/**
|
|
13
|
-
* Environment variable name for update mode
|
|
14
|
-
*/
|
|
15
|
-
const UPDATE_GOLDEN_ENV = "POLITTY_DOCS_UPDATE";
|
|
16
|
-
/**
|
|
17
|
-
* Environment variable name for doctor mode.
|
|
18
|
-
* When enabled alone, detects and reports missing section markers (read-only).
|
|
19
|
-
* When combined with POLITTY_DOCS_UPDATE=true, auto-inserts missing markers.
|
|
20
|
-
*/
|
|
21
|
-
const DOCTOR_ENV = "POLITTY_DOCS_DOCTOR";
|
|
22
|
-
/**
|
|
23
|
-
* All section types in rendering order
|
|
24
|
-
*/
|
|
25
|
-
const SECTION_TYPES = [
|
|
26
|
-
"heading",
|
|
27
|
-
"description",
|
|
28
|
-
"usage",
|
|
29
|
-
"arguments",
|
|
30
|
-
"options",
|
|
31
|
-
"global-options-link",
|
|
32
|
-
"subcommands",
|
|
33
|
-
"examples",
|
|
34
|
-
"notes"
|
|
35
|
-
];
|
|
36
|
-
/**
|
|
37
|
-
* Marker prefix for command section markers in generated documentation
|
|
38
|
-
* Format: <!-- politty:command:<scope>:<type>:start --> ... <!-- politty:command:<scope>:<type>:end -->
|
|
39
|
-
*/
|
|
40
|
-
const SECTION_MARKER_PREFIX = "politty:command";
|
|
41
|
-
/**
|
|
42
|
-
* Generate start marker for a command section
|
|
43
|
-
*/
|
|
44
|
-
function sectionStartMarker(type, scope) {
|
|
45
|
-
return `<!-- ${SECTION_MARKER_PREFIX}:${scope}:${type}:start -->`;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Generate end marker for a command section
|
|
49
|
-
*/
|
|
50
|
-
function sectionEndMarker(type, scope) {
|
|
51
|
-
return `<!-- ${SECTION_MARKER_PREFIX}:${scope}:${type}:end -->`;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Marker prefix for global options sections in generated documentation
|
|
55
|
-
* Format: <!-- politty:global-options:start --> ... <!-- politty:global-options:end -->
|
|
56
|
-
*/
|
|
57
|
-
const GLOBAL_OPTIONS_MARKER_PREFIX = "politty:global-options";
|
|
58
|
-
/**
|
|
59
|
-
* Generate start marker for a global options section
|
|
60
|
-
*/
|
|
61
|
-
function globalOptionsStartMarker() {
|
|
62
|
-
return `<!-- ${GLOBAL_OPTIONS_MARKER_PREFIX}:start -->`;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Generate end marker for a global options section
|
|
66
|
-
*/
|
|
67
|
-
function globalOptionsEndMarker() {
|
|
68
|
-
return `<!-- ${GLOBAL_OPTIONS_MARKER_PREFIX}:end -->`;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Marker prefix for root header sections in generated documentation
|
|
72
|
-
*/
|
|
73
|
-
const ROOT_HEADER_MARKER_PREFIX = "politty:root-header";
|
|
74
|
-
function rootHeaderStartMarker() {
|
|
75
|
-
return `<!-- ${ROOT_HEADER_MARKER_PREFIX}:start -->`;
|
|
76
|
-
}
|
|
77
|
-
function rootHeaderEndMarker() {
|
|
78
|
-
return `<!-- ${ROOT_HEADER_MARKER_PREFIX}:end -->`;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Marker prefix for root footer sections in generated documentation
|
|
82
|
-
*/
|
|
83
|
-
const ROOT_FOOTER_MARKER_PREFIX = "politty:root-footer";
|
|
84
|
-
function rootFooterStartMarker() {
|
|
85
|
-
return `<!-- ${ROOT_FOOTER_MARKER_PREFIX}:start -->`;
|
|
86
|
-
}
|
|
87
|
-
function rootFooterEndMarker() {
|
|
88
|
-
return `<!-- ${ROOT_FOOTER_MARKER_PREFIX}:end -->`;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Marker prefix for index sections in generated documentation
|
|
92
|
-
* Format: <!-- politty:index:<scope>:start --> ... <!-- politty:index:<scope>:end -->
|
|
93
|
-
*/
|
|
94
|
-
const INDEX_MARKER_PREFIX = "politty:index";
|
|
95
|
-
/**
|
|
96
|
-
* Generate start marker for an index section
|
|
97
|
-
*/
|
|
98
|
-
function indexStartMarker(scope) {
|
|
99
|
-
return `<!-- ${INDEX_MARKER_PREFIX}:${scope}:start -->`;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Generate end marker for an index section
|
|
103
|
-
*/
|
|
104
|
-
function indexEndMarker(scope) {
|
|
105
|
-
return `<!-- ${INDEX_MARKER_PREFIX}:${scope}:end -->`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
//#endregion
|
|
109
|
-
//#region src/docs/default-renderers.ts
|
|
110
|
-
/**
|
|
111
|
-
* Escape markdown special characters in table cells
|
|
112
|
-
*/
|
|
113
|
-
function escapeTableCell$2(str) {
|
|
114
|
-
return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Marker appended to a custom negation row/line so readers can see which
|
|
118
|
-
* positive flag it negates (e.g. `--monochrome` → `(↔ \`--color\`)`).
|
|
119
|
-
*/
|
|
120
|
-
function negationRelationMarker(opt) {
|
|
121
|
-
return `(↔ \`--${opt.cliName}\`)`;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Format default value for display
|
|
125
|
-
*/
|
|
126
|
-
function formatDefaultValue$1(value) {
|
|
127
|
-
if (value === void 0) return "-";
|
|
128
|
-
return `\`${JSON.stringify(value)}\``;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Render usage line
|
|
132
|
-
*/
|
|
133
|
-
function renderUsage(info) {
|
|
134
|
-
const parts = [info.fullCommandPath];
|
|
135
|
-
if (info.options.length > 0) parts.push("[options]");
|
|
136
|
-
if (info.subCommands.length > 0) parts.push("[command]");
|
|
137
|
-
for (const arg of info.positionalArgs) if (arg.required) parts.push(`<${arg.name}>`);
|
|
138
|
-
else parts.push(`[${arg.name}]`);
|
|
139
|
-
return parts.join(" ");
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Render arguments as table
|
|
143
|
-
*/
|
|
144
|
-
function renderArgumentsTable(info) {
|
|
145
|
-
if (info.positionalArgs.length === 0) return "";
|
|
146
|
-
const lines = [];
|
|
147
|
-
lines.push("| Argument | Description | Required |");
|
|
148
|
-
lines.push("|----------|-------------|----------|");
|
|
149
|
-
for (const arg of info.positionalArgs) {
|
|
150
|
-
const desc = escapeTableCell$2(arg.description ?? "");
|
|
151
|
-
const required = arg.required ? "Yes" : "No";
|
|
152
|
-
lines.push(`| \`${arg.name}\` | ${desc} | ${required} |`);
|
|
153
|
-
}
|
|
154
|
-
return lines.join("\n");
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Render arguments as list
|
|
158
|
-
*/
|
|
159
|
-
function renderArgumentsList(info) {
|
|
160
|
-
if (info.positionalArgs.length === 0) return "";
|
|
161
|
-
const lines = [];
|
|
162
|
-
for (const arg of info.positionalArgs) {
|
|
163
|
-
const required = arg.required ? "(required)" : "(optional)";
|
|
164
|
-
const desc = arg.description ? ` - ${arg.description}` : "";
|
|
165
|
-
lines.push(`- \`${arg.name}\`${desc} ${required}`);
|
|
166
|
-
}
|
|
167
|
-
return lines.join("\n");
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Format environment variable info for display
|
|
171
|
-
*/
|
|
172
|
-
function formatEnvInfo(env) {
|
|
173
|
-
if (!env) return "";
|
|
174
|
-
return ` [env: ${(Array.isArray(env) ? env : [env]).join(", ")}]`;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Resolve placeholder for an option (uses kebab-case cliName)
|
|
178
|
-
*/
|
|
179
|
-
function resolvePlaceholder(opt) {
|
|
180
|
-
return opt.placeholder ?? opt.cliName.toUpperCase().replace(/-/g, "_");
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Format option name for table display (e.g., `--dry-run` or `--port <PORT>`)
|
|
184
|
-
*
|
|
185
|
-
* Boolean fields with a custom inline `negation` (no separate description) are
|
|
186
|
-
* shown as `\`--cache\` / \`--disable-cache\``.
|
|
187
|
-
*/
|
|
188
|
-
function formatOptionName(opt) {
|
|
189
|
-
const placeholder = resolvePlaceholder(opt);
|
|
190
|
-
if (opt.type === "boolean") {
|
|
191
|
-
const positive = `\`--${opt.cliName}\``;
|
|
192
|
-
if (opt.negationDisplay && !opt.negationDescription) return `${positive} / \`--${opt.negationDisplay}\``;
|
|
193
|
-
return positive;
|
|
194
|
-
}
|
|
195
|
-
return `\`--${opt.cliName} <${placeholder}>\``;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Format option flags for list display (uses kebab-case cliName).
|
|
199
|
-
* Aliases are joined with `, `; the inline negation (when no separate
|
|
200
|
-
* `negationDescription` is set) is appended with ` / ` so it stays
|
|
201
|
-
* visually distinct from aliases, matching help and table output.
|
|
202
|
-
*/
|
|
203
|
-
function formatOptionFlags(opt) {
|
|
204
|
-
const placeholder = resolvePlaceholder(opt);
|
|
205
|
-
const longFlag = opt.type === "boolean" ? `--${opt.cliName}` : `--${opt.cliName} <${placeholder}>`;
|
|
206
|
-
const parts = [];
|
|
207
|
-
if (opt.alias) {
|
|
208
|
-
for (const a of opt.alias) if (a.length === 1) parts.push(`\`-${a}\``);
|
|
209
|
-
}
|
|
210
|
-
parts.push(`\`${longFlag}\``);
|
|
211
|
-
if (opt.alias) {
|
|
212
|
-
for (const a of opt.alias) if (a.length > 1) parts.push(`\`--${a}\``);
|
|
213
|
-
}
|
|
214
|
-
const aliasJoined = parts.join(", ");
|
|
215
|
-
if (opt.type === "boolean" && opt.negationDisplay && !opt.negationDescription) return `${aliasJoined} / \`--${opt.negationDisplay}\``;
|
|
216
|
-
return aliasJoined;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Format aliases for a markdown table cell
|
|
220
|
-
*/
|
|
221
|
-
function formatAliasCell(alias) {
|
|
222
|
-
if (!alias || alias.length === 0) return "-";
|
|
223
|
-
return alias.map((a) => `\`${a.length === 1 ? `-${a}` : `--${a}`}\``).join(", ");
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Format env variable names for table display
|
|
227
|
-
*/
|
|
228
|
-
function formatEnvNames(env) {
|
|
229
|
-
if (!env) return "-";
|
|
230
|
-
if (Array.isArray(env)) return env.map((e) => `\`${e}\``).join(", ");
|
|
231
|
-
return `\`${env}\``;
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Render options as markdown table
|
|
235
|
-
*
|
|
236
|
-
* Features:
|
|
237
|
-
* - Uses kebab-case (cliName) for option names (e.g., `--dry-run` instead of `--dryRun`)
|
|
238
|
-
* - Automatically adds Env column when any option has env configured
|
|
239
|
-
* - Displays multiple env vars as comma-separated list
|
|
240
|
-
*
|
|
241
|
-
* @example
|
|
242
|
-
* | Option | Alias | Description | Required | Default | Env |
|
|
243
|
-
* |--------|-------|-------------|----------|---------|-----|
|
|
244
|
-
* | `--dry-run` | `-d` | Dry run mode | No | `false` | - |
|
|
245
|
-
* | `--port <PORT>` | - | Server port | Yes | - | `PORT`, `SERVER_PORT` |
|
|
246
|
-
*/
|
|
247
|
-
function renderOptionsTable(info) {
|
|
248
|
-
if (info.options.length === 0) return "";
|
|
249
|
-
const hasEnv = info.options.some((opt) => opt.env);
|
|
250
|
-
const lines = [];
|
|
251
|
-
if (hasEnv) {
|
|
252
|
-
lines.push("| Option | Alias | Description | Required | Default | Env |");
|
|
253
|
-
lines.push("|--------|-------|-------------|----------|---------|-----|");
|
|
254
|
-
} else {
|
|
255
|
-
lines.push("| Option | Alias | Description | Required | Default |");
|
|
256
|
-
lines.push("|--------|-------|-------------|----------|---------|");
|
|
257
|
-
}
|
|
258
|
-
for (const opt of info.options) {
|
|
259
|
-
const optionName = formatOptionName(opt);
|
|
260
|
-
const alias = formatAliasCell(opt.alias);
|
|
261
|
-
const desc = escapeTableCell$2(opt.description ?? "");
|
|
262
|
-
const required = opt.required ? "Yes" : "No";
|
|
263
|
-
const defaultVal = formatDefaultValue$1(opt.defaultValue);
|
|
264
|
-
if (hasEnv) {
|
|
265
|
-
const envNames = formatEnvNames(opt.env);
|
|
266
|
-
lines.push(`| ${optionName} | ${alias} | ${desc} | ${required} | ${defaultVal} | ${envNames} |`);
|
|
267
|
-
} else lines.push(`| ${optionName} | ${alias} | ${desc} | ${required} | ${defaultVal} |`);
|
|
268
|
-
if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) {
|
|
269
|
-
const negName = `\`--${opt.negationDisplay}\``;
|
|
270
|
-
const negDesc = `${escapeTableCell$2(opt.negationDescription)} ${negationRelationMarker(opt)}`;
|
|
271
|
-
if (hasEnv) lines.push(`| ${negName} | - | ${negDesc} | ${required} | - | - |`);
|
|
272
|
-
else lines.push(`| ${negName} | - | ${negDesc} | ${required} | - |`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return lines.join("\n");
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Render options as markdown list
|
|
279
|
-
*
|
|
280
|
-
* Features:
|
|
281
|
-
* - Uses kebab-case (cliName) for option names (e.g., `--dry-run` instead of `--dryRun`)
|
|
282
|
-
* - Appends env info at the end of each option (e.g., `[env: PORT, SERVER_PORT]`)
|
|
283
|
-
*
|
|
284
|
-
* @example
|
|
285
|
-
* - `-d`, `--dry-run` - Dry run mode (default: false)
|
|
286
|
-
* - `--port <PORT>` - Server port (required) [env: PORT, SERVER_PORT]
|
|
287
|
-
*/
|
|
288
|
-
function renderOptionsList(info) {
|
|
289
|
-
if (info.options.length === 0) return "";
|
|
290
|
-
const lines = [];
|
|
291
|
-
for (const opt of info.options) {
|
|
292
|
-
const flags = formatOptionFlags(opt);
|
|
293
|
-
const desc = opt.description ? ` - ${opt.description}` : "";
|
|
294
|
-
const required = opt.required ? " (required)" : "";
|
|
295
|
-
const defaultVal = opt.defaultValue !== void 0 ? ` (default: ${JSON.stringify(opt.defaultValue)})` : "";
|
|
296
|
-
const envInfo = formatEnvInfo(opt.env);
|
|
297
|
-
lines.push(`- ${flags}${desc}${required}${defaultVal}${envInfo}`);
|
|
298
|
-
if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) lines.push(`- \`--${opt.negationDisplay}\` - ${opt.negationDescription} ${negationRelationMarker(opt)}`);
|
|
299
|
-
}
|
|
300
|
-
return lines.join("\n");
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Generate anchor from command path
|
|
304
|
-
*/
|
|
305
|
-
function generateAnchor$1(commandPath) {
|
|
306
|
-
return commandPath.join("-").toLowerCase();
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Generate relative path from one file to another.
|
|
310
|
-
* Always emits forward slashes so Markdown links remain portable across OSes.
|
|
311
|
-
*/
|
|
312
|
-
function getRelativePath(from, to) {
|
|
313
|
-
const fromPosix = from.replace(/\\/g, "/");
|
|
314
|
-
const toPosix = to.replace(/\\/g, "/");
|
|
315
|
-
return node_path.default.posix.relative(node_path.default.posix.dirname(fromPosix), toPosix);
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Render subcommands as table
|
|
319
|
-
*/
|
|
320
|
-
function renderSubcommandsTable(info, generateAnchors = true) {
|
|
321
|
-
return renderSubcommandsTableFromArray(info.subCommands, info, generateAnchors);
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Render options from array as table
|
|
325
|
-
*/
|
|
326
|
-
function renderOptionsTableFromArray(options) {
|
|
327
|
-
if (options.length === 0) return "";
|
|
328
|
-
const hasEnv = options.some((opt) => opt.env);
|
|
329
|
-
const lines = [];
|
|
330
|
-
if (hasEnv) {
|
|
331
|
-
lines.push("| Option | Alias | Description | Required | Default | Env |");
|
|
332
|
-
lines.push("|--------|-------|-------------|----------|---------|-----|");
|
|
333
|
-
} else {
|
|
334
|
-
lines.push("| Option | Alias | Description | Required | Default |");
|
|
335
|
-
lines.push("|--------|-------|-------------|----------|---------|");
|
|
336
|
-
}
|
|
337
|
-
for (const opt of options) {
|
|
338
|
-
const optionName = formatOptionName(opt);
|
|
339
|
-
const alias = formatAliasCell(opt.alias);
|
|
340
|
-
const desc = escapeTableCell$2(opt.description ?? "");
|
|
341
|
-
const required = opt.required ? "Yes" : "No";
|
|
342
|
-
const defaultVal = formatDefaultValue$1(opt.defaultValue);
|
|
343
|
-
if (hasEnv) {
|
|
344
|
-
const envNames = formatEnvNames(opt.env);
|
|
345
|
-
lines.push(`| ${optionName} | ${alias} | ${desc} | ${required} | ${defaultVal} | ${envNames} |`);
|
|
346
|
-
} else lines.push(`| ${optionName} | ${alias} | ${desc} | ${required} | ${defaultVal} |`);
|
|
347
|
-
if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) {
|
|
348
|
-
const negName = `\`--${opt.negationDisplay}\``;
|
|
349
|
-
const negDesc = `${escapeTableCell$2(opt.negationDescription)} ${negationRelationMarker(opt)}`;
|
|
350
|
-
if (hasEnv) lines.push(`| ${negName} | - | ${negDesc} | ${required} | - | - |`);
|
|
351
|
-
else lines.push(`| ${negName} | - | ${negDesc} | ${required} | - |`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return lines.join("\n");
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Render union/xor options as markdown with variant grouping
|
|
358
|
-
*/
|
|
359
|
-
function renderUnionOptionsMarkdown(extracted, style = "table") {
|
|
360
|
-
const unionOptions = extracted.unionOptions ?? [];
|
|
361
|
-
if (unionOptions.length === 0) return "";
|
|
362
|
-
const sections = [];
|
|
363
|
-
const allFieldNames = /* @__PURE__ */ new Set();
|
|
364
|
-
for (const option of unionOptions) for (const field of option.fields) allFieldNames.add(field.name);
|
|
365
|
-
const commonFieldNames = /* @__PURE__ */ new Set();
|
|
366
|
-
for (const fieldName of allFieldNames) if (unionOptions.every((o) => o.fields.some((f) => f.name === fieldName))) commonFieldNames.add(fieldName);
|
|
367
|
-
const commonFields = extracted.fields.filter((f) => commonFieldNames.has(f.name) && !f.positional);
|
|
368
|
-
if (commonFields.length > 0) sections.push(style === "table" ? renderOptionsTableFromArray(commonFields) : renderOptionsListFromArray(commonFields));
|
|
369
|
-
sections.push("> One of the following option groups is required:");
|
|
370
|
-
for (let i = 0; i < unionOptions.length; i++) {
|
|
371
|
-
const option = unionOptions[i];
|
|
372
|
-
if (!option) continue;
|
|
373
|
-
const uniqueFields = option.fields.filter((f) => !commonFieldNames.has(f.name) && !f.positional);
|
|
374
|
-
const label = option.description ?? `Variant ${i + 1}`;
|
|
375
|
-
if (uniqueFields.length === 0) {
|
|
376
|
-
sections.push(`**${label}:**\n\n_no options_`);
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
const rendered = style === "table" ? renderOptionsTableFromArray(uniqueFields) : renderOptionsListFromArray(uniqueFields);
|
|
380
|
-
sections.push(`**${label}:**\n\n${rendered}`);
|
|
381
|
-
}
|
|
382
|
-
return sections.join("\n\n");
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Render discriminatedUnion options as markdown with variant grouping
|
|
386
|
-
*/
|
|
387
|
-
function renderDiscriminatedUnionOptionsMarkdown(extracted, style = "table") {
|
|
388
|
-
const discriminator = extracted.discriminator;
|
|
389
|
-
const variants = extracted.variants ?? [];
|
|
390
|
-
if (!discriminator || variants.length === 0) return "";
|
|
391
|
-
const sections = [];
|
|
392
|
-
const allFieldNames = /* @__PURE__ */ new Set();
|
|
393
|
-
for (const variant of variants) for (const field of variant.fields) allFieldNames.add(field.name);
|
|
394
|
-
const commonFieldNames = /* @__PURE__ */ new Set();
|
|
395
|
-
for (const fieldName of allFieldNames) {
|
|
396
|
-
if (fieldName === discriminator) continue;
|
|
397
|
-
if (variants.every((v) => v.fields.some((f) => f.name === fieldName))) commonFieldNames.add(fieldName);
|
|
398
|
-
}
|
|
399
|
-
const discriminatorField = extracted.fields.find((f) => f.name === discriminator);
|
|
400
|
-
const variantValues = variants.map((v) => v.discriminatorValue).join("\\|");
|
|
401
|
-
const topFields = [];
|
|
402
|
-
if (discriminatorField) topFields.push({
|
|
403
|
-
...discriminatorField,
|
|
404
|
-
placeholder: variantValues
|
|
405
|
-
});
|
|
406
|
-
for (const fieldName of commonFieldNames) {
|
|
407
|
-
const field = extracted.fields.find((f) => f.name === fieldName);
|
|
408
|
-
if (field && !field.positional) topFields.push(field);
|
|
409
|
-
}
|
|
410
|
-
if (topFields.length > 0) sections.push(style === "table" ? renderOptionsTableFromArray(topFields) : renderOptionsListFromArray(topFields));
|
|
411
|
-
for (const variant of variants) {
|
|
412
|
-
const uniqueFields = variant.fields.filter((f) => f.name !== discriminator && !commonFieldNames.has(f.name) && !f.positional);
|
|
413
|
-
if (uniqueFields.length === 0) continue;
|
|
414
|
-
const descSuffix = variant.description ? ` ${variant.description}` : "";
|
|
415
|
-
const label = `**When \`${discriminator}\` = \`${variant.discriminatorValue}\`:**${descSuffix}`;
|
|
416
|
-
const rendered = style === "table" ? renderOptionsTableFromArray(uniqueFields) : renderOptionsListFromArray(uniqueFields);
|
|
417
|
-
sections.push(`${label}\n\n${rendered}`);
|
|
418
|
-
}
|
|
419
|
-
return sections.join("\n\n");
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Render options from array as list
|
|
423
|
-
*/
|
|
424
|
-
function renderOptionsListFromArray(options) {
|
|
425
|
-
if (options.length === 0) return "";
|
|
426
|
-
const lines = [];
|
|
427
|
-
for (const opt of options) {
|
|
428
|
-
const flags = formatOptionFlags(opt);
|
|
429
|
-
const desc = opt.description ? ` - ${opt.description}` : "";
|
|
430
|
-
const required = opt.required ? " (required)" : "";
|
|
431
|
-
const defaultVal = opt.defaultValue !== void 0 ? ` (default: ${JSON.stringify(opt.defaultValue)})` : "";
|
|
432
|
-
const envInfo = formatEnvInfo(opt.env);
|
|
433
|
-
lines.push(`- ${flags}${desc}${required}${defaultVal}${envInfo}`);
|
|
434
|
-
if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) lines.push(`- \`--${opt.negationDisplay}\` - ${opt.negationDescription} ${negationRelationMarker(opt)}`);
|
|
435
|
-
}
|
|
436
|
-
return lines.join("\n");
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Render arguments from array as table
|
|
440
|
-
*/
|
|
441
|
-
function renderArgumentsTableFromArray(args) {
|
|
442
|
-
if (args.length === 0) return "";
|
|
443
|
-
const lines = [];
|
|
444
|
-
lines.push("| Argument | Description | Required |");
|
|
445
|
-
lines.push("|----------|-------------|----------|");
|
|
446
|
-
for (const arg of args) {
|
|
447
|
-
const desc = escapeTableCell$2(arg.description ?? "");
|
|
448
|
-
const required = arg.required ? "Yes" : "No";
|
|
449
|
-
lines.push(`| \`${arg.name}\` | ${desc} | ${required} |`);
|
|
450
|
-
}
|
|
451
|
-
return lines.join("\n");
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Render arguments from array as list
|
|
455
|
-
*/
|
|
456
|
-
function renderArgumentsListFromArray(args) {
|
|
457
|
-
if (args.length === 0) return "";
|
|
458
|
-
const lines = [];
|
|
459
|
-
for (const arg of args) {
|
|
460
|
-
const required = arg.required ? "(required)" : "(optional)";
|
|
461
|
-
const desc = arg.description ? ` - ${arg.description}` : "";
|
|
462
|
-
lines.push(`- \`${arg.name}\`${desc} ${required}`);
|
|
463
|
-
}
|
|
464
|
-
return lines.join("\n");
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Render subcommands from array as table
|
|
468
|
-
*/
|
|
469
|
-
function renderSubcommandsTableFromArray(subcommands, info, generateAnchors = true) {
|
|
470
|
-
if (subcommands.length === 0) return "";
|
|
471
|
-
const hasAliases = subcommands.some((s) => s.aliases && s.aliases.length > 0);
|
|
472
|
-
const lines = [];
|
|
473
|
-
if (hasAliases) {
|
|
474
|
-
lines.push("| Command | Aliases | Description |");
|
|
475
|
-
lines.push("|---------|---------|-------------|");
|
|
476
|
-
} else {
|
|
477
|
-
lines.push("| Command | Description |");
|
|
478
|
-
lines.push("|---------|-------------|");
|
|
479
|
-
}
|
|
480
|
-
const currentFile = info.filePath;
|
|
481
|
-
const fileMap = info.fileMap;
|
|
482
|
-
for (const sub of subcommands) {
|
|
483
|
-
const fullName = sub.fullPath.join(" ");
|
|
484
|
-
const desc = escapeTableCell$2(sub.description ?? "");
|
|
485
|
-
const subCommandPath = sub.fullPath.join(" ");
|
|
486
|
-
const aliasCell = hasAliases ? sub.aliases && sub.aliases.length > 0 ? sub.aliases.map((a) => `\`${escapeTableCell$2(a)}\``).join(", ") : "-" : "";
|
|
487
|
-
let cmdCell;
|
|
488
|
-
if (generateAnchors) {
|
|
489
|
-
const anchor = generateAnchor$1(sub.fullPath);
|
|
490
|
-
const hasSubFile = fileMap !== void 0 && Object.prototype.hasOwnProperty.call(fileMap, subCommandPath);
|
|
491
|
-
const subFile = hasSubFile ? fileMap[subCommandPath] : void 0;
|
|
492
|
-
if (currentFile && subFile && currentFile !== subFile) cmdCell = `[\`${fullName}\`](${getRelativePath(currentFile, subFile)}#${anchor})`;
|
|
493
|
-
else if (fileMap && !hasSubFile) cmdCell = `\`${fullName}\``;
|
|
494
|
-
else cmdCell = `[\`${fullName}\`](#${anchor})`;
|
|
495
|
-
} else cmdCell = `\`${fullName}\``;
|
|
496
|
-
if (hasAliases) lines.push(`| ${cmdCell} | ${aliasCell} | ${desc} |`);
|
|
497
|
-
else lines.push(`| ${cmdCell} | ${desc} |`);
|
|
498
|
-
}
|
|
499
|
-
return lines.join("\n");
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Render examples as markdown
|
|
503
|
-
*
|
|
504
|
-
* @example
|
|
505
|
-
* **Basic usage**
|
|
506
|
-
*
|
|
507
|
-
* ```bash
|
|
508
|
-
* $ greet World
|
|
509
|
-
* ```
|
|
510
|
-
*
|
|
511
|
-
* Output:
|
|
512
|
-
* ```
|
|
513
|
-
* Hello, World!
|
|
514
|
-
* ```
|
|
515
|
-
*/
|
|
516
|
-
function renderExamplesDefault(examples, results, opts) {
|
|
517
|
-
if (examples.length === 0) return "";
|
|
518
|
-
const showOutput = opts?.showOutput ?? true;
|
|
519
|
-
const prefix = opts?.commandPrefix ? `${opts.commandPrefix} ` : "";
|
|
520
|
-
const lines = [];
|
|
521
|
-
for (let i = 0; i < examples.length; i++) {
|
|
522
|
-
const example = examples[i];
|
|
523
|
-
if (!example) continue;
|
|
524
|
-
const result = results?.[i];
|
|
525
|
-
lines.push(`**${example.desc}**`);
|
|
526
|
-
lines.push("");
|
|
527
|
-
lines.push("```bash");
|
|
528
|
-
lines.push(`$ ${prefix}${example.cmd}`);
|
|
529
|
-
if (showOutput) {
|
|
530
|
-
if (result) {
|
|
531
|
-
if (result.stdout) lines.push(result.stdout);
|
|
532
|
-
if (result.stderr) lines.push(`[stderr] ${result.stderr}`);
|
|
533
|
-
} else if (example.output) lines.push(example.output);
|
|
534
|
-
}
|
|
535
|
-
lines.push("```");
|
|
536
|
-
lines.push("");
|
|
537
|
-
}
|
|
538
|
-
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
539
|
-
return lines.join("\n");
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Wrap content with section markers
|
|
543
|
-
*/
|
|
544
|
-
function wrapWithMarker(type, scope, content) {
|
|
545
|
-
return `${sectionStartMarker(type, scope)}\n${content}\n${sectionEndMarker(type, scope)}`;
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* Generate a "See Global Options" link for subcommand documentation.
|
|
549
|
-
* Returns null for root command or when no global options exist.
|
|
550
|
-
*/
|
|
551
|
-
function getGlobalOptionsLink(info) {
|
|
552
|
-
if (!info.hasGlobalOptions || info.commandPath === "") return null;
|
|
553
|
-
return `See [Global Options](${info.rootDocPath && info.filePath && info.filePath !== info.rootDocPath ? `${getRelativePath(info.filePath, info.rootDocPath)}#global-options` : "#global-options"}) for options available to all commands.`;
|
|
554
|
-
}
|
|
555
|
-
function createCommandRenderer(options = {}) {
|
|
556
|
-
const { headingLevel = 1, optionStyle = "table", generateAnchors = true, includeSubcommandDetails = true, markerless = false, renderDescription: customRenderDescription, renderUsage: customRenderUsage, renderArguments: customRenderArguments, renderOptions: customRenderOptions, renderSubcommands: customRenderSubcommands, renderNotes: customRenderNotes, renderFooter: customRenderFooter, renderExamples: customRenderExamples } = options;
|
|
557
|
-
const wrap = markerless ? (_type, _scope, content) => content : wrapWithMarker;
|
|
558
|
-
return (info) => {
|
|
559
|
-
const sections = [];
|
|
560
|
-
const scope = info.commandPath;
|
|
561
|
-
const effectiveLevel = Math.min(headingLevel + (info.depth - 1), 6);
|
|
562
|
-
const h = "#".repeat(effectiveLevel);
|
|
563
|
-
const title = info.commandPath || info.name;
|
|
564
|
-
sections.push(wrap("heading", scope, `${h} ${title}`));
|
|
565
|
-
{
|
|
566
|
-
const parts = [];
|
|
567
|
-
if (info.description) parts.push(info.description);
|
|
568
|
-
if (info.aliases && info.aliases.length > 0) parts.push(`**Aliases:** ${info.aliases.map((a) => `\`${a}\``).join(", ")}`);
|
|
569
|
-
if (parts.length > 0) {
|
|
570
|
-
const context = {
|
|
571
|
-
content: parts.join("\n\n"),
|
|
572
|
-
heading: "",
|
|
573
|
-
info
|
|
574
|
-
};
|
|
575
|
-
const content = customRenderDescription ? customRenderDescription(context) : context.content;
|
|
576
|
-
if (content) sections.push(wrap("description", scope, content));
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
{
|
|
580
|
-
const context = {
|
|
581
|
-
content: `**Usage**\n\n\`\`\`\n${renderUsage(info)}\n\`\`\``,
|
|
582
|
-
heading: "**Usage**",
|
|
583
|
-
info
|
|
584
|
-
};
|
|
585
|
-
const content = customRenderUsage ? customRenderUsage(context) : context.content;
|
|
586
|
-
if (content) sections.push(wrap("usage", scope, content));
|
|
587
|
-
}
|
|
588
|
-
if (info.positionalArgs.length > 0) {
|
|
589
|
-
const renderArgs = (args, opts) => {
|
|
590
|
-
const style = opts?.style ?? optionStyle;
|
|
591
|
-
const withHeading = opts?.withHeading ?? true;
|
|
592
|
-
const content = style === "table" ? renderArgumentsTableFromArray(args) : renderArgumentsListFromArray(args);
|
|
593
|
-
return withHeading ? `**Arguments**\n\n${content}` : content;
|
|
594
|
-
};
|
|
595
|
-
const context = {
|
|
596
|
-
args: info.positionalArgs,
|
|
597
|
-
render: renderArgs,
|
|
598
|
-
heading: "**Arguments**",
|
|
599
|
-
info
|
|
600
|
-
};
|
|
601
|
-
const content = customRenderArguments ? customRenderArguments(context) : renderArgs(context.args);
|
|
602
|
-
if (content) sections.push(wrap("arguments", scope, content));
|
|
603
|
-
}
|
|
604
|
-
if (info.options.length > 0) {
|
|
605
|
-
const renderOpts = (opts, renderOpts) => {
|
|
606
|
-
const style = renderOpts?.style ?? optionStyle;
|
|
607
|
-
const withHeading = renderOpts?.withHeading ?? true;
|
|
608
|
-
const extracted = info.extracted;
|
|
609
|
-
let content;
|
|
610
|
-
if (extracted && (extracted.schemaType === "union" || extracted.schemaType === "xor") && extracted.unionOptions) content = renderUnionOptionsMarkdown(extracted, style);
|
|
611
|
-
else if (extracted && extracted.schemaType === "discriminatedUnion" && extracted.discriminator) content = renderDiscriminatedUnionOptionsMarkdown(extracted, style);
|
|
612
|
-
else content = style === "table" ? renderOptionsTableFromArray(opts) : renderOptionsListFromArray(opts);
|
|
613
|
-
return withHeading ? `**Options**\n\n${content}` : content;
|
|
614
|
-
};
|
|
615
|
-
const context = {
|
|
616
|
-
options: info.options,
|
|
617
|
-
render: renderOpts,
|
|
618
|
-
heading: "**Options**",
|
|
619
|
-
info
|
|
620
|
-
};
|
|
621
|
-
const content = customRenderOptions ? customRenderOptions(context) : renderOpts(context.options);
|
|
622
|
-
if (content) sections.push(wrap("options", scope, content));
|
|
623
|
-
}
|
|
624
|
-
{
|
|
625
|
-
const globalLink = getGlobalOptionsLink(info);
|
|
626
|
-
if (globalLink) sections.push(wrap("global-options-link", scope, globalLink));
|
|
627
|
-
}
|
|
628
|
-
if (info.subCommands.length > 0) {
|
|
629
|
-
const effectiveAnchors = generateAnchors && includeSubcommandDetails;
|
|
630
|
-
const renderSubs = (subs, opts) => {
|
|
631
|
-
const anchors = opts?.generateAnchors ?? effectiveAnchors;
|
|
632
|
-
const withHeading = opts?.withHeading ?? true;
|
|
633
|
-
const content = renderSubcommandsTableFromArray(subs, info, anchors);
|
|
634
|
-
return withHeading ? `**Commands**\n\n${content}` : content;
|
|
635
|
-
};
|
|
636
|
-
const context = {
|
|
637
|
-
subcommands: info.subCommands,
|
|
638
|
-
render: renderSubs,
|
|
639
|
-
heading: "**Commands**",
|
|
640
|
-
info
|
|
641
|
-
};
|
|
642
|
-
const content = customRenderSubcommands ? customRenderSubcommands(context) : renderSubs(context.subcommands);
|
|
643
|
-
if (content) sections.push(wrap("subcommands", scope, content));
|
|
644
|
-
}
|
|
645
|
-
if (info.examples && info.examples.length > 0) {
|
|
646
|
-
const renderEx = (examples, results, opts) => {
|
|
647
|
-
const withHeading = opts?.withHeading ?? true;
|
|
648
|
-
const content = renderExamplesDefault(examples, results, {
|
|
649
|
-
commandPrefix: info.fullCommandPath,
|
|
650
|
-
...opts
|
|
651
|
-
});
|
|
652
|
-
return withHeading ? `**Examples**\n\n${content}` : content;
|
|
653
|
-
};
|
|
654
|
-
const context = {
|
|
655
|
-
examples: info.examples,
|
|
656
|
-
results: info.exampleResults,
|
|
657
|
-
render: renderEx,
|
|
658
|
-
heading: "**Examples**",
|
|
659
|
-
info
|
|
660
|
-
};
|
|
661
|
-
const content = customRenderExamples ? customRenderExamples(context) : renderEx(context.examples, context.results);
|
|
662
|
-
if (content) sections.push(wrap("examples", scope, content));
|
|
663
|
-
}
|
|
664
|
-
if (info.notes) {
|
|
665
|
-
const context = {
|
|
666
|
-
content: `**Notes**\n\n${info.notes}`,
|
|
667
|
-
heading: "**Notes**",
|
|
668
|
-
info
|
|
669
|
-
};
|
|
670
|
-
const content = customRenderNotes ? customRenderNotes(context) : context.content;
|
|
671
|
-
if (content) sections.push(wrap("notes", scope, content));
|
|
672
|
-
}
|
|
673
|
-
{
|
|
674
|
-
const context = {
|
|
675
|
-
content: "",
|
|
676
|
-
heading: "",
|
|
677
|
-
info
|
|
678
|
-
};
|
|
679
|
-
const content = customRenderFooter ? customRenderFooter(context) : context.content;
|
|
680
|
-
if (content) sections.push(content);
|
|
681
|
-
}
|
|
682
|
-
return sections.join("\n\n") + "\n";
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
/**
|
|
686
|
-
* Default renderers presets
|
|
687
|
-
*/
|
|
688
|
-
const defaultRenderers = {
|
|
689
|
-
/** Standard command documentation */
|
|
690
|
-
command: (options) => createCommandRenderer(options),
|
|
691
|
-
/** Table style options (default) */
|
|
692
|
-
tableStyle: createCommandRenderer({ optionStyle: "table" }),
|
|
693
|
-
/** List style options */
|
|
694
|
-
listStyle: createCommandRenderer({ optionStyle: "list" })
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
//#endregion
|
|
698
|
-
//#region src/docs/doc-comparator.ts
|
|
699
|
-
/**
|
|
700
|
-
* Compare generated content with existing file
|
|
701
|
-
*/
|
|
702
|
-
function compareWithExisting(generatedContent, filePath) {
|
|
703
|
-
const absolutePath = node_path.resolve(filePath);
|
|
704
|
-
if (!node_fs.existsSync(absolutePath)) return {
|
|
705
|
-
match: false,
|
|
706
|
-
fileExists: false
|
|
707
|
-
};
|
|
708
|
-
const existingContent = node_fs.readFileSync(absolutePath, "utf-8");
|
|
709
|
-
if (generatedContent === existingContent) return {
|
|
710
|
-
match: true,
|
|
711
|
-
fileExists: true
|
|
712
|
-
};
|
|
713
|
-
return {
|
|
714
|
-
match: false,
|
|
715
|
-
diff: formatDiff(existingContent, generatedContent),
|
|
716
|
-
fileExists: true
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Format diff between two strings in unified diff format
|
|
721
|
-
*/
|
|
722
|
-
function formatDiff(expected, actual) {
|
|
723
|
-
const expectedLines = expected.split("\n");
|
|
724
|
-
const actualLines = actual.split("\n");
|
|
725
|
-
const result = [];
|
|
726
|
-
result.push("--- existing");
|
|
727
|
-
result.push("+++ generated");
|
|
728
|
-
result.push("");
|
|
729
|
-
const maxLines = Math.max(expectedLines.length, actualLines.length);
|
|
730
|
-
let inChunk = false;
|
|
731
|
-
let chunkStart = 0;
|
|
732
|
-
const chunk = [];
|
|
733
|
-
const flushChunk = () => {
|
|
734
|
-
if (chunk.length > 0) {
|
|
735
|
-
result.push(`@@ -${chunkStart + 1},${chunk.length} @@`);
|
|
736
|
-
result.push(...chunk);
|
|
737
|
-
chunk.length = 0;
|
|
738
|
-
}
|
|
739
|
-
inChunk = false;
|
|
740
|
-
};
|
|
741
|
-
for (let i = 0; i < maxLines; i++) {
|
|
742
|
-
const expectedLine = expectedLines[i];
|
|
743
|
-
const actualLine = actualLines[i];
|
|
744
|
-
if (expectedLine === actualLine) {
|
|
745
|
-
if (inChunk) {
|
|
746
|
-
chunk.push(` ${expectedLine ?? ""}`);
|
|
747
|
-
const lastChangeIndex = chunk.findIndex((line, idx) => (line.startsWith("-") || line.startsWith("+")) && chunk.slice(idx + 1).every((l) => l.startsWith(" ")));
|
|
748
|
-
if (lastChangeIndex !== -1 && chunk.length - lastChangeIndex > 3) flushChunk();
|
|
749
|
-
}
|
|
750
|
-
} else {
|
|
751
|
-
if (!inChunk) {
|
|
752
|
-
inChunk = true;
|
|
753
|
-
chunkStart = i;
|
|
754
|
-
const contextStart = Math.max(0, i - 3);
|
|
755
|
-
for (let j = contextStart; j < i; j++) chunk.push(` ${expectedLines[j] ?? ""}`);
|
|
756
|
-
}
|
|
757
|
-
if (expectedLine !== void 0 && (actualLine === void 0 || expectedLine !== actualLine)) chunk.push(`-${expectedLine}`);
|
|
758
|
-
if (actualLine !== void 0 && (expectedLine === void 0 || expectedLine !== actualLine)) chunk.push(`+${actualLine}`);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
flushChunk();
|
|
762
|
-
return result.join("\n");
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Write content to file, creating directories if needed
|
|
766
|
-
*/
|
|
767
|
-
function writeFile(filePath, content) {
|
|
768
|
-
const absolutePath = node_path.resolve(filePath);
|
|
769
|
-
const dir = node_path.dirname(absolutePath);
|
|
770
|
-
if (!node_fs.existsSync(dir)) node_fs.mkdirSync(dir, { recursive: true });
|
|
771
|
-
node_fs.writeFileSync(absolutePath, content, "utf-8");
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Read file content if it exists
|
|
775
|
-
* Returns null if file does not exist
|
|
776
|
-
*/
|
|
777
|
-
function readFile(filePath) {
|
|
778
|
-
const absolutePath = node_path.resolve(filePath);
|
|
779
|
-
if (!node_fs.existsSync(absolutePath)) return null;
|
|
780
|
-
return node_fs.readFileSync(absolutePath, "utf-8");
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Delete file if it exists
|
|
784
|
-
* @param filePath - Path to the file to delete
|
|
785
|
-
* @param fileSystem - Optional fs implementation (useful when fs is mocked)
|
|
786
|
-
*/
|
|
787
|
-
function deleteFile(filePath, fileSystem = node_fs) {
|
|
788
|
-
const absolutePath = node_path.resolve(filePath);
|
|
789
|
-
if (fileSystem.existsSync(absolutePath)) fileSystem.unlinkSync(absolutePath);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
//#endregion
|
|
793
|
-
//#region src/docs/doc-generator.ts
|
|
794
|
-
/**
|
|
795
|
-
* Build CommandInfo from a command
|
|
796
|
-
*/
|
|
797
|
-
async function buildCommandInfo(command, rootName, commandPath = []) {
|
|
798
|
-
const extracted = require_schema_extractor.getExtractedFields(command);
|
|
799
|
-
const positionalArgs = extracted?.fields.filter((f) => f.positional) ?? [];
|
|
800
|
-
const options = extracted?.fields.filter((f) => !f.positional) ?? [];
|
|
801
|
-
const subCommands = [];
|
|
802
|
-
if (command.subCommands) for (const [name, subCmd] of Object.entries(command.subCommands)) {
|
|
803
|
-
const resolved = await require_schema_extractor.resolveLazyCommand(subCmd);
|
|
804
|
-
const fullPath = [...commandPath, name];
|
|
805
|
-
subCommands.push({
|
|
806
|
-
name,
|
|
807
|
-
description: resolved.description,
|
|
808
|
-
aliases: resolved.aliases,
|
|
809
|
-
fullPath
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
return {
|
|
813
|
-
name: command.name ?? "",
|
|
814
|
-
description: command.description,
|
|
815
|
-
aliases: command.aliases,
|
|
816
|
-
fullCommandPath: commandPath.length > 0 ? `${rootName} ${commandPath.join(" ")}` : rootName,
|
|
817
|
-
commandPath: commandPath.join(" "),
|
|
818
|
-
depth: commandPath.length + 1,
|
|
819
|
-
positionalArgs,
|
|
820
|
-
options,
|
|
821
|
-
subCommands,
|
|
822
|
-
extracted,
|
|
823
|
-
command,
|
|
824
|
-
notes: command.notes,
|
|
825
|
-
examples: command.examples
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
/**
|
|
829
|
-
* Collect all commands with their paths
|
|
830
|
-
* Returns a map of command path -> CommandInfo
|
|
831
|
-
*/
|
|
832
|
-
async function collectAllCommands(command, rootName) {
|
|
833
|
-
const root = rootName ?? command.name ?? "command";
|
|
834
|
-
const result = /* @__PURE__ */ new Map();
|
|
835
|
-
async function traverse(cmd, path) {
|
|
836
|
-
const info = await buildCommandInfo(cmd, root, path);
|
|
837
|
-
const pathKey = path.join(" ");
|
|
838
|
-
result.set(pathKey, info);
|
|
839
|
-
if (cmd.subCommands) for (const [name, subCmd] of Object.entries(cmd.subCommands)) await traverse(await require_schema_extractor.resolveLazyCommand(subCmd), [...path, name]);
|
|
840
|
-
}
|
|
841
|
-
await traverse(command, []);
|
|
842
|
-
return result;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
//#endregion
|
|
846
|
-
//#region src/docs/example-executor.ts
|
|
847
|
-
/**
|
|
848
|
-
* Execute examples for a command and capture output
|
|
849
|
-
*
|
|
850
|
-
* @param examples - Examples to execute
|
|
851
|
-
* @param config - Execution configuration (mock setup/cleanup)
|
|
852
|
-
* @param rootCommand - Root command to execute against
|
|
853
|
-
* @param commandPath - Command path for subcommands (e.g., ["config", "get"])
|
|
854
|
-
* @returns Array of execution results with captured stdout/stderr
|
|
855
|
-
*/
|
|
856
|
-
async function executeExamples(examples, config, rootCommand, commandPath = []) {
|
|
857
|
-
const results = [];
|
|
858
|
-
if (config.mock) await config.mock();
|
|
859
|
-
try {
|
|
860
|
-
for (const example of examples) {
|
|
861
|
-
const result = await executeSingleExample(example, rootCommand, commandPath);
|
|
862
|
-
results.push(result);
|
|
863
|
-
}
|
|
864
|
-
} finally {
|
|
865
|
-
if (config.cleanup) await config.cleanup();
|
|
866
|
-
}
|
|
867
|
-
return results;
|
|
868
|
-
}
|
|
869
|
-
/**
|
|
870
|
-
* Execute a single example and capture output
|
|
871
|
-
*/
|
|
872
|
-
async function executeSingleExample(example, rootCommand, commandPath) {
|
|
873
|
-
const exampleArgs = parseExampleCmd(example.cmd);
|
|
874
|
-
const argv = [...commandPath, ...exampleArgs];
|
|
875
|
-
const collector = require_log_collector.createLogCollector({ passthrough: false });
|
|
876
|
-
collector.start();
|
|
877
|
-
let success = true;
|
|
878
|
-
try {
|
|
879
|
-
const { runCommand } = await Promise.resolve().then(() => require("../runner-DvFvokV6.cjs")).then((n) => n.runner_exports);
|
|
880
|
-
const result = await runCommand(rootCommand, argv);
|
|
881
|
-
success = result.success;
|
|
882
|
-
if (!result.success && result.error) console.error(result.error.message);
|
|
883
|
-
} catch (error) {
|
|
884
|
-
success = false;
|
|
885
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
886
|
-
} finally {
|
|
887
|
-
collector.stop();
|
|
888
|
-
}
|
|
889
|
-
const logs = collector.getLogs();
|
|
890
|
-
const stdout = logs.entries.filter((e) => e.stream === "stdout").map((e) => e.message).join("\n");
|
|
891
|
-
const stderr = logs.entries.filter((e) => e.stream === "stderr").map((e) => e.message).join("\n");
|
|
892
|
-
return {
|
|
893
|
-
cmd: example.cmd,
|
|
894
|
-
desc: example.desc,
|
|
895
|
-
expectedOutput: example.output,
|
|
896
|
-
stdout,
|
|
897
|
-
stderr,
|
|
898
|
-
success
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
/**
|
|
902
|
-
* Parse example command string into argv array
|
|
903
|
-
* Handles quoted strings (single and double quotes)
|
|
904
|
-
*
|
|
905
|
-
* @example
|
|
906
|
-
* parseExampleCmd('World') // ['World']
|
|
907
|
-
* parseExampleCmd('--name "John Doe"') // ['--name', 'John Doe']
|
|
908
|
-
* parseExampleCmd("--greeting 'Hello World'") // ['--greeting', 'Hello World']
|
|
909
|
-
*/
|
|
910
|
-
function parseExampleCmd(cmd) {
|
|
911
|
-
const args = [];
|
|
912
|
-
let current = "";
|
|
913
|
-
let inQuote = false;
|
|
914
|
-
let quoteChar = "";
|
|
915
|
-
for (let i = 0; i < cmd.length; i++) {
|
|
916
|
-
const char = cmd[i];
|
|
917
|
-
if ((char === "\"" || char === "'") && !inQuote) {
|
|
918
|
-
inQuote = true;
|
|
919
|
-
quoteChar = char;
|
|
920
|
-
} else if (char === quoteChar && inQuote) {
|
|
921
|
-
inQuote = false;
|
|
922
|
-
quoteChar = "";
|
|
923
|
-
} else if (char === " " && !inQuote) {
|
|
924
|
-
if (current) {
|
|
925
|
-
args.push(current);
|
|
926
|
-
current = "";
|
|
927
|
-
}
|
|
928
|
-
} else current += char;
|
|
929
|
-
}
|
|
930
|
-
if (current) args.push(current);
|
|
931
|
-
return args;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
//#endregion
|
|
935
|
-
//#region src/docs/render-args.ts
|
|
936
|
-
/**
|
|
937
|
-
* Extract ResolvedFieldMeta array from ArgsShape
|
|
938
|
-
*
|
|
939
|
-
* This converts a raw args shape (like `commonArgs`) into the
|
|
940
|
-
* ResolvedFieldMeta format used by politty's rendering functions.
|
|
941
|
-
*/
|
|
942
|
-
function extractArgsFields(args) {
|
|
943
|
-
return require_schema_extractor.extractFields(zod.z.object(args)).fields;
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Render args definition as a markdown options table
|
|
947
|
-
*
|
|
948
|
-
* This function takes raw args definitions (like `commonArgs`) and
|
|
949
|
-
* renders them as a markdown table suitable for documentation.
|
|
950
|
-
*
|
|
951
|
-
* @example
|
|
952
|
-
* import { renderArgsTable } from "politty/docs";
|
|
953
|
-
* import { commonArgs, workspaceArgs } from "./args";
|
|
954
|
-
*
|
|
955
|
-
* const table = renderArgsTable({
|
|
956
|
-
* ...commonArgs,
|
|
957
|
-
* ...workspaceArgs,
|
|
958
|
-
* });
|
|
959
|
-
* // | Option | Alias | Description | Default |
|
|
960
|
-
* // |--------|-------|-------------|---------|
|
|
961
|
-
* // | `--env-file <ENV_FILE>` | `-e` | Path to environment file | - |
|
|
962
|
-
* // ...
|
|
963
|
-
*
|
|
964
|
-
* @param args - Args shape (Record of string keys to Zod schemas with arg() metadata)
|
|
965
|
-
* @param options - Rendering options
|
|
966
|
-
* @returns Rendered markdown table string
|
|
967
|
-
*/
|
|
968
|
-
function renderArgsTable(args, options) {
|
|
969
|
-
const optionFields = extractArgsFields(args).filter((f) => !f.positional);
|
|
970
|
-
if (optionFields.length === 0) return "";
|
|
971
|
-
if (options?.columns) return renderFilteredTable(optionFields, options.columns);
|
|
972
|
-
return renderOptionsTableFromArray(optionFields);
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* Escape markdown special characters in table cells
|
|
976
|
-
*/
|
|
977
|
-
function escapeTableCell$1(str) {
|
|
978
|
-
return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
979
|
-
}
|
|
980
|
-
/**
|
|
981
|
-
* Format default value for display
|
|
982
|
-
*/
|
|
983
|
-
function formatDefaultValue(value) {
|
|
984
|
-
if (value === void 0) return "-";
|
|
985
|
-
return `\`${JSON.stringify(value)}\``;
|
|
986
|
-
}
|
|
987
|
-
/**
|
|
988
|
-
* Render table with filtered columns
|
|
989
|
-
*/
|
|
990
|
-
function renderFilteredTable(options, columns) {
|
|
991
|
-
const lines = [];
|
|
992
|
-
const headerCells = [];
|
|
993
|
-
const separatorCells = [];
|
|
994
|
-
for (const col of columns) switch (col) {
|
|
995
|
-
case "option":
|
|
996
|
-
headerCells.push("Option");
|
|
997
|
-
separatorCells.push("------");
|
|
998
|
-
break;
|
|
999
|
-
case "alias":
|
|
1000
|
-
headerCells.push("Alias");
|
|
1001
|
-
separatorCells.push("-----");
|
|
1002
|
-
break;
|
|
1003
|
-
case "description":
|
|
1004
|
-
headerCells.push("Description");
|
|
1005
|
-
separatorCells.push("-----------");
|
|
1006
|
-
break;
|
|
1007
|
-
case "required":
|
|
1008
|
-
headerCells.push("Required");
|
|
1009
|
-
separatorCells.push("--------");
|
|
1010
|
-
break;
|
|
1011
|
-
case "default":
|
|
1012
|
-
headerCells.push("Default");
|
|
1013
|
-
separatorCells.push("-------");
|
|
1014
|
-
break;
|
|
1015
|
-
case "env":
|
|
1016
|
-
headerCells.push("Env");
|
|
1017
|
-
separatorCells.push("---");
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
lines.push(`| ${headerCells.join(" | ")} |`);
|
|
1021
|
-
lines.push(`| ${separatorCells.join(" | ")} |`);
|
|
1022
|
-
for (const opt of options) {
|
|
1023
|
-
const cells = [];
|
|
1024
|
-
for (const col of columns) switch (col) {
|
|
1025
|
-
case "option": {
|
|
1026
|
-
const placeholder = opt.placeholder ?? opt.cliName.toUpperCase().replace(/-/g, "_");
|
|
1027
|
-
let optionName;
|
|
1028
|
-
if (opt.type === "boolean") {
|
|
1029
|
-
optionName = `\`--${opt.cliName}\``;
|
|
1030
|
-
if (opt.negationDisplay && !opt.negationDescription) optionName += ` / \`--${opt.negationDisplay}\``;
|
|
1031
|
-
} else optionName = `\`--${opt.cliName} <${placeholder}>\``;
|
|
1032
|
-
cells.push(optionName);
|
|
1033
|
-
break;
|
|
1034
|
-
}
|
|
1035
|
-
case "alias":
|
|
1036
|
-
cells.push(opt.alias && opt.alias.length > 0 ? opt.alias.map((a) => `\`${a.length === 1 ? `-${a}` : `--${a}`}\``).join(", ") : "-");
|
|
1037
|
-
break;
|
|
1038
|
-
case "description":
|
|
1039
|
-
cells.push(escapeTableCell$1(opt.description ?? ""));
|
|
1040
|
-
break;
|
|
1041
|
-
case "required":
|
|
1042
|
-
cells.push(opt.required ? "Yes" : "No");
|
|
1043
|
-
break;
|
|
1044
|
-
case "default":
|
|
1045
|
-
cells.push(formatDefaultValue(opt.defaultValue));
|
|
1046
|
-
break;
|
|
1047
|
-
case "env": {
|
|
1048
|
-
const envNames = opt.env ? Array.isArray(opt.env) ? opt.env.map((e) => `\`${e}\``).join(", ") : `\`${opt.env}\`` : "-";
|
|
1049
|
-
cells.push(envNames);
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
lines.push(`| ${cells.join(" | ")} |`);
|
|
1054
|
-
if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) {
|
|
1055
|
-
const negCells = [];
|
|
1056
|
-
for (const col of columns) switch (col) {
|
|
1057
|
-
case "option":
|
|
1058
|
-
negCells.push(`\`--${opt.negationDisplay}\``);
|
|
1059
|
-
break;
|
|
1060
|
-
case "description":
|
|
1061
|
-
negCells.push(`${escapeTableCell$1(opt.negationDescription)} ${negationRelationMarker(opt)}`);
|
|
1062
|
-
break;
|
|
1063
|
-
case "required":
|
|
1064
|
-
negCells.push(opt.required ? "Yes" : "No");
|
|
1065
|
-
break;
|
|
1066
|
-
case "alias":
|
|
1067
|
-
case "default":
|
|
1068
|
-
case "env":
|
|
1069
|
-
negCells.push("-");
|
|
1070
|
-
break;
|
|
1071
|
-
}
|
|
1072
|
-
lines.push(`| ${negCells.join(" | ")} |`);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
return lines.join("\n");
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
//#endregion
|
|
1079
|
-
//#region src/docs/render-index.ts
|
|
1080
|
-
/**
|
|
1081
|
-
* Escape markdown special characters in table cells
|
|
1082
|
-
*/
|
|
1083
|
-
function escapeTableCell(str) {
|
|
1084
|
-
return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1085
|
-
}
|
|
1086
|
-
/**
|
|
1087
|
-
* Generate anchor from command path
|
|
1088
|
-
*/
|
|
1089
|
-
function generateAnchor(commandPath) {
|
|
1090
|
-
return commandPath.replace(/\s+/g, "-").toLowerCase();
|
|
1091
|
-
}
|
|
1092
|
-
/**
|
|
1093
|
-
* Check if a command is a leaf (has no subcommands)
|
|
1094
|
-
*/
|
|
1095
|
-
function isLeafCommand(info) {
|
|
1096
|
-
return info.subCommands.length === 0;
|
|
1097
|
-
}
|
|
1098
|
-
function isSubcommandOf$1(childPath, parentPath) {
|
|
1099
|
-
if (childPath === parentPath) return true;
|
|
1100
|
-
if (parentPath === "") return childPath !== "";
|
|
1101
|
-
return childPath.startsWith(parentPath + " ");
|
|
1102
|
-
}
|
|
1103
|
-
/**
|
|
1104
|
-
* Expand commands to include their subcommands
|
|
1105
|
-
* If a command has subcommands, recursively find all commands under it
|
|
1106
|
-
*
|
|
1107
|
-
* @param commandPaths - Command paths to expand
|
|
1108
|
-
* @param allCommands - Map of all available commands
|
|
1109
|
-
* @param leafOnly - If true, only include leaf commands; if false, include all commands
|
|
1110
|
-
*/
|
|
1111
|
-
function expandCommands(commandPaths, allCommands, leafOnly) {
|
|
1112
|
-
const result = [];
|
|
1113
|
-
for (const cmdPath of commandPaths) {
|
|
1114
|
-
const info = allCommands.get(cmdPath);
|
|
1115
|
-
if (!info) continue;
|
|
1116
|
-
if (isLeafCommand(info)) result.push(cmdPath);
|
|
1117
|
-
else for (const [path, pathInfo] of allCommands) if (cmdPath === "" ? path.length > 0 : path.startsWith(cmdPath + " ") || path === cmdPath) {
|
|
1118
|
-
if (isLeafCommand(pathInfo) || !leafOnly) result.push(path);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
return result;
|
|
1122
|
-
}
|
|
1123
|
-
/**
|
|
1124
|
-
* Render a single category section
|
|
1125
|
-
*/
|
|
1126
|
-
function renderCategory(category, allCommands, headingLevel, leafOnly) {
|
|
1127
|
-
const h = "#".repeat(headingLevel);
|
|
1128
|
-
const lines = [];
|
|
1129
|
-
lines.push(`${h} [${category.title}](${category.docPath})`);
|
|
1130
|
-
lines.push("");
|
|
1131
|
-
lines.push(category.description);
|
|
1132
|
-
lines.push("");
|
|
1133
|
-
const commandPaths = category.noExpand ? category.commands : expandCommands(category.commands, allCommands, leafOnly);
|
|
1134
|
-
let visibleCommandPaths = commandPaths;
|
|
1135
|
-
const fallbackCommandPaths = /* @__PURE__ */ new Set();
|
|
1136
|
-
if (category.allowedCommands) {
|
|
1137
|
-
const allowed = new Set(category.allowedCommands);
|
|
1138
|
-
visibleCommandPaths = commandPaths.filter((cmdPath) => allowed.has(cmdPath));
|
|
1139
|
-
for (const configuredPath of category.commands) if (allowed.has(configuredPath) && !visibleCommandPaths.some((cmdPath) => isSubcommandOf$1(cmdPath, configuredPath))) {
|
|
1140
|
-
visibleCommandPaths.push(configuredPath);
|
|
1141
|
-
fallbackCommandPaths.add(configuredPath);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
lines.push("| Command | Description |");
|
|
1145
|
-
lines.push("|---------|-------------|");
|
|
1146
|
-
for (const cmdPath of visibleCommandPaths) {
|
|
1147
|
-
const info = allCommands.get(cmdPath);
|
|
1148
|
-
if (!info) continue;
|
|
1149
|
-
if (!category.noExpand && leafOnly && !fallbackCommandPaths.has(cmdPath) && !isLeafCommand(info)) continue;
|
|
1150
|
-
const displayName = cmdPath || info.name;
|
|
1151
|
-
const anchor = generateAnchor(displayName);
|
|
1152
|
-
const desc = escapeTableCell(info.description ?? "");
|
|
1153
|
-
lines.push(`| [${displayName}](${category.docPath}#${anchor}) | ${desc} |`);
|
|
1154
|
-
}
|
|
1155
|
-
return lines.join("\n");
|
|
1156
|
-
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Render command index from categories
|
|
1159
|
-
*
|
|
1160
|
-
* Generates a category-based index of commands with links to documentation.
|
|
1161
|
-
*
|
|
1162
|
-
* @example
|
|
1163
|
-
* const categories: CommandCategory[] = [
|
|
1164
|
-
* {
|
|
1165
|
-
* title: "Application Commands",
|
|
1166
|
-
* description: "Commands for managing applications.",
|
|
1167
|
-
* commands: ["init", "generate", "apply"],
|
|
1168
|
-
* docPath: "./cli/application.md",
|
|
1169
|
-
* },
|
|
1170
|
-
* ];
|
|
1171
|
-
*
|
|
1172
|
-
* const index = await renderCommandIndex(mainCommand, categories);
|
|
1173
|
-
* // ### [Application Commands](./cli/application.md)
|
|
1174
|
-
* //
|
|
1175
|
-
* // Commands for managing applications.
|
|
1176
|
-
* //
|
|
1177
|
-
* // | Command | Description |
|
|
1178
|
-
* // |---------|-------------|
|
|
1179
|
-
* // | [init](./cli/application.md#init) | Initialize a project |
|
|
1180
|
-
* // ...
|
|
1181
|
-
*
|
|
1182
|
-
* @param command - Root command to extract command information from
|
|
1183
|
-
* @param categories - Category definitions for grouping commands
|
|
1184
|
-
* @param options - Rendering options
|
|
1185
|
-
* @returns Rendered markdown string
|
|
1186
|
-
*/
|
|
1187
|
-
async function renderCommandIndex(command, categories, options) {
|
|
1188
|
-
const headingLevel = options?.headingLevel ?? 3;
|
|
1189
|
-
const leafOnly = options?.leafOnly ?? true;
|
|
1190
|
-
const allCommands = await collectAllCommands(command);
|
|
1191
|
-
const sections = [];
|
|
1192
|
-
for (const category of categories) {
|
|
1193
|
-
const section = renderCategory(category, allCommands, headingLevel, leafOnly);
|
|
1194
|
-
sections.push(section);
|
|
1195
|
-
}
|
|
1196
|
-
return sections.join("\n\n");
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
//#endregion
|
|
1200
|
-
//#region src/docs/golden-test.ts
|
|
1201
|
-
/**
|
|
1202
|
-
* Apply formatter to content if provided
|
|
1203
|
-
* Supports both sync and async formatters
|
|
1204
|
-
*/
|
|
1205
|
-
async function applyFormatter(content, formatter) {
|
|
1206
|
-
if (!formatter) return content;
|
|
1207
|
-
const formatted = await formatter(content);
|
|
1208
|
-
if (!content.endsWith("\n") && formatted.endsWith("\n")) return formatted.slice(0, -1);
|
|
1209
|
-
return formatted;
|
|
1210
|
-
}
|
|
1211
|
-
function isTruthyEnv(envKey) {
|
|
1212
|
-
const value = process.env[envKey];
|
|
1213
|
-
return value === "true" || value === "1";
|
|
1214
|
-
}
|
|
1215
|
-
function extractYamlFrontMatter(content) {
|
|
1216
|
-
const lines = content.split(/\r?\n/);
|
|
1217
|
-
if (lines[0] !== "---") return null;
|
|
1218
|
-
const frontMatterLines = [];
|
|
1219
|
-
for (let i = 1; i < lines.length; i++) {
|
|
1220
|
-
const line = lines[i];
|
|
1221
|
-
if (line === "---" || line === "...") return frontMatterLines.join("\n");
|
|
1222
|
-
frontMatterLines.push(line ?? "");
|
|
1223
|
-
}
|
|
1224
|
-
return null;
|
|
1225
|
-
}
|
|
1226
|
-
function stripPolittyFrontMatterForOutput(content) {
|
|
1227
|
-
const lineEnding = detectLineEnding(content);
|
|
1228
|
-
const lines = content.split(/\r?\n/);
|
|
1229
|
-
if (lines[0] !== "---") return content;
|
|
1230
|
-
let endIndex = -1;
|
|
1231
|
-
for (let i = 1; i < lines.length; i++) {
|
|
1232
|
-
const line = lines[i];
|
|
1233
|
-
if (line === "---" || line === "...") {
|
|
1234
|
-
endIndex = i;
|
|
1235
|
-
break;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
if (endIndex === -1) return content;
|
|
1239
|
-
const frontMatterLines = lines.slice(1, endIndex);
|
|
1240
|
-
const keptFrontMatterLines = [];
|
|
1241
|
-
for (let i = 0; i < frontMatterLines.length; i++) {
|
|
1242
|
-
const line = frontMatterLines[i] ?? "";
|
|
1243
|
-
if (!/^politty\s*:\s*(.*)$/.test(line)) {
|
|
1244
|
-
keptFrontMatterLines.push(line);
|
|
1245
|
-
continue;
|
|
1246
|
-
}
|
|
1247
|
-
while (i + 1 < frontMatterLines.length) {
|
|
1248
|
-
const nextLine = frontMatterLines[i + 1] ?? "";
|
|
1249
|
-
if (nextLine.trim() !== "" && !nextLine.startsWith(" ") && !nextLine.startsWith(" ")) break;
|
|
1250
|
-
i++;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
const bodyLines = lines.slice(endIndex + 1);
|
|
1254
|
-
if (!keptFrontMatterLines.some((line) => line.trim() !== "")) return bodyLines.join(lineEnding).replace(new RegExp(`^${lineEnding}`), "");
|
|
1255
|
-
return [
|
|
1256
|
-
"---",
|
|
1257
|
-
...keptFrontMatterLines,
|
|
1258
|
-
lines[endIndex] ?? "---",
|
|
1259
|
-
...bodyLines
|
|
1260
|
-
].join(lineEnding);
|
|
1261
|
-
}
|
|
1262
|
-
function stripYamlScalarQuotes(value) {
|
|
1263
|
-
const trimmed = value.trim();
|
|
1264
|
-
if (trimmed.length >= 2 && (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'"))) return trimmed.slice(1, -1);
|
|
1265
|
-
return trimmed;
|
|
1266
|
-
}
|
|
1267
|
-
function normalizeTemplatePlaceholderKey(value) {
|
|
1268
|
-
let normalized = stripYamlScalarQuotes(value);
|
|
1269
|
-
if (normalized === "") return null;
|
|
1270
|
-
const fullPlaceholder = normalized.match(/^\{\{politty:([^{}]*)\}\}$/);
|
|
1271
|
-
if (fullPlaceholder) normalized = fullPlaceholder[1] ?? "";
|
|
1272
|
-
else if (normalized.startsWith("politty:")) normalized = normalized.slice(8);
|
|
1273
|
-
return normalized === "" ? null : normalized;
|
|
1274
|
-
}
|
|
1275
|
-
function templatePlaceholderKey(placeholder) {
|
|
1276
|
-
return placeholder.slice(2, -2).slice(8);
|
|
1277
|
-
}
|
|
1278
|
-
function splitFrontMatterListValue(value) {
|
|
1279
|
-
const trimmed = value.trim();
|
|
1280
|
-
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [trimmed];
|
|
1281
|
-
return trimmed.slice(1, -1).split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
1282
|
-
}
|
|
1283
|
-
function addTemplatePlaceholderExclusion(exclusions, value) {
|
|
1284
|
-
const normalized = normalizeTemplatePlaceholderKey(value);
|
|
1285
|
-
if (normalized !== null) exclusions.add(normalized);
|
|
1286
|
-
}
|
|
1287
|
-
function collectExcludedTemplatePlaceholders(templateContent) {
|
|
1288
|
-
const exclusions = /* @__PURE__ */ new Set();
|
|
1289
|
-
const frontMatter = extractYamlFrontMatter(templateContent);
|
|
1290
|
-
if (frontMatter === null) return exclusions;
|
|
1291
|
-
let inPolittyBlock = false;
|
|
1292
|
-
let inExcludeList = false;
|
|
1293
|
-
let excludeIndent = 0;
|
|
1294
|
-
for (const line of frontMatter.split(/\r?\n/)) {
|
|
1295
|
-
const trimmed = line.trim();
|
|
1296
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1297
|
-
const topLevelPolitty = line.match(/^politty\s*:\s*(.*)$/);
|
|
1298
|
-
if (topLevelPolitty) {
|
|
1299
|
-
inPolittyBlock = (topLevelPolitty[1]?.trim() ?? "") === "";
|
|
1300
|
-
inExcludeList = false;
|
|
1301
|
-
continue;
|
|
1302
|
-
}
|
|
1303
|
-
if (!line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1304
|
-
inPolittyBlock = false;
|
|
1305
|
-
inExcludeList = false;
|
|
1306
|
-
continue;
|
|
1307
|
-
}
|
|
1308
|
-
if (!inPolittyBlock) continue;
|
|
1309
|
-
const excludeEntry = line.match(/^(\s+)(?:exclude|excludes)\s*:\s*(.*)$/);
|
|
1310
|
-
if (excludeEntry) {
|
|
1311
|
-
const value = excludeEntry[2]?.trim() ?? "";
|
|
1312
|
-
if (value === "") {
|
|
1313
|
-
inExcludeList = true;
|
|
1314
|
-
excludeIndent = excludeEntry[1]?.length ?? 0;
|
|
1315
|
-
} else {
|
|
1316
|
-
inExcludeList = false;
|
|
1317
|
-
for (const item of splitFrontMatterListValue(value)) addTemplatePlaceholderExclusion(exclusions, item);
|
|
1318
|
-
}
|
|
1319
|
-
continue;
|
|
1320
|
-
}
|
|
1321
|
-
if (!inExcludeList) continue;
|
|
1322
|
-
const listItem = line.match(/^(\s*)-\s*(.+)$/);
|
|
1323
|
-
if (!listItem || (listItem[1]?.length ?? 0) <= excludeIndent) {
|
|
1324
|
-
inExcludeList = false;
|
|
1325
|
-
continue;
|
|
1326
|
-
}
|
|
1327
|
-
addTemplatePlaceholderExclusion(exclusions, listItem[2] ?? "");
|
|
1328
|
-
}
|
|
1329
|
-
return exclusions;
|
|
1330
|
-
}
|
|
1331
|
-
function collectTemplateIndexMetadata(templateContent) {
|
|
1332
|
-
const frontMatter = extractYamlFrontMatter(templateContent);
|
|
1333
|
-
if (frontMatter === null) return {};
|
|
1334
|
-
let inPolittyBlock = false;
|
|
1335
|
-
let inIndexBlock = false;
|
|
1336
|
-
let indexIndent = 0;
|
|
1337
|
-
const metadata = {};
|
|
1338
|
-
for (const line of frontMatter.split(/\r?\n/)) {
|
|
1339
|
-
const trimmed = line.trim();
|
|
1340
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1341
|
-
const topLevelPolitty = line.match(/^politty\s*:\s*(.*)$/);
|
|
1342
|
-
if (topLevelPolitty) {
|
|
1343
|
-
inPolittyBlock = (topLevelPolitty[1]?.trim() ?? "") === "";
|
|
1344
|
-
inIndexBlock = false;
|
|
1345
|
-
continue;
|
|
1346
|
-
}
|
|
1347
|
-
if (!line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1348
|
-
inPolittyBlock = false;
|
|
1349
|
-
inIndexBlock = false;
|
|
1350
|
-
continue;
|
|
1351
|
-
}
|
|
1352
|
-
if (!inPolittyBlock) continue;
|
|
1353
|
-
const indexEntry = line.match(/^(\s+)index\s*:\s*(.*)$/);
|
|
1354
|
-
if (indexEntry) {
|
|
1355
|
-
inIndexBlock = (indexEntry[2]?.trim() ?? "") === "";
|
|
1356
|
-
indexIndent = indexEntry[1]?.length ?? 0;
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
|
-
if (!inIndexBlock) continue;
|
|
1360
|
-
if ((line.match(/^(\s*)/)?.[1]?.length ?? 0) <= indexIndent) {
|
|
1361
|
-
inIndexBlock = false;
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
const property = line.match(/^\s+(title|description)\s*:\s*(.+)$/);
|
|
1365
|
-
if (!property) continue;
|
|
1366
|
-
const key = property[1];
|
|
1367
|
-
const value = stripYamlScalarQuotes(property[2] ?? "");
|
|
1368
|
-
if (key === "title") metadata.title = value;
|
|
1369
|
-
else if (key === "description") metadata.description = value;
|
|
1370
|
-
}
|
|
1371
|
-
return metadata;
|
|
1372
|
-
}
|
|
1373
|
-
function createTemplateExclusions(rawKeys) {
|
|
1374
|
-
return {
|
|
1375
|
-
rawKeys,
|
|
1376
|
-
commandScopes: /* @__PURE__ */ new Set(),
|
|
1377
|
-
commandSections: /* @__PURE__ */ new Map(),
|
|
1378
|
-
globalOptions: false,
|
|
1379
|
-
index: false
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
|
-
function setFileMapEntry(fileMap, commandPath, filePath) {
|
|
1383
|
-
Object.defineProperty(fileMap, commandPath, {
|
|
1384
|
-
value: filePath,
|
|
1385
|
-
enumerable: true,
|
|
1386
|
-
configurable: true,
|
|
1387
|
-
writable: true
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
/**
|
|
1391
|
-
* Normalize file mapping entry to FileConfig
|
|
1392
|
-
*/
|
|
1393
|
-
function normalizeFileConfig(config) {
|
|
1394
|
-
if (Array.isArray(config)) return { commands: config };
|
|
1395
|
-
if (!("commands" in config) || !Array.isArray(config.commands)) throw new Error("Invalid file config: object form must include a \"commands\" array. Use [] to skip generation intentionally.");
|
|
1396
|
-
return config;
|
|
1397
|
-
}
|
|
1398
|
-
/**
|
|
1399
|
-
* Check if a command path is a subcommand of another
|
|
1400
|
-
*/
|
|
1401
|
-
function isSubcommandOf(childPath, parentPath) {
|
|
1402
|
-
if (parentPath === "") return true;
|
|
1403
|
-
if (childPath === parentPath) return true;
|
|
1404
|
-
return childPath.startsWith(parentPath + " ");
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Check if a pattern contains wildcards
|
|
1408
|
-
*/
|
|
1409
|
-
function containsWildcard(pattern) {
|
|
1410
|
-
return pattern.includes("*");
|
|
1411
|
-
}
|
|
1412
|
-
/**
|
|
1413
|
-
* Check if a command path matches a wildcard pattern
|
|
1414
|
-
* - `*` matches any single command segment
|
|
1415
|
-
* - Pattern segments are space-separated
|
|
1416
|
-
*
|
|
1417
|
-
* @example
|
|
1418
|
-
* matchesWildcard("config get", "* *") // true
|
|
1419
|
-
* matchesWildcard("config", "* *") // false
|
|
1420
|
-
* matchesWildcard("config get", "config *") // true
|
|
1421
|
-
* matchesWildcard("greet", "*") // true
|
|
1422
|
-
*/
|
|
1423
|
-
function matchesWildcard(path, pattern) {
|
|
1424
|
-
const pathSegments = path === "" ? [] : path.split(" ");
|
|
1425
|
-
const patternSegments = pattern === "" ? [] : pattern.split(" ");
|
|
1426
|
-
if (pathSegments.length !== patternSegments.length) return false;
|
|
1427
|
-
for (let i = 0; i < patternSegments.length; i++) {
|
|
1428
|
-
const patternSeg = patternSegments[i];
|
|
1429
|
-
const pathSeg = pathSegments[i];
|
|
1430
|
-
if (patternSeg !== "*" && patternSeg !== pathSeg) return false;
|
|
1431
|
-
}
|
|
1432
|
-
return true;
|
|
1433
|
-
}
|
|
1434
|
-
/**
|
|
1435
|
-
* Expand a wildcard pattern to matching command paths
|
|
1436
|
-
*/
|
|
1437
|
-
function expandWildcardPattern(pattern, allCommands) {
|
|
1438
|
-
const matches = [];
|
|
1439
|
-
for (const cmdPath of allCommands.keys()) if (matchesWildcard(cmdPath, pattern)) matches.push(cmdPath);
|
|
1440
|
-
return matches;
|
|
1441
|
-
}
|
|
1442
|
-
/**
|
|
1443
|
-
* Check if a path matches any ignore pattern (with wildcard support)
|
|
1444
|
-
* For wildcard patterns, also ignores subcommands of matched commands
|
|
1445
|
-
*/
|
|
1446
|
-
function matchesIgnorePattern(path, ignorePattern) {
|
|
1447
|
-
if (containsWildcard(ignorePattern)) {
|
|
1448
|
-
if (matchesWildcard(path, ignorePattern)) return true;
|
|
1449
|
-
const pathSegments = path === "" ? [] : path.split(" ");
|
|
1450
|
-
const patternSegments = ignorePattern === "" ? [] : ignorePattern.split(" ");
|
|
1451
|
-
if (pathSegments.length > patternSegments.length) return matchesWildcard(pathSegments.slice(0, patternSegments.length).join(" "), ignorePattern);
|
|
1452
|
-
return false;
|
|
1453
|
-
}
|
|
1454
|
-
return isSubcommandOf(path, ignorePattern);
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* Expand command paths to include all subcommands (with wildcard support)
|
|
1458
|
-
*/
|
|
1459
|
-
function expandCommandPaths(commandPaths, allCommands) {
|
|
1460
|
-
const expanded = /* @__PURE__ */ new Set();
|
|
1461
|
-
const resolved = commandPaths.flatMap((cmdPath) => containsWildcard(cmdPath) ? expandWildcardPattern(cmdPath, allCommands) : [cmdPath]);
|
|
1462
|
-
for (const cmdPath of resolved) for (const existingPath of allCommands.keys()) if (isSubcommandOf(existingPath, cmdPath)) expanded.add(existingPath);
|
|
1463
|
-
return Array.from(expanded);
|
|
1464
|
-
}
|
|
1465
|
-
/**
|
|
1466
|
-
* Filter out ignored commands (with wildcard support)
|
|
1467
|
-
*/
|
|
1468
|
-
function filterIgnoredCommands(commandPaths, ignores) {
|
|
1469
|
-
return commandPaths.filter((path) => {
|
|
1470
|
-
return !ignores.some((ignorePattern) => matchesIgnorePattern(path, ignorePattern));
|
|
1471
|
-
});
|
|
1472
|
-
}
|
|
1473
|
-
/**
|
|
1474
|
-
* Resolve wildcards to direct matches without subcommand expansion.
|
|
1475
|
-
* Returns the "top-level" commands for use in CommandCategory.commands,
|
|
1476
|
-
* where expandCommands in render-index handles subcommand expansion.
|
|
1477
|
-
*/
|
|
1478
|
-
function resolveTopLevelCommands(specifiedCommands, allCommands) {
|
|
1479
|
-
const result = [];
|
|
1480
|
-
for (const cmdPath of specifiedCommands) if (containsWildcard(cmdPath)) result.push(...expandWildcardPattern(cmdPath, allCommands));
|
|
1481
|
-
else if (allCommands.has(cmdPath)) result.push(cmdPath);
|
|
1482
|
-
return result;
|
|
1483
|
-
}
|
|
1484
|
-
/**
|
|
1485
|
-
* Resolve file command configuration to concrete command paths.
|
|
1486
|
-
* This applies wildcard/subcommand expansion and ignore filtering.
|
|
1487
|
-
*/
|
|
1488
|
-
function resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores) {
|
|
1489
|
-
const fileConfig = normalizeFileConfig(fileConfigRaw);
|
|
1490
|
-
const specifiedCommands = fileConfig.commands;
|
|
1491
|
-
return {
|
|
1492
|
-
fileConfig,
|
|
1493
|
-
specifiedCommands,
|
|
1494
|
-
commandPaths: filterIgnoredCommands(fileConfig.noExpand ? specifiedCommands.filter((p) => allCommands.has(p)) : expandCommandPaths(specifiedCommands, allCommands), ignores),
|
|
1495
|
-
topLevelCommands: filterIgnoredCommands(resolveTopLevelCommands(specifiedCommands, allCommands), ignores)
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
/**
|
|
1499
|
-
* Validate that there are no conflicts between files and ignores (with wildcard support)
|
|
1500
|
-
*/
|
|
1501
|
-
function validateNoConflicts(filesCommands, ignores, allCommands) {
|
|
1502
|
-
const conflicts = [];
|
|
1503
|
-
for (const filePattern of filesCommands) {
|
|
1504
|
-
const filePaths = containsWildcard(filePattern) ? expandWildcardPattern(filePattern, allCommands) : [filePattern];
|
|
1505
|
-
for (const filePath of filePaths) for (const ignorePattern of ignores) if (containsWildcard(ignorePattern)) {
|
|
1506
|
-
if (matchesWildcard(filePath, ignorePattern)) conflicts.push(`"${filePath}" is both in files and ignored by "${ignorePattern}"`);
|
|
1507
|
-
} else if (filePath === ignorePattern || isSubcommandOf(filePath, ignorePattern)) conflicts.push(`"${filePath}" is both in files and ignored by "${ignorePattern}"`);
|
|
1508
|
-
}
|
|
1509
|
-
if (conflicts.length > 0) throw new Error(`Conflict between files and ignores:\n - ${conflicts.join("\n - ")}`);
|
|
1510
|
-
}
|
|
1511
|
-
/**
|
|
1512
|
-
* Validate that all ignored paths exist in the command tree (with wildcard support)
|
|
1513
|
-
*/
|
|
1514
|
-
function validateIgnoresExist(ignores, allCommands) {
|
|
1515
|
-
const nonExistent = [];
|
|
1516
|
-
for (const ignorePattern of ignores) if (containsWildcard(ignorePattern)) {
|
|
1517
|
-
if (expandWildcardPattern(ignorePattern, allCommands).length === 0) nonExistent.push(`"${ignorePattern}"`);
|
|
1518
|
-
} else if (!allCommands.has(ignorePattern)) nonExistent.push(`"${ignorePattern}"`);
|
|
1519
|
-
if (nonExistent.length > 0) throw new Error(`Ignored command paths do not exist: ${nonExistent.join(", ")}`);
|
|
1520
|
-
}
|
|
1521
|
-
/**
|
|
1522
|
-
* Sort command paths in depth-first order while preserving the specified command order
|
|
1523
|
-
* Parent commands are immediately followed by their subcommands
|
|
1524
|
-
*/
|
|
1525
|
-
function sortDepthFirst(commandPaths, specifiedOrder) {
|
|
1526
|
-
const pathSet = new Set(commandPaths);
|
|
1527
|
-
const topLevelPaths = specifiedOrder.filter((cmd) => pathSet.has(cmd));
|
|
1528
|
-
for (const path of commandPaths) if ((path === "" ? 0 : path.split(" ").length) === 1 && !topLevelPaths.includes(path)) topLevelPaths.push(path);
|
|
1529
|
-
const result = [];
|
|
1530
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1531
|
-
function addWithChildren(cmdPath) {
|
|
1532
|
-
if (visited.has(cmdPath) || !pathSet.has(cmdPath)) return;
|
|
1533
|
-
visited.add(cmdPath);
|
|
1534
|
-
result.push(cmdPath);
|
|
1535
|
-
const children = commandPaths.filter((p) => {
|
|
1536
|
-
if (p === cmdPath || visited.has(p)) return false;
|
|
1537
|
-
if (cmdPath === "") return p.split(" ").length === 1;
|
|
1538
|
-
return p.startsWith(cmdPath + " ") && p.split(" ").length === cmdPath.split(" ").length + 1;
|
|
1539
|
-
}).sort((a, b) => a.localeCompare(b));
|
|
1540
|
-
for (const child of children) addWithChildren(child);
|
|
1541
|
-
}
|
|
1542
|
-
for (const topLevel of topLevelPaths) addWithChildren(topLevel);
|
|
1543
|
-
for (const path of commandPaths) if (!visited.has(path)) result.push(path);
|
|
1544
|
-
return result;
|
|
1545
|
-
}
|
|
1546
|
-
function generateFileHeader(fileConfig) {
|
|
1547
|
-
if (!fileConfig.title && !fileConfig.description) return null;
|
|
1548
|
-
const parts = [];
|
|
1549
|
-
if (fileConfig.title) {
|
|
1550
|
-
const heading = "#".repeat(fileConfig.headingLevel ?? 1);
|
|
1551
|
-
parts.push(`${heading} ${fileConfig.title}`);
|
|
1552
|
-
}
|
|
1553
|
-
if (fileConfig.description) {
|
|
1554
|
-
parts.push("");
|
|
1555
|
-
parts.push(fileConfig.description);
|
|
1556
|
-
}
|
|
1557
|
-
parts.push("");
|
|
1558
|
-
return parts.join("\n");
|
|
1559
|
-
}
|
|
1560
|
-
/**
|
|
1561
|
-
* Extract a leading file header (title and optional description paragraph)
|
|
1562
|
-
*/
|
|
1563
|
-
function extractFileHeader(content) {
|
|
1564
|
-
if (!/^#{1,6} /.test(content)) return null;
|
|
1565
|
-
const titleEnd = content.indexOf("\n");
|
|
1566
|
-
if (titleEnd === -1) return content;
|
|
1567
|
-
let cursor = titleEnd + 1;
|
|
1568
|
-
if (content[cursor] === "\n") cursor += 1;
|
|
1569
|
-
while (cursor < content.length) {
|
|
1570
|
-
const lineEnd = content.indexOf("\n", cursor);
|
|
1571
|
-
const line = lineEnd === -1 ? content.slice(cursor) : content.slice(cursor, lineEnd);
|
|
1572
|
-
if (line.length === 0 || /^#{1,6}\s/.test(line) || line.startsWith("<!-- politty:")) break;
|
|
1573
|
-
cursor = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
1574
|
-
}
|
|
1575
|
-
return content.slice(0, cursor);
|
|
1576
|
-
}
|
|
1577
|
-
/**
|
|
1578
|
-
* Validate and optionally update configured file header
|
|
1579
|
-
*/
|
|
1580
|
-
function processFileHeader(existingContent, fileConfig, updateMode) {
|
|
1581
|
-
const generatedHeader = generateFileHeader(fileConfig);
|
|
1582
|
-
if (!generatedHeader) return {
|
|
1583
|
-
content: existingContent,
|
|
1584
|
-
hasError: false,
|
|
1585
|
-
wasUpdated: false
|
|
1586
|
-
};
|
|
1587
|
-
if (existingContent.startsWith(generatedHeader)) return {
|
|
1588
|
-
content: existingContent,
|
|
1589
|
-
hasError: false,
|
|
1590
|
-
wasUpdated: false
|
|
1591
|
-
};
|
|
1592
|
-
const existingHeader = extractFileHeader(existingContent) ?? "";
|
|
1593
|
-
if (!updateMode) return {
|
|
1594
|
-
content: existingContent,
|
|
1595
|
-
diff: formatDiff(existingHeader, generatedHeader),
|
|
1596
|
-
hasError: true,
|
|
1597
|
-
wasUpdated: false
|
|
1598
|
-
};
|
|
1599
|
-
return {
|
|
1600
|
-
content: `${generatedHeader}${(existingHeader ? existingContent.slice(existingHeader.length) : existingContent).replace(/^\n+/, "")}`,
|
|
1601
|
-
hasError: false,
|
|
1602
|
-
wasUpdated: true
|
|
1603
|
-
};
|
|
1604
|
-
}
|
|
1605
|
-
function formatCommandPath(commandPath) {
|
|
1606
|
-
return commandPath === "" ? "<root>" : commandPath;
|
|
1607
|
-
}
|
|
1608
|
-
/**
|
|
1609
|
-
* Extract a section marker's content from document content.
|
|
1610
|
-
* Returns the content between start and end markers (including markers).
|
|
1611
|
-
*/
|
|
1612
|
-
function extractSectionMarker(content, type, scope) {
|
|
1613
|
-
return extractMarkerSection(content, sectionStartMarker(type, scope), sectionEndMarker(type, scope));
|
|
1614
|
-
}
|
|
1615
|
-
/**
|
|
1616
|
-
* Replace a section marker's content in document content.
|
|
1617
|
-
* Returns updated content, or null if marker not found.
|
|
1618
|
-
*/
|
|
1619
|
-
function replaceSectionMarker(content, type, scope, newContent) {
|
|
1620
|
-
return replaceMarkerSection(content, sectionStartMarker(type, scope), sectionEndMarker(type, scope), newContent);
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Insert a new section marker into existing content at the correct position
|
|
1624
|
-
* relative to other section markers for the same command, based on SECTION_TYPES order.
|
|
1625
|
-
* Preserves any existing content between adjacent markers by wrapping it with the new markers
|
|
1626
|
-
* instead of replacing it with generated content.
|
|
1627
|
-
* @throws If no adjacent marker is found (unreachable when at least one marker exists for the command)
|
|
1628
|
-
*/
|
|
1629
|
-
function insertSectionMarkerAtOrder(content, type, scope, generatedSection) {
|
|
1630
|
-
const typeIndex = SECTION_TYPES.indexOf(type);
|
|
1631
|
-
const startMarker = sectionStartMarker(type, scope);
|
|
1632
|
-
const endMarker = sectionEndMarker(type, scope);
|
|
1633
|
-
let prevBoundary = null;
|
|
1634
|
-
for (let i = typeIndex - 1; i >= 0; i--) {
|
|
1635
|
-
const prevType = SECTION_TYPES[i];
|
|
1636
|
-
const prevEnd = sectionEndMarker(prevType, scope);
|
|
1637
|
-
const prevEndIdx = content.indexOf(prevEnd);
|
|
1638
|
-
if (prevEndIdx !== -1) {
|
|
1639
|
-
prevBoundary = prevEndIdx + prevEnd.length;
|
|
1640
|
-
break;
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
let nextBoundary = null;
|
|
1644
|
-
for (let i = typeIndex + 1; i < SECTION_TYPES.length; i++) {
|
|
1645
|
-
const nextType = SECTION_TYPES[i];
|
|
1646
|
-
const nextStart = sectionStartMarker(nextType, scope);
|
|
1647
|
-
const nextStartIdx = content.indexOf(nextStart);
|
|
1648
|
-
if (nextStartIdx !== -1) {
|
|
1649
|
-
nextBoundary = nextStartIdx;
|
|
1650
|
-
break;
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
if (prevBoundary != null && nextBoundary != null) {
|
|
1654
|
-
const wrapped = startMarker + content.slice(prevBoundary, nextBoundary).replace(/^\n+/, "\n").replace(/\n+$/, "\n") + endMarker;
|
|
1655
|
-
return content.slice(0, prevBoundary) + "\n\n" + wrapped + "\n\n" + content.slice(nextBoundary);
|
|
1656
|
-
}
|
|
1657
|
-
if (prevBoundary != null) {
|
|
1658
|
-
let afterPos = prevBoundary;
|
|
1659
|
-
while (afterPos < content.length && content[afterPos] === "\n") afterPos++;
|
|
1660
|
-
return content.slice(0, prevBoundary) + "\n\n" + generatedSection + (afterPos < content.length ? "\n\n" : "\n") + content.slice(afterPos);
|
|
1661
|
-
}
|
|
1662
|
-
if (nextBoundary != null) {
|
|
1663
|
-
let beforePos = nextBoundary;
|
|
1664
|
-
while (beforePos > 0 && content[beforePos - 1] === "\n") beforePos--;
|
|
1665
|
-
const prefix = beforePos === 0 ? "" : "\n\n";
|
|
1666
|
-
return content.slice(0, beforePos) + prefix + generatedSection + "\n\n" + content.slice(nextBoundary);
|
|
1667
|
-
}
|
|
1668
|
-
throw new Error(`No insertion point found for section "${type}" (scope="${scope}"). This should be unreachable when at least one marker exists for the command.`);
|
|
1669
|
-
}
|
|
1670
|
-
/**
|
|
1671
|
-
* Collect all section types that have markers for a given command path.
|
|
1672
|
-
*/
|
|
1673
|
-
function collectSectionMarkers(content, commandPath) {
|
|
1674
|
-
const found = [];
|
|
1675
|
-
for (const type of SECTION_TYPES) if (extractSectionMarker(content, type, commandPath) !== null) found.push(type);
|
|
1676
|
-
return found;
|
|
1677
|
-
}
|
|
1678
|
-
/**
|
|
1679
|
-
* Collect all command paths that have any section markers in the content.
|
|
1680
|
-
*/
|
|
1681
|
-
function collectSectionMarkerPaths(content) {
|
|
1682
|
-
const sectionTypes = SECTION_TYPES.join("|");
|
|
1683
|
-
const markerPattern = new RegExp(`<!--\\s*politty:command:(.*?):(?:${sectionTypes}):start\\s*-->`, "g");
|
|
1684
|
-
const paths = /* @__PURE__ */ new Set();
|
|
1685
|
-
for (const match of content.matchAll(markerPattern)) paths.add(match[1] ?? "");
|
|
1686
|
-
return Array.from(paths);
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Insert command section markers at the correct position based on specified order.
|
|
1690
|
-
* Uses the heading marker of adjacent commands as reference points.
|
|
1691
|
-
*/
|
|
1692
|
-
function insertCommandSections(content, commandPath, newSection, specifiedOrder) {
|
|
1693
|
-
const targetIndex = specifiedOrder.indexOf(commandPath);
|
|
1694
|
-
if (targetIndex === -1) return content.trimEnd() + "\n\n" + newSection + "\n";
|
|
1695
|
-
for (let i = targetIndex + 1; i < specifiedOrder.length; i++) {
|
|
1696
|
-
const nextCmd = specifiedOrder[i];
|
|
1697
|
-
if (nextCmd === void 0) continue;
|
|
1698
|
-
const nextMarker = sectionStartMarker("heading", nextCmd);
|
|
1699
|
-
const nextIndex = content.indexOf(nextMarker);
|
|
1700
|
-
if (nextIndex !== -1) {
|
|
1701
|
-
let insertPos = nextIndex;
|
|
1702
|
-
while (insertPos > 0 && content[insertPos - 1] === "\n") insertPos--;
|
|
1703
|
-
if (insertPos < nextIndex) insertPos++;
|
|
1704
|
-
return content.slice(0, insertPos) + newSection + "\n" + content.slice(nextIndex);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
for (let i = targetIndex - 1; i >= 0; i--) {
|
|
1708
|
-
const prevCmd = specifiedOrder[i];
|
|
1709
|
-
if (prevCmd === void 0) continue;
|
|
1710
|
-
const prevMarkers = collectSectionMarkers(content, prevCmd);
|
|
1711
|
-
if (prevMarkers.length > 0) {
|
|
1712
|
-
const lastType = prevMarkers[prevMarkers.length - 1];
|
|
1713
|
-
const prevEndMarker = sectionEndMarker(lastType, prevCmd);
|
|
1714
|
-
const prevEndIndex = content.indexOf(prevEndMarker);
|
|
1715
|
-
if (prevEndIndex !== -1) {
|
|
1716
|
-
const insertPos = prevEndIndex + prevEndMarker.length;
|
|
1717
|
-
return content.slice(0, insertPos) + "\n" + newSection + content.slice(insertPos);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
return content.trimEnd() + "\n" + newSection + "\n";
|
|
1722
|
-
}
|
|
1723
|
-
/**
|
|
1724
|
-
* Remove all section markers for a command from content.
|
|
1725
|
-
* Returns the content with all markers for the command removed and excess blank lines cleaned up.
|
|
1726
|
-
*/
|
|
1727
|
-
function removeCommandSections(content, commandPath) {
|
|
1728
|
-
const markers = collectSectionMarkers(content, commandPath);
|
|
1729
|
-
for (const type of markers) {
|
|
1730
|
-
const start = sectionStartMarker(type, commandPath);
|
|
1731
|
-
const end = sectionEndMarker(type, commandPath);
|
|
1732
|
-
let startIndex = content.indexOf(start);
|
|
1733
|
-
while (startIndex !== -1) {
|
|
1734
|
-
const endIndex = content.indexOf(end, startIndex);
|
|
1735
|
-
if (endIndex === -1) break;
|
|
1736
|
-
content = content.slice(0, startIndex) + content.slice(endIndex + end.length);
|
|
1737
|
-
startIndex = content.indexOf(start, startIndex);
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
content = content.replace(/\n{3,}/g, "\n\n");
|
|
1741
|
-
return content;
|
|
1742
|
-
}
|
|
1743
|
-
/**
|
|
1744
|
-
* Strip politty marker lines from content, then collapse the blank-line gaps the removed markers
|
|
1745
|
-
* leave behind (outside fenced code blocks only, so intentional blank lines inside generated
|
|
1746
|
-
* example/code blocks are preserved) and trim leading/trailing blank lines.
|
|
1747
|
-
*/
|
|
1748
|
-
function stripPolittyMarkers(content) {
|
|
1749
|
-
let result = collapseBlankLinesOutsideCodeFences(content.split("\n").filter((line) => !/^<!-- politty:.*-->$/.test(line.trim())).join("\n"));
|
|
1750
|
-
result = result.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
1751
|
-
return result;
|
|
1752
|
-
}
|
|
1753
|
-
/**
|
|
1754
|
-
* Collapse runs of 3+ newlines to 2, but only outside fenced code blocks so that intentional
|
|
1755
|
-
* blank lines inside handwritten code samples are preserved. Fences are lines whose trimmed
|
|
1756
|
-
* content starts with ``` or ~~~.
|
|
1757
|
-
*/
|
|
1758
|
-
function collapseBlankLinesOutsideCodeFences(content) {
|
|
1759
|
-
const lines = content.split("\n");
|
|
1760
|
-
const out = [];
|
|
1761
|
-
let inFence = false;
|
|
1762
|
-
let blankRun = 0;
|
|
1763
|
-
for (const line of lines) {
|
|
1764
|
-
const trimmed = line.trim();
|
|
1765
|
-
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
1766
|
-
inFence = !inFence;
|
|
1767
|
-
blankRun = 0;
|
|
1768
|
-
out.push(line);
|
|
1769
|
-
continue;
|
|
1770
|
-
}
|
|
1771
|
-
if (!inFence && line.trim() === "") {
|
|
1772
|
-
blankRun++;
|
|
1773
|
-
if (blankRun >= 2) continue;
|
|
1774
|
-
} else if (!inFence) blankRun = 0;
|
|
1775
|
-
out.push(line);
|
|
1776
|
-
}
|
|
1777
|
-
return out.join("\n");
|
|
1778
|
-
}
|
|
1779
|
-
function detectLineEnding(content) {
|
|
1780
|
-
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
1781
|
-
}
|
|
1782
|
-
function countLineBreaks(value) {
|
|
1783
|
-
return (value.match(/\n/g) ?? []).length;
|
|
1784
|
-
}
|
|
1785
|
-
/**
|
|
1786
|
-
* Type guard for SectionType values parsed from template placeholders.
|
|
1787
|
-
*/
|
|
1788
|
-
function isSectionType(value) {
|
|
1789
|
-
return SECTION_TYPES.some((type) => type === value);
|
|
1790
|
-
}
|
|
1791
|
-
/**
|
|
1792
|
-
* Clamp a numeric heading level to the valid HeadingLevel range (1–6).
|
|
1793
|
-
* Uses a switch to return a literal union member, avoiding `as` assertions.
|
|
1794
|
-
*/
|
|
1795
|
-
function clampHeadingLevel(level) {
|
|
1796
|
-
switch (Math.min(6, Math.max(1, Math.trunc(level)))) {
|
|
1797
|
-
case 1: return 1;
|
|
1798
|
-
case 2: return 2;
|
|
1799
|
-
case 3: return 3;
|
|
1800
|
-
case 4: return 4;
|
|
1801
|
-
case 5: return 5;
|
|
1802
|
-
default: return 6;
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
function resolveTemplateCommandScope(tokens, allCommands) {
|
|
1806
|
-
if (tokens.length === 0) return allCommands === void 0 || allCommands.has("") ? "" : null;
|
|
1807
|
-
const exactScope = tokens.join(":");
|
|
1808
|
-
if (allCommands?.has(exactScope)) return exactScope;
|
|
1809
|
-
const colonSeparatedScope = tokens.join(" ");
|
|
1810
|
-
if (allCommands?.has(colonSeparatedScope)) return colonSeparatedScope;
|
|
1811
|
-
return allCommands === void 0 ? colonSeparatedScope : null;
|
|
1812
|
-
}
|
|
1813
|
-
function templateScopeFallback(tokens) {
|
|
1814
|
-
return tokens.join(" ");
|
|
1815
|
-
}
|
|
1816
|
-
/**
|
|
1817
|
-
* Parse a single {{politty:...}} placeholder string into a discriminated structure.
|
|
1818
|
-
* The `placeholder` argument should be the full `{{politty:...}}` text.
|
|
1819
|
-
*
|
|
1820
|
-
* Uses String.match / String.replace internally (not .exec) to avoid lastIndex
|
|
1821
|
-
* state issues from the shared TEMPLATE_PLACEHOLDER_REGEX constant.
|
|
1822
|
-
*/
|
|
1823
|
-
function parsePlaceholder(placeholder, allCommands) {
|
|
1824
|
-
const tokens = placeholder.slice(2, -2).split(":");
|
|
1825
|
-
const directive = tokens[1];
|
|
1826
|
-
if (directive === "command") {
|
|
1827
|
-
const rest = tokens.slice(2);
|
|
1828
|
-
if (rest.length === 1 && rest[0] === "") return {
|
|
1829
|
-
kind: "invalid",
|
|
1830
|
-
reason: `Trailing colon in "${placeholder}"; use {{politty:command}} for the root command.`
|
|
1831
|
-
};
|
|
1832
|
-
const fullScope = resolveTemplateCommandScope(rest, allCommands);
|
|
1833
|
-
if (fullScope !== null) return {
|
|
1834
|
-
kind: "command",
|
|
1835
|
-
scope: fullScope,
|
|
1836
|
-
type: void 0
|
|
1837
|
-
};
|
|
1838
|
-
if (rest.length >= 2) {
|
|
1839
|
-
const last = rest[rest.length - 1];
|
|
1840
|
-
const scopeTokens = rest.slice(0, -1);
|
|
1841
|
-
const sectionScope = resolveTemplateCommandScope(scopeTokens, allCommands);
|
|
1842
|
-
if (last !== void 0 && isSectionType(last)) return {
|
|
1843
|
-
kind: "command",
|
|
1844
|
-
scope: sectionScope ?? templateScopeFallback(scopeTokens),
|
|
1845
|
-
type: last
|
|
1846
|
-
};
|
|
1847
|
-
if (last !== void 0 && sectionScope !== null) return {
|
|
1848
|
-
kind: "invalid",
|
|
1849
|
-
reason: `Unknown section type "${last}" for command scope "${formatCommandPath(sectionScope)}". Valid section types: ${SECTION_TYPES.join(", ")}`
|
|
1850
|
-
};
|
|
1851
|
-
}
|
|
1852
|
-
return {
|
|
1853
|
-
kind: "command",
|
|
1854
|
-
scope: templateScopeFallback(rest),
|
|
1855
|
-
type: void 0
|
|
1856
|
-
};
|
|
1857
|
-
}
|
|
1858
|
-
if (directive === "global-options") {
|
|
1859
|
-
if (tokens.length !== 2) return {
|
|
1860
|
-
kind: "invalid",
|
|
1861
|
-
reason: `Malformed placeholder "${placeholder}". Expected {{politty:global-options}}.`
|
|
1862
|
-
};
|
|
1863
|
-
return { kind: "global-options" };
|
|
1864
|
-
}
|
|
1865
|
-
if (directive === "index") {
|
|
1866
|
-
if (tokens.length !== 2) return {
|
|
1867
|
-
kind: "invalid",
|
|
1868
|
-
reason: `Malformed placeholder "${placeholder}". Expected {{politty:index}}.`
|
|
1869
|
-
};
|
|
1870
|
-
return { kind: "index" };
|
|
1871
|
-
}
|
|
1872
|
-
return {
|
|
1873
|
-
kind: "invalid",
|
|
1874
|
-
reason: `Unknown politty directive "${directive ?? ""}" in "${placeholder}". Valid directives: command, global-options, index`
|
|
1875
|
-
};
|
|
1876
|
-
}
|
|
1877
|
-
function buildTemplateExclusions(rawKeys, allCommands) {
|
|
1878
|
-
const exclusions = createTemplateExclusions(rawKeys);
|
|
1879
|
-
for (const key of rawKeys) {
|
|
1880
|
-
const parsed = parsePlaceholder(`{{politty:${key}}}`, allCommands);
|
|
1881
|
-
if (parsed.kind === "command") if (parsed.type === void 0) exclusions.commandScopes.add(parsed.scope);
|
|
1882
|
-
else {
|
|
1883
|
-
let sections = exclusions.commandSections.get(parsed.scope);
|
|
1884
|
-
if (!sections) {
|
|
1885
|
-
sections = /* @__PURE__ */ new Set();
|
|
1886
|
-
exclusions.commandSections.set(parsed.scope, sections);
|
|
1887
|
-
}
|
|
1888
|
-
sections.add(parsed.type);
|
|
1889
|
-
}
|
|
1890
|
-
else if (parsed.kind === "global-options") exclusions.globalOptions = true;
|
|
1891
|
-
else if (parsed.kind === "index") exclusions.index = true;
|
|
1892
|
-
}
|
|
1893
|
-
return exclusions;
|
|
1894
|
-
}
|
|
1895
|
-
function isCommandScopeExcluded(commandPath, excludedCommandScopes) {
|
|
1896
|
-
for (const excludedScope of excludedCommandScopes) if (isSubcommandOf(commandPath, excludedScope)) return true;
|
|
1897
|
-
return false;
|
|
1898
|
-
}
|
|
1899
|
-
function isCommandSectionExcluded(commandPath, sectionType, exclusions) {
|
|
1900
|
-
if (isCommandScopeExcluded(commandPath, exclusions.commandScopes)) return true;
|
|
1901
|
-
return exclusions.commandSections.get(commandPath)?.has(sectionType) ?? false;
|
|
1902
|
-
}
|
|
1903
|
-
function getTemplateCommandTreePaths(commandPath, allCommands, ignores, exclusions) {
|
|
1904
|
-
return sortDepthFirst(filterIgnoredCommands(expandCommandPaths([commandPath], allCommands), ignores).filter((path) => !isCommandScopeExcluded(path, exclusions.commandScopes)), [commandPath]);
|
|
1905
|
-
}
|
|
1906
|
-
function shouldSkipTemplatePlaceholder(placeholder, parsed, exclusions) {
|
|
1907
|
-
if (exclusions.rawKeys.has(templatePlaceholderKey(placeholder))) return true;
|
|
1908
|
-
if (parsed.kind === "command") {
|
|
1909
|
-
if (isCommandScopeExcluded(parsed.scope, exclusions.commandScopes)) return true;
|
|
1910
|
-
return parsed.type !== void 0 && (exclusions.commandSections.get(parsed.scope)?.has(parsed.type) ?? false);
|
|
1911
|
-
}
|
|
1912
|
-
if (parsed.kind === "global-options") return exclusions.globalOptions;
|
|
1913
|
-
if (parsed.kind === "index") return exclusions.index;
|
|
1914
|
-
return false;
|
|
1915
|
-
}
|
|
1916
|
-
function isRawCommandPlaceholderUnderExcludedScope(key, exclusions) {
|
|
1917
|
-
if (!key.startsWith("command:")) return false;
|
|
1918
|
-
const tokens = key.slice(8).split(":");
|
|
1919
|
-
for (const excludedScope of exclusions.commandScopes) {
|
|
1920
|
-
if (excludedScope === "") return true;
|
|
1921
|
-
const spaceTokens = excludedScope.split(" ");
|
|
1922
|
-
if (tokens.slice(0, spaceTokens.length).join(" ") === excludedScope) return true;
|
|
1923
|
-
const colonTokens = excludedScope.split(":");
|
|
1924
|
-
if (tokens.slice(0, colonTokens.length).join(":") === excludedScope) return true;
|
|
1925
|
-
}
|
|
1926
|
-
return false;
|
|
1927
|
-
}
|
|
1928
|
-
/**
|
|
1929
|
-
* Regex matching {{politty:...}} placeholders.
|
|
1930
|
-
* NOTE: only use with String.match / String.replace, never with .exec in a loop,
|
|
1931
|
-
* because the /g flag makes the regex stateful via lastIndex.
|
|
1932
|
-
*/
|
|
1933
|
-
const TEMPLATE_PLACEHOLDER_REGEX = /\{\{politty:[^{}]*\}\}/g;
|
|
1934
|
-
function validateTemplatePlaceholderSyntax(templateContent, templatePath) {
|
|
1935
|
-
const validPlaceholderStarts = /* @__PURE__ */ new Set();
|
|
1936
|
-
for (const match of templateContent.matchAll(TEMPLATE_PLACEHOLDER_REGEX)) {
|
|
1937
|
-
const start = match.index;
|
|
1938
|
-
const end = start + match[0].length;
|
|
1939
|
-
if (templateContent[start - 1] === "{" || templateContent[end] === "}") {
|
|
1940
|
-
const snippet = templateContent.slice(Math.max(0, start - 1), Math.min(templateContent.length, end + 1)).split("\n")[0];
|
|
1941
|
-
throw new Error(`Malformed politty placeholder in template "${templatePath}": "${snippet}". Expected {{politty:...}}.`);
|
|
1942
|
-
}
|
|
1943
|
-
validPlaceholderStarts.add(start);
|
|
1944
|
-
}
|
|
1945
|
-
let searchIndex = 0;
|
|
1946
|
-
while (true) {
|
|
1947
|
-
const placeholderStart = templateContent.indexOf("{{politty:", searchIndex);
|
|
1948
|
-
if (placeholderStart === -1) return;
|
|
1949
|
-
if (!validPlaceholderStarts.has(placeholderStart)) {
|
|
1950
|
-
const snippet = templateContent.slice(placeholderStart, placeholderStart + 80).split("\n")[0];
|
|
1951
|
-
throw new Error(`Malformed politty placeholder in template "${templatePath}": "${snippet}". Expected {{politty:...}}.`);
|
|
1952
|
-
}
|
|
1953
|
-
searchIndex = placeholderStart + 10;
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
function getUnknownSectionTypeError(scope, allCommands) {
|
|
1957
|
-
const separatorIndex = scope.lastIndexOf(":");
|
|
1958
|
-
if (separatorIndex === -1) return null;
|
|
1959
|
-
const commandScope = scope.slice(0, separatorIndex);
|
|
1960
|
-
const sectionType = scope.slice(separatorIndex + 1);
|
|
1961
|
-
if (sectionType === "" || !allCommands.has(commandScope)) return null;
|
|
1962
|
-
return `Unknown section type "${sectionType}" for command scope "${formatCommandPath(commandScope)}". Valid section types: ${SECTION_TYPES.join(", ")}`;
|
|
1963
|
-
}
|
|
1964
|
-
/**
|
|
1965
|
-
* Extract a marker section from content
|
|
1966
|
-
* Returns the content between start and end markers (including markers)
|
|
1967
|
-
*/
|
|
1968
|
-
function extractMarkerSection(content, startMarker, endMarker) {
|
|
1969
|
-
const startIndex = content.indexOf(startMarker);
|
|
1970
|
-
if (startIndex === -1) return null;
|
|
1971
|
-
const endIndex = content.indexOf(endMarker, startIndex);
|
|
1972
|
-
if (endIndex === -1) return null;
|
|
1973
|
-
return content.slice(startIndex, endIndex + endMarker.length);
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* Replace a marker section in content
|
|
1977
|
-
* Returns the updated content with the new section
|
|
1978
|
-
*/
|
|
1979
|
-
function replaceMarkerSection(content, startMarker, endMarker, newSection) {
|
|
1980
|
-
const startIndex = content.indexOf(startMarker);
|
|
1981
|
-
if (startIndex === -1) return null;
|
|
1982
|
-
const endIndex = content.indexOf(endMarker, startIndex);
|
|
1983
|
-
if (endIndex === -1) return null;
|
|
1984
|
-
return content.slice(0, startIndex) + newSection + content.slice(endIndex + endMarker.length);
|
|
1985
|
-
}
|
|
1986
|
-
/**
|
|
1987
|
-
* Check if config is the { args, options? } shape (not shorthand ArgsShape)
|
|
1988
|
-
*
|
|
1989
|
-
* Distinguishes between:
|
|
1990
|
-
* - { args: ArgsShape, options?: ArgsTableOptions } → returns true
|
|
1991
|
-
* - ArgsShape (e.g., { verbose: ZodType, args: ZodType }) → returns false
|
|
1992
|
-
*
|
|
1993
|
-
* The key insight is that in the { args, options? } shape, config.args is an ArgsShape
|
|
1994
|
-
* (Record of ZodTypes), while in shorthand, config itself is the ArgsShape and config.args
|
|
1995
|
-
* would be a single ZodType if user has an option named "args".
|
|
1996
|
-
*/
|
|
1997
|
-
function isGlobalOptionsConfigWithOptions(config) {
|
|
1998
|
-
if (typeof config !== "object" || config === null || !("args" in config)) return false;
|
|
1999
|
-
return !(config.args instanceof zod.z.ZodType);
|
|
2000
|
-
}
|
|
2001
|
-
/**
|
|
2002
|
-
* Collect option fields that are actually rendered by global options markers.
|
|
2003
|
-
* Positional args are not rendered in args tables, so they must not be excluded.
|
|
2004
|
-
*/
|
|
2005
|
-
function collectRenderableGlobalOptionFields(argsShape) {
|
|
2006
|
-
return require_schema_extractor.extractFields(zod.z.object(argsShape)).fields.filter((field) => !field.positional);
|
|
2007
|
-
}
|
|
2008
|
-
/**
|
|
2009
|
-
* Compare option definitions for global-options compatibility.
|
|
2010
|
-
*/
|
|
2011
|
-
function areGlobalOptionsEquivalent(a, b) {
|
|
2012
|
-
const { schema: _aSchema, ...aRest } = a;
|
|
2013
|
-
const { schema: _bSchema, ...bRest } = b;
|
|
2014
|
-
return (0, node_util.isDeepStrictEqual)(aRest, bRest);
|
|
2015
|
-
}
|
|
2016
|
-
/**
|
|
2017
|
-
* Normalize rootDoc.globalOptions to { args, options? } form.
|
|
2018
|
-
*/
|
|
2019
|
-
function normalizeGlobalOptions(config) {
|
|
2020
|
-
if (!config) return void 0;
|
|
2021
|
-
return isGlobalOptionsConfigWithOptions(config) ? config : { args: config };
|
|
2022
|
-
}
|
|
2023
|
-
/**
|
|
2024
|
-
* Derive an ArgsShape from a globalArgs Zod schema, retaining only non-positional option fields.
|
|
2025
|
-
* Returns undefined when globalArgs is undefined or contains no option fields.
|
|
2026
|
-
* Used to build globalOptionDefinitions from globalArgs when rootDoc is not available.
|
|
2027
|
-
*/
|
|
2028
|
-
function deriveGlobalArgsShape(globalArgs) {
|
|
2029
|
-
if (!globalArgs) return void 0;
|
|
2030
|
-
const optionFields = require_schema_extractor.extractFields(globalArgs).fields.filter((f) => !f.positional);
|
|
2031
|
-
if (optionFields.length === 0) return void 0;
|
|
2032
|
-
return Object.fromEntries(optionFields.map((f) => [f.name, f.schema]));
|
|
2033
|
-
}
|
|
2034
|
-
/**
|
|
2035
|
-
* Collect global option definitions from rootDoc.
|
|
2036
|
-
* Global options are intentionally applied to all generated command sections.
|
|
2037
|
-
*/
|
|
2038
|
-
function collectGlobalOptionDefinitions(rootDoc) {
|
|
2039
|
-
const globalOptions = /* @__PURE__ */ new Map();
|
|
2040
|
-
if (!rootDoc?.globalOptions) return globalOptions;
|
|
2041
|
-
const normalized = normalizeGlobalOptions(rootDoc.globalOptions);
|
|
2042
|
-
if (!normalized) return globalOptions;
|
|
2043
|
-
for (const field of collectRenderableGlobalOptionFields(normalized.args)) globalOptions.set(field.name, field);
|
|
2044
|
-
return globalOptions;
|
|
2045
|
-
}
|
|
2046
|
-
/**
|
|
2047
|
-
* Derive CommandCategory[] from files mapping.
|
|
2048
|
-
* Category title/description come from the first command in each file entry.
|
|
2049
|
-
*/
|
|
2050
|
-
function deriveIndexFromFiles(files, rootDocPath, allCommands, ignores) {
|
|
2051
|
-
const categories = [];
|
|
2052
|
-
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
2053
|
-
const { commandPaths, topLevelCommands } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
2054
|
-
if (commandPaths.length === 0) continue;
|
|
2055
|
-
const docPath = "./" + node_path.relative(node_path.dirname(rootDocPath), filePath).replace(/\\/g, "/");
|
|
2056
|
-
const firstCmdPath = commandPaths[0];
|
|
2057
|
-
const cmdInfo = firstCmdPath !== void 0 ? allCommands.get(firstCmdPath) : void 0;
|
|
2058
|
-
const fileConfig = Array.isArray(fileConfigRaw) ? void 0 : fileConfigRaw;
|
|
2059
|
-
categories.push({
|
|
2060
|
-
title: fileConfig?.title ?? cmdInfo?.name ?? node_path.basename(filePath, node_path.extname(filePath)),
|
|
2061
|
-
description: fileConfig?.description ?? cmdInfo?.description ?? "",
|
|
2062
|
-
commands: topLevelCommands,
|
|
2063
|
-
allowedCommands: commandPaths,
|
|
2064
|
-
docPath
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
|
-
return categories;
|
|
2068
|
-
}
|
|
2069
|
-
/**
|
|
2070
|
-
* Build index categories for the {{politty:index}} placeholder from other template outputs.
|
|
2071
|
-
* Each category lists exactly the heading-producing scopes of that output (noExpand), so the
|
|
2072
|
-
* index never links to commands that template mode did not render.
|
|
2073
|
-
*/
|
|
2074
|
-
function deriveIndexFromTemplateOutputs(templateMeta, currentOutputPath, indexFilePath, allCommands) {
|
|
2075
|
-
const normalizedCurrent = normalizeDocPathForComparison(currentOutputPath);
|
|
2076
|
-
const categories = [];
|
|
2077
|
-
for (const [outputPath, meta] of templateMeta.entries()) {
|
|
2078
|
-
if (normalizeDocPathForComparison(outputPath) === normalizedCurrent) continue;
|
|
2079
|
-
const scopes = meta.headingScopes;
|
|
2080
|
-
if (scopes.length === 0) continue;
|
|
2081
|
-
const docPath = "./" + node_path.relative(node_path.dirname(indexFilePath), outputPath).replace(/\\/g, "/");
|
|
2082
|
-
const firstScope = scopes[0];
|
|
2083
|
-
const cmdInfo = firstScope !== void 0 ? allCommands.get(firstScope) : void 0;
|
|
2084
|
-
categories.push({
|
|
2085
|
-
title: meta.indexTitle ?? cmdInfo?.name ?? node_path.basename(outputPath, node_path.extname(outputPath)),
|
|
2086
|
-
description: meta.indexDescription ?? cmdInfo?.description ?? "",
|
|
2087
|
-
commands: scopes,
|
|
2088
|
-
docPath,
|
|
2089
|
-
noExpand: true
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
return categories;
|
|
2093
|
-
}
|
|
2094
|
-
/**
|
|
2095
|
-
* Collect command paths that are actually documented in configured files.
|
|
2096
|
-
*/
|
|
2097
|
-
function collectDocumentedCommandPaths(files, allCommands, ignores) {
|
|
2098
|
-
const documentedCommandPaths = /* @__PURE__ */ new Set();
|
|
2099
|
-
for (const fileConfigRaw of Object.values(files)) {
|
|
2100
|
-
const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
2101
|
-
for (const commandPath of commandPaths) documentedCommandPaths.add(commandPath);
|
|
2102
|
-
}
|
|
2103
|
-
return documentedCommandPaths;
|
|
2104
|
-
}
|
|
2105
|
-
/**
|
|
2106
|
-
* Collect command paths that are targeted in configured files.
|
|
2107
|
-
*/
|
|
2108
|
-
function collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) {
|
|
2109
|
-
const documentedTargetCommandPaths = /* @__PURE__ */ new Set();
|
|
2110
|
-
for (const filePath of Object.keys(files)) {
|
|
2111
|
-
const targetCommandsInFile = findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores);
|
|
2112
|
-
for (const commandPath of targetCommandsInFile) documentedTargetCommandPaths.add(commandPath);
|
|
2113
|
-
}
|
|
2114
|
-
return documentedTargetCommandPaths;
|
|
2115
|
-
}
|
|
2116
|
-
function commandPathMatchesTarget(commandPath, targetCommands) {
|
|
2117
|
-
return targetCommands.some((targetCommand) => isSubcommandOf(commandPath, targetCommand));
|
|
2118
|
-
}
|
|
2119
|
-
function templateMetaReferencesCommandTarget(meta, targetCommands) {
|
|
2120
|
-
return meta.referencedScopes.some((scope) => commandPathMatchesTarget(scope, targetCommands));
|
|
2121
|
-
}
|
|
2122
|
-
function templateMetaShouldProcessForTarget(meta, targetCommands) {
|
|
2123
|
-
return meta.emitsIndex || meta.emitsGlobalOptions || templateMetaReferencesCommandTarget(meta, targetCommands);
|
|
2124
|
-
}
|
|
2125
|
-
/**
|
|
2126
|
-
* Validate that excluded command options match globalOptions definitions.
|
|
2127
|
-
*/
|
|
2128
|
-
function validateGlobalOptionCompatibility(documentedCommandPaths, allCommands, globalOptions) {
|
|
2129
|
-
if (globalOptions.size === 0) return;
|
|
2130
|
-
const conflicts = [];
|
|
2131
|
-
for (const commandPath of documentedCommandPaths) {
|
|
2132
|
-
const info = allCommands.get(commandPath);
|
|
2133
|
-
if (!info) continue;
|
|
2134
|
-
for (const option of info.options) {
|
|
2135
|
-
const globalOption = globalOptions.get(option.name);
|
|
2136
|
-
if (!globalOption) continue;
|
|
2137
|
-
if (!areGlobalOptionsEquivalent(globalOption, option)) conflicts.push(`Command "${formatCommandPath(commandPath)}" option "--${option.cliName}" does not match globalOptions definition for "${option.name}".`);
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
if (conflicts.length > 0) throw new Error(`Invalid globalOptions configuration:\n - ${conflicts.join("\n - ")}`);
|
|
2141
|
-
}
|
|
2142
|
-
/**
|
|
2143
|
-
* Build global options content (anchor + args table) without markers
|
|
2144
|
-
*/
|
|
2145
|
-
function buildGlobalOptionsContent(config) {
|
|
2146
|
-
return ["<a id=\"global-options\"></a>", renderArgsTable(config.args, config.options)].join("\n");
|
|
2147
|
-
}
|
|
2148
|
-
/**
|
|
2149
|
-
* Generate global options section content with markers
|
|
2150
|
-
*/
|
|
2151
|
-
function generateGlobalOptionsSection(config) {
|
|
2152
|
-
return [
|
|
2153
|
-
globalOptionsStartMarker(),
|
|
2154
|
-
buildGlobalOptionsContent(config),
|
|
2155
|
-
globalOptionsEndMarker()
|
|
2156
|
-
].join("\n");
|
|
2157
|
-
}
|
|
2158
|
-
/**
|
|
2159
|
-
* Generate index section content with markers
|
|
2160
|
-
*/
|
|
2161
|
-
async function generateIndexSection(categories, command, scope, options) {
|
|
2162
|
-
const startMarker = indexStartMarker(scope);
|
|
2163
|
-
const endMarker = indexEndMarker(scope);
|
|
2164
|
-
return [
|
|
2165
|
-
startMarker,
|
|
2166
|
-
await renderCommandIndex(command, categories, options),
|
|
2167
|
-
endMarker
|
|
2168
|
-
].join("\n");
|
|
2169
|
-
}
|
|
2170
|
-
/**
|
|
2171
|
-
* Normalize a doc file path for equivalence checks.
|
|
2172
|
-
*/
|
|
2173
|
-
function normalizeDocPathForComparison(filePath) {
|
|
2174
|
-
return node_path.resolve(filePath);
|
|
2175
|
-
}
|
|
2176
|
-
/**
|
|
2177
|
-
* Process global options marker in file content
|
|
2178
|
-
* Returns result with updated content and any diffs
|
|
2179
|
-
*/
|
|
2180
|
-
async function processGlobalOptionsMarker(existingContent, globalOptionsConfig, updateMode, formatter, autoInsertIfMissing) {
|
|
2181
|
-
let content = existingContent;
|
|
2182
|
-
const diffs = [];
|
|
2183
|
-
let hasError = false;
|
|
2184
|
-
let wasUpdated = false;
|
|
2185
|
-
const startMarker = globalOptionsStartMarker();
|
|
2186
|
-
const endMarker = globalOptionsEndMarker();
|
|
2187
|
-
const generatedSection = await applyFormatter(generateGlobalOptionsSection(globalOptionsConfig), formatter);
|
|
2188
|
-
const existingSection = extractMarkerSection(content, startMarker, endMarker);
|
|
2189
|
-
if (!existingSection) {
|
|
2190
|
-
if (updateMode && autoInsertIfMissing) {
|
|
2191
|
-
content = content.trimEnd() + "\n\n" + generatedSection + "\n";
|
|
2192
|
-
wasUpdated = true;
|
|
2193
|
-
return {
|
|
2194
|
-
content,
|
|
2195
|
-
diffs,
|
|
2196
|
-
hasError,
|
|
2197
|
-
wasUpdated
|
|
2198
|
-
};
|
|
2199
|
-
}
|
|
2200
|
-
hasError = true;
|
|
2201
|
-
diffs.push(`Global options marker not found in file. Expected markers:\n${startMarker}\n...\n${endMarker}`);
|
|
2202
|
-
return {
|
|
2203
|
-
content,
|
|
2204
|
-
diffs,
|
|
2205
|
-
hasError,
|
|
2206
|
-
wasUpdated
|
|
2207
|
-
};
|
|
2208
|
-
}
|
|
2209
|
-
if (existingSection !== generatedSection) if (updateMode) {
|
|
2210
|
-
const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
|
|
2211
|
-
if (updated) {
|
|
2212
|
-
content = updated;
|
|
2213
|
-
wasUpdated = true;
|
|
2214
|
-
} else {
|
|
2215
|
-
hasError = true;
|
|
2216
|
-
diffs.push("Failed to replace global options section");
|
|
2217
|
-
}
|
|
2218
|
-
} else {
|
|
2219
|
-
hasError = true;
|
|
2220
|
-
diffs.push(formatDiff(existingSection, generatedSection));
|
|
2221
|
-
}
|
|
2222
|
-
return {
|
|
2223
|
-
content,
|
|
2224
|
-
diffs,
|
|
2225
|
-
hasError,
|
|
2226
|
-
wasUpdated
|
|
2227
|
-
};
|
|
2228
|
-
}
|
|
2229
|
-
/**
|
|
2230
|
-
* Process a static content marker (root-header or root-footer).
|
|
2231
|
-
* Inserts/updates the marker section with the given content.
|
|
2232
|
-
*/
|
|
2233
|
-
async function processStaticMarker(existingContent, markerLabel, startMarker, endMarker, rawContent, updateMode, formatter, autoInsertIfMissing) {
|
|
2234
|
-
let content = existingContent;
|
|
2235
|
-
const diffs = [];
|
|
2236
|
-
let hasError = false;
|
|
2237
|
-
let wasUpdated = false;
|
|
2238
|
-
const generatedSection = [
|
|
2239
|
-
startMarker,
|
|
2240
|
-
await applyFormatter(rawContent, formatter),
|
|
2241
|
-
endMarker
|
|
2242
|
-
].join("\n");
|
|
2243
|
-
const existingSection = extractMarkerSection(content, startMarker, endMarker);
|
|
2244
|
-
if (!existingSection) {
|
|
2245
|
-
if (updateMode && autoInsertIfMissing) {
|
|
2246
|
-
content = content.trimEnd() + "\n\n" + generatedSection + "\n";
|
|
2247
|
-
wasUpdated = true;
|
|
2248
|
-
return {
|
|
2249
|
-
content,
|
|
2250
|
-
diffs,
|
|
2251
|
-
hasError,
|
|
2252
|
-
wasUpdated
|
|
2253
|
-
};
|
|
2254
|
-
}
|
|
2255
|
-
hasError = true;
|
|
2256
|
-
diffs.push(`${markerLabel} marker not found in file. Expected markers:\n${startMarker}\n...\n${endMarker}`);
|
|
2257
|
-
return {
|
|
2258
|
-
content,
|
|
2259
|
-
diffs,
|
|
2260
|
-
hasError,
|
|
2261
|
-
wasUpdated
|
|
2262
|
-
};
|
|
2263
|
-
}
|
|
2264
|
-
if (existingSection !== generatedSection) if (updateMode) {
|
|
2265
|
-
const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
|
|
2266
|
-
if (updated) {
|
|
2267
|
-
content = updated;
|
|
2268
|
-
wasUpdated = true;
|
|
2269
|
-
} else {
|
|
2270
|
-
hasError = true;
|
|
2271
|
-
diffs.push(`Failed to replace ${markerLabel} section`);
|
|
2272
|
-
}
|
|
2273
|
-
} else {
|
|
2274
|
-
hasError = true;
|
|
2275
|
-
diffs.push(formatDiff(existingSection, generatedSection));
|
|
2276
|
-
}
|
|
2277
|
-
return {
|
|
2278
|
-
content,
|
|
2279
|
-
diffs,
|
|
2280
|
-
hasError,
|
|
2281
|
-
wasUpdated
|
|
2282
|
-
};
|
|
2283
|
-
}
|
|
2284
|
-
/**
|
|
2285
|
-
* Process index marker in file content
|
|
2286
|
-
* Returns result with updated content and any diffs.
|
|
2287
|
-
* If the marker is not present in the file, the section is silently skipped.
|
|
2288
|
-
*/
|
|
2289
|
-
async function processIndexMarker(existingContent, categories, command, scope, updateMode, formatter, indexOptions) {
|
|
2290
|
-
let content = existingContent;
|
|
2291
|
-
const diffs = [];
|
|
2292
|
-
let hasError = false;
|
|
2293
|
-
let wasUpdated = false;
|
|
2294
|
-
const startMarker = indexStartMarker(scope);
|
|
2295
|
-
const endMarker = indexEndMarker(scope);
|
|
2296
|
-
const hasStartMarker = content.includes(startMarker);
|
|
2297
|
-
const hasEndMarker = content.includes(endMarker);
|
|
2298
|
-
if (!hasStartMarker && !hasEndMarker) return {
|
|
2299
|
-
content,
|
|
2300
|
-
diffs,
|
|
2301
|
-
hasError,
|
|
2302
|
-
wasUpdated
|
|
2303
|
-
};
|
|
2304
|
-
if (!hasStartMarker || !hasEndMarker) {
|
|
2305
|
-
hasError = true;
|
|
2306
|
-
diffs.push("Index marker section is malformed: both start and end markers are required.");
|
|
2307
|
-
return {
|
|
2308
|
-
content,
|
|
2309
|
-
diffs,
|
|
2310
|
-
hasError,
|
|
2311
|
-
wasUpdated
|
|
2312
|
-
};
|
|
2313
|
-
}
|
|
2314
|
-
const existingSection = extractMarkerSection(content, startMarker, endMarker);
|
|
2315
|
-
if (!existingSection) {
|
|
2316
|
-
hasError = true;
|
|
2317
|
-
diffs.push("Index marker section is malformed: start marker must appear before end marker.");
|
|
2318
|
-
return {
|
|
2319
|
-
content,
|
|
2320
|
-
diffs,
|
|
2321
|
-
hasError,
|
|
2322
|
-
wasUpdated
|
|
2323
|
-
};
|
|
2324
|
-
}
|
|
2325
|
-
const generatedSection = await applyFormatter(await generateIndexSection(categories, command, scope, indexOptions), formatter);
|
|
2326
|
-
if (existingSection !== generatedSection) if (updateMode) {
|
|
2327
|
-
const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
|
|
2328
|
-
if (updated) {
|
|
2329
|
-
content = updated;
|
|
2330
|
-
wasUpdated = true;
|
|
2331
|
-
} else {
|
|
2332
|
-
hasError = true;
|
|
2333
|
-
diffs.push("Failed to replace index section");
|
|
2334
|
-
}
|
|
2335
|
-
} else {
|
|
2336
|
-
hasError = true;
|
|
2337
|
-
diffs.push(formatDiff(existingSection, generatedSection));
|
|
2338
|
-
}
|
|
2339
|
-
return {
|
|
2340
|
-
content,
|
|
2341
|
-
diffs,
|
|
2342
|
-
hasError,
|
|
2343
|
-
wasUpdated
|
|
2344
|
-
};
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* Find which file contains a specific command
|
|
2348
|
-
*/
|
|
2349
|
-
function findFileForCommand(commandPath, files, allCommands, ignores) {
|
|
2350
|
-
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
2351
|
-
const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
2352
|
-
if (commandPaths.includes(commandPath)) return filePath;
|
|
2353
|
-
}
|
|
2354
|
-
return null;
|
|
2355
|
-
}
|
|
2356
|
-
/**
|
|
2357
|
-
* Find which target commands are contained in a file
|
|
2358
|
-
* Also expands each target command to include subcommands that are NOT explicitly in specifiedCommands
|
|
2359
|
-
*/
|
|
2360
|
-
function findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) {
|
|
2361
|
-
const fileConfigRaw = files[filePath];
|
|
2362
|
-
if (!fileConfigRaw) return [];
|
|
2363
|
-
const { specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
2364
|
-
const expandedTargets = /* @__PURE__ */ new Set();
|
|
2365
|
-
for (const targetCmd of targetCommands) {
|
|
2366
|
-
if (!commandPaths.includes(targetCmd)) continue;
|
|
2367
|
-
expandedTargets.add(targetCmd);
|
|
2368
|
-
for (const cmdPath of commandPaths) if (isSubcommandOf(cmdPath, targetCmd) && !specifiedCommands.includes(cmdPath)) expandedTargets.add(cmdPath);
|
|
2369
|
-
}
|
|
2370
|
-
return Array.from(expandedTargets);
|
|
2371
|
-
}
|
|
2372
|
-
/**
|
|
2373
|
-
* Generate a single command section (already contains section markers from renderer)
|
|
2374
|
-
*/
|
|
2375
|
-
function generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions, ignores = [], excludeOptionNames, templateExclusions) {
|
|
2376
|
-
const info = allCommands.get(cmdPath);
|
|
2377
|
-
if (!info) return null;
|
|
2378
|
-
if (templateExclusions && isCommandScopeExcluded(info.commandPath, templateExclusions.commandScopes)) return null;
|
|
2379
|
-
const enriched = {
|
|
2380
|
-
...info,
|
|
2381
|
-
filePath,
|
|
2382
|
-
fileMap,
|
|
2383
|
-
rootDocPath
|
|
2384
|
-
};
|
|
2385
|
-
if (ignores.length > 0 || templateExclusions && templateExclusions.commandScopes.size > 0) enriched.subCommands = info.subCommands.filter((sub) => {
|
|
2386
|
-
const subCommandPath = sub.fullPath.join(" ");
|
|
2387
|
-
if (ignores.some((pattern) => matchesIgnorePattern(subCommandPath, pattern))) return false;
|
|
2388
|
-
return !(templateExclusions && isCommandScopeExcluded(subCommandPath, templateExclusions.commandScopes));
|
|
2389
|
-
});
|
|
2390
|
-
if (hasGlobalOptions !== void 0) enriched.hasGlobalOptions = hasGlobalOptions;
|
|
2391
|
-
if (excludeOptionNames && excludeOptionNames.size > 0) {
|
|
2392
|
-
enriched.options = info.options.filter((opt) => !excludeOptionNames.has(opt.name));
|
|
2393
|
-
if (info.extracted) enriched.extracted = filterExtractedFields(info.extracted, excludeOptionNames);
|
|
2394
|
-
}
|
|
2395
|
-
let rendered = render(enriched);
|
|
2396
|
-
if (templateExclusions) for (const [scope, sectionTypes] of templateExclusions.commandSections) {
|
|
2397
|
-
if (scope !== info.commandPath) continue;
|
|
2398
|
-
for (const sectionType of sectionTypes) {
|
|
2399
|
-
const section = extractSectionMarker(rendered, sectionType, scope);
|
|
2400
|
-
if (section !== null) rendered = rendered.replace(section, "");
|
|
2401
|
-
}
|
|
2402
|
-
rendered = collapseBlankLinesOutsideCodeFences(rendered);
|
|
2403
|
-
}
|
|
2404
|
-
return rendered;
|
|
2405
|
-
}
|
|
2406
|
-
function generateCommandTreeMarkdown(cmdPath, allCommands, render, ignores, filePath, fileMap, rootDocPath, hasGlobalOptions, excludeOptionNames, templateExclusions) {
|
|
2407
|
-
const commandPaths = getTemplateCommandTreePaths(cmdPath, allCommands, ignores, templateExclusions);
|
|
2408
|
-
const sections = [];
|
|
2409
|
-
for (const commandPath of commandPaths) {
|
|
2410
|
-
const section = generateCommandSection(commandPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions, ignores, excludeOptionNames, templateExclusions);
|
|
2411
|
-
if (section !== null) sections.push(section);
|
|
2412
|
-
}
|
|
2413
|
-
return sections.length === 0 ? null : sections.join("\n");
|
|
2414
|
-
}
|
|
2415
|
-
/**
|
|
2416
|
-
* Return a copy of ExtractedFields with the named options removed from every field collection
|
|
2417
|
-
* (top-level fields, union options, and discriminated-union variants). Used to exclude global
|
|
2418
|
-
* options from grouped option tables rendered directly from `extracted`.
|
|
2419
|
-
*/
|
|
2420
|
-
function filterExtractedFields(extracted, excludeOptionNames) {
|
|
2421
|
-
const result = {
|
|
2422
|
-
...extracted,
|
|
2423
|
-
fields: extracted.fields.filter((f) => !excludeOptionNames.has(f.name))
|
|
2424
|
-
};
|
|
2425
|
-
if (extracted.unionOptions) result.unionOptions = extracted.unionOptions.map((opt) => filterExtractedFields(opt, excludeOptionNames));
|
|
2426
|
-
if (extracted.variants) result.variants = extracted.variants.map((variant) => ({
|
|
2427
|
-
...variant,
|
|
2428
|
-
fields: variant.fields.filter((f) => !excludeOptionNames.has(f.name))
|
|
2429
|
-
}));
|
|
2430
|
-
return result;
|
|
2431
|
-
}
|
|
2432
|
-
/**
|
|
2433
|
-
* Generate markdown for a file containing multiple commands
|
|
2434
|
-
* Each command section is wrapped with markers for partial validation
|
|
2435
|
-
*/
|
|
2436
|
-
function generateFileMarkdown(commandPaths, allCommands, render, filePath, fileMap, specifiedOrder, fileConfig, rootDocPath, hasGlobalOptions, ignores = []) {
|
|
2437
|
-
const sections = [];
|
|
2438
|
-
const header = fileConfig ? generateFileHeader(fileConfig) : null;
|
|
2439
|
-
if (header) sections.push(header);
|
|
2440
|
-
const sortedPaths = sortDepthFirst(commandPaths, specifiedOrder ?? []);
|
|
2441
|
-
for (const cmdPath of sortedPaths) {
|
|
2442
|
-
const section = generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions, ignores);
|
|
2443
|
-
if (section) sections.push(section);
|
|
2444
|
-
}
|
|
2445
|
-
return `${sections.join("\n")}\n`;
|
|
2446
|
-
}
|
|
2447
|
-
/**
|
|
2448
|
-
* Build a map of command path to file path
|
|
2449
|
-
*/
|
|
2450
|
-
function buildFileMap(files, allCommands, ignores) {
|
|
2451
|
-
const fileMap = {};
|
|
2452
|
-
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
2453
|
-
const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
2454
|
-
for (const cmdPath of commandPaths) setFileMapEntry(fileMap, cmdPath, filePath);
|
|
2455
|
-
}
|
|
2456
|
-
return fileMap;
|
|
2457
|
-
}
|
|
2458
|
-
/**
|
|
2459
|
-
* Execute examples for commands based on configuration
|
|
2460
|
-
*/
|
|
2461
|
-
async function executeConfiguredExamples(allCommands, examplesConfig, rootCommand) {
|
|
2462
|
-
for (const [cmdPath, cmdConfig] of Object.entries(examplesConfig)) {
|
|
2463
|
-
const commandInfo = allCommands.get(cmdPath);
|
|
2464
|
-
if (!commandInfo?.examples?.length) continue;
|
|
2465
|
-
const config = cmdConfig === true ? {} : cmdConfig;
|
|
2466
|
-
const commandPath = cmdPath ? cmdPath.split(" ") : [];
|
|
2467
|
-
commandInfo.exampleResults = await executeExamples(commandInfo.examples, config, rootCommand, commandPath);
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
/**
|
|
2471
|
-
* Convert PathConfig to FileMapping with explicit command paths.
|
|
2472
|
-
* Uses noExpand to prevent subcommand expansion since paths are pre-resolved.
|
|
2473
|
-
*/
|
|
2474
|
-
function pathToFiles(pathConfig, allCommands) {
|
|
2475
|
-
if (typeof pathConfig === "string") return {
|
|
2476
|
-
files: { [pathConfig]: Array.from(allCommands.keys()) },
|
|
2477
|
-
rootDocPath: pathConfig
|
|
2478
|
-
};
|
|
2479
|
-
const { root, commands = {} } = pathConfig;
|
|
2480
|
-
const files = {};
|
|
2481
|
-
const assignedToOtherFiles = /* @__PURE__ */ new Set();
|
|
2482
|
-
const sortedEntries = Object.entries(commands).sort(([a], [b]) => b.split(" ").length - a.split(" ").length);
|
|
2483
|
-
for (const [cmdPath, filePath] of sortedEntries) {
|
|
2484
|
-
if (!files[filePath]) files[filePath] = {
|
|
2485
|
-
commands: [],
|
|
2486
|
-
noExpand: true
|
|
2487
|
-
};
|
|
2488
|
-
const fc = files[filePath];
|
|
2489
|
-
for (const existingPath of allCommands.keys()) if ((existingPath === cmdPath || existingPath.startsWith(cmdPath + " ")) && !assignedToOtherFiles.has(existingPath)) {
|
|
2490
|
-
fc.commands.push(existingPath);
|
|
2491
|
-
assignedToOtherFiles.add(existingPath);
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
files[root] = {
|
|
2495
|
-
commands: Array.from(allCommands.keys()).filter((p) => !assignedToOtherFiles.has(p)),
|
|
2496
|
-
noExpand: true
|
|
2497
|
-
};
|
|
2498
|
-
return {
|
|
2499
|
-
files,
|
|
2500
|
-
rootDocPath: root
|
|
2501
|
-
};
|
|
2502
|
-
}
|
|
2503
|
-
/**
|
|
2504
|
-
* Generate documentation from command definition
|
|
2505
|
-
*/
|
|
2506
|
-
async function generateDoc(config) {
|
|
2507
|
-
const { command, ignores = [], format = {}, formatter, examples: examplesConfig, targetCommands, globalArgs, customizable = false } = config;
|
|
2508
|
-
const allCommands = await collectAllCommands(command);
|
|
2509
|
-
let files;
|
|
2510
|
-
let usingPathConfig = false;
|
|
2511
|
-
let resolvedRootDocPath;
|
|
2512
|
-
if (config.path !== void 0) {
|
|
2513
|
-
if (config.files !== void 0) throw new Error("Cannot specify both \"path\" and \"files\". Use one or the other.");
|
|
2514
|
-
const converted = pathToFiles(config.path, allCommands);
|
|
2515
|
-
files = converted.files;
|
|
2516
|
-
resolvedRootDocPath = converted.rootDocPath;
|
|
2517
|
-
usingPathConfig = true;
|
|
2518
|
-
} else if (config.files !== void 0) files = config.files;
|
|
2519
|
-
else if (config.templates !== void 0) files = {};
|
|
2520
|
-
else throw new Error("Either \"path\", \"files\", or \"templates\" must be specified.");
|
|
2521
|
-
let rootDoc = config.rootDoc;
|
|
2522
|
-
if (!rootDoc && usingPathConfig && (globalArgs || config.rootInfo)) rootDoc = { path: resolvedRootDocPath };
|
|
2523
|
-
if (globalArgs && rootDoc && !rootDoc.globalOptions) {
|
|
2524
|
-
const optionFields = require_schema_extractor.extractFields(globalArgs).fields.filter((f) => !f.positional);
|
|
2525
|
-
if (optionFields.length > 0) {
|
|
2526
|
-
const globalShape = Object.fromEntries(optionFields.map((f) => [f.name, f.schema]));
|
|
2527
|
-
rootDoc = {
|
|
2528
|
-
...rootDoc,
|
|
2529
|
-
globalOptions: globalShape
|
|
2530
|
-
};
|
|
2531
|
-
}
|
|
2532
|
-
}
|
|
2533
|
-
const updateMode = isTruthyEnv(UPDATE_GOLDEN_ENV);
|
|
2534
|
-
const doctorMode = isTruthyEnv(DOCTOR_ENV);
|
|
2535
|
-
let hasDoctorIssues = false;
|
|
2536
|
-
if (rootDoc && !usingPathConfig) {
|
|
2537
|
-
const normalizedRootDocPath = normalizeDocPathForComparison(rootDoc.path);
|
|
2538
|
-
if (Object.keys(files).some((filePath) => normalizeDocPathForComparison(filePath) === normalizedRootDocPath)) throw new Error(`rootDoc.path "${rootDoc.path}" must not also appear as a key in files.`);
|
|
2539
|
-
}
|
|
2540
|
-
if (examplesConfig) await executeConfiguredExamples(allCommands, examplesConfig, command);
|
|
2541
|
-
const hasTargetCommands = targetCommands !== void 0 && targetCommands.length > 0;
|
|
2542
|
-
const globalOptionDefinitions = collectGlobalOptionDefinitions(rootDoc);
|
|
2543
|
-
const templateGlobalOptionFields = /* @__PURE__ */ new Map();
|
|
2544
|
-
if (config.templates) if (globalOptionDefinitions.size > 0) for (const [name, field] of globalOptionDefinitions) templateGlobalOptionFields.set(name, field);
|
|
2545
|
-
else {
|
|
2546
|
-
const shape = deriveGlobalArgsShape(globalArgs);
|
|
2547
|
-
if (shape) for (const field of collectRenderableGlobalOptionFields(shape)) templateGlobalOptionFields.set(field.name, field);
|
|
2548
|
-
}
|
|
2549
|
-
const documentedCommandPaths = hasTargetCommands ? collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) : collectDocumentedCommandPaths(files, allCommands, ignores);
|
|
2550
|
-
const allFilesCommands = [];
|
|
2551
|
-
for (const fileConfigRaw of Object.values(files)) {
|
|
2552
|
-
const fileConfig = normalizeFileConfig(fileConfigRaw);
|
|
2553
|
-
allFilesCommands.push(...fileConfig.commands);
|
|
2554
|
-
}
|
|
2555
|
-
validateIgnoresExist(ignores, allCommands);
|
|
2556
|
-
validateNoConflicts(allFilesCommands, ignores, allCommands);
|
|
2557
|
-
const fileMap = buildFileMap(files, allCommands, ignores);
|
|
2558
|
-
const templateContents = /* @__PURE__ */ new Map();
|
|
2559
|
-
const templateExclusions = /* @__PURE__ */ new Map();
|
|
2560
|
-
if (config.templates) for (const [outputPath, templatePath] of Object.entries(config.templates)) {
|
|
2561
|
-
const templateContent = readFile(templatePath);
|
|
2562
|
-
templateContents.set(outputPath, templateContent);
|
|
2563
|
-
if (templateContent !== null) templateExclusions.set(outputPath, buildTemplateExclusions(collectExcludedTemplatePlaceholders(templateContent), allCommands));
|
|
2564
|
-
}
|
|
2565
|
-
const templateEntries = Object.entries(config.templates ?? {});
|
|
2566
|
-
const templateMeta = /* @__PURE__ */ new Map();
|
|
2567
|
-
const templateValidationErrors = /* @__PURE__ */ new Map();
|
|
2568
|
-
if (templateEntries.length > 0) {
|
|
2569
|
-
const normalizedRootDocPath = rootDoc ? normalizeDocPathForComparison(rootDoc.path) : null;
|
|
2570
|
-
const normalizedFileKeys = new Set(Object.keys(files).map(normalizeDocPathForComparison));
|
|
2571
|
-
const normalizedTemplateOutputs = /* @__PURE__ */ new Set();
|
|
2572
|
-
const allNormalizedTemplateOutputs = new Set(templateEntries.map(([outputPath]) => normalizeDocPathForComparison(outputPath)));
|
|
2573
|
-
for (const [outputPath, templatePath] of templateEntries) {
|
|
2574
|
-
const normalizedOutput = normalizeDocPathForComparison(outputPath);
|
|
2575
|
-
const normalizedSource = normalizeDocPathForComparison(templatePath);
|
|
2576
|
-
if (normalizedFileKeys.has(normalizedOutput)) throw new Error(`Template output path "${outputPath}" conflicts with an existing files key.`);
|
|
2577
|
-
if (normalizedRootDocPath && normalizedOutput === normalizedRootDocPath) throw new Error(`Template output path "${outputPath}" conflicts with rootDoc.path "${rootDoc.path}".`);
|
|
2578
|
-
if (normalizedTemplateOutputs.has(normalizedOutput)) throw new Error(`Duplicate template output path: "${outputPath}".`);
|
|
2579
|
-
normalizedTemplateOutputs.add(normalizedOutput);
|
|
2580
|
-
if (normalizedSource === normalizedOutput) throw new Error(`Template output path "${outputPath}" must not be the same as its source template path.`);
|
|
2581
|
-
if (normalizedFileKeys.has(normalizedSource)) throw new Error(`Template source path "${templatePath}" conflicts with a files output key.`);
|
|
2582
|
-
if (normalizedRootDocPath && normalizedSource === normalizedRootDocPath) throw new Error(`Template source path "${templatePath}" conflicts with rootDoc.path "${rootDoc.path}".`);
|
|
2583
|
-
if (allNormalizedTemplateOutputs.has(normalizedSource)) throw new Error(`Template source path "${templatePath}" conflicts with a template output path.`);
|
|
2584
|
-
}
|
|
2585
|
-
const availableCommandPaths = Array.from(allCommands.keys()).join(", ");
|
|
2586
|
-
for (const [outputPath, templatePath] of templateEntries) {
|
|
2587
|
-
const templateContent = templateContents.get(outputPath) ?? null;
|
|
2588
|
-
const validationErrors = [];
|
|
2589
|
-
if (templateContent === null) {
|
|
2590
|
-
templateMeta.set(outputPath, {
|
|
2591
|
-
referencedScopes: [],
|
|
2592
|
-
headingScopes: [],
|
|
2593
|
-
commandTreeRoots: [],
|
|
2594
|
-
emitsGlobalOptions: false,
|
|
2595
|
-
emitsIndex: false
|
|
2596
|
-
});
|
|
2597
|
-
templateValidationErrors.set(outputPath, validationErrors);
|
|
2598
|
-
continue;
|
|
2599
|
-
}
|
|
2600
|
-
try {
|
|
2601
|
-
validateTemplatePlaceholderSyntax(templateContent, templatePath);
|
|
2602
|
-
} catch (error) {
|
|
2603
|
-
validationErrors.push(error instanceof Error ? error.message : String(error));
|
|
2604
|
-
}
|
|
2605
|
-
const placeholders = Array.from(new Set(templateContent.match(TEMPLATE_PLACEHOLDER_REGEX) ?? []));
|
|
2606
|
-
const scopes = /* @__PURE__ */ new Set();
|
|
2607
|
-
const headingScopes = /* @__PURE__ */ new Set();
|
|
2608
|
-
const commandTreeRoots = /* @__PURE__ */ new Set();
|
|
2609
|
-
let emitsGlobalOptions = false;
|
|
2610
|
-
let emitsIndex = false;
|
|
2611
|
-
const exclusions = templateExclusions.get(outputPath) ?? createTemplateExclusions(/* @__PURE__ */ new Set());
|
|
2612
|
-
const indexMetadata = collectTemplateIndexMetadata(templateContent);
|
|
2613
|
-
for (const placeholder of placeholders) {
|
|
2614
|
-
const placeholderKey = templatePlaceholderKey(placeholder);
|
|
2615
|
-
if (exclusions.rawKeys.has(placeholderKey) || isRawCommandPlaceholderUnderExcludedScope(placeholderKey, exclusions)) continue;
|
|
2616
|
-
const parsed = parsePlaceholder(placeholder, allCommands);
|
|
2617
|
-
if (shouldSkipTemplatePlaceholder(placeholder, parsed, exclusions)) continue;
|
|
2618
|
-
if (parsed.kind === "invalid") {
|
|
2619
|
-
validationErrors.push(`${parsed.reason} (in template "${templatePath}")`);
|
|
2620
|
-
continue;
|
|
2621
|
-
}
|
|
2622
|
-
if (parsed.kind === "command") {
|
|
2623
|
-
const { scope, type } = parsed;
|
|
2624
|
-
if (!allCommands.has(scope)) {
|
|
2625
|
-
const sectionTypeError = getUnknownSectionTypeError(scope, allCommands);
|
|
2626
|
-
if (sectionTypeError) {
|
|
2627
|
-
validationErrors.push(`${sectionTypeError} (in template "${templatePath}")`);
|
|
2628
|
-
continue;
|
|
2629
|
-
}
|
|
2630
|
-
validationErrors.push(`Unknown command scope "${scope}" in template "${templatePath}". Available: ${availableCommandPaths}`);
|
|
2631
|
-
continue;
|
|
2632
|
-
}
|
|
2633
|
-
if (ignores.some((pattern) => matchesIgnorePattern(scope, pattern))) {
|
|
2634
|
-
validationErrors.push(`Command scope "${scope}" in template "${templatePath}" conflicts with ignores configuration.`);
|
|
2635
|
-
continue;
|
|
2636
|
-
}
|
|
2637
|
-
if (type === void 0) {
|
|
2638
|
-
const commandTreePaths = getTemplateCommandTreePaths(scope, allCommands, ignores, exclusions);
|
|
2639
|
-
if (!isCommandSectionExcluded(scope, "heading", exclusions)) commandTreeRoots.add(scope);
|
|
2640
|
-
for (const commandTreePath of commandTreePaths) {
|
|
2641
|
-
scopes.add(commandTreePath);
|
|
2642
|
-
if (!isCommandSectionExcluded(commandTreePath, "heading", exclusions)) headingScopes.add(commandTreePath);
|
|
2643
|
-
}
|
|
2644
|
-
} else {
|
|
2645
|
-
scopes.add(scope);
|
|
2646
|
-
if (type === "heading" && !isCommandSectionExcluded(scope, "heading", exclusions)) {
|
|
2647
|
-
headingScopes.add(scope);
|
|
2648
|
-
commandTreeRoots.add(scope);
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
} else if (parsed.kind === "global-options") emitsGlobalOptions = true;
|
|
2652
|
-
else if (parsed.kind === "index") emitsIndex = true;
|
|
2653
|
-
}
|
|
2654
|
-
if (emitsGlobalOptions) {
|
|
2655
|
-
if (!(!!rootDoc?.globalOptions || deriveGlobalArgsShape(globalArgs) !== void 0)) validationErrors.push(`Template "${templatePath}" uses {{politty:global-options}} but no global options are configured (neither rootDoc.globalOptions nor globalArgs with non-positional options).`);
|
|
2656
|
-
}
|
|
2657
|
-
templateMeta.set(outputPath, {
|
|
2658
|
-
referencedScopes: Array.from(scopes),
|
|
2659
|
-
headingScopes: Array.from(headingScopes),
|
|
2660
|
-
commandTreeRoots: Array.from(commandTreeRoots),
|
|
2661
|
-
emitsGlobalOptions,
|
|
2662
|
-
emitsIndex,
|
|
2663
|
-
...indexMetadata.title !== void 0 ? { indexTitle: indexMetadata.title } : {},
|
|
2664
|
-
...indexMetadata.description !== void 0 ? { indexDescription: indexMetadata.description } : {}
|
|
2665
|
-
});
|
|
2666
|
-
templateValidationErrors.set(outputPath, validationErrors);
|
|
2667
|
-
}
|
|
2668
|
-
for (const meta of templateMeta.values()) {
|
|
2669
|
-
if (hasTargetCommands && !templateMetaShouldProcessForTarget(meta, targetCommands)) continue;
|
|
2670
|
-
for (const scope of meta.referencedScopes) documentedCommandPaths.add(scope);
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
if (hasTargetCommands) for (const targetCommand of targetCommands) {
|
|
2674
|
-
const targetFilePath = findFileForCommand(targetCommand, files, allCommands, ignores);
|
|
2675
|
-
const targetTemplatePath = Array.from(templateMeta.values()).some((meta) => templateMetaReferencesCommandTarget(meta, [targetCommand]));
|
|
2676
|
-
if (!targetFilePath && !targetTemplatePath) throw new Error(`Target command "${targetCommand}" not found in any file or template configuration`);
|
|
2677
|
-
}
|
|
2678
|
-
const activeTemplateMeta = hasTargetCommands && config.templates ? new Map(Array.from(templateMeta.entries()).filter(([, meta]) => templateMetaShouldProcessForTarget(meta, targetCommands))) : templateMeta;
|
|
2679
|
-
for (const [outputPath, validationErrors] of templateValidationErrors.entries()) if (validationErrors.length > 0 && activeTemplateMeta.has(outputPath)) throw new Error(validationErrors.join("\n"));
|
|
2680
|
-
const templateGlobalOptionsProviderPaths = Array.from(templateMeta.entries()).filter(([, meta]) => meta.emitsGlobalOptions).map(([outputPath]) => outputPath);
|
|
2681
|
-
const templateGlobalOptionsProviderPath = templateGlobalOptionsProviderPaths.length === 1 ? templateGlobalOptionsProviderPaths[0] : void 0;
|
|
2682
|
-
validateGlobalOptionCompatibility(documentedCommandPaths, allCommands, globalOptionDefinitions);
|
|
2683
|
-
if (globalOptionDefinitions.size === 0 && templateGlobalOptionFields.size > 0) {
|
|
2684
|
-
const emittingTemplateScopes = /* @__PURE__ */ new Set();
|
|
2685
|
-
for (const meta of activeTemplateMeta.values()) {
|
|
2686
|
-
if (!meta.emitsGlobalOptions && templateGlobalOptionsProviderPath === void 0) continue;
|
|
2687
|
-
for (const scope of meta.referencedScopes) emittingTemplateScopes.add(scope);
|
|
2688
|
-
}
|
|
2689
|
-
validateGlobalOptionCompatibility(emittingTemplateScopes, allCommands, templateGlobalOptionFields);
|
|
2690
|
-
}
|
|
2691
|
-
if (globalOptionDefinitions.size > 0) for (const info of allCommands.values()) {
|
|
2692
|
-
info.options = info.options.filter((opt) => !globalOptionDefinitions.has(opt.name));
|
|
2693
|
-
if (info.extracted) info.extracted = filterExtractedFields(info.extracted, new Set(globalOptionDefinitions.keys()));
|
|
2694
|
-
}
|
|
2695
|
-
const templateFileMap = {};
|
|
2696
|
-
for (const [scope, outputPath] of Object.entries(fileMap)) setFileMapEntry(templateFileMap, scope, outputPath);
|
|
2697
|
-
const scopeRootLength = (root) => root === "" ? 0 : root.split(" ").length;
|
|
2698
|
-
const templateOwners = /* @__PURE__ */ new Map();
|
|
2699
|
-
for (const [templateOutputPath, meta] of templateMeta.entries()) for (const scope of meta.headingScopes) {
|
|
2700
|
-
if (Object.prototype.hasOwnProperty.call(fileMap, scope)) continue;
|
|
2701
|
-
let bestRootLen = -1;
|
|
2702
|
-
for (const root of meta.commandTreeRoots) if (isSubcommandOf(scope, root)) bestRootLen = Math.max(bestRootLen, scopeRootLength(root));
|
|
2703
|
-
if (bestRootLen < 0) continue;
|
|
2704
|
-
const existing = templateOwners.get(scope);
|
|
2705
|
-
if (!existing || bestRootLen > existing.rootLen) templateOwners.set(scope, {
|
|
2706
|
-
outputPath: templateOutputPath,
|
|
2707
|
-
rootLen: bestRootLen
|
|
2708
|
-
});
|
|
2709
|
-
}
|
|
2710
|
-
for (const [scope, { outputPath }] of templateOwners) setFileMapEntry(templateFileMap, scope, outputPath);
|
|
2711
|
-
const results = [];
|
|
2712
|
-
let hasError = false;
|
|
2713
|
-
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
2714
|
-
const { fileConfig, specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
2715
|
-
if (specifiedCommands.length === 0) continue;
|
|
2716
|
-
if (commandPaths.length === 0) continue;
|
|
2717
|
-
const fileTargetCommands = hasTargetCommands ? findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) : [];
|
|
2718
|
-
if (hasTargetCommands && fileTargetCommands.length === 0) continue;
|
|
2719
|
-
let fileStatus = "match";
|
|
2720
|
-
const diffs = [];
|
|
2721
|
-
const minDepth = Math.min(...commandPaths.map((p) => allCommands.get(p)?.depth ?? 1));
|
|
2722
|
-
const adjustedHeadingLevel = Math.max(1, (format?.headingLevel ?? 1) - (minDepth - 1));
|
|
2723
|
-
const isRootDocFile = usingPathConfig && rootDoc && normalizeDocPathForComparison(filePath) === normalizeDocPathForComparison(rootDoc.path);
|
|
2724
|
-
const fileUsesMarkers = usingPathConfig || customizable;
|
|
2725
|
-
const fileRenderer = createCommandRenderer({
|
|
2726
|
-
...format,
|
|
2727
|
-
headingLevel: adjustedHeadingLevel,
|
|
2728
|
-
markerless: !fileUsesMarkers
|
|
2729
|
-
});
|
|
2730
|
-
const render = fileConfig.render ?? fileRenderer;
|
|
2731
|
-
if (Boolean(isRootDocFile) || hasTargetCommands && fileUsesMarkers) {
|
|
2732
|
-
let existingContent = readFile(filePath);
|
|
2733
|
-
const sortedCommandPaths = sortDepthFirst(commandPaths, specifiedCommands);
|
|
2734
|
-
const effectiveTargetCommands = hasTargetCommands ? fileTargetCommands : commandPaths;
|
|
2735
|
-
for (const targetCommand of effectiveTargetCommands) {
|
|
2736
|
-
const rawSection = generateCommandSection(targetCommand, allCommands, render, filePath, templateFileMap, rootDoc?.path, globalOptionDefinitions.size > 0, ignores);
|
|
2737
|
-
if (!rawSection) throw new Error(`Target command "${targetCommand}" not found in commands`);
|
|
2738
|
-
const generatedSection = await applyFormatter(rawSection, formatter);
|
|
2739
|
-
if (!existingContent) {
|
|
2740
|
-
if (updateMode) {
|
|
2741
|
-
const header = targetCommand === "" && fileConfig ? generateFileHeader(fileConfig) : null;
|
|
2742
|
-
const fullContent = header ? `${header}\n${generatedSection}` : generatedSection;
|
|
2743
|
-
writeFile(filePath, fullContent);
|
|
2744
|
-
existingContent = fullContent;
|
|
2745
|
-
fileStatus = "created";
|
|
2746
|
-
} else {
|
|
2747
|
-
hasError = true;
|
|
2748
|
-
fileStatus = "diff";
|
|
2749
|
-
diffs.push(`File does not exist. Target command "${targetCommand}" section cannot be validated.`);
|
|
2750
|
-
}
|
|
2751
|
-
continue;
|
|
2752
|
-
}
|
|
2753
|
-
const existingMarkers = collectSectionMarkers(existingContent, targetCommand);
|
|
2754
|
-
if (existingMarkers.length === 0) {
|
|
2755
|
-
if (updateMode) {
|
|
2756
|
-
existingContent = insertCommandSections(existingContent, targetCommand, generatedSection, sortedCommandPaths);
|
|
2757
|
-
writeFile(filePath, existingContent);
|
|
2758
|
-
if (fileStatus !== "created") fileStatus = "updated";
|
|
2759
|
-
} else {
|
|
2760
|
-
hasError = true;
|
|
2761
|
-
fileStatus = "diff";
|
|
2762
|
-
diffs.push(`Existing file does not contain section markers for command "${targetCommand}"`);
|
|
2763
|
-
}
|
|
2764
|
-
continue;
|
|
2765
|
-
}
|
|
2766
|
-
for (const sectionType of existingMarkers) {
|
|
2767
|
-
const existingSection = extractSectionMarker(existingContent, sectionType, targetCommand);
|
|
2768
|
-
const generatedSectionPart = extractSectionMarker(generatedSection, sectionType, targetCommand);
|
|
2769
|
-
if (!existingSection) continue;
|
|
2770
|
-
if (!generatedSectionPart) {
|
|
2771
|
-
const emptyMarker = sectionStartMarker(sectionType, targetCommand) + "\n" + sectionEndMarker(sectionType, targetCommand);
|
|
2772
|
-
if (existingSection !== emptyMarker) if (updateMode) {
|
|
2773
|
-
const updated = replaceSectionMarker(existingContent, sectionType, targetCommand, emptyMarker);
|
|
2774
|
-
if (!updated) throw new Error(`Failed to replace stale ${sectionType} section for command "${targetCommand}"`);
|
|
2775
|
-
existingContent = updated.replace(/\n{3,}/g, "\n\n");
|
|
2776
|
-
writeFile(filePath, existingContent);
|
|
2777
|
-
if (fileStatus !== "created") fileStatus = "updated";
|
|
2778
|
-
} else {
|
|
2779
|
-
hasError = true;
|
|
2780
|
-
fileStatus = "diff";
|
|
2781
|
-
diffs.push(formatDiff(existingSection, emptyMarker));
|
|
2782
|
-
}
|
|
2783
|
-
continue;
|
|
2784
|
-
}
|
|
2785
|
-
if (existingSection !== generatedSectionPart) if (updateMode) {
|
|
2786
|
-
const updated = replaceSectionMarker(existingContent, sectionType, targetCommand, generatedSectionPart);
|
|
2787
|
-
if (updated) {
|
|
2788
|
-
existingContent = updated;
|
|
2789
|
-
writeFile(filePath, existingContent);
|
|
2790
|
-
if (fileStatus !== "created") fileStatus = "updated";
|
|
2791
|
-
} else throw new Error(`Failed to replace ${sectionType} section for command "${targetCommand}"`);
|
|
2792
|
-
} else {
|
|
2793
|
-
hasError = true;
|
|
2794
|
-
fileStatus = "diff";
|
|
2795
|
-
diffs.push(formatDiff(existingSection, generatedSectionPart));
|
|
2796
|
-
}
|
|
2797
|
-
}
|
|
2798
|
-
if (doctorMode || customizable) {
|
|
2799
|
-
const generatedMarkers = collectSectionMarkers(generatedSection, targetCommand);
|
|
2800
|
-
const existingMarkerSet = new Set(existingMarkers);
|
|
2801
|
-
for (const sectionType of generatedMarkers) {
|
|
2802
|
-
if (existingMarkerSet.has(sectionType)) continue;
|
|
2803
|
-
const generatedSectionPart = extractSectionMarker(generatedSection, sectionType, targetCommand);
|
|
2804
|
-
if (!generatedSectionPart) continue;
|
|
2805
|
-
if (doctorMode && updateMode) {
|
|
2806
|
-
existingContent = insertSectionMarkerAtOrder(existingContent, sectionType, targetCommand, generatedSectionPart);
|
|
2807
|
-
writeFile(filePath, existingContent);
|
|
2808
|
-
if (fileStatus !== "created") fileStatus = "updated";
|
|
2809
|
-
} else if (doctorMode) {
|
|
2810
|
-
hasError = true;
|
|
2811
|
-
hasDoctorIssues = true;
|
|
2812
|
-
fileStatus = "diff";
|
|
2813
|
-
diffs.push(`[doctor] Missing section marker "${sectionType}" for command "${formatCommandPath(targetCommand)}". Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to insert.\n${generatedSectionPart}`);
|
|
2814
|
-
} else console.warn(`[politty] Missing "${sectionType}" section for command "${formatCommandPath(targetCommand)}" in ${filePath}. Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to insert it, or leave it removed to opt that section out.`);
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2818
|
-
if (existingContent) {
|
|
2819
|
-
const existingMarkerPaths = collectSectionMarkerPaths(existingContent);
|
|
2820
|
-
const commandPathSet = new Set(commandPaths);
|
|
2821
|
-
if (updateMode) {
|
|
2822
|
-
let removedAny = false;
|
|
2823
|
-
for (const markerPath of existingMarkerPaths) if (!commandPathSet.has(markerPath)) {
|
|
2824
|
-
existingContent = removeCommandSections(existingContent, markerPath);
|
|
2825
|
-
removedAny = true;
|
|
2826
|
-
}
|
|
2827
|
-
if (removedAny) {
|
|
2828
|
-
writeFile(filePath, existingContent);
|
|
2829
|
-
if (fileStatus !== "created") fileStatus = "updated";
|
|
2830
|
-
}
|
|
2831
|
-
} else for (const markerPath of existingMarkerPaths) if (!commandPathSet.has(markerPath)) {
|
|
2832
|
-
hasError = true;
|
|
2833
|
-
fileStatus = "diff";
|
|
2834
|
-
diffs.push(`Found orphaned section markers for deleted command "${formatCommandPath(markerPath)}"`);
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
} else {
|
|
2838
|
-
const generatedMarkdown = await applyFormatter(generateFileMarkdown(commandPaths, allCommands, render, filePath, templateFileMap, specifiedCommands, fileConfig, rootDoc?.path, globalOptionDefinitions.size > 0, ignores), formatter);
|
|
2839
|
-
const comparison = compareWithExisting(generatedMarkdown, filePath);
|
|
2840
|
-
if (comparison.match) {} else if (updateMode) {
|
|
2841
|
-
writeFile(filePath, generatedMarkdown);
|
|
2842
|
-
fileStatus = comparison.fileExists ? "updated" : "created";
|
|
2843
|
-
} else {
|
|
2844
|
-
hasError = true;
|
|
2845
|
-
fileStatus = "diff";
|
|
2846
|
-
if (comparison.diff) diffs.push(comparison.diff);
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
if (diffs.length > 0) fileStatus = "diff";
|
|
2850
|
-
results.push({
|
|
2851
|
-
path: filePath,
|
|
2852
|
-
status: fileStatus,
|
|
2853
|
-
diff: diffs.length > 0 ? diffs.join("\n\n") : void 0
|
|
2854
|
-
});
|
|
2855
|
-
}
|
|
2856
|
-
let normalizedTemplateGlobalOptions;
|
|
2857
|
-
if (rootDoc?.globalOptions) normalizedTemplateGlobalOptions = normalizeGlobalOptions(rootDoc.globalOptions);
|
|
2858
|
-
else {
|
|
2859
|
-
const shape = deriveGlobalArgsShape(globalArgs);
|
|
2860
|
-
if (shape) normalizedTemplateGlobalOptions = { args: shape };
|
|
2861
|
-
}
|
|
2862
|
-
for (const [outputPath, templatePath] of templateEntries) {
|
|
2863
|
-
if (!activeTemplateMeta.has(outputPath)) continue;
|
|
2864
|
-
const templateContent = templateContents.get(outputPath) ?? null;
|
|
2865
|
-
if (templateContent === null) {
|
|
2866
|
-
hasError = true;
|
|
2867
|
-
results.push({
|
|
2868
|
-
path: outputPath,
|
|
2869
|
-
status: "diff",
|
|
2870
|
-
diff: `Template file not found: ${templatePath}`
|
|
2871
|
-
});
|
|
2872
|
-
continue;
|
|
2873
|
-
}
|
|
2874
|
-
const meta = templateMeta.get(outputPath);
|
|
2875
|
-
const templateLineEnding = detectLineEnding(templateContent);
|
|
2876
|
-
const outputTemplateContent = stripPolittyFrontMatterForOutput(templateContent);
|
|
2877
|
-
const headingDepths = (meta?.headingScopes ?? []).map((s) => allCommands.get(s)?.depth ?? 1);
|
|
2878
|
-
const minDepth = headingDepths.length > 0 ? Math.min(...headingDepths) : 1;
|
|
2879
|
-
const adjustedHeadingLevel = clampHeadingLevel((format?.headingLevel ?? 1) - (minDepth - 1));
|
|
2880
|
-
const templateRenderer = createCommandRenderer({
|
|
2881
|
-
...format,
|
|
2882
|
-
headingLevel: adjustedHeadingLevel
|
|
2883
|
-
});
|
|
2884
|
-
const outputEmitsGlobalOptions = meta?.emitsGlobalOptions ?? false;
|
|
2885
|
-
const excludeOptionNames = (rootDoc !== void 0 && globalOptionDefinitions.size > 0 || outputEmitsGlobalOptions || templateGlobalOptionsProviderPath !== void 0) && templateGlobalOptionFields.size > 0 ? new Set(templateGlobalOptionFields.keys()) : void 0;
|
|
2886
|
-
const sectionHasGlobalOptions = excludeOptionNames !== void 0;
|
|
2887
|
-
const effectiveRootDocPath = outputEmitsGlobalOptions ? outputPath : rootDoc?.path ?? templateGlobalOptionsProviderPath;
|
|
2888
|
-
const placeholders = Array.from(new Set(outputTemplateContent.match(TEMPLATE_PLACEHOLDER_REGEX) ?? []));
|
|
2889
|
-
const replacements = /* @__PURE__ */ new Map();
|
|
2890
|
-
const exclusions = templateExclusions.get(outputPath) ?? createTemplateExclusions(/* @__PURE__ */ new Set());
|
|
2891
|
-
for (const placeholder of placeholders) {
|
|
2892
|
-
const placeholderKey = templatePlaceholderKey(placeholder);
|
|
2893
|
-
if (exclusions.rawKeys.has(placeholderKey) || isRawCommandPlaceholderUnderExcludedScope(placeholderKey, exclusions)) {
|
|
2894
|
-
replacements.set(placeholder, "");
|
|
2895
|
-
continue;
|
|
2896
|
-
}
|
|
2897
|
-
const parsed = parsePlaceholder(placeholder, allCommands);
|
|
2898
|
-
if (shouldSkipTemplatePlaceholder(placeholder, parsed, exclusions)) {
|
|
2899
|
-
replacements.set(placeholder, "");
|
|
2900
|
-
continue;
|
|
2901
|
-
}
|
|
2902
|
-
if (parsed.kind === "invalid") throw new Error(`Internal error: unresolved placeholder "${placeholder}" in template "${templatePath}": ${parsed.reason}`);
|
|
2903
|
-
if (parsed.kind === "command") {
|
|
2904
|
-
const { scope, type } = parsed;
|
|
2905
|
-
if (type === void 0) {
|
|
2906
|
-
const rawSection = generateCommandTreeMarkdown(scope, allCommands, templateRenderer, ignores, outputPath, templateFileMap, effectiveRootDocPath, sectionHasGlobalOptions, excludeOptionNames, exclusions);
|
|
2907
|
-
if (rawSection === null) {
|
|
2908
|
-
replacements.set(placeholder, "");
|
|
2909
|
-
continue;
|
|
2910
|
-
}
|
|
2911
|
-
replacements.set(placeholder, stripPolittyMarkers(rawSection));
|
|
2912
|
-
} else {
|
|
2913
|
-
const rawSection = generateCommandSection(scope, allCommands, templateRenderer, outputPath, templateFileMap, effectiveRootDocPath, sectionHasGlobalOptions, ignores, excludeOptionNames, exclusions);
|
|
2914
|
-
if (rawSection === null) {
|
|
2915
|
-
replacements.set(placeholder, "");
|
|
2916
|
-
continue;
|
|
2917
|
-
}
|
|
2918
|
-
const extracted = extractSectionMarker(rawSection, type, scope);
|
|
2919
|
-
replacements.set(placeholder, extracted === null ? "" : stripPolittyMarkers(extracted));
|
|
2920
|
-
}
|
|
2921
|
-
} else if (parsed.kind === "global-options") if (normalizedTemplateGlobalOptions) replacements.set(placeholder, buildGlobalOptionsContent(normalizedTemplateGlobalOptions));
|
|
2922
|
-
else replacements.set(placeholder, "");
|
|
2923
|
-
else if (parsed.kind === "index") {
|
|
2924
|
-
const indexContent = await renderCommandIndex(command, [...deriveIndexFromFiles(files, outputPath, allCommands, ignores), ...deriveIndexFromTemplateOutputs(templateMeta, outputPath, outputPath, allCommands)], rootDoc?.index);
|
|
2925
|
-
replacements.set(placeholder, indexContent);
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
let generated = outputTemplateContent.replace(/((?:\r?\n)*)([ \t]*)(\{\{politty:[^{}]*\}\})([ \t]*)((?:\r?\n)*)/g, (match, leadNl, leadWs, placeholder, trailWs, trailNl, offset, fullString) => {
|
|
2929
|
-
const replacement = replacements.get(placeholder);
|
|
2930
|
-
if (replacement === void 0) throw new Error(`Internal error: unresolved placeholder "${placeholder}" in template "${templatePath}".`);
|
|
2931
|
-
const startsLine = leadNl !== "" || offset === 0 || fullString[offset - 1] === "\n";
|
|
2932
|
-
const endsLine = trailNl !== "" || offset + match.length === fullString.length;
|
|
2933
|
-
if (replacement === "" && startsLine && endsLine) {
|
|
2934
|
-
if (leadNl === "" || trailNl === "") return "";
|
|
2935
|
-
const leadBreaks = countLineBreaks(leadNl);
|
|
2936
|
-
const trailBreaks = countLineBreaks(trailNl);
|
|
2937
|
-
const widest = Math.max(leadBreaks, trailBreaks);
|
|
2938
|
-
const lineEnding = leadBreaks >= trailBreaks ? detectLineEnding(leadNl) : detectLineEnding(trailNl);
|
|
2939
|
-
return widest >= 2 ? lineEnding + lineEnding : widest === 1 ? lineEnding : "";
|
|
2940
|
-
}
|
|
2941
|
-
return `${leadNl}${leadWs}${replacement}${trailWs}${trailNl}`;
|
|
2942
|
-
});
|
|
2943
|
-
generated = `${generated.trimEnd()}${templateLineEnding}`;
|
|
2944
|
-
generated = await applyFormatter(generated, formatter);
|
|
2945
|
-
const comparison = compareWithExisting(generated, outputPath);
|
|
2946
|
-
let templateStatus = "match";
|
|
2947
|
-
let templateDiff;
|
|
2948
|
-
if (comparison.match) {} else if (updateMode) {
|
|
2949
|
-
writeFile(outputPath, generated);
|
|
2950
|
-
templateStatus = comparison.fileExists ? "updated" : "created";
|
|
2951
|
-
} else {
|
|
2952
|
-
hasError = true;
|
|
2953
|
-
templateStatus = "diff";
|
|
2954
|
-
if (comparison.diff) templateDiff = comparison.diff;
|
|
2955
|
-
}
|
|
2956
|
-
results.push({
|
|
2957
|
-
path: outputPath,
|
|
2958
|
-
status: templateStatus,
|
|
2959
|
-
diff: templateDiff
|
|
2960
|
-
});
|
|
2961
|
-
}
|
|
2962
|
-
if (rootDoc) {
|
|
2963
|
-
const rootDocFilePath = rootDoc.path;
|
|
2964
|
-
let rootDocStatus = "match";
|
|
2965
|
-
const rootDocDiffs = [];
|
|
2966
|
-
const existingContent = readFile(rootDocFilePath);
|
|
2967
|
-
if (existingContent === null) {
|
|
2968
|
-
hasError = true;
|
|
2969
|
-
rootDocStatus = "diff";
|
|
2970
|
-
rootDocDiffs.push("File does not exist. Cannot validate rootDoc markers.");
|
|
2971
|
-
} else {
|
|
2972
|
-
let content = existingContent;
|
|
2973
|
-
let markerUpdated = false;
|
|
2974
|
-
const rootInfo = config.rootInfo;
|
|
2975
|
-
const rootDocFileConfig = { title: rootInfo?.title ?? command.name };
|
|
2976
|
-
if (rootDoc.headingLevel !== void 0) rootDocFileConfig.headingLevel = rootDoc.headingLevel;
|
|
2977
|
-
const rootDescription = rootInfo?.description ?? command.description;
|
|
2978
|
-
if (rootDescription !== void 0) rootDocFileConfig.description = rootDescription;
|
|
2979
|
-
const headerResult = processFileHeader(content, rootDocFileConfig, updateMode);
|
|
2980
|
-
content = headerResult.content;
|
|
2981
|
-
if (headerResult.diff) rootDocDiffs.push(headerResult.diff);
|
|
2982
|
-
if (headerResult.hasError) hasError = true;
|
|
2983
|
-
if (headerResult.wasUpdated) markerUpdated = true;
|
|
2984
|
-
if (rootInfo?.header) {
|
|
2985
|
-
const headerMarkerResult = await processStaticMarker(content, "Root header", rootHeaderStartMarker(), rootHeaderEndMarker(), rootInfo.header, updateMode, formatter, usingPathConfig);
|
|
2986
|
-
content = headerMarkerResult.content;
|
|
2987
|
-
rootDocDiffs.push(...headerMarkerResult.diffs);
|
|
2988
|
-
if (headerMarkerResult.hasError) hasError = true;
|
|
2989
|
-
if (headerMarkerResult.wasUpdated) markerUpdated = true;
|
|
2990
|
-
}
|
|
2991
|
-
if (!usingPathConfig) {
|
|
2992
|
-
const unexpectedSectionPaths = collectSectionMarkerPaths(content);
|
|
2993
|
-
if (unexpectedSectionPaths.length > 0) if (updateMode) {
|
|
2994
|
-
for (const commandPath of unexpectedSectionPaths) content = removeCommandSections(content, commandPath);
|
|
2995
|
-
markerUpdated = true;
|
|
2996
|
-
} else {
|
|
2997
|
-
hasError = true;
|
|
2998
|
-
rootDocDiffs.push(`Found unexpected section markers in rootDoc: ${unexpectedSectionPaths.map((commandPath) => `"${formatCommandPath(commandPath)}"`).join(", ")}.`);
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
const normalizedGlobalOptions = normalizeGlobalOptions(rootDoc.globalOptions);
|
|
3002
|
-
if (normalizedGlobalOptions) {
|
|
3003
|
-
const globalOptionsResult = await processGlobalOptionsMarker(content, normalizedGlobalOptions, updateMode, formatter, usingPathConfig);
|
|
3004
|
-
content = globalOptionsResult.content;
|
|
3005
|
-
rootDocDiffs.push(...globalOptionsResult.diffs);
|
|
3006
|
-
if (globalOptionsResult.hasError) hasError = true;
|
|
3007
|
-
if (globalOptionsResult.wasUpdated) markerUpdated = true;
|
|
3008
|
-
}
|
|
3009
|
-
const derivedCategories = deriveIndexFromFiles(files, rootDocFilePath, allCommands, ignores);
|
|
3010
|
-
const indexScope = node_path.relative(process.cwd(), rootDocFilePath).replace(/\\/g, "/");
|
|
3011
|
-
const indexResult = await processIndexMarker(content, derivedCategories, command, indexScope, updateMode, formatter, rootDoc.index);
|
|
3012
|
-
content = indexResult.content;
|
|
3013
|
-
rootDocDiffs.push(...indexResult.diffs);
|
|
3014
|
-
if (indexResult.hasError) hasError = true;
|
|
3015
|
-
if (indexResult.wasUpdated) markerUpdated = true;
|
|
3016
|
-
if (rootInfo?.footer) {
|
|
3017
|
-
const footerMarkerResult = await processStaticMarker(content, "Root footer", rootFooterStartMarker(), rootFooterEndMarker(), rootInfo.footer, updateMode, formatter, usingPathConfig);
|
|
3018
|
-
content = footerMarkerResult.content;
|
|
3019
|
-
rootDocDiffs.push(...footerMarkerResult.diffs);
|
|
3020
|
-
if (footerMarkerResult.hasError) hasError = true;
|
|
3021
|
-
if (footerMarkerResult.wasUpdated) markerUpdated = true;
|
|
3022
|
-
}
|
|
3023
|
-
if (updateMode && markerUpdated) {
|
|
3024
|
-
writeFile(rootDocFilePath, content);
|
|
3025
|
-
if (rootDocStatus === "match") rootDocStatus = "updated";
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
if (rootDocDiffs.length > 0) rootDocStatus = "diff";
|
|
3029
|
-
results.push({
|
|
3030
|
-
path: rootDocFilePath,
|
|
3031
|
-
status: rootDocStatus,
|
|
3032
|
-
diff: rootDocDiffs.length > 0 ? rootDocDiffs.join("\n\n") : void 0
|
|
3033
|
-
});
|
|
3034
|
-
}
|
|
3035
|
-
const errorHint = hasDoctorIssues ? `Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to fix missing markers.` : `Run with ${UPDATE_GOLDEN_ENV}=true to update.`;
|
|
3036
|
-
return {
|
|
3037
|
-
success: !hasError,
|
|
3038
|
-
files: results,
|
|
3039
|
-
error: hasError ? `Documentation is out of date. ${errorHint}` : void 0
|
|
3040
|
-
};
|
|
3041
|
-
}
|
|
3042
|
-
/**
|
|
3043
|
-
* Assert that documentation matches golden files
|
|
3044
|
-
* Throws an error if there are differences and update mode is not enabled
|
|
3045
|
-
*/
|
|
3046
|
-
async function assertDocMatch(config) {
|
|
3047
|
-
const result = await generateDoc(config);
|
|
3048
|
-
if (!result.success) {
|
|
3049
|
-
const diffMessages = result.files.filter((f) => f.status === "diff").map((f) => {
|
|
3050
|
-
let msg = `File: ${f.path}\n`;
|
|
3051
|
-
if (f.diff) msg += f.diff;
|
|
3052
|
-
return msg;
|
|
3053
|
-
}).join("\n\n");
|
|
3054
|
-
throw new Error(`Documentation does not match golden files.\n\n${diffMessages}\n\n` + (result.error ?? `Run with ${"POLITTY_DOCS_UPDATE"}=true to update the documentation.`));
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3057
|
-
/**
|
|
3058
|
-
* Initialize documentation files by deleting them
|
|
3059
|
-
* Only deletes when update mode is enabled (POLITTY_DOCS_UPDATE=true)
|
|
3060
|
-
* Use this in beforeAll to ensure skipped tests don't leave stale sections
|
|
3061
|
-
* @param config - Config containing files to initialize, or a single file path
|
|
3062
|
-
* @param fileSystem - Optional fs implementation (useful when fs is mocked)
|
|
3063
|
-
*/
|
|
3064
|
-
function initDocFile(config, fileSystem) {
|
|
3065
|
-
if (!isTruthyEnv("POLITTY_DOCS_UPDATE")) return;
|
|
3066
|
-
if (typeof config === "string") deleteFile(config, fileSystem);
|
|
3067
|
-
else {
|
|
3068
|
-
const protectedPaths = new Set(Object.values(config.templates ?? {}).map(normalizeDocPathForComparison));
|
|
3069
|
-
if (config.rootDoc) protectedPaths.add(normalizeDocPathForComparison(config.rootDoc.path));
|
|
3070
|
-
const isProtectedPath = (p) => protectedPaths.has(normalizeDocPathForComparison(p));
|
|
3071
|
-
if (config.files) for (const filePath of Object.keys(config.files)) {
|
|
3072
|
-
if (isProtectedPath(filePath)) continue;
|
|
3073
|
-
deleteFile(filePath, fileSystem);
|
|
3074
|
-
}
|
|
3075
|
-
if (config.templates) for (const outputPath of Object.keys(config.templates)) {
|
|
3076
|
-
if (isProtectedPath(outputPath)) continue;
|
|
3077
|
-
deleteFile(outputPath, fileSystem);
|
|
3078
|
-
}
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
//#endregion
|
|
3083
|
-
exports.DOCTOR_ENV = DOCTOR_ENV;
|
|
3084
|
-
exports.GLOBAL_OPTIONS_MARKER_PREFIX = GLOBAL_OPTIONS_MARKER_PREFIX;
|
|
3085
|
-
exports.INDEX_MARKER_PREFIX = INDEX_MARKER_PREFIX;
|
|
3086
|
-
exports.ROOT_FOOTER_MARKER_PREFIX = ROOT_FOOTER_MARKER_PREFIX;
|
|
3087
|
-
exports.ROOT_HEADER_MARKER_PREFIX = ROOT_HEADER_MARKER_PREFIX;
|
|
3088
|
-
exports.SECTION_MARKER_PREFIX = SECTION_MARKER_PREFIX;
|
|
3089
|
-
exports.SECTION_TYPES = SECTION_TYPES;
|
|
3090
|
-
exports.UPDATE_GOLDEN_ENV = UPDATE_GOLDEN_ENV;
|
|
3091
|
-
exports.assertDocMatch = assertDocMatch;
|
|
3092
|
-
exports.buildCommandInfo = buildCommandInfo;
|
|
3093
|
-
exports.collectAllCommands = collectAllCommands;
|
|
3094
|
-
exports.compareWithExisting = compareWithExisting;
|
|
3095
|
-
exports.createCommandRenderer = createCommandRenderer;
|
|
3096
|
-
exports.defaultRenderers = defaultRenderers;
|
|
3097
|
-
exports.executeExamples = executeExamples;
|
|
3098
|
-
exports.formatDiff = formatDiff;
|
|
3099
|
-
exports.generateDoc = generateDoc;
|
|
3100
|
-
exports.globalOptionsEndMarker = globalOptionsEndMarker;
|
|
3101
|
-
exports.globalOptionsStartMarker = globalOptionsStartMarker;
|
|
3102
|
-
exports.indexEndMarker = indexEndMarker;
|
|
3103
|
-
exports.indexStartMarker = indexStartMarker;
|
|
3104
|
-
exports.initDocFile = initDocFile;
|
|
3105
|
-
exports.renderArgsTable = renderArgsTable;
|
|
3106
|
-
exports.renderArgumentsList = renderArgumentsList;
|
|
3107
|
-
exports.renderArgumentsListFromArray = renderArgumentsListFromArray;
|
|
3108
|
-
exports.renderArgumentsTable = renderArgumentsTable;
|
|
3109
|
-
exports.renderArgumentsTableFromArray = renderArgumentsTableFromArray;
|
|
3110
|
-
exports.renderCommandIndex = renderCommandIndex;
|
|
3111
|
-
exports.renderExamplesDefault = renderExamplesDefault;
|
|
3112
|
-
exports.renderOptionsList = renderOptionsList;
|
|
3113
|
-
exports.renderOptionsListFromArray = renderOptionsListFromArray;
|
|
3114
|
-
exports.renderOptionsTable = renderOptionsTable;
|
|
3115
|
-
exports.renderOptionsTableFromArray = renderOptionsTableFromArray;
|
|
3116
|
-
exports.renderSubcommandsTable = renderSubcommandsTable;
|
|
3117
|
-
exports.renderSubcommandsTableFromArray = renderSubcommandsTableFromArray;
|
|
3118
|
-
exports.renderUsage = renderUsage;
|
|
3119
|
-
exports.resolveLazyCommand = require_schema_extractor.resolveLazyCommand;
|
|
3120
|
-
exports.rootFooterEndMarker = rootFooterEndMarker;
|
|
3121
|
-
exports.rootFooterStartMarker = rootFooterStartMarker;
|
|
3122
|
-
exports.rootHeaderEndMarker = rootHeaderEndMarker;
|
|
3123
|
-
exports.rootHeaderStartMarker = rootHeaderStartMarker;
|
|
3124
|
-
exports.sectionEndMarker = sectionEndMarker;
|
|
3125
|
-
exports.sectionStartMarker = sectionStartMarker;
|
|
3126
|
-
exports.writeFile = writeFile;
|
|
3127
|
-
//# sourceMappingURL=index.cjs.map
|