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.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
|
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
|
|
965
|
-
if (
|
|
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 =
|
|
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
|
|
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 =
|
|
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 (
|
|
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)
|
|
1136
|
-
path: filePath,
|
|
1137
|
-
status: "match"
|
|
1138
|
-
});
|
|
1139
|
-
else if (updateMode) {
|
|
1746
|
+
if (comparison.match) {} else if (updateMode) {
|
|
1140
1747
|
writeFile(filePath, generatedMarkdown);
|
|
1141
|
-
|
|
1142
|
-
path: filePath,
|
|
1143
|
-
status: comparison.fileExists ? "updated" : "created"
|
|
1144
|
-
});
|
|
1748
|
+
fileStatus = comparison.fileExists ? "updated" : "created";
|
|
1145
1749
|
} else {
|
|
1146
1750
|
hasError = true;
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
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
|