@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/README.zh.md +173 -0
  4. package/bin/openapi2cli +2 -0
  5. package/dist/analyzer/schema-analyzer.d.ts +4 -0
  6. package/dist/analyzer/schema-analyzer.d.ts.map +1 -0
  7. package/dist/analyzer/schema-analyzer.js +329 -0
  8. package/dist/analyzer/schema-analyzer.js.map +1 -0
  9. package/dist/auth/auth-provider.d.ts +22 -0
  10. package/dist/auth/auth-provider.d.ts.map +1 -0
  11. package/dist/auth/auth-provider.js +100 -0
  12. package/dist/auth/auth-provider.js.map +1 -0
  13. package/dist/generator/command-generator.d.ts +3 -0
  14. package/dist/generator/command-generator.d.ts.map +1 -0
  15. package/dist/generator/command-generator.js +96 -0
  16. package/dist/generator/command-generator.js.map +1 -0
  17. package/dist/generator/template-engine.d.ts +2 -0
  18. package/dist/generator/template-engine.d.ts.map +1 -0
  19. package/dist/generator/template-engine.js +154 -0
  20. package/dist/generator/template-engine.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +135 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/parser/oas-parser.d.ts +3 -0
  26. package/dist/parser/oas-parser.d.ts.map +1 -0
  27. package/dist/parser/oas-parser.js +64 -0
  28. package/dist/parser/oas-parser.js.map +1 -0
  29. package/dist/templates/README.md.hbs +197 -0
  30. package/dist/templates/README.zh.md.hbs +197 -0
  31. package/dist/templates/SKILL.md.hbs +134 -0
  32. package/dist/templates/api-client.ts.hbs +217 -0
  33. package/dist/templates/command.ts.hbs +130 -0
  34. package/dist/templates/flat-commands.ts.hbs +126 -0
  35. package/dist/templates/index.ts.hbs +38 -0
  36. package/dist/templates/package.json.hbs +31 -0
  37. package/dist/templates/tsconfig.json.hbs +16 -0
  38. package/dist/types/index.d.ts +104 -0
  39. package/dist/types/index.d.ts.map +1 -0
  40. package/dist/types/index.js +3 -0
  41. package/dist/types/index.js.map +1 -0
  42. 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
+ }