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.
- package/dist/completion/index.cjs +35 -25
- package/dist/completion/index.cjs.map +1 -1
- package/dist/completion/index.d.cts +17 -13
- package/dist/completion/index.d.cts.map +1 -1
- package/dist/completion/index.d.ts +17 -13
- package/dist/completion/index.d.ts.map +1 -1
- package/dist/completion/index.js +35 -25
- package/dist/completion/index.js.map +1 -1
- package/dist/docs/index.cjs +716 -278
- package/dist/docs/index.cjs.map +1 -1
- package/dist/docs/index.d.cts +100 -46
- package/dist/docs/index.d.cts.map +1 -1
- package/dist/docs/index.d.ts +100 -46
- package/dist/docs/index.d.ts.map +1 -1
- package/dist/docs/index.js +711 -279
- package/dist/docs/index.js.map +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/runner-B38UBqhv.cjs.map +1 -1
- package/dist/runner-CUN50BqK.js.map +1 -1
- package/package.json +5 -5
package/dist/docs/index.cjs
CHANGED
|
@@ -42,6 +42,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
42
42
|
const require_schema_extractor = require('../schema-extractor-B9D3Rf22.cjs');
|
|
43
43
|
const require_subcommand_router = require('../subcommand-router-Vf-0w9P4.cjs');
|
|
44
44
|
let zod = require("zod");
|
|
45
|
+
let node_util = require("node:util");
|
|
45
46
|
let node_fs = require("node:fs");
|
|
46
47
|
node_fs = __toESM(node_fs);
|
|
47
48
|
let node_path = require("node:path");
|
|
@@ -749,6 +750,232 @@ function parseExampleCmd(cmd) {
|
|
|
749
750
|
return args;
|
|
750
751
|
}
|
|
751
752
|
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/docs/render-args.ts
|
|
755
|
+
/**
|
|
756
|
+
* Extract ResolvedFieldMeta array from ArgsShape
|
|
757
|
+
*
|
|
758
|
+
* This converts a raw args shape (like `commonArgs`) into the
|
|
759
|
+
* ResolvedFieldMeta format used by politty's rendering functions.
|
|
760
|
+
*/
|
|
761
|
+
function extractArgsFields(args) {
|
|
762
|
+
return require_schema_extractor.extractFields(zod.z.object(args)).fields;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Render args definition as a markdown options table
|
|
766
|
+
*
|
|
767
|
+
* This function takes raw args definitions (like `commonArgs`) and
|
|
768
|
+
* renders them as a markdown table suitable for documentation.
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* import { renderArgsTable } from "politty/docs";
|
|
772
|
+
* import { commonArgs, workspaceArgs } from "./args";
|
|
773
|
+
*
|
|
774
|
+
* const table = renderArgsTable({
|
|
775
|
+
* ...commonArgs,
|
|
776
|
+
* ...workspaceArgs,
|
|
777
|
+
* });
|
|
778
|
+
* // | Option | Alias | Description | Default |
|
|
779
|
+
* // |--------|-------|-------------|---------|
|
|
780
|
+
* // | `--env-file <ENV_FILE>` | `-e` | Path to environment file | - |
|
|
781
|
+
* // ...
|
|
782
|
+
*
|
|
783
|
+
* @param args - Args shape (Record of string keys to Zod schemas with arg() metadata)
|
|
784
|
+
* @param options - Rendering options
|
|
785
|
+
* @returns Rendered markdown table string
|
|
786
|
+
*/
|
|
787
|
+
function renderArgsTable(args, options) {
|
|
788
|
+
const optionFields = extractArgsFields(args).filter((f) => !f.positional);
|
|
789
|
+
if (optionFields.length === 0) return "";
|
|
790
|
+
if (options?.columns) return renderFilteredTable(optionFields, options.columns);
|
|
791
|
+
return renderOptionsTableFromArray(optionFields);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Escape markdown special characters in table cells
|
|
795
|
+
*/
|
|
796
|
+
function escapeTableCell$1(str) {
|
|
797
|
+
return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Format default value for display
|
|
801
|
+
*/
|
|
802
|
+
function formatDefaultValue(value) {
|
|
803
|
+
if (value === void 0) return "-";
|
|
804
|
+
return `\`${JSON.stringify(value)}\``;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Render table with filtered columns
|
|
808
|
+
*/
|
|
809
|
+
function renderFilteredTable(options, columns) {
|
|
810
|
+
const lines = [];
|
|
811
|
+
const headerCells = [];
|
|
812
|
+
const separatorCells = [];
|
|
813
|
+
for (const col of columns) switch (col) {
|
|
814
|
+
case "option":
|
|
815
|
+
headerCells.push("Option");
|
|
816
|
+
separatorCells.push("------");
|
|
817
|
+
break;
|
|
818
|
+
case "alias":
|
|
819
|
+
headerCells.push("Alias");
|
|
820
|
+
separatorCells.push("-----");
|
|
821
|
+
break;
|
|
822
|
+
case "description":
|
|
823
|
+
headerCells.push("Description");
|
|
824
|
+
separatorCells.push("-----------");
|
|
825
|
+
break;
|
|
826
|
+
case "required":
|
|
827
|
+
headerCells.push("Required");
|
|
828
|
+
separatorCells.push("--------");
|
|
829
|
+
break;
|
|
830
|
+
case "default":
|
|
831
|
+
headerCells.push("Default");
|
|
832
|
+
separatorCells.push("-------");
|
|
833
|
+
break;
|
|
834
|
+
case "env":
|
|
835
|
+
headerCells.push("Env");
|
|
836
|
+
separatorCells.push("---");
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
lines.push(`| ${headerCells.join(" | ")} |`);
|
|
840
|
+
lines.push(`| ${separatorCells.join(" | ")} |`);
|
|
841
|
+
for (const opt of options) {
|
|
842
|
+
const cells = [];
|
|
843
|
+
for (const col of columns) switch (col) {
|
|
844
|
+
case "option": {
|
|
845
|
+
const placeholder = opt.placeholder ?? opt.cliName.toUpperCase().replace(/-/g, "_");
|
|
846
|
+
const optionName = opt.type === "boolean" ? `\`--${opt.cliName}\`` : `\`--${opt.cliName} <${placeholder}>\``;
|
|
847
|
+
cells.push(optionName);
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
case "alias":
|
|
851
|
+
cells.push(opt.alias ? `\`-${opt.alias}\`` : "-");
|
|
852
|
+
break;
|
|
853
|
+
case "description":
|
|
854
|
+
cells.push(escapeTableCell$1(opt.description ?? ""));
|
|
855
|
+
break;
|
|
856
|
+
case "required":
|
|
857
|
+
cells.push(opt.required ? "Yes" : "No");
|
|
858
|
+
break;
|
|
859
|
+
case "default":
|
|
860
|
+
cells.push(formatDefaultValue(opt.defaultValue));
|
|
861
|
+
break;
|
|
862
|
+
case "env": {
|
|
863
|
+
const envNames = opt.env ? Array.isArray(opt.env) ? opt.env.map((e) => `\`${e}\``).join(", ") : `\`${opt.env}\`` : "-";
|
|
864
|
+
cells.push(envNames);
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
869
|
+
}
|
|
870
|
+
return lines.join("\n");
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/docs/render-index.ts
|
|
875
|
+
/**
|
|
876
|
+
* Escape markdown special characters in table cells
|
|
877
|
+
*/
|
|
878
|
+
function escapeTableCell(str) {
|
|
879
|
+
return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Generate anchor from command path
|
|
883
|
+
*/
|
|
884
|
+
function generateAnchor(commandPath) {
|
|
885
|
+
return commandPath.replace(/\s+/g, "-").toLowerCase();
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Check if a command is a leaf (has no subcommands)
|
|
889
|
+
*/
|
|
890
|
+
function isLeafCommand(info) {
|
|
891
|
+
return info.subCommands.length === 0;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Expand commands to include their subcommands
|
|
895
|
+
* If a command has subcommands, recursively find all commands under it
|
|
896
|
+
*
|
|
897
|
+
* @param commandPaths - Command paths to expand
|
|
898
|
+
* @param allCommands - Map of all available commands
|
|
899
|
+
* @param leafOnly - If true, only include leaf commands; if false, include all commands
|
|
900
|
+
*/
|
|
901
|
+
function expandCommands(commandPaths, allCommands, leafOnly) {
|
|
902
|
+
const result = [];
|
|
903
|
+
for (const cmdPath of commandPaths) {
|
|
904
|
+
const info = allCommands.get(cmdPath);
|
|
905
|
+
if (!info) continue;
|
|
906
|
+
if (isLeafCommand(info)) result.push(cmdPath);
|
|
907
|
+
else for (const [path, pathInfo] of allCommands) if (cmdPath === "" ? path.length > 0 : path.startsWith(cmdPath + " ") || path === cmdPath) {
|
|
908
|
+
if (isLeafCommand(pathInfo) || !leafOnly) result.push(path);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return result;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Render a single category section
|
|
915
|
+
*/
|
|
916
|
+
function renderCategory(category, allCommands, headingLevel, leafOnly) {
|
|
917
|
+
const h = "#".repeat(headingLevel);
|
|
918
|
+
const lines = [];
|
|
919
|
+
lines.push(`${h} [${category.title}](${category.docPath})`);
|
|
920
|
+
lines.push("");
|
|
921
|
+
lines.push(category.description);
|
|
922
|
+
lines.push("");
|
|
923
|
+
const commandPaths = expandCommands(category.commands, allCommands, leafOnly);
|
|
924
|
+
lines.push("| Command | Description |");
|
|
925
|
+
lines.push("|---------|-------------|");
|
|
926
|
+
for (const cmdPath of commandPaths) {
|
|
927
|
+
const info = allCommands.get(cmdPath);
|
|
928
|
+
if (!info) continue;
|
|
929
|
+
if (leafOnly && !isLeafCommand(info)) continue;
|
|
930
|
+
const displayName = cmdPath || info.name;
|
|
931
|
+
const anchor = generateAnchor(displayName);
|
|
932
|
+
const desc = escapeTableCell(info.description ?? "");
|
|
933
|
+
lines.push(`| [${displayName}](${category.docPath}#${anchor}) | ${desc} |`);
|
|
934
|
+
}
|
|
935
|
+
return lines.join("\n");
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Render command index from categories
|
|
939
|
+
*
|
|
940
|
+
* Generates a category-based index of commands with links to documentation.
|
|
941
|
+
*
|
|
942
|
+
* @example
|
|
943
|
+
* const categories: CommandCategory[] = [
|
|
944
|
+
* {
|
|
945
|
+
* title: "Application Commands",
|
|
946
|
+
* description: "Commands for managing applications.",
|
|
947
|
+
* commands: ["init", "generate", "apply"],
|
|
948
|
+
* docPath: "./cli/application.md",
|
|
949
|
+
* },
|
|
950
|
+
* ];
|
|
951
|
+
*
|
|
952
|
+
* const index = await renderCommandIndex(mainCommand, categories);
|
|
953
|
+
* // ### [Application Commands](./cli/application.md)
|
|
954
|
+
* //
|
|
955
|
+
* // Commands for managing applications.
|
|
956
|
+
* //
|
|
957
|
+
* // | Command | Description |
|
|
958
|
+
* // |---------|-------------|
|
|
959
|
+
* // | [init](./cli/application.md#init) | Initialize a project |
|
|
960
|
+
* // ...
|
|
961
|
+
*
|
|
962
|
+
* @param command - Root command to extract command information from
|
|
963
|
+
* @param categories - Category definitions for grouping commands
|
|
964
|
+
* @param options - Rendering options
|
|
965
|
+
* @returns Rendered markdown string
|
|
966
|
+
*/
|
|
967
|
+
async function renderCommandIndex(command, categories, options) {
|
|
968
|
+
const headingLevel = options?.headingLevel ?? 3;
|
|
969
|
+
const leafOnly = options?.leafOnly ?? true;
|
|
970
|
+
const allCommands = await collectAllCommands(command);
|
|
971
|
+
const sections = [];
|
|
972
|
+
for (const category of categories) {
|
|
973
|
+
const section = renderCategory(category, allCommands, headingLevel, leafOnly);
|
|
974
|
+
sections.push(section);
|
|
975
|
+
}
|
|
976
|
+
return sections.join("\n\n");
|
|
977
|
+
}
|
|
978
|
+
|
|
752
979
|
//#endregion
|
|
753
980
|
//#region src/docs/types.ts
|
|
754
981
|
/**
|
|
@@ -772,6 +999,40 @@ function commandStartMarker(commandPath) {
|
|
|
772
999
|
function commandEndMarker(commandPath) {
|
|
773
1000
|
return `<!-- ${COMMAND_MARKER_PREFIX}:${commandPath}:end -->`;
|
|
774
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Marker prefix for global options sections in generated documentation
|
|
1004
|
+
* Format: <!-- politty:global-options:start --> ... <!-- politty:global-options:end -->
|
|
1005
|
+
*/
|
|
1006
|
+
const GLOBAL_OPTIONS_MARKER_PREFIX = "politty:global-options";
|
|
1007
|
+
/**
|
|
1008
|
+
* Generate start marker for a global options section
|
|
1009
|
+
*/
|
|
1010
|
+
function globalOptionsStartMarker() {
|
|
1011
|
+
return `<!-- ${GLOBAL_OPTIONS_MARKER_PREFIX}:start -->`;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Generate end marker for a global options section
|
|
1015
|
+
*/
|
|
1016
|
+
function globalOptionsEndMarker() {
|
|
1017
|
+
return `<!-- ${GLOBAL_OPTIONS_MARKER_PREFIX}:end -->`;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Marker prefix for index sections in generated documentation
|
|
1021
|
+
* Format: <!-- politty:index:start --> ... <!-- politty:index:end -->
|
|
1022
|
+
*/
|
|
1023
|
+
const INDEX_MARKER_PREFIX = "politty:index";
|
|
1024
|
+
/**
|
|
1025
|
+
* Generate start marker for an index section
|
|
1026
|
+
*/
|
|
1027
|
+
function indexStartMarker() {
|
|
1028
|
+
return `<!-- ${INDEX_MARKER_PREFIX}:start -->`;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Generate end marker for an index section
|
|
1032
|
+
*/
|
|
1033
|
+
function indexEndMarker() {
|
|
1034
|
+
return `<!-- ${INDEX_MARKER_PREFIX}:end -->`;
|
|
1035
|
+
}
|
|
775
1036
|
|
|
776
1037
|
//#endregion
|
|
777
1038
|
//#region src/docs/golden-test.ts
|
|
@@ -781,7 +1042,9 @@ function commandEndMarker(commandPath) {
|
|
|
781
1042
|
*/
|
|
782
1043
|
async function applyFormatter(content, formatter) {
|
|
783
1044
|
if (!formatter) return content;
|
|
784
|
-
|
|
1045
|
+
const formatted = await formatter(content);
|
|
1046
|
+
if (!content.endsWith("\n") && formatted.endsWith("\n")) return formatted.slice(0, -1);
|
|
1047
|
+
return formatted;
|
|
785
1048
|
}
|
|
786
1049
|
/**
|
|
787
1050
|
* Check if update mode is enabled via environment variable
|
|
@@ -795,6 +1058,7 @@ function isUpdateMode() {
|
|
|
795
1058
|
*/
|
|
796
1059
|
function normalizeFileConfig(config) {
|
|
797
1060
|
if (Array.isArray(config)) return { commands: config };
|
|
1061
|
+
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.");
|
|
798
1062
|
return config;
|
|
799
1063
|
}
|
|
800
1064
|
/**
|
|
@@ -881,6 +1145,31 @@ function filterIgnoredCommands(commandPaths, ignores) {
|
|
|
881
1145
|
});
|
|
882
1146
|
}
|
|
883
1147
|
/**
|
|
1148
|
+
* Resolve wildcards to direct matches without subcommand expansion.
|
|
1149
|
+
* Returns the "top-level" commands for use in CommandCategory.commands,
|
|
1150
|
+
* where expandCommands in render-index handles subcommand expansion.
|
|
1151
|
+
*/
|
|
1152
|
+
function resolveTopLevelCommands(specifiedCommands, allCommands) {
|
|
1153
|
+
const result = [];
|
|
1154
|
+
for (const cmdPath of specifiedCommands) if (containsWildcard(cmdPath)) result.push(...expandWildcardPattern(cmdPath, allCommands));
|
|
1155
|
+
else if (allCommands.has(cmdPath)) result.push(cmdPath);
|
|
1156
|
+
return result;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Resolve file command configuration to concrete command paths.
|
|
1160
|
+
* This applies wildcard/subcommand expansion and ignore filtering.
|
|
1161
|
+
*/
|
|
1162
|
+
function resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores) {
|
|
1163
|
+
const fileConfig = normalizeFileConfig(fileConfigRaw);
|
|
1164
|
+
const specifiedCommands = fileConfig.commands;
|
|
1165
|
+
return {
|
|
1166
|
+
fileConfig,
|
|
1167
|
+
specifiedCommands,
|
|
1168
|
+
commandPaths: filterIgnoredCommands(expandCommandPaths(specifiedCommands, allCommands), ignores),
|
|
1169
|
+
topLevelCommands: filterIgnoredCommands(resolveTopLevelCommands(specifiedCommands, allCommands), ignores)
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
884
1173
|
* Validate that there are no conflicts between files and ignores (with wildcard support)
|
|
885
1174
|
*/
|
|
886
1175
|
function validateNoConflicts(filesCommands, ignores, allCommands) {
|
|
@@ -928,13 +1217,13 @@ function sortDepthFirst(commandPaths, specifiedOrder) {
|
|
|
928
1217
|
for (const path of commandPaths) if (!visited.has(path)) result.push(path);
|
|
929
1218
|
return result;
|
|
930
1219
|
}
|
|
931
|
-
/**
|
|
932
|
-
* Generate file header from FileConfig
|
|
933
|
-
*/
|
|
934
1220
|
function generateFileHeader(fileConfig) {
|
|
935
1221
|
if (!fileConfig.title && !fileConfig.description) return null;
|
|
936
1222
|
const parts = [];
|
|
937
|
-
if (fileConfig.title)
|
|
1223
|
+
if (fileConfig.title) {
|
|
1224
|
+
const heading = "#".repeat(fileConfig.headingLevel ?? 1);
|
|
1225
|
+
parts.push(`${heading} ${fileConfig.title}`);
|
|
1226
|
+
}
|
|
938
1227
|
if (fileConfig.description) {
|
|
939
1228
|
parts.push("");
|
|
940
1229
|
parts.push(fileConfig.description);
|
|
@@ -943,6 +1232,51 @@ function generateFileHeader(fileConfig) {
|
|
|
943
1232
|
return parts.join("\n");
|
|
944
1233
|
}
|
|
945
1234
|
/**
|
|
1235
|
+
* Extract a leading file header (title and optional description paragraph)
|
|
1236
|
+
*/
|
|
1237
|
+
function extractFileHeader(content) {
|
|
1238
|
+
if (!/^#{1,6} /.test(content)) return null;
|
|
1239
|
+
const titleEnd = content.indexOf("\n");
|
|
1240
|
+
if (titleEnd === -1) return content;
|
|
1241
|
+
let cursor = titleEnd + 1;
|
|
1242
|
+
if (content[cursor] === "\n") cursor += 1;
|
|
1243
|
+
while (cursor < content.length) {
|
|
1244
|
+
const lineEnd = content.indexOf("\n", cursor);
|
|
1245
|
+
const line = lineEnd === -1 ? content.slice(cursor) : content.slice(cursor, lineEnd);
|
|
1246
|
+
if (line.length === 0 || /^#{1,6}\s/.test(line) || line.startsWith("<!-- politty:")) break;
|
|
1247
|
+
cursor = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
1248
|
+
}
|
|
1249
|
+
return content.slice(0, cursor);
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Validate and optionally update configured file header
|
|
1253
|
+
*/
|
|
1254
|
+
function processFileHeader(existingContent, fileConfig, updateMode) {
|
|
1255
|
+
const generatedHeader = generateFileHeader(fileConfig);
|
|
1256
|
+
if (!generatedHeader) return {
|
|
1257
|
+
content: existingContent,
|
|
1258
|
+
hasError: false,
|
|
1259
|
+
wasUpdated: false
|
|
1260
|
+
};
|
|
1261
|
+
if (existingContent.startsWith(generatedHeader)) return {
|
|
1262
|
+
content: existingContent,
|
|
1263
|
+
hasError: false,
|
|
1264
|
+
wasUpdated: false
|
|
1265
|
+
};
|
|
1266
|
+
const existingHeader = extractFileHeader(existingContent) ?? "";
|
|
1267
|
+
if (!updateMode) return {
|
|
1268
|
+
content: existingContent,
|
|
1269
|
+
diff: formatDiff(existingHeader, generatedHeader),
|
|
1270
|
+
hasError: true,
|
|
1271
|
+
wasUpdated: false
|
|
1272
|
+
};
|
|
1273
|
+
return {
|
|
1274
|
+
content: `${generatedHeader}${(existingHeader ? existingContent.slice(existingHeader.length) : existingContent).replace(/^\n+/, "")}`,
|
|
1275
|
+
hasError: false,
|
|
1276
|
+
wasUpdated: true
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
946
1280
|
* Extract a command section from content using markers
|
|
947
1281
|
* Returns the content between start and end markers (including markers)
|
|
948
1282
|
*/
|
|
@@ -956,6 +1290,18 @@ function extractCommandSection(content, commandPath) {
|
|
|
956
1290
|
return content.slice(startIndex, endIndex + endMarker.length);
|
|
957
1291
|
}
|
|
958
1292
|
/**
|
|
1293
|
+
* Collect command paths from command start markers in content.
|
|
1294
|
+
*/
|
|
1295
|
+
function collectCommandMarkerPaths(content) {
|
|
1296
|
+
const markerPattern = /<!--\s*politty:command:(.*?):start\s*-->/g;
|
|
1297
|
+
const paths = [];
|
|
1298
|
+
for (const match of content.matchAll(markerPattern)) paths.push(match[1] ?? "");
|
|
1299
|
+
return paths;
|
|
1300
|
+
}
|
|
1301
|
+
function formatCommandPath(commandPath) {
|
|
1302
|
+
return commandPath === "" ? "<root>" : commandPath;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
959
1305
|
* Replace a command section in content using markers
|
|
960
1306
|
* Returns the updated content with the new section
|
|
961
1307
|
*/
|
|
@@ -987,25 +1333,291 @@ function insertCommandSection(content, commandPath, newSection, specifiedOrder)
|
|
|
987
1333
|
return content.slice(0, insertPos) + newSection + "\n" + content.slice(nextIndex);
|
|
988
1334
|
}
|
|
989
1335
|
}
|
|
990
|
-
for (let i = targetIndex - 1; i >= 0; i--) {
|
|
991
|
-
const prevCmd = specifiedOrder[i];
|
|
992
|
-
if (prevCmd === void 0) continue;
|
|
993
|
-
const prevEndMarker = commandEndMarker(prevCmd);
|
|
994
|
-
const prevEndIndex = content.indexOf(prevEndMarker);
|
|
995
|
-
if (prevEndIndex !== -1) {
|
|
996
|
-
const insertPos = prevEndIndex + prevEndMarker.length;
|
|
997
|
-
return content.slice(0, insertPos) + "\n" + newSection + content.slice(insertPos);
|
|
1336
|
+
for (let i = targetIndex - 1; i >= 0; i--) {
|
|
1337
|
+
const prevCmd = specifiedOrder[i];
|
|
1338
|
+
if (prevCmd === void 0) continue;
|
|
1339
|
+
const prevEndMarker = commandEndMarker(prevCmd);
|
|
1340
|
+
const prevEndIndex = content.indexOf(prevEndMarker);
|
|
1341
|
+
if (prevEndIndex !== -1) {
|
|
1342
|
+
const insertPos = prevEndIndex + prevEndMarker.length;
|
|
1343
|
+
return content.slice(0, insertPos) + "\n" + newSection + content.slice(insertPos);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return content.trimEnd() + "\n" + newSection + "\n";
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Extract a marker section from content
|
|
1350
|
+
* Returns the content between start and end markers (including markers)
|
|
1351
|
+
*/
|
|
1352
|
+
function extractMarkerSection(content, startMarker, endMarker) {
|
|
1353
|
+
const startIndex = content.indexOf(startMarker);
|
|
1354
|
+
if (startIndex === -1) return null;
|
|
1355
|
+
const endIndex = content.indexOf(endMarker, startIndex);
|
|
1356
|
+
if (endIndex === -1) return null;
|
|
1357
|
+
return content.slice(startIndex, endIndex + endMarker.length);
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Replace a marker section in content
|
|
1361
|
+
* Returns the updated content with the new section
|
|
1362
|
+
*/
|
|
1363
|
+
function replaceMarkerSection(content, startMarker, endMarker, newSection) {
|
|
1364
|
+
const startIndex = content.indexOf(startMarker);
|
|
1365
|
+
if (startIndex === -1) return null;
|
|
1366
|
+
const endIndex = content.indexOf(endMarker, startIndex);
|
|
1367
|
+
if (endIndex === -1) return null;
|
|
1368
|
+
return content.slice(0, startIndex) + newSection + content.slice(endIndex + endMarker.length);
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Check if config is the { args, options? } shape (not shorthand ArgsShape)
|
|
1372
|
+
*
|
|
1373
|
+
* Distinguishes between:
|
|
1374
|
+
* - { args: ArgsShape, options?: ArgsTableOptions } → returns true
|
|
1375
|
+
* - ArgsShape (e.g., { verbose: ZodType, args: ZodType }) → returns false
|
|
1376
|
+
*
|
|
1377
|
+
* The key insight is that in the { args, options? } shape, config.args is an ArgsShape
|
|
1378
|
+
* (Record of ZodTypes), while in shorthand, config itself is the ArgsShape and config.args
|
|
1379
|
+
* would be a single ZodType if user has an option named "args".
|
|
1380
|
+
*/
|
|
1381
|
+
function isGlobalOptionsConfigWithOptions(config) {
|
|
1382
|
+
if (typeof config !== "object" || config === null || !("args" in config)) return false;
|
|
1383
|
+
return !(config.args instanceof zod.z.ZodType);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Collect option fields that are actually rendered by global options markers.
|
|
1387
|
+
* Positional args are not rendered in args tables, so they must not be excluded.
|
|
1388
|
+
*/
|
|
1389
|
+
function collectRenderableGlobalOptionFields(argsShape) {
|
|
1390
|
+
return require_schema_extractor.extractFields(zod.z.object(argsShape)).fields.filter((field) => !field.positional);
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Compare option definitions for global-options compatibility.
|
|
1394
|
+
*/
|
|
1395
|
+
function areGlobalOptionsEquivalent(a, b) {
|
|
1396
|
+
const { schema: _aSchema, ...aRest } = a;
|
|
1397
|
+
const { schema: _bSchema, ...bRest } = b;
|
|
1398
|
+
return (0, node_util.isDeepStrictEqual)(aRest, bRest);
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Normalize rootDoc.globalOptions to { args, options? } form.
|
|
1402
|
+
*/
|
|
1403
|
+
function normalizeGlobalOptions(config) {
|
|
1404
|
+
if (!config) return void 0;
|
|
1405
|
+
return isGlobalOptionsConfigWithOptions(config) ? config : { args: config };
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Collect global option definitions from rootDoc.
|
|
1409
|
+
* Global options are intentionally applied to all generated command sections.
|
|
1410
|
+
*/
|
|
1411
|
+
function collectGlobalOptionDefinitions(rootDoc) {
|
|
1412
|
+
const globalOptions = /* @__PURE__ */ new Map();
|
|
1413
|
+
if (!rootDoc?.globalOptions) return globalOptions;
|
|
1414
|
+
const normalized = normalizeGlobalOptions(rootDoc.globalOptions);
|
|
1415
|
+
if (!normalized) return globalOptions;
|
|
1416
|
+
for (const field of collectRenderableGlobalOptionFields(normalized.args)) globalOptions.set(field.name, field);
|
|
1417
|
+
return globalOptions;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Derive CommandCategory[] from files mapping.
|
|
1421
|
+
* Category title/description come from the first command in each file entry.
|
|
1422
|
+
*/
|
|
1423
|
+
function deriveIndexFromFiles(files, rootDocPath, allCommands, ignores) {
|
|
1424
|
+
const categories = [];
|
|
1425
|
+
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
1426
|
+
const { commandPaths, topLevelCommands } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
1427
|
+
if (commandPaths.length === 0) continue;
|
|
1428
|
+
const docPath = "./" + node_path.relative(node_path.dirname(rootDocPath), filePath).replace(/\\/g, "/");
|
|
1429
|
+
const firstCmdPath = commandPaths[0];
|
|
1430
|
+
const cmdInfo = firstCmdPath !== void 0 ? allCommands.get(firstCmdPath) : void 0;
|
|
1431
|
+
categories.push({
|
|
1432
|
+
title: cmdInfo?.name ?? node_path.basename(filePath, node_path.extname(filePath)),
|
|
1433
|
+
description: cmdInfo?.description ?? "",
|
|
1434
|
+
commands: topLevelCommands,
|
|
1435
|
+
docPath
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
return categories;
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Collect command paths that are actually documented in configured files.
|
|
1442
|
+
*/
|
|
1443
|
+
function collectDocumentedCommandPaths(files, allCommands, ignores) {
|
|
1444
|
+
const documentedCommandPaths = /* @__PURE__ */ new Set();
|
|
1445
|
+
for (const fileConfigRaw of Object.values(files)) {
|
|
1446
|
+
const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
1447
|
+
for (const commandPath of commandPaths) documentedCommandPaths.add(commandPath);
|
|
1448
|
+
}
|
|
1449
|
+
return documentedCommandPaths;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Collect command paths that are targeted in configured files.
|
|
1453
|
+
*/
|
|
1454
|
+
function collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) {
|
|
1455
|
+
const documentedTargetCommandPaths = /* @__PURE__ */ new Set();
|
|
1456
|
+
for (const filePath of Object.keys(files)) {
|
|
1457
|
+
const targetCommandsInFile = findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores);
|
|
1458
|
+
for (const commandPath of targetCommandsInFile) documentedTargetCommandPaths.add(commandPath);
|
|
1459
|
+
}
|
|
1460
|
+
return documentedTargetCommandPaths;
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Validate that excluded command options match globalOptions definitions.
|
|
1464
|
+
*/
|
|
1465
|
+
function validateGlobalOptionCompatibility(documentedCommandPaths, allCommands, globalOptions) {
|
|
1466
|
+
if (globalOptions.size === 0) return;
|
|
1467
|
+
const conflicts = [];
|
|
1468
|
+
for (const commandPath of documentedCommandPaths) {
|
|
1469
|
+
const info = allCommands.get(commandPath);
|
|
1470
|
+
if (!info) continue;
|
|
1471
|
+
for (const option of info.options) {
|
|
1472
|
+
const globalOption = globalOptions.get(option.name);
|
|
1473
|
+
if (!globalOption) continue;
|
|
1474
|
+
if (!areGlobalOptionsEquivalent(globalOption, option)) conflicts.push(`Command "${formatCommandPath(commandPath)}" option "--${option.cliName}" does not match globalOptions definition for "${option.name}".`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (conflicts.length > 0) throw new Error(`Invalid globalOptions configuration:\n - ${conflicts.join("\n - ")}`);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Generate global options section content with markers
|
|
1481
|
+
*/
|
|
1482
|
+
function generateGlobalOptionsSection(config) {
|
|
1483
|
+
const startMarker = globalOptionsStartMarker();
|
|
1484
|
+
const endMarker = globalOptionsEndMarker();
|
|
1485
|
+
return [
|
|
1486
|
+
startMarker,
|
|
1487
|
+
renderArgsTable(config.args, config.options),
|
|
1488
|
+
endMarker
|
|
1489
|
+
].join("\n");
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Generate index section content with markers
|
|
1493
|
+
*/
|
|
1494
|
+
async function generateIndexSection(categories, command, options) {
|
|
1495
|
+
const startMarker = indexStartMarker();
|
|
1496
|
+
const endMarker = indexEndMarker();
|
|
1497
|
+
return [
|
|
1498
|
+
startMarker,
|
|
1499
|
+
await renderCommandIndex(command, categories, options),
|
|
1500
|
+
endMarker
|
|
1501
|
+
].join("\n");
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Normalize a doc file path for equivalence checks.
|
|
1505
|
+
*/
|
|
1506
|
+
function normalizeDocPathForComparison(filePath) {
|
|
1507
|
+
return node_path.resolve(filePath);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Process global options marker in file content
|
|
1511
|
+
* Returns result with updated content and any diffs
|
|
1512
|
+
*/
|
|
1513
|
+
async function processGlobalOptionsMarker(existingContent, globalOptionsConfig, updateMode, formatter) {
|
|
1514
|
+
let content = existingContent;
|
|
1515
|
+
const diffs = [];
|
|
1516
|
+
let hasError = false;
|
|
1517
|
+
let wasUpdated = false;
|
|
1518
|
+
const startMarker = globalOptionsStartMarker();
|
|
1519
|
+
const endMarker = globalOptionsEndMarker();
|
|
1520
|
+
const generatedSection = await applyFormatter(generateGlobalOptionsSection(globalOptionsConfig), formatter);
|
|
1521
|
+
const existingSection = extractMarkerSection(content, startMarker, endMarker);
|
|
1522
|
+
if (!existingSection) {
|
|
1523
|
+
hasError = true;
|
|
1524
|
+
diffs.push(`Global options marker not found in file. Expected markers:\n${startMarker}\n...\n${endMarker}`);
|
|
1525
|
+
return {
|
|
1526
|
+
content,
|
|
1527
|
+
diffs,
|
|
1528
|
+
hasError,
|
|
1529
|
+
wasUpdated
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
if (existingSection !== generatedSection) if (updateMode) {
|
|
1533
|
+
const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
|
|
1534
|
+
if (updated) {
|
|
1535
|
+
content = updated;
|
|
1536
|
+
wasUpdated = true;
|
|
1537
|
+
} else {
|
|
1538
|
+
hasError = true;
|
|
1539
|
+
diffs.push("Failed to replace global options section");
|
|
1540
|
+
}
|
|
1541
|
+
} else {
|
|
1542
|
+
hasError = true;
|
|
1543
|
+
diffs.push(formatDiff(existingSection, generatedSection));
|
|
1544
|
+
}
|
|
1545
|
+
return {
|
|
1546
|
+
content,
|
|
1547
|
+
diffs,
|
|
1548
|
+
hasError,
|
|
1549
|
+
wasUpdated
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Process index marker in file content
|
|
1554
|
+
* Returns result with updated content and any diffs.
|
|
1555
|
+
* If the marker is not present in the file, the section is silently skipped.
|
|
1556
|
+
*/
|
|
1557
|
+
async function processIndexMarker(existingContent, categories, command, updateMode, formatter, indexOptions) {
|
|
1558
|
+
let content = existingContent;
|
|
1559
|
+
const diffs = [];
|
|
1560
|
+
let hasError = false;
|
|
1561
|
+
let wasUpdated = false;
|
|
1562
|
+
const startMarker = indexStartMarker();
|
|
1563
|
+
const endMarker = indexEndMarker();
|
|
1564
|
+
const hasStartMarker = content.includes(startMarker);
|
|
1565
|
+
const hasEndMarker = content.includes(endMarker);
|
|
1566
|
+
if (!hasStartMarker && !hasEndMarker) return {
|
|
1567
|
+
content,
|
|
1568
|
+
diffs,
|
|
1569
|
+
hasError,
|
|
1570
|
+
wasUpdated
|
|
1571
|
+
};
|
|
1572
|
+
if (!hasStartMarker || !hasEndMarker) {
|
|
1573
|
+
hasError = true;
|
|
1574
|
+
diffs.push("Index marker section is malformed: both start and end markers are required.");
|
|
1575
|
+
return {
|
|
1576
|
+
content,
|
|
1577
|
+
diffs,
|
|
1578
|
+
hasError,
|
|
1579
|
+
wasUpdated
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
const existingSection = extractMarkerSection(content, startMarker, endMarker);
|
|
1583
|
+
if (!existingSection) {
|
|
1584
|
+
hasError = true;
|
|
1585
|
+
diffs.push("Index marker section is malformed: start marker must appear before end marker.");
|
|
1586
|
+
return {
|
|
1587
|
+
content,
|
|
1588
|
+
diffs,
|
|
1589
|
+
hasError,
|
|
1590
|
+
wasUpdated
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
const generatedSection = await applyFormatter(await generateIndexSection(categories, command, indexOptions), formatter);
|
|
1594
|
+
if (existingSection !== generatedSection) if (updateMode) {
|
|
1595
|
+
const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
|
|
1596
|
+
if (updated) {
|
|
1597
|
+
content = updated;
|
|
1598
|
+
wasUpdated = true;
|
|
1599
|
+
} else {
|
|
1600
|
+
hasError = true;
|
|
1601
|
+
diffs.push("Failed to replace index section");
|
|
998
1602
|
}
|
|
1603
|
+
} else {
|
|
1604
|
+
hasError = true;
|
|
1605
|
+
diffs.push(formatDiff(existingSection, generatedSection));
|
|
999
1606
|
}
|
|
1000
|
-
return
|
|
1607
|
+
return {
|
|
1608
|
+
content,
|
|
1609
|
+
diffs,
|
|
1610
|
+
hasError,
|
|
1611
|
+
wasUpdated
|
|
1612
|
+
};
|
|
1001
1613
|
}
|
|
1002
1614
|
/**
|
|
1003
1615
|
* Find which file contains a specific command
|
|
1004
1616
|
*/
|
|
1005
1617
|
function findFileForCommand(commandPath, files, allCommands, ignores) {
|
|
1006
1618
|
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
1007
|
-
const
|
|
1008
|
-
if (
|
|
1619
|
+
const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
1620
|
+
if (commandPaths.includes(commandPath)) return filePath;
|
|
1009
1621
|
}
|
|
1010
1622
|
return null;
|
|
1011
1623
|
}
|
|
@@ -1016,8 +1628,7 @@ function findFileForCommand(commandPath, files, allCommands, ignores) {
|
|
|
1016
1628
|
function findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) {
|
|
1017
1629
|
const fileConfigRaw = files[filePath];
|
|
1018
1630
|
if (!fileConfigRaw) return [];
|
|
1019
|
-
const specifiedCommands =
|
|
1020
|
-
const commandPaths = filterIgnoredCommands(expandCommandPaths(specifiedCommands, allCommands), ignores);
|
|
1631
|
+
const { specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
1021
1632
|
const expandedTargets = /* @__PURE__ */ new Set();
|
|
1022
1633
|
for (const targetCmd of targetCommands) {
|
|
1023
1634
|
if (!commandPaths.includes(targetCmd)) continue;
|
|
@@ -1056,7 +1667,7 @@ function generateFileMarkdown(commandPaths, allCommands, render, filePath, fileM
|
|
|
1056
1667
|
const section = generateCommandSection(cmdPath, allCommands, render, filePath, fileMap);
|
|
1057
1668
|
if (section) sections.push(section);
|
|
1058
1669
|
}
|
|
1059
|
-
return sections.join("\n")
|
|
1670
|
+
return `${sections.join("\n")}\n`;
|
|
1060
1671
|
}
|
|
1061
1672
|
/**
|
|
1062
1673
|
* Build a map of command path to file path
|
|
@@ -1064,8 +1675,7 @@ function generateFileMarkdown(commandPaths, allCommands, render, filePath, fileM
|
|
|
1064
1675
|
function buildFileMap(files, allCommands, ignores) {
|
|
1065
1676
|
const fileMap = {};
|
|
1066
1677
|
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
1067
|
-
const
|
|
1068
|
-
const commandPaths = filterIgnoredCommands(expandCommandPaths(specifiedCommands, allCommands), ignores);
|
|
1678
|
+
const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
1069
1679
|
for (const cmdPath of commandPaths) fileMap[cmdPath] = filePath;
|
|
1070
1680
|
}
|
|
1071
1681
|
return fileMap;
|
|
@@ -1086,10 +1696,21 @@ async function executeConfiguredExamples(allCommands, examplesConfig, rootComman
|
|
|
1086
1696
|
* Generate documentation from command definition
|
|
1087
1697
|
*/
|
|
1088
1698
|
async function generateDoc(config) {
|
|
1089
|
-
const { command, files, ignores = [], format = {}, formatter, examples: examplesConfig, targetCommands } = config;
|
|
1699
|
+
const { command, rootDoc, files, ignores = [], format = {}, formatter, examples: examplesConfig, targetCommands } = config;
|
|
1090
1700
|
const updateMode = isUpdateMode();
|
|
1701
|
+
if (rootDoc) {
|
|
1702
|
+
const normalizedRootDocPath = normalizeDocPathForComparison(rootDoc.path);
|
|
1703
|
+
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.`);
|
|
1704
|
+
}
|
|
1091
1705
|
const allCommands = await collectAllCommands(command);
|
|
1092
1706
|
if (examplesConfig) await executeConfiguredExamples(allCommands, examplesConfig, command);
|
|
1707
|
+
const hasTargetCommands = targetCommands !== void 0 && targetCommands.length > 0;
|
|
1708
|
+
if (hasTargetCommands) {
|
|
1709
|
+
for (const targetCommand of targetCommands) if (!findFileForCommand(targetCommand, files, allCommands, ignores)) throw new Error(`Target command "${targetCommand}" not found in any file configuration`);
|
|
1710
|
+
}
|
|
1711
|
+
const globalOptionDefinitions = collectGlobalOptionDefinitions(rootDoc);
|
|
1712
|
+
validateGlobalOptionCompatibility(hasTargetCommands ? collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) : collectDocumentedCommandPaths(files, allCommands, ignores), allCommands, globalOptionDefinitions);
|
|
1713
|
+
if (globalOptionDefinitions.size > 0) for (const info of allCommands.values()) info.options = info.options.filter((opt) => !globalOptionDefinitions.has(opt.name));
|
|
1093
1714
|
const allFilesCommands = [];
|
|
1094
1715
|
for (const fileConfigRaw of Object.values(files)) {
|
|
1095
1716
|
const fileConfig = normalizeFileConfig(fileConfigRaw);
|
|
@@ -1100,15 +1721,14 @@ async function generateDoc(config) {
|
|
|
1100
1721
|
const fileMap = buildFileMap(files, allCommands, ignores);
|
|
1101
1722
|
const results = [];
|
|
1102
1723
|
let hasError = false;
|
|
1103
|
-
if (targetCommands && targetCommands.length > 0) {
|
|
1104
|
-
for (const targetCommand of targetCommands) if (!findFileForCommand(targetCommand, files, allCommands, ignores)) throw new Error(`Target command "${targetCommand}" not found in any file configuration`);
|
|
1105
|
-
}
|
|
1106
1724
|
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
1107
|
-
const fileConfig =
|
|
1108
|
-
const specifiedCommands = fileConfig.commands;
|
|
1725
|
+
const { fileConfig, specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
1109
1726
|
if (specifiedCommands.length === 0) continue;
|
|
1110
|
-
const commandPaths = filterIgnoredCommands(expandCommandPaths(specifiedCommands, allCommands), ignores);
|
|
1111
1727
|
if (commandPaths.length === 0) continue;
|
|
1728
|
+
const fileTargetCommands = hasTargetCommands ? findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) : [];
|
|
1729
|
+
if (hasTargetCommands && fileTargetCommands.length === 0) continue;
|
|
1730
|
+
let fileStatus = "match";
|
|
1731
|
+
const diffs = [];
|
|
1112
1732
|
const minDepth = Math.min(...commandPaths.map((p) => allCommands.get(p)?.depth ?? 1));
|
|
1113
1733
|
const adjustedHeadingLevel = Math.max(1, (format?.headingLevel ?? 1) - (minDepth - 1));
|
|
1114
1734
|
const fileRenderer = createCommandRenderer({
|
|
@@ -1116,12 +1736,8 @@ async function generateDoc(config) {
|
|
|
1116
1736
|
headingLevel: adjustedHeadingLevel
|
|
1117
1737
|
});
|
|
1118
1738
|
const render = fileConfig.render ?? fileRenderer;
|
|
1119
|
-
if (
|
|
1120
|
-
const fileTargetCommands = findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores);
|
|
1121
|
-
if (fileTargetCommands.length === 0) continue;
|
|
1739
|
+
if (hasTargetCommands) {
|
|
1122
1740
|
let existingContent = readFile(filePath);
|
|
1123
|
-
let fileStatus = "match";
|
|
1124
|
-
const diffs = [];
|
|
1125
1741
|
for (const targetCommand of fileTargetCommands) {
|
|
1126
1742
|
const rawSection = generateCommandSection(targetCommand, allCommands, render, filePath, fileMap);
|
|
1127
1743
|
if (!rawSection) throw new Error(`Target command "${targetCommand}" not found in commands`);
|
|
@@ -1167,33 +1783,75 @@ async function generateDoc(config) {
|
|
|
1167
1783
|
diffs.push(formatDiff(existingSection, generatedSectionOnly));
|
|
1168
1784
|
}
|
|
1169
1785
|
}
|
|
1170
|
-
results.push({
|
|
1171
|
-
path: filePath,
|
|
1172
|
-
status: fileStatus,
|
|
1173
|
-
diff: diffs.length > 0 ? diffs.join("\n\n") : void 0
|
|
1174
|
-
});
|
|
1175
1786
|
} else {
|
|
1176
1787
|
const generatedMarkdown = await applyFormatter(generateFileMarkdown(commandPaths, allCommands, render, filePath, fileMap, specifiedCommands, fileConfig), formatter);
|
|
1177
1788
|
const comparison = compareWithExisting(generatedMarkdown, filePath);
|
|
1178
|
-
if (comparison.match)
|
|
1179
|
-
path: filePath,
|
|
1180
|
-
status: "match"
|
|
1181
|
-
});
|
|
1182
|
-
else if (updateMode) {
|
|
1789
|
+
if (comparison.match) {} else if (updateMode) {
|
|
1183
1790
|
writeFile(filePath, generatedMarkdown);
|
|
1184
|
-
|
|
1185
|
-
path: filePath,
|
|
1186
|
-
status: comparison.fileExists ? "updated" : "created"
|
|
1187
|
-
});
|
|
1791
|
+
fileStatus = comparison.fileExists ? "updated" : "created";
|
|
1188
1792
|
} else {
|
|
1189
1793
|
hasError = true;
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1794
|
+
fileStatus = "diff";
|
|
1795
|
+
if (comparison.diff) diffs.push(comparison.diff);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (diffs.length > 0) fileStatus = "diff";
|
|
1799
|
+
results.push({
|
|
1800
|
+
path: filePath,
|
|
1801
|
+
status: fileStatus,
|
|
1802
|
+
diff: diffs.length > 0 ? diffs.join("\n\n") : void 0
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
if (rootDoc) {
|
|
1806
|
+
const rootDocFilePath = rootDoc.path;
|
|
1807
|
+
let rootDocStatus = "match";
|
|
1808
|
+
const rootDocDiffs = [];
|
|
1809
|
+
const existingContent = readFile(rootDocFilePath);
|
|
1810
|
+
if (existingContent === null) {
|
|
1811
|
+
hasError = true;
|
|
1812
|
+
rootDocStatus = "diff";
|
|
1813
|
+
rootDocDiffs.push("File does not exist. Cannot validate rootDoc markers.");
|
|
1814
|
+
} else {
|
|
1815
|
+
let content = existingContent;
|
|
1816
|
+
let markerUpdated = false;
|
|
1817
|
+
const rootDocFileConfig = { title: command.name };
|
|
1818
|
+
if (rootDoc.headingLevel !== void 0) rootDocFileConfig.headingLevel = rootDoc.headingLevel;
|
|
1819
|
+
if (command.description !== void 0) rootDocFileConfig.description = command.description;
|
|
1820
|
+
const headerResult = processFileHeader(content, rootDocFileConfig, updateMode);
|
|
1821
|
+
content = headerResult.content;
|
|
1822
|
+
if (headerResult.diff) rootDocDiffs.push(headerResult.diff);
|
|
1823
|
+
if (headerResult.hasError) hasError = true;
|
|
1824
|
+
if (headerResult.wasUpdated) markerUpdated = true;
|
|
1825
|
+
const unexpectedCommandMarkers = Array.from(new Set(collectCommandMarkerPaths(content)));
|
|
1826
|
+
if (unexpectedCommandMarkers.length > 0) {
|
|
1827
|
+
hasError = true;
|
|
1828
|
+
rootDocDiffs.push(`Found unexpected command marker sections in rootDoc: ${unexpectedCommandMarkers.map((commandPath) => `"${formatCommandPath(commandPath)}"`).join(", ")}.`);
|
|
1829
|
+
}
|
|
1830
|
+
const normalizedGlobalOptions = normalizeGlobalOptions(rootDoc.globalOptions);
|
|
1831
|
+
if (normalizedGlobalOptions) {
|
|
1832
|
+
const globalOptionsResult = await processGlobalOptionsMarker(content, normalizedGlobalOptions, updateMode, formatter);
|
|
1833
|
+
content = globalOptionsResult.content;
|
|
1834
|
+
rootDocDiffs.push(...globalOptionsResult.diffs);
|
|
1835
|
+
if (globalOptionsResult.hasError) hasError = true;
|
|
1836
|
+
if (globalOptionsResult.wasUpdated) markerUpdated = true;
|
|
1837
|
+
}
|
|
1838
|
+
const derivedCategories = deriveIndexFromFiles(files, rootDocFilePath, allCommands, ignores);
|
|
1839
|
+
const indexResult = await processIndexMarker(content, derivedCategories, command, updateMode, formatter, rootDoc.index);
|
|
1840
|
+
content = indexResult.content;
|
|
1841
|
+
rootDocDiffs.push(...indexResult.diffs);
|
|
1842
|
+
if (indexResult.hasError) hasError = true;
|
|
1843
|
+
if (indexResult.wasUpdated) markerUpdated = true;
|
|
1844
|
+
if (updateMode && markerUpdated) {
|
|
1845
|
+
writeFile(rootDocFilePath, content);
|
|
1846
|
+
if (rootDocStatus === "match") rootDocStatus = "updated";
|
|
1195
1847
|
}
|
|
1196
1848
|
}
|
|
1849
|
+
if (rootDocDiffs.length > 0) rootDocStatus = "diff";
|
|
1850
|
+
results.push({
|
|
1851
|
+
path: rootDocFilePath,
|
|
1852
|
+
status: rootDocStatus,
|
|
1853
|
+
diff: rootDocDiffs.length > 0 ? rootDocDiffs.join("\n\n") : void 0
|
|
1854
|
+
});
|
|
1197
1855
|
}
|
|
1198
1856
|
return {
|
|
1199
1857
|
success: !hasError,
|
|
@@ -1229,234 +1887,10 @@ function initDocFile(config, fileSystem) {
|
|
|
1229
1887
|
else for (const filePath of Object.keys(config.files)) deleteFile(filePath, fileSystem);
|
|
1230
1888
|
}
|
|
1231
1889
|
|
|
1232
|
-
//#endregion
|
|
1233
|
-
//#region src/docs/render-args.ts
|
|
1234
|
-
/**
|
|
1235
|
-
* Extract ResolvedFieldMeta array from ArgsShape
|
|
1236
|
-
*
|
|
1237
|
-
* This converts a raw args shape (like `commonArgs`) into the
|
|
1238
|
-
* ResolvedFieldMeta format used by politty's rendering functions.
|
|
1239
|
-
*/
|
|
1240
|
-
function extractArgsFields(args) {
|
|
1241
|
-
return require_schema_extractor.extractFields(zod.z.object(args)).fields;
|
|
1242
|
-
}
|
|
1243
|
-
/**
|
|
1244
|
-
* Render args definition as a markdown options table
|
|
1245
|
-
*
|
|
1246
|
-
* This function takes raw args definitions (like `commonArgs`) and
|
|
1247
|
-
* renders them as a markdown table suitable for documentation.
|
|
1248
|
-
*
|
|
1249
|
-
* @example
|
|
1250
|
-
* import { renderArgsTable } from "politty/docs";
|
|
1251
|
-
* import { commonArgs, workspaceArgs } from "./args";
|
|
1252
|
-
*
|
|
1253
|
-
* const table = renderArgsTable({
|
|
1254
|
-
* ...commonArgs,
|
|
1255
|
-
* ...workspaceArgs,
|
|
1256
|
-
* });
|
|
1257
|
-
* // | Option | Alias | Description | Default |
|
|
1258
|
-
* // |--------|-------|-------------|---------|
|
|
1259
|
-
* // | `--env-file <ENV_FILE>` | `-e` | Path to environment file | - |
|
|
1260
|
-
* // ...
|
|
1261
|
-
*
|
|
1262
|
-
* @param args - Args shape (Record of string keys to Zod schemas with arg() metadata)
|
|
1263
|
-
* @param options - Rendering options
|
|
1264
|
-
* @returns Rendered markdown table string
|
|
1265
|
-
*/
|
|
1266
|
-
function renderArgsTable(args, options) {
|
|
1267
|
-
const optionFields = extractArgsFields(args).filter((f) => !f.positional);
|
|
1268
|
-
if (optionFields.length === 0) return "";
|
|
1269
|
-
if (options?.columns) return renderFilteredTable(optionFields, options.columns);
|
|
1270
|
-
return renderOptionsTableFromArray(optionFields);
|
|
1271
|
-
}
|
|
1272
|
-
/**
|
|
1273
|
-
* Escape markdown special characters in table cells
|
|
1274
|
-
*/
|
|
1275
|
-
function escapeTableCell$1(str) {
|
|
1276
|
-
return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1277
|
-
}
|
|
1278
|
-
/**
|
|
1279
|
-
* Format default value for display
|
|
1280
|
-
*/
|
|
1281
|
-
function formatDefaultValue(value) {
|
|
1282
|
-
if (value === void 0) return "-";
|
|
1283
|
-
return `\`${JSON.stringify(value)}\``;
|
|
1284
|
-
}
|
|
1285
|
-
/**
|
|
1286
|
-
* Render table with filtered columns
|
|
1287
|
-
*/
|
|
1288
|
-
function renderFilteredTable(options, columns) {
|
|
1289
|
-
const lines = [];
|
|
1290
|
-
const headerCells = [];
|
|
1291
|
-
const separatorCells = [];
|
|
1292
|
-
for (const col of columns) switch (col) {
|
|
1293
|
-
case "option":
|
|
1294
|
-
headerCells.push("Option");
|
|
1295
|
-
separatorCells.push("------");
|
|
1296
|
-
break;
|
|
1297
|
-
case "alias":
|
|
1298
|
-
headerCells.push("Alias");
|
|
1299
|
-
separatorCells.push("-----");
|
|
1300
|
-
break;
|
|
1301
|
-
case "description":
|
|
1302
|
-
headerCells.push("Description");
|
|
1303
|
-
separatorCells.push("-----------");
|
|
1304
|
-
break;
|
|
1305
|
-
case "required":
|
|
1306
|
-
headerCells.push("Required");
|
|
1307
|
-
separatorCells.push("--------");
|
|
1308
|
-
break;
|
|
1309
|
-
case "default":
|
|
1310
|
-
headerCells.push("Default");
|
|
1311
|
-
separatorCells.push("-------");
|
|
1312
|
-
break;
|
|
1313
|
-
case "env":
|
|
1314
|
-
headerCells.push("Env");
|
|
1315
|
-
separatorCells.push("---");
|
|
1316
|
-
break;
|
|
1317
|
-
}
|
|
1318
|
-
lines.push(`| ${headerCells.join(" | ")} |`);
|
|
1319
|
-
lines.push(`| ${separatorCells.join(" | ")} |`);
|
|
1320
|
-
for (const opt of options) {
|
|
1321
|
-
const cells = [];
|
|
1322
|
-
for (const col of columns) switch (col) {
|
|
1323
|
-
case "option": {
|
|
1324
|
-
const placeholder = opt.placeholder ?? opt.cliName.toUpperCase().replace(/-/g, "_");
|
|
1325
|
-
const optionName = opt.type === "boolean" ? `\`--${opt.cliName}\`` : `\`--${opt.cliName} <${placeholder}>\``;
|
|
1326
|
-
cells.push(optionName);
|
|
1327
|
-
break;
|
|
1328
|
-
}
|
|
1329
|
-
case "alias":
|
|
1330
|
-
cells.push(opt.alias ? `\`-${opt.alias}\`` : "-");
|
|
1331
|
-
break;
|
|
1332
|
-
case "description":
|
|
1333
|
-
cells.push(escapeTableCell$1(opt.description ?? ""));
|
|
1334
|
-
break;
|
|
1335
|
-
case "required":
|
|
1336
|
-
cells.push(opt.required ? "Yes" : "No");
|
|
1337
|
-
break;
|
|
1338
|
-
case "default":
|
|
1339
|
-
cells.push(formatDefaultValue(opt.defaultValue));
|
|
1340
|
-
break;
|
|
1341
|
-
case "env": {
|
|
1342
|
-
const envNames = opt.env ? Array.isArray(opt.env) ? opt.env.map((e) => `\`${e}\``).join(", ") : `\`${opt.env}\`` : "-";
|
|
1343
|
-
cells.push(envNames);
|
|
1344
|
-
break;
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
lines.push(`| ${cells.join(" | ")} |`);
|
|
1348
|
-
}
|
|
1349
|
-
return lines.join("\n");
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
//#endregion
|
|
1353
|
-
//#region src/docs/render-index.ts
|
|
1354
|
-
/**
|
|
1355
|
-
* Escape markdown special characters in table cells
|
|
1356
|
-
*/
|
|
1357
|
-
function escapeTableCell(str) {
|
|
1358
|
-
return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1359
|
-
}
|
|
1360
|
-
/**
|
|
1361
|
-
* Generate anchor from command path
|
|
1362
|
-
*/
|
|
1363
|
-
function generateAnchor(commandPath) {
|
|
1364
|
-
return commandPath.replace(/\s+/g, "-").toLowerCase();
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* Check if a command is a leaf (has no subcommands)
|
|
1368
|
-
*/
|
|
1369
|
-
function isLeafCommand(info) {
|
|
1370
|
-
return info.subCommands.length === 0;
|
|
1371
|
-
}
|
|
1372
|
-
/**
|
|
1373
|
-
* Expand commands to include their subcommands
|
|
1374
|
-
* If a command has subcommands, recursively find all commands under it
|
|
1375
|
-
*
|
|
1376
|
-
* @param commandPaths - Command paths to expand
|
|
1377
|
-
* @param allCommands - Map of all available commands
|
|
1378
|
-
* @param leafOnly - If true, only include leaf commands; if false, include all commands
|
|
1379
|
-
*/
|
|
1380
|
-
function expandCommands(commandPaths, allCommands, leafOnly) {
|
|
1381
|
-
const result = [];
|
|
1382
|
-
for (const cmdPath of commandPaths) {
|
|
1383
|
-
const info = allCommands.get(cmdPath);
|
|
1384
|
-
if (!info) continue;
|
|
1385
|
-
if (isLeafCommand(info)) result.push(cmdPath);
|
|
1386
|
-
else for (const [path, pathInfo] of allCommands) if (cmdPath === "" ? path.length > 0 : path.startsWith(cmdPath + " ") || path === cmdPath) {
|
|
1387
|
-
if (isLeafCommand(pathInfo) || !leafOnly) result.push(path);
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
return result;
|
|
1391
|
-
}
|
|
1392
|
-
/**
|
|
1393
|
-
* Render a single category section
|
|
1394
|
-
*/
|
|
1395
|
-
function renderCategory(category, allCommands, headingLevel, leafOnly) {
|
|
1396
|
-
const h = "#".repeat(headingLevel);
|
|
1397
|
-
const lines = [];
|
|
1398
|
-
lines.push(`${h} [${category.title}](${category.docPath})`);
|
|
1399
|
-
lines.push("");
|
|
1400
|
-
lines.push(category.description);
|
|
1401
|
-
lines.push("");
|
|
1402
|
-
const commandPaths = expandCommands(category.commands, allCommands, leafOnly);
|
|
1403
|
-
lines.push("| Command | Description |");
|
|
1404
|
-
lines.push("|---------|-------------|");
|
|
1405
|
-
for (const cmdPath of commandPaths) {
|
|
1406
|
-
const info = allCommands.get(cmdPath);
|
|
1407
|
-
if (!info) continue;
|
|
1408
|
-
if (leafOnly && !isLeafCommand(info)) continue;
|
|
1409
|
-
const displayName = cmdPath || info.name;
|
|
1410
|
-
const anchor = generateAnchor(displayName);
|
|
1411
|
-
const desc = escapeTableCell(info.description ?? "");
|
|
1412
|
-
lines.push(`| [${displayName}](${category.docPath}#${anchor}) | ${desc} |`);
|
|
1413
|
-
}
|
|
1414
|
-
return lines.join("\n");
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* Render command index from categories
|
|
1418
|
-
*
|
|
1419
|
-
* Generates a category-based index of commands with links to documentation.
|
|
1420
|
-
*
|
|
1421
|
-
* @example
|
|
1422
|
-
* const categories: CommandCategory[] = [
|
|
1423
|
-
* {
|
|
1424
|
-
* title: "Application Commands",
|
|
1425
|
-
* description: "Commands for managing applications.",
|
|
1426
|
-
* commands: ["init", "generate", "apply"],
|
|
1427
|
-
* docPath: "./cli/application.md",
|
|
1428
|
-
* },
|
|
1429
|
-
* ];
|
|
1430
|
-
*
|
|
1431
|
-
* const index = await renderCommandIndex(mainCommand, categories);
|
|
1432
|
-
* // ### [Application Commands](./cli/application.md)
|
|
1433
|
-
* //
|
|
1434
|
-
* // Commands for managing applications.
|
|
1435
|
-
* //
|
|
1436
|
-
* // | Command | Description |
|
|
1437
|
-
* // |---------|-------------|
|
|
1438
|
-
* // | [init](./cli/application.md#init) | Initialize a project |
|
|
1439
|
-
* // ...
|
|
1440
|
-
*
|
|
1441
|
-
* @param command - Root command to extract command information from
|
|
1442
|
-
* @param categories - Category definitions for grouping commands
|
|
1443
|
-
* @param options - Rendering options
|
|
1444
|
-
* @returns Rendered markdown string
|
|
1445
|
-
*/
|
|
1446
|
-
async function renderCommandIndex(command, categories, options) {
|
|
1447
|
-
const headingLevel = options?.headingLevel ?? 3;
|
|
1448
|
-
const leafOnly = options?.leafOnly ?? true;
|
|
1449
|
-
const allCommands = await collectAllCommands(command);
|
|
1450
|
-
const sections = [];
|
|
1451
|
-
for (const category of categories) {
|
|
1452
|
-
const section = renderCategory(category, allCommands, headingLevel, leafOnly);
|
|
1453
|
-
sections.push(section);
|
|
1454
|
-
}
|
|
1455
|
-
return sections.join("\n\n");
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
1890
|
//#endregion
|
|
1459
1891
|
exports.COMMAND_MARKER_PREFIX = COMMAND_MARKER_PREFIX;
|
|
1892
|
+
exports.GLOBAL_OPTIONS_MARKER_PREFIX = GLOBAL_OPTIONS_MARKER_PREFIX;
|
|
1893
|
+
exports.INDEX_MARKER_PREFIX = INDEX_MARKER_PREFIX;
|
|
1460
1894
|
exports.UPDATE_GOLDEN_ENV = UPDATE_GOLDEN_ENV;
|
|
1461
1895
|
exports.__exportAll = __exportAll;
|
|
1462
1896
|
exports.__toESM = __toESM;
|
|
@@ -1471,6 +1905,10 @@ exports.defaultRenderers = defaultRenderers;
|
|
|
1471
1905
|
exports.executeExamples = executeExamples;
|
|
1472
1906
|
exports.formatDiff = formatDiff;
|
|
1473
1907
|
exports.generateDoc = generateDoc;
|
|
1908
|
+
exports.globalOptionsEndMarker = globalOptionsEndMarker;
|
|
1909
|
+
exports.globalOptionsStartMarker = globalOptionsStartMarker;
|
|
1910
|
+
exports.indexEndMarker = indexEndMarker;
|
|
1911
|
+
exports.indexStartMarker = indexStartMarker;
|
|
1474
1912
|
exports.initDocFile = initDocFile;
|
|
1475
1913
|
exports.renderArgsTable = renderArgsTable;
|
|
1476
1914
|
exports.renderArgumentsList = renderArgumentsList;
|