skydive-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -0
- package/dist/js/bin.mjs +2221 -0
- package/dist/js/boot-ChlVx-ts.mjs +5525 -0
- package/dist/js/print-BHbFMxQv.mjs +442 -0
- package/package.json +57 -0
package/dist/js/bin.mjs
ADDED
|
@@ -0,0 +1,2221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { hideBin } from "yargs/helpers";
|
|
3
|
+
import yargs from "yargs";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import Conf from "conf";
|
|
7
|
+
import { err, ok } from "neverthrow";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import open from "open";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import zlib from "node:zlib";
|
|
14
|
+
|
|
15
|
+
//#region package.json
|
|
16
|
+
var version$1 = "0.1.0";
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/types.ts
|
|
20
|
+
const NON_INTERACTIVE_ENV_VARS = [
|
|
21
|
+
"CI",
|
|
22
|
+
"CLAUDECODE",
|
|
23
|
+
"CODEX",
|
|
24
|
+
"OPENCLAW"
|
|
25
|
+
];
|
|
26
|
+
function isNonInteractive() {
|
|
27
|
+
return NON_INTERACTIVE_ENV_VARS.some((key) => process.env[key]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/config.ts
|
|
32
|
+
/** Default host for the public management API (`/v1`, API-key auth). */
|
|
33
|
+
const DEFAULT_API_URL = "https://api.skydive.com";
|
|
34
|
+
/**
|
|
35
|
+
* Default origin for the interactive chat client (`skydive chat`).
|
|
36
|
+
*
|
|
37
|
+
* The API host that serves better-auth (`/api/auth/*`) and the internal tRPC
|
|
38
|
+
* API (`/api/v1/trpc`) that chat streams over. We target the API host
|
|
39
|
+
* directly (not the web front door) because chat opens a WebSocket and
|
|
40
|
+
* authenticates with a bearer token on the upgrade request. Same host as
|
|
41
|
+
* `DEFAULT_API_URL`; override with `--api-url` / `SKYDIVE_APP_URL` for local
|
|
42
|
+
* dev or while the DNS record is still being provisioned.
|
|
43
|
+
*/
|
|
44
|
+
const DEFAULT_APP_URL = "https://api.skydive.com";
|
|
45
|
+
/** Web front door, for pages opened in the user's browser. */
|
|
46
|
+
const DEFAULT_WEB_URL = "https://skydive.com";
|
|
47
|
+
/**
|
|
48
|
+
* Origin for browser-facing links (e.g. opening a conversation's web page).
|
|
49
|
+
* The app origin is the API host, which serves no web UI in production, so
|
|
50
|
+
* map the default to the web front door. Overridden origins (local dev,
|
|
51
|
+
* previews) serve both and pass through unchanged.
|
|
52
|
+
*/
|
|
53
|
+
function resolveWebUrl(appUrl) {
|
|
54
|
+
return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
|
|
55
|
+
}
|
|
56
|
+
/** Prefix on every Skydive API key. Kept in sync with the API's
|
|
57
|
+
* `API_KEY_PREFIX` (`apps/anyone/api/src/lib/api-key.ts`); the CLI is a
|
|
58
|
+
* standalone published package so it can't import the backend constant. */
|
|
59
|
+
const API_KEY_PREFIX = "sky_live_";
|
|
60
|
+
/** Where users mint and copy API keys. Shown in the login prompt. */
|
|
61
|
+
const API_KEYS_URL = "skydive.com/account";
|
|
62
|
+
const store = new Conf({
|
|
63
|
+
projectName: process.env["SKYDIVE_CONFIG_NAME"] ?? "skydive",
|
|
64
|
+
projectSuffix: "",
|
|
65
|
+
configFileMode: 384
|
|
66
|
+
});
|
|
67
|
+
function resolveConfig(opts) {
|
|
68
|
+
const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
|
|
69
|
+
const apiUrl = process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL;
|
|
70
|
+
if (!apiKey) return err({ message: "Not authenticated. Run `skydive auth login` first." });
|
|
71
|
+
return ok({
|
|
72
|
+
apiKey,
|
|
73
|
+
apiUrl
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the bearer credential for the management API (`agents` / `keys` /
|
|
78
|
+
* `secrets`). Prefers an API key (`SKYDIVE_API_KEY` env, then stored), and
|
|
79
|
+
* falls back to the `--web` chat session token: the server's `/v1` gate routes
|
|
80
|
+
* any non-`sky_` bearer through the signed-in session, so a device login alone
|
|
81
|
+
* is enough to run management commands — no separate API key required.
|
|
82
|
+
*/
|
|
83
|
+
function resolveManagementAuth(opts) {
|
|
84
|
+
const apiUrl = process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL;
|
|
85
|
+
const apiKey = process.env["SKYDIVE_API_KEY"] ?? store.get("apiKey");
|
|
86
|
+
if (apiKey) return ok({
|
|
87
|
+
token: apiKey,
|
|
88
|
+
apiUrl,
|
|
89
|
+
kind: "api-key"
|
|
90
|
+
});
|
|
91
|
+
const sessionToken = process.env["SKYDIVE_SESSION_TOKEN"] ?? store.get("sessionToken");
|
|
92
|
+
if (sessionToken) return ok({
|
|
93
|
+
token: sessionToken,
|
|
94
|
+
apiUrl,
|
|
95
|
+
kind: "session"
|
|
96
|
+
});
|
|
97
|
+
return err({ message: "Not authenticated. Run `skydive auth login` (API key) or `skydive auth login --web`." });
|
|
98
|
+
}
|
|
99
|
+
function saveConfig(config) {
|
|
100
|
+
store.set("apiKey", config.apiKey);
|
|
101
|
+
store.set("apiUrl", config.apiUrl);
|
|
102
|
+
}
|
|
103
|
+
function deleteConfig() {
|
|
104
|
+
store.clear();
|
|
105
|
+
}
|
|
106
|
+
function getConfigPath() {
|
|
107
|
+
return store.path;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Where the chat TUI persists its prompt history (up-arrow recall). Kept
|
|
111
|
+
* beside the config file so all CLI state lives in one directory.
|
|
112
|
+
*/
|
|
113
|
+
function getPromptHistoryPath() {
|
|
114
|
+
return path.join(path.dirname(store.path), "prompt-history.jsonl");
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the chat/auth origin. Precedence: `SKYDIVE_APP_URL` env > explicit
|
|
118
|
+
* `--api-url` style override > stored value > `SKYDIVE_API_URL` env >
|
|
119
|
+
* `DEFAULT_APP_URL`.
|
|
120
|
+
*
|
|
121
|
+
* The `SKYDIVE_API_URL` fallback matters for previews: the device/`--web`
|
|
122
|
+
* flow and chat hit the same api service as the management API, so pointing
|
|
123
|
+
* `SKYDIVE_API_URL` at a preview stack is enough — you don't also have to set
|
|
124
|
+
* `SKYDIVE_APP_URL`. Otherwise auth would silently fall through to prod
|
|
125
|
+
* (`DEFAULT_APP_URL`) and hand back a prod verification URL.
|
|
126
|
+
*/
|
|
127
|
+
function resolveAppUrl(opts) {
|
|
128
|
+
return process.env["SKYDIVE_APP_URL"] ?? opts.appUrl ?? store.get("appUrl") ?? process.env["SKYDIVE_API_URL"] ?? DEFAULT_APP_URL;
|
|
129
|
+
}
|
|
130
|
+
function resolveSession(opts) {
|
|
131
|
+
const sessionToken = process.env["SKYDIVE_SESSION_TOKEN"] ?? store.get("sessionToken");
|
|
132
|
+
const appUrl = resolveAppUrl(opts);
|
|
133
|
+
if (!sessionToken) return err({ message: "Not signed in for chat. Run `skydive chat` to sign in." });
|
|
134
|
+
return ok({
|
|
135
|
+
sessionToken,
|
|
136
|
+
appUrl
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function saveSession(session) {
|
|
140
|
+
store.set("sessionToken", session.sessionToken);
|
|
141
|
+
store.set("sessionObtainedAt", (/* @__PURE__ */ new Date()).toISOString());
|
|
142
|
+
store.set("appUrl", session.appUrl);
|
|
143
|
+
}
|
|
144
|
+
function getSavedTheme(mode) {
|
|
145
|
+
return store.get(mode === "dark" ? "themeDark" : "themeLight");
|
|
146
|
+
}
|
|
147
|
+
function saveTheme(mode, themeId) {
|
|
148
|
+
store.set(mode === "dark" ? "themeDark" : "themeLight", themeId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/api-client.ts
|
|
153
|
+
const USER_AGENT = `skydive-cli/${version$1}`;
|
|
154
|
+
const AgentSchema = z.object({
|
|
155
|
+
id: z.string(),
|
|
156
|
+
name: z.string(),
|
|
157
|
+
url: z.string().nullable(),
|
|
158
|
+
description: z.string().nullable(),
|
|
159
|
+
model: z.string().nullable(),
|
|
160
|
+
gitUrl: z.string()
|
|
161
|
+
});
|
|
162
|
+
const ListAgentsResponseSchema = z.object({
|
|
163
|
+
agents: z.array(AgentSchema),
|
|
164
|
+
count: z.number(),
|
|
165
|
+
nextCursor: z.string().nullable()
|
|
166
|
+
});
|
|
167
|
+
const AgentResponseSchema = z.object({ agent: AgentSchema });
|
|
168
|
+
const ApiKeySchema = z.object({
|
|
169
|
+
id: z.string(),
|
|
170
|
+
name: z.string(),
|
|
171
|
+
prefix: z.string(),
|
|
172
|
+
lastUsedAt: z.string().nullable(),
|
|
173
|
+
createdAt: z.string()
|
|
174
|
+
});
|
|
175
|
+
const CreatedApiKeySchema = ApiKeySchema.extend({ key: z.string() });
|
|
176
|
+
const ListApiKeysResponseSchema = z.object({ keys: z.array(ApiKeySchema) });
|
|
177
|
+
const ListSecretKeysResponseSchema = z.object({ keys: z.array(z.string()) });
|
|
178
|
+
const SetSecretResponseSchema = z.object({ key: z.string() });
|
|
179
|
+
var SkydiveApiClient = class {
|
|
180
|
+
baseUrl;
|
|
181
|
+
token;
|
|
182
|
+
constructor(config) {
|
|
183
|
+
this.baseUrl = `${config.apiUrl.replace(/\/$/, "")}/v1`;
|
|
184
|
+
this.token = config.token;
|
|
185
|
+
}
|
|
186
|
+
async listAgents(params) {
|
|
187
|
+
const query = new URLSearchParams();
|
|
188
|
+
if (params.scope) query.set("scope", params.scope);
|
|
189
|
+
if (params.limit) query.set("limit", String(params.limit));
|
|
190
|
+
if (params.cursor) query.set("cursor", params.cursor);
|
|
191
|
+
const qs = query.toString();
|
|
192
|
+
return this.request({
|
|
193
|
+
method: "GET",
|
|
194
|
+
path: `/agents${qs ? `?${qs}` : ""}`,
|
|
195
|
+
schema: ListAgentsResponseSchema
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async getAgent(id) {
|
|
199
|
+
return this.request({
|
|
200
|
+
method: "GET",
|
|
201
|
+
path: `/agents/${encodeURIComponent(id)}`,
|
|
202
|
+
schema: AgentResponseSchema
|
|
203
|
+
}).then((r) => r.map((res) => res.agent));
|
|
204
|
+
}
|
|
205
|
+
async createAgent(params) {
|
|
206
|
+
return this.request({
|
|
207
|
+
method: "POST",
|
|
208
|
+
path: "/agents",
|
|
209
|
+
body: params,
|
|
210
|
+
schema: AgentResponseSchema
|
|
211
|
+
}).then((r) => r.map((res) => res.agent));
|
|
212
|
+
}
|
|
213
|
+
async listKeys(agentId) {
|
|
214
|
+
return this.request({
|
|
215
|
+
method: "GET",
|
|
216
|
+
path: `/agents/${encodeURIComponent(agentId)}/api-keys`,
|
|
217
|
+
schema: ListApiKeysResponseSchema
|
|
218
|
+
}).then((r) => r.map((res) => res.keys));
|
|
219
|
+
}
|
|
220
|
+
async createKey(agentId, name) {
|
|
221
|
+
return this.request({
|
|
222
|
+
method: "POST",
|
|
223
|
+
path: `/agents/${encodeURIComponent(agentId)}/api-keys`,
|
|
224
|
+
body: { name },
|
|
225
|
+
schema: CreatedApiKeySchema
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async revokeKey(agentId, id) {
|
|
229
|
+
return this.request({
|
|
230
|
+
method: "DELETE",
|
|
231
|
+
path: `/agents/${encodeURIComponent(agentId)}/api-keys/${encodeURIComponent(id)}`
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async listSecrets(agentId) {
|
|
235
|
+
return this.request({
|
|
236
|
+
method: "GET",
|
|
237
|
+
path: `/agents/${encodeURIComponent(agentId)}/secrets`,
|
|
238
|
+
schema: ListSecretKeysResponseSchema
|
|
239
|
+
}).then((r) => r.map((res) => res.keys));
|
|
240
|
+
}
|
|
241
|
+
async setSecret(agentId, key, value) {
|
|
242
|
+
return this.request({
|
|
243
|
+
method: "PUT",
|
|
244
|
+
path: `/agents/${encodeURIComponent(agentId)}/secrets/${encodeURIComponent(key)}`,
|
|
245
|
+
body: { value },
|
|
246
|
+
schema: SetSecretResponseSchema
|
|
247
|
+
}).then((r) => r.map((res) => res.key));
|
|
248
|
+
}
|
|
249
|
+
async deleteSecret(agentId, key) {
|
|
250
|
+
return this.request({
|
|
251
|
+
method: "DELETE",
|
|
252
|
+
path: `/agents/${encodeURIComponent(agentId)}/secrets/${encodeURIComponent(key)}`
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async request(params) {
|
|
256
|
+
const url = `${this.baseUrl}${params.path}`;
|
|
257
|
+
const headers = {
|
|
258
|
+
authorization: `Bearer ${this.token}`,
|
|
259
|
+
"user-agent": USER_AGENT
|
|
260
|
+
};
|
|
261
|
+
if (params.body) headers["content-type"] = "application/json";
|
|
262
|
+
let response;
|
|
263
|
+
try {
|
|
264
|
+
response = await fetch(url, {
|
|
265
|
+
method: params.method,
|
|
266
|
+
headers,
|
|
267
|
+
body: params.body ? JSON.stringify(params.body) : void 0
|
|
268
|
+
});
|
|
269
|
+
} catch (e) {
|
|
270
|
+
return err({
|
|
271
|
+
message: `Network error: ${e instanceof Error ? e.message : String(e)}`,
|
|
272
|
+
status: null
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
let message = `HTTP ${response.status}`;
|
|
277
|
+
try {
|
|
278
|
+
const body = await response.json();
|
|
279
|
+
if (body.error && typeof body.error === "string") message = body.error;
|
|
280
|
+
else if (body.error?.message) message = body.error.message;
|
|
281
|
+
} catch {}
|
|
282
|
+
return err({
|
|
283
|
+
message,
|
|
284
|
+
status: response.status
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (response.status === 204 || !params.schema) return ok(void 0);
|
|
288
|
+
let body;
|
|
289
|
+
try {
|
|
290
|
+
body = await response.json();
|
|
291
|
+
} catch {
|
|
292
|
+
return err({
|
|
293
|
+
message: "Invalid JSON response",
|
|
294
|
+
status: response.status
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const parsed = params.schema.safeParse(body);
|
|
298
|
+
if (!parsed.success) return err({
|
|
299
|
+
message: `Unexpected response shape: ${parsed.error.message}`,
|
|
300
|
+
status: response.status
|
|
301
|
+
});
|
|
302
|
+
return ok(parsed.data);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/auth/device.ts
|
|
308
|
+
const CLI_CLIENT_ID = "skydive-cli";
|
|
309
|
+
const deviceCodeResponseSchema = z.object({
|
|
310
|
+
device_code: z.string(),
|
|
311
|
+
user_code: z.string(),
|
|
312
|
+
verification_uri: z.string(),
|
|
313
|
+
verification_uri_complete: z.string().optional(),
|
|
314
|
+
expires_in: z.number().int().positive(),
|
|
315
|
+
interval: z.number().int().positive().optional()
|
|
316
|
+
});
|
|
317
|
+
const tokenSuccessSchema = z.object({
|
|
318
|
+
access_token: z.string(),
|
|
319
|
+
token_type: z.string().optional(),
|
|
320
|
+
expires_in: z.number().int().optional(),
|
|
321
|
+
refresh_token: z.string().optional()
|
|
322
|
+
});
|
|
323
|
+
const tokenErrorSchema = z.object({
|
|
324
|
+
error: z.enum([
|
|
325
|
+
"authorization_pending",
|
|
326
|
+
"slow_down",
|
|
327
|
+
"access_denied",
|
|
328
|
+
"expired_token",
|
|
329
|
+
"invalid_grant",
|
|
330
|
+
"invalid_request",
|
|
331
|
+
"invalid_client"
|
|
332
|
+
]),
|
|
333
|
+
error_description: z.string().optional()
|
|
334
|
+
});
|
|
335
|
+
const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
336
|
+
async function requestDeviceCode({ appUrl, signal }) {
|
|
337
|
+
const res = await fetch(`${appUrl}/api/auth/device/code`, {
|
|
338
|
+
method: "POST",
|
|
339
|
+
headers: { "content-type": "application/json" },
|
|
340
|
+
body: JSON.stringify({ client_id: CLI_CLIENT_ID }),
|
|
341
|
+
signal
|
|
342
|
+
});
|
|
343
|
+
if (!res.ok) throw new Error(`device/code failed: ${res.status} ${await res.text().catch(() => "")}`);
|
|
344
|
+
return deviceCodeResponseSchema.parse(await res.json());
|
|
345
|
+
}
|
|
346
|
+
async function pollDeviceToken({ appUrl, deviceCode, currentIntervalMs, signal }) {
|
|
347
|
+
let res;
|
|
348
|
+
try {
|
|
349
|
+
res = await fetch(`${appUrl}/api/auth/device/token`, {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: { "content-type": "application/json" },
|
|
352
|
+
body: JSON.stringify({
|
|
353
|
+
grant_type: GRANT_TYPE,
|
|
354
|
+
device_code: deviceCode,
|
|
355
|
+
client_id: CLI_CLIENT_ID
|
|
356
|
+
}),
|
|
357
|
+
signal
|
|
358
|
+
});
|
|
359
|
+
} catch (e) {
|
|
360
|
+
return {
|
|
361
|
+
kind: "error",
|
|
362
|
+
message: e instanceof Error ? e.message : String(e)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const body = await res.json().catch(() => null);
|
|
366
|
+
if (res.ok) {
|
|
367
|
+
const parsed = tokenSuccessSchema.safeParse(body);
|
|
368
|
+
if (!parsed.success) return {
|
|
369
|
+
kind: "error",
|
|
370
|
+
message: "malformed token response"
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
kind: "success",
|
|
374
|
+
accessToken: parsed.data.access_token,
|
|
375
|
+
refreshToken: parsed.data.refresh_token ?? null
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const parsed = tokenErrorSchema.safeParse(body);
|
|
379
|
+
if (!parsed.success) return {
|
|
380
|
+
kind: "error",
|
|
381
|
+
message: `device/token failed: ${res.status}`
|
|
382
|
+
};
|
|
383
|
+
switch (parsed.data.error) {
|
|
384
|
+
case "authorization_pending": return {
|
|
385
|
+
kind: "pending",
|
|
386
|
+
nextIntervalMs: currentIntervalMs
|
|
387
|
+
};
|
|
388
|
+
case "slow_down": return {
|
|
389
|
+
kind: "pending",
|
|
390
|
+
nextIntervalMs: currentIntervalMs + 5e3
|
|
391
|
+
};
|
|
392
|
+
case "access_denied": return { kind: "denied" };
|
|
393
|
+
case "expired_token": return { kind: "expired" };
|
|
394
|
+
default: return {
|
|
395
|
+
kind: "error",
|
|
396
|
+
message: parsed.data.error_description ?? parsed.data.error
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
//#endregion
|
|
402
|
+
//#region src/auth/organization.ts
|
|
403
|
+
const workspaceSchema = z.object({
|
|
404
|
+
id: z.string(),
|
|
405
|
+
name: z.string(),
|
|
406
|
+
slug: z.string()
|
|
407
|
+
});
|
|
408
|
+
function authHeaders(sessionToken) {
|
|
409
|
+
return { authorization: `Bearer ${sessionToken}` };
|
|
410
|
+
}
|
|
411
|
+
async function listWorkspaces({ appUrl, sessionToken }) {
|
|
412
|
+
try {
|
|
413
|
+
const res = await fetch(`${appUrl}/api/auth/organization/list`, { headers: authHeaders(sessionToken) });
|
|
414
|
+
if (!res.ok) return err({ message: `failed to list workspaces (${res.status})` });
|
|
415
|
+
const parsed = z.array(workspaceSchema).safeParse(await res.json());
|
|
416
|
+
if (!parsed.success) return err({ message: "unexpected workspace list response" });
|
|
417
|
+
return ok(parsed.data);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/** The workspace bound to the current session, or `null` if none is set. */
|
|
423
|
+
async function getActiveWorkspaceId({ appUrl, sessionToken }) {
|
|
424
|
+
try {
|
|
425
|
+
const res = await fetch(`${appUrl}/api/auth/get-session?disableCookieCache=true`, { headers: authHeaders(sessionToken) });
|
|
426
|
+
if (!res.ok) return err({ message: `failed to read session (${res.status})` });
|
|
427
|
+
const parsed = z.object({ session: z.object({ activeOrganizationId: z.string().nullable().optional() }).nullable().optional() }).nullable().safeParse(await res.json());
|
|
428
|
+
if (!parsed.success) return err({ message: "unexpected session response" });
|
|
429
|
+
return ok(parsed.data?.session?.activeOrganizationId ?? null);
|
|
430
|
+
} catch (e) {
|
|
431
|
+
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function setActiveWorkspace({ appUrl, sessionToken, organizationId }) {
|
|
435
|
+
try {
|
|
436
|
+
const res = await fetch(`${appUrl}/api/auth/organization/set-active`, {
|
|
437
|
+
method: "POST",
|
|
438
|
+
headers: {
|
|
439
|
+
...authHeaders(sessionToken),
|
|
440
|
+
"content-type": "application/json"
|
|
441
|
+
},
|
|
442
|
+
body: JSON.stringify({ organizationId })
|
|
443
|
+
});
|
|
444
|
+
if (!res.ok) return err({ message: `failed to set active workspace (${res.status})` });
|
|
445
|
+
return ok(void 0);
|
|
446
|
+
} catch (e) {
|
|
447
|
+
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Ensures the session has an active workspace. The device flow issues a
|
|
452
|
+
* session without one (unlike a normal web sign-in, which sets it), so the
|
|
453
|
+
* internal API rejects every request with "no active organization" until we
|
|
454
|
+
* set it. Picks the account's first workspace by join order — run `skydive
|
|
455
|
+
* workspace list` + `skydive workspace switch` afterward if that's the wrong
|
|
456
|
+
* one (e.g. a personal workspace joined before a shared team workspace).
|
|
457
|
+
*/
|
|
458
|
+
async function ensureActiveOrganization({ appUrl, sessionToken }) {
|
|
459
|
+
const workspaces = await listWorkspaces({
|
|
460
|
+
appUrl,
|
|
461
|
+
sessionToken
|
|
462
|
+
});
|
|
463
|
+
if (workspaces.isErr()) return err(workspaces.error);
|
|
464
|
+
const workspace = workspaces.value[0];
|
|
465
|
+
if (!workspace) return err({ message: "your account has no organization yet" });
|
|
466
|
+
const setResult = await setActiveWorkspace({
|
|
467
|
+
appUrl,
|
|
468
|
+
sessionToken,
|
|
469
|
+
organizationId: workspace.id
|
|
470
|
+
});
|
|
471
|
+
if (setResult.isErr()) return err(setResult.error);
|
|
472
|
+
return ok({
|
|
473
|
+
organizationId: workspace.id,
|
|
474
|
+
name: workspace.name
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/auth/device-login.ts
|
|
480
|
+
const DEFAULT_INTERVAL_MS = 5e3;
|
|
481
|
+
/**
|
|
482
|
+
* Headless device-authorization login for the chat session. Prints the
|
|
483
|
+
* verification URL + user code to stderr (stdout stays clean for piping),
|
|
484
|
+
* best-effort opens the browser, polls until the user approves, then
|
|
485
|
+
* persists the session. Node-compatible — no TUI dependencies — so it can
|
|
486
|
+
* run as a plain command or be awaited by `skydive chat` before booting the
|
|
487
|
+
* interface.
|
|
488
|
+
*/
|
|
489
|
+
async function loginWithDevice({ appUrl, openBrowser = true }) {
|
|
490
|
+
let code;
|
|
491
|
+
try {
|
|
492
|
+
code = await requestDeviceCode({ appUrl });
|
|
493
|
+
} catch (e) {
|
|
494
|
+
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
495
|
+
}
|
|
496
|
+
const verificationUrl = code.verification_uri_complete ?? code.verification_uri;
|
|
497
|
+
process.stderr.write(`\nTo sign in, open:\n ${verificationUrl}\n`);
|
|
498
|
+
process.stderr.write(`and enter the code: ${formatUserCode(code.user_code)}\n\n`);
|
|
499
|
+
if (openBrowser) await open(verificationUrl).catch(() => void 0);
|
|
500
|
+
process.stderr.write("Waiting for approval…\n");
|
|
501
|
+
let intervalMs = (code.interval ?? 5) * 1e3 || DEFAULT_INTERVAL_MS;
|
|
502
|
+
const expiresAt = Date.now() + code.expires_in * 1e3;
|
|
503
|
+
while (Date.now() < expiresAt) {
|
|
504
|
+
await sleep(intervalMs);
|
|
505
|
+
const result = await pollDeviceToken({
|
|
506
|
+
appUrl,
|
|
507
|
+
deviceCode: code.device_code,
|
|
508
|
+
currentIntervalMs: intervalMs
|
|
509
|
+
});
|
|
510
|
+
switch (result.kind) {
|
|
511
|
+
case "success": {
|
|
512
|
+
const org = await ensureActiveOrganization({
|
|
513
|
+
appUrl,
|
|
514
|
+
sessionToken: result.accessToken
|
|
515
|
+
});
|
|
516
|
+
if (org.isErr()) return err({ message: org.error.message });
|
|
517
|
+
saveSession({
|
|
518
|
+
sessionToken: result.accessToken,
|
|
519
|
+
appUrl
|
|
520
|
+
});
|
|
521
|
+
return ok({
|
|
522
|
+
appUrl,
|
|
523
|
+
sessionToken: result.accessToken
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
case "pending":
|
|
527
|
+
intervalMs = result.nextIntervalMs;
|
|
528
|
+
break;
|
|
529
|
+
case "denied": return err({ message: "Authorization was denied." });
|
|
530
|
+
case "expired": return err({ message: "The device code expired. Please try again." });
|
|
531
|
+
case "error": break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return err({ message: "Timed out waiting for authorization." });
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Insert a dash in the middle of an 8-char code so it's easier to read and
|
|
538
|
+
* type. better-auth strips dashes server-side.
|
|
539
|
+
*/
|
|
540
|
+
function formatUserCode(code) {
|
|
541
|
+
if (code.length !== 8) return code;
|
|
542
|
+
return `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
543
|
+
}
|
|
544
|
+
function sleep(ms) {
|
|
545
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/output.ts
|
|
550
|
+
function output(argv, data) {
|
|
551
|
+
if (argv.json) {
|
|
552
|
+
console.log(JSON.stringify(data, null, 2));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (typeof data === "string") {
|
|
556
|
+
console.log(data);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
console.log(JSON.stringify(data, null, 2));
|
|
560
|
+
}
|
|
561
|
+
function printTable(headers, rows) {
|
|
562
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
563
|
+
const pad = (s, w) => s.padEnd(w);
|
|
564
|
+
const line = (cells) => cells.map((c, i) => pad(c, widths[i] ?? 0)).join(" ");
|
|
565
|
+
console.log(line(headers));
|
|
566
|
+
console.log(widths.map((w) => "-".repeat(w)).join(" "));
|
|
567
|
+
for (const row of rows) console.log(line(row));
|
|
568
|
+
}
|
|
569
|
+
function printError(message) {
|
|
570
|
+
console.error(`Error: ${message}`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/commands/auth.ts
|
|
575
|
+
const loginCommand = {
|
|
576
|
+
command: "login",
|
|
577
|
+
describe: "Authenticate with a Skydive API key (or --web for chat)",
|
|
578
|
+
builder: (y) => y.option("api-key", {
|
|
579
|
+
type: "string",
|
|
580
|
+
describe: `API key (${API_KEY_PREFIX}...)`
|
|
581
|
+
}).option("web", {
|
|
582
|
+
type: "boolean",
|
|
583
|
+
default: false,
|
|
584
|
+
describe: "Sign in for `skydive chat` via the browser (device flow) instead of an API key"
|
|
585
|
+
}),
|
|
586
|
+
handler: async (argv) => {
|
|
587
|
+
if (argv.web) {
|
|
588
|
+
await runWebLogin(argv);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
let apiKey = argv["api-key"] ?? process.env["SKYDIVE_API_KEY"];
|
|
592
|
+
if (!apiKey) {
|
|
593
|
+
const rl = createInterface({
|
|
594
|
+
input: process.stdin,
|
|
595
|
+
output: process.stderr
|
|
596
|
+
});
|
|
597
|
+
apiKey = await new Promise((resolve) => {
|
|
598
|
+
rl.question(`Enter your API key (from ${API_KEYS_URL}): `, (answer) => {
|
|
599
|
+
rl.close();
|
|
600
|
+
resolve(answer.trim());
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (!apiKey) {
|
|
605
|
+
printError("No API key provided.");
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
if (!apiKey.startsWith(API_KEY_PREFIX)) {
|
|
609
|
+
printError(`API key must start with ${API_KEY_PREFIX}`);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
const apiUrl = argv["api-url"] ?? DEFAULT_API_URL;
|
|
613
|
+
const result = await new SkydiveApiClient({
|
|
614
|
+
token: apiKey,
|
|
615
|
+
apiUrl
|
|
616
|
+
}).listAgents({ limit: 1 });
|
|
617
|
+
if (result.isErr()) {
|
|
618
|
+
printError(`Invalid API key or unreachable server: ${result.error.message}`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
saveConfig({
|
|
622
|
+
apiKey,
|
|
623
|
+
apiUrl
|
|
624
|
+
});
|
|
625
|
+
if (argv.json) output(argv, {
|
|
626
|
+
authenticated: true,
|
|
627
|
+
prefix: apiKey.slice(0, 12),
|
|
628
|
+
configPath: getConfigPath()
|
|
629
|
+
});
|
|
630
|
+
else if (!argv.quiet) {
|
|
631
|
+
console.log(`Authenticated successfully.`);
|
|
632
|
+
console.log(` Key: ${apiKey.slice(0, 12)}...`);
|
|
633
|
+
console.log(` Config: ${getConfigPath()}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
async function runWebLogin(argv) {
|
|
638
|
+
if (isNonInteractive()) {
|
|
639
|
+
printError("`auth login --web` needs an interactive terminal. Set SKYDIVE_SESSION_TOKEN for non-interactive use.");
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
const result = await loginWithDevice({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
643
|
+
if (result.isErr()) {
|
|
644
|
+
printError(result.error.message);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
if (argv.json) output(argv, {
|
|
648
|
+
authenticated: true,
|
|
649
|
+
mode: "session",
|
|
650
|
+
appUrl: result.value.appUrl,
|
|
651
|
+
configPath: getConfigPath()
|
|
652
|
+
});
|
|
653
|
+
else if (!argv.quiet) {
|
|
654
|
+
console.log("Signed in for chat.");
|
|
655
|
+
console.log(` App: ${result.value.appUrl}`);
|
|
656
|
+
console.log(` Config: ${getConfigPath()}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const logoutCommand = {
|
|
660
|
+
command: "logout",
|
|
661
|
+
describe: "Clear stored credentials (API key and chat session)",
|
|
662
|
+
handler: async (argv) => {
|
|
663
|
+
deleteConfig();
|
|
664
|
+
if (argv.json) output(argv, { authenticated: false });
|
|
665
|
+
else if (!argv.quiet) console.log("Logged out.");
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
const statusCommand = {
|
|
669
|
+
command: "status",
|
|
670
|
+
describe: "Show current authentication state",
|
|
671
|
+
handler: async (argv) => {
|
|
672
|
+
const apiKey = resolveConfig({ apiUrl: argv["api-url"] });
|
|
673
|
+
const session = resolveSession({});
|
|
674
|
+
if (argv.json) {
|
|
675
|
+
output(argv, {
|
|
676
|
+
authenticated: apiKey.isOk(),
|
|
677
|
+
prefix: apiKey.isOk() ? apiKey.value.apiKey.slice(0, 12) : null,
|
|
678
|
+
apiUrl: apiKey.isOk() ? apiKey.value.apiUrl : null,
|
|
679
|
+
chat: {
|
|
680
|
+
authenticated: session.isOk(),
|
|
681
|
+
appUrl: session.isOk() ? session.value.appUrl : null
|
|
682
|
+
},
|
|
683
|
+
configPath: getConfigPath()
|
|
684
|
+
});
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (apiKey.isErr() && session.isErr()) {
|
|
688
|
+
console.log("Not authenticated. Run `skydive auth login` (API key) or `skydive auth login --web` (chat).");
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (apiKey.isOk()) {
|
|
692
|
+
console.log("API key:");
|
|
693
|
+
console.log(` Key: ${apiKey.value.apiKey.slice(0, 12)}...`);
|
|
694
|
+
console.log(` API: ${apiKey.value.apiUrl}`);
|
|
695
|
+
} else console.log("API key: not configured.");
|
|
696
|
+
if (session.isOk()) {
|
|
697
|
+
console.log("Chat session:");
|
|
698
|
+
console.log(` App: ${session.value.appUrl}`);
|
|
699
|
+
} else console.log("Chat session: not signed in.");
|
|
700
|
+
console.log(`Config: ${getConfigPath()}`);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
const authCommand = {
|
|
704
|
+
command: "auth",
|
|
705
|
+
describe: "Manage authentication",
|
|
706
|
+
builder: (y) => y.command(loginCommand).command(logoutCommand).command(statusCommand).demandCommand(1, "Specify a subcommand: login, logout, status"),
|
|
707
|
+
handler: () => {}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/commands/agents.ts
|
|
712
|
+
function requireClient$2(argv) {
|
|
713
|
+
const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
|
|
714
|
+
if (result.isErr()) {
|
|
715
|
+
printError(result.error.message);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
return new SkydiveApiClient(result.value);
|
|
719
|
+
}
|
|
720
|
+
const listCommand$3 = {
|
|
721
|
+
command: "list",
|
|
722
|
+
describe: "List agents",
|
|
723
|
+
builder: (y) => y.option("limit", {
|
|
724
|
+
type: "number",
|
|
725
|
+
default: 20,
|
|
726
|
+
describe: "Max results"
|
|
727
|
+
}).option("scope", {
|
|
728
|
+
type: "string",
|
|
729
|
+
choices: ["mine", "org"],
|
|
730
|
+
describe: "Filter scope"
|
|
731
|
+
}),
|
|
732
|
+
handler: async (argv) => {
|
|
733
|
+
const result = await requireClient$2(argv).listAgents({
|
|
734
|
+
limit: argv.limit,
|
|
735
|
+
scope: argv.scope
|
|
736
|
+
});
|
|
737
|
+
if (result.isErr()) {
|
|
738
|
+
printError(result.error.message);
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
const { agents } = result.value;
|
|
742
|
+
if (argv.json) {
|
|
743
|
+
output(argv, agents);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (agents.length === 0) {
|
|
747
|
+
console.log("No agents found.");
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
printTable([
|
|
751
|
+
"Name",
|
|
752
|
+
"URL",
|
|
753
|
+
"Model"
|
|
754
|
+
], agents.map((a) => [
|
|
755
|
+
a.name,
|
|
756
|
+
a.url ?? "-",
|
|
757
|
+
a.model ?? "default"
|
|
758
|
+
]));
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
const getCommand = {
|
|
762
|
+
command: "get <id>",
|
|
763
|
+
describe: "Get agent details",
|
|
764
|
+
builder: (y) => y.positional("id", {
|
|
765
|
+
type: "string",
|
|
766
|
+
demandOption: true,
|
|
767
|
+
describe: "Agent ID"
|
|
768
|
+
}),
|
|
769
|
+
handler: async (argv) => {
|
|
770
|
+
const result = await requireClient$2(argv).getAgent(argv.id);
|
|
771
|
+
if (result.isErr()) {
|
|
772
|
+
printError(result.error.message);
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
const agent = result.value;
|
|
776
|
+
if (argv.json) {
|
|
777
|
+
output(argv, agent);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (argv.quiet) {
|
|
781
|
+
console.log(agent.id);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
console.log(`Name: ${agent.name}`);
|
|
785
|
+
console.log(`ID: ${agent.id}`);
|
|
786
|
+
console.log(`Model: ${agent.model ?? "default"}`);
|
|
787
|
+
if (agent.description) console.log(`Description: ${agent.description}`);
|
|
788
|
+
if (agent.url) console.log(`Endpoint: ${agent.url}`);
|
|
789
|
+
console.log(`Git: ${agent.gitUrl}`);
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
const createCommand$1 = {
|
|
793
|
+
command: "create",
|
|
794
|
+
describe: "Create a new agent",
|
|
795
|
+
builder: (y) => y.option("name", {
|
|
796
|
+
type: "string",
|
|
797
|
+
demandOption: true,
|
|
798
|
+
describe: "Agent name"
|
|
799
|
+
}).option("model", {
|
|
800
|
+
type: "string",
|
|
801
|
+
describe: "Model to use"
|
|
802
|
+
}),
|
|
803
|
+
handler: async (argv) => {
|
|
804
|
+
const result = await requireClient$2(argv).createAgent({
|
|
805
|
+
name: argv.name,
|
|
806
|
+
model: argv.model
|
|
807
|
+
});
|
|
808
|
+
if (result.isErr()) {
|
|
809
|
+
printError(result.error.message);
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
const agent = result.value;
|
|
813
|
+
if (argv.json) {
|
|
814
|
+
output(argv, agent);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (argv.quiet) {
|
|
818
|
+
console.log(agent.id);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
console.log(`Agent created.`);
|
|
822
|
+
console.log(` Name: ${agent.name}`);
|
|
823
|
+
console.log(` ID: ${agent.id}`);
|
|
824
|
+
console.log(` Git: ${agent.gitUrl}`);
|
|
825
|
+
if (agent.url) console.log(` API: ${agent.url}`);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
const agentsCommand = {
|
|
829
|
+
command: "agents",
|
|
830
|
+
describe: "Manage agents",
|
|
831
|
+
builder: (y) => y.command(listCommand$3).command(getCommand).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
|
|
832
|
+
handler: () => {}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
//#endregion
|
|
836
|
+
//#region src/commands/keys.ts
|
|
837
|
+
function requireClient$1(argv) {
|
|
838
|
+
const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
|
|
839
|
+
if (result.isErr()) {
|
|
840
|
+
printError(result.error.message);
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
return new SkydiveApiClient(result.value);
|
|
844
|
+
}
|
|
845
|
+
const listCommand$2 = {
|
|
846
|
+
command: "list",
|
|
847
|
+
describe: "List API keys for an agent",
|
|
848
|
+
handler: async (argv) => {
|
|
849
|
+
const result = await requireClient$1(argv).listKeys(argv["agent-id"]);
|
|
850
|
+
if (result.isErr()) {
|
|
851
|
+
printError(result.error.message);
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
const keys = result.value;
|
|
855
|
+
if (argv.json) {
|
|
856
|
+
output(argv, keys);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (keys.length === 0) {
|
|
860
|
+
console.log("No API keys found.");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
printTable([
|
|
864
|
+
"Name",
|
|
865
|
+
"Prefix",
|
|
866
|
+
"Last Used",
|
|
867
|
+
"Created"
|
|
868
|
+
], keys.map((k) => [
|
|
869
|
+
k.name,
|
|
870
|
+
k.prefix,
|
|
871
|
+
k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : "Never",
|
|
872
|
+
new Date(k.createdAt).toLocaleDateString()
|
|
873
|
+
]));
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
const createCommand = {
|
|
877
|
+
command: "create <name>",
|
|
878
|
+
describe: "Create a new API key for an agent",
|
|
879
|
+
builder: (y) => y.positional("name", {
|
|
880
|
+
type: "string",
|
|
881
|
+
demandOption: true,
|
|
882
|
+
describe: "Key name"
|
|
883
|
+
}),
|
|
884
|
+
handler: async (argv) => {
|
|
885
|
+
const result = await requireClient$1(argv).createKey(argv["agent-id"], argv.name);
|
|
886
|
+
if (result.isErr()) {
|
|
887
|
+
printError(result.error.message);
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
const key = result.value;
|
|
891
|
+
if (argv.json) {
|
|
892
|
+
output(argv, key);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (argv.quiet) {
|
|
896
|
+
console.log(key.key);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
console.log(`API key created.`);
|
|
900
|
+
console.log(` Name: ${key.name}`);
|
|
901
|
+
console.log(` Key: ${key.key}`);
|
|
902
|
+
console.log("");
|
|
903
|
+
console.log(" Save this key — it will not be shown again.");
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
const revokeCommand = {
|
|
907
|
+
command: "revoke <id>",
|
|
908
|
+
describe: "Revoke an API key",
|
|
909
|
+
builder: (y) => y.positional("id", {
|
|
910
|
+
type: "string",
|
|
911
|
+
demandOption: true,
|
|
912
|
+
describe: "Key ID"
|
|
913
|
+
}),
|
|
914
|
+
handler: async (argv) => {
|
|
915
|
+
const result = await requireClient$1(argv).revokeKey(argv["agent-id"], argv.id);
|
|
916
|
+
if (result.isErr()) {
|
|
917
|
+
printError(result.error.message);
|
|
918
|
+
process.exit(1);
|
|
919
|
+
}
|
|
920
|
+
if (argv.json) output(argv, {
|
|
921
|
+
revoked: true,
|
|
922
|
+
id: argv.id
|
|
923
|
+
});
|
|
924
|
+
else if (!argv.quiet) console.log(`Key ${argv.id} revoked.`);
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
const keysCommand = {
|
|
928
|
+
command: "keys",
|
|
929
|
+
describe: "Manage API keys for an agent",
|
|
930
|
+
builder: (y) => y.option("agent-id", {
|
|
931
|
+
type: "string",
|
|
932
|
+
demandOption: true,
|
|
933
|
+
describe: "Agent ID"
|
|
934
|
+
}).command(listCommand$2).command(createCommand).command(revokeCommand).demandCommand(1, "Specify a subcommand: list, create, revoke"),
|
|
935
|
+
handler: () => {}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
//#endregion
|
|
939
|
+
//#region src/commands/secrets.ts
|
|
940
|
+
function requireClient(argv) {
|
|
941
|
+
const result = resolveManagementAuth({ apiUrl: argv["api-url"] });
|
|
942
|
+
if (result.isErr()) {
|
|
943
|
+
printError(result.error.message);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
return new SkydiveApiClient(result.value);
|
|
947
|
+
}
|
|
948
|
+
/** Read all of stdin as UTF-8, trimming a single trailing newline. */
|
|
949
|
+
async function readStdin() {
|
|
950
|
+
const chunks = [];
|
|
951
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
952
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
953
|
+
}
|
|
954
|
+
const listCommand$1 = {
|
|
955
|
+
command: "list",
|
|
956
|
+
describe: "List secret names for an agent (values are never shown)",
|
|
957
|
+
handler: async (argv) => {
|
|
958
|
+
const result = await requireClient(argv).listSecrets(argv["agent-id"]);
|
|
959
|
+
if (result.isErr()) {
|
|
960
|
+
printError(result.error.message);
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
963
|
+
const keys = result.value;
|
|
964
|
+
if (argv.json) {
|
|
965
|
+
output(argv, keys);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (keys.length === 0) {
|
|
969
|
+
console.log("No secrets found.");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
printTable(["Name"], keys.map((k) => [k]));
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
const setCommand = {
|
|
976
|
+
command: "set <key> [value]",
|
|
977
|
+
describe: "Set (create or overwrite) a secret. Reads value from stdin if omitted.",
|
|
978
|
+
builder: (y) => y.positional("key", {
|
|
979
|
+
type: "string",
|
|
980
|
+
demandOption: true,
|
|
981
|
+
describe: "Secret name (uppercase letters, digits, underscores)"
|
|
982
|
+
}).positional("value", {
|
|
983
|
+
type: "string",
|
|
984
|
+
describe: "Secret value. If omitted, read from stdin — keeps the value out of shell history."
|
|
985
|
+
}),
|
|
986
|
+
handler: async (argv) => {
|
|
987
|
+
let value = argv.value;
|
|
988
|
+
if (value === void 0) {
|
|
989
|
+
if (process.stdin.isTTY) {
|
|
990
|
+
printError("No value given. Pass it as an argument or pipe it on stdin, e.g. `echo -n \"$TOKEN\" | skydive secrets set MY_KEY --agent-id <id>`.");
|
|
991
|
+
process.exit(1);
|
|
992
|
+
}
|
|
993
|
+
value = await readStdin();
|
|
994
|
+
}
|
|
995
|
+
if (!value) {
|
|
996
|
+
printError("Empty secret value.");
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
const result = await requireClient(argv).setSecret(argv["agent-id"], argv.key, value);
|
|
1000
|
+
if (result.isErr()) {
|
|
1001
|
+
printError(result.error.message);
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
if (argv.json) output(argv, {
|
|
1005
|
+
key: result.value,
|
|
1006
|
+
set: true
|
|
1007
|
+
});
|
|
1008
|
+
else if (!argv.quiet) console.log(`Secret ${result.value} set.`);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
const rmCommand = {
|
|
1012
|
+
command: "rm <key>",
|
|
1013
|
+
aliases: ["delete"],
|
|
1014
|
+
describe: "Remove a secret from the agent",
|
|
1015
|
+
builder: (y) => y.positional("key", {
|
|
1016
|
+
type: "string",
|
|
1017
|
+
demandOption: true,
|
|
1018
|
+
describe: "Secret name"
|
|
1019
|
+
}),
|
|
1020
|
+
handler: async (argv) => {
|
|
1021
|
+
const result = await requireClient(argv).deleteSecret(argv["agent-id"], argv.key);
|
|
1022
|
+
if (result.isErr()) {
|
|
1023
|
+
printError(result.error.message);
|
|
1024
|
+
process.exit(1);
|
|
1025
|
+
}
|
|
1026
|
+
if (argv.json) output(argv, {
|
|
1027
|
+
key: argv.key,
|
|
1028
|
+
removed: true
|
|
1029
|
+
});
|
|
1030
|
+
else if (!argv.quiet) console.log(`Secret ${argv.key} removed.`);
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
const secretsCommand = {
|
|
1034
|
+
command: "secrets",
|
|
1035
|
+
describe: "Manage an agent’s secrets",
|
|
1036
|
+
builder: (y) => y.option("agent-id", {
|
|
1037
|
+
type: "string",
|
|
1038
|
+
demandOption: true,
|
|
1039
|
+
describe: "Agent ID"
|
|
1040
|
+
}).command(listCommand$1).command(setCommand).command(rmCommand).demandCommand(1, "Specify a subcommand: list, set, rm"),
|
|
1041
|
+
handler: () => {}
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region src/chat/tui/theme.ts
|
|
1046
|
+
const tokyonight = {
|
|
1047
|
+
id: "tokyonight",
|
|
1048
|
+
label: "Tokyo Night",
|
|
1049
|
+
mode: "dark",
|
|
1050
|
+
palette: {
|
|
1051
|
+
fg: "#c0caf5",
|
|
1052
|
+
muted: "#7a7a7a",
|
|
1053
|
+
dim: "#565f89",
|
|
1054
|
+
faint: "#3b4261",
|
|
1055
|
+
surface: "#16161e",
|
|
1056
|
+
accent: "#7aa2f7",
|
|
1057
|
+
success: "#9ece6a",
|
|
1058
|
+
warning: "#e0af68",
|
|
1059
|
+
error: "#f7768e",
|
|
1060
|
+
user: "#7aa2f7",
|
|
1061
|
+
assistant: "#c0caf5",
|
|
1062
|
+
tool: "#bb9af7",
|
|
1063
|
+
reasoning: "#565f89",
|
|
1064
|
+
syntaxBlue: "#7aa2f7",
|
|
1065
|
+
syntaxCyan: "#7dcfff",
|
|
1066
|
+
syntaxTeal: "#73daca",
|
|
1067
|
+
syntaxGreen: "#9ece6a",
|
|
1068
|
+
syntaxYellow: "#e0af68",
|
|
1069
|
+
syntaxOrange: "#ff9e64",
|
|
1070
|
+
syntaxRed: "#f7768e",
|
|
1071
|
+
syntaxMagenta: "#bb9af7"
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
const tokyonightDay = {
|
|
1075
|
+
id: "tokyonight-day",
|
|
1076
|
+
label: "Tokyo Night Day",
|
|
1077
|
+
mode: "light",
|
|
1078
|
+
palette: {
|
|
1079
|
+
fg: "#3760bf",
|
|
1080
|
+
muted: "#848cb5",
|
|
1081
|
+
dim: "#9da3c2",
|
|
1082
|
+
faint: "#c4c8da",
|
|
1083
|
+
surface: "#d0d1d8",
|
|
1084
|
+
accent: "#2e7de9",
|
|
1085
|
+
success: "#587539",
|
|
1086
|
+
warning: "#8c6c3e",
|
|
1087
|
+
error: "#f52a65",
|
|
1088
|
+
user: "#2e7de9",
|
|
1089
|
+
assistant: "#3760bf",
|
|
1090
|
+
tool: "#9854f1",
|
|
1091
|
+
reasoning: "#848cb5",
|
|
1092
|
+
syntaxBlue: "#2e7de9",
|
|
1093
|
+
syntaxCyan: "#007197",
|
|
1094
|
+
syntaxTeal: "#118c74",
|
|
1095
|
+
syntaxGreen: "#587539",
|
|
1096
|
+
syntaxYellow: "#8c6c3e",
|
|
1097
|
+
syntaxOrange: "#b15c00",
|
|
1098
|
+
syntaxRed: "#f52a65",
|
|
1099
|
+
syntaxMagenta: "#9854f1"
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
const catppuccinMocha = {
|
|
1103
|
+
id: "catppuccin-mocha",
|
|
1104
|
+
label: "Catppuccin Mocha",
|
|
1105
|
+
mode: "dark",
|
|
1106
|
+
palette: {
|
|
1107
|
+
fg: "#cdd6f4",
|
|
1108
|
+
muted: "#7f849c",
|
|
1109
|
+
dim: "#6c7086",
|
|
1110
|
+
faint: "#45475a",
|
|
1111
|
+
surface: "#181825",
|
|
1112
|
+
accent: "#89b4fa",
|
|
1113
|
+
success: "#a6e3a1",
|
|
1114
|
+
warning: "#f9e2af",
|
|
1115
|
+
error: "#f38ba8",
|
|
1116
|
+
user: "#89b4fa",
|
|
1117
|
+
assistant: "#cdd6f4",
|
|
1118
|
+
tool: "#cba6f7",
|
|
1119
|
+
reasoning: "#6c7086",
|
|
1120
|
+
syntaxBlue: "#89b4fa",
|
|
1121
|
+
syntaxCyan: "#89dceb",
|
|
1122
|
+
syntaxTeal: "#94e2d5",
|
|
1123
|
+
syntaxGreen: "#a6e3a1",
|
|
1124
|
+
syntaxYellow: "#f9e2af",
|
|
1125
|
+
syntaxOrange: "#fab387",
|
|
1126
|
+
syntaxRed: "#f38ba8",
|
|
1127
|
+
syntaxMagenta: "#cba6f7"
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
const catppuccinLatte = {
|
|
1131
|
+
id: "catppuccin-latte",
|
|
1132
|
+
label: "Catppuccin Latte",
|
|
1133
|
+
mode: "light",
|
|
1134
|
+
palette: {
|
|
1135
|
+
fg: "#4c4f69",
|
|
1136
|
+
muted: "#8c8fa1",
|
|
1137
|
+
dim: "#9ca0b0",
|
|
1138
|
+
faint: "#bcc0cc",
|
|
1139
|
+
surface: "#e6e9ef",
|
|
1140
|
+
accent: "#1e66f5",
|
|
1141
|
+
success: "#40a02b",
|
|
1142
|
+
warning: "#df8e1d",
|
|
1143
|
+
error: "#d20f39",
|
|
1144
|
+
user: "#1e66f5",
|
|
1145
|
+
assistant: "#4c4f69",
|
|
1146
|
+
tool: "#8839ef",
|
|
1147
|
+
reasoning: "#9ca0b0",
|
|
1148
|
+
syntaxBlue: "#1e66f5",
|
|
1149
|
+
syntaxCyan: "#04a5e5",
|
|
1150
|
+
syntaxTeal: "#179299",
|
|
1151
|
+
syntaxGreen: "#40a02b",
|
|
1152
|
+
syntaxYellow: "#df8e1d",
|
|
1153
|
+
syntaxOrange: "#fe640b",
|
|
1154
|
+
syntaxRed: "#d20f39",
|
|
1155
|
+
syntaxMagenta: "#8839ef"
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
const gruvboxDark = {
|
|
1159
|
+
id: "gruvbox-dark",
|
|
1160
|
+
label: "Gruvbox Dark",
|
|
1161
|
+
mode: "dark",
|
|
1162
|
+
palette: {
|
|
1163
|
+
fg: "#ebdbb2",
|
|
1164
|
+
muted: "#928374",
|
|
1165
|
+
dim: "#7c6f64",
|
|
1166
|
+
faint: "#504945",
|
|
1167
|
+
surface: "#1d2021",
|
|
1168
|
+
accent: "#83a598",
|
|
1169
|
+
success: "#b8bb26",
|
|
1170
|
+
warning: "#fabd2f",
|
|
1171
|
+
error: "#fb4934",
|
|
1172
|
+
user: "#83a598",
|
|
1173
|
+
assistant: "#ebdbb2",
|
|
1174
|
+
tool: "#d3869b",
|
|
1175
|
+
reasoning: "#7c6f64",
|
|
1176
|
+
syntaxBlue: "#83a598",
|
|
1177
|
+
syntaxCyan: "#8ec07c",
|
|
1178
|
+
syntaxTeal: "#8ec07c",
|
|
1179
|
+
syntaxGreen: "#b8bb26",
|
|
1180
|
+
syntaxYellow: "#fabd2f",
|
|
1181
|
+
syntaxOrange: "#fe8019",
|
|
1182
|
+
syntaxRed: "#fb4934",
|
|
1183
|
+
syntaxMagenta: "#d3869b"
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
const gruvboxLight = {
|
|
1187
|
+
id: "gruvbox-light",
|
|
1188
|
+
label: "Gruvbox Light",
|
|
1189
|
+
mode: "light",
|
|
1190
|
+
palette: {
|
|
1191
|
+
fg: "#3c3836",
|
|
1192
|
+
muted: "#928374",
|
|
1193
|
+
dim: "#a89984",
|
|
1194
|
+
faint: "#d5c4a1",
|
|
1195
|
+
surface: "#ebdbb2",
|
|
1196
|
+
accent: "#076678",
|
|
1197
|
+
success: "#79740e",
|
|
1198
|
+
warning: "#b57614",
|
|
1199
|
+
error: "#9d0006",
|
|
1200
|
+
user: "#076678",
|
|
1201
|
+
assistant: "#3c3836",
|
|
1202
|
+
tool: "#8f3f71",
|
|
1203
|
+
reasoning: "#a89984",
|
|
1204
|
+
syntaxBlue: "#076678",
|
|
1205
|
+
syntaxCyan: "#427b58",
|
|
1206
|
+
syntaxTeal: "#427b58",
|
|
1207
|
+
syntaxGreen: "#79740e",
|
|
1208
|
+
syntaxYellow: "#b57614",
|
|
1209
|
+
syntaxOrange: "#af3a03",
|
|
1210
|
+
syntaxRed: "#9d0006",
|
|
1211
|
+
syntaxMagenta: "#8f3f71"
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
const solarizedDark = {
|
|
1215
|
+
id: "solarized-dark",
|
|
1216
|
+
label: "Solarized Dark",
|
|
1217
|
+
mode: "dark",
|
|
1218
|
+
palette: {
|
|
1219
|
+
fg: "#93a1a1",
|
|
1220
|
+
muted: "#586e75",
|
|
1221
|
+
dim: "#586e75",
|
|
1222
|
+
faint: "#073642",
|
|
1223
|
+
surface: "#00212b",
|
|
1224
|
+
accent: "#268bd2",
|
|
1225
|
+
success: "#859900",
|
|
1226
|
+
warning: "#b58900",
|
|
1227
|
+
error: "#dc322f",
|
|
1228
|
+
user: "#268bd2",
|
|
1229
|
+
assistant: "#93a1a1",
|
|
1230
|
+
tool: "#6c71c4",
|
|
1231
|
+
reasoning: "#586e75",
|
|
1232
|
+
syntaxBlue: "#268bd2",
|
|
1233
|
+
syntaxCyan: "#2aa198",
|
|
1234
|
+
syntaxTeal: "#2aa198",
|
|
1235
|
+
syntaxGreen: "#859900",
|
|
1236
|
+
syntaxYellow: "#b58900",
|
|
1237
|
+
syntaxOrange: "#cb4b16",
|
|
1238
|
+
syntaxRed: "#dc322f",
|
|
1239
|
+
syntaxMagenta: "#6c71c4"
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
const solarizedLight = {
|
|
1243
|
+
id: "solarized-light",
|
|
1244
|
+
label: "Solarized Light",
|
|
1245
|
+
mode: "light",
|
|
1246
|
+
palette: {
|
|
1247
|
+
fg: "#657b83",
|
|
1248
|
+
muted: "#839496",
|
|
1249
|
+
dim: "#93a1a1",
|
|
1250
|
+
faint: "#eee8d5",
|
|
1251
|
+
surface: "#eee8d5",
|
|
1252
|
+
accent: "#268bd2",
|
|
1253
|
+
success: "#859900",
|
|
1254
|
+
warning: "#b58900",
|
|
1255
|
+
error: "#dc322f",
|
|
1256
|
+
user: "#268bd2",
|
|
1257
|
+
assistant: "#657b83",
|
|
1258
|
+
tool: "#6c71c4",
|
|
1259
|
+
reasoning: "#93a1a1",
|
|
1260
|
+
syntaxBlue: "#268bd2",
|
|
1261
|
+
syntaxCyan: "#2aa198",
|
|
1262
|
+
syntaxTeal: "#2aa198",
|
|
1263
|
+
syntaxGreen: "#859900",
|
|
1264
|
+
syntaxYellow: "#b58900",
|
|
1265
|
+
syntaxOrange: "#cb4b16",
|
|
1266
|
+
syntaxRed: "#dc322f",
|
|
1267
|
+
syntaxMagenta: "#6c71c4"
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
const nord = {
|
|
1271
|
+
id: "nord",
|
|
1272
|
+
label: "Nord",
|
|
1273
|
+
mode: "dark",
|
|
1274
|
+
palette: {
|
|
1275
|
+
fg: "#d8dee9",
|
|
1276
|
+
muted: "#616e88",
|
|
1277
|
+
dim: "#4c566a",
|
|
1278
|
+
faint: "#3b4252",
|
|
1279
|
+
surface: "#272c36",
|
|
1280
|
+
accent: "#88c0d0",
|
|
1281
|
+
success: "#a3be8c",
|
|
1282
|
+
warning: "#ebcb8b",
|
|
1283
|
+
error: "#bf616a",
|
|
1284
|
+
user: "#88c0d0",
|
|
1285
|
+
assistant: "#d8dee9",
|
|
1286
|
+
tool: "#b48ead",
|
|
1287
|
+
reasoning: "#4c566a",
|
|
1288
|
+
syntaxBlue: "#81a1c1",
|
|
1289
|
+
syntaxCyan: "#88c0d0",
|
|
1290
|
+
syntaxTeal: "#8fbcbb",
|
|
1291
|
+
syntaxGreen: "#a3be8c",
|
|
1292
|
+
syntaxYellow: "#ebcb8b",
|
|
1293
|
+
syntaxOrange: "#d08770",
|
|
1294
|
+
syntaxRed: "#bf616a",
|
|
1295
|
+
syntaxMagenta: "#b48ead"
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
const dracula = {
|
|
1299
|
+
id: "dracula",
|
|
1300
|
+
label: "Dracula",
|
|
1301
|
+
mode: "dark",
|
|
1302
|
+
palette: {
|
|
1303
|
+
fg: "#f8f8f2",
|
|
1304
|
+
muted: "#6272a4",
|
|
1305
|
+
dim: "#6272a4",
|
|
1306
|
+
faint: "#44475a",
|
|
1307
|
+
surface: "#21222c",
|
|
1308
|
+
accent: "#bd93f9",
|
|
1309
|
+
success: "#50fa7b",
|
|
1310
|
+
warning: "#ffb86c",
|
|
1311
|
+
error: "#ff5555",
|
|
1312
|
+
user: "#bd93f9",
|
|
1313
|
+
assistant: "#f8f8f2",
|
|
1314
|
+
tool: "#ff79c6",
|
|
1315
|
+
reasoning: "#6272a4",
|
|
1316
|
+
syntaxBlue: "#bd93f9",
|
|
1317
|
+
syntaxCyan: "#8be9fd",
|
|
1318
|
+
syntaxTeal: "#8be9fd",
|
|
1319
|
+
syntaxGreen: "#50fa7b",
|
|
1320
|
+
syntaxYellow: "#f1fa8c",
|
|
1321
|
+
syntaxOrange: "#ffb86c",
|
|
1322
|
+
syntaxRed: "#ff5555",
|
|
1323
|
+
syntaxMagenta: "#ff79c6"
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
const oneDark = {
|
|
1327
|
+
id: "one-dark",
|
|
1328
|
+
label: "One Dark",
|
|
1329
|
+
mode: "dark",
|
|
1330
|
+
palette: {
|
|
1331
|
+
fg: "#abb2bf",
|
|
1332
|
+
muted: "#5c6370",
|
|
1333
|
+
dim: "#5c6370",
|
|
1334
|
+
faint: "#3e4451",
|
|
1335
|
+
surface: "#21252b",
|
|
1336
|
+
accent: "#61afef",
|
|
1337
|
+
success: "#98c379",
|
|
1338
|
+
warning: "#e5c07b",
|
|
1339
|
+
error: "#e06c75",
|
|
1340
|
+
user: "#61afef",
|
|
1341
|
+
assistant: "#abb2bf",
|
|
1342
|
+
tool: "#c678dd",
|
|
1343
|
+
reasoning: "#5c6370",
|
|
1344
|
+
syntaxBlue: "#61afef",
|
|
1345
|
+
syntaxCyan: "#56b6c2",
|
|
1346
|
+
syntaxTeal: "#56b6c2",
|
|
1347
|
+
syntaxGreen: "#98c379",
|
|
1348
|
+
syntaxYellow: "#e5c07b",
|
|
1349
|
+
syntaxOrange: "#d19a66",
|
|
1350
|
+
syntaxRed: "#e06c75",
|
|
1351
|
+
syntaxMagenta: "#c678dd"
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
const oneLight = {
|
|
1355
|
+
id: "one-light",
|
|
1356
|
+
label: "One Light",
|
|
1357
|
+
mode: "light",
|
|
1358
|
+
palette: {
|
|
1359
|
+
fg: "#383a42",
|
|
1360
|
+
muted: "#a0a1a7",
|
|
1361
|
+
dim: "#a0a1a7",
|
|
1362
|
+
faint: "#e5e5e6",
|
|
1363
|
+
surface: "#f0f0f1",
|
|
1364
|
+
accent: "#4078f2",
|
|
1365
|
+
success: "#50a14f",
|
|
1366
|
+
warning: "#c18401",
|
|
1367
|
+
error: "#e45649",
|
|
1368
|
+
user: "#4078f2",
|
|
1369
|
+
assistant: "#383a42",
|
|
1370
|
+
tool: "#a626a4",
|
|
1371
|
+
reasoning: "#a0a1a7",
|
|
1372
|
+
syntaxBlue: "#4078f2",
|
|
1373
|
+
syntaxCyan: "#0184bc",
|
|
1374
|
+
syntaxTeal: "#0184bc",
|
|
1375
|
+
syntaxGreen: "#50a14f",
|
|
1376
|
+
syntaxYellow: "#c18401",
|
|
1377
|
+
syntaxOrange: "#986801",
|
|
1378
|
+
syntaxRed: "#e45649",
|
|
1379
|
+
syntaxMagenta: "#a626a4"
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
const rosePine = {
|
|
1383
|
+
id: "rose-pine",
|
|
1384
|
+
label: "Rosé Pine",
|
|
1385
|
+
mode: "dark",
|
|
1386
|
+
palette: {
|
|
1387
|
+
fg: "#e0def4",
|
|
1388
|
+
muted: "#908caa",
|
|
1389
|
+
dim: "#6e6a86",
|
|
1390
|
+
faint: "#403d52",
|
|
1391
|
+
surface: "#16141f",
|
|
1392
|
+
accent: "#c4a7e7",
|
|
1393
|
+
success: "#9ccfd8",
|
|
1394
|
+
warning: "#f6c177",
|
|
1395
|
+
error: "#eb6f92",
|
|
1396
|
+
user: "#c4a7e7",
|
|
1397
|
+
assistant: "#e0def4",
|
|
1398
|
+
tool: "#ebbcba",
|
|
1399
|
+
reasoning: "#908caa",
|
|
1400
|
+
syntaxBlue: "#9ccfd8",
|
|
1401
|
+
syntaxCyan: "#9ccfd8",
|
|
1402
|
+
syntaxTeal: "#31748f",
|
|
1403
|
+
syntaxGreen: "#31748f",
|
|
1404
|
+
syntaxYellow: "#f6c177",
|
|
1405
|
+
syntaxOrange: "#ebbcba",
|
|
1406
|
+
syntaxRed: "#eb6f92",
|
|
1407
|
+
syntaxMagenta: "#c4a7e7"
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
const rosePineDawn = {
|
|
1411
|
+
id: "rose-pine-dawn",
|
|
1412
|
+
label: "Rosé Pine Dawn",
|
|
1413
|
+
mode: "light",
|
|
1414
|
+
palette: {
|
|
1415
|
+
fg: "#575279",
|
|
1416
|
+
muted: "#797593",
|
|
1417
|
+
dim: "#9893a5",
|
|
1418
|
+
faint: "#cecacd",
|
|
1419
|
+
surface: "#f2e9e1",
|
|
1420
|
+
accent: "#907aa9",
|
|
1421
|
+
success: "#56949f",
|
|
1422
|
+
warning: "#ea9d34",
|
|
1423
|
+
error: "#b4637a",
|
|
1424
|
+
user: "#907aa9",
|
|
1425
|
+
assistant: "#575279",
|
|
1426
|
+
tool: "#d7827e",
|
|
1427
|
+
reasoning: "#9893a5",
|
|
1428
|
+
syntaxBlue: "#56949f",
|
|
1429
|
+
syntaxCyan: "#56949f",
|
|
1430
|
+
syntaxTeal: "#286983",
|
|
1431
|
+
syntaxGreen: "#286983",
|
|
1432
|
+
syntaxYellow: "#ea9d34",
|
|
1433
|
+
syntaxOrange: "#d7827e",
|
|
1434
|
+
syntaxRed: "#b4637a",
|
|
1435
|
+
syntaxMagenta: "#907aa9"
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
const everforestDark = {
|
|
1439
|
+
id: "everforest-dark",
|
|
1440
|
+
label: "Everforest Dark",
|
|
1441
|
+
mode: "dark",
|
|
1442
|
+
palette: {
|
|
1443
|
+
fg: "#d3c6aa",
|
|
1444
|
+
muted: "#859289",
|
|
1445
|
+
dim: "#7a8478",
|
|
1446
|
+
faint: "#414b50",
|
|
1447
|
+
surface: "#232a2e",
|
|
1448
|
+
accent: "#7fbbb3",
|
|
1449
|
+
success: "#a7c080",
|
|
1450
|
+
warning: "#dbbc7f",
|
|
1451
|
+
error: "#e67e80",
|
|
1452
|
+
user: "#7fbbb3",
|
|
1453
|
+
assistant: "#d3c6aa",
|
|
1454
|
+
tool: "#d699b6",
|
|
1455
|
+
reasoning: "#7a8478",
|
|
1456
|
+
syntaxBlue: "#7fbbb3",
|
|
1457
|
+
syntaxCyan: "#83c092",
|
|
1458
|
+
syntaxTeal: "#83c092",
|
|
1459
|
+
syntaxGreen: "#a7c080",
|
|
1460
|
+
syntaxYellow: "#dbbc7f",
|
|
1461
|
+
syntaxOrange: "#e69875",
|
|
1462
|
+
syntaxRed: "#e67e80",
|
|
1463
|
+
syntaxMagenta: "#d699b6"
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
const everforestLight = {
|
|
1467
|
+
id: "everforest-light",
|
|
1468
|
+
label: "Everforest Light",
|
|
1469
|
+
mode: "light",
|
|
1470
|
+
palette: {
|
|
1471
|
+
fg: "#5c6a72",
|
|
1472
|
+
muted: "#939f91",
|
|
1473
|
+
dim: "#a6b0a0",
|
|
1474
|
+
faint: "#e0dcc7",
|
|
1475
|
+
surface: "#f4f0d9",
|
|
1476
|
+
accent: "#3a94c5",
|
|
1477
|
+
success: "#8da101",
|
|
1478
|
+
warning: "#dfa000",
|
|
1479
|
+
error: "#f85552",
|
|
1480
|
+
user: "#3a94c5",
|
|
1481
|
+
assistant: "#5c6a72",
|
|
1482
|
+
tool: "#df69ba",
|
|
1483
|
+
reasoning: "#a6b0a0",
|
|
1484
|
+
syntaxBlue: "#3a94c5",
|
|
1485
|
+
syntaxCyan: "#35a77c",
|
|
1486
|
+
syntaxTeal: "#35a77c",
|
|
1487
|
+
syntaxGreen: "#8da101",
|
|
1488
|
+
syntaxYellow: "#dfa000",
|
|
1489
|
+
syntaxOrange: "#f57d26",
|
|
1490
|
+
syntaxRed: "#f85552",
|
|
1491
|
+
syntaxMagenta: "#df69ba"
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
const githubDark = {
|
|
1495
|
+
id: "github-dark",
|
|
1496
|
+
label: "GitHub Dark",
|
|
1497
|
+
mode: "dark",
|
|
1498
|
+
palette: {
|
|
1499
|
+
fg: "#c9d1d9",
|
|
1500
|
+
muted: "#8b949e",
|
|
1501
|
+
dim: "#6e7681",
|
|
1502
|
+
faint: "#30363d",
|
|
1503
|
+
surface: "#161b22",
|
|
1504
|
+
accent: "#58a6ff",
|
|
1505
|
+
success: "#3fb950",
|
|
1506
|
+
warning: "#d29922",
|
|
1507
|
+
error: "#f85149",
|
|
1508
|
+
user: "#58a6ff",
|
|
1509
|
+
assistant: "#c9d1d9",
|
|
1510
|
+
tool: "#bc8cff",
|
|
1511
|
+
reasoning: "#6e7681",
|
|
1512
|
+
syntaxBlue: "#58a6ff",
|
|
1513
|
+
syntaxCyan: "#39c5cf",
|
|
1514
|
+
syntaxTeal: "#39c5cf",
|
|
1515
|
+
syntaxGreen: "#3fb950",
|
|
1516
|
+
syntaxYellow: "#d29922",
|
|
1517
|
+
syntaxOrange: "#db6d28",
|
|
1518
|
+
syntaxRed: "#f85149",
|
|
1519
|
+
syntaxMagenta: "#bc8cff"
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
const githubLight = {
|
|
1523
|
+
id: "github-light",
|
|
1524
|
+
label: "GitHub Light",
|
|
1525
|
+
mode: "light",
|
|
1526
|
+
palette: {
|
|
1527
|
+
fg: "#24292f",
|
|
1528
|
+
muted: "#57606a",
|
|
1529
|
+
dim: "#8c959f",
|
|
1530
|
+
faint: "#d0d7de",
|
|
1531
|
+
surface: "#f6f8fa",
|
|
1532
|
+
accent: "#0969da",
|
|
1533
|
+
success: "#1a7f37",
|
|
1534
|
+
warning: "#9a6700",
|
|
1535
|
+
error: "#cf222e",
|
|
1536
|
+
user: "#0969da",
|
|
1537
|
+
assistant: "#24292f",
|
|
1538
|
+
tool: "#8250df",
|
|
1539
|
+
reasoning: "#8c959f",
|
|
1540
|
+
syntaxBlue: "#0969da",
|
|
1541
|
+
syntaxCyan: "#1b7c83",
|
|
1542
|
+
syntaxTeal: "#1b7c83",
|
|
1543
|
+
syntaxGreen: "#1a7f37",
|
|
1544
|
+
syntaxYellow: "#9a6700",
|
|
1545
|
+
syntaxOrange: "#bc4c00",
|
|
1546
|
+
syntaxRed: "#cf222e",
|
|
1547
|
+
syntaxMagenta: "#8250df"
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
const kanagawa = {
|
|
1551
|
+
id: "kanagawa",
|
|
1552
|
+
label: "Kanagawa",
|
|
1553
|
+
mode: "dark",
|
|
1554
|
+
palette: {
|
|
1555
|
+
fg: "#dcd7ba",
|
|
1556
|
+
muted: "#727169",
|
|
1557
|
+
dim: "#54546d",
|
|
1558
|
+
faint: "#363646",
|
|
1559
|
+
surface: "#16161d",
|
|
1560
|
+
accent: "#7e9cd8",
|
|
1561
|
+
success: "#98bb6c",
|
|
1562
|
+
warning: "#e6c384",
|
|
1563
|
+
error: "#e46876",
|
|
1564
|
+
user: "#7e9cd8",
|
|
1565
|
+
assistant: "#dcd7ba",
|
|
1566
|
+
tool: "#957fb8",
|
|
1567
|
+
reasoning: "#727169",
|
|
1568
|
+
syntaxBlue: "#7e9cd8",
|
|
1569
|
+
syntaxCyan: "#7aa89f",
|
|
1570
|
+
syntaxTeal: "#7aa89f",
|
|
1571
|
+
syntaxGreen: "#98bb6c",
|
|
1572
|
+
syntaxYellow: "#e6c384",
|
|
1573
|
+
syntaxOrange: "#ffa066",
|
|
1574
|
+
syntaxRed: "#e46876",
|
|
1575
|
+
syntaxMagenta: "#957fb8"
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
const themes = [
|
|
1579
|
+
tokyonight,
|
|
1580
|
+
tokyonightDay,
|
|
1581
|
+
catppuccinMocha,
|
|
1582
|
+
catppuccinLatte,
|
|
1583
|
+
gruvboxDark,
|
|
1584
|
+
gruvboxLight,
|
|
1585
|
+
solarizedDark,
|
|
1586
|
+
solarizedLight,
|
|
1587
|
+
nord,
|
|
1588
|
+
dracula,
|
|
1589
|
+
oneDark,
|
|
1590
|
+
oneLight,
|
|
1591
|
+
rosePine,
|
|
1592
|
+
rosePineDawn,
|
|
1593
|
+
everforestDark,
|
|
1594
|
+
everforestLight,
|
|
1595
|
+
githubDark,
|
|
1596
|
+
githubLight,
|
|
1597
|
+
kanagawa
|
|
1598
|
+
];
|
|
1599
|
+
const DEFAULT_THEME_ID = {
|
|
1600
|
+
dark: tokyonight.id,
|
|
1601
|
+
light: tokyonightDay.id
|
|
1602
|
+
};
|
|
1603
|
+
/** All-undefined palette for NO_COLOR: every fg/bg falls back to the
|
|
1604
|
+
* terminal's own defaults, so nothing emits color. Not listed in `themes` —
|
|
1605
|
+
* it's forced, never picked. */
|
|
1606
|
+
const monoTheme = {
|
|
1607
|
+
id: "mono",
|
|
1608
|
+
label: "No color",
|
|
1609
|
+
mode: "dark",
|
|
1610
|
+
palette: {
|
|
1611
|
+
fg: void 0,
|
|
1612
|
+
muted: void 0,
|
|
1613
|
+
dim: void 0,
|
|
1614
|
+
faint: void 0,
|
|
1615
|
+
surface: void 0,
|
|
1616
|
+
accent: void 0,
|
|
1617
|
+
success: void 0,
|
|
1618
|
+
warning: void 0,
|
|
1619
|
+
error: void 0,
|
|
1620
|
+
user: void 0,
|
|
1621
|
+
assistant: void 0,
|
|
1622
|
+
tool: void 0,
|
|
1623
|
+
reasoning: void 0,
|
|
1624
|
+
syntaxBlue: void 0,
|
|
1625
|
+
syntaxCyan: void 0,
|
|
1626
|
+
syntaxTeal: void 0,
|
|
1627
|
+
syntaxGreen: void 0,
|
|
1628
|
+
syntaxYellow: void 0,
|
|
1629
|
+
syntaxOrange: void 0,
|
|
1630
|
+
syntaxRed: void 0,
|
|
1631
|
+
syntaxMagenta: void 0
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
function themesForMode(mode) {
|
|
1635
|
+
return themes.filter((t) => t.mode === mode);
|
|
1636
|
+
}
|
|
1637
|
+
function findTheme(id) {
|
|
1638
|
+
if (id === monoTheme.id) return monoTheme;
|
|
1639
|
+
return themes.find((t) => t.id === id);
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* The saved theme to use for a mode: the persisted pick if it exists *and*
|
|
1643
|
+
* still matches the mode (a stale/renamed id falls back), else the default.
|
|
1644
|
+
*/
|
|
1645
|
+
function themeForMode(mode, savedId) {
|
|
1646
|
+
if (savedId) {
|
|
1647
|
+
const saved = findTheme(savedId);
|
|
1648
|
+
if (saved && saved.mode === mode) return saved;
|
|
1649
|
+
}
|
|
1650
|
+
return findTheme(DEFAULT_THEME_ID[mode]) ?? tokyonight;
|
|
1651
|
+
}
|
|
1652
|
+
/** https://no-color.org — any non-empty value disables color output. */
|
|
1653
|
+
function noColorRequested(env = process.env) {
|
|
1654
|
+
const v = env["NO_COLOR"];
|
|
1655
|
+
return v !== void 0 && v !== "";
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Fallback light/dark sniff for terminals that never answer the OSC 10/11
|
|
1659
|
+
* query: `COLORFGBG` is "<fg>;<bg>" (sometimes "<fg>;default;<bg>") with
|
|
1660
|
+
* ANSI palette indexes. Background 7/15 (white/bright white) means a light
|
|
1661
|
+
* terminal; anything else we call dark. Returns null when unset/unparsable.
|
|
1662
|
+
*/
|
|
1663
|
+
function themeModeFromColorFgBg(env = process.env) {
|
|
1664
|
+
const raw = env["COLORFGBG"];
|
|
1665
|
+
if (!raw) return null;
|
|
1666
|
+
const parts = raw.split(";");
|
|
1667
|
+
const bg = parts[parts.length - 1];
|
|
1668
|
+
if (bg === void 0 || !/^\d+$/.test(bg)) return null;
|
|
1669
|
+
const idx = Number(bg);
|
|
1670
|
+
return idx === 7 || idx === 15 ? "light" : "dark";
|
|
1671
|
+
}
|
|
1672
|
+
/** Single source of truth for colors. Mutated in place by `applyTheme` so
|
|
1673
|
+
* existing `theme.fg`-style reads across the TUI stay valid. */
|
|
1674
|
+
const theme = { ...tokyonight.palette };
|
|
1675
|
+
let version = 0;
|
|
1676
|
+
function themeVersion() {
|
|
1677
|
+
return version;
|
|
1678
|
+
}
|
|
1679
|
+
let mode = tokyonight.mode;
|
|
1680
|
+
function themeMode() {
|
|
1681
|
+
return mode;
|
|
1682
|
+
}
|
|
1683
|
+
function applyTheme(def) {
|
|
1684
|
+
Object.assign(theme, def.palette);
|
|
1685
|
+
mode = def.mode;
|
|
1686
|
+
version++;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
//#endregion
|
|
1690
|
+
//#region src/chat/bun-runtime.ts
|
|
1691
|
+
/**
|
|
1692
|
+
* The chat TUI renders through OpenTUI, whose native core is Bun-first (its
|
|
1693
|
+
* FFI/native-module loading needs the Bun runtime; the Node path requires an
|
|
1694
|
+
* experimental `node:ffi` only present in very new Node). Rather than make the
|
|
1695
|
+
* user install Bun by hand, the CLI provisions a private, pinned Bun the first
|
|
1696
|
+
* time `chat` runs and transparently re-execs itself under it. Every other
|
|
1697
|
+
* command still runs under whatever Node the CLI was launched with — Bun is
|
|
1698
|
+
* only ever fetched for `chat`.
|
|
1699
|
+
*/
|
|
1700
|
+
/**
|
|
1701
|
+
* Bun release the CLI pins. Bump deliberately: the download is integrity-checked
|
|
1702
|
+
* against this exact release's published `SHASUMS256.txt`, and the cached binary
|
|
1703
|
+
* is keyed by version so bumping here transparently re-provisions on next `chat`.
|
|
1704
|
+
*/
|
|
1705
|
+
const PINNED_BUN_VERSION = "1.3.14";
|
|
1706
|
+
const BUN_RELEASE_BASE = process.env["SKYDIVE_BUN_RELEASE_BASE"] ?? "https://github.com/oven-sh/bun/releases/download";
|
|
1707
|
+
/** Guard env var: set on the child so a re-exec can never recurse. */
|
|
1708
|
+
const REEXEC_GUARD = "SKYDIVE_BUN_REEXEC";
|
|
1709
|
+
function isBun() {
|
|
1710
|
+
return typeof process !== "undefined" && "bun" in process.versions;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Map the current platform/arch to Bun's release asset basename (without the
|
|
1714
|
+
* `.zip`). Returns null on platforms Bun doesn't publish a build for, so the
|
|
1715
|
+
* caller can fall back to the manual-install message instead of a 404.
|
|
1716
|
+
*
|
|
1717
|
+
* We deliberately pick the portable (non-`baseline`, non-`musl`) x64 builds;
|
|
1718
|
+
* the baseline variant only matters for pre-2013 CPUs and isn't worth
|
|
1719
|
+
* auto-detecting here. If that ever bites someone, `SKYDIVE_BUN_PATH` lets them
|
|
1720
|
+
* point at their own Bun.
|
|
1721
|
+
*/
|
|
1722
|
+
function bunAssetTarget(platform = process.platform, arch = process.arch) {
|
|
1723
|
+
const a = arch === "arm64" ? "aarch64" : arch === "x64" ? "x64" : null;
|
|
1724
|
+
if (!a) return null;
|
|
1725
|
+
switch (platform) {
|
|
1726
|
+
case "linux": return `bun-linux-${a}`;
|
|
1727
|
+
case "darwin": return `bun-darwin-${a}`;
|
|
1728
|
+
case "win32": return a === "x64" ? "bun-windows-x64" : null;
|
|
1729
|
+
default: return null;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
/** Directory holding CLI-managed binaries, beside the config file. */
|
|
1733
|
+
function binDir() {
|
|
1734
|
+
return path.join(path.dirname(getConfigPath()), "bin");
|
|
1735
|
+
}
|
|
1736
|
+
/** Absolute path to the version-pinned cached Bun (may not exist yet). */
|
|
1737
|
+
function cachedBunPath(version = PINNED_BUN_VERSION) {
|
|
1738
|
+
const exe = process.platform === "win32" ? "bun.exe" : "bun";
|
|
1739
|
+
return path.join(binDir(), `bun-${version}`, exe);
|
|
1740
|
+
}
|
|
1741
|
+
function isExecutableFile(p) {
|
|
1742
|
+
try {
|
|
1743
|
+
if (!fs.statSync(p).isFile()) return false;
|
|
1744
|
+
if (process.platform !== "win32") fs.accessSync(p, fs.constants.X_OK);
|
|
1745
|
+
return true;
|
|
1746
|
+
} catch {
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Locate a usable Bun without downloading. Order:
|
|
1752
|
+
* 1. `SKYDIVE_BUN_PATH` (explicit override / air-gapped installs)
|
|
1753
|
+
* 2. the version-pinned cache this CLI manages
|
|
1754
|
+
* 3. `bun` already on PATH
|
|
1755
|
+
* Returns null if none is usable.
|
|
1756
|
+
*/
|
|
1757
|
+
function findExistingBun() {
|
|
1758
|
+
const override = process.env["SKYDIVE_BUN_PATH"];
|
|
1759
|
+
if (override && isExecutableFile(override)) return override;
|
|
1760
|
+
const cached = cachedBunPath();
|
|
1761
|
+
if (isExecutableFile(cached)) return cached;
|
|
1762
|
+
const onPath = resolveBunOnPath();
|
|
1763
|
+
if (onPath) return onPath;
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
/** `which`/`where` for bun, without spawning a shell. */
|
|
1767
|
+
function resolveBunOnPath() {
|
|
1768
|
+
const pathVar = process.env["PATH"] ?? "";
|
|
1769
|
+
if (!pathVar) return null;
|
|
1770
|
+
const exts = process.platform === "win32" ? (process.env["PATHEXT"] ?? ".EXE").split(";") : [""];
|
|
1771
|
+
for (const dir of pathVar.split(path.delimiter)) {
|
|
1772
|
+
if (!dir) continue;
|
|
1773
|
+
for (const ext of exts) {
|
|
1774
|
+
const candidate = path.join(dir, `bun${ext}`);
|
|
1775
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
return null;
|
|
1779
|
+
}
|
|
1780
|
+
async function fetchOk(url) {
|
|
1781
|
+
const res = await fetch(url, {
|
|
1782
|
+
headers: { "user-agent": "skydive-cli" },
|
|
1783
|
+
redirect: "follow"
|
|
1784
|
+
});
|
|
1785
|
+
if (!res.ok) throw new Error(`GET ${url} -> ${res.status} ${res.statusText}`);
|
|
1786
|
+
return res;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Look up the published SHA-256 for `assetName` in the pinned release's
|
|
1790
|
+
* `SHASUMS256.txt`. Verifying against the release's own manifest keeps
|
|
1791
|
+
* integrity checking correct across version bumps without hardcoding a hash
|
|
1792
|
+
* per platform.
|
|
1793
|
+
*/
|
|
1794
|
+
async function expectedSha256(version, assetName) {
|
|
1795
|
+
const url = `${BUN_RELEASE_BASE}/bun-v${version}/SHASUMS256.txt`;
|
|
1796
|
+
const text = await (await fetchOk(url)).text();
|
|
1797
|
+
for (const line of text.split("\n")) {
|
|
1798
|
+
const [sum, name] = line.trim().split(/\s+/);
|
|
1799
|
+
if (name === assetName && sum) return sum.toLowerCase();
|
|
1800
|
+
}
|
|
1801
|
+
throw new Error(`${assetName} not found in ${url}`);
|
|
1802
|
+
}
|
|
1803
|
+
function sha256(buf) {
|
|
1804
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Extract the single `bun` executable from a Bun release zip buffer. The zip
|
|
1808
|
+
* lays the binary out as `<target>/bun` (or `bun.exe` on Windows). Implemented
|
|
1809
|
+
* without a zip dependency by parsing the central directory — Bun's release zips
|
|
1810
|
+
* use a streaming data descriptor, so the *local* file header carries zeroed
|
|
1811
|
+
* sizes; the central directory is the only place with correct sizes/offsets.
|
|
1812
|
+
*/
|
|
1813
|
+
function extractBunFromZip(zip, target) {
|
|
1814
|
+
const exeName = process.platform === "win32" ? "bun.exe" : "bun";
|
|
1815
|
+
let eocd = -1;
|
|
1816
|
+
for (let i = zip.length - 22; i >= 0; i--) if (zip.readUInt32LE(i) === 101010256) {
|
|
1817
|
+
eocd = i;
|
|
1818
|
+
break;
|
|
1819
|
+
}
|
|
1820
|
+
if (eocd < 0) throw new Error("release zip has no end-of-central-directory");
|
|
1821
|
+
const entryCount = zip.readUInt16LE(eocd + 10);
|
|
1822
|
+
let off = zip.readUInt32LE(eocd + 16);
|
|
1823
|
+
for (let n = 0; n < entryCount; n++) {
|
|
1824
|
+
if (zip.readUInt32LE(off) !== 33639248) throw new Error("malformed central directory in release zip");
|
|
1825
|
+
const method = zip.readUInt16LE(off + 10);
|
|
1826
|
+
const compSize = zip.readUInt32LE(off + 20);
|
|
1827
|
+
const nameLen = zip.readUInt16LE(off + 28);
|
|
1828
|
+
const extraLen = zip.readUInt16LE(off + 30);
|
|
1829
|
+
const commentLen = zip.readUInt16LE(off + 32);
|
|
1830
|
+
const localHeaderOffset = zip.readUInt32LE(off + 42);
|
|
1831
|
+
const name = zip.toString("utf8", off + 46, off + 46 + nameLen);
|
|
1832
|
+
if (name === `${target}/${exeName}` || name === exeName) {
|
|
1833
|
+
const lNameLen = zip.readUInt16LE(localHeaderOffset + 26);
|
|
1834
|
+
const lExtraLen = zip.readUInt16LE(localHeaderOffset + 28);
|
|
1835
|
+
const dataStart = localHeaderOffset + 30 + lNameLen + lExtraLen;
|
|
1836
|
+
const data = zip.subarray(dataStart, dataStart + compSize);
|
|
1837
|
+
if (method === 0) return Buffer.from(data);
|
|
1838
|
+
if (method === 8) return zlib.inflateRawSync(data);
|
|
1839
|
+
throw new Error(`unsupported zip compression method ${method}`);
|
|
1840
|
+
}
|
|
1841
|
+
off += 46 + nameLen + extraLen + commentLen;
|
|
1842
|
+
}
|
|
1843
|
+
throw new Error(`bun binary not found in release zip for ${target}`);
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Download, verify, and cache the pinned Bun for this platform. Returns the
|
|
1847
|
+
* path to the cached executable, or null if Bun can't be provisioned (no
|
|
1848
|
+
* network, unsupported platform, checksum mismatch) — the caller then prints
|
|
1849
|
+
* the manual-install guidance. Never throws for the expected failure modes.
|
|
1850
|
+
*/
|
|
1851
|
+
async function downloadBun(onProgress) {
|
|
1852
|
+
const target = bunAssetTarget();
|
|
1853
|
+
if (!target) return null;
|
|
1854
|
+
const assetName = `${target}.zip`;
|
|
1855
|
+
const dest = cachedBunPath();
|
|
1856
|
+
try {
|
|
1857
|
+
onProgress?.(`Fetching Bun v${PINNED_BUN_VERSION} (one-time setup)…`);
|
|
1858
|
+
const url = `${BUN_RELEASE_BASE}/bun-v${PINNED_BUN_VERSION}/${assetName}`;
|
|
1859
|
+
const [zipRes, want] = await Promise.all([fetchOk(url), expectedSha256(PINNED_BUN_VERSION, assetName)]);
|
|
1860
|
+
const zip = Buffer.from(await zipRes.arrayBuffer());
|
|
1861
|
+
const got = sha256(zip);
|
|
1862
|
+
if (got !== want) {
|
|
1863
|
+
onProgress?.(`Bun download failed integrity check (expected ${want}, got ${got}).`);
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
const bin = extractBunFromZip(zip, target);
|
|
1867
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
1868
|
+
const tmp = path.join(path.dirname(dest), `.bun.tmp-${process.pid}-${Date.now()}`);
|
|
1869
|
+
fs.writeFileSync(tmp, bin, { mode: 493 });
|
|
1870
|
+
if (process.platform !== "win32") fs.chmodSync(tmp, 493);
|
|
1871
|
+
fs.renameSync(tmp, dest);
|
|
1872
|
+
return dest;
|
|
1873
|
+
} catch (e) {
|
|
1874
|
+
onProgress?.(`Could not download Bun automatically: ${e instanceof Error ? e.message : String(e)}`);
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Ensure a usable Bun exists: already running under Bun, found on disk/PATH, or
|
|
1880
|
+
* freshly downloaded and cached. Pure resolution — does not re-exec.
|
|
1881
|
+
*/
|
|
1882
|
+
async function resolveBun(onProgress) {
|
|
1883
|
+
if (isBun()) return { kind: "already-bun" };
|
|
1884
|
+
const existing = findExistingBun();
|
|
1885
|
+
if (existing) return {
|
|
1886
|
+
kind: "found",
|
|
1887
|
+
bunPath: existing
|
|
1888
|
+
};
|
|
1889
|
+
const downloaded = await downloadBun(onProgress);
|
|
1890
|
+
if (downloaded) return {
|
|
1891
|
+
kind: "found",
|
|
1892
|
+
bunPath: downloaded
|
|
1893
|
+
};
|
|
1894
|
+
return { kind: "unavailable" };
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* The heart of the transparent-Bun story. Called by `chat` before it touches
|
|
1898
|
+
* OpenTUI:
|
|
1899
|
+
* - Under Bun already, or if a re-exec guard is set: return 'proceed'.
|
|
1900
|
+
* - Otherwise resolve/provision Bun and re-exec this exact CLI invocation
|
|
1901
|
+
* under it (inheriting stdio + argv), then exit with the child's code.
|
|
1902
|
+
* - If Bun can't be provisioned: return 'unavailable' so the caller prints
|
|
1903
|
+
* the existing manual-install message.
|
|
1904
|
+
*
|
|
1905
|
+
* Returns 'proceed' only when it's safe to load OpenTUI in this process.
|
|
1906
|
+
*/
|
|
1907
|
+
function ensureBunAndReexec(onProgress) {
|
|
1908
|
+
if (isBun() || process.env[REEXEC_GUARD] === "1") return Promise.resolve("proceed");
|
|
1909
|
+
return resolveBun(onProgress).then((resolution) => {
|
|
1910
|
+
if (resolution.kind === "already-bun") return "proceed";
|
|
1911
|
+
if (resolution.kind === "unavailable") return "unavailable";
|
|
1912
|
+
const argv = process.argv.slice(1);
|
|
1913
|
+
const result = spawnSync(resolution.bunPath, argv, {
|
|
1914
|
+
stdio: "inherit",
|
|
1915
|
+
env: {
|
|
1916
|
+
...process.env,
|
|
1917
|
+
[REEXEC_GUARD]: "1"
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
if (result.error) {
|
|
1921
|
+
onProgress?.(`Failed to launch chat under Bun (${resolution.bunPath}): ${result.error.message}`);
|
|
1922
|
+
return "unavailable";
|
|
1923
|
+
}
|
|
1924
|
+
process.exit(result.status ?? 0);
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
//#endregion
|
|
1929
|
+
//#region src/commands/chat.ts
|
|
1930
|
+
const chatCommand = {
|
|
1931
|
+
command: "chat",
|
|
1932
|
+
describe: "Open the interactive chat TUI, or -p for a one-shot",
|
|
1933
|
+
builder: (y) => y.option("print", {
|
|
1934
|
+
alias: "p",
|
|
1935
|
+
type: "string",
|
|
1936
|
+
describe: "Non-interactive: send one prompt, print the reply, and exit (like `claude -p`). Reads the prompt from stdin if given no value. Runs under Node — no Bun required."
|
|
1937
|
+
}).option("agent", {
|
|
1938
|
+
type: "string",
|
|
1939
|
+
describe: "Target agent, by id, slug, or name. With -p, the agent to send the one-shot prompt to. Without -p, pre-selects the agent and opens its conversation list, skipping the agent picker. Optional when the account has exactly one agent."
|
|
1940
|
+
}).option("conversation", {
|
|
1941
|
+
type: "string",
|
|
1942
|
+
describe: "For -p: continue an existing conversation by id instead of starting a new one."
|
|
1943
|
+
}).option("share-machine", {
|
|
1944
|
+
type: "boolean",
|
|
1945
|
+
default: false,
|
|
1946
|
+
describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; you approve per agent)"
|
|
1947
|
+
}).option("theme", {
|
|
1948
|
+
type: "string",
|
|
1949
|
+
describe: `Pin a colorscheme (disables automatic light/dark switching). One of: ${themes.map((t) => t.id).join(", ")}. Defaults to following the terminal background; also settable via SKYDIVE_THEME.`
|
|
1950
|
+
}).option("notify", {
|
|
1951
|
+
type: "boolean",
|
|
1952
|
+
default: true,
|
|
1953
|
+
describe: "Show a desktop notification when a run finishes or needs your input while this terminal is unfocused (use --no-notify to disable)"
|
|
1954
|
+
}).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)"),
|
|
1955
|
+
handler: async (argv) => {
|
|
1956
|
+
const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
|
|
1957
|
+
if (argv.print !== void 0) {
|
|
1958
|
+
await runPrintMode({
|
|
1959
|
+
argv,
|
|
1960
|
+
appUrl
|
|
1961
|
+
});
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (await ensureBunAndReexec((msg) => process.stderr.write(`${msg}\n`)) === "unavailable") {
|
|
1965
|
+
printError(`chat needs the Bun runtime and it couldn't be set up automatically (no network, an unsupported platform, or a failed download). Install Bun (https://bun.sh) and run chat under it, e.g. \`bun ${process.argv[1] ?? "skydive"} chat\`, or point SKYDIVE_BUN_PATH at an existing bun binary. For a non-interactive one-shot that runs under Node, use \`chat -p "<prompt>"\`.`);
|
|
1966
|
+
process.exit(1);
|
|
1967
|
+
}
|
|
1968
|
+
let session = resolveSession({ appUrl });
|
|
1969
|
+
if (session.isErr()) {
|
|
1970
|
+
if (isNonInteractive()) {
|
|
1971
|
+
printError("Not signed in for chat and no interactive terminal. Run `skydive auth login --web`, or set SKYDIVE_SESSION_TOKEN.");
|
|
1972
|
+
process.exit(1);
|
|
1973
|
+
}
|
|
1974
|
+
const login = await loginWithDevice({ appUrl });
|
|
1975
|
+
if (login.isErr()) {
|
|
1976
|
+
printError(login.error.message);
|
|
1977
|
+
process.exit(1);
|
|
1978
|
+
}
|
|
1979
|
+
session = resolveSession({ appUrl });
|
|
1980
|
+
if (session.isErr()) {
|
|
1981
|
+
printError("Signed in, but no session was stored.");
|
|
1982
|
+
process.exit(1);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const themeId = argv.theme ?? process.env["SKYDIVE_THEME"] ?? void 0;
|
|
1986
|
+
if (themeId !== void 0 && !themes.some((t) => t.id === themeId)) {
|
|
1987
|
+
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
1988
|
+
process.exit(1);
|
|
1989
|
+
}
|
|
1990
|
+
const { runChat } = await import("./boot-ChlVx-ts.mjs");
|
|
1991
|
+
await runChat({
|
|
1992
|
+
appUrl,
|
|
1993
|
+
sessionToken: session.value.sessionToken,
|
|
1994
|
+
shareMachine: argv["share-machine"],
|
|
1995
|
+
promptHistoryPath: getPromptHistoryPath(),
|
|
1996
|
+
theme: themeId,
|
|
1997
|
+
notifications: argv.notify,
|
|
1998
|
+
agentSelector: argv.agent ?? null
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
async function runPrintMode({ argv, appUrl }) {
|
|
2003
|
+
const session = resolveSession({ appUrl });
|
|
2004
|
+
if (session.isErr()) {
|
|
2005
|
+
printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2006
|
+
process.exit(1);
|
|
2007
|
+
}
|
|
2008
|
+
const { runPrint, readStdin } = await import("./print-BHbFMxQv.mjs").then((n) => n.t);
|
|
2009
|
+
let prompt = (argv.print ?? "").trim();
|
|
2010
|
+
if (!prompt) {
|
|
2011
|
+
if (process.stdin.isTTY) {
|
|
2012
|
+
printError("No prompt given. Pass it inline (`-p \"your prompt\"`) or pipe it on stdin.");
|
|
2013
|
+
process.exit(1);
|
|
2014
|
+
}
|
|
2015
|
+
prompt = (await readStdin()).trim();
|
|
2016
|
+
if (!prompt) {
|
|
2017
|
+
printError("Empty prompt on stdin.");
|
|
2018
|
+
process.exit(1);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
try {
|
|
2022
|
+
const result = await runPrint({
|
|
2023
|
+
appUrl,
|
|
2024
|
+
sessionToken: session.value.sessionToken,
|
|
2025
|
+
prompt,
|
|
2026
|
+
agentSelector: argv.agent ?? null,
|
|
2027
|
+
conversationId: argv.conversation ?? null,
|
|
2028
|
+
json: argv.json
|
|
2029
|
+
});
|
|
2030
|
+
if (argv.json) output(argv, result);
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
2033
|
+
process.exit(1);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
//#endregion
|
|
2038
|
+
//#region src/commands/workspace.ts
|
|
2039
|
+
function requireSession(argv) {
|
|
2040
|
+
const session = resolveSession({ appUrl: argv["api-url"] });
|
|
2041
|
+
if (session.isErr()) {
|
|
2042
|
+
printError("Not signed in for chat. Run `skydive auth login --web` first.");
|
|
2043
|
+
process.exit(1);
|
|
2044
|
+
}
|
|
2045
|
+
return session.value;
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* List the account's workspaces, marking the active one. Shared by the `list`
|
|
2049
|
+
* subcommand and the bare `workspace` invocation. When `hint` is true (the bare
|
|
2050
|
+
* invocation), a follow-up line points at `workspace switch` so a user who ran
|
|
2051
|
+
* `workspace` on its own learns how to change context. The hint is suppressed
|
|
2052
|
+
* under --json and --quiet so machine/scripted output stays clean.
|
|
2053
|
+
*/
|
|
2054
|
+
async function runList(argv, { hint }) {
|
|
2055
|
+
const session = requireSession(argv);
|
|
2056
|
+
const [workspaces, activeId] = await Promise.all([listWorkspaces(session), getActiveWorkspaceId(session)]);
|
|
2057
|
+
if (workspaces.isErr()) {
|
|
2058
|
+
printError(workspaces.error.message);
|
|
2059
|
+
process.exit(1);
|
|
2060
|
+
}
|
|
2061
|
+
if (activeId.isErr()) {
|
|
2062
|
+
printError(activeId.error.message);
|
|
2063
|
+
process.exit(1);
|
|
2064
|
+
}
|
|
2065
|
+
if (argv.json) {
|
|
2066
|
+
output(argv, workspaces.value.map((w) => ({
|
|
2067
|
+
...w,
|
|
2068
|
+
active: w.id === activeId.value
|
|
2069
|
+
})));
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
printTable([
|
|
2073
|
+
"",
|
|
2074
|
+
"Name",
|
|
2075
|
+
"Slug"
|
|
2076
|
+
], workspaces.value.map((w) => [
|
|
2077
|
+
w.id === activeId.value ? "*" : "",
|
|
2078
|
+
w.name,
|
|
2079
|
+
w.slug
|
|
2080
|
+
]));
|
|
2081
|
+
if (hint && !argv.quiet) console.log("\nRun `skydive workspace switch <slug>` to switch which workspace `skydive chat` uses.");
|
|
2082
|
+
}
|
|
2083
|
+
const listCommand = {
|
|
2084
|
+
command: "list",
|
|
2085
|
+
describe: "List workspaces on your account",
|
|
2086
|
+
handler: (argv) => runList(argv, { hint: false })
|
|
2087
|
+
};
|
|
2088
|
+
const switchCommand = {
|
|
2089
|
+
command: "switch <workspace>",
|
|
2090
|
+
describe: "Switch which workspace `skydive chat` uses",
|
|
2091
|
+
builder: (y) => y.positional("workspace", {
|
|
2092
|
+
type: "string",
|
|
2093
|
+
demandOption: true,
|
|
2094
|
+
describe: "Workspace slug, name, or ID"
|
|
2095
|
+
}),
|
|
2096
|
+
handler: async (argv) => {
|
|
2097
|
+
const session = requireSession(argv);
|
|
2098
|
+
const workspaces = await listWorkspaces(session);
|
|
2099
|
+
if (workspaces.isErr()) {
|
|
2100
|
+
printError(workspaces.error.message);
|
|
2101
|
+
process.exit(1);
|
|
2102
|
+
}
|
|
2103
|
+
const needle = argv.workspace.toLowerCase();
|
|
2104
|
+
const match = workspaces.value.find((w) => w.id === argv.workspace) ?? workspaces.value.find((w) => w.slug.toLowerCase() === needle) ?? workspaces.value.find((w) => w.name.toLowerCase() === needle);
|
|
2105
|
+
if (!match) {
|
|
2106
|
+
printError(`No workspace matches "${argv.workspace}". Run \`skydive workspace list\` to see available workspaces.`);
|
|
2107
|
+
process.exit(1);
|
|
2108
|
+
}
|
|
2109
|
+
const result = await setActiveWorkspace({
|
|
2110
|
+
...session,
|
|
2111
|
+
organizationId: match.id
|
|
2112
|
+
});
|
|
2113
|
+
if (result.isErr()) {
|
|
2114
|
+
printError(result.error.message);
|
|
2115
|
+
process.exit(1);
|
|
2116
|
+
}
|
|
2117
|
+
if (argv.json) output(argv, {
|
|
2118
|
+
switched: true,
|
|
2119
|
+
workspace: match
|
|
2120
|
+
});
|
|
2121
|
+
else if (!argv.quiet) console.log(`Switched to workspace: ${match.name} (${match.slug})`);
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
const workspaceCommand = {
|
|
2125
|
+
command: "workspace",
|
|
2126
|
+
describe: "List or switch which workspace `skydive chat` uses",
|
|
2127
|
+
builder: (y) => y.command(listCommand).command(switchCommand),
|
|
2128
|
+
handler: (argv) => runList(argv, { hint: true })
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
//#endregion
|
|
2132
|
+
//#region src/cli.ts
|
|
2133
|
+
function createCli(argv) {
|
|
2134
|
+
return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch which workspace `skydive chat` uses").option("json", {
|
|
2135
|
+
type: "boolean",
|
|
2136
|
+
default: false,
|
|
2137
|
+
global: true,
|
|
2138
|
+
describe: "Output as JSON"
|
|
2139
|
+
}).option("quiet", {
|
|
2140
|
+
type: "boolean",
|
|
2141
|
+
default: false,
|
|
2142
|
+
global: true,
|
|
2143
|
+
describe: "Print only primary value"
|
|
2144
|
+
}).option("api-url", {
|
|
2145
|
+
type: "string",
|
|
2146
|
+
global: true,
|
|
2147
|
+
describe: "Override API base URL"
|
|
2148
|
+
}).command(authCommand).command(chatCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
2149
|
+
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
2150
|
+
process.exit(1);
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
//#endregion
|
|
2155
|
+
//#region src/default-command.ts
|
|
2156
|
+
const TOP_LEVEL_FLAGS = new Set([
|
|
2157
|
+
"-h",
|
|
2158
|
+
"--help",
|
|
2159
|
+
"-v",
|
|
2160
|
+
"--version"
|
|
2161
|
+
]);
|
|
2162
|
+
const VALUE_FLAGS = new Set([
|
|
2163
|
+
"--print",
|
|
2164
|
+
"-p",
|
|
2165
|
+
"--agent",
|
|
2166
|
+
"--conversation",
|
|
2167
|
+
"--theme",
|
|
2168
|
+
"--api-url"
|
|
2169
|
+
]);
|
|
2170
|
+
/**
|
|
2171
|
+
* True when `args` contains a bare positional (a subcommand token or a stray
|
|
2172
|
+
* argument) — i.e. a non-dash token that is NOT the value of a preceding
|
|
2173
|
+
* value-taking flag. `skydive --agent grace` has no positional (`grace` is the
|
|
2174
|
+
* value of `--agent`); `skydive agents` does.
|
|
2175
|
+
*/
|
|
2176
|
+
function hasPositional(args) {
|
|
2177
|
+
let expectValue = false;
|
|
2178
|
+
for (const a of args) {
|
|
2179
|
+
if (expectValue) {
|
|
2180
|
+
expectValue = false;
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
if (a.startsWith("-")) {
|
|
2184
|
+
if (!a.includes("=") && VALUE_FLAGS.has(a)) expectValue = true;
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
return true;
|
|
2188
|
+
}
|
|
2189
|
+
return false;
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Decide whether a bare `skydive` invocation (no subcommand) should default to
|
|
2193
|
+
* the interactive chat TUI. True only when it's safe to take over the terminal:
|
|
2194
|
+
* no command/positional present, not a top-level help/version request, not a
|
|
2195
|
+
* known non-interactive env (CI, coding agents), and both stdin and stdout are
|
|
2196
|
+
* real TTYs. Everything else falls through to yargs so scripted/piped use gets
|
|
2197
|
+
* the normal "specify a command" behavior instead of hanging on a prompt.
|
|
2198
|
+
*/
|
|
2199
|
+
function shouldDefaultToChat(args, tty, nonInteractive = isNonInteractive()) {
|
|
2200
|
+
if (hasPositional(args)) return false;
|
|
2201
|
+
if (args.some((a) => TOP_LEVEL_FLAGS.has(a))) return false;
|
|
2202
|
+
if (nonInteractive) return false;
|
|
2203
|
+
return tty.stdinIsTTY && tty.stdoutIsTTY;
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Given the raw args (post-`hideBin`), return the args yargs should parse,
|
|
2207
|
+
* prepending `chat` when a bare interactive invocation should open the TUI.
|
|
2208
|
+
*/
|
|
2209
|
+
function resolveArgv(args, tty = {
|
|
2210
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
2211
|
+
stdoutIsTTY: Boolean(process.stdout.isTTY)
|
|
2212
|
+
}) {
|
|
2213
|
+
return shouldDefaultToChat(args, tty) ? ["chat", ...args] : args;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
//#endregion
|
|
2217
|
+
//#region src/bin.ts
|
|
2218
|
+
createCli(resolveArgv(hideBin(process.argv))).parse();
|
|
2219
|
+
|
|
2220
|
+
//#endregion
|
|
2221
|
+
export { noColorRequested as a, themeMode as c, themesForMode as d, DEFAULT_API_URL as f, saveTheme as g, resolveWebUrl as h, monoTheme as i, themeModeFromColorFgBg as l, getSavedTheme as m, applyTheme as n, theme as o, DEFAULT_APP_URL as p, findTheme as r, themeForMode as s, DEFAULT_THEME_ID as t, themeVersion as u };
|