@tronsfey/openapi2cli 1.0.10
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/LICENSE +21 -0
- package/README.md +173 -0
- package/README.zh.md +173 -0
- package/bin/openapi2cli +2 -0
- package/dist/analyzer/schema-analyzer.d.ts +4 -0
- package/dist/analyzer/schema-analyzer.d.ts.map +1 -0
- package/dist/analyzer/schema-analyzer.js +329 -0
- package/dist/analyzer/schema-analyzer.js.map +1 -0
- package/dist/auth/auth-provider.d.ts +22 -0
- package/dist/auth/auth-provider.d.ts.map +1 -0
- package/dist/auth/auth-provider.js +100 -0
- package/dist/auth/auth-provider.js.map +1 -0
- package/dist/generator/command-generator.d.ts +3 -0
- package/dist/generator/command-generator.d.ts.map +1 -0
- package/dist/generator/command-generator.js +96 -0
- package/dist/generator/command-generator.js.map +1 -0
- package/dist/generator/template-engine.d.ts +2 -0
- package/dist/generator/template-engine.d.ts.map +1 -0
- package/dist/generator/template-engine.js +154 -0
- package/dist/generator/template-engine.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/oas-parser.d.ts +3 -0
- package/dist/parser/oas-parser.d.ts.map +1 -0
- package/dist/parser/oas-parser.js +64 -0
- package/dist/parser/oas-parser.js.map +1 -0
- package/dist/templates/README.md.hbs +197 -0
- package/dist/templates/README.zh.md.hbs +197 -0
- package/dist/templates/SKILL.md.hbs +134 -0
- package/dist/templates/api-client.ts.hbs +217 -0
- package/dist/templates/command.ts.hbs +130 -0
- package/dist/templates/flat-commands.ts.hbs +126 -0
- package/dist/templates/index.ts.hbs +38 -0
- package/dist/templates/package.json.hbs +31 -0
- package/dist/templates/tsconfig.json.hbs +16 -0
- package/dist/types/index.d.ts +104 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +88 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
[English](./README.md) | 中文
|
|
2
|
+
|
|
3
|
+
# {{structure.name}}
|
|
4
|
+
|
|
5
|
+
{{structure.description}}
|
|
6
|
+
|
|
7
|
+
| | |
|
|
8
|
+
|---|---|
|
|
9
|
+
| **Base URL** | `{{structure.baseUrl}}` |
|
|
10
|
+
| **版本** | `{{structure.version}}` |
|
|
11
|
+
| **认证方式** | {{#eq structure.authConfig.type "bearer"}}Bearer Token(`{{structure.authConfig.envVar}}`){{/eq}}{{#eq structure.authConfig.type "apiKey"}}API Key(`{{structure.authConfig.envVar}}`){{/eq}}{{#eq structure.authConfig.type "basic"}}HTTP Basic 认证(`{{structure.authConfig.envVar}}`){{/eq}}{{#eq structure.authConfig.type "none"}}无需认证{{/eq}} |
|
|
12
|
+
|
|
13
|
+
## 快速开始
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install && npm run build && npm link
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
{{#eq structure.authConfig.type "bearer"}}
|
|
20
|
+
```bash
|
|
21
|
+
export {{structure.authConfig.envVar}}=your-token-here
|
|
22
|
+
```
|
|
23
|
+
{{/eq}}
|
|
24
|
+
{{#eq structure.authConfig.type "apiKey"}}
|
|
25
|
+
```bash
|
|
26
|
+
export {{structure.authConfig.envVar}}=your-api-key
|
|
27
|
+
```
|
|
28
|
+
{{/eq}}
|
|
29
|
+
{{#eq structure.authConfig.type "basic"}}
|
|
30
|
+
```bash
|
|
31
|
+
export {{structure.authConfig.envVar}}=用户名:密码
|
|
32
|
+
```
|
|
33
|
+
{{/eq}}
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
$ {{structure.name}} --help
|
|
37
|
+
Usage: {{structure.name}} [options] [command]
|
|
38
|
+
|
|
39
|
+
{{structure.description}}
|
|
40
|
+
|
|
41
|
+
Commands:
|
|
42
|
+
{{#each structure.groups}} {{pad name 26}}{{description}}
|
|
43
|
+
{{/each}}{{#each structure.flatCommands}} {{pad name 26}}{{description}}
|
|
44
|
+
{{/each}}
|
|
45
|
+
Options:
|
|
46
|
+
--endpoint <url> 覆盖 API 基础地址(默认:\"{{structure.baseUrl}}\")
|
|
47
|
+
--format <format> 输出格式(可选:\"json\"、\"yaml\"、\"table\",默认:\"json\")
|
|
48
|
+
--verbose 开启请求详细日志
|
|
49
|
+
-V, --version 显示版本号
|
|
50
|
+
-h, --help 显示帮助信息
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 命令列表
|
|
56
|
+
|
|
57
|
+
{{#each structure.groups}}
|
|
58
|
+
### `{{../structure.name}} {{name}}`
|
|
59
|
+
|
|
60
|
+
{{description}}
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
$ {{../../structure.name}} {{name}} --help
|
|
64
|
+
Usage: {{../../structure.name}} {{name}} [options] [command]
|
|
65
|
+
|
|
66
|
+
{{description}}
|
|
67
|
+
|
|
68
|
+
Commands:
|
|
69
|
+
{{#each subcommands}} {{pad name 26}}{{description}}
|
|
70
|
+
{{/each}}
|
|
71
|
+
Options:
|
|
72
|
+
-h, --help 显示帮助信息
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
{{#each subcommands}}
|
|
76
|
+
#### `{{../../structure.name}} {{../name}} {{name}}`
|
|
77
|
+
|
|
78
|
+
{{description}} — `{{uppercase method}} {{path}}`
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
$ {{../../../structure.name}} {{../../name}} {{name}} --help
|
|
82
|
+
Usage: {{../../../structure.name}} {{../../name}} {{name}} [options]
|
|
83
|
+
|
|
84
|
+
{{description}}
|
|
85
|
+
|
|
86
|
+
Options:
|
|
87
|
+
{{#each options}} {{pad (optionFlag name required type) 28}}{{description}}{{#if defaultValue}}(默认:{{defaultValue}}){{/if}}{{#if enum}}(可选值:{{join enum "、"}}){{/if}}
|
|
88
|
+
{{/each}} -h, --help 显示帮助信息
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**示例:**
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
$ {{../../../structure.name}} {{../../name}} {{name}}{{#each options}}{{#if required}} --{{name}} <{{name}}>{{/if}}{{/each}}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
{{#if (sampleResponseJson this)}}
|
|
98
|
+
**响应示例:**
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{{sampleResponseJson this}}
|
|
102
|
+
```
|
|
103
|
+
{{/if}}
|
|
104
|
+
{{#if (subcommandHelpText this)}}
|
|
105
|
+
**数据结构:**
|
|
106
|
+
```
|
|
107
|
+
{{subcommandHelpText this}}
|
|
108
|
+
```
|
|
109
|
+
{{/if}}
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
{{/each}}
|
|
113
|
+
{{/each}}
|
|
114
|
+
{{#if (hasItems structure.flatCommands)}}
|
|
115
|
+
## 顶层命令
|
|
116
|
+
|
|
117
|
+
未分组的操作直接注册到 CLI 根命令。
|
|
118
|
+
|
|
119
|
+
{{#each structure.flatCommands}}
|
|
120
|
+
### `{{../structure.name}} {{name}}`
|
|
121
|
+
|
|
122
|
+
{{description}} — `{{uppercase method}} {{path}}`
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
$ {{../../structure.name}} {{name}} --help
|
|
126
|
+
Usage: {{../../structure.name}} {{name}} [options]
|
|
127
|
+
|
|
128
|
+
{{description}}
|
|
129
|
+
|
|
130
|
+
Options:
|
|
131
|
+
{{#each options}} {{pad (optionFlag name required type) 28}}{{description}}{{#if defaultValue}}(默认:{{defaultValue}}){{/if}}{{#if enum}}(可选值:{{join enum "、"}}){{/if}}
|
|
132
|
+
{{/each}} -h, --help 显示帮助信息
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**示例:**
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
$ {{../../structure.name}} {{name}}{{#each options}}{{#if required}} --{{name}} <{{name}}>{{/if}}{{/each}}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
{{#if (sampleResponseJson this)}}
|
|
142
|
+
**响应示例:**
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{{sampleResponseJson this}}
|
|
146
|
+
```
|
|
147
|
+
{{/if}}
|
|
148
|
+
{{#if (subcommandHelpText this)}}
|
|
149
|
+
**数据结构:**
|
|
150
|
+
```
|
|
151
|
+
{{subcommandHelpText this}}
|
|
152
|
+
```
|
|
153
|
+
{{/if}}
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
{{/each}}
|
|
157
|
+
{{/if}}
|
|
158
|
+
|
|
159
|
+
## 全局选项
|
|
160
|
+
|
|
161
|
+
| 选项 | 说明 | 默认值 |
|
|
162
|
+
|------|------|--------|
|
|
163
|
+
| `--endpoint <url>` | 覆盖 API 基础地址 | `{{structure.baseUrl}}` |
|
|
164
|
+
| `--format <format>` | 输出格式 | `json`(可选:`json`、`yaml`、`table`) |
|
|
165
|
+
| `--verbose` | 打印每次请求的 HTTP 方法和 URL | `false` |
|
|
166
|
+
|
|
167
|
+
## 请求体
|
|
168
|
+
|
|
169
|
+
对于 `POST`、`PUT`、`PATCH` 命令,使用 `--data` 传入请求体:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# 内联 JSON
|
|
173
|
+
{{structure.name}} <group> <command> --data '{"key": "value"}'
|
|
174
|
+
|
|
175
|
+
# 从文件读取
|
|
176
|
+
{{structure.name}} <group> <command> --data @payload.json
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 输出格式
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# 默认:格式化 JSON
|
|
183
|
+
{{structure.name}} <group> <command> [options]
|
|
184
|
+
|
|
185
|
+
# 表格视图(适合数组类型数据)
|
|
186
|
+
{{structure.name}} <group> <command> [options] --format table
|
|
187
|
+
|
|
188
|
+
# 配合 jq 过滤
|
|
189
|
+
{{structure.name}} <group> <command> [options] | jq '.items[]'
|
|
190
|
+
|
|
191
|
+
# 保存到文件
|
|
192
|
+
{{structure.name}} <group> <command> [options] > result.json
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
_由 [openapi2cli](https://github.com/tronsfey928/openapi2cli) 根据 OpenAPI 规范 v{{structure.version}} 生成。_
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: {{structure.name}}
|
|
3
|
+
description: >-
|
|
4
|
+
Use this skill to interact with the {{structure.name}} API from the command
|
|
5
|
+
line. Invoke when the user wants to call API operations, retrieve or mutate
|
|
6
|
+
data, or automate requests against {{structure.baseUrl}}.
|
|
7
|
+
Available command groups: {{#each structure.groups}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}{{#if (hasItems structure.flatCommands)}}{{#if structure.groups}}; {{/if}}top-level: {{#each structure.flatCommands}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}{{/if}}.
|
|
8
|
+
allowed-tools: Bash({{structure.name}} *)
|
|
9
|
+
argument-hint: "[describe the API call you want to make]"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# {{structure.name}}
|
|
13
|
+
|
|
14
|
+
{{structure.description}}
|
|
15
|
+
|
|
16
|
+
**Base URL:** `{{structure.baseUrl}}`
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install && npm run build && npm link
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
{{#eq structure.authConfig.type "bearer"}}
|
|
25
|
+
```bash
|
|
26
|
+
export {{structure.authConfig.envVar}}=<bearer-token>
|
|
27
|
+
```
|
|
28
|
+
{{/eq}}
|
|
29
|
+
{{#eq structure.authConfig.type "apiKey"}}
|
|
30
|
+
```bash
|
|
31
|
+
export {{structure.authConfig.envVar}}=<api-key>
|
|
32
|
+
```
|
|
33
|
+
{{/eq}}
|
|
34
|
+
{{#eq structure.authConfig.type "basic"}}
|
|
35
|
+
```bash
|
|
36
|
+
export {{structure.authConfig.envVar}}=<username:password>
|
|
37
|
+
```
|
|
38
|
+
{{/eq}}
|
|
39
|
+
|
|
40
|
+
## Available Commands
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
$ {{structure.name}} --help
|
|
44
|
+
Usage: {{structure.name}} [options] [command]
|
|
45
|
+
|
|
46
|
+
Commands:
|
|
47
|
+
{{#each structure.groups}} {{pad name 26}}{{description}}
|
|
48
|
+
{{/each}}{{#each structure.flatCommands}} {{pad name 26}}{{description}}
|
|
49
|
+
{{/each}}
|
|
50
|
+
Options:
|
|
51
|
+
--endpoint <url> Override base URL (default: "{{structure.baseUrl}}")
|
|
52
|
+
--format <format> Output format: json|yaml|table (default: "json")
|
|
53
|
+
--verbose Log HTTP method and URL before each request
|
|
54
|
+
-h, --help display help for command
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
{{#each structure.groups}}
|
|
58
|
+
### `{{../structure.name}} {{name}}` — {{description}}
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
$ {{../../structure.name}} {{name}} --help
|
|
62
|
+
Usage: {{../../structure.name}} {{name}} [options] [command]
|
|
63
|
+
|
|
64
|
+
Commands:
|
|
65
|
+
{{#each subcommands}} {{pad name 26}}{{description}}
|
|
66
|
+
{{/each}}```
|
|
67
|
+
|
|
68
|
+
{{#each subcommands}}
|
|
69
|
+
**`{{../../structure.name}} {{../name}} {{name}}`** — {{description}}
|
|
70
|
+
|
|
71
|
+
`{{uppercase method}} {{path}}`
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
$ {{../../../structure.name}} {{../../name}} {{name}}{{#each options}}{{#if required}} --{{name}} <{{name}}>{{/if}}{{/each}}{{#each options}}{{#unless required}}{{#eq type "boolean"}} --{{name}}{{else}} [--{{name}} <value>]{{/eq}}{{/unless}}{{/each}}
|
|
75
|
+
```
|
|
76
|
+
{{#if (sampleResponseJson this)}}
|
|
77
|
+
```json
|
|
78
|
+
{{sampleResponseJson this}}
|
|
79
|
+
```
|
|
80
|
+
{{/if}}
|
|
81
|
+
{{#if (subcommandHelpText this)}}
|
|
82
|
+
```
|
|
83
|
+
{{subcommandHelpText this}}
|
|
84
|
+
```
|
|
85
|
+
{{/if}}
|
|
86
|
+
|
|
87
|
+
{{/each}}
|
|
88
|
+
{{/each}}
|
|
89
|
+
{{#if (hasItems structure.flatCommands)}}
|
|
90
|
+
### Top-Level Commands
|
|
91
|
+
|
|
92
|
+
{{#each structure.flatCommands}}
|
|
93
|
+
**`{{../structure.name}} {{name}}`** — {{description}}
|
|
94
|
+
|
|
95
|
+
`{{uppercase method}} {{path}}`
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
$ {{../../structure.name}} {{name}}{{#each options}}{{#if required}} --{{name}} <{{name}}>{{/if}}{{/each}}
|
|
99
|
+
```
|
|
100
|
+
{{#if (sampleResponseJson this)}}
|
|
101
|
+
```json
|
|
102
|
+
{{sampleResponseJson this}}
|
|
103
|
+
```
|
|
104
|
+
{{/if}}
|
|
105
|
+
{{#if (subcommandHelpText this)}}
|
|
106
|
+
```
|
|
107
|
+
{{subcommandHelpText this}}
|
|
108
|
+
```
|
|
109
|
+
{{/if}}
|
|
110
|
+
|
|
111
|
+
{{/each}}
|
|
112
|
+
{{/if}}
|
|
113
|
+
|
|
114
|
+
## Common Patterns
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# List with table view
|
|
118
|
+
{{structure.name}} <group> <command> --format table
|
|
119
|
+
|
|
120
|
+
# POST / PUT / PATCH with inline JSON body
|
|
121
|
+
{{structure.name}} <group> <command> --data '{"field": "value"}'
|
|
122
|
+
|
|
123
|
+
# POST / PUT / PATCH with body from file
|
|
124
|
+
{{structure.name}} <group> <command> --data @payload.json
|
|
125
|
+
|
|
126
|
+
# Filter response with jq
|
|
127
|
+
{{structure.name}} <group> <command> | jq '.[] | select(.status == "active")'
|
|
128
|
+
|
|
129
|
+
# Override endpoint (e.g. staging)
|
|
130
|
+
{{structure.name}} <group> <command> --endpoint https://staging.example.com
|
|
131
|
+
|
|
132
|
+
# Verbose: log request method + URL
|
|
133
|
+
{{structure.name}} <group> <command> --verbose
|
|
134
|
+
```
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
|
4
|
+
|
|
5
|
+
// Available auth schemes: {{join structure.allAuthSchemes ", "}}
|
|
6
|
+
|
|
7
|
+
// ─── Dynamic token helpers ────────────────────────────────────────────────────
|
|
8
|
+
{{#eq structure.authConfig.type "oauth2-cc"}}
|
|
9
|
+
/**
|
|
10
|
+
* OAuth2 Client Credentials token exchange (RFC 6749 §4.4).
|
|
11
|
+
* Reads {{structure.authConfig.clientIdEnvVar}} and {{structure.authConfig.clientSecretEnvVar}}
|
|
12
|
+
* from the environment, calls the token endpoint, and caches the result for
|
|
13
|
+
* the lifetime of the process.
|
|
14
|
+
*/
|
|
15
|
+
let _oauth2Token: string | null = null;
|
|
16
|
+
async function _getOAuth2Token(): Promise<string | null> {
|
|
17
|
+
if (_oauth2Token) return _oauth2Token;
|
|
18
|
+
const clientId = process.env[{{jsString structure.authConfig.clientIdEnvVar}}];
|
|
19
|
+
const clientSecret = process.env[{{jsString structure.authConfig.clientSecretEnvVar}}];
|
|
20
|
+
if (!clientId || !clientSecret) return null;
|
|
21
|
+
const body = new URLSearchParams({
|
|
22
|
+
grant_type: 'client_credentials',
|
|
23
|
+
client_id: clientId,
|
|
24
|
+
client_secret: clientSecret,
|
|
25
|
+
});
|
|
26
|
+
const scopes = process.env[{{jsString structure.authConfig.scopesEnvVar}}];
|
|
27
|
+
if (scopes) body.set('scope', scopes);
|
|
28
|
+
const resp = await axios.post({{jsString structure.authConfig.tokenUrl}}, body.toString(), {
|
|
29
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
30
|
+
});
|
|
31
|
+
_oauth2Token = (resp.data as { access_token: string }).access_token;
|
|
32
|
+
return _oauth2Token;
|
|
33
|
+
}
|
|
34
|
+
{{/eq}}
|
|
35
|
+
{{#eq structure.authConfig.type "dynamic"}}
|
|
36
|
+
/**
|
|
37
|
+
* Custom dynamic token provider.
|
|
38
|
+
* Reads env vars, sends them as a JSON body to the token endpoint,
|
|
39
|
+
* and caches the result for the lifetime of the process.
|
|
40
|
+
* Token endpoint: {{structure.authConfig.tokenUrl}}
|
|
41
|
+
*/
|
|
42
|
+
let _dynamicToken: string | null = null;
|
|
43
|
+
async function _getDynamicToken(): Promise<string | null> {
|
|
44
|
+
if (_dynamicToken) return _dynamicToken;
|
|
45
|
+
const tokenBody: Record<string, string> = {};
|
|
46
|
+
{{#each structure.authConfig.tokenEnvVars}}
|
|
47
|
+
const _v{{@index}} = process.env[{{jsString env}}];
|
|
48
|
+
if (_v{{@index}}) tokenBody[{{jsString name}}] = _v{{@index}};
|
|
49
|
+
{{/each}}
|
|
50
|
+
if (!Object.keys(tokenBody).length) return null;
|
|
51
|
+
const resp = await axios.post({{jsString structure.authConfig.tokenUrl}}, tokenBody);
|
|
52
|
+
_dynamicToken = (resp.data as { access_token: string }).access_token;
|
|
53
|
+
return _dynamicToken;
|
|
54
|
+
}
|
|
55
|
+
{{/eq}}
|
|
56
|
+
|
|
57
|
+
// ─── Auth header builder ──────────────────────────────────────────────────────
|
|
58
|
+
async function _getAuthHeaders(): Promise<Record<string, string>> {
|
|
59
|
+
const headers: Record<string, string> = {};
|
|
60
|
+
{{#eq structure.authConfig.type "bearer"}}
|
|
61
|
+
const token = process.env[{{jsString structure.authConfig.envVar}}];
|
|
62
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
63
|
+
{{/eq}}
|
|
64
|
+
{{#eq structure.authConfig.type "apiKey"}}
|
|
65
|
+
const key = process.env[{{jsString structure.authConfig.envVar}}];
|
|
66
|
+
if (key) headers[{{jsString structure.authConfig.headerName}}] = key;
|
|
67
|
+
{{/eq}}
|
|
68
|
+
{{#eq structure.authConfig.type "basic"}}
|
|
69
|
+
const credentials = process.env[{{jsString structure.authConfig.envVar}}];
|
|
70
|
+
if (credentials) headers['Authorization'] = `Basic ${Buffer.from(credentials).toString('base64')}`;
|
|
71
|
+
{{/eq}}
|
|
72
|
+
{{#eq structure.authConfig.type "oauth2-cc"}}
|
|
73
|
+
const token = await _getOAuth2Token();
|
|
74
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
75
|
+
{{/eq}}
|
|
76
|
+
{{#eq structure.authConfig.type "dynamic"}}
|
|
77
|
+
const token = await _getDynamicToken();
|
|
78
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
79
|
+
{{/eq}}
|
|
80
|
+
return headers;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── URL builder ──────────────────────────────────────────────────────────────
|
|
84
|
+
function _buildUrl(
|
|
85
|
+
path: string,
|
|
86
|
+
params: Record<string, string | undefined>
|
|
87
|
+
): { url: string; queryParams: Record<string, string> } {
|
|
88
|
+
const mutableParams = { ...params };
|
|
89
|
+
const url = path.replace(/\{(\w+)\}/g, (_, key: string) => {
|
|
90
|
+
const val = mutableParams[key];
|
|
91
|
+
if (!val) throw new Error(`Missing required path parameter: ${key}`);
|
|
92
|
+
delete mutableParams[key];
|
|
93
|
+
return encodeURIComponent(val);
|
|
94
|
+
});
|
|
95
|
+
const queryParams = Object.fromEntries(
|
|
96
|
+
Object.entries(mutableParams).filter(([, v]) => v !== undefined)
|
|
97
|
+
) as Record<string, string>;
|
|
98
|
+
return { url, queryParams };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Link header parser (RFC 5988) ───────────────────────────────────────────
|
|
102
|
+
function _parseLinkNext(linkHeader: string | undefined): string | null {
|
|
103
|
+
if (!linkHeader) return null;
|
|
104
|
+
for (const part of linkHeader.split(',')) {
|
|
105
|
+
const match = part.match(/<([^>]+)>;\s*rel="next"/);
|
|
106
|
+
if (match) return match[1];
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── API client factory ───────────────────────────────────────────────────────
|
|
112
|
+
interface RequestOptions {
|
|
113
|
+
method: string;
|
|
114
|
+
path: string;
|
|
115
|
+
params: Record<string, string | undefined>;
|
|
116
|
+
body?: unknown;
|
|
117
|
+
verbose?: boolean;
|
|
118
|
+
allPages?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createApiClient(baseURL: string): {
|
|
122
|
+
request: (opts: RequestOptions) => Promise<unknown>;
|
|
123
|
+
requestStream: (opts: RequestOptions) => AsyncGenerator<string, void, unknown>;
|
|
124
|
+
} {
|
|
125
|
+
const instance: AxiosInstance = axios.create({ baseURL, proxy: false });
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
async request({ method, path, params, body, verbose, allPages }: RequestOptions): Promise<unknown> {
|
|
129
|
+
const { url, queryParams } = _buildUrl(path, params);
|
|
130
|
+
const authHeaders = await _getAuthHeaders();
|
|
131
|
+
|
|
132
|
+
const config: AxiosRequestConfig = {
|
|
133
|
+
method,
|
|
134
|
+
url,
|
|
135
|
+
params: Object.keys(queryParams).length ? queryParams : undefined,
|
|
136
|
+
data: body,
|
|
137
|
+
headers: authHeaders,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (verbose) {
|
|
141
|
+
console.error(chalk.dim('→'), chalk.bold(method.toUpperCase()), baseURL + url);
|
|
142
|
+
if (body !== undefined) console.error(chalk.dim(' body:'), JSON.stringify(body));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (allPages) {
|
|
146
|
+
const results: unknown[] = [];
|
|
147
|
+
let nextUrl: string | null = url;
|
|
148
|
+
let isFirst = true;
|
|
149
|
+
while (nextUrl) {
|
|
150
|
+
const pageConfig: AxiosRequestConfig = isFirst
|
|
151
|
+
? { ...config }
|
|
152
|
+
: { method, url: nextUrl, headers: authHeaders };
|
|
153
|
+
isFirst = false;
|
|
154
|
+
const response: AxiosResponse = await instance.request(pageConfig);
|
|
155
|
+
if (verbose) {
|
|
156
|
+
console.error(chalk.dim('←'), chalk.bold(String(response.status)), response.statusText);
|
|
157
|
+
}
|
|
158
|
+
const data = response.data;
|
|
159
|
+
if (Array.isArray(data)) {
|
|
160
|
+
results.push(...data);
|
|
161
|
+
} else {
|
|
162
|
+
results.push(data);
|
|
163
|
+
}
|
|
164
|
+
nextUrl = _parseLinkNext(response.headers['link'] as string | undefined);
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const response: AxiosResponse = await instance.request(config);
|
|
170
|
+
if (verbose) {
|
|
171
|
+
console.error(chalk.dim('←'), chalk.bold(String(response.status)), response.statusText);
|
|
172
|
+
}
|
|
173
|
+
return response.data;
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Consume a Server-Sent Events stream.
|
|
178
|
+
* Uses eventsource-parser (Node.js-native) via the Web Streams API.
|
|
179
|
+
* Yields each non-empty data payload. "[DONE]" sentinels are silently dropped.
|
|
180
|
+
* Requires Node.js ≥18 for the built-in fetch API.
|
|
181
|
+
*/
|
|
182
|
+
async *requestStream({ method, path, params, body, verbose }: RequestOptions): AsyncGenerator<string, void, unknown> {
|
|
183
|
+
const { url, queryParams } = _buildUrl(path, params);
|
|
184
|
+
const qs = new URLSearchParams(queryParams).toString();
|
|
185
|
+
const fullUrl = baseURL + url + (qs ? `?${qs}` : '');
|
|
186
|
+
const authHeaders = await _getAuthHeaders();
|
|
187
|
+
|
|
188
|
+
if (verbose) {
|
|
189
|
+
console.error(chalk.dim('→ SSE'), chalk.bold(method.toUpperCase()), fullUrl);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const response = await fetch(fullUrl, {
|
|
193
|
+
method,
|
|
194
|
+
headers: { 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache', ...authHeaders },
|
|
195
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
199
|
+
if (!response.body) return;
|
|
200
|
+
|
|
201
|
+
if (verbose) {
|
|
202
|
+
console.error(chalk.dim('← SSE'), chalk.bold(String(response.status)), response.statusText);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Pipe: Uint8Array → decoded text → parsed SSE events
|
|
206
|
+
const eventStream = response.body
|
|
207
|
+
.pipeThrough(new TextDecoderStream())
|
|
208
|
+
.pipeThrough(new EventSourceParserStream());
|
|
209
|
+
|
|
210
|
+
for await (const event of eventStream) {
|
|
211
|
+
if (event.data && event.data !== '[DONE]') {
|
|
212
|
+
yield event.data;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { createApiClient } from '../lib/api-client';
|
|
4
|
+
|
|
5
|
+
export function register{{pascalCase group.name}}Commands(program: Command): void {
|
|
6
|
+
const {{camelCase group.name}}Cmd = program
|
|
7
|
+
.command({{jsString group.name}})
|
|
8
|
+
.description({{jsString group.description}});
|
|
9
|
+
|
|
10
|
+
{{#each group.subcommands}}
|
|
11
|
+
{{camelCase ../group.name}}Cmd
|
|
12
|
+
.command({{jsString name}})
|
|
13
|
+
.description({{jsString description}})
|
|
14
|
+
{{#each aliases}}
|
|
15
|
+
.alias({{jsString this}})
|
|
16
|
+
{{/each}}
|
|
17
|
+
{{#each options}}
|
|
18
|
+
{{#if enum}}
|
|
19
|
+
.addOption(new Option('{{optionFlag name required type}}', {{jsString description}}){{#if defaultValue}}.default({{jsString defaultValue}}){{/if}}.choices([{{#each enum}}{{jsString this}}{{#unless @last}}, {{/unless}}{{/each}}]))
|
|
20
|
+
{{else}}
|
|
21
|
+
{{#eq type "boolean"}}
|
|
22
|
+
.option('{{optionFlag name required type}}', {{jsString description}})
|
|
23
|
+
{{else}}
|
|
24
|
+
.option('{{optionFlag name required type}}', {{jsString description}}{{#if defaultValue}}, {{jsString defaultValue}}{{/if}})
|
|
25
|
+
{{/eq}}
|
|
26
|
+
{{/if}}
|
|
27
|
+
{{/each}}
|
|
28
|
+
{{#if (subcommandHelpText this)}}
|
|
29
|
+
.addHelpText('after', {{subcommandHelpText this}})
|
|
30
|
+
{{/if}}
|
|
31
|
+
.action(async (opts: Record<string, unknown>, cmd: Command) => {
|
|
32
|
+
const allOpts = cmd.optsWithGlobals<Record<string, unknown>>();
|
|
33
|
+
const endpoint = (allOpts['endpoint'] as string | undefined) ?? {{jsString ../structure.baseUrl}};
|
|
34
|
+
const format = (allOpts['format'] as string | undefined) ?? 'json';
|
|
35
|
+
const verbose = Boolean(allOpts['verbose']);
|
|
36
|
+
const allPages = Boolean(allOpts['allPages']);
|
|
37
|
+
const query = allOpts['query'] as string | undefined;
|
|
38
|
+
const client = createApiClient(endpoint);
|
|
39
|
+
|
|
40
|
+
// Separate body keys from path/query params
|
|
41
|
+
const bodyKeys = new Set<string>([{{#if requestBody}}{{#each requestBody.fields}}'{{optKey}}', {{/each}}'data'{{/if}}]);
|
|
42
|
+
const params: Record<string, string | undefined> = {};
|
|
43
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
44
|
+
if (!bodyKeys.has(k)) params[k] = v as string | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
{{#if requestBody}}
|
|
48
|
+
// Assemble request body
|
|
49
|
+
let body: unknown;
|
|
50
|
+
const rawData = opts['data'] as string | undefined;
|
|
51
|
+
if (rawData) {
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
+
const { readFileSync } = require('fs') as typeof import('fs');
|
|
54
|
+
body = rawData.startsWith('@')
|
|
55
|
+
? JSON.parse(readFileSync(rawData.slice(1), 'utf-8'))
|
|
56
|
+
: JSON.parse(rawData);
|
|
57
|
+
{{#if (hasItems requestBody.fields)}}
|
|
58
|
+
} else {
|
|
59
|
+
const bodyObj: Record<string, unknown> = {};
|
|
60
|
+
{{#each requestBody.fields}}if (opts['{{optKey}}'] !== undefined) bodyObj[{{jsString name}}] = opts['{{optKey}}'];
|
|
61
|
+
{{/each}}if (Object.keys(bodyObj).length) body = bodyObj;
|
|
62
|
+
{{/if}}
|
|
63
|
+
}
|
|
64
|
+
{{/if}}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
{{#eq streaming "sse"}}
|
|
68
|
+
// Server-Sent Events — stream events to stdout, one compact JSON line each
|
|
69
|
+
for await (const eventData of client.requestStream({
|
|
70
|
+
method: '{{method}}',
|
|
71
|
+
path: '{{path}}',
|
|
72
|
+
params,
|
|
73
|
+
verbose,
|
|
74
|
+
})) {
|
|
75
|
+
try {
|
|
76
|
+
let output: unknown = JSON.parse(eventData);
|
|
77
|
+
if (query) {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
79
|
+
const jmespath = require('jmespath') as { search: (d: unknown, e: string) => unknown };
|
|
80
|
+
output = jmespath.search(output, query);
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(JSON.stringify(output) + '\n');
|
|
83
|
+
} catch {
|
|
84
|
+
process.stdout.write(eventData + '\n');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
{{else}}
|
|
88
|
+
const result = await client.request({
|
|
89
|
+
method: '{{method}}',
|
|
90
|
+
path: '{{path}}',
|
|
91
|
+
params,
|
|
92
|
+
{{#if requestBody}}body,{{/if}}
|
|
93
|
+
verbose,
|
|
94
|
+
allPages,
|
|
95
|
+
});
|
|
96
|
+
formatOutput(result, format, query);
|
|
97
|
+
{{/eq}}
|
|
98
|
+
} catch (err: unknown) {
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
100
|
+
console.error(chalk.red('Error:'), message);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
{{/each}}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatOutput(data: unknown, format: string, query?: string): void {
|
|
109
|
+
let output = data;
|
|
110
|
+
if (query) {
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
112
|
+
const jmespath = require('jmespath') as { search: (d: unknown, e: string) => unknown };
|
|
113
|
+
output = jmespath.search(data, query);
|
|
114
|
+
}
|
|
115
|
+
if (format === 'json') {
|
|
116
|
+
console.log(JSON.stringify(output, null, 2));
|
|
117
|
+
} else if (format === 'yaml') {
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
119
|
+
const yaml = require('yaml') as { stringify: (d: unknown) => string };
|
|
120
|
+
process.stdout.write(yaml.stringify(output));
|
|
121
|
+
} else if (format === 'table') {
|
|
122
|
+
if (Array.isArray(output)) {
|
|
123
|
+
console.table(output);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(JSON.stringify(output, null, 2));
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
console.log(JSON.stringify(output, null, 2));
|
|
129
|
+
}
|
|
130
|
+
}
|