skydive-cli 0.5.0-beta.8 → 0.5.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/CHANGELOG.md +13 -0
- package/README.md +61 -13
- package/dist/js/api-BFQ4PQDA.mjs +315 -0
- package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
- package/dist/js/bin.mjs +674 -307
- package/dist/js/{boot-BCClEpCM.mjs → boot-DD4T-61U.mjs} +4558 -930
- package/dist/js/chunk-BbwQpWto.mjs +33 -0
- package/dist/js/{client-DabRpc_T.mjs → client--k9cjfkX.mjs} +437 -39
- package/dist/js/{client-c4c5MmgN.mjs → client-Btq6bMzX.mjs} +108 -2
- package/dist/js/client-Ct0-JZSS.mjs +5 -0
- package/dist/js/daemon-CCgNLD0H.mjs +7 -0
- package/dist/js/{daemon-k2kVkJ8D.mjs → daemon-Do1jU2UF.mjs} +123 -43
- package/dist/js/daemon-client-C7nE-lLK.mjs +8 -0
- package/dist/js/{daemon-client-fxf1A25Z.mjs → daemon-client-Dvad009G.mjs} +1 -1
- package/dist/js/dist-CRtjM7ba.mjs +1750 -0
- package/dist/js/forward-C-f04uyE.mjs +208 -0
- package/dist/js/{profiler-DawY0V0Z.mjs → install-CtAVvERm.mjs} +545 -215
- package/dist/js/launcher.mjs +49 -0
- package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
- package/dist/js/{print-Wakr3GJd.mjs → print-Bx8qUC9U.mjs} +3 -3
- package/dist/js/{print-BpuyEfWX.mjs → print-D_UEjdSw.mjs} +257 -35
- package/dist/js/{print-share-CKLPmsg0.mjs → print-share-Cz0EO2RK.mjs} +9 -3
- package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
- package/dist/js/{raw-pty-DY4KelZW.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
- package/dist/js/{rest-I3imNduB.mjs → rest-B9__Zsuk.mjs} +144 -19
- package/dist/js/rest-Dc0EEok3.mjs +6 -0
- package/dist/js/tls-cert-BpCaD5AT.mjs +4 -0
- package/dist/js/tls-cert-Rua2oV7n.mjs +67 -0
- package/package.json +12 -4
- package/dist/js/api-DG5W6iwx.mjs +0 -131
- package/dist/js/client-BuU34IVE.mjs +0 -5
- package/dist/js/daemon-BxU59xie.mjs +0 -6
- package/dist/js/daemon-client-C2BvZgKO.mjs +0 -7
- package/dist/js/forward-18QoL5dO.mjs +0 -68
- package/dist/js/raw-pty-DmdUf4_w.mjs +0 -5
- package/dist/js/rest-D29qNkto.mjs +0 -6
- /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
- /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-BF2NZZE3.mjs} +0 -0
- /package/dist/js/{output-DYzzdXYV.mjs → output-C9mb3sUB.mjs} +0 -0
package/dist/js/bin.mjs
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { t as
|
|
6
|
-
import { t as
|
|
7
|
-
import "./billing-blocked-
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-
|
|
11
|
-
import {
|
|
2
|
+
import { c as version, s as name, t as createRestClient } from "./rest-B9__Zsuk.mjs";
|
|
3
|
+
import { B as resolveSession$1, C as deleteConfig, D as getPreference, E as getLastSeenVersion, G as setLastSeenVersion, H as saveConfig, I as resolveAppUrl, L as resolveChatAuth, M as getStoredApiKeyId, N as getStoredApiKeyWorkspaceName, O as getPromptHistoryPath, P as getUpdateCheckDisabled, R as resolveConfig, S as PREFERENCES, T as getDefaultAgent, U as saveSession, _ as API_KEYS_URL, b as DEFAULT_API_URL, i as resolveAgent$1, j as getShareMachineDefault, v as API_KEY_FAMILY_PREFIX, w as getConfigPath, y as API_KEY_PREFIX, z as resolveManagementAuth } from "./print-D_UEjdSw.mjs";
|
|
4
|
+
import { A as installAgentAlias, B as resolveWorkspaceId, D as machineOsFromPlatform, E as buildImportSeedPrompt, F as detectShell, I as ensureActiveOrganization, L as getActiveWorkspaceId, M as takenAliasNames, N as SUPPORTED_SHELLS, O as aliasActivationHint, P as defaultInstallEnv, R as getSessionIdentity, V as setActiveWorkspace, c as getAutomaticLogsDir, j as slugifyAliasName, k as dedupeAliasName, l as maybeStartProfiling, o as brandHelpArt, t as installCrashHandler, u as profileDir, w as themes, z as listWorkspaces } from "./install-CtAVvERm.mjs";
|
|
5
|
+
import { n as printError, r as printTable, t as output } from "./output-C9mb3sUB.mjs";
|
|
6
|
+
import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
|
|
7
|
+
import "./billing-blocked-D3l5kJlX.mjs";
|
|
8
|
+
import { a as resolveMachineIdentity } from "./client--k9cjfkX.mjs";
|
|
9
|
+
import "./tls-cert-Rua2oV7n.mjs";
|
|
10
|
+
import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-BFQ4PQDA.mjs";
|
|
11
|
+
import { d as daemonPaths, i as queryDaemonStatus, n as ensureDaemonRunning, s as stopDaemon, u as PORTAL_DAEMON_FLAG } from "./daemon-Do1jU2UF.mjs";
|
|
12
|
+
import { a as encodeFsRequest, i as decodeFsResponse, n as SANDBOX_STREAM_PATH, r as streamSpecToQuery, t as SandboxStream } from "./client-Btq6bMzX.mjs";
|
|
12
13
|
import { hideBin } from "yargs/helpers";
|
|
13
14
|
import yargs from "yargs";
|
|
14
|
-
import
|
|
15
|
+
import { hostname, tmpdir } from "node:os";
|
|
15
16
|
import path, { join } from "node:path";
|
|
16
17
|
import { err, ok } from "neverthrow";
|
|
17
18
|
import { z } from "zod";
|
|
18
19
|
import open from "open";
|
|
19
20
|
import fs from "node:fs";
|
|
20
21
|
import { spawn, spawnSync } from "node:child_process";
|
|
21
|
-
import { createHash
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
22
23
|
import zlib from "node:zlib";
|
|
23
24
|
import fsp, { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
24
|
-
import {
|
|
25
|
+
import { WebSocket } from "ws";
|
|
25
26
|
import semver from "semver";
|
|
26
27
|
import { createInterface } from "node:readline";
|
|
28
|
+
import { createInterface as createInterface$1 } from "node:readline/promises";
|
|
29
|
+
import * as tar from "tar";
|
|
27
30
|
|
|
28
31
|
//#region src/types.ts
|
|
29
32
|
const NON_INTERACTIVE_ENV_VARS = [
|
|
@@ -39,6 +42,7 @@ function isNonInteractive() {
|
|
|
39
42
|
//#endregion
|
|
40
43
|
//#region src/api-client.ts
|
|
41
44
|
const USER_AGENT = `skydive-cli/${version}`;
|
|
45
|
+
const CLI_VERSION_HEADER = "x-skydive-cli-version";
|
|
42
46
|
/** Largest page `/v1/agents` will return, enforced server-side. */
|
|
43
47
|
const MAX_AGENT_PAGE = 100;
|
|
44
48
|
const AgentSchema = z.object({
|
|
@@ -70,10 +74,12 @@ var SkydiveApiClient = class {
|
|
|
70
74
|
baseUrl;
|
|
71
75
|
token;
|
|
72
76
|
authKind;
|
|
77
|
+
workspaceId;
|
|
73
78
|
constructor(config) {
|
|
74
79
|
this.baseUrl = `${config.apiUrl.replace(/\/$/, "")}/v1`;
|
|
75
80
|
this.token = config.token;
|
|
76
81
|
this.authKind = config.kind;
|
|
82
|
+
this.workspaceId = config.workspaceId;
|
|
77
83
|
}
|
|
78
84
|
/**
|
|
79
85
|
* List one page of agents in the caller's workspace. The server caps a
|
|
@@ -157,8 +163,10 @@ var SkydiveApiClient = class {
|
|
|
157
163
|
const url = `${this.baseUrl}${params.path}`;
|
|
158
164
|
const headers = {
|
|
159
165
|
authorization: `Bearer ${this.token}`,
|
|
160
|
-
"user-agent": USER_AGENT
|
|
166
|
+
"user-agent": USER_AGENT,
|
|
167
|
+
[CLI_VERSION_HEADER]: version
|
|
161
168
|
};
|
|
169
|
+
if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
|
|
162
170
|
if (params.body) headers["content-type"] = "application/json";
|
|
163
171
|
let response;
|
|
164
172
|
try {
|
|
@@ -616,7 +624,7 @@ const logoutCommand = {
|
|
|
616
624
|
const keyId = getStoredApiKeyId();
|
|
617
625
|
let keyRevoked = false;
|
|
618
626
|
if (keyId) {
|
|
619
|
-
const session = resolveSession({ appUrl: argv["api-url"] });
|
|
627
|
+
const session = resolveSession$1({ appUrl: argv["api-url"] });
|
|
620
628
|
const config = resolveConfig({ apiUrl: argv["api-url"] });
|
|
621
629
|
const credential = session.isOk() ? {
|
|
622
630
|
appUrl: session.value.appUrl,
|
|
@@ -647,7 +655,7 @@ const statusCommand$1 = {
|
|
|
647
655
|
describe: "Show current authentication state",
|
|
648
656
|
handler: async (argv) => {
|
|
649
657
|
const apiKey = resolveConfig({ apiUrl: argv["api-url"] });
|
|
650
|
-
const session = resolveSession({});
|
|
658
|
+
const session = resolveSession$1({});
|
|
651
659
|
const management = resolveManagementAuth({ apiUrl: argv["api-url"] });
|
|
652
660
|
const identity = session.isOk() ? (await getSessionIdentity({
|
|
653
661
|
appUrl: session.value.appUrl,
|
|
@@ -718,11 +726,6 @@ const authCommand = {
|
|
|
718
726
|
* matches on it to keep notices off the user's prompt (see isCompletionProbe).
|
|
719
727
|
*/
|
|
720
728
|
const COMPLETION_PROBE_FLAG = "--get-yargs-completions";
|
|
721
|
-
const SUPPORTED_SHELLS = [
|
|
722
|
-
"bash",
|
|
723
|
-
"zsh",
|
|
724
|
-
"fish"
|
|
725
|
-
];
|
|
726
729
|
/**
|
|
727
730
|
* True when this invocation is a shell asking for completion candidates
|
|
728
731
|
* rather than a user running a command. Such a run must print candidates and
|
|
@@ -894,42 +897,19 @@ complete -c ${name} -f -k -a '(${fn})'
|
|
|
894
897
|
${blockEnd(name)}
|
|
895
898
|
`;
|
|
896
899
|
}
|
|
897
|
-
const defaultInstallEnv = () => ({
|
|
898
|
-
home: os.homedir(),
|
|
899
|
-
platform: process.platform,
|
|
900
|
-
env: process.env,
|
|
901
|
-
exists: fs.existsSync
|
|
902
|
-
});
|
|
903
|
-
/**
|
|
904
|
-
* The shell to install for when the user didn't name one. `$SHELL` is the
|
|
905
|
-
* only signal available: a completion install runs as a child process, so the
|
|
906
|
-
* invoking shell's own variables (`$ZSH_VERSION`, `$FISH_VERSION`) aren't
|
|
907
|
-
* visible here.
|
|
908
|
-
*/
|
|
909
|
-
function detectShell(env = process.env) {
|
|
910
|
-
const shell = env["SHELL"];
|
|
911
|
-
if (!shell) return null;
|
|
912
|
-
const name = path.basename(shell);
|
|
913
|
-
return SUPPORTED_SHELLS.find((candidate) => name === candidate) ?? null;
|
|
914
|
-
}
|
|
915
900
|
function completionTarget(shell, deps = defaultInstallEnv()) {
|
|
916
901
|
const { home, platform, env, exists } = deps;
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
mode: "block"
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
const rc = path.join(home, ".bashrc");
|
|
932
|
-
const profile = path.join(home, ".bash_profile");
|
|
902
|
+
const join = path.posix.join;
|
|
903
|
+
if (shell === "fish") return {
|
|
904
|
+
path: join(env["XDG_CONFIG_HOME"] || join(home, ".config"), "fish", "completions", "skydive.fish"),
|
|
905
|
+
mode: "file"
|
|
906
|
+
};
|
|
907
|
+
if (shell === "zsh") return {
|
|
908
|
+
path: join(env["ZDOTDIR"] || home, ".zshrc"),
|
|
909
|
+
mode: "block"
|
|
910
|
+
};
|
|
911
|
+
const rc = join(home, ".bashrc");
|
|
912
|
+
const profile = join(home, ".bash_profile");
|
|
933
913
|
const preferred = platform === "darwin" ? profile : rc;
|
|
934
914
|
const alternate = platform === "darwin" ? rc : profile;
|
|
935
915
|
if (!exists(preferred) && exists(alternate)) return {
|
|
@@ -1020,7 +1000,7 @@ const completionCommand = {
|
|
|
1020
1000
|
* resolve-or-exit block isn't copy-pasted per command.
|
|
1021
1001
|
*/
|
|
1022
1002
|
function requireSession(argv) {
|
|
1023
|
-
const session = resolveSession({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
1003
|
+
const session = resolveSession$1({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
1024
1004
|
if (session.isErr()) {
|
|
1025
1005
|
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1026
1006
|
process.exit(1);
|
|
@@ -1032,7 +1012,7 @@ function requireSession(argv) {
|
|
|
1032
1012
|
* {@link resolveManagementAuth}) or exit with a friendly hint, then build the
|
|
1033
1013
|
* `/v1` client. Shared by `agents`, `keys` and `secrets`.
|
|
1034
1014
|
*/
|
|
1035
|
-
function requireManagementClient(argv) {
|
|
1015
|
+
function requireManagementClient(argv, opts = {}) {
|
|
1036
1016
|
const auth = resolveManagementAuth({ apiUrl: argv["api-url"] });
|
|
1037
1017
|
if (auth.isErr()) {
|
|
1038
1018
|
printError(auth.error.message);
|
|
@@ -1040,7 +1020,10 @@ function requireManagementClient(argv) {
|
|
|
1040
1020
|
}
|
|
1041
1021
|
if (auth.value.kind === "api-key" && auth.value.pinnedWorkspaceName) process.stderr.write(`Using this machine's API key, which always acts in the "${auth.value.pinnedWorkspaceName}" workspace (even after \`workspace switch\`). Run \`skydive auth login\` to change that.
|
|
1042
1022
|
`);
|
|
1043
|
-
return new SkydiveApiClient(
|
|
1023
|
+
return new SkydiveApiClient({
|
|
1024
|
+
...auth.value,
|
|
1025
|
+
...opts.workspaceId ? { workspaceId: opts.workspaceId } : {}
|
|
1026
|
+
});
|
|
1044
1027
|
}
|
|
1045
1028
|
/** Resolve the session (see {@link requireSession}) and build a REST client. */
|
|
1046
1029
|
function requireRestClient(argv) {
|
|
@@ -1057,7 +1040,32 @@ function requireRestClient(argv) {
|
|
|
1057
1040
|
|
|
1058
1041
|
//#endregion
|
|
1059
1042
|
//#region src/commands/agents.ts
|
|
1060
|
-
const
|
|
1043
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1044
|
+
/**
|
|
1045
|
+
* Resolve an agent from an id or a name. A uuid selector hits `GET
|
|
1046
|
+
* /agents/:id` directly; anything else is matched case-insensitively against
|
|
1047
|
+
* the roster's names. Ambiguity is an error rather than a silent pick, so an
|
|
1048
|
+
* alias never lands on the wrong agent.
|
|
1049
|
+
*/
|
|
1050
|
+
async function resolveAgent(client, selector) {
|
|
1051
|
+
if (UUID_RE.test(selector)) {
|
|
1052
|
+
const result = await client.getAgent(selector);
|
|
1053
|
+
if (result.isErr()) throw new Error(result.error.message);
|
|
1054
|
+
return result.value;
|
|
1055
|
+
}
|
|
1056
|
+
const list = await client.listAgents({
|
|
1057
|
+
scope: "org",
|
|
1058
|
+
limit: 100
|
|
1059
|
+
});
|
|
1060
|
+
if (list.isErr()) throw new Error(list.error.message);
|
|
1061
|
+
const needle = selector.trim().toLowerCase();
|
|
1062
|
+
const matches = list.value.agents.filter((a) => a.name.toLowerCase() === needle);
|
|
1063
|
+
const [first] = matches;
|
|
1064
|
+
if (matches.length === 1 && first) return first;
|
|
1065
|
+
if (matches.length === 0) throw new Error(`No agent named "${selector}". Pass an id, or run \`skydive agents list\`.`);
|
|
1066
|
+
throw new Error(`Multiple agents named "${selector}". Pass the id instead:\n${matches.map((a) => ` ${a.name} ${a.id}`).join("\n")}`);
|
|
1067
|
+
}
|
|
1068
|
+
const listCommand$6 = {
|
|
1061
1069
|
command: "list",
|
|
1062
1070
|
describe: "List agents in the active workspace",
|
|
1063
1071
|
builder: (y) => y.option("limit", {
|
|
@@ -1206,10 +1214,82 @@ const createCommand$1 = {
|
|
|
1206
1214
|
if (agent.url) console.log(` API: ${agent.url}`);
|
|
1207
1215
|
}
|
|
1208
1216
|
};
|
|
1217
|
+
/**
|
|
1218
|
+
* Resolve the `--workspace` selector (slug/name/id) to a workspace, using the
|
|
1219
|
+
* chat/session credential (workspace listing is a session endpoint). Exits
|
|
1220
|
+
* with a clear message on a bad selector or when listing isn't available
|
|
1221
|
+
* (e.g. a pinned API key). Returns null when the flag is absent.
|
|
1222
|
+
*/
|
|
1223
|
+
async function resolveAliasWorkspace(argv) {
|
|
1224
|
+
if (!argv.workspace) return null;
|
|
1225
|
+
const auth = resolveChatAuth({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
1226
|
+
if (auth.isErr()) {
|
|
1227
|
+
printError(`${auth.error.message} Run \`skydive auth login\` to resolve a workspace by name.`);
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
const resolved = await resolveWorkspaceId({
|
|
1231
|
+
appUrl: auth.value.appUrl,
|
|
1232
|
+
sessionToken: auth.value.token,
|
|
1233
|
+
selector: argv.workspace
|
|
1234
|
+
});
|
|
1235
|
+
if (resolved.isErr()) {
|
|
1236
|
+
printError(resolved.error.message);
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
return resolved.value;
|
|
1240
|
+
}
|
|
1241
|
+
const aliasCommand = {
|
|
1242
|
+
command: "alias <agent> [name]",
|
|
1243
|
+
describe: "Add a shell alias that opens an agent's chat",
|
|
1244
|
+
builder: (y) => y.positional("agent", {
|
|
1245
|
+
type: "string",
|
|
1246
|
+
demandOption: true,
|
|
1247
|
+
describe: "Agent id or name to alias"
|
|
1248
|
+
}).positional("name", {
|
|
1249
|
+
type: "string",
|
|
1250
|
+
describe: "Alias to use (default: derived from the agent name)"
|
|
1251
|
+
}).option("shell", {
|
|
1252
|
+
type: "string",
|
|
1253
|
+
choices: SUPPORTED_SHELLS,
|
|
1254
|
+
describe: "Shell to install for (default: $SHELL)"
|
|
1255
|
+
}).option("workspace", {
|
|
1256
|
+
type: "string",
|
|
1257
|
+
describe: "Workspace the agent lives in (slug, name, or id). Scopes the lookup to it and pins it in the alias, so the alias opens the right agent even when it isn't your active workspace."
|
|
1258
|
+
}).example("skydive agents alias Ripple", "alias `ripple` -> that agent").example("skydive agents alias Ripple rip", "use a custom alias token").example("skydive agents alias Ripple --workspace acme", "pin the workspace in the alias"),
|
|
1259
|
+
handler: async (argv) => {
|
|
1260
|
+
const shell = argv.shell ? SUPPORTED_SHELLS.find((s) => s === argv.shell) : detectShell();
|
|
1261
|
+
if (!shell) throw new Error(argv.shell ? `Unsupported shell "${argv.shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}.` : `Could not tell which shell to use from $SHELL. Name one with --shell (${SUPPORTED_SHELLS.join(", ")}).`);
|
|
1262
|
+
const workspace = await resolveAliasWorkspace(argv);
|
|
1263
|
+
const agent = await resolveAgent(requireManagementClient(argv, { workspaceId: workspace?.id }), argv.agent);
|
|
1264
|
+
const preferred = argv.name ? slugifyAliasName(argv.name) : slugifyAliasName(agent.name);
|
|
1265
|
+
const alias = dedupeAliasName(preferred, takenAliasNames(shell));
|
|
1266
|
+
const result = installAgentAlias({
|
|
1267
|
+
alias,
|
|
1268
|
+
agentId: agent.id,
|
|
1269
|
+
shell,
|
|
1270
|
+
...workspace ? { workspaceId: workspace.id } : {}
|
|
1271
|
+
}, void 0);
|
|
1272
|
+
if (argv.json) {
|
|
1273
|
+
output(argv, {
|
|
1274
|
+
...result,
|
|
1275
|
+
workspace: workspace ?? null
|
|
1276
|
+
});
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (argv.quiet) {
|
|
1280
|
+
console.log(result.alias);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const where = workspace ? ` (workspace ${workspace.name})` : "";
|
|
1284
|
+
console.log(`${result.updated ? "Updated" : "Added"} alias \`${result.alias}\` -> ${agent.name}${where} in ${result.path}`);
|
|
1285
|
+
if (alias !== preferred) console.log(`(\`${preferred}\` was taken, so used \`${alias}\`.)`);
|
|
1286
|
+
console.log(aliasActivationHint(result));
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1209
1289
|
const agentsCommand = {
|
|
1210
1290
|
command: "agents",
|
|
1211
1291
|
describe: "Manage agents",
|
|
1212
|
-
builder: (y) => y.command(listCommand$
|
|
1292
|
+
builder: (y) => y.command(listCommand$6).command(getCommand$2).command(createCommand$1).command(aliasCommand).demandCommand(1, "Specify a subcommand: list, get, create, alias"),
|
|
1213
1293
|
handler: () => {}
|
|
1214
1294
|
};
|
|
1215
1295
|
|
|
@@ -1347,7 +1427,7 @@ function sha256(buf) {
|
|
|
1347
1427
|
* sizes; the central directory is the only place with correct sizes/offsets.
|
|
1348
1428
|
*/
|
|
1349
1429
|
function extractBunFromZip(zip, target) {
|
|
1350
|
-
const exeName =
|
|
1430
|
+
const exeName = target.startsWith("bun-windows") ? "bun.exe" : "bun";
|
|
1351
1431
|
let eocd = -1;
|
|
1352
1432
|
for (let i = zip.length - 22; i >= 0; i--) if (zip.readUInt32LE(i) === 101010256) {
|
|
1353
1433
|
eocd = i;
|
|
@@ -1463,19 +1543,6 @@ function ensureBunAndReexec(onProgress) {
|
|
|
1463
1543
|
|
|
1464
1544
|
//#endregion
|
|
1465
1545
|
//#region src/commands/import.ts
|
|
1466
|
-
/**
|
|
1467
|
-
* The first message of the import conversation, sent as the user. It hands
|
|
1468
|
-
* the real work to the agent's `import-config` skill; this text only needs
|
|
1469
|
-
* to establish intent, anchor the starting directory, and set the two
|
|
1470
|
-
* expectations the user cares about most (plan first, no credentials).
|
|
1471
|
-
*/
|
|
1472
|
-
function buildImportSeedPrompt(projectDir) {
|
|
1473
|
-
return [
|
|
1474
|
-
"I'm migrating from another coding agent. Import my setup from this machine.",
|
|
1475
|
-
"",
|
|
1476
|
-
`Use your import-config skill. I ran this from \`${projectDir}\`, so start there and in my home directory. Don't assume one tool — do the discovery sweep so you catch whatever I actually use (Claude Code, Cursor, Codex, Gemini CLI, Copilot, Windsurf, Cline, OpenCode, Aider, and any nested AGENTS.md). Show me the plan first: everything you found, what you'll bring over, where it lands in you, and anything you're leaving out (credentials especially). Then wait for my OK before committing anything.`
|
|
1477
|
-
].join("\n");
|
|
1478
|
-
}
|
|
1479
1546
|
const importCommand = {
|
|
1480
1547
|
command: "import",
|
|
1481
1548
|
describe: "Import your existing coding-agent setup (Claude Code, Cursor, Codex, Gemini, Copilot, and more) into an agent (opens a machine-shared chat)",
|
|
@@ -1504,30 +1571,33 @@ const importCommand = {
|
|
|
1504
1571
|
printError(`import needs the Bun runtime and it couldn't be set up automatically. Install Bun (https://bun.sh) and run \`bun ${process.argv[1] ?? "skydive"} import\`, point SKYDIVE_BUN_PATH at an existing bun binary, or use the non-interactive \`import -p\` (runs under Node).`);
|
|
1505
1572
|
process.exit(1);
|
|
1506
1573
|
}
|
|
1507
|
-
let session = resolveSession({ appUrl });
|
|
1574
|
+
let session = resolveSession$1({ appUrl });
|
|
1508
1575
|
if (session.isErr()) {
|
|
1509
1576
|
const login = await loginWithDevice({ appUrl });
|
|
1510
1577
|
if (login.isErr()) {
|
|
1511
1578
|
printError(login.error.message);
|
|
1512
1579
|
process.exit(1);
|
|
1513
1580
|
}
|
|
1514
|
-
session = resolveSession({ appUrl });
|
|
1581
|
+
session = resolveSession$1({ appUrl });
|
|
1515
1582
|
if (session.isErr()) {
|
|
1516
1583
|
printError("Signed in, but no session was stored.");
|
|
1517
1584
|
process.exit(1);
|
|
1518
1585
|
}
|
|
1519
1586
|
}
|
|
1520
|
-
const { runChat } = await import("./boot-
|
|
1587
|
+
const { runChat } = await import("./boot-DD4T-61U.mjs");
|
|
1521
1588
|
await runChat({
|
|
1522
1589
|
appUrl,
|
|
1523
1590
|
sessionToken: session.value.sessionToken,
|
|
1591
|
+
workspaceId: void 0,
|
|
1524
1592
|
shareMachine: true,
|
|
1525
1593
|
promptHistoryPath: getPromptHistoryPath(),
|
|
1526
1594
|
notifications: true,
|
|
1527
1595
|
agentSelector: argv.agent ?? null,
|
|
1528
1596
|
conversationId: null,
|
|
1529
1597
|
newConversation: false,
|
|
1530
|
-
|
|
1598
|
+
defaultAgentSelector: getDefaultAgent(),
|
|
1599
|
+
forceOnboarding: false,
|
|
1600
|
+
seedPrompt: buildImportSeedPrompt(process.cwd(), machineOsFromPlatform(process.platform))
|
|
1531
1601
|
});
|
|
1532
1602
|
}
|
|
1533
1603
|
};
|
|
@@ -1539,20 +1609,20 @@ const importCommand = {
|
|
|
1539
1609
|
* (`chat -p "go ahead" --resume <id> --share-machine`) to apply it.
|
|
1540
1610
|
*/
|
|
1541
1611
|
async function runImportPrintMode({ argv, appUrl }) {
|
|
1542
|
-
const session = resolveSession({ appUrl });
|
|
1612
|
+
const session = resolveSession$1({ appUrl });
|
|
1543
1613
|
if (session.isErr()) {
|
|
1544
1614
|
printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1545
1615
|
process.exit(1);
|
|
1546
1616
|
}
|
|
1547
|
-
const { connectMachineShare } = await import("./print-share-
|
|
1617
|
+
const { connectMachineShare } = await import("./print-share-Cz0EO2RK.mjs");
|
|
1548
1618
|
const machineShare = await connectMachineShare({
|
|
1549
1619
|
appUrl,
|
|
1550
1620
|
sessionToken: session.value.sessionToken,
|
|
1551
1621
|
timeoutHint: "Check `skydive portal status`, then re-run."
|
|
1552
1622
|
});
|
|
1553
1623
|
const extra = (argv.print ?? "").trim();
|
|
1554
|
-
const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
|
|
1555
|
-
const { runPrint } = await import("./print-
|
|
1624
|
+
const prompt = buildImportSeedPrompt(process.cwd(), machineOsFromPlatform(process.platform)) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
|
|
1625
|
+
const { runPrint } = await import("./print-Bx8qUC9U.mjs");
|
|
1556
1626
|
try {
|
|
1557
1627
|
const result = await runPrint({
|
|
1558
1628
|
appUrl,
|
|
@@ -1576,7 +1646,7 @@ async function runImportPrintMode({ argv, appUrl }) {
|
|
|
1576
1646
|
|
|
1577
1647
|
//#endregion
|
|
1578
1648
|
//#region src/commands/keys.ts
|
|
1579
|
-
const listCommand$
|
|
1649
|
+
const listCommand$5 = {
|
|
1580
1650
|
command: "list",
|
|
1581
1651
|
describe: "List API keys for an agent",
|
|
1582
1652
|
handler: async (argv) => {
|
|
@@ -1665,7 +1735,7 @@ const keysCommand = {
|
|
|
1665
1735
|
type: "string",
|
|
1666
1736
|
demandOption: true,
|
|
1667
1737
|
describe: "Agent ID"
|
|
1668
|
-
}).command(listCommand$
|
|
1738
|
+
}).command(listCommand$5).command(createCommand).command(revokeCommand$1).demandCommand(1, "Specify a subcommand: list, create, revoke"),
|
|
1669
1739
|
handler: () => {}
|
|
1670
1740
|
};
|
|
1671
1741
|
|
|
@@ -1677,7 +1747,7 @@ async function readStdin() {
|
|
|
1677
1747
|
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1678
1748
|
return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
1679
1749
|
}
|
|
1680
|
-
const listCommand$
|
|
1750
|
+
const listCommand$4 = {
|
|
1681
1751
|
command: "list",
|
|
1682
1752
|
describe: "List secret names for an agent (values are never shown)",
|
|
1683
1753
|
handler: async (argv) => {
|
|
@@ -1763,7 +1833,7 @@ const secretsCommand = {
|
|
|
1763
1833
|
type: "string",
|
|
1764
1834
|
demandOption: true,
|
|
1765
1835
|
describe: "Agent ID"
|
|
1766
|
-
}).command(listCommand$
|
|
1836
|
+
}).command(listCommand$4).command(setCommand$1).command(rmCommand).demandCommand(1, "Specify a subcommand: list, set, rm"),
|
|
1767
1837
|
handler: () => {}
|
|
1768
1838
|
};
|
|
1769
1839
|
|
|
@@ -1860,6 +1930,24 @@ async function resolveAgentWorkspace({ appUrl, sessionToken, agentSelector }) {
|
|
|
1860
1930
|
|
|
1861
1931
|
//#endregion
|
|
1862
1932
|
//#region src/commands/chat.ts
|
|
1933
|
+
/**
|
|
1934
|
+
* Resolve the `--workspace` selector to an id, or undefined when the flag is
|
|
1935
|
+
* absent. A bad selector is fatal — a chat scoped to the wrong (or a
|
|
1936
|
+
* nonexistent) workspace is worse than failing fast with how to list them.
|
|
1937
|
+
*/
|
|
1938
|
+
async function resolveWorkspaceFlag({ appUrl, sessionToken, selector }) {
|
|
1939
|
+
if (!selector) return void 0;
|
|
1940
|
+
const resolved = await resolveWorkspaceId({
|
|
1941
|
+
appUrl,
|
|
1942
|
+
sessionToken,
|
|
1943
|
+
selector
|
|
1944
|
+
});
|
|
1945
|
+
if (resolved.isErr()) {
|
|
1946
|
+
printError(resolved.error.message);
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
return resolved.value.id;
|
|
1950
|
+
}
|
|
1863
1951
|
const chatCommand = {
|
|
1864
1952
|
command: "chat",
|
|
1865
1953
|
describe: "Open the interactive chat TUI, or -p for a one-shot",
|
|
@@ -1870,6 +1958,9 @@ const chatCommand = {
|
|
|
1870
1958
|
}).option("agent", {
|
|
1871
1959
|
type: "string",
|
|
1872
1960
|
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."
|
|
1961
|
+
}).option("workspace", {
|
|
1962
|
+
type: "string",
|
|
1963
|
+
describe: "Scope this chat to a workspace, by slug, name, or id. Every request for this invocation is scoped to it (no persistent switch of your active workspace), so a slug/name --agent resolves inside it and the roster is that workspace. Useful in an alias for an agent that lives in a workspace other than your active one."
|
|
1873
1964
|
}).option("conversation", {
|
|
1874
1965
|
alias: "resume",
|
|
1875
1966
|
type: "string",
|
|
@@ -1878,6 +1969,10 @@ const chatCommand = {
|
|
|
1878
1969
|
type: "boolean",
|
|
1879
1970
|
default: false,
|
|
1880
1971
|
describe: "Start a fresh conversation instead of opening the conversation list. With --agent, opens a new conversation with that agent directly. Without --agent, opens a new conversation once you pick an agent (auto-picked if the account has only one). Ignored with --resume, which always continues the named conversation."
|
|
1972
|
+
}).option("onboarding", {
|
|
1973
|
+
type: "boolean",
|
|
1974
|
+
default: false,
|
|
1975
|
+
describe: "Force the first-run onboarding flow (auto-create a first agent, then offer the machine import) even when the workspace already has agents. Also settable via SKYDIVE_FORCE_ONBOARDING=1. Handy for replaying the new-user experience; ignored with -p, --resume, or --agent."
|
|
1881
1976
|
}).option("share-machine", {
|
|
1882
1977
|
type: "boolean",
|
|
1883
1978
|
describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; in the TUI you approve per agent, with -p the flag grants the target agent for the run). Defaults to `shareMachineDefault` in the CLI config file; --no-share-machine disables for this invocation."
|
|
@@ -1919,21 +2014,27 @@ const chatCommand = {
|
|
|
1919
2014
|
process.exit(1);
|
|
1920
2015
|
}
|
|
1921
2016
|
}
|
|
2017
|
+
const workspaceId = await resolveWorkspaceFlag({
|
|
2018
|
+
appUrl,
|
|
2019
|
+
sessionToken: auth.value.token,
|
|
2020
|
+
selector: argv.workspace
|
|
2021
|
+
});
|
|
1922
2022
|
const themeId = argv.theme ?? process.env["SKYDIVE_THEME"] ?? void 0;
|
|
1923
2023
|
if (themeId !== void 0 && !themes.some((t) => t.id === themeId)) {
|
|
1924
2024
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
1925
2025
|
process.exit(1);
|
|
1926
2026
|
}
|
|
1927
|
-
await ensureAgentWorkspace({
|
|
2027
|
+
if (!workspaceId) await ensureAgentWorkspace({
|
|
1928
2028
|
appUrl,
|
|
1929
2029
|
sessionToken: auth.value.token,
|
|
1930
2030
|
agentSelector: argv.agent ?? null
|
|
1931
2031
|
});
|
|
1932
|
-
const { runChat } = await import("./boot-
|
|
2032
|
+
const { runChat } = await import("./boot-DD4T-61U.mjs");
|
|
1933
2033
|
await runChat({
|
|
1934
2034
|
appUrl: auth.value.appUrl,
|
|
1935
2035
|
sessionToken: auth.value.token,
|
|
1936
2036
|
authKind: auth.value.kind,
|
|
2037
|
+
workspaceId,
|
|
1937
2038
|
shareMachine: resolveShareMachine(argv),
|
|
1938
2039
|
promptHistoryPath: getPromptHistoryPath(),
|
|
1939
2040
|
theme: themeId,
|
|
@@ -1941,6 +2042,8 @@ const chatCommand = {
|
|
|
1941
2042
|
agentSelector: argv.agent ?? null,
|
|
1942
2043
|
conversationId: argv.conversation ?? null,
|
|
1943
2044
|
newConversation: resolveNewConversation(argv),
|
|
2045
|
+
forceOnboarding: resolveForceOnboarding(argv),
|
|
2046
|
+
defaultAgentSelector: getDefaultAgent(),
|
|
1944
2047
|
seedPrompt: null
|
|
1945
2048
|
});
|
|
1946
2049
|
}
|
|
@@ -1962,6 +2065,20 @@ function resolveNewConversation(argv) {
|
|
|
1962
2065
|
return argv.new === true && !argv.conversation;
|
|
1963
2066
|
}
|
|
1964
2067
|
/**
|
|
2068
|
+
* Whether to force the first-run onboarding flow: the `--onboarding` flag or
|
|
2069
|
+
* `SKYDIVE_FORCE_ONBOARDING` (any non-empty, non-"0"/"false" value). Lets the
|
|
2070
|
+
* new-user experience be replayed in a workspace that already has agents. The
|
|
2071
|
+
* TUI applies it only to a bare interactive launch — an explicit
|
|
2072
|
+
* `--agent`/`--resume`/`--new` still takes precedence.
|
|
2073
|
+
*/
|
|
2074
|
+
function resolveForceOnboarding(argv, env = process.env) {
|
|
2075
|
+
if (argv.onboarding === true) return true;
|
|
2076
|
+
const raw = env["SKYDIVE_FORCE_ONBOARDING"];
|
|
2077
|
+
if (raw === void 0) return false;
|
|
2078
|
+
const value = raw.trim().toLowerCase();
|
|
2079
|
+
return value !== "" && value !== "0" && value !== "false";
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
1965
2082
|
* Cross-workspace `--agent <uuid>` handling shared by the TUI and -p paths:
|
|
1966
2083
|
* on a hit in another workspace, switch to it and say so on stderr (stdout
|
|
1967
2084
|
* may carry the -p reply/JSON envelope); when the id matches no workspace on
|
|
@@ -1996,8 +2113,13 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
1996
2113
|
printError(`${auth.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
|
|
1997
2114
|
process.exit(1);
|
|
1998
2115
|
}
|
|
1999
|
-
const { runPrint, readStdin } = await import("./print-
|
|
2000
|
-
await
|
|
2116
|
+
const { runPrint, readStdin } = await import("./print-Bx8qUC9U.mjs");
|
|
2117
|
+
const workspaceId = await resolveWorkspaceFlag({
|
|
2118
|
+
appUrl,
|
|
2119
|
+
sessionToken: auth.value.token,
|
|
2120
|
+
selector: argv.workspace
|
|
2121
|
+
});
|
|
2122
|
+
if (!workspaceId) await ensureAgentWorkspace({
|
|
2001
2123
|
appUrl,
|
|
2002
2124
|
sessionToken: auth.value.token,
|
|
2003
2125
|
agentSelector: argv.agent ?? null
|
|
@@ -2021,7 +2143,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2021
2143
|
process.exit(1);
|
|
2022
2144
|
}
|
|
2023
2145
|
} else {
|
|
2024
|
-
const { connectMachineShare } = await import("./print-share-
|
|
2146
|
+
const { connectMachineShare } = await import("./print-share-Cz0EO2RK.mjs");
|
|
2025
2147
|
machineShare = await connectMachineShare({
|
|
2026
2148
|
appUrl: auth.value.appUrl,
|
|
2027
2149
|
sessionToken: auth.value.token,
|
|
@@ -2032,6 +2154,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2032
2154
|
const result = await runPrint({
|
|
2033
2155
|
appUrl: auth.value.appUrl,
|
|
2034
2156
|
sessionToken: auth.value.token,
|
|
2157
|
+
workspaceId,
|
|
2035
2158
|
prompt,
|
|
2036
2159
|
agentSelector: argv.agent ?? null,
|
|
2037
2160
|
conversationId: argv.conversation ?? null,
|
|
@@ -2041,7 +2164,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2041
2164
|
});
|
|
2042
2165
|
if (argv.json) output(argv, result);
|
|
2043
2166
|
} catch (error) {
|
|
2044
|
-
const { BillingBlockedError, BILLING_BLOCKED_EXIT_CODE, billingBlockedJsonResult } = await import("./billing-blocked-
|
|
2167
|
+
const { BillingBlockedError, BILLING_BLOCKED_EXIT_CODE, billingBlockedJsonResult } = await import("./billing-blocked-SE6tySLd.mjs");
|
|
2045
2168
|
if (error instanceof BillingBlockedError) {
|
|
2046
2169
|
if (argv.json) output(argv, billingBlockedJsonResult(error, null));
|
|
2047
2170
|
printError(error.message);
|
|
@@ -2085,7 +2208,7 @@ const getCommand$1 = {
|
|
|
2085
2208
|
printError(`${auth.error.message} Run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
|
|
2086
2209
|
process.exit(1);
|
|
2087
2210
|
}
|
|
2088
|
-
const { messageGet } = await import("./print-
|
|
2211
|
+
const { messageGet } = await import("./print-Bx8qUC9U.mjs");
|
|
2089
2212
|
try {
|
|
2090
2213
|
const result = await messageGet({
|
|
2091
2214
|
appUrl: auth.value.appUrl,
|
|
@@ -2095,7 +2218,7 @@ const getCommand$1 = {
|
|
|
2095
2218
|
});
|
|
2096
2219
|
if (argv.json) output(argv, result);
|
|
2097
2220
|
} catch (error) {
|
|
2098
|
-
const { BillingBlockedError, BILLING_BLOCKED_EXIT_CODE, billingBlockedJsonResult } = await import("./billing-blocked-
|
|
2221
|
+
const { BillingBlockedError, BILLING_BLOCKED_EXIT_CODE, billingBlockedJsonResult } = await import("./billing-blocked-SE6tySLd.mjs");
|
|
2099
2222
|
if (error instanceof BillingBlockedError) {
|
|
2100
2223
|
if (argv.json) output(argv, billingBlockedJsonResult(error, argv["message-id"]));
|
|
2101
2224
|
printError(error.message);
|
|
@@ -2132,7 +2255,7 @@ function messageToText(message) {
|
|
|
2132
2255
|
}
|
|
2133
2256
|
return chunks.join("\n").trim();
|
|
2134
2257
|
}
|
|
2135
|
-
const listCommand$
|
|
2258
|
+
const listCommand$3 = {
|
|
2136
2259
|
command: "list",
|
|
2137
2260
|
describe: "List an agent's conversations",
|
|
2138
2261
|
builder: (y) => y.option("agent", {
|
|
@@ -2144,7 +2267,7 @@ const listCommand$2 = {
|
|
|
2144
2267
|
}),
|
|
2145
2268
|
handler: async (argv) => {
|
|
2146
2269
|
const client = requireRestClient(argv);
|
|
2147
|
-
const agent = resolveAgent(await client.listAgents({
|
|
2270
|
+
const agent = resolveAgent$1(await client.listAgents({
|
|
2148
2271
|
scope: "org",
|
|
2149
2272
|
onPage: null
|
|
2150
2273
|
}), argv.agent ?? null);
|
|
@@ -2201,7 +2324,7 @@ const showCommand = {
|
|
|
2201
2324
|
console.log(recap ?? "(no recap available)");
|
|
2202
2325
|
return;
|
|
2203
2326
|
}
|
|
2204
|
-
const messages = await client.listMessages({ conversationId });
|
|
2327
|
+
const { messages } = await client.listMessages({ conversationId });
|
|
2205
2328
|
if (argv.json) {
|
|
2206
2329
|
output(argv, messages);
|
|
2207
2330
|
return;
|
|
@@ -2222,7 +2345,7 @@ const conversationsCommand = {
|
|
|
2222
2345
|
command: "conversations",
|
|
2223
2346
|
aliases: ["conv"],
|
|
2224
2347
|
describe: "List and read agent conversations",
|
|
2225
|
-
builder: (y) => y.command(listCommand$
|
|
2348
|
+
builder: (y) => y.command(listCommand$3).command(showCommand).demandCommand(1, "Specify a subcommand: list, show"),
|
|
2226
2349
|
handler: () => {}
|
|
2227
2350
|
};
|
|
2228
2351
|
|
|
@@ -2264,7 +2387,7 @@ async function runList(argv, { hint }) {
|
|
|
2264
2387
|
]));
|
|
2265
2388
|
if (hint && !argv.quiet) console.log("\nRun `skydive workspace switch` to pick a workspace, or pass its slug directly.");
|
|
2266
2389
|
}
|
|
2267
|
-
const listCommand$
|
|
2390
|
+
const listCommand$2 = {
|
|
2268
2391
|
command: "list",
|
|
2269
2392
|
describe: "List workspaces on your account",
|
|
2270
2393
|
handler: (argv) => runList(argv, { hint: false })
|
|
@@ -2288,7 +2411,7 @@ const switchCommand = {
|
|
|
2288
2411
|
printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
|
|
2289
2412
|
process.exit(1);
|
|
2290
2413
|
}
|
|
2291
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
2414
|
+
const { runWorkspacePicker } = await import("./boot-DD4T-61U.mjs");
|
|
2292
2415
|
await runWorkspacePicker(session);
|
|
2293
2416
|
return;
|
|
2294
2417
|
}
|
|
@@ -2321,7 +2444,7 @@ const switchCommand = {
|
|
|
2321
2444
|
const workspaceCommand = {
|
|
2322
2445
|
command: "workspace",
|
|
2323
2446
|
describe: "List or switch the workspace all `skydive` commands act on",
|
|
2324
|
-
builder: (y) => y.command(listCommand$
|
|
2447
|
+
builder: (y) => y.command(listCommand$2).command(switchCommand),
|
|
2325
2448
|
handler: (argv) => runList(argv, { hint: true })
|
|
2326
2449
|
};
|
|
2327
2450
|
|
|
@@ -2337,10 +2460,10 @@ const workspaceCommand = {
|
|
|
2337
2460
|
*/
|
|
2338
2461
|
async function resolveGrantTarget(argv) {
|
|
2339
2462
|
const session = requireSession(argv);
|
|
2340
|
-
const [{ devices, agents }, identity] = await Promise.all([fetchPortalDevices(session), resolveMachineIdentity()]);
|
|
2463
|
+
const [{ devices, agents }, identity] = await Promise.all([fetchPortalDevices(session), resolveMachineIdentity(null)]);
|
|
2341
2464
|
return {
|
|
2342
2465
|
session,
|
|
2343
|
-
agent: resolveAgent(agents, argv.agent),
|
|
2466
|
+
agent: resolveAgent$1(agents, argv.agent),
|
|
2344
2467
|
device: findThisDevice(devices, identity.machineName),
|
|
2345
2468
|
identity
|
|
2346
2469
|
};
|
|
@@ -2364,10 +2487,10 @@ const openCommand = {
|
|
|
2364
2487
|
}),
|
|
2365
2488
|
handler: async (argv) => {
|
|
2366
2489
|
const session = requireSession(argv);
|
|
2367
|
-
const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
2490
|
+
const agent = argv.agent ? resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
2368
2491
|
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
2369
|
-
const { machineName } = await resolveMachineIdentity();
|
|
2370
|
-
const { PortalDaemonClient } = await import("./daemon-client-
|
|
2492
|
+
const { machineName } = await resolveMachineIdentity(null);
|
|
2493
|
+
const { PortalDaemonClient } = await import("./daemon-client-C7nE-lLK.mjs");
|
|
2371
2494
|
let lastLine = "";
|
|
2372
2495
|
let signalConnected;
|
|
2373
2496
|
const connected = new Promise((resolve) => {
|
|
@@ -2454,23 +2577,66 @@ const statusCommand = {
|
|
|
2454
2577
|
command: "status",
|
|
2455
2578
|
describe: "Show your portal machines, connection state, and granted agents",
|
|
2456
2579
|
handler: async (argv) => {
|
|
2457
|
-
const
|
|
2580
|
+
const session = requireSession(argv);
|
|
2581
|
+
const [{ devices, agents }, daemon, identity] = await Promise.all([
|
|
2582
|
+
fetchPortalDevices(session),
|
|
2583
|
+
queryDaemonStatus(session.appUrl),
|
|
2584
|
+
resolveMachineIdentity(null)
|
|
2585
|
+
]);
|
|
2458
2586
|
if (argv.json) {
|
|
2459
2587
|
output(argv, {
|
|
2460
2588
|
devices,
|
|
2461
|
-
agents
|
|
2589
|
+
agents,
|
|
2590
|
+
daemon
|
|
2462
2591
|
});
|
|
2463
2592
|
return;
|
|
2464
2593
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2594
|
+
const machineName = daemon?.portal.machineName || identity.machineName;
|
|
2595
|
+
if (devices.length > 0) {
|
|
2596
|
+
const { headers, rows } = buildDeviceTable(devices, agents, machineName);
|
|
2597
|
+
printTable(headers, rows);
|
|
2468
2598
|
}
|
|
2469
|
-
const
|
|
2470
|
-
|
|
2599
|
+
const hint = portalStatusHint({
|
|
2600
|
+
daemon,
|
|
2601
|
+
thisDevice: findThisDevice(devices, machineName)
|
|
2602
|
+
});
|
|
2603
|
+
if (!hint) return;
|
|
2604
|
+
if (devices.length > 0) console.log("");
|
|
2605
|
+
console.log(hint);
|
|
2471
2606
|
}
|
|
2472
2607
|
};
|
|
2473
2608
|
/**
|
|
2609
|
+
* The remedy line under `portal status`, decided by cross-checking the LOCAL
|
|
2610
|
+
* daemon against the server's view of this machine.
|
|
2611
|
+
*
|
|
2612
|
+
* `portal open` is the right answer only when nothing is running here. When a
|
|
2613
|
+
* daemon IS running, the interesting case is a daemon that still believes it is
|
|
2614
|
+
* connected while the server has long since reaped its presence: a socket whose
|
|
2615
|
+
* route died without a FIN/RST (a mid-connection network switch is the usual
|
|
2616
|
+
* cause) stays `ESTABLISHED` forever, so the daemon never redials. Opening
|
|
2617
|
+
* another portal cannot help — the daemon has to be restarted.
|
|
2618
|
+
*
|
|
2619
|
+
* Returns null when there is nothing useful to add.
|
|
2620
|
+
*/
|
|
2621
|
+
function portalStatusHint({ daemon, thisDevice }) {
|
|
2622
|
+
if (!daemon) {
|
|
2623
|
+
if (!thisDevice) return "No machines registered. Run `skydive portal open` to register this one.";
|
|
2624
|
+
if (thisDevice.connected) return null;
|
|
2625
|
+
return "No portal daemon is running here. Run `skydive portal open` to connect this machine.";
|
|
2626
|
+
}
|
|
2627
|
+
const { status, error } = daemon.portal;
|
|
2628
|
+
switch (status) {
|
|
2629
|
+
case "connected":
|
|
2630
|
+
if (!thisDevice) return "Portal daemon is connected but this machine is not in the list yet. Re-run in a moment.";
|
|
2631
|
+
if (thisDevice.connected) return null;
|
|
2632
|
+
return "The connection is stale. Run `skydive portal daemon restart`.";
|
|
2633
|
+
case "error": return `Portal daemon is running (pid ${daemon.pid}) but its connection failed${error ? `: ${error}` : ""}. Run \`skydive portal daemon restart\` to redial.`;
|
|
2634
|
+
case "connecting": return `Portal daemon is running (pid ${daemon.pid}) and is still connecting. Re-run \`skydive portal status\` in a moment.`;
|
|
2635
|
+
case "off": return `Portal daemon is running (pid ${daemon.pid}) but its portal is off. Run \`skydive portal open\` to share this machine.`;
|
|
2636
|
+
default: return status;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2474
2640
|
* Build the `portal status` table. Grant ids are shown as agent names when the
|
|
2475
2641
|
* org roster resolves them (deleted agents fall back to the raw id), and the
|
|
2476
2642
|
* device this CLI would register as is marked so "which row is me" doesn't
|
|
@@ -2610,18 +2776,22 @@ const forwardCommand = {
|
|
|
2610
2776
|
}),
|
|
2611
2777
|
handler: async (argv) => {
|
|
2612
2778
|
const session = requireSession(argv);
|
|
2613
|
-
const agent = resolveAgent((await fetchPortalDevices(session)).agents, argv.agent ?? null);
|
|
2779
|
+
const agent = resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent ?? null);
|
|
2614
2780
|
const targetPort = argv.port;
|
|
2615
2781
|
const localPort = argv["local-port"] ?? targetPort;
|
|
2616
|
-
const { startForward } = await import("./forward-
|
|
2782
|
+
const { startForward } = await import("./forward-C-f04uyE.mjs");
|
|
2783
|
+
const { defaultTlsCertSource } = await import("./tls-cert-BpCaD5AT.mjs");
|
|
2784
|
+
const log = (msg) => console.error(msg);
|
|
2617
2785
|
const listener = await startForward({
|
|
2618
2786
|
auth: session,
|
|
2619
2787
|
agentId: agent.id,
|
|
2620
2788
|
localPort,
|
|
2621
2789
|
targetPort,
|
|
2622
|
-
|
|
2790
|
+
tlsCertSource: defaultTlsCertSource(process.env, log),
|
|
2791
|
+
log
|
|
2623
2792
|
});
|
|
2624
2793
|
console.log(`Forwarding http://localhost:${listener.port} -> ${agent.name}'s sandbox port ${targetPort}. ctrl+c to stop.`);
|
|
2794
|
+
if (listener.tls) console.log(` https://${listener.tls.hostname}:${listener.tls.port} (TLS — browsers negotiate HTTP/2 here, which loads module-heavy dev servers much faster)`);
|
|
2625
2795
|
await new Promise((resolve) => {
|
|
2626
2796
|
process.once("SIGINT", () => resolve());
|
|
2627
2797
|
process.once("SIGTERM", () => resolve());
|
|
@@ -2639,7 +2809,7 @@ const portalCommand = {
|
|
|
2639
2809
|
//#endregion
|
|
2640
2810
|
//#region src/commands/sandbox.ts
|
|
2641
2811
|
/** POSIX single-quote one word so the remote shell treats it as one token. */
|
|
2642
|
-
function shellQuote
|
|
2812
|
+
function shellQuote(word) {
|
|
2643
2813
|
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(word)) return word;
|
|
2644
2814
|
return `'${word.replace(/'/g, `'\\''`)}'`;
|
|
2645
2815
|
}
|
|
@@ -2654,7 +2824,7 @@ function shellQuote$1(word) {
|
|
|
2654
2824
|
* for too. yargs number-coerces bare numerals, so words are stringified.
|
|
2655
2825
|
*/
|
|
2656
2826
|
function joinCommandWords(words) {
|
|
2657
|
-
return words.map(String).map(shellQuote
|
|
2827
|
+
return words.map(String).map(shellQuote).join(" ").trim();
|
|
2658
2828
|
}
|
|
2659
2829
|
/**
|
|
2660
2830
|
* `skydive sandbox` — the standalone counterpart to the chat TUI's `/sandbox`
|
|
@@ -2677,8 +2847,8 @@ const sandboxCommand = {
|
|
|
2677
2847
|
}).example("skydive sandbox --agent grace", "Live terminal (Ctrl-] detaches)").example("skydive sandbox --agent grace -- tail -n 50 /tmp/harness.log", "One-shot command (use `--` so its flags reach the sandbox)").example("skydive sandbox --agent grace -- sh -c 'ls /tmp | wc -l'", "Shell features go through an explicit `sh -c`"),
|
|
2678
2848
|
handler: async (argv) => {
|
|
2679
2849
|
const session = requireSession(argv);
|
|
2680
|
-
const { createRestClient } = await import("./rest-
|
|
2681
|
-
const { resolveAgent } = await import("./print-
|
|
2850
|
+
const { createRestClient } = await import("./rest-Dc0EEok3.mjs");
|
|
2851
|
+
const { resolveAgent } = await import("./print-Bx8qUC9U.mjs");
|
|
2682
2852
|
const client = createRestClient({
|
|
2683
2853
|
appUrl: session.appUrl,
|
|
2684
2854
|
sessionToken: session.sessionToken
|
|
@@ -2747,7 +2917,7 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2747
2917
|
return 1;
|
|
2748
2918
|
}
|
|
2749
2919
|
console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
|
|
2750
|
-
const { runRawPtyPassthrough } = await import("./raw-pty-
|
|
2920
|
+
const { runRawPtyPassthrough } = await import("./raw-pty-Ci2qFR9F.mjs");
|
|
2751
2921
|
const result = await runRawPtyPassthrough({
|
|
2752
2922
|
stdin: process.stdin,
|
|
2753
2923
|
stdout: process.stdout,
|
|
@@ -2759,64 +2929,178 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2759
2929
|
return result.code;
|
|
2760
2930
|
}
|
|
2761
2931
|
|
|
2932
|
+
//#endregion
|
|
2933
|
+
//#region src/chat/sandbox/fs-client.ts
|
|
2934
|
+
function wsBase(appUrl) {
|
|
2935
|
+
const base = appUrl.replace(/\/+$/, "");
|
|
2936
|
+
if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
|
|
2937
|
+
if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
|
|
2938
|
+
return `wss://${base}`;
|
|
2939
|
+
}
|
|
2940
|
+
function toU8(data) {
|
|
2941
|
+
if (Buffer.isBuffer(data)) return new Uint8Array(data);
|
|
2942
|
+
if (Array.isArray(data)) return new Uint8Array(Buffer.concat(data));
|
|
2943
|
+
return new Uint8Array(Buffer.from(data));
|
|
2944
|
+
}
|
|
2945
|
+
var SandboxFsClient = class SandboxFsClient {
|
|
2946
|
+
ws;
|
|
2947
|
+
ready;
|
|
2948
|
+
nextReqId = 1;
|
|
2949
|
+
pending = /* @__PURE__ */ new Map();
|
|
2950
|
+
closedReason = null;
|
|
2951
|
+
constructor(opts) {
|
|
2952
|
+
const spec = {
|
|
2953
|
+
mode: "fs",
|
|
2954
|
+
agentId: opts.agentId
|
|
2955
|
+
};
|
|
2956
|
+
const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
|
|
2957
|
+
for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
|
|
2958
|
+
this.ws = new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } });
|
|
2959
|
+
this.ws.binaryType = "nodebuffer";
|
|
2960
|
+
this.ready = new Promise((resolve, reject) => {
|
|
2961
|
+
this.ws.once("open", () => resolve());
|
|
2962
|
+
this.ws.once("error", (err) => reject(err));
|
|
2963
|
+
});
|
|
2964
|
+
this.ws.on("message", (data, isBinary) => {
|
|
2965
|
+
if (!isBinary) return;
|
|
2966
|
+
const res = decodeFsResponse(toU8(data));
|
|
2967
|
+
if (!res) return;
|
|
2968
|
+
const p = this.pending.get(res.reqId);
|
|
2969
|
+
if (!p) return;
|
|
2970
|
+
this.pending.delete(res.reqId);
|
|
2971
|
+
if (res.status === "error") p.reject(new Error(res.message));
|
|
2972
|
+
else p.resolve({
|
|
2973
|
+
result: res.result,
|
|
2974
|
+
blob: res.blob
|
|
2975
|
+
});
|
|
2976
|
+
});
|
|
2977
|
+
const failAll = (reason) => {
|
|
2978
|
+
this.closedReason = reason;
|
|
2979
|
+
for (const [, p] of this.pending) p.reject(new Error(reason));
|
|
2980
|
+
this.pending.clear();
|
|
2981
|
+
};
|
|
2982
|
+
this.ws.on("error", (err) => failAll(err.message));
|
|
2983
|
+
this.ws.on("close", () => failAll(this.closedReason ?? "the sandbox filesystem connection closed"));
|
|
2984
|
+
}
|
|
2985
|
+
/** Open and wait for the channel to be ready to accept requests. */
|
|
2986
|
+
static async connect(opts) {
|
|
2987
|
+
const client = new SandboxFsClient(opts);
|
|
2988
|
+
await client.ready;
|
|
2989
|
+
return client;
|
|
2990
|
+
}
|
|
2991
|
+
request(req, blob) {
|
|
2992
|
+
if (this.closedReason) return Promise.reject(new Error(this.closedReason));
|
|
2993
|
+
const reqId = this.nextReqId++;
|
|
2994
|
+
return new Promise((resolve, reject) => {
|
|
2995
|
+
this.pending.set(reqId, {
|
|
2996
|
+
resolve,
|
|
2997
|
+
reject
|
|
2998
|
+
});
|
|
2999
|
+
try {
|
|
3000
|
+
this.ws.send(encodeFsRequest(reqId, req, blob));
|
|
3001
|
+
} catch (error) {
|
|
3002
|
+
this.pending.delete(reqId);
|
|
3003
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
3004
|
+
}
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
async list(path) {
|
|
3008
|
+
const { result } = await this.request({
|
|
3009
|
+
op: "list",
|
|
3010
|
+
path
|
|
3011
|
+
}, null);
|
|
3012
|
+
const entries = result["entries"];
|
|
3013
|
+
if (!Array.isArray(entries)) return [];
|
|
3014
|
+
return entries;
|
|
3015
|
+
}
|
|
3016
|
+
async stat(path) {
|
|
3017
|
+
const { result } = await this.request({
|
|
3018
|
+
op: "stat",
|
|
3019
|
+
path
|
|
3020
|
+
}, null);
|
|
3021
|
+
return result["stat"];
|
|
3022
|
+
}
|
|
3023
|
+
async read(path, range) {
|
|
3024
|
+
const req = range ? {
|
|
3025
|
+
op: "read",
|
|
3026
|
+
path,
|
|
3027
|
+
offset: range.offset,
|
|
3028
|
+
length: range.length
|
|
3029
|
+
} : {
|
|
3030
|
+
op: "read",
|
|
3031
|
+
path
|
|
3032
|
+
};
|
|
3033
|
+
const { blob } = await this.request(req, null);
|
|
3034
|
+
return blob;
|
|
3035
|
+
}
|
|
3036
|
+
async write(path, data) {
|
|
3037
|
+
await this.request({
|
|
3038
|
+
op: "write",
|
|
3039
|
+
path
|
|
3040
|
+
}, data);
|
|
3041
|
+
}
|
|
3042
|
+
async mkdir(path) {
|
|
3043
|
+
await this.request({
|
|
3044
|
+
op: "mkdir",
|
|
3045
|
+
path
|
|
3046
|
+
}, null);
|
|
3047
|
+
}
|
|
3048
|
+
async rename(oldPath, newPath) {
|
|
3049
|
+
await this.request({
|
|
3050
|
+
op: "rename",
|
|
3051
|
+
oldPath,
|
|
3052
|
+
newPath
|
|
3053
|
+
}, null);
|
|
3054
|
+
}
|
|
3055
|
+
async remove(path) {
|
|
3056
|
+
await this.request({
|
|
3057
|
+
op: "remove",
|
|
3058
|
+
path
|
|
3059
|
+
}, null);
|
|
3060
|
+
}
|
|
3061
|
+
async exists(path) {
|
|
3062
|
+
const { result } = await this.request({
|
|
3063
|
+
op: "exists",
|
|
3064
|
+
path
|
|
3065
|
+
}, null);
|
|
3066
|
+
return result["exists"] === true;
|
|
3067
|
+
}
|
|
3068
|
+
close() {
|
|
3069
|
+
this.closedReason ??= "closed by caller";
|
|
3070
|
+
this.ws.close();
|
|
3071
|
+
}
|
|
3072
|
+
};
|
|
3073
|
+
|
|
2762
3074
|
//#endregion
|
|
2763
3075
|
//#region src/commands/fs.ts
|
|
2764
3076
|
/**
|
|
2765
|
-
* POSIX-quote one word so the remote shell treats it as a single token. Wraps
|
|
2766
|
-
* `shell-quote`'s `quote` (already a repo dependency, used elsewhere in the
|
|
2767
|
-
* CLI) rather than hand-rolling the escaping — it is the well-tested source of
|
|
2768
|
-
* truth for making a path/arg with spaces, quotes, `$`, `;`, globs, etc. inert
|
|
2769
|
-
* when interpolated into an `sh -c` line.
|
|
2770
|
-
*/
|
|
2771
|
-
function shellQuote(word) {
|
|
2772
|
-
return quote([word]);
|
|
2773
|
-
}
|
|
2774
|
-
const MISSING_FILE_EXIT = 44;
|
|
2775
|
-
/**
|
|
2776
|
-
* Build the remote shell command that base64-encodes a file to stdout. Exits
|
|
2777
|
-
* {@link MISSING_FILE_EXIT} when the path doesn't exist, 1 when it's a
|
|
2778
|
-
* directory, so the caller can tell "create new" from a genuine error.
|
|
2779
|
-
*/
|
|
2780
|
-
function buildReadCommand(path) {
|
|
2781
|
-
const q = shellQuote(path);
|
|
2782
|
-
return `sh -c ${shellQuote(`if [ ! -e ${q} ]; then exit ${MISSING_FILE_EXIT}; fi; if [ -d ${q} ]; then echo 'path is a directory' >&2; exit 1; fi; base64 < ${q}`)}`;
|
|
2783
|
-
}
|
|
2784
|
-
/**
|
|
2785
|
-
* Build the remote shell command that decodes `b64` and writes it to `path`
|
|
2786
|
-
* atomically (temp file + `mv`) so a dropped connection can't truncate the
|
|
2787
|
-
* original in place. `token` is a caller-supplied unique suffix for the temp
|
|
2788
|
-
* file — generated in JS (not the shell's `$$`, which shell-quote would escape
|
|
2789
|
-
* to a literal, so every write would collide on the same temp name).
|
|
2790
|
-
*/
|
|
2791
|
-
function buildWriteCommand(path, b64, token) {
|
|
2792
|
-
const tmp = `${path}.skydive-edit.${token}`;
|
|
2793
|
-
return `sh -c ${shellQuote(`printf %s ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)} && mv -f ${shellQuote(tmp)} ${shellQuote(path)}`)}`;
|
|
2794
|
-
}
|
|
2795
|
-
/**
|
|
2796
3077
|
* Resolve the editor command from an explicit override, then $VISUAL, $EDITOR,
|
|
2797
|
-
* then
|
|
3078
|
+
* then a platform default. Split so `EDITOR="code --wait"` (command + flags) is
|
|
3079
|
+
* honored. The default is `notepad` on Windows (where `vi` does not exist and
|
|
3080
|
+
* would ENOENT) and `vi` everywhere else.
|
|
2798
3081
|
*/
|
|
2799
|
-
function resolveEditor(override, env) {
|
|
2800
|
-
const
|
|
3082
|
+
function resolveEditor(override, env, platform = process.platform) {
|
|
3083
|
+
const fallback = platform === "win32" ? "notepad" : "vi";
|
|
3084
|
+
const parts = (override || env.VISUAL || env.EDITOR || fallback).split(/\s+/).filter(Boolean);
|
|
2801
3085
|
return {
|
|
2802
|
-
bin: parts[0] ??
|
|
3086
|
+
bin: parts[0] ?? fallback,
|
|
2803
3087
|
args: parts.slice(1)
|
|
2804
3088
|
};
|
|
2805
3089
|
}
|
|
2806
3090
|
/**
|
|
2807
3091
|
* `skydive fs edit <path>` — edit a file that lives on the agent's computer
|
|
2808
3092
|
* (its sandbox) with your LOCAL editor. It reads the remote file down over the
|
|
2809
|
-
*
|
|
2810
|
-
* changes back when you save and close — so you edit a remote file as if
|
|
2811
|
-
* were local, without opening the chat TUI.
|
|
3093
|
+
* multiplexed `fs` sandbox-stream channel, opens it in `$EDITOR`, and writes
|
|
3094
|
+
* your changes back when you save and close — so you edit a remote file as if
|
|
3095
|
+
* it were local, without opening the chat TUI.
|
|
2812
3096
|
*
|
|
2813
|
-
*
|
|
2814
|
-
*
|
|
2815
|
-
*
|
|
2816
|
-
* embedded
|
|
2817
|
-
*
|
|
2818
|
-
*
|
|
2819
|
-
*
|
|
3097
|
+
* It rides the first-class `fs` relay channel ({@link SandboxFsClient}): file
|
|
3098
|
+
* bytes move as the frame's trailing binary blob in BOTH directions, so binary
|
|
3099
|
+
* content, trailing newlines, and CRLF are preserved and — unlike the old
|
|
3100
|
+
* exec-mode path that embedded a base64 payload in the upgrade URL — a large
|
|
3101
|
+
* file no longer blows past the WebSocket URL / request-header length limit.
|
|
3102
|
+
* Access gating (EDIT on the agent + the `sandbox-fs-enabled` kill switch) is
|
|
3103
|
+
* enforced by the relay, exactly as for `skydive sandbox`.
|
|
2820
3104
|
*/
|
|
2821
3105
|
const fsEditCommand = {
|
|
2822
3106
|
command: "edit <path>",
|
|
@@ -2835,8 +3119,8 @@ const fsEditCommand = {
|
|
|
2835
3119
|
handler: async (argv) => {
|
|
2836
3120
|
const session = requireSession(argv);
|
|
2837
3121
|
const remotePath = argv.path;
|
|
2838
|
-
const { createRestClient } = await import("./rest-
|
|
2839
|
-
const { resolveAgent } = await import("./print-
|
|
3122
|
+
const { createRestClient } = await import("./rest-Dc0EEok3.mjs");
|
|
3123
|
+
const { resolveAgent } = await import("./print-Bx8qUC9U.mjs");
|
|
2840
3124
|
const client = createRestClient({
|
|
2841
3125
|
appUrl: session.appUrl,
|
|
2842
3126
|
sessionToken: session.sessionToken
|
|
@@ -2851,150 +3135,66 @@ const fsEditCommand = {
|
|
|
2851
3135
|
printError(error instanceof Error ? error.message : String(error));
|
|
2852
3136
|
process.exit(1);
|
|
2853
3137
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
if (read.kind === "error") {
|
|
2864
|
-
printError(read.message);
|
|
3138
|
+
let fs;
|
|
3139
|
+
try {
|
|
3140
|
+
fs = await SandboxFsClient.connect({
|
|
3141
|
+
appUrl: session.appUrl,
|
|
3142
|
+
sessionToken: session.sessionToken,
|
|
3143
|
+
agentId: agent.id
|
|
3144
|
+
});
|
|
3145
|
+
} catch (error) {
|
|
3146
|
+
printError(`Could not open the sandbox filesystem channel: ${error instanceof Error ? error.message : String(error)}`);
|
|
2865
3147
|
process.exit(1);
|
|
2866
3148
|
}
|
|
2867
|
-
const
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
3149
|
+
const cleanup = async (dir) => {
|
|
3150
|
+
await rm(dir, {
|
|
3151
|
+
recursive: true,
|
|
3152
|
+
force: true
|
|
3153
|
+
}).catch((error) => {
|
|
3154
|
+
console.error(`Note: could not remove temp dir ${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3155
|
+
});
|
|
3156
|
+
};
|
|
3157
|
+
let local = null;
|
|
3158
|
+
let dir = null;
|
|
2871
3159
|
try {
|
|
3160
|
+
console.error(`Reading ${remotePath} from ${agent.name}'s sandbox…`);
|
|
3161
|
+
const isNew = !await fs.exists(remotePath);
|
|
3162
|
+
const original = isNew ? Buffer.alloc(0) : Buffer.from(await fs.read(remotePath, null));
|
|
3163
|
+
if (isNew) console.error(`${remotePath} does not exist yet — it will be created when you save.`);
|
|
3164
|
+
dir = await mkdtemp(join(tmpdir(), "skydive-edit-"));
|
|
3165
|
+
const base = remotePath.split("/").pop() || "file";
|
|
3166
|
+
local = join(dir, base);
|
|
2872
3167
|
await writeFile(local, original);
|
|
2873
3168
|
if (!await openEditor(argv.editor, local)) {
|
|
2874
3169
|
printError("Editor exited non-zero; not saving any changes.");
|
|
3170
|
+
fs.close();
|
|
3171
|
+
await cleanup(dir);
|
|
2875
3172
|
process.exit(1);
|
|
2876
3173
|
}
|
|
2877
3174
|
const edited = await readFile(local);
|
|
2878
3175
|
if (edited.equals(original)) {
|
|
2879
3176
|
console.error("No changes — nothing written.");
|
|
3177
|
+
fs.close();
|
|
3178
|
+
await cleanup(dir);
|
|
2880
3179
|
process.exit(0);
|
|
2881
3180
|
}
|
|
2882
3181
|
console.error(`Writing changes back to ${remotePath}…`);
|
|
2883
|
-
|
|
2884
|
-
...opts,
|
|
2885
|
-
path: remotePath,
|
|
2886
|
-
content: edited
|
|
2887
|
-
});
|
|
2888
|
-
if (wrote.kind === "error") {
|
|
2889
|
-
printError(`${wrote.message}\nYour edited copy is kept at ${local} so you don't lose it.`);
|
|
2890
|
-
process.exit(1);
|
|
2891
|
-
}
|
|
3182
|
+
await fs.write(remotePath, new Uint8Array(edited));
|
|
2892
3183
|
console.error("Saved.");
|
|
3184
|
+
fs.close();
|
|
3185
|
+
await cleanup(dir);
|
|
2893
3186
|
process.exit(0);
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
}).catch((error) => {
|
|
2899
|
-
console.error(`Note: could not remove temp dir ${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2900
|
-
});
|
|
3187
|
+
} catch (error) {
|
|
3188
|
+
fs.close();
|
|
3189
|
+
printError(`${error instanceof Error ? error.message : String(error)}${local ? `\nYour edited copy is kept at ${local} so you don't lose it.` : ""}`);
|
|
3190
|
+
process.exit(1);
|
|
2901
3191
|
}
|
|
2902
3192
|
}
|
|
2903
3193
|
};
|
|
2904
3194
|
/**
|
|
2905
|
-
* Read a remote file as bytes. Runs `base64` on the file in the sandbox and
|
|
2906
|
-
* decodes the streamed stdout locally. Distinguishes "file is missing" (exit
|
|
2907
|
-
* code 44, so `edit` can create it) from a real read failure.
|
|
2908
|
-
*/
|
|
2909
|
-
function readRemote({ session, agentId, path }) {
|
|
2910
|
-
const command = buildReadCommand(path);
|
|
2911
|
-
const chunks = [];
|
|
2912
|
-
const stderr = [];
|
|
2913
|
-
return new Promise((resolve) => {
|
|
2914
|
-
SandboxStream.open({
|
|
2915
|
-
mode: "exec",
|
|
2916
|
-
appUrl: session.appUrl,
|
|
2917
|
-
sessionToken: session.sessionToken,
|
|
2918
|
-
agentId,
|
|
2919
|
-
command,
|
|
2920
|
-
onEvent: (e) => {
|
|
2921
|
-
switch (e.type) {
|
|
2922
|
-
case "data":
|
|
2923
|
-
chunks.push(Buffer.from(e.bytes));
|
|
2924
|
-
break;
|
|
2925
|
-
case "exit":
|
|
2926
|
-
if (e.code === MISSING_FILE_EXIT) resolve({
|
|
2927
|
-
kind: "missing",
|
|
2928
|
-
content: Buffer.alloc(0)
|
|
2929
|
-
});
|
|
2930
|
-
else if (e.code === 0) resolve({
|
|
2931
|
-
kind: "ok",
|
|
2932
|
-
content: Buffer.from(Buffer.concat(chunks).toString("utf8").replace(/\s+/g, ""), "base64")
|
|
2933
|
-
});
|
|
2934
|
-
else resolve({
|
|
2935
|
-
kind: "error",
|
|
2936
|
-
message: stderr.join("").trim() || `Could not read the file (exit ${e.code}).`
|
|
2937
|
-
});
|
|
2938
|
-
break;
|
|
2939
|
-
case "error":
|
|
2940
|
-
stderr.push(e.message);
|
|
2941
|
-
break;
|
|
2942
|
-
case "close":
|
|
2943
|
-
resolve({
|
|
2944
|
-
kind: "error",
|
|
2945
|
-
message: e.failure || "The connection to the sandbox closed before the file could be read."
|
|
2946
|
-
});
|
|
2947
|
-
break;
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
});
|
|
2951
|
-
});
|
|
2952
|
-
}
|
|
2953
|
-
/**
|
|
2954
|
-
* Write bytes to a remote file. Exec has no stdin, so the base64 payload is
|
|
2955
|
-
* embedded in the command and decoded on the far side. Writes atomically via a
|
|
2956
|
-
* temp file + `mv` so a dropped connection mid-write can't leave a truncated
|
|
2957
|
-
* file in place of the original.
|
|
2958
|
-
*/
|
|
2959
|
-
function writeRemote({ session, agentId, path, content }) {
|
|
2960
|
-
const token = randomBytes(6).toString("hex");
|
|
2961
|
-
const command = buildWriteCommand(path, content.toString("base64"), token);
|
|
2962
|
-
const stderr = [];
|
|
2963
|
-
return new Promise((resolve) => {
|
|
2964
|
-
SandboxStream.open({
|
|
2965
|
-
mode: "exec",
|
|
2966
|
-
appUrl: session.appUrl,
|
|
2967
|
-
sessionToken: session.sessionToken,
|
|
2968
|
-
agentId,
|
|
2969
|
-
command,
|
|
2970
|
-
onEvent: (e) => {
|
|
2971
|
-
switch (e.type) {
|
|
2972
|
-
case "data":
|
|
2973
|
-
stderr.push(Buffer.from(e.bytes).toString("utf8"));
|
|
2974
|
-
break;
|
|
2975
|
-
case "exit":
|
|
2976
|
-
resolve(e.code === 0 ? { kind: "ok" } : {
|
|
2977
|
-
kind: "error",
|
|
2978
|
-
message: stderr.join("").trim() || `Could not write the file (exit ${e.code}).`
|
|
2979
|
-
});
|
|
2980
|
-
break;
|
|
2981
|
-
case "error":
|
|
2982
|
-
stderr.push(e.message);
|
|
2983
|
-
break;
|
|
2984
|
-
case "close":
|
|
2985
|
-
resolve({
|
|
2986
|
-
kind: "error",
|
|
2987
|
-
message: e.failure || "The connection to the sandbox closed before the file was written."
|
|
2988
|
-
});
|
|
2989
|
-
break;
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
});
|
|
2993
|
-
});
|
|
2994
|
-
}
|
|
2995
|
-
/**
|
|
2996
3195
|
* Open `file` in the caller's editor and resolve to whether it exited cleanly.
|
|
2997
|
-
* Precedence: explicit `--editor`, then $VISUAL, $EDITOR, then
|
|
3196
|
+
* Precedence: explicit `--editor`, then $VISUAL, $EDITOR, then the platform
|
|
3197
|
+
* default (`notepad` on Windows, `vi` elsewhere). The editor
|
|
2998
3198
|
* inherits the real TTY so full-screen editors (vim/nano) work.
|
|
2999
3199
|
*/
|
|
3000
3200
|
function openEditor(override, file) {
|
|
@@ -3015,7 +3215,7 @@ function openEditor(override, file) {
|
|
|
3015
3215
|
*
|
|
3016
3216
|
* - `fs edit <path>` edit one remote file with your local editor (shipped).
|
|
3017
3217
|
* - `fs mount <dir>` mount the whole sandbox filesystem locally (coming in a
|
|
3018
|
-
* later PR, on
|
|
3218
|
+
* later PR, on the same first-class `fs` relay channel).
|
|
3019
3219
|
*
|
|
3020
3220
|
* Grouping these under one command keeps the file surface discoverable and
|
|
3021
3221
|
* leaves room to grow (`fs ls`, `fs cat`, `fs pull`) without adding more
|
|
@@ -3303,7 +3503,27 @@ function resolveSelfPath(execPath = process.execPath) {
|
|
|
3303
3503
|
|
|
3304
3504
|
//#endregion
|
|
3305
3505
|
//#region src/update-check/sources.ts
|
|
3506
|
+
/**
|
|
3507
|
+
* How this CLI was installed, which decides whether `skydive update` (and the
|
|
3508
|
+
* background update check) self-updates the binary from the release CDN or
|
|
3509
|
+
* hands off to the package manager.
|
|
3510
|
+
*
|
|
3511
|
+
* Two signals, runtime env first:
|
|
3512
|
+
* - `SKYDIVE_CLI_INSTALL_SOURCE` env var — set by the npm launcher
|
|
3513
|
+
* (src/launcher.ts) to `package-manager` before it execs the compiled
|
|
3514
|
+
* binary. The binary itself is `--define`'d to `binary`, but when it was
|
|
3515
|
+
* launched via the npm-installed launcher, npm owns its bytes and must not
|
|
3516
|
+
* be self-mutated — so the env override wins.
|
|
3517
|
+
* - The `SKYDIVE_CLI_INSTALL_SOURCE` compile-time constant — stamped
|
|
3518
|
+
* `binary` into the standalone binary, undefined in the npm JS bundle.
|
|
3519
|
+
*
|
|
3520
|
+
* Any value other than exactly `binary` (including unset) means
|
|
3521
|
+
* package-manager, the safe default: we never self-update something we can't
|
|
3522
|
+
* prove is a standalone binary.
|
|
3523
|
+
*/
|
|
3306
3524
|
function resolveInstallSource() {
|
|
3525
|
+
const runtime = typeof process !== "undefined" ? process.env?.["SKYDIVE_CLI_INSTALL_SOURCE"] : void 0;
|
|
3526
|
+
if (runtime !== void 0) return runtime === "binary" ? "binary" : "package-manager";
|
|
3307
3527
|
return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
|
|
3308
3528
|
}
|
|
3309
3529
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
@@ -3717,6 +3937,11 @@ function resolvePreference(key) {
|
|
|
3717
3937
|
}
|
|
3718
3938
|
return pref;
|
|
3719
3939
|
}
|
|
3940
|
+
/** Human rendering of a preference value; unset string preferences (null)
|
|
3941
|
+
* print as empty rather than the string "null". */
|
|
3942
|
+
function formatValue(value) {
|
|
3943
|
+
return value === null ? "" : String(value);
|
|
3944
|
+
}
|
|
3720
3945
|
/** One row's worth of state for a preference, used by `list` and `get`. */
|
|
3721
3946
|
function snapshot(pref) {
|
|
3722
3947
|
return {
|
|
@@ -3726,7 +3951,7 @@ function snapshot(pref) {
|
|
|
3726
3951
|
describe: pref.describe
|
|
3727
3952
|
};
|
|
3728
3953
|
}
|
|
3729
|
-
const listCommand = {
|
|
3954
|
+
const listCommand$1 = {
|
|
3730
3955
|
command: "list",
|
|
3731
3956
|
aliases: ["ls"],
|
|
3732
3957
|
describe: "Show every editable preference, its value, and its source",
|
|
@@ -3743,7 +3968,7 @@ const listCommand = {
|
|
|
3743
3968
|
"Description"
|
|
3744
3969
|
], rows.map((r) => [
|
|
3745
3970
|
r.key,
|
|
3746
|
-
|
|
3971
|
+
formatValue(r.value),
|
|
3747
3972
|
r.source,
|
|
3748
3973
|
r.describe
|
|
3749
3974
|
]));
|
|
@@ -3764,7 +3989,7 @@ const getCommand = {
|
|
|
3764
3989
|
output(argv, snapshot(pref));
|
|
3765
3990
|
return;
|
|
3766
3991
|
}
|
|
3767
|
-
console.log(
|
|
3992
|
+
console.log(formatValue(pref.read()));
|
|
3768
3993
|
}
|
|
3769
3994
|
};
|
|
3770
3995
|
const setCommand = {
|
|
@@ -3782,17 +4007,16 @@ const setCommand = {
|
|
|
3782
4007
|
}),
|
|
3783
4008
|
handler: (argv) => {
|
|
3784
4009
|
const pref = resolvePreference(argv.key);
|
|
3785
|
-
const
|
|
3786
|
-
if (
|
|
3787
|
-
printError(`${pref.key}: ${
|
|
4010
|
+
const result = pref.set(argv.value ?? "");
|
|
4011
|
+
if (result.isErr()) {
|
|
4012
|
+
printError(`${pref.key}: ${result.error}`);
|
|
3788
4013
|
process.exit(1);
|
|
3789
4014
|
}
|
|
3790
|
-
pref.write(parsed.value);
|
|
3791
4015
|
if (argv.json) {
|
|
3792
4016
|
output(argv, snapshot(pref));
|
|
3793
4017
|
return;
|
|
3794
4018
|
}
|
|
3795
|
-
if (!argv.quiet) console.log(`Set ${pref.key} = ${
|
|
4019
|
+
if (!argv.quiet) console.log(`Set ${pref.key} = ${formatValue(pref.read())}`);
|
|
3796
4020
|
}
|
|
3797
4021
|
};
|
|
3798
4022
|
const unsetCommand = {
|
|
@@ -3812,10 +4036,13 @@ const unsetCommand = {
|
|
|
3812
4036
|
output(argv, snapshot(pref));
|
|
3813
4037
|
return;
|
|
3814
4038
|
}
|
|
3815
|
-
if (!argv.quiet)
|
|
4039
|
+
if (!argv.quiet) {
|
|
4040
|
+
const fallback = pref.read();
|
|
4041
|
+
console.log(fallback === null ? `Unset ${pref.key}` : `Unset ${pref.key} (now ${formatValue(fallback)} by default)`);
|
|
4042
|
+
}
|
|
3816
4043
|
}
|
|
3817
4044
|
};
|
|
3818
|
-
const pathCommand = {
|
|
4045
|
+
const pathCommand$1 = {
|
|
3819
4046
|
command: "path",
|
|
3820
4047
|
describe: "Print the on-disk config file path",
|
|
3821
4048
|
handler: (argv) => {
|
|
@@ -3829,14 +4056,154 @@ const pathCommand = {
|
|
|
3829
4056
|
const configCommand = {
|
|
3830
4057
|
command: "config",
|
|
3831
4058
|
describe: "View and set CLI preferences (safe, hand-editable keys only)",
|
|
3832
|
-
builder: (y) => y.command(listCommand).command(getCommand).command(setCommand).command(unsetCommand).command(pathCommand),
|
|
4059
|
+
builder: (y) => y.command(listCommand$1).command(getCommand).command(setCommand).command(unsetCommand).command(pathCommand$1),
|
|
4060
|
+
handler: (argv) => listCommand$1.handler(argv)
|
|
4061
|
+
};
|
|
4062
|
+
|
|
4063
|
+
//#endregion
|
|
4064
|
+
//#region src/commands/logs.ts
|
|
4065
|
+
const ISSUE_REPOSITORY_URL = "https://github.com/Create-Inc/skydive-cli";
|
|
4066
|
+
function issueUrl(archivePath) {
|
|
4067
|
+
const url = new URL(`${ISSUE_REPOSITORY_URL}/issues/new`);
|
|
4068
|
+
url.searchParams.set("title", "CLI issue: ");
|
|
4069
|
+
url.searchParams.set("body", [
|
|
4070
|
+
"## What happened?",
|
|
4071
|
+
"",
|
|
4072
|
+
"<!-- Tell us what you were doing and what went wrong. -->",
|
|
4073
|
+
"",
|
|
4074
|
+
"## Diagnostics",
|
|
4075
|
+
"",
|
|
4076
|
+
`Please attach \`${path.basename(archivePath)}\` to this issue.`
|
|
4077
|
+
].join("\n"));
|
|
4078
|
+
return url.toString();
|
|
4079
|
+
}
|
|
4080
|
+
async function listLogSessions(activeSessionPath = profileDir()) {
|
|
4081
|
+
const root = getAutomaticLogsDir();
|
|
4082
|
+
try {
|
|
4083
|
+
const entries = await fs.promises.readdir(root, { withFileTypes: true });
|
|
4084
|
+
return (await Promise.all(entries.filter((entry) => entry.isDirectory() && path.join(root, entry.name) !== activeSessionPath).map(async (entry) => {
|
|
4085
|
+
const sessionPath = path.join(root, entry.name);
|
|
4086
|
+
return {
|
|
4087
|
+
name: entry.name,
|
|
4088
|
+
path: sessionPath,
|
|
4089
|
+
modifiedAt: (await fs.promises.stat(sessionPath)).mtime.toISOString()
|
|
4090
|
+
};
|
|
4091
|
+
}))).sort((a, b) => b.name.localeCompare(a.name));
|
|
4092
|
+
} catch (_error) {
|
|
4093
|
+
return [];
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
async function resolveSession(name) {
|
|
4097
|
+
const sessions = await listLogSessions();
|
|
4098
|
+
const session = name ? sessions.find((candidate) => candidate.name === name) : sessions[0];
|
|
4099
|
+
if (session) return session;
|
|
4100
|
+
printError(name ? `Log session "${name}" was not found. Run \`skydive logs list\`.` : "No diagnostic logs found yet.");
|
|
4101
|
+
process.exit(1);
|
|
4102
|
+
}
|
|
4103
|
+
async function offerIssueReport(archivePath) {
|
|
4104
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4105
|
+
console.log(`Report an issue: ${ISSUE_REPOSITORY_URL}`);
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
const prompt = createInterface$1({
|
|
4109
|
+
input: process.stdin,
|
|
4110
|
+
output: process.stdout
|
|
4111
|
+
});
|
|
4112
|
+
try {
|
|
4113
|
+
if ((await prompt.question("Open a prefilled GitHub issue in your browser? [Y/n] ")).trim().toLowerCase() === "n") {
|
|
4114
|
+
console.log(`File an issue: ${ISSUE_REPOSITORY_URL}`);
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
await open(issueUrl(archivePath));
|
|
4118
|
+
console.log(`Attach the diagnostics archive: ${archivePath}`);
|
|
4119
|
+
} finally {
|
|
4120
|
+
prompt.close();
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
async function createArchive(source, destination) {
|
|
4124
|
+
await tar.create({
|
|
4125
|
+
gzip: true,
|
|
4126
|
+
file: destination,
|
|
4127
|
+
cwd: path.dirname(source)
|
|
4128
|
+
}, [path.basename(source)]);
|
|
4129
|
+
}
|
|
4130
|
+
const listCommand = {
|
|
4131
|
+
command: "list",
|
|
4132
|
+
aliases: ["ls"],
|
|
4133
|
+
describe: "List recent diagnostic sessions",
|
|
4134
|
+
handler: async (argv) => {
|
|
4135
|
+
const sessions = await listLogSessions();
|
|
4136
|
+
if (argv.json) {
|
|
4137
|
+
output(argv, sessions);
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
if (sessions.length === 0) {
|
|
4141
|
+
console.log("No diagnostic logs found yet.");
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
printTable(["Session", "Modified"], sessions.map((session) => [session.name, session.modifiedAt]));
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4147
|
+
const pathCommand = {
|
|
4148
|
+
command: "path [session]",
|
|
4149
|
+
describe: "Print a diagnostic session directory (latest by default)",
|
|
4150
|
+
builder: (y) => y.positional("session", {
|
|
4151
|
+
type: "string",
|
|
4152
|
+
describe: "Session name from `skydive logs list`"
|
|
4153
|
+
}),
|
|
4154
|
+
handler: async (argv) => {
|
|
4155
|
+
const session = await resolveSession(argv.session);
|
|
4156
|
+
if (argv.json) {
|
|
4157
|
+
output(argv, session);
|
|
4158
|
+
return;
|
|
4159
|
+
}
|
|
4160
|
+
console.log(session.path);
|
|
4161
|
+
}
|
|
4162
|
+
};
|
|
4163
|
+
const exportCommand = {
|
|
4164
|
+
command: "export [session]",
|
|
4165
|
+
describe: "Archive a diagnostic session for sharing (latest by default)",
|
|
4166
|
+
builder: (y) => y.positional("session", {
|
|
4167
|
+
type: "string",
|
|
4168
|
+
describe: "Session name from `skydive logs list`"
|
|
4169
|
+
}).option("output", {
|
|
4170
|
+
alias: "o",
|
|
4171
|
+
type: "string",
|
|
4172
|
+
describe: "Archive path (defaults to the current directory)"
|
|
4173
|
+
}),
|
|
4174
|
+
handler: async (argv) => {
|
|
4175
|
+
const session = await resolveSession(argv.session);
|
|
4176
|
+
const destination = path.resolve(argv.output ?? `skydive-logs-${session.name}.tar.gz`);
|
|
4177
|
+
try {
|
|
4178
|
+
await createArchive(session.path, destination);
|
|
4179
|
+
} catch (error) {
|
|
4180
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
4181
|
+
process.exit(1);
|
|
4182
|
+
}
|
|
4183
|
+
if (argv.json) {
|
|
4184
|
+
output(argv, {
|
|
4185
|
+
path: destination,
|
|
4186
|
+
session: session.name,
|
|
4187
|
+
issueUrl: issueUrl(destination),
|
|
4188
|
+
repositoryUrl: ISSUE_REPOSITORY_URL
|
|
4189
|
+
});
|
|
4190
|
+
return;
|
|
4191
|
+
}
|
|
4192
|
+
console.log(destination);
|
|
4193
|
+
await offerIssueReport(destination);
|
|
4194
|
+
}
|
|
4195
|
+
};
|
|
4196
|
+
const logsCommand = {
|
|
4197
|
+
command: "logs",
|
|
4198
|
+
describe: "Find and export recent local diagnostic logs",
|
|
4199
|
+
builder: (y) => y.command(listCommand).command(pathCommand).command(exportCommand),
|
|
3833
4200
|
handler: (argv) => listCommand.handler(argv)
|
|
3834
4201
|
};
|
|
3835
4202
|
|
|
3836
4203
|
//#endregion
|
|
3837
4204
|
//#region src/cli.ts
|
|
3838
4205
|
function createCli(argv) {
|
|
3839
|
-
return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").parserConfiguration({ "populate--": true }).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 completion install", "Set up TAB completion for your shell").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 config set updateCheck false", "Disable the daily update check").example("skydive workspace switch acme-team", "Switch the workspace all `skydive` commands act on").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").example("skydive fs edit --agent grace /tmp/notes.md", "Edit a file on the agent's computer with your local editor").option("json", {
|
|
4206
|
+
return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").parserConfiguration({ "populate--": true }).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 completion install", "Set up TAB completion for your shell").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 config set updateCheck false", "Disable the daily update check").example("skydive logs export", "Archive the latest local diagnostics for sharing").example("skydive workspace switch acme-team", "Switch the workspace all `skydive` commands act on").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").example("skydive fs edit --agent grace /tmp/notes.md", "Edit a file on the agent's computer with your local editor").option("json", {
|
|
3840
4207
|
type: "boolean",
|
|
3841
4208
|
default: false,
|
|
3842
4209
|
global: true,
|
|
@@ -3850,7 +4217,7 @@ function createCli(argv) {
|
|
|
3850
4217
|
type: "string",
|
|
3851
4218
|
global: true,
|
|
3852
4219
|
describe: "Override API base URL"
|
|
3853
|
-
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(importCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).command(fsCommand).command(updateCommand).command(configCommand).command(completionCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
4220
|
+
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(importCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).command(fsCommand).command(updateCommand).command(configCommand).command(logsCommand).command(completionCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
3854
4221
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
3855
4222
|
process.exit(1);
|
|
3856
4223
|
});
|
|
@@ -4063,7 +4430,7 @@ function setupUpdateCheck(argv) {
|
|
|
4063
4430
|
|
|
4064
4431
|
//#endregion
|
|
4065
4432
|
//#region src/changelog.generated.ts
|
|
4066
|
-
const CHANGELOG_MD = "# Changelog\n\nAll notable changes to the Skydive CLI are documented here.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Added\n\n- `skydive fs` — a namespace for working with the files on an agent's computer (its sandbox) from your own machine. Its first subcommand, `skydive fs edit <path>`, reads a remote file down over the same relay `skydive sandbox` uses, opens it in your local editor (`$VISUAL`/`$EDITOR`, override with `--editor`, defaults to `vi`), and writes your changes back atomically when you save and close — so you can edit a remote file as if it were local without opening the chat TUI. A path that doesn't exist yet is created on save; no changes means nothing is written. Access is gated on EDIT permission for the agent, exactly like `skydive sandbox`. (A first-class `fs` relay channel and `skydive fs mount` land in follow-ups.)\n\n## [0.4.0] - 2026-08-07\n\n### Added\n\n- `skydive chat` works with API key auth: an API key alone now opens the chat TUI (and headless `chat -p`) without a browser login, using the key's pinned workspace.\n- `skydive config` command to view and set the safe, hand-editable CLI preferences (`shareMachineDefault`, `updateCheck`) without opening `config.json`. `config`/`config list` shows each key, its effective value, and whether it comes from the file or the built-in default; `config get`/`set`/`unset` read and change one key; `config path` prints the config file location. It only ever touches those preference keys, never credentials or flow-managed state.\n- Billing-blocked sends and runs now surface the server-authored recovery\n guidance from the typed billing outcome. The chat TUI renders it as a\n warning-toned system-notice row — live when a send is refused or a run is\n stopped at the billing boundary, and again when reopening a conversation\n that holds a persisted billing notice — instead of a generic error.\n Headless `chat -p` and `messages get` print the guidance (state, recovery\n step, and manage-billing URL) on stderr, keep any partial output already\n streamed on stdout, and exit with code `5` (distinct from the generic `1`)\n so scripted callers can tell a billing pause from a failure.\n- Syntax highlighting for 14 more languages in the chat transcript and file/review panes (python, bash, go, rust, c, cpp, java, ruby, css, html, json, yaml, and more) — previously only JavaScript/TypeScript rendered colored.\n\n### Changed\n\n- `skydive agent list` lists the full agent roster by default instead of a truncated page.\n\n### Fixed\n\n- Long multi-line bash tool-call commands (e.g. heredocs) are clamped with a `+N more · click to expand` tail instead of dumping their whole body into the scrollback.\n- Tool error output is clipped the same way as ok output, so a failing command no longer floods the transcript.\n- The TUI toast overlay has an opaque background instead of letting the transcript bleed through.\n\n## [0.3.0] - 2026-08-04\n\n### Added\n\n**Chat TUI**\n\n- `/fork` slash command to fork a conversation at any point and continue in a new thread.\n- `/conversation` switches threads from inside a live session.\n- `/compact` triggers manual conversation compaction.\n- `/portal` and `/copy` slash commands.\n- Archive the current conversation from the chat TUI.\n- `--new` flag on `skydive chat` to start a fresh conversation.\n- Debounced server-side conversation search in the picker, with title matches ranked first.\n- File review pane orientation toggle, and review comments in the file pane.\n- Pinned plan card controls and a jump-to-latest hint.\n- Terminal title (OSC) follows the live conversation title on every screen; inside cmux the workspace is renamed too, and linked PRs are pushed to the cmux sidebar.\n- Every participating agent shows in the conversation list; connect cards each have their own identity, and ctrl+r asks which connect card to act on.\n- Colored Skydive splash art in `skydive --help` and the TUI pickers.\n- New-agent name prefilled from the server's suggestion.\n\n**CLI & platform**\n\n- `skydive update` self-updater, with an update notice when a new version is available.\n- Shell completions for bash, zsh, and fish via `completion install`.\n- Portal daemon: one connection per host with per-conversation working directories.\n- Agent-led coding-agent import (`skydive import`).\n- Per-workspace scoping on the REST client, and cross-org `--agent <uuid>` auto-switches workspace.\n- `skydive-beta` wrapper for the canary channel.\n\n### Fixed\n\n- `skydive update` no longer claims an update is available on every npm install.\n- Chat memory is bounded (payload clamps + windowed rows) with a memory watchdog and OOM diagnostic report; transcript rows are memoized so streaming stops re-rendering the whole chat.\n- Chat-send failures map to real error messages.\n- Notification and sidebar previews use the last text block of the reply and strip inline markdown.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n- Sending to non-web channel conversations is blocked in the TUI.\n- GitHub bot connect opens on the web front door, not api.skydive.com.\n- TUI activity counter times from the run, not the screen.\n\n## [0.2.0] - 2026-07-29\n\n### Added\n\n**Sandbox & machine access**\n\n- Standalone `skydive sandbox` command for direct access to an agent's sandbox.\n- `/sandbox` in the chat TUI — live PTY session and one-shot command execution.\n- Headless machine sharing via `skydive portal`, with self-registering portal grants.\n- `shareMachineDefault` config key for always-on portal sharing.\n\n**Chat TUI**\n\n- File pane (`ctrl+g`) with Changes and Files tabs: a shared file tree plus source viewer and a workspace browser, mouse-resizable and responsive to terminal size.\n- Agent todo list rendered in the chat TUI, above the composer, mirroring the web chat's todo card.\n- Fuzzy finder in the conversation and agent pickers.\n- Slash-command autocomplete menu.\n- Paste or drag-and-drop any file into the composer; paste clipboard images with Cmd+V.\n- Run local shell commands with `!` in the composer.\n- Conversation recaps, streamed live title updates, and per-agent attribution on assistant turns.\n- Working timer rolls up into minutes and hours.\n- Conversation picker paginates past 50 conversations and shows only your own conversations.\n- Esc leaves a live run; typing `exit` quits the chat.\n- Cursor Dark theme.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n\n**Headless & scripting**\n\n- Resume a conversation by id.\n- `conversations list` and `conversations show` for transcript reads.\n- `messages get`, with run recovery keyed on message id.\n- Connect cards surface in headless `-p` mode so a driving agent never gets stuck.\n\n**Authentication**\n\n- `skydive auth login` via the browser now auto-mints an API key, so one login yields both a chat session and a usable management credential. Management commands announce the key's pinned workspace when it drives them, so a workspace mismatch is visible at use time.\n- Workspace picker on the device authorization page.\n- Account and workspace identity shown in `auth status`.\n\n**Platform**\n\n- Standalone binary builds compiling the CLI into a per-target executable.\n- Interactive workspace switcher; management commands follow the active workspace.\n- Terminal host integrations and agent notifications.\n\n### Fixed\n\n- Transcript errors collapse to one line, click to expand (REST and portal errors keep their full body).\n- Dragged/pasted image file paths attach the file instead of inserting path text, including macOS paths with literal parentheses.\n- Bare URLs in chat markdown are hyperlinked so they survive text wrap.\n- Composer draft is preserved across TUI overlays.\n- Relative connect links resolve before opening the browser.\n- Numbered markdown headings render colored in the TUI.\n- Chat transcript pages by 75% of a screen; picker rows stay on one line.\n- Run starts push to the TUI over the conversation stream.\n- Security: remediated high-severity dependency findings and cleared tar/shell-quote CVEs.\n\n## [0.1.0] - 2026-07-21\n\nInitial public release: `skydive chat` TUI, agent and conversation management,\ndevice authorization, and headless `-p` mode.\n";
|
|
4433
|
+
const CHANGELOG_MD = "# Changelog\n\nAll notable changes to the Skydive CLI are documented here.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Changed\n\n- The chat TUI status bar now always shows where the portal stands, instead of only lighting up once everything already worked. It reads `portal off`, `portal connecting`, `portal on` (open on this machine, but this agent has no grant), `portal shared` (green — this agent can run commands here), `portal error`, or `portal unavailable` when you signed in with an API key and no portal is possible. Previously the badge rendered nothing in every one of those states, so \"why can't the agent reach my machine?\" had no answer on screen.\n- One word for that feature across the whole CLI: **portal**. The footer hint is now `ctrl+t portal` / `ctrl+t close portal` (was `local access` / `revoke access`), `/status` reports a `portal` row (was `local access`), and `/share` and `/portal` describe themselves as opening the portal and granting an agent access to your machine. `/portal` used to say \"access your computer\", which collided with `/computer` — that command still means the **agent's** sandbox, and \"computer\" stays reserved for it. `--share-machine` and the `shareMachineDefault` config key are unchanged.\n- `npm i -g skydive-cli` (and yarn/pnpm/bun) now installs a tiny launcher plus one prebuilt, os/cpu-matched binary that carries its own runtime — so on the common platforms `skydive chat` and `skydive import` start instantly and **do not download a Bun runtime on first use**. On any platform without a prebuilt binary (or if the optional dependency is skipped), the CLI automatically falls back to the existing behavior: it runs under your Node and provisions Bun on first `chat`, exactly as before. Nothing about the commands, flags, config, or `SKYDIVE_*` environment variables changes, and `skydive update` still upgrades npm installs via your package manager and standalone-binary installs in place.\n\n### Added\n\n- Files an agent shares into the conversation now show as a saveable row in the chat TUI. When the agent sends you a file (a report, an export, a screenshot), it appears as a `📎` row with the filename and size and a \"click to save\" hint; clicking it downloads the file to your `~/Downloads` (falling back to your home directory) and opens it with your default app, then the row shows where it landed. Clicking a saved row again re-opens it. Previously these files were only reachable in the web app — in the terminal they were invisible.\n- `platform ask` forms now render in the chat TUI. The card lists each question with lettered options (A/B/C/D plus Other); `ctrl+r` opens a full overlay picker (one question at a time, arrows + enter, Other as a typed row — the Claude Code interaction). The answer sends as a normal message. Headless `chat -p` prints the question as an `[action needed]` block instead of dropping it.\n- Every API request now reports this install's version (`x-skydive-cli-version`) so the server can refuse features the installed CLI cannot render — `platform ask` needs the #21411 canary (`0.5.0-beta.43` or newer), not 0.4.0 and not earlier 0.5.0 betas.\n- `/search` (alias `/find`) in the chat TUI — opens the conversation picker with its search field focused, so \"find a conversation\" has a discoverable command instead of being a feature you had to know the bare picker already had. Pass a query inline (`/search billing deep dive`) and the picker opens already narrowed to the matches; bare `/search` opens it ready for you to type. The search runs server-side across conversation titles, message bodies, and participant names, the same search the picker exposes.\n- First run in a workspace with no agents now sets you up automatically: instead of dropping you on an empty agent picker, `skydive` creates your first agent for you (with a suggested name and avatar, mirroring the web onboarding flow) and then offers to have it learn how you already work — reading the other coding agents set up on your machine (Claude Code, Cursor, Codex, and more), the same import that `skydive import` runs, with a plan shown first and credentials never copied. Decline and you go straight into the new agent's chat. You can replay this flow anytime with `skydive --onboarding` (or `SKYDIVE_FORCE_ONBOARDING=1`), even in a workspace that already has agents.\n- `/computer` in the chat TUI — a snapshot of the agent's sandbox drawn as colored bar graphs: CPU, memory, and disk usage. Bars turn amber past 75%% and red past 90%% so a glance tells you whether the box is under pressure. The numbers come from the Skydive API (the same live resource sample the web computer view reads), not by connecting to the sandbox — so the card shows instantly, never waking or hanging on a box that isn't running: when the agent has no live sandbox it simply reads \"no sandbox running\". The card keeps itself current quietly (a slow background refresh, no flicker or \"updating\" chrome); only the most recent card refreshes, and it silently stops while you're scrolled up (no wasted polls on a card you're not looking at) and resumes at the bottom.\n- `/computer restart` in the chat TUI — reboot the agent's sandbox from chat. It asks a `y`/`n` confirm first (the restart stops whatever run is going and brings up a fresh box), then enqueues the same restart the web \"Restart\" control does. Available to anyone with edit access to the agent; view-only members are told they can't.\n- `skydive fs` — a namespace for working with the files on an agent's computer (its sandbox) from your own machine. Its first subcommand, `skydive fs edit <path>`, reads a remote file down over the same relay `skydive sandbox` uses, opens it in your local editor (`$VISUAL`/`$EDITOR`, override with `--editor`, defaults to `vi`), and writes your changes back atomically when you save and close — so you can edit a remote file as if it were local without opening the chat TUI. A path that doesn't exist yet is created on save; no changes means nothing is written. Access is gated on EDIT permission for the agent, exactly like `skydive sandbox`. (A first-class `fs` relay channel and `skydive fs mount` land in follow-ups.)\n\n## [0.4.0] - 2026-08-07\n\n### Added\n\n- `skydive chat` works with API key auth: an API key alone now opens the chat TUI (and headless `chat -p`) without a browser login, using the key's pinned workspace.\n- `skydive config` command to view and set the safe, hand-editable CLI preferences (`shareMachineDefault`, `updateCheck`) without opening `config.json`. `config`/`config list` shows each key, its effective value, and whether it comes from the file or the built-in default; `config get`/`set`/`unset` read and change one key; `config path` prints the config file location. It only ever touches those preference keys, never credentials or flow-managed state.\n- Billing-blocked sends and runs now surface the server-authored recovery\n guidance from the typed billing outcome. The chat TUI renders it as a\n warning-toned system-notice row — live when a send is refused or a run is\n stopped at the billing boundary, and again when reopening a conversation\n that holds a persisted billing notice — instead of a generic error.\n Headless `chat -p` and `messages get` print the guidance (state, recovery\n step, and manage-billing URL) on stderr, keep any partial output already\n streamed on stdout, and exit with code `5` (distinct from the generic `1`)\n so scripted callers can tell a billing pause from a failure.\n- Syntax highlighting for 14 more languages in the chat transcript and file/review panes (python, bash, go, rust, c, cpp, java, ruby, css, html, json, yaml, and more) — previously only JavaScript/TypeScript rendered colored.\n\n### Changed\n\n- `skydive agent list` lists the full agent roster by default instead of a truncated page.\n\n### Fixed\n\n- Long multi-line bash tool-call commands (e.g. heredocs) are clamped with a `+N more · click to expand` tail instead of dumping their whole body into the scrollback.\n- Tool error output is clipped the same way as ok output, so a failing command no longer floods the transcript.\n- The TUI toast overlay has an opaque background instead of letting the transcript bleed through.\n\n## [0.3.0] - 2026-08-04\n\n### Added\n\n**Chat TUI**\n\n- `/fork` slash command to fork a conversation at any point and continue in a new thread.\n- `/conversation` switches threads from inside a live session.\n- `/compact` triggers manual conversation compaction.\n- `/portal` and `/copy` slash commands.\n- Archive the current conversation from the chat TUI.\n- `--new` flag on `skydive chat` to start a fresh conversation.\n- Debounced server-side conversation search in the picker, with title matches ranked first.\n- File review pane orientation toggle, and review comments in the file pane.\n- Pinned plan card controls and a jump-to-latest hint.\n- Terminal title (OSC) follows the live conversation title on every screen; inside cmux the workspace is renamed too, and linked PRs are pushed to the cmux sidebar.\n- Every participating agent shows in the conversation list; connect cards each have their own identity, and ctrl+r asks which connect card to act on.\n- Colored Skydive splash art in `skydive --help` and the TUI pickers.\n- New-agent name prefilled from the server's suggestion.\n\n**CLI & platform**\n\n- `skydive update` self-updater, with an update notice when a new version is available.\n- Shell completions for bash, zsh, and fish via `completion install`.\n- Portal daemon: one connection per host with per-conversation working directories.\n- Agent-led coding-agent import (`skydive import`).\n- Per-workspace scoping on the REST client, and cross-org `--agent <uuid>` auto-switches workspace.\n- `skydive-beta` wrapper for the canary channel.\n\n### Fixed\n\n- `skydive update` no longer claims an update is available on every npm install.\n- Chat memory is bounded (payload clamps + windowed rows) with a memory watchdog and OOM diagnostic report; transcript rows are memoized so streaming stops re-rendering the whole chat.\n- Chat-send failures map to real error messages.\n- Notification and sidebar previews use the last text block of the reply and strip inline markdown.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n- Sending to non-web channel conversations is blocked in the TUI.\n- GitHub bot connect opens on the web front door, not api.skydive.com.\n- TUI activity counter times from the run, not the screen.\n\n## [0.2.0] - 2026-07-29\n\n### Added\n\n**Sandbox & machine access**\n\n- Standalone `skydive sandbox` command for direct access to an agent's sandbox.\n- `/sandbox` in the chat TUI — live PTY session and one-shot command execution.\n- Headless machine sharing via `skydive portal`, with self-registering portal grants.\n- `shareMachineDefault` config key for always-on portal sharing.\n\n**Chat TUI**\n\n- File pane (`ctrl+g`) with Changes and Files tabs: a shared file tree plus source viewer and a workspace browser, mouse-resizable and responsive to terminal size.\n- Agent todo list rendered in the chat TUI, above the composer, mirroring the web chat's todo card.\n- Fuzzy finder in the conversation and agent pickers.\n- Slash-command autocomplete menu.\n- Paste or drag-and-drop any file into the composer; paste clipboard images with Cmd+V.\n- Run local shell commands with `!` in the composer.\n- Conversation recaps, streamed live title updates, and per-agent attribution on assistant turns.\n- Working timer rolls up into minutes and hours.\n- Conversation picker paginates past 50 conversations and shows only your own conversations.\n- Esc leaves a live run; typing `exit` quits the chat.\n- Cursor Dark theme.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n\n**Headless & scripting**\n\n- Resume a conversation by id.\n- `conversations list` and `conversations show` for transcript reads.\n- `messages get`, with run recovery keyed on message id.\n- Connect cards surface in headless `-p` mode so a driving agent never gets stuck.\n\n**Authentication**\n\n- `skydive auth login` via the browser now auto-mints an API key, so one login yields both a chat session and a usable management credential. Management commands announce the key's pinned workspace when it drives them, so a workspace mismatch is visible at use time.\n- Workspace picker on the device authorization page.\n- Account and workspace identity shown in `auth status`.\n\n**Platform**\n\n- Standalone binary builds compiling the CLI into a per-target executable.\n- Interactive workspace switcher; management commands follow the active workspace.\n- Terminal host integrations and agent notifications.\n\n### Fixed\n\n- Transcript errors collapse to one line, click to expand (REST and portal errors keep their full body).\n- Dragged/pasted image file paths attach the file instead of inserting path text, including macOS paths with literal parentheses.\n- Bare URLs in chat markdown are hyperlinked so they survive text wrap.\n- Composer draft is preserved across TUI overlays.\n- Relative connect links resolve before opening the browser.\n- Numbered markdown headings render colored in the TUI.\n- Chat transcript pages by 75% of a screen; picker rows stay on one line.\n- Run starts push to the TUI over the conversation stream.\n- Security: remediated high-severity dependency findings and cleared tar/shell-quote CVEs.\n\n## [0.1.0] - 2026-07-21\n\nInitial public release: `skydive chat` TUI, agent and conversation management,\ndevice authorization, and headless `-p` mode.\n";
|
|
4067
4434
|
|
|
4068
4435
|
//#endregion
|
|
4069
4436
|
//#region src/whats-new.ts
|
|
@@ -4206,7 +4573,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
|
|
|
4206
4573
|
process.exit(0);
|
|
4207
4574
|
}
|
|
4208
4575
|
if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
|
|
4209
|
-
const { runPortalDaemon } = await import("./daemon-
|
|
4576
|
+
const { runPortalDaemon } = await import("./daemon-CCgNLD0H.mjs");
|
|
4210
4577
|
runPortalDaemon(process.argv);
|
|
4211
4578
|
} else runCli();
|
|
4212
4579
|
function runCli() {
|