cli-forge 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.eslintrc.json +35 -0
  2. package/LICENSE.md +5 -0
  3. package/README.md +181 -5
  4. package/cli.js +9 -0
  5. package/dist/bin/cli.d.ts +23 -0
  6. package/{bin → dist/bin}/cli.js +2 -2
  7. package/dist/bin/cli.js.map +1 -0
  8. package/dist/bin/commands/generate-documentation.d.ts +14 -0
  9. package/{bin → dist/bin}/commands/generate-documentation.js +58 -11
  10. package/dist/bin/commands/generate-documentation.js.map +1 -0
  11. package/{bin → dist/bin}/commands/init.d.ts +11 -11
  12. package/{bin → dist/bin}/commands/init.js +11 -6
  13. package/dist/bin/commands/init.js.map +1 -0
  14. package/dist/bin/utils/fs.js.map +1 -0
  15. package/{src → dist}/index.d.ts +2 -1
  16. package/dist/index.js.map +1 -0
  17. package/dist/lib/cli-option-groups.js.map +1 -0
  18. package/dist/lib/composable-builder.d.ts +24 -0
  19. package/dist/lib/composable-builder.js +17 -0
  20. package/dist/lib/composable-builder.js.map +1 -0
  21. package/dist/lib/configuration-providers.js.map +1 -0
  22. package/{src → dist}/lib/documentation.d.ts +3 -3
  23. package/dist/lib/documentation.js.map +1 -0
  24. package/dist/lib/format-help.js.map +1 -0
  25. package/{src → dist}/lib/interactive-shell.d.ts +1 -1
  26. package/{src → dist}/lib/interactive-shell.js +6 -3
  27. package/dist/lib/interactive-shell.js.map +1 -0
  28. package/{src → dist}/lib/internal-cli.d.ts +38 -21
  29. package/{src → dist}/lib/internal-cli.js +48 -3
  30. package/dist/lib/internal-cli.js.map +1 -0
  31. package/dist/lib/public-api.d.ts +332 -0
  32. package/dist/lib/public-api.js.map +1 -0
  33. package/dist/lib/test-harness.js.map +1 -0
  34. package/dist/lib/utils.js.map +1 -0
  35. package/dist/middleware/zod.d.ts +4 -0
  36. package/dist/middleware/zod.js +18 -0
  37. package/dist/middleware/zod.js.map +1 -0
  38. package/dist/middleware.d.ts +1 -0
  39. package/dist/middleware.js +5 -0
  40. package/dist/middleware.js.map +1 -0
  41. package/package.json +29 -10
  42. package/project.json +7 -0
  43. package/src/bin/cli.ts +17 -0
  44. package/src/bin/commands/generate-documentation.ts +403 -0
  45. package/src/bin/commands/init.ts +320 -0
  46. package/src/bin/utils/fs.ts +11 -0
  47. package/src/index.ts +12 -0
  48. package/src/lib/cli-option-groups.ts +69 -0
  49. package/src/lib/composable-builder.ts +57 -0
  50. package/src/lib/configuration-providers.ts +36 -0
  51. package/src/lib/documentation.spec.ts +156 -0
  52. package/src/lib/documentation.ts +107 -0
  53. package/src/lib/format-help.ts +149 -0
  54. package/src/lib/interactive-shell.ts +115 -0
  55. package/src/lib/internal-cli.spec.ts +345 -0
  56. package/src/lib/internal-cli.ts +689 -0
  57. package/src/lib/public-api.ts +943 -0
  58. package/src/lib/test-harness.spec.ts +29 -0
  59. package/src/lib/test-harness.ts +69 -0
  60. package/src/lib/utils.spec.ts +25 -0
  61. package/src/lib/utils.ts +144 -0
  62. package/src/middleware/zod.ts +21 -0
  63. package/src/middleware.ts +1 -0
  64. package/tsconfig.json +23 -0
  65. package/tsconfig.lib.json +20 -0
  66. package/tsconfig.lib.json.tsbuildinfo +1 -0
  67. package/tsconfig.spec.json +26 -0
  68. package/vitest.config.mts +18 -0
  69. package/bin/cli.d.ts +0 -6
  70. package/bin/cli.js.map +0 -1
  71. package/bin/commands/generate-documentation.d.ts +0 -14
  72. package/bin/commands/generate-documentation.js.map +0 -1
  73. package/bin/commands/init.js.map +0 -1
  74. package/bin/utils/fs.js.map +0 -1
  75. package/src/index.js.map +0 -1
  76. package/src/lib/cli-option-groups.js.map +0 -1
  77. package/src/lib/composable-builder.d.ts +0 -3
  78. package/src/lib/composable-builder.js +0 -7
  79. package/src/lib/composable-builder.js.map +0 -1
  80. package/src/lib/configuration-providers.js.map +0 -1
  81. package/src/lib/documentation.js.map +0 -1
  82. package/src/lib/format-help.js.map +0 -1
  83. package/src/lib/interactive-shell.js.map +0 -1
  84. package/src/lib/internal-cli.js.map +0 -1
  85. package/src/lib/public-api.d.ts +0 -215
  86. package/src/lib/public-api.js.map +0 -1
  87. package/src/lib/test-harness.js.map +0 -1
  88. package/src/lib/utils.js.map +0 -1
  89. /package/{bin → dist/bin}/utils/fs.d.ts +0 -0
  90. /package/{bin → dist/bin}/utils/fs.js +0 -0
  91. /package/{src → dist}/index.js +0 -0
  92. /package/{src → dist}/lib/cli-option-groups.d.ts +0 -0
  93. /package/{src → dist}/lib/cli-option-groups.js +0 -0
  94. /package/{src → dist}/lib/configuration-providers.d.ts +0 -0
  95. /package/{src → dist}/lib/configuration-providers.js +0 -0
  96. /package/{src → dist}/lib/documentation.js +0 -0
  97. /package/{src → dist}/lib/format-help.d.ts +0 -0
  98. /package/{src → dist}/lib/format-help.js +0 -0
  99. /package/{src → dist}/lib/public-api.js +0 -0
  100. /package/{src → dist}/lib/test-harness.d.ts +0 -0
  101. /package/{src → dist}/lib/test-harness.js +0 -0
  102. /package/{src → dist}/lib/utils.d.ts +0 -0
  103. /package/{src → dist}/lib/utils.js +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-harness.js","sourceRoot":"","sources":["../../src/lib/test-harness.ts"],"names":[],"mappings":";;;AACA,iDAA6C;AA8B7C;;;GAGG;AACH,MAAa,WAAW;IACd,GAAG,CAAiB;IAE5B,YAAY,GAAW;QACrB,IAAI,GAAG,YAAY,0BAAW,EAAE,CAAC;YAC/B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;YACf,WAAW,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CACb,mEAAmE,CACpE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAc;QACxB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAExC,OAAO;YACL,IAAI,EAAE,IAAI;YACV,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY;SACpC,CAAC;IACJ,CAAC;CACF;AAtBD,kCAsBC;AAED,SAAS,WAAW,CAAC,GAAgB;IACnC,IAAI,GAAG,CAAC,aAAa,EAAE,OAAO,EAAE,CAAC;QAC/B,GAAG,CAAC,aAAa,CAAC,OAAO,GAAG,GAAG,EAAE;YAC/B,6BAA6B;QAC/B,CAAC,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,kBAAkB,EAAE,CAAC;QAC7C,WAAW,CAAC,GAAG,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { ParsedArgs } from '@cli-forge/parser';
2
+ import { MiddlewareFunction } from '../lib/public-api';
3
+ import type { z, ZodObject, ZodPipe } from 'zod';
4
+ export declare function zodMiddleware<TArgs extends ParsedArgs, TSchema extends ZodObject | ZodPipe<ZodObject>>(schema: TSchema): MiddlewareFunction<TArgs, TArgs & z.infer<TSchema>>;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.zodMiddleware = zodMiddleware;
4
+ /*
5
+ * Middleware that uses a Zod schema to validate and transform command arguments.
6
+ * @param schema The Zod schema to use for validation and transformation.
7
+ * @returns A middleware function that applies the Zod schema to the command arguments.
8
+ */
9
+ function zodMiddleware(schema) {
10
+ return async (args) => {
11
+ const parsed = (await schema.parseAsync(args));
12
+ if (typeof parsed !== 'object') {
13
+ throw new Error('Zod schema did not return an object');
14
+ }
15
+ return { ...args, ...parsed };
16
+ };
17
+ }
18
+ //# sourceMappingURL=zod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod.js","sourceRoot":"","sources":["../../src/middleware/zod.ts"],"names":[],"mappings":";;AASA,sCAWC;AAhBD;;;;GAIG;AACH,SAAgB,aAAa,CAG3B,MAAe;IACf,OAAO,KAAK,EAAE,IAAW,EAAE,EAAE;QAC3B,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAqB,CAAC;QACnE,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC;IAChC,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ export * from './middleware/zod';
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./middleware/zod"), exports);
5
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":";;;AAAA,2DAAiC"}
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "cli-forge",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "dependencies": {
5
5
  "tslib": "^2.3.0",
6
- "@cli-forge/parser": "0.10.1"
6
+ "@cli-forge/parser": "0.12.0"
7
7
  },
8
8
  "peerDependencies": {
9
- "markdown-factory": "0.2.0",
10
- "tsx": "4.19.0"
9
+ "markdown-factory": "^0.2.0",
10
+ "tsx": "^4.19.0",
11
+ "zod": "^4.1.13"
11
12
  },
12
13
  "peerDependenciesMeta": {
13
14
  "markdown-factory": {
@@ -17,22 +18,40 @@
17
18
  "tsx": {
18
19
  "optional": true,
19
20
  "dev": true
21
+ },
22
+ "zod": {
23
+ "optional": true
20
24
  }
21
25
  },
22
26
  "type": "commonjs",
23
- "main": "./src/index.js",
24
- "typings": "./src/index.d.ts",
27
+ "main": "./dist/index.js",
28
+ "typings": "./dist/index.d.ts",
25
29
  "license": "ISC",
30
+ "homepage": "https://craigory.dev/cli-forge/",
26
31
  "repository": {
27
32
  "type": "git",
28
33
  "directory": "packages/cli-forge",
29
34
  "url": "https://github.com/AgentEnder/cli-forge"
30
35
  },
31
36
  "bin": {
32
- "cli-forge": "./bin/cli.js"
37
+ "cli-forge": "./cli.js"
38
+ },
39
+ "exports": {
40
+ ".": {
41
+ "require": "./dist/index.js",
42
+ "types": "./dist/index.d.ts"
43
+ },
44
+ "./middleware": {
45
+ "require": "./dist/middleware.js",
46
+ "types": "./dist/middleware.d.ts"
47
+ },
48
+ "./middleware/*": {
49
+ "require": "./dist/middleware/*.js",
50
+ "types": "./dist/middleware/*.d.ts"
51
+ },
52
+ "./package.json": "./package.json"
33
53
  },
34
54
  "publishConfig": {
35
55
  "access": "public"
36
- },
37
- "types": "./src/index.d.ts"
38
- }
56
+ }
57
+ }
package/project.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "cli-forge",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/cli-forge/src",
5
+ "projectType": "library",
6
+ "tags": []
7
+ }
package/src/bin/cli.ts ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cli } from '../lib/public-api';
4
+ import { generateDocumentationCommand } from './commands/generate-documentation';
5
+ import { initCommand } from './commands/init';
6
+
7
+ const mycli = cli('cli-forge', {
8
+ description: "CLI tool for working with cli-forge based CLI's.",
9
+ }).commands(generateDocumentationCommand, initCommand);
10
+
11
+ export default mycli;
12
+
13
+ if (require.main === module) {
14
+ (async () => {
15
+ await mycli.forge();
16
+ })();
17
+ }
@@ -0,0 +1,403 @@
1
+ import type { ParsedArgs } from '@cli-forge/parser';
2
+
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { dirname, isAbsolute, join, relative } from 'node:path';
5
+ import { join as joinPathFragments, normalize } from 'node:path/posix';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ import cli, { ArgumentsOf, CLI } from '../..';
9
+ import { Documentation, generateDocumentation } from '../../lib/documentation';
10
+ import { ensureDirSync } from '../utils/fs';
11
+ import { InternalCLI } from '../../lib/internal-cli';
12
+
13
+ type mdfactory = typeof import('markdown-factory');
14
+
15
+ type GenerateDocsArgs = ArgumentsOf<typeof withGenerateDocumentationArgs>;
16
+
17
+ export function withGenerateDocumentationArgs<T extends ParsedArgs>(
18
+ cmd: CLI<T>
19
+ ) {
20
+ return cmd
21
+ .positional('cli', {
22
+ type: 'string',
23
+ description: 'Path to the cli that docs should be generated for.',
24
+ required: true,
25
+ })
26
+ .option('output', {
27
+ alias: ['o'],
28
+ type: 'string',
29
+ description: 'Where should the documentation be output?',
30
+ default: 'docs',
31
+ })
32
+ .option('format', {
33
+ type: 'string',
34
+ description: 'What format should the documentation be output in?',
35
+ default: 'md',
36
+ choices: ['json', 'md'],
37
+ })
38
+ .option('export', {
39
+ type: 'string',
40
+ description:
41
+ 'The name of the export that contains the CLI instance. By default, docs will be generated for the default export.',
42
+ })
43
+ .option('tsconfig', {
44
+ type: 'string',
45
+ description:
46
+ 'Specifies the `tsconfig` used when loading typescript based CLIs.',
47
+ });
48
+ }
49
+
50
+ export const generateDocumentationCommand: CLI<any, any, any> = cli('generate-documentation', {
51
+ description: 'Generate documentation for the given CLI',
52
+ examples: [
53
+ 'cli-forge generate-documentation ./bin/my-cli',
54
+ 'cli-forge generate-documentation ./bin/my-cli --format json',
55
+ 'cli-forge generate-documentation ./bin/my-cli --export mycli',
56
+ ],
57
+ builder: (b) => withGenerateDocumentationArgs(b),
58
+ handler: async (args) => {
59
+ const cliModule = await loadCLIModule(args);
60
+ const cli = readCLIFromModule(cliModule, args);
61
+
62
+ const documentation = generateDocumentation(cli);
63
+ if (args.format === 'md') {
64
+ await generateMarkdownDocumentation(documentation, args);
65
+ } else if (args.format === 'json') {
66
+ const outfile = args.output.endsWith('json')
67
+ ? args.output
68
+ : join(args.output, cli.name + '.json');
69
+ const outdir = dirname(outfile);
70
+ ensureDirSync(outdir);
71
+ writeFileSync(outfile, JSON.stringify(documentation, null, 2));
72
+ }
73
+ },
74
+ });
75
+
76
+ async function generateMarkdownDocumentation(
77
+ docs: Documentation,
78
+ args: GenerateDocsArgs
79
+ ) {
80
+ const md = await importMarkdownFactory();
81
+ await generateMarkdownForSingleCommand(docs, args.output, args.output, md);
82
+ }
83
+
84
+ async function generateMarkdownForSingleCommand(
85
+ docs: Documentation,
86
+ out: string,
87
+ docsRoot: string,
88
+ md: mdfactory
89
+ ) {
90
+ const subcommands = docs.subcommands;
91
+ const outdir = subcommands.length ? out : dirname(out);
92
+ const outname = subcommands.length ? 'index' : docs.name;
93
+
94
+ ensureDirSync(outdir);
95
+
96
+ writeFileSync(
97
+ join(outdir, outname + '.md'),
98
+ md.h1(
99
+ docs.name,
100
+ ...[
101
+ [md.bold('Usage:'), md.code(docs.usage)].join(' '),
102
+ docs.description,
103
+ getPositionalArgsFragment(docs.positionals, md),
104
+ getFlagArgsFragment(docs.options, 'Flags', md),
105
+ ...docs.groupedOptions.map((group) =>
106
+ getFlagArgsFragment(
107
+ Object.fromEntries(group.keys.map((key) => [key.key, key])),
108
+ group.label,
109
+ md
110
+ )
111
+ ),
112
+ getSubcommandsFragment(docs.subcommands, outdir, docsRoot, md),
113
+ getExamplesFragment(docs.examples, md),
114
+ getEpilogueFragment(docs.epilogue, md),
115
+ ].filter(isTruthy)
116
+ )
117
+ );
118
+ for (const subcommand of docs.subcommands) {
119
+ await generateMarkdownForSingleCommand(
120
+ subcommand,
121
+ join(outdir, subcommand.name),
122
+ docsRoot,
123
+ md
124
+ );
125
+ }
126
+ }
127
+
128
+ function formatOption(option: Documentation['options'][string], md: mdfactory) {
129
+ return md.h3(
130
+ option.deprecated ? md.strikethrough(option.key) : option.key,
131
+ ...[
132
+ option.deprecated ? md.bold(md.italics('Deprecated')) : undefined,
133
+ md.bold('Type:') +
134
+ ' ' +
135
+ ('items' in option && option.type === 'array'
136
+ ? `${option.items}[]`
137
+ : option.type),
138
+ option.description,
139
+ option.default !== undefined
140
+ ? renderDefaultValueSection(option.default, md)
141
+ : undefined,
142
+ // No need to show required if it's required and has a default, as its not actually required to pass.
143
+ option.required && !option.default ? md.bold('Required') : undefined,
144
+ 'choices' in option && option.choices
145
+ ? md.bold('Valid values:') +
146
+ ' ' +
147
+ (() => {
148
+ const choicesAsString = (
149
+ typeof option.choices === 'function'
150
+ ? option.choices()
151
+ : option.choices
152
+ ).map((t: any) => md.code(t.toString()));
153
+ return choicesAsString.join(', ');
154
+ })()
155
+ : undefined,
156
+ option.alias?.length
157
+ ? md.h4('Aliases', md.ul(...option.alias))
158
+ : undefined,
159
+ ].filter(isTruthy)
160
+ );
161
+ }
162
+
163
+ function getPositionalArgsFragment(
164
+ positionals: Documentation['positionals'],
165
+ md: mdfactory
166
+ ) {
167
+ if (positionals?.length === 0) {
168
+ return undefined;
169
+ }
170
+ return md.h2(
171
+ 'Positional Arguments',
172
+ ...positionals.map((positional) => formatOption(positional, md))
173
+ );
174
+ }
175
+
176
+ function getFlagArgsFragment(
177
+ options: Documentation['options'],
178
+ label: string,
179
+ md: mdfactory
180
+ ) {
181
+ if (Object.keys(options).length === 0) {
182
+ return undefined;
183
+ }
184
+ return md.h2(
185
+ label,
186
+ ...Object.values(options).map((option) => formatOption(option, md))
187
+ );
188
+ }
189
+
190
+ function getSubcommandsFragment(
191
+ subcommands: Documentation['subcommands'],
192
+ outdir: string,
193
+ docsRoot: string,
194
+ md: mdfactory
195
+ ) {
196
+ if (subcommands.length === 0) {
197
+ return undefined;
198
+ }
199
+ return md.h2(
200
+ 'Subcommands',
201
+ ...subcommands.map((subcommand) =>
202
+ md.link(
203
+ './' +
204
+ joinPathFragments(
205
+ normalize(relative(docsRoot, outdir)),
206
+ subcommand.name + '.md'
207
+ ),
208
+ subcommand.name
209
+ )
210
+ )
211
+ );
212
+ }
213
+
214
+ function isTruthy<T>(value: T | undefined | null): value is T {
215
+ return !!value;
216
+ }
217
+
218
+ async function importMarkdownFactory(): Promise<mdfactory> {
219
+ try {
220
+ return await import('markdown-factory');
221
+ } catch {
222
+ throw new Error(
223
+ 'Could not find markdown-factory. Please install it to generate markdown documentation.'
224
+ );
225
+ }
226
+ }
227
+
228
+ function isCLI(obj: unknown): obj is InternalCLI {
229
+ if (obj instanceof InternalCLI) {
230
+ return true;
231
+ }
232
+ if (typeof obj !== 'object' || !obj) {
233
+ return false;
234
+ }
235
+ if (!('constructor' in obj)) {
236
+ return false;
237
+ }
238
+ if (!('name' in obj.constructor)) {
239
+ return false;
240
+ }
241
+ return obj.constructor.name === InternalCLI.name;
242
+ }
243
+
244
+ function getExamplesFragment(
245
+ examples: string[],
246
+ md: typeof import('markdown-factory')
247
+ ): string | undefined {
248
+ if (examples.length === 0) {
249
+ return undefined;
250
+ }
251
+ return md.h2(
252
+ 'Examples',
253
+ ...examples.map((example) => md.codeBlock(example, 'shell'))
254
+ );
255
+ }
256
+
257
+ /**
258
+ * Reads the CLI instance from the provided module. For some reason,
259
+ * when importing a module that uses `export default` in typescript
260
+ * the default export is nested under a `default` property on the module...
261
+ * We for code that looks like `export default 5`, on import we get back
262
+ * `{ default: {default: 5}}`. To work around this, and make things work
263
+ * we try to read the CLI instance from the module in a few different ways.
264
+ *
265
+ * @param cliModule The imported module
266
+ * @param exportSpecifier The name of the export to read the CLI from. Defaults to `default`.
267
+ * @returns A CLI instance.
268
+ */
269
+ function readCLIFromModule(
270
+ cliModule: any,
271
+ args: GenerateDocsArgs
272
+ ): InternalCLI {
273
+ let cli = cliModule;
274
+ if (args.export) {
275
+ cli = cliModule[args.export] ?? cliModule['default']?.[args.export];
276
+ } else {
277
+ cli =
278
+ cliModule['default']?.['default'] ?? cliModule['default'] ?? cliModule;
279
+ }
280
+ if (!isCLI(cli)) {
281
+ throw new Error(
282
+ `${args.cli}${args.export ? '#' + args.export : ''} is not a CLI.`
283
+ );
284
+ }
285
+ return cli;
286
+ }
287
+
288
+ /**
289
+ * Detects whether a file should be loaded as ESM or CJS.
290
+ * Checks file extension first (.mjs/.mts = ESM, .cjs/.cts = CJS),
291
+ * then falls back to the nearest package.json's "type" field.
292
+ */
293
+ function detectModuleType(filePath: string): 'esm' | 'cjs' {
294
+ const ext = filePath.split('.').pop()?.toLowerCase();
295
+
296
+ // Explicit extensions take precedence
297
+ if (ext === 'mjs' || ext === 'mts') {
298
+ return 'esm';
299
+ }
300
+ if (ext === 'cjs' || ext === 'cts') {
301
+ return 'cjs';
302
+ }
303
+
304
+ // Find nearest package.json and check "type" field
305
+ const absolutePath = isAbsolute(filePath)
306
+ ? filePath
307
+ : join(process.cwd(), filePath);
308
+ let dir = dirname(absolutePath);
309
+ const root = dirname(dir);
310
+
311
+ while (dir !== root) {
312
+ const pkgPath = join(dir, 'package.json');
313
+ if (existsSync(pkgPath)) {
314
+ try {
315
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
316
+ return pkg.type === 'module' ? 'esm' : 'cjs';
317
+ } catch {
318
+ // Ignore parse errors, continue searching
319
+ }
320
+ }
321
+ dir = dirname(dir);
322
+ }
323
+
324
+ // Default to CJS (Node.js default)
325
+ return 'cjs';
326
+ }
327
+
328
+ async function loadCLIModule(
329
+ args: ArgumentsOf<typeof withGenerateDocumentationArgs>
330
+ ) {
331
+ if (isAbsolute(args.cli)) {
332
+ args.cli = relative(process.cwd(), args.cli);
333
+ }
334
+
335
+ const cliPath = [
336
+ args.cli,
337
+ `${args.cli}.ts`,
338
+ `${args.cli}.js`,
339
+ `${args.cli}.cjs`,
340
+ `${args.cli}.mjs`,
341
+ join(args.cli, 'index.ts'),
342
+ join(args.cli, 'index.js'),
343
+ join(args.cli, 'index.cjs'),
344
+ join(args.cli, 'index.mjs'),
345
+ ].find((f) => {
346
+ const p = isAbsolute(f) ? f : join(process.cwd(), f);
347
+ console.log('Checking for CLI at', p);
348
+ return existsSync(p);
349
+ });
350
+
351
+ if (!cliPath) {
352
+ throw new Error(`Could not find CLI module at ${args.cli}
353
+
354
+ Ensure that the path is correct and that the CLI module exists.`);
355
+ }
356
+
357
+ const moduleType = detectModuleType(cliPath);
358
+
359
+ try {
360
+ if (moduleType === 'esm') {
361
+ const tsx = (await import('tsx/esm/api')) as typeof import('tsx/esm/api');
362
+ return tsx.tsImport(cliPath, {
363
+ tsconfig: args.tsconfig,
364
+ parentURL: pathToFileURL(
365
+ join(process.cwd(), 'fake-file-for-import.ts')
366
+ ).toString(),
367
+ });
368
+ } else {
369
+ const tsx = (await import('tsx/cjs/api')) as typeof import('tsx/cjs/api');
370
+ return tsx.require(cliPath, join(process.cwd(), 'fake-file-for-require.ts'));
371
+ }
372
+ } catch {
373
+ try {
374
+ return await import(cliPath);
375
+ } catch (e) {
376
+ if (cliPath.endsWith('.ts')) {
377
+ console.warn(
378
+ '[cli-forge]: Generating docs for a typescript CLI requires installing `tsx` as a dev dependency, targeting the build artifacts instead, or otherwise registering a typescript loader with node.'
379
+ );
380
+ }
381
+ throw e;
382
+ }
383
+ }
384
+ }
385
+
386
+ function getEpilogueFragment(
387
+ epilogue: string | undefined,
388
+ md: typeof import('markdown-factory')
389
+ ) {
390
+ if (!epilogue) {
391
+ return undefined;
392
+ }
393
+ return md.blockQuote(epilogue);
394
+ }
395
+
396
+ function renderDefaultValueSection(defaultValue: unknown, md: mdfactory) {
397
+ const json = JSON.stringify(defaultValue, null, 2);
398
+ if (json.split('\n').length > 1) {
399
+ return md.lines(md.bold('Default:'), md.codeBlock(json, 'json'));
400
+ } else {
401
+ return md.bold('Default:') + ' ' + md.code(json);
402
+ }
403
+ }