@happyvertical/smrt-app-cli 0.37.2 → 0.37.4

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/index.js CHANGED
@@ -1,1024 +1,930 @@
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";
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
- 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
- }
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
- return response.resources.find((r) => r.slug === slug);
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
- return resource.commands.find((c) => c.commandName === commandName);
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
- 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
- }
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
- 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
- }
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
- if (typeof s !== "string") return [];
86
- return s.split("/").filter((p) => p.length > 0);
87
- }
88
- const JSON_BUFFER_LIMIT = 10 * 1024 * 1024;
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
- try {
91
- return await renderResponseUnchecked(response, options);
92
- } catch (err) {
93
- if (err instanceof BrokenPipeError) {
94
- return { exitCode: 0 };
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
- 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
- }
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
- 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
- }
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
- 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
- });
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
- 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
- }
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
- if (!text) return void 0;
257
- try {
258
- return JSON.stringify(JSON.parse(text), null, 2);
259
- } catch {
260
- return void 0;
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
- 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" };
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
- 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" };
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
- 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;
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
- 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
- };
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
- 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
- };
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
- 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
- };
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
- 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
- }
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
- 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;
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
- 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
- }
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
- 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
- }
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
- 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;
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
- 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
- }
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
- 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
- `);
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
- 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;
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
- if (process.platform === "win32") return true;
688
- return spawnSync("sh", ["-lc", `command -v ${cmd}`], { stdio: "ignore" }).status === 0;
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
- 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
- }
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
- 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
- }
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
- 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
- }
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
- const idHint = command.scope === "item" ? " <id>" : "";
804
- const description = command.description ? ` — ${command.description}` : "";
805
- return `${command.commandName}${idHint}${description}`;
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
- 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"]);
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
- 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
- }
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
- 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);
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
- 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;
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
- 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
- };
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
- 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
- };
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 };