@ultra-network/cli 0.1.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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/cli.cjs +541 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +520 -0
- package/dist/cli.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ultra Network
|
|
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
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @ultra-network/cli
|
|
2
|
+
|
|
3
|
+
A spec-driven command-line client for the [Ultra Network](https://ultranetwork.co) Public API v1.
|
|
4
|
+
|
|
5
|
+
Every operation in `https://ultranetwork.co/api/v1/openapi.json` becomes an `ultra <command>` subcommand. New endpoints become new subcommands automatically — no CLI code change needed.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g @ultra-network/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or run without installing:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx @ultra-network/cli list_trips --limit=5
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
export ULTRA_API_KEY=ulk_…
|
|
23
|
+
ultra --help # list commands grouped by resource
|
|
24
|
+
ultra list_trips --limit=10 # GET /api/v1/trips?limit=10
|
|
25
|
+
ultra get_trip --id=<uuid> # GET /api/v1/trips/{id}
|
|
26
|
+
ultra create_trip --body='{"title":"Demo","client_id":"…"}'
|
|
27
|
+
ultra create_trip --body=@payload.json # read body from file
|
|
28
|
+
cat payload.json | ultra create_trip --body=- # read body from stdin
|
|
29
|
+
ultra list_bookings --trip_id=<uuid> --status # HTTP status on stderr
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Get an API key at [ultranetwork.co/contact](https://ultranetwork.co/contact).
|
|
33
|
+
|
|
34
|
+
## Environment
|
|
35
|
+
|
|
36
|
+
| Var | Default | Purpose |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `ULTRA_API_KEY` | (required) | Bearer key for `/api/v1` |
|
|
39
|
+
| `ULTRA_API_SPEC` | `https://ultranetwork.co/api/v1/openapi.json` | OpenAPI source |
|
|
40
|
+
| `ULTRA_API_BASE_URL` | (from `spec.servers[0].url`) | Override server base URL |
|
|
41
|
+
| `ULTRA_API_TAGS` | (all) | CSV filter — only expose operations matching these tags |
|
|
42
|
+
|
|
43
|
+
## Global flags
|
|
44
|
+
|
|
45
|
+
| Flag | Purpose |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `--spec=<url\|path>` | Override the spec source for one invocation |
|
|
48
|
+
| `--base-url=<url>` | Override the base URL for one invocation |
|
|
49
|
+
| `--status` | Print `HTTP <code>` + `request_id` to stderr |
|
|
50
|
+
| `--raw` | Print response body unchanged (no JSON pretty-print) |
|
|
51
|
+
| `--quiet` | Suppress progress lines on stderr |
|
|
52
|
+
| `--help, -h` | Top-level help, or per-command help when a command is named |
|
|
53
|
+
| `--version, -V` | Print CLI + spec version |
|
|
54
|
+
|
|
55
|
+
## Exit codes
|
|
56
|
+
|
|
57
|
+
| Code | Meaning |
|
|
58
|
+
|---|---|
|
|
59
|
+
| 0 | 2xx response |
|
|
60
|
+
| 1 | 4xx response |
|
|
61
|
+
| 2 | 5xx response or network failure |
|
|
62
|
+
| 3 | Usage / parse error |
|
|
63
|
+
| 4 | Spec load failure |
|
|
64
|
+
|
|
65
|
+
## Architecture
|
|
66
|
+
|
|
67
|
+
Spec-driven dumb-pipe. At boot the CLI fetches the OpenAPI document, walks the paths, and renders one subcommand per operation. The HTTP layer is shared with the [`@ultra-network/mcp`](https://www.npmjs.com/package/@ultra-network/mcp) server so both surfaces stay in lockstep with the live API.
|
|
68
|
+
|
|
69
|
+
## Docs
|
|
70
|
+
|
|
71
|
+
- [Developer docs](https://github.com/timebinder/ultra-developers) — auth, API reference, MCP, errors, pagination
|
|
72
|
+
- [CLI cookbook](https://github.com/timebinder/ultra-developers/blob/main/examples/cli-cookbook.md) — practical recipes
|
|
73
|
+
- [Live API reference](https://ultranetwork.co/api/v1/docs) — interactive Scalar UI
|
|
74
|
+
|
|
75
|
+
## Licence
|
|
76
|
+
|
|
77
|
+
MIT
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/cli.ts
|
|
22
|
+
var cli_exports = {};
|
|
23
|
+
__export(cli_exports, {
|
|
24
|
+
run: () => run
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(cli_exports);
|
|
27
|
+
|
|
28
|
+
// ../ultra-api-client/src/loadSpec.ts
|
|
29
|
+
var import_promises = require("fs/promises");
|
|
30
|
+
async function loadSpec(source) {
|
|
31
|
+
if (/^https?:\/\//.test(source)) {
|
|
32
|
+
const res = await fetch(source);
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new Error(`Failed to fetch OpenAPI spec from ${source}: HTTP ${res.status}`);
|
|
35
|
+
}
|
|
36
|
+
return await res.json();
|
|
37
|
+
}
|
|
38
|
+
const text = await (0, import_promises.readFile)(source, "utf8");
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ../ultra-api-client/src/toolGenerator.ts
|
|
43
|
+
var HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
|
|
44
|
+
function deriveToolName(method, path) {
|
|
45
|
+
const slug = path.replace(/^\/+/, "").split("/").map((seg) => {
|
|
46
|
+
const param = /^\{(.+)\}$/.exec(seg);
|
|
47
|
+
return param ? `by_${param[1]}` : seg;
|
|
48
|
+
}).join("_");
|
|
49
|
+
return `${method}_${slug}`.replace(/[^a-z0-9_]/gi, "_").toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
function extractOperations(doc, opts = {}) {
|
|
52
|
+
const out = [];
|
|
53
|
+
const tagFilter = (opts.tagFilter ?? []).map((t) => t.toLowerCase());
|
|
54
|
+
for (const [pathTemplate, pathItem] of Object.entries(doc.paths ?? {})) {
|
|
55
|
+
for (const method of HTTP_METHODS) {
|
|
56
|
+
const op = pathItem[method];
|
|
57
|
+
if (!op) continue;
|
|
58
|
+
const opTags = (op.tags ?? []).map((t) => t.toLowerCase());
|
|
59
|
+
if (tagFilter.length && !opTags.some((t) => tagFilter.includes(t))) continue;
|
|
60
|
+
const params = op.parameters ?? [];
|
|
61
|
+
const pathParams = params.filter((p) => p.in === "path").map((p) => p.name);
|
|
62
|
+
const queryParams = params.filter((p) => p.in === "query").map((p) => p.name);
|
|
63
|
+
const properties = {};
|
|
64
|
+
const required = [];
|
|
65
|
+
for (const p of params) {
|
|
66
|
+
if (p.in !== "path" && p.in !== "query") continue;
|
|
67
|
+
const schema = p.schema ?? { type: "string" };
|
|
68
|
+
properties[p.name] = p.description ? { ...schema, description: p.description } : schema;
|
|
69
|
+
if (p.required) required.push(p.name);
|
|
70
|
+
}
|
|
71
|
+
let hasBody = false;
|
|
72
|
+
let bodyContentType;
|
|
73
|
+
if (op.requestBody?.content) {
|
|
74
|
+
const ct = Object.keys(op.requestBody.content)[0];
|
|
75
|
+
if (ct) {
|
|
76
|
+
bodyContentType = ct;
|
|
77
|
+
const bodySchema = op.requestBody.content[ct].schema;
|
|
78
|
+
if (bodySchema) {
|
|
79
|
+
properties.body = bodySchema;
|
|
80
|
+
hasBody = true;
|
|
81
|
+
if (op.requestBody.required) required.push("body");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const inputSchema = {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties,
|
|
88
|
+
...required.length ? { required } : {}
|
|
89
|
+
};
|
|
90
|
+
out.push({
|
|
91
|
+
name: op.operationId ? toSnake(op.operationId) : deriveToolName(method, pathTemplate),
|
|
92
|
+
description: op.summary || op.description || `${method.toUpperCase()} ${pathTemplate}`,
|
|
93
|
+
method,
|
|
94
|
+
pathTemplate,
|
|
95
|
+
inputSchema,
|
|
96
|
+
pathParams,
|
|
97
|
+
queryParams,
|
|
98
|
+
hasBody,
|
|
99
|
+
bodyContentType
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const seen = /* @__PURE__ */ new Set();
|
|
104
|
+
return out.filter((o) => {
|
|
105
|
+
if (seen.has(o.name)) return false;
|
|
106
|
+
seen.add(o.name);
|
|
107
|
+
return true;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function toSnake(s) {
|
|
111
|
+
return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-z0-9_]/gi, "_").toLowerCase();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ../ultra-api-client/src/httpExecutor.ts
|
|
115
|
+
async function executeOperation(op, args, opts, fetchImpl = fetch) {
|
|
116
|
+
let path = op.pathTemplate;
|
|
117
|
+
for (const name of op.pathParams) {
|
|
118
|
+
const v = args[name];
|
|
119
|
+
if (v === void 0 || v === null) {
|
|
120
|
+
throw new Error(`Missing required path parameter: ${name}`);
|
|
121
|
+
}
|
|
122
|
+
path = path.replace(`{${name}}`, encodeURIComponent(String(v)));
|
|
123
|
+
}
|
|
124
|
+
const url = new URL(opts.baseUrl.replace(/\/$/, "") + path);
|
|
125
|
+
for (const name of op.queryParams) {
|
|
126
|
+
const v = args[name];
|
|
127
|
+
if (v === void 0 || v === null) continue;
|
|
128
|
+
if (Array.isArray(v)) {
|
|
129
|
+
v.forEach((item) => url.searchParams.append(name, String(item)));
|
|
130
|
+
} else {
|
|
131
|
+
url.searchParams.set(name, String(v));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const headers = {
|
|
135
|
+
Accept: "application/json",
|
|
136
|
+
"User-Agent": opts.userAgent || "ultra-api-client/1.0"
|
|
137
|
+
};
|
|
138
|
+
if (opts.apiKey) {
|
|
139
|
+
if (!/^ulk_[A-Za-z0-9_-]+$/.test(opts.apiKey)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"Invalid API key format (expected `ulk_\u2026`). Check that ULTRA_API_KEY contains exactly one key value \u2014 multi-line input or a grep with multiple matches will fail here."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
145
|
+
}
|
|
146
|
+
let body;
|
|
147
|
+
if (op.hasBody && args.body !== void 0) {
|
|
148
|
+
headers["Content-Type"] = op.bodyContentType || "application/json";
|
|
149
|
+
body = typeof args.body === "string" ? args.body : JSON.stringify(args.body);
|
|
150
|
+
}
|
|
151
|
+
const res = await fetchImpl(url.toString(), {
|
|
152
|
+
method: op.method.toUpperCase(),
|
|
153
|
+
headers,
|
|
154
|
+
body
|
|
155
|
+
});
|
|
156
|
+
const contentType = res.headers.get("content-type") || "";
|
|
157
|
+
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text();
|
|
158
|
+
return {
|
|
159
|
+
status: res.status,
|
|
160
|
+
body: parsed,
|
|
161
|
+
request_id: res.headers.get("x-request-id") ?? void 0
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/parseArgs.ts
|
|
166
|
+
var GLOBAL_FLAGS = /* @__PURE__ */ new Set(["spec", "base-url", "status", "raw", "quiet"]);
|
|
167
|
+
function parseArgs(argv) {
|
|
168
|
+
const flags = {};
|
|
169
|
+
const global = {};
|
|
170
|
+
let command;
|
|
171
|
+
let bodySource;
|
|
172
|
+
let wantsHelp = false;
|
|
173
|
+
let wantsVersion = false;
|
|
174
|
+
const tokens = [...argv];
|
|
175
|
+
const remaining = [];
|
|
176
|
+
while (tokens.length) {
|
|
177
|
+
const t = tokens.shift();
|
|
178
|
+
if (t === "--help" || t === "-h") {
|
|
179
|
+
wantsHelp = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (t === "--version" || t === "-V") {
|
|
183
|
+
wantsVersion = true;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const m = /^--([^=]+)(?:=(.*))?$/.exec(t);
|
|
187
|
+
if (m && GLOBAL_FLAGS.has(m[1])) {
|
|
188
|
+
const key = m[1];
|
|
189
|
+
const value = m[2] ?? (tokens[0] && !tokens[0].startsWith("--") ? tokens.shift() : "true");
|
|
190
|
+
if (key === "spec") global.spec = value;
|
|
191
|
+
else if (key === "base-url") global.baseUrl = value;
|
|
192
|
+
else if (key === "status") global.status = value === "true" || value === "";
|
|
193
|
+
else if (key === "raw") global.raw = value === "true" || value === "";
|
|
194
|
+
else if (key === "quiet") global.quiet = value === "true" || value === "";
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (!command && !t.startsWith("-")) {
|
|
198
|
+
command = t;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
remaining.push(t);
|
|
202
|
+
}
|
|
203
|
+
if (wantsVersion) return { kind: "version", flags, global };
|
|
204
|
+
if (!command) return { kind: "help", flags, global };
|
|
205
|
+
if (wantsHelp) return { kind: "command-help", command, flags, global };
|
|
206
|
+
while (remaining.length) {
|
|
207
|
+
const t = remaining.shift();
|
|
208
|
+
const m = /^--([^=]+)(?:=(.*))?$/.exec(t);
|
|
209
|
+
if (!m) {
|
|
210
|
+
throw new Error(`Unexpected positional argument: ${t}`);
|
|
211
|
+
}
|
|
212
|
+
const key = m[1];
|
|
213
|
+
let value = m[2];
|
|
214
|
+
if (value === void 0) {
|
|
215
|
+
if (remaining[0] !== void 0 && !remaining[0].startsWith("--")) {
|
|
216
|
+
value = remaining.shift();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (key === "body") {
|
|
220
|
+
if (value === void 0) throw new Error("--body requires a value");
|
|
221
|
+
if (value === "-") bodySource = { kind: "stdin" };
|
|
222
|
+
else if (value.startsWith("@")) bodySource = { kind: "file", path: value.slice(1) };
|
|
223
|
+
else bodySource = { kind: "inline", text: value };
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (value === void 0) {
|
|
227
|
+
flags[key] = true;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const existing = flags[key];
|
|
231
|
+
if (existing === void 0) {
|
|
232
|
+
flags[key] = value;
|
|
233
|
+
} else if (Array.isArray(existing)) {
|
|
234
|
+
existing.push(value);
|
|
235
|
+
} else if (typeof existing === "string") {
|
|
236
|
+
flags[key] = [existing, value];
|
|
237
|
+
} else {
|
|
238
|
+
flags[key] = value;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { kind: "command", command, flags, bodySource, global };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/help.ts
|
|
245
|
+
var HEADER = `ultra \u2014 Ultra Network public API CLI
|
|
246
|
+
|
|
247
|
+
A spec-driven wrapper over /api/v1/*. Every endpoint in the OpenAPI document
|
|
248
|
+
becomes a subcommand. Use \`ultra <command> --help\` for per-command flags.
|
|
249
|
+
|
|
250
|
+
ENVIRONMENT
|
|
251
|
+
ULTRA_API_KEY Bearer key for /api/v1 (required for non-discovery)
|
|
252
|
+
ULTRA_API_SPEC OpenAPI source (default: https://ultranetwork.co/api/v1/openapi.json)
|
|
253
|
+
ULTRA_API_BASE_URL Override server base URL
|
|
254
|
+
ULTRA_API_TAGS CSV tag filter for which operations are exposed
|
|
255
|
+
|
|
256
|
+
GLOBAL FLAGS
|
|
257
|
+
--spec=<url|path> Override the spec source for this invocation
|
|
258
|
+
--base-url=<url> Override the base URL for this invocation
|
|
259
|
+
--status Print HTTP status + request_id to stderr
|
|
260
|
+
--raw Print response body unchanged (no JSON pretty-print)
|
|
261
|
+
--quiet Suppress progress lines on stderr
|
|
262
|
+
--help, -h Show this help (or per-command help)
|
|
263
|
+
--version, -V Print CLI + spec version
|
|
264
|
+
`;
|
|
265
|
+
function renderTopLevelHelp(operations) {
|
|
266
|
+
const out = [HEADER, "COMMANDS"];
|
|
267
|
+
if (operations.length === 0) {
|
|
268
|
+
out.push(" (no operations available \u2014 check ULTRA_API_SPEC)");
|
|
269
|
+
return out.join("\n");
|
|
270
|
+
}
|
|
271
|
+
const groups = /* @__PURE__ */ new Map();
|
|
272
|
+
for (const op of operations) {
|
|
273
|
+
const group = op.pathTemplate.replace(/^\/+/, "").split("/")[0] || "misc";
|
|
274
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
275
|
+
groups.get(group).push(op);
|
|
276
|
+
}
|
|
277
|
+
const nameWidth = Math.min(40, Math.max(...operations.map((o) => o.name.length)));
|
|
278
|
+
for (const [group, ops] of [...groups.entries()].sort()) {
|
|
279
|
+
out.push("");
|
|
280
|
+
out.push(` ${group}/`);
|
|
281
|
+
for (const op of ops.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
282
|
+
const padded = op.name.padEnd(nameWidth);
|
|
283
|
+
out.push(` ${padded} ${op.description}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
out.push("");
|
|
287
|
+
out.push("Run `ultra <command> --help` for input details.");
|
|
288
|
+
return out.join("\n");
|
|
289
|
+
}
|
|
290
|
+
function renderCommandHelp(op) {
|
|
291
|
+
const out = [];
|
|
292
|
+
out.push(`ultra ${op.name}`);
|
|
293
|
+
out.push("");
|
|
294
|
+
out.push(` ${op.description}`);
|
|
295
|
+
out.push(` ${op.method.toUpperCase()} ${op.pathTemplate}`);
|
|
296
|
+
out.push("");
|
|
297
|
+
const schema = op.inputSchema;
|
|
298
|
+
const required = new Set(schema.required ?? []);
|
|
299
|
+
const props = schema.properties ?? {};
|
|
300
|
+
if (op.pathParams.length) {
|
|
301
|
+
out.push("PATH PARAMETERS (required)");
|
|
302
|
+
for (const name of op.pathParams) {
|
|
303
|
+
out.push(formatParam(name, props[name], required.has(name)));
|
|
304
|
+
}
|
|
305
|
+
out.push("");
|
|
306
|
+
}
|
|
307
|
+
if (op.queryParams.length) {
|
|
308
|
+
out.push("QUERY PARAMETERS");
|
|
309
|
+
for (const name of op.queryParams) {
|
|
310
|
+
out.push(formatParam(name, props[name], required.has(name)));
|
|
311
|
+
}
|
|
312
|
+
out.push("");
|
|
313
|
+
}
|
|
314
|
+
if (op.hasBody) {
|
|
315
|
+
out.push("REQUEST BODY");
|
|
316
|
+
out.push(` --body=<json> | --body=@path/to/file.json | --body=- (stdin)`);
|
|
317
|
+
out.push(` Content-Type: ${op.bodyContentType || "application/json"}`);
|
|
318
|
+
if (required.has("body")) out.push(" (required)");
|
|
319
|
+
out.push("");
|
|
320
|
+
}
|
|
321
|
+
out.push("EXAMPLE");
|
|
322
|
+
out.push(formatExample(op));
|
|
323
|
+
return out.join("\n");
|
|
324
|
+
}
|
|
325
|
+
function formatParam(name, schema, isRequired) {
|
|
326
|
+
const tag = isRequired ? " (required)" : "";
|
|
327
|
+
const type = schema?.type ? ` <${schema.type}>` : "";
|
|
328
|
+
const desc = schema?.description ? ` \u2014 ${schema.description}` : "";
|
|
329
|
+
const enumVals = schema?.enum?.length ? ` [${schema.enum.join("|")}]` : "";
|
|
330
|
+
return ` --${name}${type}${tag}${enumVals}${desc}`;
|
|
331
|
+
}
|
|
332
|
+
function formatExample(op) {
|
|
333
|
+
const parts = [" ultra", op.name];
|
|
334
|
+
for (const p of op.pathParams) parts.push(`--${p}=<${p}>`);
|
|
335
|
+
for (const q of op.queryParams.slice(0, 2)) parts.push(`--${q}=<value>`);
|
|
336
|
+
if (op.hasBody) parts.push(`--body='{"\u2026":"\u2026"}'`);
|
|
337
|
+
return parts.join(" ");
|
|
338
|
+
}
|
|
339
|
+
function renderVersion(cliVersion, specVersion) {
|
|
340
|
+
return `ultra ${cliVersion}
|
|
341
|
+
spec ${specVersion ?? "(unknown)"}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/coerce.ts
|
|
345
|
+
function coerceArgs(rawFlags, inputSchema) {
|
|
346
|
+
const props = inputSchema.properties ?? {};
|
|
347
|
+
const out = {};
|
|
348
|
+
for (const [k, v] of Object.entries(rawFlags)) {
|
|
349
|
+
const schema = props[k];
|
|
350
|
+
out[k] = coerceOne(v, schema);
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
function coerceOne(v, schema) {
|
|
355
|
+
if (typeof v === "boolean") return v;
|
|
356
|
+
if (Array.isArray(v)) return v.map((x) => coerceScalar(x, schema?.items ?? schema));
|
|
357
|
+
return coerceScalar(v, schema);
|
|
358
|
+
}
|
|
359
|
+
function coerceScalar(s, schema) {
|
|
360
|
+
const t = schema?.type;
|
|
361
|
+
if (t === "integer") {
|
|
362
|
+
const n = Number.parseInt(s, 10);
|
|
363
|
+
if (Number.isNaN(n)) throw new Error(`Expected integer, got: ${s}`);
|
|
364
|
+
return n;
|
|
365
|
+
}
|
|
366
|
+
if (t === "number") {
|
|
367
|
+
const n = Number(s);
|
|
368
|
+
if (Number.isNaN(n)) throw new Error(`Expected number, got: ${s}`);
|
|
369
|
+
return n;
|
|
370
|
+
}
|
|
371
|
+
if (t === "boolean") {
|
|
372
|
+
if (s === "true" || s === "1") return true;
|
|
373
|
+
if (s === "false" || s === "0") return false;
|
|
374
|
+
throw new Error(`Expected boolean, got: ${s}`);
|
|
375
|
+
}
|
|
376
|
+
return s;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/readBody.ts
|
|
380
|
+
var import_promises2 = require("fs/promises");
|
|
381
|
+
async function readBody(bodySource, stdin = process.stdin) {
|
|
382
|
+
if (!bodySource) return void 0;
|
|
383
|
+
let text;
|
|
384
|
+
if (bodySource.kind === "inline") {
|
|
385
|
+
text = bodySource.text;
|
|
386
|
+
} else if (bodySource.kind === "file") {
|
|
387
|
+
text = await (0, import_promises2.readFile)(bodySource.path, "utf8");
|
|
388
|
+
} else {
|
|
389
|
+
text = await collectStream(stdin);
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
return JSON.parse(text);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
395
|
+
throw new Error(`--body is not valid JSON: ${msg}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function collectStream(s) {
|
|
399
|
+
const chunks = [];
|
|
400
|
+
for await (const chunk of s) {
|
|
401
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
402
|
+
}
|
|
403
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/cli.ts
|
|
407
|
+
var CLI_VERSION = "1.0.0";
|
|
408
|
+
var SPEC_URL_DEFAULT = "https://ultranetwork.co/api/v1/openapi.json";
|
|
409
|
+
async function run(deps = {}) {
|
|
410
|
+
const argv = deps.argv ?? process.argv.slice(2);
|
|
411
|
+
const env = deps.env ?? process.env;
|
|
412
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
413
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
414
|
+
const stdin = deps.stdin ?? process.stdin;
|
|
415
|
+
let parsed;
|
|
416
|
+
try {
|
|
417
|
+
parsed = parseArgs(argv);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
stderr.write(`Error: ${err.message}
|
|
420
|
+
`);
|
|
421
|
+
return 3;
|
|
422
|
+
}
|
|
423
|
+
const specSource = parsed.global.spec || (env.ULTRA_API_SPEC || SPEC_URL_DEFAULT).trim();
|
|
424
|
+
const tagFilter = (env.ULTRA_API_TAGS || "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
425
|
+
let doc;
|
|
426
|
+
try {
|
|
427
|
+
doc = deps.specOverride ?? await loadSpec(specSource);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
stderr.write(`Failed to load OpenAPI spec from ${specSource}: ${err.message}
|
|
430
|
+
`);
|
|
431
|
+
return 4;
|
|
432
|
+
}
|
|
433
|
+
const operations = extractOperations(doc, { tagFilter });
|
|
434
|
+
const opsByName = new Map(operations.map((o) => [o.name, o]));
|
|
435
|
+
if (parsed.kind === "help") {
|
|
436
|
+
stdout.write(renderTopLevelHelp(operations));
|
|
437
|
+
stdout.write("\n");
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
if (parsed.kind === "version") {
|
|
441
|
+
stdout.write(renderVersion(CLI_VERSION, doc.info?.version));
|
|
442
|
+
stdout.write("\n");
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
445
|
+
if (parsed.kind === "command-help") {
|
|
446
|
+
const op2 = opsByName.get(parsed.command);
|
|
447
|
+
if (!op2) return unknownCommand(parsed.command, operations, stderr);
|
|
448
|
+
stdout.write(renderCommandHelp(op2));
|
|
449
|
+
stdout.write("\n");
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
const op = opsByName.get(parsed.command);
|
|
453
|
+
if (!op) return unknownCommand(parsed.command, operations, stderr);
|
|
454
|
+
const apiKey = (env.ULTRA_API_KEY || "").trim();
|
|
455
|
+
if (!apiKey && !parsed.global.quiet) {
|
|
456
|
+
stderr.write("[ultra] WARNING: ULTRA_API_KEY is not set \u2014 call will likely 401.\n");
|
|
457
|
+
}
|
|
458
|
+
let args;
|
|
459
|
+
try {
|
|
460
|
+
args = coerceArgs(parsed.flags, op.inputSchema);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
stderr.write(`Argument error: ${err.message}
|
|
463
|
+
`);
|
|
464
|
+
return 3;
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const body = await readBody(parsed.bodySource, stdin);
|
|
468
|
+
if (body !== void 0) args.body = body;
|
|
469
|
+
} catch (err) {
|
|
470
|
+
stderr.write(`Body error: ${err.message}
|
|
471
|
+
`);
|
|
472
|
+
return 3;
|
|
473
|
+
}
|
|
474
|
+
if (op.hasBody && args.body === void 0 && requiresBody(op)) {
|
|
475
|
+
stderr.write(`This command requires --body=<json|@file|->. Run \`ultra ${op.name} --help\`.
|
|
476
|
+
`);
|
|
477
|
+
return 3;
|
|
478
|
+
}
|
|
479
|
+
const baseUrl = parsed.global.baseUrl || (env.ULTRA_API_BASE_URL || "").trim() || doc.servers?.[0]?.url || "https://ultranetwork.co/api/v1";
|
|
480
|
+
let result;
|
|
481
|
+
try {
|
|
482
|
+
result = await executeOperation(
|
|
483
|
+
op,
|
|
484
|
+
args,
|
|
485
|
+
{ baseUrl, apiKey: apiKey || void 0, userAgent: `ultra-cli/${CLI_VERSION}` },
|
|
486
|
+
deps.fetchImpl
|
|
487
|
+
);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
stderr.write(`Network error: ${err.message}
|
|
490
|
+
`);
|
|
491
|
+
return 2;
|
|
492
|
+
}
|
|
493
|
+
if (parsed.global.status && !parsed.global.quiet) {
|
|
494
|
+
stderr.write(`HTTP ${result.status}${result.request_id ? ` (request_id=${result.request_id})` : ""}
|
|
495
|
+
`);
|
|
496
|
+
}
|
|
497
|
+
if (parsed.global.raw) {
|
|
498
|
+
const text = typeof result.body === "string" ? result.body : JSON.stringify(result.body);
|
|
499
|
+
stdout.write(text);
|
|
500
|
+
stdout.write("\n");
|
|
501
|
+
} else {
|
|
502
|
+
stdout.write(JSON.stringify(result.body, null, 2));
|
|
503
|
+
stdout.write("\n");
|
|
504
|
+
}
|
|
505
|
+
if (result.status >= 500) return 2;
|
|
506
|
+
if (result.status >= 400) return 1;
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
function unknownCommand(name, operations, stderr) {
|
|
510
|
+
stderr.write(`Unknown command: ${name}
|
|
511
|
+
`);
|
|
512
|
+
const suggestions = operations.map((o) => o.name).filter((n) => n.startsWith(name.slice(0, Math.max(3, Math.floor(name.length / 2))))).slice(0, 5);
|
|
513
|
+
if (suggestions.length) {
|
|
514
|
+
stderr.write(`Did you mean: ${suggestions.join(", ")}?
|
|
515
|
+
`);
|
|
516
|
+
}
|
|
517
|
+
stderr.write("Run `ultra --help` to list commands.\n");
|
|
518
|
+
return 3;
|
|
519
|
+
}
|
|
520
|
+
function requiresBody(op) {
|
|
521
|
+
const required = op.inputSchema.required ?? [];
|
|
522
|
+
return required.includes("body");
|
|
523
|
+
}
|
|
524
|
+
var isDirect = typeof require !== "undefined" && require.main === module;
|
|
525
|
+
if (isDirect) {
|
|
526
|
+
run().then(
|
|
527
|
+
(code) => {
|
|
528
|
+
process.exit(code);
|
|
529
|
+
},
|
|
530
|
+
(err) => {
|
|
531
|
+
process.stderr.write(`[ultra] fatal: ${err.message}
|
|
532
|
+
`);
|
|
533
|
+
process.exit(2);
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
538
|
+
0 && (module.exports = {
|
|
539
|
+
run
|
|
540
|
+
});
|
|
541
|
+
//# sourceMappingURL=cli.cjs.map
|