politty 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { n as getExtractedFields, t as extractFields } from "../schema-extractor-1YXqFSDT.js";
2
2
  import { i as createLogCollector, n as resolveLazyCommand } from "../subcommand-router-DtCeT_O9.js";
3
3
  import { z } from "zod";
4
+ import { isDeepStrictEqual } from "node:util";
4
5
  import * as fs from "node:fs";
5
6
  import * as path from "node:path";
6
7
 
@@ -706,6 +707,232 @@ function parseExampleCmd(cmd) {
706
707
  return args;
707
708
  }
708
709
 
710
+ //#endregion
711
+ //#region src/docs/render-args.ts
712
+ /**
713
+ * Extract ResolvedFieldMeta array from ArgsShape
714
+ *
715
+ * This converts a raw args shape (like `commonArgs`) into the
716
+ * ResolvedFieldMeta format used by politty's rendering functions.
717
+ */
718
+ function extractArgsFields(args) {
719
+ return extractFields(z.object(args)).fields;
720
+ }
721
+ /**
722
+ * Render args definition as a markdown options table
723
+ *
724
+ * This function takes raw args definitions (like `commonArgs`) and
725
+ * renders them as a markdown table suitable for documentation.
726
+ *
727
+ * @example
728
+ * import { renderArgsTable } from "politty/docs";
729
+ * import { commonArgs, workspaceArgs } from "./args";
730
+ *
731
+ * const table = renderArgsTable({
732
+ * ...commonArgs,
733
+ * ...workspaceArgs,
734
+ * });
735
+ * // | Option | Alias | Description | Default |
736
+ * // |--------|-------|-------------|---------|
737
+ * // | `--env-file <ENV_FILE>` | `-e` | Path to environment file | - |
738
+ * // ...
739
+ *
740
+ * @param args - Args shape (Record of string keys to Zod schemas with arg() metadata)
741
+ * @param options - Rendering options
742
+ * @returns Rendered markdown table string
743
+ */
744
+ function renderArgsTable(args, options) {
745
+ const optionFields = extractArgsFields(args).filter((f) => !f.positional);
746
+ if (optionFields.length === 0) return "";
747
+ if (options?.columns) return renderFilteredTable(optionFields, options.columns);
748
+ return renderOptionsTableFromArray(optionFields);
749
+ }
750
+ /**
751
+ * Escape markdown special characters in table cells
752
+ */
753
+ function escapeTableCell$1(str) {
754
+ return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
755
+ }
756
+ /**
757
+ * Format default value for display
758
+ */
759
+ function formatDefaultValue(value) {
760
+ if (value === void 0) return "-";
761
+ return `\`${JSON.stringify(value)}\``;
762
+ }
763
+ /**
764
+ * Render table with filtered columns
765
+ */
766
+ function renderFilteredTable(options, columns) {
767
+ const lines = [];
768
+ const headerCells = [];
769
+ const separatorCells = [];
770
+ for (const col of columns) switch (col) {
771
+ case "option":
772
+ headerCells.push("Option");
773
+ separatorCells.push("------");
774
+ break;
775
+ case "alias":
776
+ headerCells.push("Alias");
777
+ separatorCells.push("-----");
778
+ break;
779
+ case "description":
780
+ headerCells.push("Description");
781
+ separatorCells.push("-----------");
782
+ break;
783
+ case "required":
784
+ headerCells.push("Required");
785
+ separatorCells.push("--------");
786
+ break;
787
+ case "default":
788
+ headerCells.push("Default");
789
+ separatorCells.push("-------");
790
+ break;
791
+ case "env":
792
+ headerCells.push("Env");
793
+ separatorCells.push("---");
794
+ break;
795
+ }
796
+ lines.push(`| ${headerCells.join(" | ")} |`);
797
+ lines.push(`| ${separatorCells.join(" | ")} |`);
798
+ for (const opt of options) {
799
+ const cells = [];
800
+ for (const col of columns) switch (col) {
801
+ case "option": {
802
+ const placeholder = opt.placeholder ?? opt.cliName.toUpperCase().replace(/-/g, "_");
803
+ const optionName = opt.type === "boolean" ? `\`--${opt.cliName}\`` : `\`--${opt.cliName} <${placeholder}>\``;
804
+ cells.push(optionName);
805
+ break;
806
+ }
807
+ case "alias":
808
+ cells.push(opt.alias ? `\`-${opt.alias}\`` : "-");
809
+ break;
810
+ case "description":
811
+ cells.push(escapeTableCell$1(opt.description ?? ""));
812
+ break;
813
+ case "required":
814
+ cells.push(opt.required ? "Yes" : "No");
815
+ break;
816
+ case "default":
817
+ cells.push(formatDefaultValue(opt.defaultValue));
818
+ break;
819
+ case "env": {
820
+ const envNames = opt.env ? Array.isArray(opt.env) ? opt.env.map((e) => `\`${e}\``).join(", ") : `\`${opt.env}\`` : "-";
821
+ cells.push(envNames);
822
+ break;
823
+ }
824
+ }
825
+ lines.push(`| ${cells.join(" | ")} |`);
826
+ }
827
+ return lines.join("\n");
828
+ }
829
+
830
+ //#endregion
831
+ //#region src/docs/render-index.ts
832
+ /**
833
+ * Escape markdown special characters in table cells
834
+ */
835
+ function escapeTableCell(str) {
836
+ return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
837
+ }
838
+ /**
839
+ * Generate anchor from command path
840
+ */
841
+ function generateAnchor(commandPath) {
842
+ return commandPath.replace(/\s+/g, "-").toLowerCase();
843
+ }
844
+ /**
845
+ * Check if a command is a leaf (has no subcommands)
846
+ */
847
+ function isLeafCommand(info) {
848
+ return info.subCommands.length === 0;
849
+ }
850
+ /**
851
+ * Expand commands to include their subcommands
852
+ * If a command has subcommands, recursively find all commands under it
853
+ *
854
+ * @param commandPaths - Command paths to expand
855
+ * @param allCommands - Map of all available commands
856
+ * @param leafOnly - If true, only include leaf commands; if false, include all commands
857
+ */
858
+ function expandCommands(commandPaths, allCommands, leafOnly) {
859
+ const result = [];
860
+ for (const cmdPath of commandPaths) {
861
+ const info = allCommands.get(cmdPath);
862
+ if (!info) continue;
863
+ if (isLeafCommand(info)) result.push(cmdPath);
864
+ else for (const [path, pathInfo] of allCommands) if (cmdPath === "" ? path.length > 0 : path.startsWith(cmdPath + " ") || path === cmdPath) {
865
+ if (isLeafCommand(pathInfo) || !leafOnly) result.push(path);
866
+ }
867
+ }
868
+ return result;
869
+ }
870
+ /**
871
+ * Render a single category section
872
+ */
873
+ function renderCategory(category, allCommands, headingLevel, leafOnly) {
874
+ const h = "#".repeat(headingLevel);
875
+ const lines = [];
876
+ lines.push(`${h} [${category.title}](${category.docPath})`);
877
+ lines.push("");
878
+ lines.push(category.description);
879
+ lines.push("");
880
+ const commandPaths = expandCommands(category.commands, allCommands, leafOnly);
881
+ lines.push("| Command | Description |");
882
+ lines.push("|---------|-------------|");
883
+ for (const cmdPath of commandPaths) {
884
+ const info = allCommands.get(cmdPath);
885
+ if (!info) continue;
886
+ if (leafOnly && !isLeafCommand(info)) continue;
887
+ const displayName = cmdPath || info.name;
888
+ const anchor = generateAnchor(displayName);
889
+ const desc = escapeTableCell(info.description ?? "");
890
+ lines.push(`| [${displayName}](${category.docPath}#${anchor}) | ${desc} |`);
891
+ }
892
+ return lines.join("\n");
893
+ }
894
+ /**
895
+ * Render command index from categories
896
+ *
897
+ * Generates a category-based index of commands with links to documentation.
898
+ *
899
+ * @example
900
+ * const categories: CommandCategory[] = [
901
+ * {
902
+ * title: "Application Commands",
903
+ * description: "Commands for managing applications.",
904
+ * commands: ["init", "generate", "apply"],
905
+ * docPath: "./cli/application.md",
906
+ * },
907
+ * ];
908
+ *
909
+ * const index = await renderCommandIndex(mainCommand, categories);
910
+ * // ### [Application Commands](./cli/application.md)
911
+ * //
912
+ * // Commands for managing applications.
913
+ * //
914
+ * // | Command | Description |
915
+ * // |---------|-------------|
916
+ * // | [init](./cli/application.md#init) | Initialize a project |
917
+ * // ...
918
+ *
919
+ * @param command - Root command to extract command information from
920
+ * @param categories - Category definitions for grouping commands
921
+ * @param options - Rendering options
922
+ * @returns Rendered markdown string
923
+ */
924
+ async function renderCommandIndex(command, categories, options) {
925
+ const headingLevel = options?.headingLevel ?? 3;
926
+ const leafOnly = options?.leafOnly ?? true;
927
+ const allCommands = await collectAllCommands(command);
928
+ const sections = [];
929
+ for (const category of categories) {
930
+ const section = renderCategory(category, allCommands, headingLevel, leafOnly);
931
+ sections.push(section);
932
+ }
933
+ return sections.join("\n\n");
934
+ }
935
+
709
936
  //#endregion
710
937
  //#region src/docs/types.ts
711
938
  /**
@@ -729,6 +956,40 @@ function commandStartMarker(commandPath) {
729
956
  function commandEndMarker(commandPath) {
730
957
  return `<!-- ${COMMAND_MARKER_PREFIX}:${commandPath}:end -->`;
731
958
  }
959
+ /**
960
+ * Marker prefix for global options sections in generated documentation
961
+ * Format: <!-- politty:global-options:start --> ... <!-- politty:global-options:end -->
962
+ */
963
+ const GLOBAL_OPTIONS_MARKER_PREFIX = "politty:global-options";
964
+ /**
965
+ * Generate start marker for a global options section
966
+ */
967
+ function globalOptionsStartMarker() {
968
+ return `<!-- ${GLOBAL_OPTIONS_MARKER_PREFIX}:start -->`;
969
+ }
970
+ /**
971
+ * Generate end marker for a global options section
972
+ */
973
+ function globalOptionsEndMarker() {
974
+ return `<!-- ${GLOBAL_OPTIONS_MARKER_PREFIX}:end -->`;
975
+ }
976
+ /**
977
+ * Marker prefix for index sections in generated documentation
978
+ * Format: <!-- politty:index:start --> ... <!-- politty:index:end -->
979
+ */
980
+ const INDEX_MARKER_PREFIX = "politty:index";
981
+ /**
982
+ * Generate start marker for an index section
983
+ */
984
+ function indexStartMarker() {
985
+ return `<!-- ${INDEX_MARKER_PREFIX}:start -->`;
986
+ }
987
+ /**
988
+ * Generate end marker for an index section
989
+ */
990
+ function indexEndMarker() {
991
+ return `<!-- ${INDEX_MARKER_PREFIX}:end -->`;
992
+ }
732
993
 
733
994
  //#endregion
734
995
  //#region src/docs/golden-test.ts
@@ -738,7 +999,9 @@ function commandEndMarker(commandPath) {
738
999
  */
739
1000
  async function applyFormatter(content, formatter) {
740
1001
  if (!formatter) return content;
741
- return await formatter(content);
1002
+ const formatted = await formatter(content);
1003
+ if (!content.endsWith("\n") && formatted.endsWith("\n")) return formatted.slice(0, -1);
1004
+ return formatted;
742
1005
  }
743
1006
  /**
744
1007
  * Check if update mode is enabled via environment variable
@@ -752,6 +1015,7 @@ function isUpdateMode() {
752
1015
  */
753
1016
  function normalizeFileConfig(config) {
754
1017
  if (Array.isArray(config)) return { commands: config };
1018
+ 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.");
755
1019
  return config;
756
1020
  }
757
1021
  /**
@@ -838,6 +1102,31 @@ function filterIgnoredCommands(commandPaths, ignores) {
838
1102
  });
839
1103
  }
840
1104
  /**
1105
+ * Resolve wildcards to direct matches without subcommand expansion.
1106
+ * Returns the "top-level" commands for use in CommandCategory.commands,
1107
+ * where expandCommands in render-index handles subcommand expansion.
1108
+ */
1109
+ function resolveTopLevelCommands(specifiedCommands, allCommands) {
1110
+ const result = [];
1111
+ for (const cmdPath of specifiedCommands) if (containsWildcard(cmdPath)) result.push(...expandWildcardPattern(cmdPath, allCommands));
1112
+ else if (allCommands.has(cmdPath)) result.push(cmdPath);
1113
+ return result;
1114
+ }
1115
+ /**
1116
+ * Resolve file command configuration to concrete command paths.
1117
+ * This applies wildcard/subcommand expansion and ignore filtering.
1118
+ */
1119
+ function resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores) {
1120
+ const fileConfig = normalizeFileConfig(fileConfigRaw);
1121
+ const specifiedCommands = fileConfig.commands;
1122
+ return {
1123
+ fileConfig,
1124
+ specifiedCommands,
1125
+ commandPaths: filterIgnoredCommands(expandCommandPaths(specifiedCommands, allCommands), ignores),
1126
+ topLevelCommands: filterIgnoredCommands(resolveTopLevelCommands(specifiedCommands, allCommands), ignores)
1127
+ };
1128
+ }
1129
+ /**
841
1130
  * Validate that there are no conflicts between files and ignores (with wildcard support)
842
1131
  */
843
1132
  function validateNoConflicts(filesCommands, ignores, allCommands) {
@@ -885,13 +1174,13 @@ function sortDepthFirst(commandPaths, specifiedOrder) {
885
1174
  for (const path of commandPaths) if (!visited.has(path)) result.push(path);
886
1175
  return result;
887
1176
  }
888
- /**
889
- * Generate file header from FileConfig
890
- */
891
1177
  function generateFileHeader(fileConfig) {
892
1178
  if (!fileConfig.title && !fileConfig.description) return null;
893
1179
  const parts = [];
894
- if (fileConfig.title) parts.push(`# ${fileConfig.title}`);
1180
+ if (fileConfig.title) {
1181
+ const heading = "#".repeat(fileConfig.headingLevel ?? 1);
1182
+ parts.push(`${heading} ${fileConfig.title}`);
1183
+ }
895
1184
  if (fileConfig.description) {
896
1185
  parts.push("");
897
1186
  parts.push(fileConfig.description);
@@ -900,6 +1189,51 @@ function generateFileHeader(fileConfig) {
900
1189
  return parts.join("\n");
901
1190
  }
902
1191
  /**
1192
+ * Extract a leading file header (title and optional description paragraph)
1193
+ */
1194
+ function extractFileHeader(content) {
1195
+ if (!/^#{1,6} /.test(content)) return null;
1196
+ const titleEnd = content.indexOf("\n");
1197
+ if (titleEnd === -1) return content;
1198
+ let cursor = titleEnd + 1;
1199
+ if (content[cursor] === "\n") cursor += 1;
1200
+ while (cursor < content.length) {
1201
+ const lineEnd = content.indexOf("\n", cursor);
1202
+ const line = lineEnd === -1 ? content.slice(cursor) : content.slice(cursor, lineEnd);
1203
+ if (line.length === 0 || /^#{1,6}\s/.test(line) || line.startsWith("<!-- politty:")) break;
1204
+ cursor = lineEnd === -1 ? content.length : lineEnd + 1;
1205
+ }
1206
+ return content.slice(0, cursor);
1207
+ }
1208
+ /**
1209
+ * Validate and optionally update configured file header
1210
+ */
1211
+ function processFileHeader(existingContent, fileConfig, updateMode) {
1212
+ const generatedHeader = generateFileHeader(fileConfig);
1213
+ if (!generatedHeader) return {
1214
+ content: existingContent,
1215
+ hasError: false,
1216
+ wasUpdated: false
1217
+ };
1218
+ if (existingContent.startsWith(generatedHeader)) return {
1219
+ content: existingContent,
1220
+ hasError: false,
1221
+ wasUpdated: false
1222
+ };
1223
+ const existingHeader = extractFileHeader(existingContent) ?? "";
1224
+ if (!updateMode) return {
1225
+ content: existingContent,
1226
+ diff: formatDiff(existingHeader, generatedHeader),
1227
+ hasError: true,
1228
+ wasUpdated: false
1229
+ };
1230
+ return {
1231
+ content: `${generatedHeader}${(existingHeader ? existingContent.slice(existingHeader.length) : existingContent).replace(/^\n+/, "")}`,
1232
+ hasError: false,
1233
+ wasUpdated: true
1234
+ };
1235
+ }
1236
+ /**
903
1237
  * Extract a command section from content using markers
904
1238
  * Returns the content between start and end markers (including markers)
905
1239
  */
@@ -913,6 +1247,18 @@ function extractCommandSection(content, commandPath) {
913
1247
  return content.slice(startIndex, endIndex + endMarker.length);
914
1248
  }
915
1249
  /**
1250
+ * Collect command paths from command start markers in content.
1251
+ */
1252
+ function collectCommandMarkerPaths(content) {
1253
+ const markerPattern = /<!--\s*politty:command:(.*?):start\s*-->/g;
1254
+ const paths = [];
1255
+ for (const match of content.matchAll(markerPattern)) paths.push(match[1] ?? "");
1256
+ return paths;
1257
+ }
1258
+ function formatCommandPath(commandPath) {
1259
+ return commandPath === "" ? "<root>" : commandPath;
1260
+ }
1261
+ /**
916
1262
  * Replace a command section in content using markers
917
1263
  * Returns the updated content with the new section
918
1264
  */
@@ -944,25 +1290,291 @@ function insertCommandSection(content, commandPath, newSection, specifiedOrder)
944
1290
  return content.slice(0, insertPos) + newSection + "\n" + content.slice(nextIndex);
945
1291
  }
946
1292
  }
947
- for (let i = targetIndex - 1; i >= 0; i--) {
948
- const prevCmd = specifiedOrder[i];
949
- if (prevCmd === void 0) continue;
950
- const prevEndMarker = commandEndMarker(prevCmd);
951
- const prevEndIndex = content.indexOf(prevEndMarker);
952
- if (prevEndIndex !== -1) {
953
- const insertPos = prevEndIndex + prevEndMarker.length;
954
- return content.slice(0, insertPos) + "\n" + newSection + content.slice(insertPos);
1293
+ for (let i = targetIndex - 1; i >= 0; i--) {
1294
+ const prevCmd = specifiedOrder[i];
1295
+ if (prevCmd === void 0) continue;
1296
+ const prevEndMarker = commandEndMarker(prevCmd);
1297
+ const prevEndIndex = content.indexOf(prevEndMarker);
1298
+ if (prevEndIndex !== -1) {
1299
+ const insertPos = prevEndIndex + prevEndMarker.length;
1300
+ return content.slice(0, insertPos) + "\n" + newSection + content.slice(insertPos);
1301
+ }
1302
+ }
1303
+ return content.trimEnd() + "\n" + newSection + "\n";
1304
+ }
1305
+ /**
1306
+ * Extract a marker section from content
1307
+ * Returns the content between start and end markers (including markers)
1308
+ */
1309
+ function extractMarkerSection(content, startMarker, endMarker) {
1310
+ const startIndex = content.indexOf(startMarker);
1311
+ if (startIndex === -1) return null;
1312
+ const endIndex = content.indexOf(endMarker, startIndex);
1313
+ if (endIndex === -1) return null;
1314
+ return content.slice(startIndex, endIndex + endMarker.length);
1315
+ }
1316
+ /**
1317
+ * Replace a marker section in content
1318
+ * Returns the updated content with the new section
1319
+ */
1320
+ function replaceMarkerSection(content, startMarker, endMarker, newSection) {
1321
+ const startIndex = content.indexOf(startMarker);
1322
+ if (startIndex === -1) return null;
1323
+ const endIndex = content.indexOf(endMarker, startIndex);
1324
+ if (endIndex === -1) return null;
1325
+ return content.slice(0, startIndex) + newSection + content.slice(endIndex + endMarker.length);
1326
+ }
1327
+ /**
1328
+ * Check if config is the { args, options? } shape (not shorthand ArgsShape)
1329
+ *
1330
+ * Distinguishes between:
1331
+ * - { args: ArgsShape, options?: ArgsTableOptions } → returns true
1332
+ * - ArgsShape (e.g., { verbose: ZodType, args: ZodType }) → returns false
1333
+ *
1334
+ * The key insight is that in the { args, options? } shape, config.args is an ArgsShape
1335
+ * (Record of ZodTypes), while in shorthand, config itself is the ArgsShape and config.args
1336
+ * would be a single ZodType if user has an option named "args".
1337
+ */
1338
+ function isGlobalOptionsConfigWithOptions(config) {
1339
+ if (typeof config !== "object" || config === null || !("args" in config)) return false;
1340
+ return !(config.args instanceof z.ZodType);
1341
+ }
1342
+ /**
1343
+ * Collect option fields that are actually rendered by global options markers.
1344
+ * Positional args are not rendered in args tables, so they must not be excluded.
1345
+ */
1346
+ function collectRenderableGlobalOptionFields(argsShape) {
1347
+ return extractFields(z.object(argsShape)).fields.filter((field) => !field.positional);
1348
+ }
1349
+ /**
1350
+ * Compare option definitions for global-options compatibility.
1351
+ */
1352
+ function areGlobalOptionsEquivalent(a, b) {
1353
+ const { schema: _aSchema, ...aRest } = a;
1354
+ const { schema: _bSchema, ...bRest } = b;
1355
+ return isDeepStrictEqual(aRest, bRest);
1356
+ }
1357
+ /**
1358
+ * Normalize rootDoc.globalOptions to { args, options? } form.
1359
+ */
1360
+ function normalizeGlobalOptions(config) {
1361
+ if (!config) return void 0;
1362
+ return isGlobalOptionsConfigWithOptions(config) ? config : { args: config };
1363
+ }
1364
+ /**
1365
+ * Collect global option definitions from rootDoc.
1366
+ * Global options are intentionally applied to all generated command sections.
1367
+ */
1368
+ function collectGlobalOptionDefinitions(rootDoc) {
1369
+ const globalOptions = /* @__PURE__ */ new Map();
1370
+ if (!rootDoc?.globalOptions) return globalOptions;
1371
+ const normalized = normalizeGlobalOptions(rootDoc.globalOptions);
1372
+ if (!normalized) return globalOptions;
1373
+ for (const field of collectRenderableGlobalOptionFields(normalized.args)) globalOptions.set(field.name, field);
1374
+ return globalOptions;
1375
+ }
1376
+ /**
1377
+ * Derive CommandCategory[] from files mapping.
1378
+ * Category title/description come from the first command in each file entry.
1379
+ */
1380
+ function deriveIndexFromFiles(files, rootDocPath, allCommands, ignores) {
1381
+ const categories = [];
1382
+ for (const [filePath, fileConfigRaw] of Object.entries(files)) {
1383
+ const { commandPaths, topLevelCommands } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1384
+ if (commandPaths.length === 0) continue;
1385
+ const docPath = "./" + path.relative(path.dirname(rootDocPath), filePath).replace(/\\/g, "/");
1386
+ const firstCmdPath = commandPaths[0];
1387
+ const cmdInfo = firstCmdPath !== void 0 ? allCommands.get(firstCmdPath) : void 0;
1388
+ categories.push({
1389
+ title: cmdInfo?.name ?? path.basename(filePath, path.extname(filePath)),
1390
+ description: cmdInfo?.description ?? "",
1391
+ commands: topLevelCommands,
1392
+ docPath
1393
+ });
1394
+ }
1395
+ return categories;
1396
+ }
1397
+ /**
1398
+ * Collect command paths that are actually documented in configured files.
1399
+ */
1400
+ function collectDocumentedCommandPaths(files, allCommands, ignores) {
1401
+ const documentedCommandPaths = /* @__PURE__ */ new Set();
1402
+ for (const fileConfigRaw of Object.values(files)) {
1403
+ const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1404
+ for (const commandPath of commandPaths) documentedCommandPaths.add(commandPath);
1405
+ }
1406
+ return documentedCommandPaths;
1407
+ }
1408
+ /**
1409
+ * Collect command paths that are targeted in configured files.
1410
+ */
1411
+ function collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) {
1412
+ const documentedTargetCommandPaths = /* @__PURE__ */ new Set();
1413
+ for (const filePath of Object.keys(files)) {
1414
+ const targetCommandsInFile = findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores);
1415
+ for (const commandPath of targetCommandsInFile) documentedTargetCommandPaths.add(commandPath);
1416
+ }
1417
+ return documentedTargetCommandPaths;
1418
+ }
1419
+ /**
1420
+ * Validate that excluded command options match globalOptions definitions.
1421
+ */
1422
+ function validateGlobalOptionCompatibility(documentedCommandPaths, allCommands, globalOptions) {
1423
+ if (globalOptions.size === 0) return;
1424
+ const conflicts = [];
1425
+ for (const commandPath of documentedCommandPaths) {
1426
+ const info = allCommands.get(commandPath);
1427
+ if (!info) continue;
1428
+ for (const option of info.options) {
1429
+ const globalOption = globalOptions.get(option.name);
1430
+ if (!globalOption) continue;
1431
+ if (!areGlobalOptionsEquivalent(globalOption, option)) conflicts.push(`Command "${formatCommandPath(commandPath)}" option "--${option.cliName}" does not match globalOptions definition for "${option.name}".`);
1432
+ }
1433
+ }
1434
+ if (conflicts.length > 0) throw new Error(`Invalid globalOptions configuration:\n - ${conflicts.join("\n - ")}`);
1435
+ }
1436
+ /**
1437
+ * Generate global options section content with markers
1438
+ */
1439
+ function generateGlobalOptionsSection(config) {
1440
+ const startMarker = globalOptionsStartMarker();
1441
+ const endMarker = globalOptionsEndMarker();
1442
+ return [
1443
+ startMarker,
1444
+ renderArgsTable(config.args, config.options),
1445
+ endMarker
1446
+ ].join("\n");
1447
+ }
1448
+ /**
1449
+ * Generate index section content with markers
1450
+ */
1451
+ async function generateIndexSection(categories, command, options) {
1452
+ const startMarker = indexStartMarker();
1453
+ const endMarker = indexEndMarker();
1454
+ return [
1455
+ startMarker,
1456
+ await renderCommandIndex(command, categories, options),
1457
+ endMarker
1458
+ ].join("\n");
1459
+ }
1460
+ /**
1461
+ * Normalize a doc file path for equivalence checks.
1462
+ */
1463
+ function normalizeDocPathForComparison(filePath) {
1464
+ return path.resolve(filePath);
1465
+ }
1466
+ /**
1467
+ * Process global options marker in file content
1468
+ * Returns result with updated content and any diffs
1469
+ */
1470
+ async function processGlobalOptionsMarker(existingContent, globalOptionsConfig, updateMode, formatter) {
1471
+ let content = existingContent;
1472
+ const diffs = [];
1473
+ let hasError = false;
1474
+ let wasUpdated = false;
1475
+ const startMarker = globalOptionsStartMarker();
1476
+ const endMarker = globalOptionsEndMarker();
1477
+ const generatedSection = await applyFormatter(generateGlobalOptionsSection(globalOptionsConfig), formatter);
1478
+ const existingSection = extractMarkerSection(content, startMarker, endMarker);
1479
+ if (!existingSection) {
1480
+ hasError = true;
1481
+ diffs.push(`Global options marker not found in file. Expected markers:\n${startMarker}\n...\n${endMarker}`);
1482
+ return {
1483
+ content,
1484
+ diffs,
1485
+ hasError,
1486
+ wasUpdated
1487
+ };
1488
+ }
1489
+ if (existingSection !== generatedSection) if (updateMode) {
1490
+ const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
1491
+ if (updated) {
1492
+ content = updated;
1493
+ wasUpdated = true;
1494
+ } else {
1495
+ hasError = true;
1496
+ diffs.push("Failed to replace global options section");
1497
+ }
1498
+ } else {
1499
+ hasError = true;
1500
+ diffs.push(formatDiff(existingSection, generatedSection));
1501
+ }
1502
+ return {
1503
+ content,
1504
+ diffs,
1505
+ hasError,
1506
+ wasUpdated
1507
+ };
1508
+ }
1509
+ /**
1510
+ * Process index marker in file content
1511
+ * Returns result with updated content and any diffs.
1512
+ * If the marker is not present in the file, the section is silently skipped.
1513
+ */
1514
+ async function processIndexMarker(existingContent, categories, command, updateMode, formatter, indexOptions) {
1515
+ let content = existingContent;
1516
+ const diffs = [];
1517
+ let hasError = false;
1518
+ let wasUpdated = false;
1519
+ const startMarker = indexStartMarker();
1520
+ const endMarker = indexEndMarker();
1521
+ const hasStartMarker = content.includes(startMarker);
1522
+ const hasEndMarker = content.includes(endMarker);
1523
+ if (!hasStartMarker && !hasEndMarker) return {
1524
+ content,
1525
+ diffs,
1526
+ hasError,
1527
+ wasUpdated
1528
+ };
1529
+ if (!hasStartMarker || !hasEndMarker) {
1530
+ hasError = true;
1531
+ diffs.push("Index marker section is malformed: both start and end markers are required.");
1532
+ return {
1533
+ content,
1534
+ diffs,
1535
+ hasError,
1536
+ wasUpdated
1537
+ };
1538
+ }
1539
+ const existingSection = extractMarkerSection(content, startMarker, endMarker);
1540
+ if (!existingSection) {
1541
+ hasError = true;
1542
+ diffs.push("Index marker section is malformed: start marker must appear before end marker.");
1543
+ return {
1544
+ content,
1545
+ diffs,
1546
+ hasError,
1547
+ wasUpdated
1548
+ };
1549
+ }
1550
+ const generatedSection = await applyFormatter(await generateIndexSection(categories, command, indexOptions), formatter);
1551
+ if (existingSection !== generatedSection) if (updateMode) {
1552
+ const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
1553
+ if (updated) {
1554
+ content = updated;
1555
+ wasUpdated = true;
1556
+ } else {
1557
+ hasError = true;
1558
+ diffs.push("Failed to replace index section");
955
1559
  }
1560
+ } else {
1561
+ hasError = true;
1562
+ diffs.push(formatDiff(existingSection, generatedSection));
956
1563
  }
957
- return content.trimEnd() + "\n" + newSection + "\n";
1564
+ return {
1565
+ content,
1566
+ diffs,
1567
+ hasError,
1568
+ wasUpdated
1569
+ };
958
1570
  }
959
1571
  /**
960
1572
  * Find which file contains a specific command
961
1573
  */
962
1574
  function findFileForCommand(commandPath, files, allCommands, ignores) {
963
1575
  for (const [filePath, fileConfigRaw] of Object.entries(files)) {
964
- const specifiedCommands = normalizeFileConfig(fileConfigRaw).commands;
965
- if (filterIgnoredCommands(expandCommandPaths(specifiedCommands, allCommands), ignores).includes(commandPath)) return filePath;
1576
+ const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1577
+ if (commandPaths.includes(commandPath)) return filePath;
966
1578
  }
967
1579
  return null;
968
1580
  }
@@ -973,8 +1585,7 @@ function findFileForCommand(commandPath, files, allCommands, ignores) {
973
1585
  function findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) {
974
1586
  const fileConfigRaw = files[filePath];
975
1587
  if (!fileConfigRaw) return [];
976
- const specifiedCommands = normalizeFileConfig(fileConfigRaw).commands;
977
- const commandPaths = filterIgnoredCommands(expandCommandPaths(specifiedCommands, allCommands), ignores);
1588
+ const { specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
978
1589
  const expandedTargets = /* @__PURE__ */ new Set();
979
1590
  for (const targetCmd of targetCommands) {
980
1591
  if (!commandPaths.includes(targetCmd)) continue;
@@ -1013,7 +1624,7 @@ function generateFileMarkdown(commandPaths, allCommands, render, filePath, fileM
1013
1624
  const section = generateCommandSection(cmdPath, allCommands, render, filePath, fileMap);
1014
1625
  if (section) sections.push(section);
1015
1626
  }
1016
- return sections.join("\n");
1627
+ return `${sections.join("\n")}\n`;
1017
1628
  }
1018
1629
  /**
1019
1630
  * Build a map of command path to file path
@@ -1021,8 +1632,7 @@ function generateFileMarkdown(commandPaths, allCommands, render, filePath, fileM
1021
1632
  function buildFileMap(files, allCommands, ignores) {
1022
1633
  const fileMap = {};
1023
1634
  for (const [filePath, fileConfigRaw] of Object.entries(files)) {
1024
- const specifiedCommands = normalizeFileConfig(fileConfigRaw).commands;
1025
- const commandPaths = filterIgnoredCommands(expandCommandPaths(specifiedCommands, allCommands), ignores);
1635
+ const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1026
1636
  for (const cmdPath of commandPaths) fileMap[cmdPath] = filePath;
1027
1637
  }
1028
1638
  return fileMap;
@@ -1043,10 +1653,21 @@ async function executeConfiguredExamples(allCommands, examplesConfig, rootComman
1043
1653
  * Generate documentation from command definition
1044
1654
  */
1045
1655
  async function generateDoc(config) {
1046
- const { command, files, ignores = [], format = {}, formatter, examples: examplesConfig, targetCommands } = config;
1656
+ const { command, rootDoc, files, ignores = [], format = {}, formatter, examples: examplesConfig, targetCommands } = config;
1047
1657
  const updateMode = isUpdateMode();
1658
+ if (rootDoc) {
1659
+ const normalizedRootDocPath = normalizeDocPathForComparison(rootDoc.path);
1660
+ 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.`);
1661
+ }
1048
1662
  const allCommands = await collectAllCommands(command);
1049
1663
  if (examplesConfig) await executeConfiguredExamples(allCommands, examplesConfig, command);
1664
+ const hasTargetCommands = targetCommands !== void 0 && targetCommands.length > 0;
1665
+ if (hasTargetCommands) {
1666
+ for (const targetCommand of targetCommands) if (!findFileForCommand(targetCommand, files, allCommands, ignores)) throw new Error(`Target command "${targetCommand}" not found in any file configuration`);
1667
+ }
1668
+ const globalOptionDefinitions = collectGlobalOptionDefinitions(rootDoc);
1669
+ validateGlobalOptionCompatibility(hasTargetCommands ? collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) : collectDocumentedCommandPaths(files, allCommands, ignores), allCommands, globalOptionDefinitions);
1670
+ if (globalOptionDefinitions.size > 0) for (const info of allCommands.values()) info.options = info.options.filter((opt) => !globalOptionDefinitions.has(opt.name));
1050
1671
  const allFilesCommands = [];
1051
1672
  for (const fileConfigRaw of Object.values(files)) {
1052
1673
  const fileConfig = normalizeFileConfig(fileConfigRaw);
@@ -1057,15 +1678,14 @@ async function generateDoc(config) {
1057
1678
  const fileMap = buildFileMap(files, allCommands, ignores);
1058
1679
  const results = [];
1059
1680
  let hasError = false;
1060
- if (targetCommands && targetCommands.length > 0) {
1061
- for (const targetCommand of targetCommands) if (!findFileForCommand(targetCommand, files, allCommands, ignores)) throw new Error(`Target command "${targetCommand}" not found in any file configuration`);
1062
- }
1063
1681
  for (const [filePath, fileConfigRaw] of Object.entries(files)) {
1064
- const fileConfig = normalizeFileConfig(fileConfigRaw);
1065
- const specifiedCommands = fileConfig.commands;
1682
+ const { fileConfig, specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1066
1683
  if (specifiedCommands.length === 0) continue;
1067
- const commandPaths = filterIgnoredCommands(expandCommandPaths(specifiedCommands, allCommands), ignores);
1068
1684
  if (commandPaths.length === 0) continue;
1685
+ const fileTargetCommands = hasTargetCommands ? findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) : [];
1686
+ if (hasTargetCommands && fileTargetCommands.length === 0) continue;
1687
+ let fileStatus = "match";
1688
+ const diffs = [];
1069
1689
  const minDepth = Math.min(...commandPaths.map((p) => allCommands.get(p)?.depth ?? 1));
1070
1690
  const adjustedHeadingLevel = Math.max(1, (format?.headingLevel ?? 1) - (minDepth - 1));
1071
1691
  const fileRenderer = createCommandRenderer({
@@ -1073,12 +1693,8 @@ async function generateDoc(config) {
1073
1693
  headingLevel: adjustedHeadingLevel
1074
1694
  });
1075
1695
  const render = fileConfig.render ?? fileRenderer;
1076
- if (targetCommands !== void 0 && targetCommands.length > 0) {
1077
- const fileTargetCommands = findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores);
1078
- if (fileTargetCommands.length === 0) continue;
1696
+ if (hasTargetCommands) {
1079
1697
  let existingContent = readFile(filePath);
1080
- let fileStatus = "match";
1081
- const diffs = [];
1082
1698
  for (const targetCommand of fileTargetCommands) {
1083
1699
  const rawSection = generateCommandSection(targetCommand, allCommands, render, filePath, fileMap);
1084
1700
  if (!rawSection) throw new Error(`Target command "${targetCommand}" not found in commands`);
@@ -1124,33 +1740,75 @@ async function generateDoc(config) {
1124
1740
  diffs.push(formatDiff(existingSection, generatedSectionOnly));
1125
1741
  }
1126
1742
  }
1127
- results.push({
1128
- path: filePath,
1129
- status: fileStatus,
1130
- diff: diffs.length > 0 ? diffs.join("\n\n") : void 0
1131
- });
1132
1743
  } else {
1133
1744
  const generatedMarkdown = await applyFormatter(generateFileMarkdown(commandPaths, allCommands, render, filePath, fileMap, specifiedCommands, fileConfig), formatter);
1134
1745
  const comparison = compareWithExisting(generatedMarkdown, filePath);
1135
- if (comparison.match) results.push({
1136
- path: filePath,
1137
- status: "match"
1138
- });
1139
- else if (updateMode) {
1746
+ if (comparison.match) {} else if (updateMode) {
1140
1747
  writeFile(filePath, generatedMarkdown);
1141
- results.push({
1142
- path: filePath,
1143
- status: comparison.fileExists ? "updated" : "created"
1144
- });
1748
+ fileStatus = comparison.fileExists ? "updated" : "created";
1145
1749
  } else {
1146
1750
  hasError = true;
1147
- results.push({
1148
- path: filePath,
1149
- status: "diff",
1150
- diff: comparison.diff
1151
- });
1751
+ fileStatus = "diff";
1752
+ if (comparison.diff) diffs.push(comparison.diff);
1753
+ }
1754
+ }
1755
+ if (diffs.length > 0) fileStatus = "diff";
1756
+ results.push({
1757
+ path: filePath,
1758
+ status: fileStatus,
1759
+ diff: diffs.length > 0 ? diffs.join("\n\n") : void 0
1760
+ });
1761
+ }
1762
+ if (rootDoc) {
1763
+ const rootDocFilePath = rootDoc.path;
1764
+ let rootDocStatus = "match";
1765
+ const rootDocDiffs = [];
1766
+ const existingContent = readFile(rootDocFilePath);
1767
+ if (existingContent === null) {
1768
+ hasError = true;
1769
+ rootDocStatus = "diff";
1770
+ rootDocDiffs.push("File does not exist. Cannot validate rootDoc markers.");
1771
+ } else {
1772
+ let content = existingContent;
1773
+ let markerUpdated = false;
1774
+ const rootDocFileConfig = { title: command.name };
1775
+ if (rootDoc.headingLevel !== void 0) rootDocFileConfig.headingLevel = rootDoc.headingLevel;
1776
+ if (command.description !== void 0) rootDocFileConfig.description = command.description;
1777
+ const headerResult = processFileHeader(content, rootDocFileConfig, updateMode);
1778
+ content = headerResult.content;
1779
+ if (headerResult.diff) rootDocDiffs.push(headerResult.diff);
1780
+ if (headerResult.hasError) hasError = true;
1781
+ if (headerResult.wasUpdated) markerUpdated = true;
1782
+ const unexpectedCommandMarkers = Array.from(new Set(collectCommandMarkerPaths(content)));
1783
+ if (unexpectedCommandMarkers.length > 0) {
1784
+ hasError = true;
1785
+ rootDocDiffs.push(`Found unexpected command marker sections in rootDoc: ${unexpectedCommandMarkers.map((commandPath) => `"${formatCommandPath(commandPath)}"`).join(", ")}.`);
1786
+ }
1787
+ const normalizedGlobalOptions = normalizeGlobalOptions(rootDoc.globalOptions);
1788
+ if (normalizedGlobalOptions) {
1789
+ const globalOptionsResult = await processGlobalOptionsMarker(content, normalizedGlobalOptions, updateMode, formatter);
1790
+ content = globalOptionsResult.content;
1791
+ rootDocDiffs.push(...globalOptionsResult.diffs);
1792
+ if (globalOptionsResult.hasError) hasError = true;
1793
+ if (globalOptionsResult.wasUpdated) markerUpdated = true;
1794
+ }
1795
+ const derivedCategories = deriveIndexFromFiles(files, rootDocFilePath, allCommands, ignores);
1796
+ const indexResult = await processIndexMarker(content, derivedCategories, command, updateMode, formatter, rootDoc.index);
1797
+ content = indexResult.content;
1798
+ rootDocDiffs.push(...indexResult.diffs);
1799
+ if (indexResult.hasError) hasError = true;
1800
+ if (indexResult.wasUpdated) markerUpdated = true;
1801
+ if (updateMode && markerUpdated) {
1802
+ writeFile(rootDocFilePath, content);
1803
+ if (rootDocStatus === "match") rootDocStatus = "updated";
1152
1804
  }
1153
1805
  }
1806
+ if (rootDocDiffs.length > 0) rootDocStatus = "diff";
1807
+ results.push({
1808
+ path: rootDocFilePath,
1809
+ status: rootDocStatus,
1810
+ diff: rootDocDiffs.length > 0 ? rootDocDiffs.join("\n\n") : void 0
1811
+ });
1154
1812
  }
1155
1813
  return {
1156
1814
  success: !hasError,
@@ -1187,231 +1845,5 @@ function initDocFile(config, fileSystem) {
1187
1845
  }
1188
1846
 
1189
1847
  //#endregion
1190
- //#region src/docs/render-args.ts
1191
- /**
1192
- * Extract ResolvedFieldMeta array from ArgsShape
1193
- *
1194
- * This converts a raw args shape (like `commonArgs`) into the
1195
- * ResolvedFieldMeta format used by politty's rendering functions.
1196
- */
1197
- function extractArgsFields(args) {
1198
- return extractFields(z.object(args)).fields;
1199
- }
1200
- /**
1201
- * Render args definition as a markdown options table
1202
- *
1203
- * This function takes raw args definitions (like `commonArgs`) and
1204
- * renders them as a markdown table suitable for documentation.
1205
- *
1206
- * @example
1207
- * import { renderArgsTable } from "politty/docs";
1208
- * import { commonArgs, workspaceArgs } from "./args";
1209
- *
1210
- * const table = renderArgsTable({
1211
- * ...commonArgs,
1212
- * ...workspaceArgs,
1213
- * });
1214
- * // | Option | Alias | Description | Default |
1215
- * // |--------|-------|-------------|---------|
1216
- * // | `--env-file <ENV_FILE>` | `-e` | Path to environment file | - |
1217
- * // ...
1218
- *
1219
- * @param args - Args shape (Record of string keys to Zod schemas with arg() metadata)
1220
- * @param options - Rendering options
1221
- * @returns Rendered markdown table string
1222
- */
1223
- function renderArgsTable(args, options) {
1224
- const optionFields = extractArgsFields(args).filter((f) => !f.positional);
1225
- if (optionFields.length === 0) return "";
1226
- if (options?.columns) return renderFilteredTable(optionFields, options.columns);
1227
- return renderOptionsTableFromArray(optionFields);
1228
- }
1229
- /**
1230
- * Escape markdown special characters in table cells
1231
- */
1232
- function escapeTableCell$1(str) {
1233
- return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
1234
- }
1235
- /**
1236
- * Format default value for display
1237
- */
1238
- function formatDefaultValue(value) {
1239
- if (value === void 0) return "-";
1240
- return `\`${JSON.stringify(value)}\``;
1241
- }
1242
- /**
1243
- * Render table with filtered columns
1244
- */
1245
- function renderFilteredTable(options, columns) {
1246
- const lines = [];
1247
- const headerCells = [];
1248
- const separatorCells = [];
1249
- for (const col of columns) switch (col) {
1250
- case "option":
1251
- headerCells.push("Option");
1252
- separatorCells.push("------");
1253
- break;
1254
- case "alias":
1255
- headerCells.push("Alias");
1256
- separatorCells.push("-----");
1257
- break;
1258
- case "description":
1259
- headerCells.push("Description");
1260
- separatorCells.push("-----------");
1261
- break;
1262
- case "required":
1263
- headerCells.push("Required");
1264
- separatorCells.push("--------");
1265
- break;
1266
- case "default":
1267
- headerCells.push("Default");
1268
- separatorCells.push("-------");
1269
- break;
1270
- case "env":
1271
- headerCells.push("Env");
1272
- separatorCells.push("---");
1273
- break;
1274
- }
1275
- lines.push(`| ${headerCells.join(" | ")} |`);
1276
- lines.push(`| ${separatorCells.join(" | ")} |`);
1277
- for (const opt of options) {
1278
- const cells = [];
1279
- for (const col of columns) switch (col) {
1280
- case "option": {
1281
- const placeholder = opt.placeholder ?? opt.cliName.toUpperCase().replace(/-/g, "_");
1282
- const optionName = opt.type === "boolean" ? `\`--${opt.cliName}\`` : `\`--${opt.cliName} <${placeholder}>\``;
1283
- cells.push(optionName);
1284
- break;
1285
- }
1286
- case "alias":
1287
- cells.push(opt.alias ? `\`-${opt.alias}\`` : "-");
1288
- break;
1289
- case "description":
1290
- cells.push(escapeTableCell$1(opt.description ?? ""));
1291
- break;
1292
- case "required":
1293
- cells.push(opt.required ? "Yes" : "No");
1294
- break;
1295
- case "default":
1296
- cells.push(formatDefaultValue(opt.defaultValue));
1297
- break;
1298
- case "env": {
1299
- const envNames = opt.env ? Array.isArray(opt.env) ? opt.env.map((e) => `\`${e}\``).join(", ") : `\`${opt.env}\`` : "-";
1300
- cells.push(envNames);
1301
- break;
1302
- }
1303
- }
1304
- lines.push(`| ${cells.join(" | ")} |`);
1305
- }
1306
- return lines.join("\n");
1307
- }
1308
-
1309
- //#endregion
1310
- //#region src/docs/render-index.ts
1311
- /**
1312
- * Escape markdown special characters in table cells
1313
- */
1314
- function escapeTableCell(str) {
1315
- return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
1316
- }
1317
- /**
1318
- * Generate anchor from command path
1319
- */
1320
- function generateAnchor(commandPath) {
1321
- return commandPath.replace(/\s+/g, "-").toLowerCase();
1322
- }
1323
- /**
1324
- * Check if a command is a leaf (has no subcommands)
1325
- */
1326
- function isLeafCommand(info) {
1327
- return info.subCommands.length === 0;
1328
- }
1329
- /**
1330
- * Expand commands to include their subcommands
1331
- * If a command has subcommands, recursively find all commands under it
1332
- *
1333
- * @param commandPaths - Command paths to expand
1334
- * @param allCommands - Map of all available commands
1335
- * @param leafOnly - If true, only include leaf commands; if false, include all commands
1336
- */
1337
- function expandCommands(commandPaths, allCommands, leafOnly) {
1338
- const result = [];
1339
- for (const cmdPath of commandPaths) {
1340
- const info = allCommands.get(cmdPath);
1341
- if (!info) continue;
1342
- if (isLeafCommand(info)) result.push(cmdPath);
1343
- else for (const [path, pathInfo] of allCommands) if (cmdPath === "" ? path.length > 0 : path.startsWith(cmdPath + " ") || path === cmdPath) {
1344
- if (isLeafCommand(pathInfo) || !leafOnly) result.push(path);
1345
- }
1346
- }
1347
- return result;
1348
- }
1349
- /**
1350
- * Render a single category section
1351
- */
1352
- function renderCategory(category, allCommands, headingLevel, leafOnly) {
1353
- const h = "#".repeat(headingLevel);
1354
- const lines = [];
1355
- lines.push(`${h} [${category.title}](${category.docPath})`);
1356
- lines.push("");
1357
- lines.push(category.description);
1358
- lines.push("");
1359
- const commandPaths = expandCommands(category.commands, allCommands, leafOnly);
1360
- lines.push("| Command | Description |");
1361
- lines.push("|---------|-------------|");
1362
- for (const cmdPath of commandPaths) {
1363
- const info = allCommands.get(cmdPath);
1364
- if (!info) continue;
1365
- if (leafOnly && !isLeafCommand(info)) continue;
1366
- const displayName = cmdPath || info.name;
1367
- const anchor = generateAnchor(displayName);
1368
- const desc = escapeTableCell(info.description ?? "");
1369
- lines.push(`| [${displayName}](${category.docPath}#${anchor}) | ${desc} |`);
1370
- }
1371
- return lines.join("\n");
1372
- }
1373
- /**
1374
- * Render command index from categories
1375
- *
1376
- * Generates a category-based index of commands with links to documentation.
1377
- *
1378
- * @example
1379
- * const categories: CommandCategory[] = [
1380
- * {
1381
- * title: "Application Commands",
1382
- * description: "Commands for managing applications.",
1383
- * commands: ["init", "generate", "apply"],
1384
- * docPath: "./cli/application.md",
1385
- * },
1386
- * ];
1387
- *
1388
- * const index = await renderCommandIndex(mainCommand, categories);
1389
- * // ### [Application Commands](./cli/application.md)
1390
- * //
1391
- * // Commands for managing applications.
1392
- * //
1393
- * // | Command | Description |
1394
- * // |---------|-------------|
1395
- * // | [init](./cli/application.md#init) | Initialize a project |
1396
- * // ...
1397
- *
1398
- * @param command - Root command to extract command information from
1399
- * @param categories - Category definitions for grouping commands
1400
- * @param options - Rendering options
1401
- * @returns Rendered markdown string
1402
- */
1403
- async function renderCommandIndex(command, categories, options) {
1404
- const headingLevel = options?.headingLevel ?? 3;
1405
- const leafOnly = options?.leafOnly ?? true;
1406
- const allCommands = await collectAllCommands(command);
1407
- const sections = [];
1408
- for (const category of categories) {
1409
- const section = renderCategory(category, allCommands, headingLevel, leafOnly);
1410
- sections.push(section);
1411
- }
1412
- return sections.join("\n\n");
1413
- }
1414
-
1415
- //#endregion
1416
- export { COMMAND_MARKER_PREFIX, UPDATE_GOLDEN_ENV, assertDocMatch, buildCommandInfo, collectAllCommands, commandEndMarker, commandStartMarker, compareWithExisting, createCommandRenderer, defaultRenderers, executeExamples, formatDiff, generateDoc, initDocFile, renderArgsTable, renderArgumentsList, renderArgumentsListFromArray, renderArgumentsTable, renderArgumentsTableFromArray, renderCommandIndex, renderExamplesDefault, renderOptionsList, renderOptionsListFromArray, renderOptionsTable, renderOptionsTableFromArray, renderSubcommandsTable, renderSubcommandsTableFromArray, renderUsage, resolveLazyCommand, writeFile };
1848
+ export { COMMAND_MARKER_PREFIX, GLOBAL_OPTIONS_MARKER_PREFIX, INDEX_MARKER_PREFIX, UPDATE_GOLDEN_ENV, assertDocMatch, buildCommandInfo, collectAllCommands, commandEndMarker, commandStartMarker, compareWithExisting, createCommandRenderer, defaultRenderers, executeExamples, formatDiff, generateDoc, globalOptionsEndMarker, globalOptionsStartMarker, indexEndMarker, indexStartMarker, initDocFile, renderArgsTable, renderArgumentsList, renderArgumentsListFromArray, renderArgumentsTable, renderArgumentsTableFromArray, renderCommandIndex, renderExamplesDefault, renderOptionsList, renderOptionsListFromArray, renderOptionsTable, renderOptionsTableFromArray, renderSubcommandsTable, renderSubcommandsTableFromArray, renderUsage, resolveLazyCommand, writeFile };
1417
1849
  //# sourceMappingURL=index.js.map