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