openapi-agent-reference 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +28 -0
- package/LICENSE +21 -0
- package/README.md +143 -25
- package/bin/openapi-md.js +5 -0
- package/package.json +29 -7
- package/src/cli.js +167 -0
- package/src/index.js +17 -0
- package/src/openapi-to-markdown.js +107 -83
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. The project follows [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 2.0.0 - 2026-07-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `openapi-md` executable and `groups` / `generate` subcommands.
|
|
12
|
+
- JSON group discovery with `groups --json`.
|
|
13
|
+
- Standard input and standard output support through `-`.
|
|
14
|
+
- Public CommonJS library entry point.
|
|
15
|
+
- Cross-platform CLI integration tests and npm publishing safeguards.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The installed CLI command is now `openapi-md`.
|
|
20
|
+
- The minimum supported Node.js version is now 22.
|
|
21
|
+
|
|
22
|
+
## 1.0.2
|
|
23
|
+
|
|
24
|
+
- Added exact primary-tag group filtering and group discovery.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Development
|
|
4
|
+
|
|
5
|
+
Use Node.js 22 or newer.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm ci
|
|
9
|
+
npm test
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Run the CLI directly while developing:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
node bin/openapi-md.js groups openapi.json
|
|
16
|
+
node bin/openapi-md.js generate openapi.json --group "Access" --output -
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Pull requests
|
|
20
|
+
|
|
21
|
+
- Add or update tests for behavior changes.
|
|
22
|
+
- Keep the CLI backward compatible unless the change is intentionally released as a new major version.
|
|
23
|
+
- Update `README.md` and `CHANGELOG.md` for user-visible changes.
|
|
24
|
+
- Run `npm pack --dry-run` and inspect the publish file list before releasing.
|
|
25
|
+
|
|
26
|
+
## Releases
|
|
27
|
+
|
|
28
|
+
Use Semantic Versioning. Publishing is performed by the GitHub Actions release workflow through npm trusted publishing; do not add npm access tokens to the repository.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dikong1
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,43 +1,161 @@
|
|
|
1
|
-
# OpenAPI Agent Reference
|
|
1
|
+
# OpenAPI Agent Reference
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate compact, navigable Markdown API references from OpenAPI JSON. The output is designed for developers and AI agents and includes authentication, parameters, request and response bodies, examples, and reusable schemas.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Requirements
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- operation IDs, authentication requirements, deprecation status, and descriptions;
|
|
9
|
-
- path, query, header, and cookie parameters with constraints;
|
|
10
|
-
- request bodies and response bodies for every documented media type;
|
|
11
|
-
- explicit and schema-generated JSON examples;
|
|
12
|
-
- response headers and OpenAPI links;
|
|
13
|
-
- OAuth, bearer, API-key, and other security scheme details;
|
|
14
|
-
- a reusable schema catalog with required fields and `$ref` links;
|
|
15
|
-
- OpenAPI 3.x support plus practical Swagger 2.0 compatibility.
|
|
7
|
+
Node.js 22 or newer. The package has no runtime dependencies.
|
|
16
8
|
|
|
17
|
-
##
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
Run the published package without installing it:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx openapi-agent-reference@latest groups openapi.json
|
|
15
|
+
npx openapi-agent-reference@latest generate openapi.json --group "Equipment Service" -o EQUIPMENT_SERVICE.md
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or install it in a project:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install --save-dev openapi-agent-reference
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The short command is then available through `npx` and npm scripts:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx openapi-md groups openapi.json
|
|
28
|
+
npx openapi-md generate openapi.json -o API_REFERENCE.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CLI
|
|
32
|
+
|
|
33
|
+
### Discover groups
|
|
34
|
+
|
|
35
|
+
Groups are primary OpenAPI operation tags. List their exact, case-sensitive names before generating a focused reference:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
openapi-md groups openapi.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Example output:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
GROUP OPERATIONS
|
|
45
|
+
------------------------ ----------
|
|
46
|
+
Access 9
|
|
47
|
+
Equipment Service 49
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For machine-readable output:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
openapi-md groups openapi.json --json
|
|
54
|
+
```
|
|
18
55
|
|
|
19
|
-
|
|
56
|
+
```json
|
|
57
|
+
[
|
|
58
|
+
{ "name": "Access", "operations": 9 },
|
|
59
|
+
{ "name": "Equipment Service", "operations": 49 }
|
|
60
|
+
]
|
|
61
|
+
```
|
|
20
62
|
|
|
21
|
-
|
|
63
|
+
### Generate one group
|
|
22
64
|
|
|
23
65
|
```bash
|
|
24
|
-
|
|
66
|
+
openapi-md generate openapi.json --group "Access" --output ACCESS.md
|
|
25
67
|
```
|
|
26
68
|
|
|
27
|
-
|
|
69
|
+
The result contains only the selected group's endpoints and the reusable schemas they reference, including transitive schema dependencies. A misspelled group produces a close-match suggestion and the complete list of valid groups.
|
|
70
|
+
|
|
71
|
+
### Generate the complete reference
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
openapi-md generate openapi.json --output API_REFERENCE.md
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Pipelines and standard streams
|
|
78
|
+
|
|
79
|
+
Use `-` for standard input or standard output:
|
|
28
80
|
|
|
29
81
|
```bash
|
|
30
|
-
|
|
82
|
+
curl https://example.com/openapi.json | openapi-md groups - --json
|
|
83
|
+
openapi-md generate openapi.json --group Users --output -
|
|
31
84
|
```
|
|
32
85
|
|
|
33
|
-
|
|
86
|
+
Diagnostics are written to stderr, so Markdown and JSON output can be piped safely.
|
|
87
|
+
|
|
88
|
+
### Options
|
|
34
89
|
|
|
35
90
|
```text
|
|
36
|
-
-o, --output <file
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
-
|
|
91
|
+
-o, --output <file|-> Output file, or - for stdout
|
|
92
|
+
-g, --group <name> Generate only the exact, case-sensitive group
|
|
93
|
+
--json JSON output for the groups command
|
|
94
|
+
--no-schema-catalog Omit reusable schemas
|
|
95
|
+
--no-examples Omit generated JSON examples
|
|
96
|
+
--include-extensions Include x-* operation extensions
|
|
97
|
+
-v, --version Show the installed version
|
|
98
|
+
-h, --help Show CLI help
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The original flag-based syntax remains available for backward compatibility:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
openapi-md openapi.json --list-groups
|
|
105
|
+
openapi-md openapi.json --group Access -o ACCESS.md
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Programmatic API
|
|
109
|
+
|
|
110
|
+
The CommonJS entry point exposes the generator and OpenAPI inspection helpers:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
const fs = require('node:fs');
|
|
114
|
+
const {
|
|
115
|
+
generateMarkdown,
|
|
116
|
+
groupList,
|
|
117
|
+
} = require('openapi-agent-reference');
|
|
118
|
+
|
|
119
|
+
const spec = JSON.parse(fs.readFileSync('openapi.json', 'utf8'));
|
|
120
|
+
|
|
121
|
+
console.log(groupList(spec));
|
|
122
|
+
|
|
123
|
+
const markdown = generateMarkdown(spec, {
|
|
124
|
+
group: 'Access',
|
|
125
|
+
examples: true,
|
|
126
|
+
schemaCatalog: true,
|
|
127
|
+
});
|
|
41
128
|
```
|
|
42
129
|
|
|
43
|
-
|
|
130
|
+
Public exports:
|
|
131
|
+
|
|
132
|
+
- `generateMarkdown(spec, options)`
|
|
133
|
+
- `groupList(spec)`
|
|
134
|
+
- `collectOperations(spec)`
|
|
135
|
+
- `formatGroupList(spec)`
|
|
136
|
+
- `referencedSchemaNames(groups, spec)`
|
|
137
|
+
- `sampleForSchema(schema, spec)`
|
|
138
|
+
|
|
139
|
+
## Generated reference content
|
|
140
|
+
|
|
141
|
+
- Endpoints grouped by their primary OpenAPI tag
|
|
142
|
+
- Operation IDs, descriptions, authentication, and deprecation state
|
|
143
|
+
- Path, query, header, and cookie parameters with constraints
|
|
144
|
+
- Request and response bodies for every documented media type
|
|
145
|
+
- Explicit and schema-generated JSON examples
|
|
146
|
+
- Response headers and OpenAPI links
|
|
147
|
+
- OAuth, bearer, API-key, and other security scheme details
|
|
148
|
+
- Linked reusable schemas with required fields and recursive-reference protection
|
|
149
|
+
- OpenAPI 3.x support and practical Swagger 2.0 compatibility
|
|
150
|
+
|
|
151
|
+
Local `$ref` values are resolved and linked. External `$ref` URLs are preserved but are not downloaded.
|
|
152
|
+
|
|
153
|
+
## Development and releases
|
|
154
|
+
|
|
155
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development and release guidance. Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
|
|
156
|
+
|
|
157
|
+
Publishing uses npm trusted publishing from GitHub Actions. Before the first automated release, configure `npm-publish.yml` as the trusted publisher in the package settings on npmjs.com.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openapi-agent-reference",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Generate agent-friendly Markdown API reference documentation from OpenAPI JSON.",
|
|
5
5
|
"type": "commonjs",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
6
11
|
"bin": {
|
|
7
|
-
"openapi-
|
|
12
|
+
"openapi-md": "bin/openapi-md.js"
|
|
8
13
|
},
|
|
9
14
|
"scripts": {
|
|
10
|
-
"generate": "node
|
|
11
|
-
"
|
|
15
|
+
"generate": "node bin/openapi-md.js generate openapi.json -o API_REFERENCE.md",
|
|
16
|
+
"groups": "node bin/openapi-md.js groups openapi.json",
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"prepublishOnly": "npm test"
|
|
12
19
|
},
|
|
13
20
|
"engines": {
|
|
14
|
-
"node": ">=
|
|
21
|
+
"node": ">=22"
|
|
15
22
|
},
|
|
16
23
|
"license": "MIT",
|
|
17
24
|
"repository": {
|
|
@@ -23,7 +30,22 @@
|
|
|
23
30
|
"url": "https://github.com/Dikong1/extractOpenApi/issues"
|
|
24
31
|
},
|
|
25
32
|
"files": [
|
|
33
|
+
"bin/",
|
|
26
34
|
"src/",
|
|
27
|
-
"README.md"
|
|
28
|
-
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"CONTRIBUTING.md"
|
|
39
|
+
],
|
|
40
|
+
"keywords": [
|
|
41
|
+
"openapi",
|
|
42
|
+
"swagger",
|
|
43
|
+
"markdown",
|
|
44
|
+
"api-documentation",
|
|
45
|
+
"ai-agent",
|
|
46
|
+
"cli"
|
|
47
|
+
],
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
29
51
|
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const packageJson = require('../package.json');
|
|
4
|
+
const {
|
|
5
|
+
collectOperations,
|
|
6
|
+
formatGroupList,
|
|
7
|
+
generateMarkdown,
|
|
8
|
+
groupList,
|
|
9
|
+
} = require('./openapi-to-markdown');
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
return `Usage:
|
|
13
|
+
openapi-md groups [openapi.json|-] [--json]
|
|
14
|
+
openapi-md generate [openapi.json|-] [options]
|
|
15
|
+
openapi-md [openapi.json|-] [options] Legacy syntax
|
|
16
|
+
|
|
17
|
+
Commands:
|
|
18
|
+
groups List exact primary-tag group names and operation counts
|
|
19
|
+
generate Generate a full or single-group Markdown reference
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-o, --output <file|-> Output Markdown file, or - for stdout (default: API_REFERENCE.md)
|
|
23
|
+
-g, --group <name> Generate only the exact, case-sensitive group
|
|
24
|
+
--json Emit machine-readable JSON from the groups command
|
|
25
|
+
--list-groups Legacy alias for the groups command
|
|
26
|
+
--no-schema-catalog Omit the reusable schema catalog
|
|
27
|
+
--no-examples Omit generated JSON examples
|
|
28
|
+
--include-extensions Include x-* operation extensions
|
|
29
|
+
-v, --version Show the package version
|
|
30
|
+
-h, --help Show this help
|
|
31
|
+
|
|
32
|
+
Use - as the input path to read OpenAPI JSON from stdin.
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requiredValue(argv, index, option) {
|
|
37
|
+
const value = argv[index + 1];
|
|
38
|
+
if (value === undefined || value.startsWith('-')) throw new Error(`${option} requires a value`);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const args = [...argv];
|
|
44
|
+
let command = 'generate';
|
|
45
|
+
if (args[0] === 'groups' || args[0] === 'generate') command = args.shift();
|
|
46
|
+
|
|
47
|
+
const options = {
|
|
48
|
+
command,
|
|
49
|
+
input: 'openapi.json',
|
|
50
|
+
output: 'API_REFERENCE.md',
|
|
51
|
+
schemaCatalog: true,
|
|
52
|
+
examples: true,
|
|
53
|
+
includeExtensions: false,
|
|
54
|
+
group: undefined,
|
|
55
|
+
json: false,
|
|
56
|
+
};
|
|
57
|
+
let inputSet = false;
|
|
58
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
59
|
+
const arg = args[index];
|
|
60
|
+
if (arg === '-h' || arg === '--help') options.help = true;
|
|
61
|
+
else if (arg === '-v' || arg === '--version') options.version = true;
|
|
62
|
+
else if (arg === '--list-groups') options.command = 'groups';
|
|
63
|
+
else if (arg === '--json') options.json = true;
|
|
64
|
+
else if (arg === '--no-schema-catalog') options.schemaCatalog = false;
|
|
65
|
+
else if (arg === '--no-examples') options.examples = false;
|
|
66
|
+
else if (arg === '--include-extensions') options.includeExtensions = true;
|
|
67
|
+
else if (arg === '-g' || arg === '--group') {
|
|
68
|
+
options.group = requiredValue(args, index, arg);
|
|
69
|
+
index += 1;
|
|
70
|
+
} else if (arg === '-o' || arg === '--output') {
|
|
71
|
+
const value = args[index + 1];
|
|
72
|
+
if (value === undefined) throw new Error(`${arg} requires a value`);
|
|
73
|
+
options.output = value;
|
|
74
|
+
index += 1;
|
|
75
|
+
} else if (arg === '-' && !inputSet) {
|
|
76
|
+
options.input = arg;
|
|
77
|
+
inputSet = true;
|
|
78
|
+
} else if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}`);
|
|
79
|
+
else if (!inputSet) {
|
|
80
|
+
options.input = arg;
|
|
81
|
+
inputSet = true;
|
|
82
|
+
} else throw new Error(`Unexpected argument: ${arg}`);
|
|
83
|
+
}
|
|
84
|
+
if (options.command === 'groups' && options.group !== undefined) {
|
|
85
|
+
throw new Error('The groups command cannot be combined with --group');
|
|
86
|
+
}
|
|
87
|
+
if (options.command !== 'groups' && options.json) {
|
|
88
|
+
throw new Error('--json is only available with the groups command');
|
|
89
|
+
}
|
|
90
|
+
return options;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readSpec(input) {
|
|
94
|
+
const label = input === '-' ? 'stdin' : path.resolve(input);
|
|
95
|
+
const source = fs.readFileSync(input === '-' ? 0 : label, 'utf8').replace(/^\uFEFF/, '');
|
|
96
|
+
let spec;
|
|
97
|
+
try { spec = JSON.parse(source); } catch (error) { throw new Error(`Invalid JSON in ${label}: ${error.message}`); }
|
|
98
|
+
if (!spec || typeof spec !== 'object' || (!spec.openapi && !spec.swagger) || !spec.paths || typeof spec.paths !== 'object') {
|
|
99
|
+
throw new Error('Input is not a valid OpenAPI document with a paths object');
|
|
100
|
+
}
|
|
101
|
+
return spec;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function writeFileSafely(outputPath, content) {
|
|
105
|
+
const absolutePath = path.resolve(outputPath);
|
|
106
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
107
|
+
const temporaryPath = path.join(
|
|
108
|
+
path.dirname(absolutePath),
|
|
109
|
+
`.${path.basename(absolutePath)}.${process.pid}.${Date.now()}.tmp`,
|
|
110
|
+
);
|
|
111
|
+
fs.writeFileSync(temporaryPath, content, 'utf8');
|
|
112
|
+
try {
|
|
113
|
+
fs.renameSync(temporaryPath, absolutePath);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (!['EEXIST', 'EPERM'].includes(error.code)) throw error;
|
|
116
|
+
fs.copyFileSync(temporaryPath, absolutePath);
|
|
117
|
+
fs.unlinkSync(temporaryPath);
|
|
118
|
+
} finally {
|
|
119
|
+
if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
|
|
120
|
+
}
|
|
121
|
+
return absolutePath;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function run(argv = process.argv.slice(2), streams = process) {
|
|
125
|
+
let options;
|
|
126
|
+
try { options = parseArgs(argv); } catch (error) {
|
|
127
|
+
streams.stderr.write(`Error: ${error.message}\n\n${usage()}`);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
if (options.help) {
|
|
131
|
+
streams.stdout.write(usage());
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
if (options.version) {
|
|
135
|
+
streams.stdout.write(`${packageJson.version}\n`);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const spec = readSpec(options.input);
|
|
140
|
+
if (options.command === 'groups') {
|
|
141
|
+
const output = options.json ? `${JSON.stringify(groupList(spec), null, 2)}\n` : formatGroupList(spec);
|
|
142
|
+
streams.stdout.write(output);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const markdown = generateMarkdown(spec, options);
|
|
147
|
+
const groups = collectOperations(spec).filter(([name]) => options.group === undefined || name === options.group);
|
|
148
|
+
const operations = groups.reduce((count, [, items]) => count + items.length, 0);
|
|
149
|
+
if (options.output === '-') {
|
|
150
|
+
streams.stdout.write(markdown);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
const outputPath = writeFileSafely(options.output, markdown);
|
|
154
|
+
const scope = options.group === undefined ? '' : ` for group ${JSON.stringify(options.group)}`;
|
|
155
|
+
streams.stdout.write(`Generated ${outputPath}${scope} (${operations} operations, ${Buffer.byteLength(markdown)} bytes)\n`);
|
|
156
|
+
return 0;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
streams.stderr.write(`Error: ${error.message}\n`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function main(argv = process.argv.slice(2)) {
|
|
164
|
+
process.exitCode = run(argv);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { main, parseArgs, readSpec, run, usage, writeFileSafely };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const {
|
|
2
|
+
collectOperations,
|
|
3
|
+
formatGroupList,
|
|
4
|
+
generateMarkdown,
|
|
5
|
+
groupList,
|
|
6
|
+
referencedSchemaNames,
|
|
7
|
+
sampleForSchema,
|
|
8
|
+
} = require('./openapi-to-markdown');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
collectOperations,
|
|
12
|
+
formatGroupList,
|
|
13
|
+
generateMarkdown,
|
|
14
|
+
groupList,
|
|
15
|
+
referencedSchemaNames,
|
|
16
|
+
sampleForSchema,
|
|
17
|
+
};
|
|
@@ -1,49 +1,5 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
|
|
6
1
|
const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
|
|
7
2
|
|
|
8
|
-
function usage() {
|
|
9
|
-
return `Usage: openapi-to-md [openapi.json] [options]
|
|
10
|
-
|
|
11
|
-
Options:
|
|
12
|
-
-o, --output <file> Output Markdown file (default: API_REFERENCE.md)
|
|
13
|
-
--no-schema-catalog Omit the reusable schema catalog
|
|
14
|
-
--no-examples Omit generated JSON examples
|
|
15
|
-
--include-extensions Include x-* operation extensions
|
|
16
|
-
-h, --help Show this help
|
|
17
|
-
`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseArgs(argv) {
|
|
21
|
-
const options = {
|
|
22
|
-
input: 'openapi.json',
|
|
23
|
-
output: 'API_REFERENCE.md',
|
|
24
|
-
schemaCatalog: true,
|
|
25
|
-
examples: true,
|
|
26
|
-
includeExtensions: false,
|
|
27
|
-
};
|
|
28
|
-
let inputSet = false;
|
|
29
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
30
|
-
const arg = argv[index];
|
|
31
|
-
if (arg === '-h' || arg === '--help') options.help = true;
|
|
32
|
-
else if (arg === '--no-schema-catalog') options.schemaCatalog = false;
|
|
33
|
-
else if (arg === '--no-examples') options.examples = false;
|
|
34
|
-
else if (arg === '--include-extensions') options.includeExtensions = true;
|
|
35
|
-
else if (arg === '-o' || arg === '--output') {
|
|
36
|
-
if (!argv[index + 1]) throw new Error(`${arg} requires a file path`);
|
|
37
|
-
options.output = argv[++index];
|
|
38
|
-
} else if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}`);
|
|
39
|
-
else if (!inputSet) {
|
|
40
|
-
options.input = arg;
|
|
41
|
-
inputSet = true;
|
|
42
|
-
} else throw new Error(`Unexpected argument: ${arg}`);
|
|
43
|
-
}
|
|
44
|
-
return options;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
3
|
function escapeTable(value) {
|
|
48
4
|
return String(value ?? '—').replace(/\r?\n/g, '<br>').replace(/\|/g, '\\|');
|
|
49
5
|
}
|
|
@@ -264,6 +220,83 @@ function collectOperations(spec) {
|
|
|
264
220
|
});
|
|
265
221
|
}
|
|
266
222
|
|
|
223
|
+
function formatGroupList(spec) {
|
|
224
|
+
const groups = collectOperations(spec);
|
|
225
|
+
const width = Math.max('GROUP'.length, ...groups.map(([name]) => name.length));
|
|
226
|
+
return [
|
|
227
|
+
`${'GROUP'.padEnd(width)} OPERATIONS`,
|
|
228
|
+
`${'-'.repeat(width)} ----------`,
|
|
229
|
+
...groups.map(([name, operations]) => `${name.padEnd(width)} ${operations.length}`),
|
|
230
|
+
'',
|
|
231
|
+
].join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function groupList(spec) {
|
|
235
|
+
return collectOperations(spec).map(([name, operations]) => ({
|
|
236
|
+
name,
|
|
237
|
+
operations: operations.length,
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function levenshteinDistance(left, right) {
|
|
242
|
+
const a = left.toLocaleLowerCase();
|
|
243
|
+
const b = right.toLocaleLowerCase();
|
|
244
|
+
const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
245
|
+
for (let row = 1; row <= a.length; row += 1) {
|
|
246
|
+
const current = [row];
|
|
247
|
+
for (let column = 1; column <= b.length; column += 1) {
|
|
248
|
+
current[column] = Math.min(
|
|
249
|
+
current[column - 1] + 1,
|
|
250
|
+
previous[column] + 1,
|
|
251
|
+
previous[column - 1] + (a[row - 1] === b[column - 1] ? 0 : 1),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
previous.splice(0, previous.length, ...current);
|
|
255
|
+
}
|
|
256
|
+
return previous[b.length];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function closestGroup(name, groups) {
|
|
260
|
+
if (!groups.length) return undefined;
|
|
261
|
+
const candidates = groups
|
|
262
|
+
.map(([candidate]) => ({ candidate, distance: levenshteinDistance(name, candidate) }))
|
|
263
|
+
.sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate));
|
|
264
|
+
const best = candidates[0];
|
|
265
|
+
const threshold = Math.max(2, Math.floor(Math.max(name.length, best.candidate.length) / 3));
|
|
266
|
+
return best.distance <= threshold ? best.candidate : undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function referencedSchemaNames(groups, spec) {
|
|
270
|
+
const names = new Set();
|
|
271
|
+
const visitedRefs = new Set();
|
|
272
|
+
const visitedObjects = new Set();
|
|
273
|
+
|
|
274
|
+
function visit(value) {
|
|
275
|
+
if (!value || typeof value !== 'object' || visitedObjects.has(value)) return;
|
|
276
|
+
visitedObjects.add(value);
|
|
277
|
+
if (typeof value.$ref === 'string' && value.$ref.startsWith('#/')) {
|
|
278
|
+
const schemaMatch = value.$ref.match(/^#\/(?:components\/schemas|definitions)\/(.+)$/);
|
|
279
|
+
if (schemaMatch) {
|
|
280
|
+
const name = decodeURIComponent(schemaMatch[1].replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
281
|
+
names.add(name);
|
|
282
|
+
}
|
|
283
|
+
if (!visitedRefs.has(value.$ref)) {
|
|
284
|
+
visitedRefs.add(value.$ref);
|
|
285
|
+
visit(resolveLocalRef(spec, value.$ref));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const nested of Object.values(value)) visit(nested);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const [, operations] of groups) {
|
|
292
|
+
for (const { operation, pathItem } of operations) {
|
|
293
|
+
visit(operation);
|
|
294
|
+
visit(pathItem.parameters);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return names;
|
|
298
|
+
}
|
|
299
|
+
|
|
267
300
|
function renderParameters(parameters, spec) {
|
|
268
301
|
if (!parameters.length) return '';
|
|
269
302
|
const lines = ['#### Parameters', '', '| Name | In | Required | Type | Description / constraints |', '|---|---|:---:|---|---|'];
|
|
@@ -418,27 +451,40 @@ function renderSchema(name, schema, spec, options) {
|
|
|
418
451
|
return lines.join('\n');
|
|
419
452
|
}
|
|
420
453
|
|
|
421
|
-
function renderSchemas(spec, options) {
|
|
454
|
+
function renderSchemas(spec, options, includedNames) {
|
|
422
455
|
const schemas = spec.components?.schemas || spec.definitions || {};
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
456
|
+
const entries = Object.entries(schemas)
|
|
457
|
+
.filter(([name]) => !includedNames || includedNames.has(name))
|
|
458
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
459
|
+
if (!options.schemaCatalog || !entries.length) return '';
|
|
460
|
+
const lines = ['## Schema catalog', '', `${entries.length} reusable schema(s).`, ''];
|
|
461
|
+
for (const [name, schema] of entries) {
|
|
426
462
|
lines.push(renderSchema(name, schema, spec, options));
|
|
427
463
|
}
|
|
428
464
|
return lines.join('\n');
|
|
429
465
|
}
|
|
430
466
|
|
|
431
467
|
function generateMarkdown(spec, options = {}) {
|
|
432
|
-
options = { schemaCatalog: true, examples: true, includeExtensions: false, ...options };
|
|
468
|
+
options = { schemaCatalog: true, examples: true, includeExtensions: false, group: undefined, ...options };
|
|
433
469
|
if (!spec || typeof spec !== 'object') throw new Error('The OpenAPI document must be a JSON object');
|
|
434
470
|
if (!spec.openapi && !spec.swagger) throw new Error('Missing required "openapi" or "swagger" version field');
|
|
435
471
|
if (!spec.paths || typeof spec.paths !== 'object') throw new Error('Missing required "paths" object');
|
|
436
472
|
|
|
437
473
|
const title = spec.info?.title || 'API Reference';
|
|
438
|
-
const
|
|
474
|
+
const allGroups = collectOperations(spec);
|
|
475
|
+
let groups = allGroups;
|
|
476
|
+
if (options.group !== undefined) {
|
|
477
|
+
groups = allGroups.filter(([name]) => name === options.group);
|
|
478
|
+
if (!groups.length) {
|
|
479
|
+
const available = allGroups.map(([name]) => name).join(', ') || '(none)';
|
|
480
|
+
const suggestion = closestGroup(options.group, allGroups);
|
|
481
|
+
const hint = suggestion ? ` Did you mean ${JSON.stringify(suggestion)}?` : '';
|
|
482
|
+
throw new Error(`Unknown group ${JSON.stringify(options.group)}.${hint} Available groups: ${available}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
439
485
|
const operationCount = groups.reduce((count, [, operations]) => count + operations.length, 0);
|
|
440
486
|
const lines = [
|
|
441
|
-
`# ${title} API Reference`,
|
|
487
|
+
`# ${title}${options.group !== undefined ? ` — ${options.group}` : ''} API Reference`,
|
|
442
488
|
'',
|
|
443
489
|
'> Generated from the OpenAPI document. Optimized for human readers and AI agents: use operation IDs for tool names, honor required parameters, and validate request/response bodies against the linked schemas.',
|
|
444
490
|
'',
|
|
@@ -477,40 +523,18 @@ function generateMarkdown(spec, options = {}) {
|
|
|
477
523
|
for (const item of operations) lines.push(renderOperation(item, spec, options));
|
|
478
524
|
}
|
|
479
525
|
|
|
480
|
-
const
|
|
526
|
+
const includedSchemas = options.group !== undefined ? referencedSchemaNames(groups, spec) : undefined;
|
|
527
|
+
const schemas = renderSchemas(spec, options, includedSchemas);
|
|
481
528
|
if (schemas) lines.push(schemas);
|
|
482
529
|
lines.push('---', '', '_Generated by openapi-agent-reference. Do not edit manually; regenerate after the OpenAPI document changes._', '');
|
|
483
530
|
return lines.join('\n').replace(/\n{4,}/g, '\n\n\n');
|
|
484
531
|
}
|
|
485
532
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
process.stdout.write(usage());
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
try {
|
|
498
|
-
const inputPath = path.resolve(options.input);
|
|
499
|
-
const outputPath = path.resolve(options.output);
|
|
500
|
-
const source = fs.readFileSync(inputPath, 'utf8').replace(/^\uFEFF/, '');
|
|
501
|
-
let spec;
|
|
502
|
-
try { spec = JSON.parse(source); } catch (error) { throw new Error(`Invalid JSON in ${inputPath}: ${error.message}`); }
|
|
503
|
-
const markdown = generateMarkdown(spec, options);
|
|
504
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
505
|
-
fs.writeFileSync(outputPath, markdown, 'utf8');
|
|
506
|
-
const operations = collectOperations(spec).reduce((count, [, items]) => count + items.length, 0);
|
|
507
|
-
console.log(`Generated ${outputPath} (${operations} operations, ${Buffer.byteLength(markdown)} bytes)`);
|
|
508
|
-
} catch (error) {
|
|
509
|
-
console.error(`Error: ${error.message}`);
|
|
510
|
-
process.exitCode = 1;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (require.main === module) main();
|
|
515
|
-
|
|
516
|
-
module.exports = { generateMarkdown, parseArgs, sampleForSchema, collectOperations };
|
|
533
|
+
module.exports = {
|
|
534
|
+
collectOperations,
|
|
535
|
+
formatGroupList,
|
|
536
|
+
generateMarkdown,
|
|
537
|
+
groupList,
|
|
538
|
+
referencedSchemaNames,
|
|
539
|
+
sampleForSchema,
|
|
540
|
+
};
|