@terminus-ai/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1055 -0
  3. package/bin/agent-discovery.mjs +71 -0
  4. package/bin/agent-icon.mjs +77 -0
  5. package/bin/agent-models.mjs +77 -0
  6. package/bin/agent-type.mjs +51 -0
  7. package/bin/agentdev.mjs +657 -0
  8. package/bin/app-route-script.mjs +59 -0
  9. package/bin/app-runtime-contract.mjs +2 -0
  10. package/bin/appdev-remote.mjs +346 -0
  11. package/bin/appdev.mjs +4446 -0
  12. package/bin/apps.mjs +5512 -0
  13. package/bin/capability-calls.mjs +437 -0
  14. package/bin/capsule-data.mjs +260 -0
  15. package/bin/client.mjs +189 -0
  16. package/bin/commands.mjs +1194 -0
  17. package/bin/dev-capsules.mjs +1599 -0
  18. package/bin/dev-contract.mjs +262 -0
  19. package/bin/dev-data.mjs +287 -0
  20. package/bin/dev-members.mjs +18 -0
  21. package/bin/dev-net.mjs +316 -0
  22. package/bin/dev-notification-popup.mjs +628 -0
  23. package/bin/dev-ports.mjs +567 -0
  24. package/bin/dev-server-binding.mjs +35 -0
  25. package/bin/dev-server-ops.mjs +1086 -0
  26. package/bin/dev-ui/IoskeleyMono-400.woff2 +0 -0
  27. package/bin/dev-ui/IoskeleyMono-600.woff2 +0 -0
  28. package/bin/dev-ui/OFL.txt +92 -0
  29. package/bin/dev-ui/agent-robot.webp +0 -0
  30. package/bin/dev-ui/app.js +5217 -0
  31. package/bin/dev-ui/highlight.js +195 -0
  32. package/bin/dev-ui/index.html +34 -0
  33. package/bin/dev-ui/style.css +3640 -0
  34. package/bin/devlint.mjs +112 -0
  35. package/bin/devserver.mjs +2127 -0
  36. package/bin/devtriggers.mjs +367 -0
  37. package/bin/endpoints.mjs +156 -0
  38. package/bin/errors.mjs +61 -0
  39. package/bin/files.mjs +169 -0
  40. package/bin/horizontal-capabilities/v1/contract.json +280 -0
  41. package/bin/http.mjs +500 -0
  42. package/bin/lint-manifests/justbash-commands.json +88 -0
  43. package/bin/lint-manifests/python-stdlib.json +295 -0
  44. package/bin/login-page.mjs +488 -0
  45. package/bin/schedules.mjs +664 -0
  46. package/bin/server-sandbox.mjs +204 -0
  47. package/bin/servicedev.mjs +425 -0
  48. package/bin/sync.mjs +357 -0
  49. package/bin/terminus.js +3666 -0
  50. package/bin/toolchain.mjs +125 -0
  51. package/bin/vendor/app-runtime-v1/app-host.json +124 -0
  52. package/bin/vendor/app-runtime-v1/capability-calls.json +412 -0
  53. package/bin/vendor/app-runtime-v1/doors.json +2867 -0
  54. package/bin/vendor/appd/node-harness.mjs +209 -0
  55. package/bin/vendor/appd/python-harness.py +12 -0
  56. package/bin/vendor/appd/server-protocol.json +84 -0
  57. package/bin/vendor/where.mjs +541 -0
  58. package/bin/versioning.mjs +72 -0
  59. package/bin/write-rules.mjs +398 -0
  60. package/package.json +41 -0
package/bin/http.mjs ADDED
@@ -0,0 +1,500 @@
1
+ /**
2
+ * The one HTTP client the CLI talks to Terminus through.
3
+ *
4
+ * - Where: one API base (`--api-base`, else TERMINUS_API_BASE, else the saved
5
+ * login's, else production) and one token (TERMINUS_TOKEN, else the saved
6
+ * login), resolved here and nowhere else.
7
+ * - What: every door is a template from bin/endpoints.mjs, so the CLI can only
8
+ * call a door that table lists — and the table is checked against the
9
+ * backend's route contract. The one exception is the app host's
10
+ * passthrough in `terminus dev` (`openResponse`), which forwards an app's
11
+ * own `/_terminus` requests by the app host's contract instead.
12
+ * - How: every request says `User-Agent: terminus-cli/<version>`; the request
13
+ * has 30 s, plus time for what it uploads, to be answered, and the answer
14
+ * may take as long as its bytes keep arriving; a failure keeps the
15
+ * backend's `{error: {code, message, details}}` on the thrown CliError
16
+ * (`status`, `apiCode`, `details`) and picks the exit code from it; and only
17
+ * a request that is safe to repeat (GET, HEAD, or one carrying an
18
+ * Idempotency-Key) is repeated after a transient failure, with backoff.
19
+ */
20
+
21
+ import { readFileSync } from "node:fs";
22
+ import { readFile } from "node:fs/promises";
23
+ import os from "node:os";
24
+ import path from "node:path";
25
+ import { setTimeout as sleep } from "node:timers/promises";
26
+
27
+ import { endpointPath } from "./endpoints.mjs";
28
+ import { CliError, authError } from "./errors.mjs";
29
+
30
+ export const DEFAULT_TERMINUS_API_BASE = "https://api.terminus.build";
31
+
32
+ export const CLI_VERSION = JSON.parse(
33
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
34
+ ).version;
35
+
36
+ /** Every request names the client it came from; the backend records it on
37
+ * skill uses and in the sign-in audit. */
38
+ export const USER_AGENT = `terminus-cli/${CLI_VERSION}`;
39
+
40
+ /* ── Where the API is ───────────────────────────────────────────────────── */
41
+
42
+ export function normalizeBase(value) {
43
+ return String(value).trim().replace(/\/+$/, "");
44
+ }
45
+
46
+ export function normalizeApiBase(value) {
47
+ const base = normalizeBase(value);
48
+ return base.endsWith("/v1") ? base : `${base}/v1`;
49
+ }
50
+
51
+ // Empty values from dotenv templates must not shadow durable credentials.
52
+ export function presentValue(value) {
53
+ if (typeof value !== "string") return undefined;
54
+ const trimmed = value.trim();
55
+ return trimmed || undefined;
56
+ }
57
+
58
+ /** The API base a command talks to: `--api-base`, TERMINUS_API_BASE, the
59
+ * base the saved login was made against, or production. */
60
+ export async function commandApiBase(flags = {}) {
61
+ const explicit = presentValue(flags.api_base) ?? presentValue(process.env.TERMINUS_API_BASE);
62
+ if (explicit) return normalizeApiBase(explicit);
63
+ const session = await readSession();
64
+ return normalizeApiBase(session?.api_base ?? DEFAULT_TERMINUS_API_BASE);
65
+ }
66
+
67
+ /* ── Who is asking ──────────────────────────────────────────────────────── */
68
+
69
+ export function sessionPath() {
70
+ return path.join(os.homedir(), ".terminus", "session.json");
71
+ }
72
+
73
+ export async function readSession() {
74
+ try {
75
+ return JSON.parse(await readFile(sessionPath(), "utf8"));
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /** A saved login is a browser device session, and it always carries its
82
+ * expiry. Files from older builds (an API key, or a token without an
83
+ * expiry) are not logins; `terminus login` replaces them. */
84
+ export function sessionTokenIsUsable(session, now = Date.now()) {
85
+ const token = presentValue(session?.token);
86
+ if (!token || session.api_key) return false;
87
+ const expiresAt = Date.parse(session.expires_at ?? "");
88
+ return Number.isFinite(expiresAt) && expiresAt > now + 30_000;
89
+ }
90
+
91
+ /** The exit-3 error for a missing, expired, or pre-device login. */
92
+ export function signInError(session) {
93
+ if (presentValue(session?.token) && session.expires_at && !session.api_key) {
94
+ return authError("Your login has expired. Run `terminus login` to sign in again.");
95
+ }
96
+ return authError("Not signed in. Run `terminus login` (or set TERMINUS_TOKEN for automation).");
97
+ }
98
+
99
+ /**
100
+ * The token a command signs its requests with: TERMINUS_TOKEN (a session
101
+ * token the environment supplies, for automation), else the saved login.
102
+ * `source` says which, and `session` is the saved login when it is that.
103
+ * Required and missing, it throws the exit-3 sign-in error; optional and
104
+ * missing, it answers null.
105
+ */
106
+ export async function resolveToken({ required = true } = {}) {
107
+ const fromEnvironment = presentValue(process.env.TERMINUS_TOKEN);
108
+ if (fromEnvironment) return { token: fromEnvironment, source: "env", session: null };
109
+ const session = await readSession();
110
+ if (sessionTokenIsUsable(session)) return { token: session.token.trim(), source: "session", session };
111
+ if (required) throw signInError(session);
112
+ return null;
113
+ }
114
+
115
+ /* ── How long, and how often ────────────────────────────────────────────── */
116
+
117
+ const REQUEST_TIMEOUT_MS = 30_000;
118
+ const IDLE_TIMEOUT_MS = 30_000;
119
+ /** The slowest connection a transfer is budgeted for. */
120
+ const MIN_BYTES_PER_SECOND = 64 * 1024;
121
+ const MAX_TRANSFER_TIMEOUT_MS = 30 * 60_000;
122
+ const MAX_ATTEMPTS = 3;
123
+ const RETRY_BASE_MS = 250;
124
+ /** A server that asks for a longer pause than this gets it from the person. */
125
+ const MAX_RETRY_AFTER_MS = 10_000;
126
+
127
+ /** The time a transfer of `bytes` gets: 30 s, plus the bytes at 64 KiB/s. */
128
+ export function transferTimeoutMs(bytes = 0) {
129
+ const extra = Math.ceil((Math.max(0, Number(bytes) || 0) / MIN_BYTES_PER_SECOND) * 1000);
130
+ return Math.min(REQUEST_TIMEOUT_MS + extra, MAX_TRANSFER_TIMEOUT_MS);
131
+ }
132
+
133
+ function backoffMs(attempt) {
134
+ const base = RETRY_BASE_MS * 2 ** (attempt - 1);
135
+ return Math.round(base * (0.8 + Math.random() * 0.4));
136
+ }
137
+
138
+ function retryAfterMs(headers) {
139
+ const raw = headers.get("retry-after");
140
+ if (!raw) return null;
141
+ const seconds = Number(raw);
142
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
143
+ const at = Date.parse(raw);
144
+ return Number.isFinite(at) ? Math.max(0, at - Date.now()) : null;
145
+ }
146
+
147
+ /* ── One request ────────────────────────────────────────────────────────── */
148
+
149
+ const CANCELLED = Symbol("cancelled");
150
+ const TIMED_OUT = Symbol("timed out");
151
+ /** What makes a failure worth one more attempt, when the request is safe to
152
+ * repeat: `{ waitMs }` (null waits the usual backoff). */
153
+ const TRANSIENT = Symbol("transient");
154
+
155
+ /**
156
+ * Send one request to `url` and read its answer.
157
+ *
158
+ * `body` is a string or bytes — or a function that returns a fresh body for
159
+ * each attempt, for a stream. `bodyBytes` is what it carries and
160
+ * `expectedBytes` what the answer carries, when known: both only size the
161
+ * timeouts. `as` is how the answer is read: "json" (an empty answer is `{}`),
162
+ * "bytes", or "none". `idempotent: true` declares a request safe to repeat
163
+ * that is not a GET and carries no Idempotency-Key, like a content-addressed
164
+ * upload.
165
+ */
166
+ export async function send(url, options = {}) {
167
+ const method = String(options.method ?? "GET").toUpperCase();
168
+ const headers = { "User-Agent": USER_AGENT, ...(options.headers ?? {}) };
169
+ const safeToRepeat = options.idempotent
170
+ ?? (method === "GET" || method === "HEAD"
171
+ || Object.keys(headers).some((name) => name.toLowerCase() === "idempotency-key"));
172
+ for (let attempt = 1; ; attempt += 1) {
173
+ try {
174
+ return await exchange(url, method, headers, options);
175
+ } catch (error) {
176
+ const transient = error?.[TRANSIENT];
177
+ if (!transient || !safeToRepeat || attempt >= MAX_ATTEMPTS) throw error;
178
+ await sleep(transient.waitMs ?? backoffMs(attempt));
179
+ }
180
+ }
181
+ }
182
+
183
+ async function exchange(url, method, headers, options) {
184
+ const label = options.label ?? "Terminus API";
185
+ const caller = options.signal;
186
+ if (caller?.aborted) throw new CliError(`${label} request was cancelled: ${url}`);
187
+ const controller = new AbortController();
188
+ const onCaller = () => controller.abort(CANCELLED);
189
+ caller?.addEventListener("abort", onCaller, { once: true });
190
+ const requestMs = options.timeoutMs ?? transferTimeoutMs(options.bodyBytes ?? 0);
191
+ let phase = "request";
192
+ let timer = setTimeout(() => controller.abort(TIMED_OUT), requestMs);
193
+ let whole = null;
194
+ const fail = (error) => {
195
+ if (controller.signal.reason === CANCELLED) {
196
+ return new CliError(`${label} request was cancelled: ${url}`);
197
+ }
198
+ if (controller.signal.reason === TIMED_OUT) {
199
+ return new CliError(
200
+ phase === "request"
201
+ ? `${label} request timed out after ${Math.round(requestMs / 1000)}s: ${url}`
202
+ : `${label} stopped answering ${url} partway (nothing arrived for ${IDLE_TIMEOUT_MS / 1000}s, or the answer overran its time)`,
203
+ { code: "unavailable" },
204
+ );
205
+ }
206
+ const detail = error?.cause?.code ?? error?.cause?.message ?? error?.message ?? String(error);
207
+ const failure = new CliError(`could not reach the ${label} (${url}): ${detail}`, { code: "unavailable" });
208
+ failure[TRANSIENT] = { waitMs: null };
209
+ return failure;
210
+ };
211
+ try {
212
+ let response;
213
+ try {
214
+ const body = typeof options.body === "function" ? options.body() : options.body;
215
+ response = await fetch(url, {
216
+ method,
217
+ headers,
218
+ body,
219
+ ...(options.duplex ? { duplex: options.duplex } : {}),
220
+ signal: controller.signal,
221
+ });
222
+ } catch (error) {
223
+ throw fail(error);
224
+ }
225
+ clearTimeout(timer);
226
+ phase = "response";
227
+ // The answer may take as long as it keeps arriving; when its size is
228
+ // known, it also gets that size's transfer budget and no more.
229
+ const expected = Number(options.expectedBytes);
230
+ if (Number.isFinite(expected) && expected > 0) {
231
+ whole = setTimeout(() => controller.abort(TIMED_OUT), transferTimeoutMs(expected));
232
+ }
233
+ let bytes;
234
+ try {
235
+ bytes = await readAll(response, controller, () => {
236
+ clearTimeout(timer);
237
+ timer = setTimeout(() => controller.abort(TIMED_OUT), IDLE_TIMEOUT_MS);
238
+ });
239
+ } catch (error) {
240
+ throw fail(error);
241
+ }
242
+ if (!response.ok) throw responseError(response.status, bytes, response.headers, label);
243
+ return readAs(bytes, options.as ?? "json", url, label);
244
+ } finally {
245
+ clearTimeout(timer);
246
+ if (whole) clearTimeout(whole);
247
+ caller?.removeEventListener("abort", onCaller);
248
+ }
249
+ }
250
+
251
+ async function readAll(response, controller, onChunk) {
252
+ if (!response.body) return Buffer.alloc(0);
253
+ const chunks = [];
254
+ let size = 0;
255
+ onChunk();
256
+ for await (const chunk of response.body) {
257
+ onChunk();
258
+ chunks.push(chunk);
259
+ size += chunk.byteLength;
260
+ }
261
+ return Buffer.concat(chunks, size);
262
+ }
263
+
264
+ function readAs(bytes, as, url, label) {
265
+ if (as === "bytes") return bytes;
266
+ if (as === "none") return undefined;
267
+ const text = bytes.toString("utf8");
268
+ if (!text.trim()) return {};
269
+ try {
270
+ return JSON.parse(text);
271
+ } catch {
272
+ throw new CliError(`the ${label} answered ${url} with something that is not JSON`);
273
+ }
274
+ }
275
+
276
+ function isPlainObject(value) {
277
+ return value !== null && typeof value === "object" && !Array.isArray(value);
278
+ }
279
+
280
+ /**
281
+ * The CliError for a non-2xx answer, keeping what the backend said:
282
+ * `{error: {code, message, details?}}`. Anything else in the body (a proxy's
283
+ * page, say) is quoted, briefly, as the message.
284
+ *
285
+ * The exit code follows the answer: 401, `unauthorized`, or `session_ended`
286
+ * is "not signed in" (3); a 403 — `forbidden`, `grant_required`, a skill that
287
+ * is not open — is "not allowed" (77); 429 is rate limited (75); 502, 503,
288
+ * and 504 are Terminus being unavailable (69); anything else is an error (1).
289
+ */
290
+ export function responseError(status, bytes, headers = new Headers(), label = "Terminus API") {
291
+ const text = Buffer.from(bytes ?? []).toString("utf8");
292
+ let parsed = null;
293
+ try {
294
+ parsed = text ? JSON.parse(text) : null;
295
+ } catch {
296
+ parsed = null;
297
+ }
298
+ const envelope = isPlainObject(parsed?.error) ? parsed.error : null;
299
+ const apiCode = typeof envelope?.code === "string" && envelope.code ? envelope.code : undefined;
300
+ const details = envelope && Object.hasOwn(envelope, "details") ? envelope.details : undefined;
301
+ const said = typeof envelope?.message === "string" && envelope.message.trim()
302
+ ? envelope.message.trim()
303
+ : text.replace(/\s+/g, " ").trim().slice(0, 200) || `HTTP ${status}`;
304
+ const signedOut = status === 401 || apiCode === "unauthorized" || apiCode === "session_ended";
305
+ const code = signedOut
306
+ ? "auth"
307
+ : status === 403
308
+ ? "forbidden"
309
+ : status === 429
310
+ ? "rate_limited"
311
+ : status === 502 || status === 503 || status === 504
312
+ ? "unavailable"
313
+ : "error";
314
+ let message = `${label} returned ${status}${apiCode ? ` (${apiCode})` : ""}: ${said}`;
315
+ if (signedOut) {
316
+ message += " — your login may have expired or been revoked; run `terminus login`";
317
+ }
318
+ const wait = retryAfterMs(headers);
319
+ if (code === "rate_limited") {
320
+ message += wait ? ` (retry in ~${Math.max(1, Math.round(wait / 1000))}s)` : " (retry shortly)";
321
+ }
322
+ const error = new CliError(message, { code, status, apiCode, details });
323
+ if (code === "unavailable" && (wait === null || wait <= MAX_RETRY_AFTER_MS)) {
324
+ error[TRANSIENT] = { waitMs: wait };
325
+ }
326
+ return error;
327
+ }
328
+
329
+ /* ── An answer read as it arrives ───────────────────────────────────────── */
330
+
331
+ /**
332
+ * Send one request and hand back its Response the moment its status and
333
+ * headers arrive, the body unread and the status unchecked: for an answer the
334
+ * caller reads as it comes — a turn's SSE frames (`Api#stream`), an icon passed
335
+ * on with its ETag (`Api#open`), or the app host's passthrough in `terminus
336
+ * dev` (bin/appdev-remote.mjs), which routes an app's own requests by the app
337
+ * host's contract, not bin/endpoints.mjs, and hands status, headers, and body
338
+ * to the page untouched.
339
+ *
340
+ * The request has `timeoutMs` (by default 30 s plus its upload) to be
341
+ * answered; `null` waits as long as the platform takes, whose own deadlines
342
+ * then answer. Reading the body is the caller's, and so is cancelling it
343
+ * (`signal`). Never repeated.
344
+ */
345
+ export async function openResponse(url, options = {}) {
346
+ const method = String(options.method ?? "GET").toUpperCase();
347
+ const label = options.label ?? "Terminus API";
348
+ const caller = options.signal;
349
+ if (caller?.aborted) throw new CliError(`${label} request was cancelled: ${url}`);
350
+ const requestMs = options.timeoutMs === null
351
+ ? null
352
+ : options.timeoutMs ?? transferTimeoutMs(options.bodyBytes ?? 0);
353
+ const timeout = new AbortController();
354
+ const timer = requestMs === null ? null : setTimeout(() => timeout.abort(TIMED_OUT), requestMs);
355
+ try {
356
+ return await fetch(url, {
357
+ method,
358
+ headers: { "User-Agent": USER_AGENT, ...(options.headers ?? {}) },
359
+ body: options.body,
360
+ redirect: options.redirect ?? "follow",
361
+ signal: caller ? AbortSignal.any([caller, timeout.signal]) : timeout.signal,
362
+ });
363
+ } catch (error) {
364
+ if (caller?.aborted) throw new CliError(`${label} request was cancelled: ${url}`);
365
+ if (timeout.signal.aborted) {
366
+ throw new CliError(
367
+ `${label} request timed out after ${Math.round(requestMs / 1000)}s: ${url}`,
368
+ { code: "unavailable" },
369
+ );
370
+ }
371
+ const detail = error?.cause?.code ?? error?.cause?.message ?? error?.message ?? String(error);
372
+ throw new CliError(`could not reach the ${label} (${url}): ${detail}`, { code: "unavailable" });
373
+ } finally {
374
+ if (timer) clearTimeout(timer);
375
+ }
376
+ }
377
+
378
+ /* ── The client a command holds ─────────────────────────────────────────── */
379
+
380
+ function queryString(query) {
381
+ if (!query) return "";
382
+ const params = new URLSearchParams();
383
+ for (const [key, value] of Object.entries(query)) {
384
+ if (value !== undefined && value !== null) params.set(key, String(value));
385
+ }
386
+ const text = params.toString();
387
+ return text ? `?${text}` : "";
388
+ }
389
+
390
+ /**
391
+ * The Terminus API as one command sees it: a base, the token it signs with
392
+ * (none for anonymous reads), and the doors of bin/endpoints.mjs.
393
+ */
394
+ export class Api {
395
+ #me = null;
396
+
397
+ constructor({ base = DEFAULT_TERMINUS_API_BASE, token = null, identity = null } = {}) {
398
+ this.base = normalizeApiBase(base);
399
+ this.token = token;
400
+ /** `{ source: "env" | "session", session }`, when a token was resolved. */
401
+ this.identity = identity;
402
+ }
403
+
404
+ authHeaders() {
405
+ return this.token ? { Authorization: `Bearer ${this.token}` } : {};
406
+ }
407
+
408
+ /** The account this client speaks for, read once. */
409
+ me() {
410
+ this.#me ??= this.json("GET /v1/auth/me");
411
+ return this.#me;
412
+ }
413
+
414
+ /** One door's method and absolute URL. */
415
+ url(template, { params, query } = {}) {
416
+ const { method, path: route } = endpointPath(template, params);
417
+ return { method, url: `${this.base.replace(/\/v1$/, "")}${route}${queryString(query)}` };
418
+ }
419
+
420
+ /** A door's method, URL, headers, and payload: `body` goes as JSON,
421
+ * `raw: { data, type }` as the bytes it is. */
422
+ #prepare(template, { params, query, body, raw, headers, idempotencyKey, accept }) {
423
+ const { method, url } = this.url(template, { params, query });
424
+ const payload = raw ? raw.data : body === undefined ? undefined : JSON.stringify(body);
425
+ return {
426
+ method,
427
+ url,
428
+ headers: {
429
+ ...(accept ? { Accept: accept } : {}),
430
+ ...(raw ? { "Content-Type": raw.type } : payload === undefined ? {} : { "Content-Type": "application/json" }),
431
+ ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
432
+ ...this.authHeaders(),
433
+ ...(headers ?? {}),
434
+ },
435
+ body: payload,
436
+ bodyBytes: payload === undefined ? 0 : Buffer.byteLength(payload),
437
+ };
438
+ }
439
+
440
+ /**
441
+ * One request through a door. `body` is sent as JSON; `raw: { data, type }`
442
+ * sends bytes as they are. `idempotencyKey` rides the Idempotency-Key
443
+ * header, which also makes the request safe to repeat.
444
+ */
445
+ request(template, options = {}) {
446
+ const as = options.as ?? "json";
447
+ const prepared = this.#prepare(template, {
448
+ ...options,
449
+ accept: as === "json" ? "application/json" : "*/*",
450
+ });
451
+ return send(prepared.url, {
452
+ ...prepared,
453
+ timeoutMs: options.timeoutMs,
454
+ expectedBytes: options.expectedBytes,
455
+ signal: options.signal,
456
+ as,
457
+ });
458
+ }
459
+
460
+ json(template, options = {}) {
461
+ return this.request(template, { ...options, as: "json" });
462
+ }
463
+
464
+ bytes(template, options = {}) {
465
+ return this.request(template, { ...options, as: "bytes" });
466
+ }
467
+
468
+ /** One request through a door, answered the way `openResponse` answers:
469
+ * the Response as soon as it starts, its body unread and its status the
470
+ * caller's to judge (an icon's 304 is an answer, not a failure). */
471
+ open(template, options = {}) {
472
+ const prepared = this.#prepare(template, options);
473
+ return openResponse(prepared.url, {
474
+ ...prepared,
475
+ timeoutMs: options.timeoutMs,
476
+ signal: options.signal,
477
+ });
478
+ }
479
+
480
+ /** `open`, for a door whose answer is read as it arrives (a turn's SSE
481
+ * frames): a refusal throws the CliError `json` would, with what the
482
+ * backend said. */
483
+ async stream(template, options = {}) {
484
+ const response = await this.open(template, options);
485
+ if (response.ok) return response;
486
+ const bytes = Buffer.from(await response.arrayBuffer().catch(() => new ArrayBuffer(0)));
487
+ throw responseError(response.status, bytes, response.headers);
488
+ }
489
+ }
490
+
491
+ /**
492
+ * The client a command talks to Terminus with: its API base, and the token
493
+ * `auth` asks for — "required" (the default; exit 3 when nobody is signed
494
+ * in), "optional" (anonymous when nobody is), or "none".
495
+ */
496
+ export async function connect(flags = {}, { auth = "required" } = {}) {
497
+ const base = await commandApiBase(flags);
498
+ const identity = auth === "none" ? null : await resolveToken({ required: auth === "required" });
499
+ return new Api({ base, token: identity?.token ?? null, identity });
500
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "source": "just-bash@3.2.0 getCommandNames() — the version terminus-backend's bashd pins",
3
+ "commands": [
4
+ "alias",
5
+ "awk",
6
+ "base64",
7
+ "basename",
8
+ "bash",
9
+ "cat",
10
+ "chmod",
11
+ "clear",
12
+ "column",
13
+ "comm",
14
+ "cp",
15
+ "cut",
16
+ "date",
17
+ "diff",
18
+ "dirname",
19
+ "du",
20
+ "echo",
21
+ "egrep",
22
+ "env",
23
+ "expand",
24
+ "expr",
25
+ "false",
26
+ "fgrep",
27
+ "file",
28
+ "find",
29
+ "fold",
30
+ "grep",
31
+ "gunzip",
32
+ "gzip",
33
+ "head",
34
+ "help",
35
+ "history",
36
+ "hostname",
37
+ "html-to-markdown",
38
+ "join",
39
+ "jq",
40
+ "ln",
41
+ "ls",
42
+ "md5sum",
43
+ "mkdir",
44
+ "mv",
45
+ "nl",
46
+ "od",
47
+ "paste",
48
+ "printenv",
49
+ "printf",
50
+ "pwd",
51
+ "readlink",
52
+ "rev",
53
+ "rg",
54
+ "rm",
55
+ "rmdir",
56
+ "sed",
57
+ "seq",
58
+ "sh",
59
+ "sha1sum",
60
+ "sha256sum",
61
+ "sleep",
62
+ "sort",
63
+ "split",
64
+ "sqlite3",
65
+ "stat",
66
+ "strings",
67
+ "tac",
68
+ "tail",
69
+ "tar",
70
+ "tee",
71
+ "time",
72
+ "timeout",
73
+ "touch",
74
+ "tr",
75
+ "tree",
76
+ "true",
77
+ "unalias",
78
+ "unexpand",
79
+ "uniq",
80
+ "wc",
81
+ "which",
82
+ "whoami",
83
+ "xan",
84
+ "xargs",
85
+ "yq",
86
+ "zcat"
87
+ ]
88
+ }