@happyvertical/smrt-app-cli 0.30.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/AGENTS.md +22 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/dist/bin/smrt-mcp-bridge.d.ts +24 -0
- package/dist/bin/smrt-mcp-bridge.js +29 -0
- package/dist/config-Bgq_EQoJ.js +206 -0
- package/dist/index.d.ts +369 -0
- package/dist/index.js +1024 -0
- package/dist/smrt-mcp-bridge.d.ts +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
import { r as requestJson, g as getServerUrl, a as getStoredToken, s as saveAuth, l as loadCliConfig, c as clearStoredToken } from "./config-Bgq_EQoJ.js";
|
|
2
|
+
import { b, d, e } from "./config-Bgq_EQoJ.js";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
async function fetchResourceList(context, options = {}) {
|
|
5
|
+
try {
|
|
6
|
+
return await requestJson(
|
|
7
|
+
context,
|
|
8
|
+
options.path ?? "/api/_resources",
|
|
9
|
+
{ method: "GET" },
|
|
10
|
+
{
|
|
11
|
+
fetch: options.fetch,
|
|
12
|
+
requireAuth: options.requireAuth,
|
|
13
|
+
loadedConfig: options.loadedConfig
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof Error && /401|unauthor/i.test(error.message)) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Not authenticated to ${context.envPrefix.toLowerCase()}. Run \`${context.envPrefix.toLowerCase()} auth login\` first.`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function findResourceBySlug(response, slug) {
|
|
26
|
+
return response.resources.find((r) => r.slug === slug);
|
|
27
|
+
}
|
|
28
|
+
function findCommand(resource, commandName) {
|
|
29
|
+
return resource.commands.find((c) => c.commandName === commandName);
|
|
30
|
+
}
|
|
31
|
+
async function invokeCommand(options) {
|
|
32
|
+
const { context, resource, command, parsed, fetch: fetchImpl } = options;
|
|
33
|
+
const url = await buildUrl(context, resource, command, parsed, options.id);
|
|
34
|
+
const headers = new Headers();
|
|
35
|
+
const token = await getStoredToken(context);
|
|
36
|
+
if (token) headers.set("authorization", `Bearer ${token}`);
|
|
37
|
+
let body;
|
|
38
|
+
if (command.httpMethod !== "GET" && command.httpMethod !== "DELETE") {
|
|
39
|
+
if (Object.keys(parsed.body).length > 0 || parsed.fromPositional) {
|
|
40
|
+
headers.set("content-type", "application/json");
|
|
41
|
+
body = JSON.stringify(parsed.body);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const impl = fetchImpl ?? fetch;
|
|
45
|
+
return impl(url, {
|
|
46
|
+
method: command.httpMethod,
|
|
47
|
+
headers,
|
|
48
|
+
body
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function buildUrl(context, resource, command, parsed, id) {
|
|
52
|
+
const serverUrl = await getServerUrl(context);
|
|
53
|
+
const segments = ["api"];
|
|
54
|
+
for (const piece of splitPath(resource.apiPath)) {
|
|
55
|
+
segments.push(encodeURIComponent(piece));
|
|
56
|
+
}
|
|
57
|
+
if (command.scope === "item") {
|
|
58
|
+
if (!id) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Command \`${resource.slug} ${command.commandName}\` requires an id positional argument.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
segments.push(encodeURIComponent(id));
|
|
64
|
+
}
|
|
65
|
+
for (const seg of command.pathSegments) {
|
|
66
|
+
for (const piece of splitPath(seg)) {
|
|
67
|
+
segments.push(encodeURIComponent(piece));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const base = `${serverUrl}/${segments.join("/")}`;
|
|
71
|
+
if (command.httpMethod === "GET" && Object.keys(parsed.query).length > 0) {
|
|
72
|
+
const params = new URLSearchParams();
|
|
73
|
+
for (const [k, v] of Object.entries(parsed.query)) {
|
|
74
|
+
if (Array.isArray(v)) {
|
|
75
|
+
for (const x of v) params.append(k, x);
|
|
76
|
+
} else {
|
|
77
|
+
params.set(k, v);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return `${base}?${params.toString()}`;
|
|
81
|
+
}
|
|
82
|
+
return base;
|
|
83
|
+
}
|
|
84
|
+
function splitPath(s) {
|
|
85
|
+
if (typeof s !== "string") return [];
|
|
86
|
+
return s.split("/").filter((p) => p.length > 0);
|
|
87
|
+
}
|
|
88
|
+
const JSON_BUFFER_LIMIT = 10 * 1024 * 1024;
|
|
89
|
+
async function renderResponse(response, options = {}) {
|
|
90
|
+
try {
|
|
91
|
+
return await renderResponseUnchecked(response, options);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err instanceof BrokenPipeError) {
|
|
94
|
+
return { exitCode: 0 };
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function renderResponseUnchecked(response, options = {}) {
|
|
100
|
+
const stdout = options.stdout ?? process.stdout;
|
|
101
|
+
const stderr = options.stderr ?? process.stderr;
|
|
102
|
+
const isTty = options.stdoutIsTty ?? Boolean(stdout.isTTY);
|
|
103
|
+
const ct = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
104
|
+
const cl = Number(response.headers.get("content-length") ?? "");
|
|
105
|
+
const isJson = ct.startsWith("application/json") || /\+json(\s|;|$)/.test(ct);
|
|
106
|
+
const isText = ct.startsWith("text/");
|
|
107
|
+
if (response.status === 204) {
|
|
108
|
+
return { exitCode: 0 };
|
|
109
|
+
}
|
|
110
|
+
if (response.status >= 400) {
|
|
111
|
+
if (isJson) {
|
|
112
|
+
const result = await readUntilLimitOrStream(
|
|
113
|
+
response,
|
|
114
|
+
JSON_BUFFER_LIMIT,
|
|
115
|
+
stderr
|
|
116
|
+
);
|
|
117
|
+
if (result.overflowed) {
|
|
118
|
+
stderr.write(
|
|
119
|
+
"\n[smrt-app-cli] error response exceeded 10MB cap; streamed raw\n"
|
|
120
|
+
);
|
|
121
|
+
} else if (result.text) {
|
|
122
|
+
const pretty = safePrettyJson(result.text) ?? result.text;
|
|
123
|
+
stderr.write(`${pretty}
|
|
124
|
+
`);
|
|
125
|
+
}
|
|
126
|
+
return { exitCode: response.status >= 500 ? 2 : 1 };
|
|
127
|
+
}
|
|
128
|
+
if (isText) {
|
|
129
|
+
await pipeBody(response, stderr);
|
|
130
|
+
return { exitCode: response.status >= 500 ? 2 : 1 };
|
|
131
|
+
}
|
|
132
|
+
stderr.write(
|
|
133
|
+
`error: ${response.status} ${response.statusText || "HTTP error"}
|
|
134
|
+
`
|
|
135
|
+
);
|
|
136
|
+
return { exitCode: response.status >= 500 ? 2 : 1 };
|
|
137
|
+
}
|
|
138
|
+
if (isJson) {
|
|
139
|
+
if (cl && cl > JSON_BUFFER_LIMIT) {
|
|
140
|
+
stderr.write(
|
|
141
|
+
`[smrt-app-cli] response too large to pretty-print (${cl} bytes); streaming raw JSON
|
|
142
|
+
`
|
|
143
|
+
);
|
|
144
|
+
await pipeBody(response, stdout);
|
|
145
|
+
return { exitCode: 0 };
|
|
146
|
+
}
|
|
147
|
+
const result = await readUntilLimitOrStream(
|
|
148
|
+
response,
|
|
149
|
+
JSON_BUFFER_LIMIT,
|
|
150
|
+
stdout
|
|
151
|
+
);
|
|
152
|
+
if (result.overflowed) {
|
|
153
|
+
stderr.write(
|
|
154
|
+
"[smrt-app-cli] response exceeded 10MB cap; streamed raw JSON\n"
|
|
155
|
+
);
|
|
156
|
+
return { exitCode: 0 };
|
|
157
|
+
}
|
|
158
|
+
if (!result.text) return { exitCode: 0 };
|
|
159
|
+
const pretty = safePrettyJson(result.text) ?? result.text;
|
|
160
|
+
stdout.write(`${pretty}
|
|
161
|
+
`);
|
|
162
|
+
return { exitCode: 0 };
|
|
163
|
+
}
|
|
164
|
+
if (isText) {
|
|
165
|
+
await pipeBody(response, stdout);
|
|
166
|
+
return { exitCode: 0 };
|
|
167
|
+
}
|
|
168
|
+
if (isTty) {
|
|
169
|
+
const size = cl ? ` (${cl} bytes)` : "";
|
|
170
|
+
stderr.write(
|
|
171
|
+
`[smrt-app-cli] binary response${size}; redirect to a file to capture: <cli> ... > out.bin
|
|
172
|
+
`
|
|
173
|
+
);
|
|
174
|
+
return { exitCode: 1 };
|
|
175
|
+
}
|
|
176
|
+
await pipeBody(response, stdout);
|
|
177
|
+
return { exitCode: 0 };
|
|
178
|
+
}
|
|
179
|
+
async function readUntilLimitOrStream(response, limit, out) {
|
|
180
|
+
if (!response.body) return { text: "", overflowed: false };
|
|
181
|
+
const writeStream = out ?? process.stdout;
|
|
182
|
+
const reader = response.body.getReader();
|
|
183
|
+
const chunks = [];
|
|
184
|
+
let size = 0;
|
|
185
|
+
while (true) {
|
|
186
|
+
const { done, value } = await reader.read();
|
|
187
|
+
if (done) break;
|
|
188
|
+
if (!value) continue;
|
|
189
|
+
size += value.byteLength;
|
|
190
|
+
if (size > limit) {
|
|
191
|
+
for (const c of chunks) await writeChunk(writeStream, c);
|
|
192
|
+
await writeChunk(writeStream, value);
|
|
193
|
+
while (true) {
|
|
194
|
+
const next = await reader.read();
|
|
195
|
+
if (next.done) break;
|
|
196
|
+
if (next.value) await writeChunk(writeStream, next.value);
|
|
197
|
+
}
|
|
198
|
+
return { text: "", overflowed: true };
|
|
199
|
+
}
|
|
200
|
+
chunks.push(value);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
text: new TextDecoder().decode(
|
|
204
|
+
Buffer.concat(chunks.map((c) => Buffer.from(c)))
|
|
205
|
+
),
|
|
206
|
+
overflowed: false
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
class BrokenPipeError extends Error {
|
|
210
|
+
constructor(cause) {
|
|
211
|
+
super("Output stream closed");
|
|
212
|
+
this.name = "BrokenPipeError";
|
|
213
|
+
if (cause !== void 0) this.cause = cause;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function writeChunk(out, chunk) {
|
|
217
|
+
const stream = out;
|
|
218
|
+
let ok;
|
|
219
|
+
try {
|
|
220
|
+
ok = stream.write(Buffer.from(chunk));
|
|
221
|
+
} catch (err) {
|
|
222
|
+
throw new BrokenPipeError(err);
|
|
223
|
+
}
|
|
224
|
+
if (ok) return;
|
|
225
|
+
await new Promise((resolve, reject) => {
|
|
226
|
+
const onDrain = () => {
|
|
227
|
+
stream.off("error", onError);
|
|
228
|
+
resolve();
|
|
229
|
+
};
|
|
230
|
+
const onError = (err) => {
|
|
231
|
+
stream.off("drain", onDrain);
|
|
232
|
+
reject(new BrokenPipeError(err));
|
|
233
|
+
};
|
|
234
|
+
stream.once("drain", onDrain);
|
|
235
|
+
stream.once("error", onError);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async function pipeBody(response, out) {
|
|
239
|
+
if (!response.body) return;
|
|
240
|
+
const reader = response.body.getReader();
|
|
241
|
+
try {
|
|
242
|
+
while (true) {
|
|
243
|
+
const { done, value } = await reader.read();
|
|
244
|
+
if (done) break;
|
|
245
|
+
if (!value) continue;
|
|
246
|
+
await writeChunk(out, value);
|
|
247
|
+
}
|
|
248
|
+
} finally {
|
|
249
|
+
try {
|
|
250
|
+
reader.releaseLock();
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function safePrettyJson(text) {
|
|
256
|
+
if (!text) return void 0;
|
|
257
|
+
try {
|
|
258
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
259
|
+
} catch {
|
|
260
|
+
return void 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function classifySchema(schema) {
|
|
264
|
+
if (!schema || Object.keys(schema).length === 0) return { kind: "missing" };
|
|
265
|
+
if (schema.type !== "object") {
|
|
266
|
+
return { kind: "unsupported", reason: "schema root is not an object" };
|
|
267
|
+
}
|
|
268
|
+
if (schema.oneOf || schema.anyOf || schema.allOf || schema.$ref) {
|
|
269
|
+
return { kind: "unsupported", reason: "oneOf/anyOf/allOf/$ref" };
|
|
270
|
+
}
|
|
271
|
+
const props = schema.properties ?? {};
|
|
272
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
273
|
+
const status = classifyProperty(prop);
|
|
274
|
+
if (status.kind === "unsupported") {
|
|
275
|
+
return {
|
|
276
|
+
kind: "unsupported",
|
|
277
|
+
reason: `${name}: ${status.reason}`
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { kind: "ok" };
|
|
282
|
+
}
|
|
283
|
+
function classifyProperty(prop) {
|
|
284
|
+
const t = normaliseType(prop);
|
|
285
|
+
if (!t) return { kind: "unsupported", reason: "no type" };
|
|
286
|
+
if (prop.oneOf || prop.anyOf || prop.allOf || prop.$ref) {
|
|
287
|
+
return { kind: "unsupported", reason: "oneOf/anyOf/allOf/$ref" };
|
|
288
|
+
}
|
|
289
|
+
if (t.primary === "object") {
|
|
290
|
+
return { kind: "unsupported", reason: "nested object" };
|
|
291
|
+
}
|
|
292
|
+
if (t.primary === "array") {
|
|
293
|
+
const items = prop.items;
|
|
294
|
+
const itemsType = items?.type;
|
|
295
|
+
if (itemsType !== "string" && itemsType !== "integer" && itemsType !== "number") {
|
|
296
|
+
return { kind: "unsupported", reason: "array of non-primitives" };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { kind: "ok" };
|
|
300
|
+
}
|
|
301
|
+
function normaliseType(prop) {
|
|
302
|
+
const raw = prop.type;
|
|
303
|
+
if (typeof raw === "string") {
|
|
304
|
+
return {
|
|
305
|
+
primary: raw,
|
|
306
|
+
nullable: Boolean(prop.nullable)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (Array.isArray(raw)) {
|
|
310
|
+
const nullable = raw.includes("null");
|
|
311
|
+
const nonNull = raw.find((t) => t !== "null");
|
|
312
|
+
if (!nonNull) return void 0;
|
|
313
|
+
return { primary: nonNull, nullable };
|
|
314
|
+
}
|
|
315
|
+
return void 0;
|
|
316
|
+
}
|
|
317
|
+
function buildFlagParser(schema, options = {}) {
|
|
318
|
+
const status = options.positionalOnly ? { kind: "unsupported", reason: "forced positional" } : classifySchema(schema);
|
|
319
|
+
if (status.kind !== "ok") {
|
|
320
|
+
return { status, parse: makePositionalParser() };
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
status,
|
|
324
|
+
parse: makeRichParser(schema)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function makePositionalParser() {
|
|
328
|
+
return (argv, httpMethod) => {
|
|
329
|
+
const positional = argv.find((a) => !a.startsWith("-"));
|
|
330
|
+
if (!positional) {
|
|
331
|
+
return { body: {}, query: {}, fromPositional: true };
|
|
332
|
+
}
|
|
333
|
+
let parsed;
|
|
334
|
+
try {
|
|
335
|
+
parsed = JSON.parse(positional);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Could not parse positional JSON argument: ${error.message}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
342
|
+
throw new Error("Positional JSON argument must be an object.");
|
|
343
|
+
}
|
|
344
|
+
if (httpMethod === "GET") {
|
|
345
|
+
return {
|
|
346
|
+
body: {},
|
|
347
|
+
query: objectToQuery(parsed),
|
|
348
|
+
fromPositional: true
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return { body: parsed, query: {}, fromPositional: true };
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function makeRichParser(schema) {
|
|
355
|
+
const props = schema.properties ?? {};
|
|
356
|
+
const required = new Set(schema.required ?? [] ?? []);
|
|
357
|
+
const additionalProperties = schema.additionalProperties !== false;
|
|
358
|
+
return (argv, httpMethod) => {
|
|
359
|
+
const out = {};
|
|
360
|
+
let positionalJson;
|
|
361
|
+
for (let i = 0; i < argv.length; i++) {
|
|
362
|
+
const arg = argv[i];
|
|
363
|
+
if (!arg.startsWith("-")) {
|
|
364
|
+
if (positionalJson) {
|
|
365
|
+
throw new Error(`Unexpected extra positional argument: ${arg}`);
|
|
366
|
+
}
|
|
367
|
+
let parsed;
|
|
368
|
+
try {
|
|
369
|
+
parsed = JSON.parse(arg);
|
|
370
|
+
} catch {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Unknown positional argument (not valid JSON): ${arg}`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
376
|
+
throw new Error("Positional JSON argument must be an object.");
|
|
377
|
+
}
|
|
378
|
+
positionalJson = parsed;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (arg === "--") break;
|
|
382
|
+
if (arg.startsWith("--no-")) {
|
|
383
|
+
const flagName = arg.slice("--no-".length);
|
|
384
|
+
const prop2 = props[flagName];
|
|
385
|
+
if (!prop2) {
|
|
386
|
+
if (additionalProperties) {
|
|
387
|
+
out[flagName] = false;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
391
|
+
}
|
|
392
|
+
const t2 = normaliseType(prop2);
|
|
393
|
+
if (t2?.primary !== "boolean") {
|
|
394
|
+
throw new Error(`--no-${flagName} requires a boolean flag.`);
|
|
395
|
+
}
|
|
396
|
+
out[flagName] = false;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
let key;
|
|
400
|
+
let value;
|
|
401
|
+
const eqIdx = arg.indexOf("=");
|
|
402
|
+
if (eqIdx >= 0) {
|
|
403
|
+
key = arg.slice(2, eqIdx);
|
|
404
|
+
value = arg.slice(eqIdx + 1);
|
|
405
|
+
} else {
|
|
406
|
+
key = arg.slice(2);
|
|
407
|
+
const prop2 = props[key];
|
|
408
|
+
const t2 = prop2 ? normaliseType(prop2) : void 0;
|
|
409
|
+
if (t2?.primary === "boolean") {
|
|
410
|
+
const next2 = argv[i + 1];
|
|
411
|
+
if (next2 === "true" || next2 === "false") {
|
|
412
|
+
out[key] = next2 === "true";
|
|
413
|
+
i += 1;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
out[key] = true;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const next = argv[i + 1];
|
|
420
|
+
if (next === void 0 || next.startsWith("-")) {
|
|
421
|
+
throw new Error(`Flag --${key} requires a value.`);
|
|
422
|
+
}
|
|
423
|
+
value = next;
|
|
424
|
+
i += 1;
|
|
425
|
+
}
|
|
426
|
+
const prop = props[key];
|
|
427
|
+
if (!prop) {
|
|
428
|
+
if (!additionalProperties) {
|
|
429
|
+
throw new Error(`Unknown flag: --${key}`);
|
|
430
|
+
}
|
|
431
|
+
appendValue(out, key, value);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const t = normaliseType(prop);
|
|
435
|
+
if (!t) throw new Error(`Unknown flag: --${key}`);
|
|
436
|
+
if (t.nullable && value === "null") {
|
|
437
|
+
out[key] = null;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
switch (t.primary) {
|
|
441
|
+
case "string": {
|
|
442
|
+
const allowed = prop.enum;
|
|
443
|
+
if (allowed && !allowed.includes(value)) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
`--${key}: expected one of ${allowed.join(", ")}; got ${JSON.stringify(value)}.`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
out[key] = value;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case "integer": {
|
|
452
|
+
const n = Number(value);
|
|
453
|
+
if (!Number.isInteger(n)) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
`--${key}: expected integer; got ${JSON.stringify(value)}.`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
out[key] = n;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case "number": {
|
|
462
|
+
const n = Number(value);
|
|
463
|
+
if (Number.isNaN(n)) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`--${key}: expected number; got ${JSON.stringify(value)}.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
out[key] = n;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case "boolean": {
|
|
472
|
+
if (value === "true" || value === "false") {
|
|
473
|
+
out[key] = value === "true";
|
|
474
|
+
} else {
|
|
475
|
+
throw new Error(
|
|
476
|
+
`--${key}: boolean accepts true/false; got ${JSON.stringify(value)}.`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
case "array": {
|
|
482
|
+
const items = prop.items;
|
|
483
|
+
const itemsType = items?.type;
|
|
484
|
+
const parts = value.includes(",") ? value.split(",") : [value];
|
|
485
|
+
const arr = Array.isArray(out[key]) ? out[key] : [];
|
|
486
|
+
for (const part of parts) {
|
|
487
|
+
arr.push(coerce(part, itemsType));
|
|
488
|
+
}
|
|
489
|
+
out[key] = arr;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
default:
|
|
493
|
+
throw new Error(`--${key}: unsupported schema type ${t.primary}.`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (positionalJson) {
|
|
497
|
+
for (const [k, v] of Object.entries(positionalJson)) {
|
|
498
|
+
if (out[k] === void 0) out[k] = v;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
502
|
+
if (out[name] !== void 0) continue;
|
|
503
|
+
if (prop.default !== void 0) {
|
|
504
|
+
out[name] = prop.default;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
for (const name of required) {
|
|
508
|
+
if (out[name] === void 0) {
|
|
509
|
+
throw new Error(`Missing required flag: --${name}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (httpMethod === "GET") {
|
|
513
|
+
return { body: {}, query: objectToQuery(out), fromPositional: false };
|
|
514
|
+
}
|
|
515
|
+
return { body: out, query: {}, fromPositional: false };
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function appendValue(out, key, value) {
|
|
519
|
+
if (out[key] === void 0) {
|
|
520
|
+
out[key] = value;
|
|
521
|
+
} else if (Array.isArray(out[key])) {
|
|
522
|
+
out[key].push(value);
|
|
523
|
+
} else {
|
|
524
|
+
out[key] = [out[key], value];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function coerce(value, type) {
|
|
528
|
+
if (type === "integer") {
|
|
529
|
+
const n = Number(value);
|
|
530
|
+
if (!Number.isInteger(n)) throw new Error(`expected integer; got ${value}`);
|
|
531
|
+
return n;
|
|
532
|
+
}
|
|
533
|
+
if (type === "number") {
|
|
534
|
+
const n = Number(value);
|
|
535
|
+
if (Number.isNaN(n)) throw new Error(`expected number; got ${value}`);
|
|
536
|
+
return n;
|
|
537
|
+
}
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
function objectToQuery(obj) {
|
|
541
|
+
const q = {};
|
|
542
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
543
|
+
if (v === void 0 || v === null) continue;
|
|
544
|
+
if (Array.isArray(v)) {
|
|
545
|
+
q[k] = v.map((x) => String(x));
|
|
546
|
+
} else {
|
|
547
|
+
q[k] = String(v);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return q;
|
|
551
|
+
}
|
|
552
|
+
async function runAuthLogin(options, args) {
|
|
553
|
+
const stdout = options.stdout ?? process.stdout;
|
|
554
|
+
const sleep = options.sleepMs ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
555
|
+
const { noOpen, serverUrl } = parseAuthLoginArgs(args, options.noOpenDefault);
|
|
556
|
+
const targetServer = (serverUrl ?? await getServerUrl(options.context)).replace(/\/+$/u, "");
|
|
557
|
+
const start = await requestJson(
|
|
558
|
+
options.context,
|
|
559
|
+
"/api/cli/auth/start",
|
|
560
|
+
{ method: "POST" },
|
|
561
|
+
{ auth: false, serverUrl: targetServer }
|
|
562
|
+
);
|
|
563
|
+
stdout.write(`Open ${start.verificationUrl}
|
|
564
|
+
`);
|
|
565
|
+
stdout.write(`Code: ${start.userCode}
|
|
566
|
+
`);
|
|
567
|
+
if (!noOpen) openVerificationUrl(start.verificationUrl);
|
|
568
|
+
const expiresAt = new Date(start.expiresAt).getTime();
|
|
569
|
+
let interval = start.interval ?? 2;
|
|
570
|
+
const stderr = options.stderr ?? process.stderr;
|
|
571
|
+
while (Date.now() < expiresAt) {
|
|
572
|
+
await sleep(interval * 1e3);
|
|
573
|
+
try {
|
|
574
|
+
const token = await requestJson(
|
|
575
|
+
options.context,
|
|
576
|
+
"/api/cli/auth/token",
|
|
577
|
+
{
|
|
578
|
+
body: JSON.stringify({ deviceCode: start.deviceCode }),
|
|
579
|
+
method: "POST"
|
|
580
|
+
},
|
|
581
|
+
{ auth: false, serverUrl: targetServer }
|
|
582
|
+
);
|
|
583
|
+
if (token.status === "approved" && token.accessToken) {
|
|
584
|
+
await saveAuth(options.context, targetServer, token.accessToken);
|
|
585
|
+
stdout.write(`Authenticated to ${targetServer}
|
|
586
|
+
`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (token.status === "expired") break;
|
|
590
|
+
interval = token.interval ?? interval;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (error instanceof Error) {
|
|
593
|
+
const status = error.status;
|
|
594
|
+
if (status === 410 || error.message.includes("HTTP 410")) break;
|
|
595
|
+
if (isTransientPollError(error, status)) {
|
|
596
|
+
stderr.write(
|
|
597
|
+
`[smrt-app-cli] auth poll: ${error.message} (retrying in ${interval}s)
|
|
598
|
+
`
|
|
599
|
+
);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
throw new Error("Terminal login request expired.");
|
|
607
|
+
}
|
|
608
|
+
function isTransientPollError(error, status) {
|
|
609
|
+
if (status !== void 0) {
|
|
610
|
+
if (status >= 500 && status < 600) return true;
|
|
611
|
+
if (status >= 400 && status < 500) return false;
|
|
612
|
+
}
|
|
613
|
+
const msg = error.message;
|
|
614
|
+
if (/^HTTP 4(?!10)\d\d/.test(msg)) return false;
|
|
615
|
+
if (/^HTTP 5\d\d/.test(msg)) return true;
|
|
616
|
+
if (/fetch failed/i.test(msg)) return true;
|
|
617
|
+
if (/ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN/i.test(msg)) return true;
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
async function runAuthStatus(options) {
|
|
621
|
+
const stdout = options.stdout ?? process.stdout;
|
|
622
|
+
const config = await loadCliConfig(options.context);
|
|
623
|
+
const serverUrl = await getServerUrl(options.context, config);
|
|
624
|
+
const token = await getStoredToken(options.context, config);
|
|
625
|
+
if (!token) {
|
|
626
|
+
stdout.write(
|
|
627
|
+
`${JSON.stringify({ authenticated: false, serverUrl }, null, 2)}
|
|
628
|
+
`
|
|
629
|
+
);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const session = await requestJson(
|
|
634
|
+
options.context,
|
|
635
|
+
"/api/cli/auth/session",
|
|
636
|
+
{ method: "GET" }
|
|
637
|
+
);
|
|
638
|
+
stdout.write(`${JSON.stringify({ ...session, serverUrl }, null, 2)}
|
|
639
|
+
`);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
stdout.write(
|
|
642
|
+
`${JSON.stringify(
|
|
643
|
+
{
|
|
644
|
+
authenticated: false,
|
|
645
|
+
serverUrl,
|
|
646
|
+
error: error instanceof Error ? error.message : String(error)
|
|
647
|
+
},
|
|
648
|
+
null,
|
|
649
|
+
2
|
|
650
|
+
)}
|
|
651
|
+
`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async function runAuthLogout(options) {
|
|
656
|
+
const stdout = options.stdout ?? process.stdout;
|
|
657
|
+
try {
|
|
658
|
+
await requestJson(options.context, "/api/cli/auth/session", {
|
|
659
|
+
method: "DELETE"
|
|
660
|
+
});
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
await clearStoredToken(options.context);
|
|
664
|
+
stdout.write(`${JSON.stringify({ authenticated: false }, null, 2)}
|
|
665
|
+
`);
|
|
666
|
+
}
|
|
667
|
+
function parseAuthLoginArgs(args, noOpenDefault = false) {
|
|
668
|
+
const result = {
|
|
669
|
+
noOpen: noOpenDefault
|
|
670
|
+
};
|
|
671
|
+
for (let i = 0; i < args.length; i++) {
|
|
672
|
+
const arg = args[i];
|
|
673
|
+
if (arg === "--no-open") {
|
|
674
|
+
result.noOpen = true;
|
|
675
|
+
} else if (arg === "--server") {
|
|
676
|
+
result.serverUrl = args[i + 1];
|
|
677
|
+
i += 1;
|
|
678
|
+
} else if (arg?.startsWith("--server=")) {
|
|
679
|
+
result.serverUrl = arg.slice("--server=".length);
|
|
680
|
+
} else {
|
|
681
|
+
throw new Error(`Unknown auth login option: ${arg}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
function commandExists(cmd) {
|
|
687
|
+
if (process.platform === "win32") return true;
|
|
688
|
+
return spawnSync("sh", ["-lc", `command -v ${cmd}`], { stdio: "ignore" }).status === 0;
|
|
689
|
+
}
|
|
690
|
+
function openVerificationUrl(url) {
|
|
691
|
+
const candidates = process.platform === "darwin" ? [{ args: [url], command: "open" }] : process.platform === "win32" ? (
|
|
692
|
+
// `explorer.exe <url>` interprets `?` as a wildcard and silently
|
|
693
|
+
// drops everything after it — every device-code URL has query
|
|
694
|
+
// params (`?userCode=...`), so the browser never opened on
|
|
695
|
+
// Windows. `cmd.exe /c start "" "<url>"` is the conventional
|
|
696
|
+
// shell-out: the empty `""` is the window title (required
|
|
697
|
+
// because `start` would otherwise consume the URL as the
|
|
698
|
+
// title), and the quoted URL preserves `?`. (#1311 review #5.)
|
|
699
|
+
[{ args: ["/c", "start", "", url], command: "cmd.exe" }]
|
|
700
|
+
) : [
|
|
701
|
+
{ args: [url], command: "xdg-open" },
|
|
702
|
+
{ args: ["open", url], command: "gio" }
|
|
703
|
+
];
|
|
704
|
+
const opener = candidates.find((c) => commandExists(c.command));
|
|
705
|
+
if (!opener) return;
|
|
706
|
+
const child = spawn(opener.command, opener.args, {
|
|
707
|
+
detached: true,
|
|
708
|
+
stdio: "ignore",
|
|
709
|
+
windowsHide: true
|
|
710
|
+
});
|
|
711
|
+
child.on("error", () => void 0);
|
|
712
|
+
child.unref();
|
|
713
|
+
}
|
|
714
|
+
async function runMcpCommand(options, args) {
|
|
715
|
+
const stdout = options.stdout ?? process.stdout;
|
|
716
|
+
const sub = args[0];
|
|
717
|
+
if (sub === "tools") {
|
|
718
|
+
const result = await requestJson(
|
|
719
|
+
options.context,
|
|
720
|
+
"/api/mcp/tools",
|
|
721
|
+
{ method: "GET" },
|
|
722
|
+
{ fetch: options.fetch }
|
|
723
|
+
);
|
|
724
|
+
stdout.write(`${JSON.stringify(result, null, 2)}
|
|
725
|
+
`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (sub === "call") {
|
|
729
|
+
const name = args[1];
|
|
730
|
+
const payload = args[2] ?? "{}";
|
|
731
|
+
if (!name) throw new Error("Usage: mcp call <tool> [<json>]");
|
|
732
|
+
let parsed;
|
|
733
|
+
try {
|
|
734
|
+
parsed = JSON.parse(payload);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
throw new Error(
|
|
737
|
+
`Could not parse mcp call payload: ${error.message}`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const result = await requestJson(
|
|
741
|
+
options.context,
|
|
742
|
+
"/api/mcp/call",
|
|
743
|
+
{
|
|
744
|
+
body: JSON.stringify({ arguments: parsed, name }),
|
|
745
|
+
method: "POST"
|
|
746
|
+
},
|
|
747
|
+
{ fetch: options.fetch }
|
|
748
|
+
);
|
|
749
|
+
stdout.write(`${JSON.stringify(result, null, 2)}
|
|
750
|
+
`);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
throw new Error("Usage: mcp tools | mcp call <tool> [<json>]");
|
|
754
|
+
}
|
|
755
|
+
async function runResourcesCommand(options, args) {
|
|
756
|
+
const stdout = options.stdout ?? process.stdout;
|
|
757
|
+
const stderr = options.stderr ?? process.stderr;
|
|
758
|
+
const json = args.includes("--json");
|
|
759
|
+
const debug = args.includes("--debug");
|
|
760
|
+
const response = options.injectResponse ?? await fetchResourceList(options.context, {
|
|
761
|
+
fetch: options.fetch
|
|
762
|
+
});
|
|
763
|
+
if (json) {
|
|
764
|
+
stdout.write(`${JSON.stringify(response, null, 2)}
|
|
765
|
+
`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (response.resources.length === 0) {
|
|
769
|
+
if (!response.user.authenticated) {
|
|
770
|
+
stdout.write("(no resources — not authenticated)\n");
|
|
771
|
+
} else {
|
|
772
|
+
stdout.write("(no resources discovered)\n");
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
for (const resource of response.resources) {
|
|
777
|
+
stdout.write(`${resource.slug} (${resource.className})
|
|
778
|
+
`);
|
|
779
|
+
for (const command of resource.commands) {
|
|
780
|
+
stdout.write(` - ${formatCommandLine(resource, command)}
|
|
781
|
+
`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (response.warnings.length > 0) {
|
|
785
|
+
if (debug) {
|
|
786
|
+
stderr.write(`
|
|
787
|
+
warnings:
|
|
788
|
+
`);
|
|
789
|
+
for (const w of response.warnings) {
|
|
790
|
+
stderr.write(` - ${w}
|
|
791
|
+
`);
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
stderr.write(
|
|
795
|
+
`
|
|
796
|
+
${response.warnings.length} command${response.warnings.length === 1 ? "" : "s"} unavailable (use --debug for details)
|
|
797
|
+
`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function formatCommandLine(_resource, command) {
|
|
803
|
+
const idHint = command.scope === "item" ? " <id>" : "";
|
|
804
|
+
const description = command.description ? ` — ${command.description}` : "";
|
|
805
|
+
return `${command.commandName}${idHint}${description}`;
|
|
806
|
+
}
|
|
807
|
+
function createAppCli(options) {
|
|
808
|
+
const envPrefix = options.envPrefix ?? options.name.toUpperCase();
|
|
809
|
+
const configDir = options.configDir ?? options.name.toLowerCase();
|
|
810
|
+
const context = {
|
|
811
|
+
envPrefix,
|
|
812
|
+
appSlug: configDir,
|
|
813
|
+
defaultServerUrl: options.defaultServerUrl
|
|
814
|
+
};
|
|
815
|
+
const extraByName = /* @__PURE__ */ new Map();
|
|
816
|
+
for (const cmd of options.extraCommands ?? []) {
|
|
817
|
+
extraByName.set(cmd.name, cmd);
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
run: (argv) => runCli(context, options, extraByName, argv),
|
|
821
|
+
startMcpBridge: async (serverInfo) => {
|
|
822
|
+
const { runMcpStdioBridge: runMcpStdioBridge2 } = await import("./config-Bgq_EQoJ.js").then((n) => n.f);
|
|
823
|
+
await runMcpStdioBridge2({
|
|
824
|
+
...context,
|
|
825
|
+
serverInfo: {
|
|
826
|
+
name: serverInfo?.name ?? `${options.name}-mcp`,
|
|
827
|
+
version: serverInfo?.version ?? "0.0.0"
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const BUILT_IN_COMMANDS = /* @__PURE__ */ new Set(["auth", "resources", "mcp"]);
|
|
834
|
+
async function runCli(context, options, extras, argv) {
|
|
835
|
+
const stdout = process.stdout;
|
|
836
|
+
const stderr = process.stderr;
|
|
837
|
+
try {
|
|
838
|
+
await dispatchCli(context, options, extras, argv, stdout, stderr);
|
|
839
|
+
} catch (error) {
|
|
840
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
841
|
+
stderr.write(`${message}
|
|
842
|
+
`);
|
|
843
|
+
process.exitCode = 1;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async function dispatchCli(context, options, extras, argv, stdout, stderr) {
|
|
847
|
+
if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help") {
|
|
848
|
+
printUsage(options, extras, stdout);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const [command, ...rest] = argv;
|
|
852
|
+
const extra = extras.get(command);
|
|
853
|
+
if (extra) {
|
|
854
|
+
if (BUILT_IN_COMMANDS.has(command)) {
|
|
855
|
+
stderr.write(
|
|
856
|
+
`[smrt-app-cli] extra command \`${command}\` shadows a built-in.
|
|
857
|
+
`
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
const ctx = await buildAppContext(context, extra.needsResources ?? false);
|
|
861
|
+
await extra.run(rest, ctx);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (command === "auth") {
|
|
865
|
+
const sub = rest[0];
|
|
866
|
+
const opts = { context, stdout, stderr };
|
|
867
|
+
if (sub === "login") return runAuthLogin(opts, rest.slice(1));
|
|
868
|
+
if (sub === "status") return runAuthStatus(opts);
|
|
869
|
+
if (sub === "logout") return runAuthLogout(opts);
|
|
870
|
+
throw new Error(
|
|
871
|
+
"Usage: auth login [--server <url>] [--no-open] | status | logout"
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
if (command === "mcp") {
|
|
875
|
+
return runMcpCommand({ context, stdout }, rest);
|
|
876
|
+
}
|
|
877
|
+
if (command === "resources") {
|
|
878
|
+
return runResourcesCommand({ context, stdout, stderr }, rest);
|
|
879
|
+
}
|
|
880
|
+
await runResourceCommand(context, command, rest, stdout, stderr);
|
|
881
|
+
}
|
|
882
|
+
async function runResourceCommand(context, slug, rest, stdout, stderr) {
|
|
883
|
+
const response = await fetchResourceList(context);
|
|
884
|
+
const resource = findResourceBySlug(response, slug);
|
|
885
|
+
if (!resource) {
|
|
886
|
+
const suggestions = response.resources.map((r) => r.slug).filter((s) => similar(s, slug)).slice(0, 3);
|
|
887
|
+
const hint = suggestions.length ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
888
|
+
throw new Error(`Unknown resource: ${slug}.${hint}`);
|
|
889
|
+
}
|
|
890
|
+
const commandName = rest[0];
|
|
891
|
+
if (!commandName) {
|
|
892
|
+
throw new Error(
|
|
893
|
+
`Usage: ${slug} <command> [id] [...]. Available: ${resource.commands.map((c) => c.commandName).join(", ")}`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
const command = findCommand(resource, commandName);
|
|
897
|
+
if (!command) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`Unknown command \`${commandName}\` on resource \`${slug}\`. Available: ${resource.commands.map((c) => c.commandName).join(", ")}`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
let positional = rest.slice(1);
|
|
903
|
+
let id;
|
|
904
|
+
if (command.scope === "item") {
|
|
905
|
+
id = positional[0];
|
|
906
|
+
if (!id) {
|
|
907
|
+
throw new Error(
|
|
908
|
+
`Command \`${slug} ${commandName}\` requires an id positional argument.`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
if (findCommand(resource, id)) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`\`${id}\` is a command on \`${slug}\`, not an id. Did you mean: \`${slug} ${id}${findCommand(resource, id)?.scope === "item" ? " <id>" : ""}\`? \`${slug} ${commandName}\` is an item-scope command and needs an id as the next argument.`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
positional = positional.slice(1);
|
|
917
|
+
}
|
|
918
|
+
const parser = buildFlagParser(command.parameters);
|
|
919
|
+
if (parser.status.kind === "unsupported") {
|
|
920
|
+
stderr.write(
|
|
921
|
+
`[smrt-app-cli] complex schema for \`${slug} ${commandName}\`; pass JSON payload directly
|
|
922
|
+
`
|
|
923
|
+
);
|
|
924
|
+
} else if (parser.status.kind === "missing") {
|
|
925
|
+
stderr.write(
|
|
926
|
+
`[smrt-app-cli] schema unavailable for \`${slug} ${commandName}\`; pass JSON payload directly: $ <cli> ${slug} ${commandName}${id ? ` ${id}` : ""} '<json>'
|
|
927
|
+
`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const parsed = parser.parse(positional, command.httpMethod);
|
|
931
|
+
const response2 = await invokeCommand({
|
|
932
|
+
context,
|
|
933
|
+
resource,
|
|
934
|
+
command,
|
|
935
|
+
parsed,
|
|
936
|
+
id
|
|
937
|
+
});
|
|
938
|
+
const { exitCode } = await renderResponse(response2, { stdout, stderr });
|
|
939
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
940
|
+
}
|
|
941
|
+
async function buildAppContext(context, eagerResources) {
|
|
942
|
+
const config = await loadCliConfig(context);
|
|
943
|
+
const serverUrl = await getServerUrl(context, config);
|
|
944
|
+
const token = await getStoredToken(context, config);
|
|
945
|
+
let resourcesPromise = null;
|
|
946
|
+
const getResources = () => {
|
|
947
|
+
if (!resourcesPromise) {
|
|
948
|
+
resourcesPromise = fetchResourceList(context, { loadedConfig: config });
|
|
949
|
+
}
|
|
950
|
+
return resourcesPromise;
|
|
951
|
+
};
|
|
952
|
+
if (eagerResources) {
|
|
953
|
+
void getResources();
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
config,
|
|
957
|
+
serverUrl,
|
|
958
|
+
token,
|
|
959
|
+
getResources,
|
|
960
|
+
requestJson: (path, init, opts) => requestJson(context, path, init, { loadedConfig: config, ...opts }),
|
|
961
|
+
request: async (path, init) => {
|
|
962
|
+
const headers = new Headers(init?.headers);
|
|
963
|
+
if (token) headers.set("authorization", `Bearer ${token}`);
|
|
964
|
+
return fetch(`${serverUrl}${path}`, { ...init, headers });
|
|
965
|
+
},
|
|
966
|
+
stdout: process.stdout,
|
|
967
|
+
stderr: process.stderr
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function printUsage(options, extras, out) {
|
|
971
|
+
const name = options.name;
|
|
972
|
+
out.write(`Usage:
|
|
973
|
+
`);
|
|
974
|
+
out.write(` ${name} auth login [--server <url>] [--no-open]
|
|
975
|
+
`);
|
|
976
|
+
out.write(` ${name} auth status
|
|
977
|
+
`);
|
|
978
|
+
out.write(` ${name} auth logout
|
|
979
|
+
`);
|
|
980
|
+
out.write(` ${name} resources [--json] [--debug]
|
|
981
|
+
`);
|
|
982
|
+
out.write(
|
|
983
|
+
` ${name} <resource> <command> [id] [--flags...] [json-payload]
|
|
984
|
+
`
|
|
985
|
+
);
|
|
986
|
+
out.write(` ${name} mcp tools
|
|
987
|
+
`);
|
|
988
|
+
out.write(` ${name} mcp call <tool> [<json>]
|
|
989
|
+
`);
|
|
990
|
+
for (const cmd of extras.values()) {
|
|
991
|
+
out.write(` ${name} ${cmd.name} — ${cmd.description}
|
|
992
|
+
`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function similar(a, b2) {
|
|
996
|
+
if (a === b2) return true;
|
|
997
|
+
if (Math.abs(a.length - b2.length) > 2) return false;
|
|
998
|
+
let diff = 0;
|
|
999
|
+
for (let i = 0; i < Math.max(a.length, b2.length); i++) {
|
|
1000
|
+
if (a[i] !== b2[i]) diff += 1;
|
|
1001
|
+
if (diff > 2) return false;
|
|
1002
|
+
}
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
export {
|
|
1006
|
+
buildFlagParser,
|
|
1007
|
+
buildUrl,
|
|
1008
|
+
classifySchema,
|
|
1009
|
+
clearStoredToken,
|
|
1010
|
+
createAppCli,
|
|
1011
|
+
b as createMcpStdioBridge,
|
|
1012
|
+
fetchResourceList,
|
|
1013
|
+
findCommand,
|
|
1014
|
+
findResourceBySlug,
|
|
1015
|
+
getServerUrl,
|
|
1016
|
+
getStoredToken,
|
|
1017
|
+
invokeCommand,
|
|
1018
|
+
loadCliConfig,
|
|
1019
|
+
renderResponse,
|
|
1020
|
+
requestJson,
|
|
1021
|
+
d as runMcpStdioBridge,
|
|
1022
|
+
saveAuth,
|
|
1023
|
+
e as saveCliConfig
|
|
1024
|
+
};
|