skydive-cli 0.5.0-beta.4 → 0.5.0-beta.44
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 +10 -0
- package/README.md +35 -4
- package/dist/js/api-TwLD7ibI.mjs +315 -0
- package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
- package/dist/js/bin.mjs +658 -279
- package/dist/js/{boot-B_93T51X.mjs → boot-ClKjP9Mn.mjs} +3263 -755
- package/dist/js/chunk-BbwQpWto.mjs +33 -0
- package/dist/js/{client-Dd5sMXPv.mjs → client-BnpgrtCC.mjs} +348 -141
- package/dist/js/{client-Cn2af31H.mjs → client-Btq6bMzX.mjs} +108 -2
- package/dist/js/client-Cga4PJUc.mjs +5 -0
- package/dist/js/daemon-client-D5YCk5Hc.mjs +8 -0
- package/dist/js/{daemon-client-DPjmvIDE.mjs → daemon-client-KKNss0NK.mjs} +12 -8
- package/dist/js/daemon-mLypjXdu.mjs +7 -0
- package/dist/js/{daemon-BYlIJN6x.mjs → daemon-w8vtZ81t.mjs} +43 -7
- package/dist/js/dist-CRtjM7ba.mjs +1750 -0
- package/dist/js/forward-Ct8Vd2WW.mjs +208 -0
- package/dist/js/{profiler-RmWjKIHD.mjs → install-DrtNldUN.mjs} +529 -215
- package/dist/js/launcher.mjs +49 -0
- package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
- package/dist/js/{print-ba_0hiV9.mjs → print-Dc-xRapT.mjs} +3 -3
- package/dist/js/{print-CbayCa87.mjs → print-Ds16-Q4O.mjs} +289 -37
- package/dist/js/{print-share-uwUG16Ov.mjs → print-share-Df6eAG12.mjs} +8 -2
- package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
- package/dist/js/{raw-pty-B6mAroiI.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
- package/dist/js/rest-Do3mPVpC.mjs +6 -0
- package/dist/js/{rest-BY2nADw5.mjs → rest-DpsJUYTL.mjs} +117 -28
- package/dist/js/tls-cert-CLgSQALB.mjs +4 -0
- package/dist/js/tls-cert-CV-pwxVN.mjs +67 -0
- package/package.json +11 -4
- package/dist/js/client-DZstLQ_1.mjs +0 -4
- package/dist/js/daemon-CBvFKDpy.mjs +0 -5
- package/dist/js/daemon-client-Z4Ce4zf-.mjs +0 -6
- package/dist/js/raw-pty-3EkG-jjH.mjs +0 -5
- package/dist/js/rest-DADJh0bi.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,28 +1,31 @@
|
|
|
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 { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-
|
|
10
|
-
import
|
|
2
|
+
import { o as name, s as version, t as createRestClient } from "./rest-DpsJUYTL.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-Ds16-Q4O.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-DrtNldUN.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 { r as resolveMachineIdentity } from "./client-BnpgrtCC.mjs";
|
|
9
|
+
import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-w8vtZ81t.mjs";
|
|
10
|
+
import "./tls-cert-CV-pwxVN.mjs";
|
|
11
|
+
import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-TwLD7ibI.mjs";
|
|
12
|
+
import { a as encodeFsRequest, i as decodeFsResponse, n as SANDBOX_STREAM_PATH, r as streamSpecToQuery, t as SandboxStream } from "./client-Btq6bMzX.mjs";
|
|
11
13
|
import { hideBin } from "yargs/helpers";
|
|
12
14
|
import yargs from "yargs";
|
|
13
|
-
import
|
|
15
|
+
import { hostname, tmpdir } from "node:os";
|
|
14
16
|
import path, { join } from "node:path";
|
|
15
17
|
import { err, ok } from "neverthrow";
|
|
16
18
|
import { z } from "zod";
|
|
17
19
|
import open from "open";
|
|
18
20
|
import fs from "node:fs";
|
|
19
21
|
import { spawn, spawnSync } from "node:child_process";
|
|
20
|
-
import { createHash
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
21
23
|
import zlib from "node:zlib";
|
|
22
24
|
import fsp, { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
23
|
-
import {
|
|
25
|
+
import { WebSocket } from "ws";
|
|
24
26
|
import semver from "semver";
|
|
25
27
|
import { createInterface } from "node:readline";
|
|
28
|
+
import { createInterface as createInterface$1 } from "node:readline/promises";
|
|
26
29
|
|
|
27
30
|
//#region src/types.ts
|
|
28
31
|
const NON_INTERACTIVE_ENV_VARS = [
|
|
@@ -38,6 +41,7 @@ function isNonInteractive() {
|
|
|
38
41
|
//#endregion
|
|
39
42
|
//#region src/api-client.ts
|
|
40
43
|
const USER_AGENT = `skydive-cli/${version}`;
|
|
44
|
+
const CLI_VERSION_HEADER = "x-skydive-cli-version";
|
|
41
45
|
/** Largest page `/v1/agents` will return, enforced server-side. */
|
|
42
46
|
const MAX_AGENT_PAGE = 100;
|
|
43
47
|
const AgentSchema = z.object({
|
|
@@ -69,10 +73,12 @@ var SkydiveApiClient = class {
|
|
|
69
73
|
baseUrl;
|
|
70
74
|
token;
|
|
71
75
|
authKind;
|
|
76
|
+
workspaceId;
|
|
72
77
|
constructor(config) {
|
|
73
78
|
this.baseUrl = `${config.apiUrl.replace(/\/$/, "")}/v1`;
|
|
74
79
|
this.token = config.token;
|
|
75
80
|
this.authKind = config.kind;
|
|
81
|
+
this.workspaceId = config.workspaceId;
|
|
76
82
|
}
|
|
77
83
|
/**
|
|
78
84
|
* List one page of agents in the caller's workspace. The server caps a
|
|
@@ -156,8 +162,10 @@ var SkydiveApiClient = class {
|
|
|
156
162
|
const url = `${this.baseUrl}${params.path}`;
|
|
157
163
|
const headers = {
|
|
158
164
|
authorization: `Bearer ${this.token}`,
|
|
159
|
-
"user-agent": USER_AGENT
|
|
165
|
+
"user-agent": USER_AGENT,
|
|
166
|
+
[CLI_VERSION_HEADER]: version
|
|
160
167
|
};
|
|
168
|
+
if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
|
|
161
169
|
if (params.body) headers["content-type"] = "application/json";
|
|
162
170
|
let response;
|
|
163
171
|
try {
|
|
@@ -615,7 +623,7 @@ const logoutCommand = {
|
|
|
615
623
|
const keyId = getStoredApiKeyId();
|
|
616
624
|
let keyRevoked = false;
|
|
617
625
|
if (keyId) {
|
|
618
|
-
const session = resolveSession({ appUrl: argv["api-url"] });
|
|
626
|
+
const session = resolveSession$1({ appUrl: argv["api-url"] });
|
|
619
627
|
const config = resolveConfig({ apiUrl: argv["api-url"] });
|
|
620
628
|
const credential = session.isOk() ? {
|
|
621
629
|
appUrl: session.value.appUrl,
|
|
@@ -646,7 +654,7 @@ const statusCommand$1 = {
|
|
|
646
654
|
describe: "Show current authentication state",
|
|
647
655
|
handler: async (argv) => {
|
|
648
656
|
const apiKey = resolveConfig({ apiUrl: argv["api-url"] });
|
|
649
|
-
const session = resolveSession({});
|
|
657
|
+
const session = resolveSession$1({});
|
|
650
658
|
const management = resolveManagementAuth({ apiUrl: argv["api-url"] });
|
|
651
659
|
const identity = session.isOk() ? (await getSessionIdentity({
|
|
652
660
|
appUrl: session.value.appUrl,
|
|
@@ -717,11 +725,6 @@ const authCommand = {
|
|
|
717
725
|
* matches on it to keep notices off the user's prompt (see isCompletionProbe).
|
|
718
726
|
*/
|
|
719
727
|
const COMPLETION_PROBE_FLAG = "--get-yargs-completions";
|
|
720
|
-
const SUPPORTED_SHELLS = [
|
|
721
|
-
"bash",
|
|
722
|
-
"zsh",
|
|
723
|
-
"fish"
|
|
724
|
-
];
|
|
725
728
|
/**
|
|
726
729
|
* True when this invocation is a shell asking for completion candidates
|
|
727
730
|
* rather than a user running a command. Such a run must print candidates and
|
|
@@ -893,24 +896,6 @@ complete -c ${name} -f -k -a '(${fn})'
|
|
|
893
896
|
${blockEnd(name)}
|
|
894
897
|
`;
|
|
895
898
|
}
|
|
896
|
-
const defaultInstallEnv = () => ({
|
|
897
|
-
home: os.homedir(),
|
|
898
|
-
platform: process.platform,
|
|
899
|
-
env: process.env,
|
|
900
|
-
exists: fs.existsSync
|
|
901
|
-
});
|
|
902
|
-
/**
|
|
903
|
-
* The shell to install for when the user didn't name one. `$SHELL` is the
|
|
904
|
-
* only signal available: a completion install runs as a child process, so the
|
|
905
|
-
* invoking shell's own variables (`$ZSH_VERSION`, `$FISH_VERSION`) aren't
|
|
906
|
-
* visible here.
|
|
907
|
-
*/
|
|
908
|
-
function detectShell(env = process.env) {
|
|
909
|
-
const shell = env["SHELL"];
|
|
910
|
-
if (!shell) return null;
|
|
911
|
-
const name = path.basename(shell);
|
|
912
|
-
return SUPPORTED_SHELLS.find((candidate) => name === candidate) ?? null;
|
|
913
|
-
}
|
|
914
899
|
function completionTarget(shell, deps = defaultInstallEnv()) {
|
|
915
900
|
const { home, platform, env, exists } = deps;
|
|
916
901
|
if (shell === "fish") {
|
|
@@ -1019,7 +1004,7 @@ const completionCommand = {
|
|
|
1019
1004
|
* resolve-or-exit block isn't copy-pasted per command.
|
|
1020
1005
|
*/
|
|
1021
1006
|
function requireSession(argv) {
|
|
1022
|
-
const session = resolveSession({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
1007
|
+
const session = resolveSession$1({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
1023
1008
|
if (session.isErr()) {
|
|
1024
1009
|
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1025
1010
|
process.exit(1);
|
|
@@ -1031,7 +1016,7 @@ function requireSession(argv) {
|
|
|
1031
1016
|
* {@link resolveManagementAuth}) or exit with a friendly hint, then build the
|
|
1032
1017
|
* `/v1` client. Shared by `agents`, `keys` and `secrets`.
|
|
1033
1018
|
*/
|
|
1034
|
-
function requireManagementClient(argv) {
|
|
1019
|
+
function requireManagementClient(argv, opts = {}) {
|
|
1035
1020
|
const auth = resolveManagementAuth({ apiUrl: argv["api-url"] });
|
|
1036
1021
|
if (auth.isErr()) {
|
|
1037
1022
|
printError(auth.error.message);
|
|
@@ -1039,7 +1024,10 @@ function requireManagementClient(argv) {
|
|
|
1039
1024
|
}
|
|
1040
1025
|
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.
|
|
1041
1026
|
`);
|
|
1042
|
-
return new SkydiveApiClient(
|
|
1027
|
+
return new SkydiveApiClient({
|
|
1028
|
+
...auth.value,
|
|
1029
|
+
...opts.workspaceId ? { workspaceId: opts.workspaceId } : {}
|
|
1030
|
+
});
|
|
1043
1031
|
}
|
|
1044
1032
|
/** Resolve the session (see {@link requireSession}) and build a REST client. */
|
|
1045
1033
|
function requireRestClient(argv) {
|
|
@@ -1056,7 +1044,32 @@ function requireRestClient(argv) {
|
|
|
1056
1044
|
|
|
1057
1045
|
//#endregion
|
|
1058
1046
|
//#region src/commands/agents.ts
|
|
1059
|
-
const
|
|
1047
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1048
|
+
/**
|
|
1049
|
+
* Resolve an agent from an id or a name. A uuid selector hits `GET
|
|
1050
|
+
* /agents/:id` directly; anything else is matched case-insensitively against
|
|
1051
|
+
* the roster's names. Ambiguity is an error rather than a silent pick, so an
|
|
1052
|
+
* alias never lands on the wrong agent.
|
|
1053
|
+
*/
|
|
1054
|
+
async function resolveAgent(client, selector) {
|
|
1055
|
+
if (UUID_RE.test(selector)) {
|
|
1056
|
+
const result = await client.getAgent(selector);
|
|
1057
|
+
if (result.isErr()) throw new Error(result.error.message);
|
|
1058
|
+
return result.value;
|
|
1059
|
+
}
|
|
1060
|
+
const list = await client.listAgents({
|
|
1061
|
+
scope: "org",
|
|
1062
|
+
limit: 100
|
|
1063
|
+
});
|
|
1064
|
+
if (list.isErr()) throw new Error(list.error.message);
|
|
1065
|
+
const needle = selector.trim().toLowerCase();
|
|
1066
|
+
const matches = list.value.agents.filter((a) => a.name.toLowerCase() === needle);
|
|
1067
|
+
const [first] = matches;
|
|
1068
|
+
if (matches.length === 1 && first) return first;
|
|
1069
|
+
if (matches.length === 0) throw new Error(`No agent named "${selector}". Pass an id, or run \`skydive agents list\`.`);
|
|
1070
|
+
throw new Error(`Multiple agents named "${selector}". Pass the id instead:\n${matches.map((a) => ` ${a.name} ${a.id}`).join("\n")}`);
|
|
1071
|
+
}
|
|
1072
|
+
const listCommand$6 = {
|
|
1060
1073
|
command: "list",
|
|
1061
1074
|
describe: "List agents in the active workspace",
|
|
1062
1075
|
builder: (y) => y.option("limit", {
|
|
@@ -1205,10 +1218,82 @@ const createCommand$1 = {
|
|
|
1205
1218
|
if (agent.url) console.log(` API: ${agent.url}`);
|
|
1206
1219
|
}
|
|
1207
1220
|
};
|
|
1221
|
+
/**
|
|
1222
|
+
* Resolve the `--workspace` selector (slug/name/id) to a workspace, using the
|
|
1223
|
+
* chat/session credential (workspace listing is a session endpoint). Exits
|
|
1224
|
+
* with a clear message on a bad selector or when listing isn't available
|
|
1225
|
+
* (e.g. a pinned API key). Returns null when the flag is absent.
|
|
1226
|
+
*/
|
|
1227
|
+
async function resolveAliasWorkspace(argv) {
|
|
1228
|
+
if (!argv.workspace) return null;
|
|
1229
|
+
const auth = resolveChatAuth({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
1230
|
+
if (auth.isErr()) {
|
|
1231
|
+
printError(`${auth.error.message} Run \`skydive auth login\` to resolve a workspace by name.`);
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1234
|
+
const resolved = await resolveWorkspaceId({
|
|
1235
|
+
appUrl: auth.value.appUrl,
|
|
1236
|
+
sessionToken: auth.value.token,
|
|
1237
|
+
selector: argv.workspace
|
|
1238
|
+
});
|
|
1239
|
+
if (resolved.isErr()) {
|
|
1240
|
+
printError(resolved.error.message);
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
return resolved.value;
|
|
1244
|
+
}
|
|
1245
|
+
const aliasCommand = {
|
|
1246
|
+
command: "alias <agent> [name]",
|
|
1247
|
+
describe: "Add a shell alias that opens an agent's chat",
|
|
1248
|
+
builder: (y) => y.positional("agent", {
|
|
1249
|
+
type: "string",
|
|
1250
|
+
demandOption: true,
|
|
1251
|
+
describe: "Agent id or name to alias"
|
|
1252
|
+
}).positional("name", {
|
|
1253
|
+
type: "string",
|
|
1254
|
+
describe: "Alias to use (default: derived from the agent name)"
|
|
1255
|
+
}).option("shell", {
|
|
1256
|
+
type: "string",
|
|
1257
|
+
choices: SUPPORTED_SHELLS,
|
|
1258
|
+
describe: "Shell to install for (default: $SHELL)"
|
|
1259
|
+
}).option("workspace", {
|
|
1260
|
+
type: "string",
|
|
1261
|
+
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."
|
|
1262
|
+
}).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"),
|
|
1263
|
+
handler: async (argv) => {
|
|
1264
|
+
const shell = argv.shell ? SUPPORTED_SHELLS.find((s) => s === argv.shell) : detectShell();
|
|
1265
|
+
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(", ")}).`);
|
|
1266
|
+
const workspace = await resolveAliasWorkspace(argv);
|
|
1267
|
+
const agent = await resolveAgent(requireManagementClient(argv, { workspaceId: workspace?.id }), argv.agent);
|
|
1268
|
+
const preferred = argv.name ? slugifyAliasName(argv.name) : slugifyAliasName(agent.name);
|
|
1269
|
+
const alias = dedupeAliasName(preferred, takenAliasNames(shell));
|
|
1270
|
+
const result = installAgentAlias({
|
|
1271
|
+
alias,
|
|
1272
|
+
agentId: agent.id,
|
|
1273
|
+
shell,
|
|
1274
|
+
...workspace ? { workspaceId: workspace.id } : {}
|
|
1275
|
+
}, void 0);
|
|
1276
|
+
if (argv.json) {
|
|
1277
|
+
output(argv, {
|
|
1278
|
+
...result,
|
|
1279
|
+
workspace: workspace ?? null
|
|
1280
|
+
});
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (argv.quiet) {
|
|
1284
|
+
console.log(result.alias);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const where = workspace ? ` (workspace ${workspace.name})` : "";
|
|
1288
|
+
console.log(`${result.updated ? "Updated" : "Added"} alias \`${result.alias}\` -> ${agent.name}${where} in ${result.path}`);
|
|
1289
|
+
if (alias !== preferred) console.log(`(\`${preferred}\` was taken, so used \`${alias}\`.)`);
|
|
1290
|
+
console.log(aliasActivationHint(result));
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1208
1293
|
const agentsCommand = {
|
|
1209
1294
|
command: "agents",
|
|
1210
1295
|
describe: "Manage agents",
|
|
1211
|
-
builder: (y) => y.command(listCommand$
|
|
1296
|
+
builder: (y) => y.command(listCommand$6).command(getCommand$2).command(createCommand$1).command(aliasCommand).demandCommand(1, "Specify a subcommand: list, get, create, alias"),
|
|
1212
1297
|
handler: () => {}
|
|
1213
1298
|
};
|
|
1214
1299
|
|
|
@@ -1462,19 +1547,6 @@ function ensureBunAndReexec(onProgress) {
|
|
|
1462
1547
|
|
|
1463
1548
|
//#endregion
|
|
1464
1549
|
//#region src/commands/import.ts
|
|
1465
|
-
/**
|
|
1466
|
-
* The first message of the import conversation, sent as the user. It hands
|
|
1467
|
-
* the real work to the agent's `import-config` skill; this text only needs
|
|
1468
|
-
* to establish intent, anchor the starting directory, and set the two
|
|
1469
|
-
* expectations the user cares about most (plan first, no credentials).
|
|
1470
|
-
*/
|
|
1471
|
-
function buildImportSeedPrompt(projectDir) {
|
|
1472
|
-
return [
|
|
1473
|
-
"I'm migrating from another coding agent. Import my setup from this machine.",
|
|
1474
|
-
"",
|
|
1475
|
-
`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.`
|
|
1476
|
-
].join("\n");
|
|
1477
|
-
}
|
|
1478
1550
|
const importCommand = {
|
|
1479
1551
|
command: "import",
|
|
1480
1552
|
describe: "Import your existing coding-agent setup (Claude Code, Cursor, Codex, Gemini, Copilot, and more) into an agent (opens a machine-shared chat)",
|
|
@@ -1503,30 +1575,33 @@ const importCommand = {
|
|
|
1503
1575
|
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).`);
|
|
1504
1576
|
process.exit(1);
|
|
1505
1577
|
}
|
|
1506
|
-
let session = resolveSession({ appUrl });
|
|
1578
|
+
let session = resolveSession$1({ appUrl });
|
|
1507
1579
|
if (session.isErr()) {
|
|
1508
1580
|
const login = await loginWithDevice({ appUrl });
|
|
1509
1581
|
if (login.isErr()) {
|
|
1510
1582
|
printError(login.error.message);
|
|
1511
1583
|
process.exit(1);
|
|
1512
1584
|
}
|
|
1513
|
-
session = resolveSession({ appUrl });
|
|
1585
|
+
session = resolveSession$1({ appUrl });
|
|
1514
1586
|
if (session.isErr()) {
|
|
1515
1587
|
printError("Signed in, but no session was stored.");
|
|
1516
1588
|
process.exit(1);
|
|
1517
1589
|
}
|
|
1518
1590
|
}
|
|
1519
|
-
const { runChat } = await import("./boot-
|
|
1591
|
+
const { runChat } = await import("./boot-ClKjP9Mn.mjs");
|
|
1520
1592
|
await runChat({
|
|
1521
1593
|
appUrl,
|
|
1522
1594
|
sessionToken: session.value.sessionToken,
|
|
1595
|
+
workspaceId: void 0,
|
|
1523
1596
|
shareMachine: true,
|
|
1524
1597
|
promptHistoryPath: getPromptHistoryPath(),
|
|
1525
1598
|
notifications: true,
|
|
1526
1599
|
agentSelector: argv.agent ?? null,
|
|
1527
1600
|
conversationId: null,
|
|
1528
1601
|
newConversation: false,
|
|
1529
|
-
|
|
1602
|
+
defaultAgentSelector: getDefaultAgent(),
|
|
1603
|
+
forceOnboarding: false,
|
|
1604
|
+
seedPrompt: buildImportSeedPrompt(process.cwd(), machineOsFromPlatform(process.platform))
|
|
1530
1605
|
});
|
|
1531
1606
|
}
|
|
1532
1607
|
};
|
|
@@ -1538,20 +1613,20 @@ const importCommand = {
|
|
|
1538
1613
|
* (`chat -p "go ahead" --resume <id> --share-machine`) to apply it.
|
|
1539
1614
|
*/
|
|
1540
1615
|
async function runImportPrintMode({ argv, appUrl }) {
|
|
1541
|
-
const session = resolveSession({ appUrl });
|
|
1616
|
+
const session = resolveSession$1({ appUrl });
|
|
1542
1617
|
if (session.isErr()) {
|
|
1543
1618
|
printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1544
1619
|
process.exit(1);
|
|
1545
1620
|
}
|
|
1546
|
-
const { connectMachineShare } = await import("./print-share-
|
|
1621
|
+
const { connectMachineShare } = await import("./print-share-Df6eAG12.mjs");
|
|
1547
1622
|
const machineShare = await connectMachineShare({
|
|
1548
1623
|
appUrl,
|
|
1549
1624
|
sessionToken: session.value.sessionToken,
|
|
1550
1625
|
timeoutHint: "Check `skydive portal status`, then re-run."
|
|
1551
1626
|
});
|
|
1552
1627
|
const extra = (argv.print ?? "").trim();
|
|
1553
|
-
const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
|
|
1554
|
-
const { runPrint } = await import("./print-
|
|
1628
|
+
const prompt = buildImportSeedPrompt(process.cwd(), machineOsFromPlatform(process.platform)) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
|
|
1629
|
+
const { runPrint } = await import("./print-Dc-xRapT.mjs");
|
|
1555
1630
|
try {
|
|
1556
1631
|
const result = await runPrint({
|
|
1557
1632
|
appUrl,
|
|
@@ -1575,7 +1650,7 @@ async function runImportPrintMode({ argv, appUrl }) {
|
|
|
1575
1650
|
|
|
1576
1651
|
//#endregion
|
|
1577
1652
|
//#region src/commands/keys.ts
|
|
1578
|
-
const listCommand$
|
|
1653
|
+
const listCommand$5 = {
|
|
1579
1654
|
command: "list",
|
|
1580
1655
|
describe: "List API keys for an agent",
|
|
1581
1656
|
handler: async (argv) => {
|
|
@@ -1664,7 +1739,7 @@ const keysCommand = {
|
|
|
1664
1739
|
type: "string",
|
|
1665
1740
|
demandOption: true,
|
|
1666
1741
|
describe: "Agent ID"
|
|
1667
|
-
}).command(listCommand$
|
|
1742
|
+
}).command(listCommand$5).command(createCommand).command(revokeCommand$1).demandCommand(1, "Specify a subcommand: list, create, revoke"),
|
|
1668
1743
|
handler: () => {}
|
|
1669
1744
|
};
|
|
1670
1745
|
|
|
@@ -1676,7 +1751,7 @@ async function readStdin() {
|
|
|
1676
1751
|
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1677
1752
|
return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
1678
1753
|
}
|
|
1679
|
-
const listCommand$
|
|
1754
|
+
const listCommand$4 = {
|
|
1680
1755
|
command: "list",
|
|
1681
1756
|
describe: "List secret names for an agent (values are never shown)",
|
|
1682
1757
|
handler: async (argv) => {
|
|
@@ -1762,7 +1837,7 @@ const secretsCommand = {
|
|
|
1762
1837
|
type: "string",
|
|
1763
1838
|
demandOption: true,
|
|
1764
1839
|
describe: "Agent ID"
|
|
1765
|
-
}).command(listCommand$
|
|
1840
|
+
}).command(listCommand$4).command(setCommand$1).command(rmCommand).demandCommand(1, "Specify a subcommand: list, set, rm"),
|
|
1766
1841
|
handler: () => {}
|
|
1767
1842
|
};
|
|
1768
1843
|
|
|
@@ -1859,6 +1934,24 @@ async function resolveAgentWorkspace({ appUrl, sessionToken, agentSelector }) {
|
|
|
1859
1934
|
|
|
1860
1935
|
//#endregion
|
|
1861
1936
|
//#region src/commands/chat.ts
|
|
1937
|
+
/**
|
|
1938
|
+
* Resolve the `--workspace` selector to an id, or undefined when the flag is
|
|
1939
|
+
* absent. A bad selector is fatal — a chat scoped to the wrong (or a
|
|
1940
|
+
* nonexistent) workspace is worse than failing fast with how to list them.
|
|
1941
|
+
*/
|
|
1942
|
+
async function resolveWorkspaceFlag({ appUrl, sessionToken, selector }) {
|
|
1943
|
+
if (!selector) return void 0;
|
|
1944
|
+
const resolved = await resolveWorkspaceId({
|
|
1945
|
+
appUrl,
|
|
1946
|
+
sessionToken,
|
|
1947
|
+
selector
|
|
1948
|
+
});
|
|
1949
|
+
if (resolved.isErr()) {
|
|
1950
|
+
printError(resolved.error.message);
|
|
1951
|
+
process.exit(1);
|
|
1952
|
+
}
|
|
1953
|
+
return resolved.value.id;
|
|
1954
|
+
}
|
|
1862
1955
|
const chatCommand = {
|
|
1863
1956
|
command: "chat",
|
|
1864
1957
|
describe: "Open the interactive chat TUI, or -p for a one-shot",
|
|
@@ -1869,6 +1962,9 @@ const chatCommand = {
|
|
|
1869
1962
|
}).option("agent", {
|
|
1870
1963
|
type: "string",
|
|
1871
1964
|
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."
|
|
1965
|
+
}).option("workspace", {
|
|
1966
|
+
type: "string",
|
|
1967
|
+
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."
|
|
1872
1968
|
}).option("conversation", {
|
|
1873
1969
|
alias: "resume",
|
|
1874
1970
|
type: "string",
|
|
@@ -1877,6 +1973,10 @@ const chatCommand = {
|
|
|
1877
1973
|
type: "boolean",
|
|
1878
1974
|
default: false,
|
|
1879
1975
|
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."
|
|
1976
|
+
}).option("onboarding", {
|
|
1977
|
+
type: "boolean",
|
|
1978
|
+
default: false,
|
|
1979
|
+
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."
|
|
1880
1980
|
}).option("share-machine", {
|
|
1881
1981
|
type: "boolean",
|
|
1882
1982
|
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."
|
|
@@ -1918,21 +2018,27 @@ const chatCommand = {
|
|
|
1918
2018
|
process.exit(1);
|
|
1919
2019
|
}
|
|
1920
2020
|
}
|
|
2021
|
+
const workspaceId = await resolveWorkspaceFlag({
|
|
2022
|
+
appUrl,
|
|
2023
|
+
sessionToken: auth.value.token,
|
|
2024
|
+
selector: argv.workspace
|
|
2025
|
+
});
|
|
1921
2026
|
const themeId = argv.theme ?? process.env["SKYDIVE_THEME"] ?? void 0;
|
|
1922
2027
|
if (themeId !== void 0 && !themes.some((t) => t.id === themeId)) {
|
|
1923
2028
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
1924
2029
|
process.exit(1);
|
|
1925
2030
|
}
|
|
1926
|
-
await ensureAgentWorkspace({
|
|
2031
|
+
if (!workspaceId) await ensureAgentWorkspace({
|
|
1927
2032
|
appUrl,
|
|
1928
2033
|
sessionToken: auth.value.token,
|
|
1929
2034
|
agentSelector: argv.agent ?? null
|
|
1930
2035
|
});
|
|
1931
|
-
const { runChat } = await import("./boot-
|
|
2036
|
+
const { runChat } = await import("./boot-ClKjP9Mn.mjs");
|
|
1932
2037
|
await runChat({
|
|
1933
2038
|
appUrl: auth.value.appUrl,
|
|
1934
2039
|
sessionToken: auth.value.token,
|
|
1935
2040
|
authKind: auth.value.kind,
|
|
2041
|
+
workspaceId,
|
|
1936
2042
|
shareMachine: resolveShareMachine(argv),
|
|
1937
2043
|
promptHistoryPath: getPromptHistoryPath(),
|
|
1938
2044
|
theme: themeId,
|
|
@@ -1940,6 +2046,8 @@ const chatCommand = {
|
|
|
1940
2046
|
agentSelector: argv.agent ?? null,
|
|
1941
2047
|
conversationId: argv.conversation ?? null,
|
|
1942
2048
|
newConversation: resolveNewConversation(argv),
|
|
2049
|
+
forceOnboarding: resolveForceOnboarding(argv),
|
|
2050
|
+
defaultAgentSelector: getDefaultAgent(),
|
|
1943
2051
|
seedPrompt: null
|
|
1944
2052
|
});
|
|
1945
2053
|
}
|
|
@@ -1961,6 +2069,20 @@ function resolveNewConversation(argv) {
|
|
|
1961
2069
|
return argv.new === true && !argv.conversation;
|
|
1962
2070
|
}
|
|
1963
2071
|
/**
|
|
2072
|
+
* Whether to force the first-run onboarding flow: the `--onboarding` flag or
|
|
2073
|
+
* `SKYDIVE_FORCE_ONBOARDING` (any non-empty, non-"0"/"false" value). Lets the
|
|
2074
|
+
* new-user experience be replayed in a workspace that already has agents. The
|
|
2075
|
+
* TUI applies it only to a bare interactive launch — an explicit
|
|
2076
|
+
* `--agent`/`--resume`/`--new` still takes precedence.
|
|
2077
|
+
*/
|
|
2078
|
+
function resolveForceOnboarding(argv, env = process.env) {
|
|
2079
|
+
if (argv.onboarding === true) return true;
|
|
2080
|
+
const raw = env["SKYDIVE_FORCE_ONBOARDING"];
|
|
2081
|
+
if (raw === void 0) return false;
|
|
2082
|
+
const value = raw.trim().toLowerCase();
|
|
2083
|
+
return value !== "" && value !== "0" && value !== "false";
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
1964
2086
|
* Cross-workspace `--agent <uuid>` handling shared by the TUI and -p paths:
|
|
1965
2087
|
* on a hit in another workspace, switch to it and say so on stderr (stdout
|
|
1966
2088
|
* may carry the -p reply/JSON envelope); when the id matches no workspace on
|
|
@@ -1995,8 +2117,13 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
1995
2117
|
printError(`${auth.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
|
|
1996
2118
|
process.exit(1);
|
|
1997
2119
|
}
|
|
1998
|
-
const { runPrint, readStdin } = await import("./print-
|
|
1999
|
-
await
|
|
2120
|
+
const { runPrint, readStdin } = await import("./print-Dc-xRapT.mjs");
|
|
2121
|
+
const workspaceId = await resolveWorkspaceFlag({
|
|
2122
|
+
appUrl,
|
|
2123
|
+
sessionToken: auth.value.token,
|
|
2124
|
+
selector: argv.workspace
|
|
2125
|
+
});
|
|
2126
|
+
if (!workspaceId) await ensureAgentWorkspace({
|
|
2000
2127
|
appUrl,
|
|
2001
2128
|
sessionToken: auth.value.token,
|
|
2002
2129
|
agentSelector: argv.agent ?? null
|
|
@@ -2020,7 +2147,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2020
2147
|
process.exit(1);
|
|
2021
2148
|
}
|
|
2022
2149
|
} else {
|
|
2023
|
-
const { connectMachineShare } = await import("./print-share-
|
|
2150
|
+
const { connectMachineShare } = await import("./print-share-Df6eAG12.mjs");
|
|
2024
2151
|
machineShare = await connectMachineShare({
|
|
2025
2152
|
appUrl: auth.value.appUrl,
|
|
2026
2153
|
sessionToken: auth.value.token,
|
|
@@ -2031,6 +2158,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2031
2158
|
const result = await runPrint({
|
|
2032
2159
|
appUrl: auth.value.appUrl,
|
|
2033
2160
|
sessionToken: auth.value.token,
|
|
2161
|
+
workspaceId,
|
|
2034
2162
|
prompt,
|
|
2035
2163
|
agentSelector: argv.agent ?? null,
|
|
2036
2164
|
conversationId: argv.conversation ?? null,
|
|
@@ -2040,7 +2168,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2040
2168
|
});
|
|
2041
2169
|
if (argv.json) output(argv, result);
|
|
2042
2170
|
} catch (error) {
|
|
2043
|
-
const { BillingBlockedError, BILLING_BLOCKED_EXIT_CODE, billingBlockedJsonResult } = await import("./billing-blocked-
|
|
2171
|
+
const { BillingBlockedError, BILLING_BLOCKED_EXIT_CODE, billingBlockedJsonResult } = await import("./billing-blocked-SE6tySLd.mjs");
|
|
2044
2172
|
if (error instanceof BillingBlockedError) {
|
|
2045
2173
|
if (argv.json) output(argv, billingBlockedJsonResult(error, null));
|
|
2046
2174
|
printError(error.message);
|
|
@@ -2084,7 +2212,7 @@ const getCommand$1 = {
|
|
|
2084
2212
|
printError(`${auth.error.message} Run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
|
|
2085
2213
|
process.exit(1);
|
|
2086
2214
|
}
|
|
2087
|
-
const { messageGet } = await import("./print-
|
|
2215
|
+
const { messageGet } = await import("./print-Dc-xRapT.mjs");
|
|
2088
2216
|
try {
|
|
2089
2217
|
const result = await messageGet({
|
|
2090
2218
|
appUrl: auth.value.appUrl,
|
|
@@ -2094,7 +2222,7 @@ const getCommand$1 = {
|
|
|
2094
2222
|
});
|
|
2095
2223
|
if (argv.json) output(argv, result);
|
|
2096
2224
|
} catch (error) {
|
|
2097
|
-
const { BillingBlockedError, BILLING_BLOCKED_EXIT_CODE, billingBlockedJsonResult } = await import("./billing-blocked-
|
|
2225
|
+
const { BillingBlockedError, BILLING_BLOCKED_EXIT_CODE, billingBlockedJsonResult } = await import("./billing-blocked-SE6tySLd.mjs");
|
|
2098
2226
|
if (error instanceof BillingBlockedError) {
|
|
2099
2227
|
if (argv.json) output(argv, billingBlockedJsonResult(error, argv["message-id"]));
|
|
2100
2228
|
printError(error.message);
|
|
@@ -2131,7 +2259,7 @@ function messageToText(message) {
|
|
|
2131
2259
|
}
|
|
2132
2260
|
return chunks.join("\n").trim();
|
|
2133
2261
|
}
|
|
2134
|
-
const listCommand$
|
|
2262
|
+
const listCommand$3 = {
|
|
2135
2263
|
command: "list",
|
|
2136
2264
|
describe: "List an agent's conversations",
|
|
2137
2265
|
builder: (y) => y.option("agent", {
|
|
@@ -2143,7 +2271,7 @@ const listCommand$2 = {
|
|
|
2143
2271
|
}),
|
|
2144
2272
|
handler: async (argv) => {
|
|
2145
2273
|
const client = requireRestClient(argv);
|
|
2146
|
-
const agent = resolveAgent(await client.listAgents({
|
|
2274
|
+
const agent = resolveAgent$1(await client.listAgents({
|
|
2147
2275
|
scope: "org",
|
|
2148
2276
|
onPage: null
|
|
2149
2277
|
}), argv.agent ?? null);
|
|
@@ -2200,7 +2328,7 @@ const showCommand = {
|
|
|
2200
2328
|
console.log(recap ?? "(no recap available)");
|
|
2201
2329
|
return;
|
|
2202
2330
|
}
|
|
2203
|
-
const messages = await client.listMessages({ conversationId });
|
|
2331
|
+
const { messages } = await client.listMessages({ conversationId });
|
|
2204
2332
|
if (argv.json) {
|
|
2205
2333
|
output(argv, messages);
|
|
2206
2334
|
return;
|
|
@@ -2221,7 +2349,7 @@ const conversationsCommand = {
|
|
|
2221
2349
|
command: "conversations",
|
|
2222
2350
|
aliases: ["conv"],
|
|
2223
2351
|
describe: "List and read agent conversations",
|
|
2224
|
-
builder: (y) => y.command(listCommand$
|
|
2352
|
+
builder: (y) => y.command(listCommand$3).command(showCommand).demandCommand(1, "Specify a subcommand: list, show"),
|
|
2225
2353
|
handler: () => {}
|
|
2226
2354
|
};
|
|
2227
2355
|
|
|
@@ -2263,7 +2391,7 @@ async function runList(argv, { hint }) {
|
|
|
2263
2391
|
]));
|
|
2264
2392
|
if (hint && !argv.quiet) console.log("\nRun `skydive workspace switch` to pick a workspace, or pass its slug directly.");
|
|
2265
2393
|
}
|
|
2266
|
-
const listCommand$
|
|
2394
|
+
const listCommand$2 = {
|
|
2267
2395
|
command: "list",
|
|
2268
2396
|
describe: "List workspaces on your account",
|
|
2269
2397
|
handler: (argv) => runList(argv, { hint: false })
|
|
@@ -2287,7 +2415,7 @@ const switchCommand = {
|
|
|
2287
2415
|
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.");
|
|
2288
2416
|
process.exit(1);
|
|
2289
2417
|
}
|
|
2290
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
2418
|
+
const { runWorkspacePicker } = await import("./boot-ClKjP9Mn.mjs");
|
|
2291
2419
|
await runWorkspacePicker(session);
|
|
2292
2420
|
return;
|
|
2293
2421
|
}
|
|
@@ -2320,7 +2448,7 @@ const switchCommand = {
|
|
|
2320
2448
|
const workspaceCommand = {
|
|
2321
2449
|
command: "workspace",
|
|
2322
2450
|
describe: "List or switch the workspace all `skydive` commands act on",
|
|
2323
|
-
builder: (y) => y.command(listCommand$
|
|
2451
|
+
builder: (y) => y.command(listCommand$2).command(switchCommand),
|
|
2324
2452
|
handler: (argv) => runList(argv, { hint: true })
|
|
2325
2453
|
};
|
|
2326
2454
|
|
|
@@ -2336,10 +2464,10 @@ const workspaceCommand = {
|
|
|
2336
2464
|
*/
|
|
2337
2465
|
async function resolveGrantTarget(argv) {
|
|
2338
2466
|
const session = requireSession(argv);
|
|
2339
|
-
const [{ devices, agents }, identity] = await Promise.all([fetchPortalDevices(session), resolveMachineIdentity()]);
|
|
2467
|
+
const [{ devices, agents }, identity] = await Promise.all([fetchPortalDevices(session), resolveMachineIdentity(null)]);
|
|
2340
2468
|
return {
|
|
2341
2469
|
session,
|
|
2342
|
-
agent: resolveAgent(agents, argv.agent),
|
|
2470
|
+
agent: resolveAgent$1(agents, argv.agent),
|
|
2343
2471
|
device: findThisDevice(devices, identity.machineName),
|
|
2344
2472
|
identity
|
|
2345
2473
|
};
|
|
@@ -2363,10 +2491,10 @@ const openCommand = {
|
|
|
2363
2491
|
}),
|
|
2364
2492
|
handler: async (argv) => {
|
|
2365
2493
|
const session = requireSession(argv);
|
|
2366
|
-
const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
2494
|
+
const agent = argv.agent ? resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
2367
2495
|
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
2368
|
-
const { machineName } = await resolveMachineIdentity();
|
|
2369
|
-
const { PortalDaemonClient } = await import("./daemon-client-
|
|
2496
|
+
const { machineName } = await resolveMachineIdentity(null);
|
|
2497
|
+
const { PortalDaemonClient } = await import("./daemon-client-D5YCk5Hc.mjs");
|
|
2370
2498
|
let lastLine = "";
|
|
2371
2499
|
let signalConnected;
|
|
2372
2500
|
const connected = new Promise((resolve) => {
|
|
@@ -2395,7 +2523,7 @@ const openCommand = {
|
|
|
2395
2523
|
client.enable();
|
|
2396
2524
|
await connected;
|
|
2397
2525
|
if (agent) {
|
|
2398
|
-
await client.grantAgent(agent.id);
|
|
2526
|
+
await client.grantAgent(agent.id, null);
|
|
2399
2527
|
console.log(`portal: granted ${agent.name} access to this machine (persists until revoked)`);
|
|
2400
2528
|
}
|
|
2401
2529
|
console.log(`portal: open. granted agents can run commands on ${machineName} as your user, cwd ${cwd}. ctrl+c to close.`);
|
|
@@ -2415,7 +2543,8 @@ const grantCommand = {
|
|
|
2415
2543
|
})).id;
|
|
2416
2544
|
await grantPortalAccess(session, {
|
|
2417
2545
|
deviceId,
|
|
2418
|
-
agentId: agent.id
|
|
2546
|
+
agentId: agent.id,
|
|
2547
|
+
conversationId: null
|
|
2419
2548
|
});
|
|
2420
2549
|
if (argv.json) {
|
|
2421
2550
|
output(argv, {
|
|
@@ -2424,7 +2553,7 @@ const grantCommand = {
|
|
|
2424
2553
|
});
|
|
2425
2554
|
return;
|
|
2426
2555
|
}
|
|
2427
|
-
console.log(`Granted ${agent.name} access to ${device?.friendlyName ?? friendlyName}. The agent can run commands here whenever its portal is open. Start with \`skydive portal open\`, or pass \`--share-machine\` on a \`chat -p\` run. Revoke with \`skydive portal revoke --agent ${agent.name}\`.`);
|
|
2556
|
+
console.log(`Granted ${agent.name} access to ${device?.friendlyName ?? friendlyName}. The agent can run commands here whenever its portal is open. Start with \`skydive portal open\`, or pass \`--share-machine\` on a \`chat -p\` run. Revoke with \`skydive portal revoke --agent "${agent.name}"\`.`);
|
|
2428
2557
|
}
|
|
2429
2558
|
};
|
|
2430
2559
|
const revokeCommand = {
|
|
@@ -2464,7 +2593,7 @@ const statusCommand = {
|
|
|
2464
2593
|
console.log("No machines registered. Run `skydive portal open` to register this one.");
|
|
2465
2594
|
return;
|
|
2466
2595
|
}
|
|
2467
|
-
const { headers, rows } = buildDeviceTable(devices, agents, (await resolveMachineIdentity()).machineName);
|
|
2596
|
+
const { headers, rows } = buildDeviceTable(devices, agents, (await resolveMachineIdentity(null)).machineName);
|
|
2468
2597
|
printTable(headers, rows);
|
|
2469
2598
|
}
|
|
2470
2599
|
};
|
|
@@ -2592,17 +2721,56 @@ const daemonCommand = {
|
|
|
2592
2721
|
builder: (y) => y.command(daemonStatusCommand).command(daemonStopCommand).command(daemonStartCommand).command(daemonRestartCommand).command(daemonLogsCommand).demandCommand(1, "Specify a subcommand: status, stop, start, restart, logs"),
|
|
2593
2722
|
handler: () => {}
|
|
2594
2723
|
};
|
|
2724
|
+
const forwardCommand = {
|
|
2725
|
+
command: "forward <port>",
|
|
2726
|
+
describe: "Forward localhost:<port> to the agent sandbox's port, so a dev server running in the sandbox is reachable at http://localhost:<port> (stays in the foreground; ctrl+c to stop)",
|
|
2727
|
+
builder: (y) => y.positional("port", {
|
|
2728
|
+
type: "number",
|
|
2729
|
+
demandOption: true,
|
|
2730
|
+
describe: "Sandbox port to forward to (e.g. 3000 for a dev server)"
|
|
2731
|
+
}).option("agent", {
|
|
2732
|
+
type: "string",
|
|
2733
|
+
describe: "Agent, by id or name (defaults when the account has one)"
|
|
2734
|
+
}).option("local-port", {
|
|
2735
|
+
type: "number",
|
|
2736
|
+
describe: "Local port to listen on (default: same as <port>)"
|
|
2737
|
+
}),
|
|
2738
|
+
handler: async (argv) => {
|
|
2739
|
+
const session = requireSession(argv);
|
|
2740
|
+
const agent = resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent ?? null);
|
|
2741
|
+
const targetPort = argv.port;
|
|
2742
|
+
const localPort = argv["local-port"] ?? targetPort;
|
|
2743
|
+
const { startForward } = await import("./forward-Ct8Vd2WW.mjs");
|
|
2744
|
+
const { defaultTlsCertSource } = await import("./tls-cert-CLgSQALB.mjs");
|
|
2745
|
+
const log = (msg) => console.error(msg);
|
|
2746
|
+
const listener = await startForward({
|
|
2747
|
+
auth: session,
|
|
2748
|
+
agentId: agent.id,
|
|
2749
|
+
localPort,
|
|
2750
|
+
targetPort,
|
|
2751
|
+
tlsCertSource: defaultTlsCertSource(process.env, log),
|
|
2752
|
+
log
|
|
2753
|
+
});
|
|
2754
|
+
console.log(`Forwarding http://localhost:${listener.port} -> ${agent.name}'s sandbox port ${targetPort}. ctrl+c to stop.`);
|
|
2755
|
+
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)`);
|
|
2756
|
+
await new Promise((resolve) => {
|
|
2757
|
+
process.once("SIGINT", () => resolve());
|
|
2758
|
+
process.once("SIGTERM", () => resolve());
|
|
2759
|
+
});
|
|
2760
|
+
await listener.close();
|
|
2761
|
+
}
|
|
2762
|
+
};
|
|
2595
2763
|
const portalCommand = {
|
|
2596
2764
|
command: "portal",
|
|
2597
2765
|
describe: "Open this machine's portal to agents and manage their access",
|
|
2598
|
-
builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, daemon"),
|
|
2766
|
+
builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(forwardCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, forward, daemon"),
|
|
2599
2767
|
handler: () => {}
|
|
2600
2768
|
};
|
|
2601
2769
|
|
|
2602
2770
|
//#endregion
|
|
2603
2771
|
//#region src/commands/sandbox.ts
|
|
2604
2772
|
/** POSIX single-quote one word so the remote shell treats it as one token. */
|
|
2605
|
-
function shellQuote
|
|
2773
|
+
function shellQuote(word) {
|
|
2606
2774
|
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(word)) return word;
|
|
2607
2775
|
return `'${word.replace(/'/g, `'\\''`)}'`;
|
|
2608
2776
|
}
|
|
@@ -2617,7 +2785,7 @@ function shellQuote$1(word) {
|
|
|
2617
2785
|
* for too. yargs number-coerces bare numerals, so words are stringified.
|
|
2618
2786
|
*/
|
|
2619
2787
|
function joinCommandWords(words) {
|
|
2620
|
-
return words.map(String).map(shellQuote
|
|
2788
|
+
return words.map(String).map(shellQuote).join(" ").trim();
|
|
2621
2789
|
}
|
|
2622
2790
|
/**
|
|
2623
2791
|
* `skydive sandbox` — the standalone counterpart to the chat TUI's `/sandbox`
|
|
@@ -2640,8 +2808,8 @@ const sandboxCommand = {
|
|
|
2640
2808
|
}).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`"),
|
|
2641
2809
|
handler: async (argv) => {
|
|
2642
2810
|
const session = requireSession(argv);
|
|
2643
|
-
const { createRestClient } = await import("./rest-
|
|
2644
|
-
const { resolveAgent } = await import("./print-
|
|
2811
|
+
const { createRestClient } = await import("./rest-Do3mPVpC.mjs");
|
|
2812
|
+
const { resolveAgent } = await import("./print-Dc-xRapT.mjs");
|
|
2645
2813
|
const client = createRestClient({
|
|
2646
2814
|
appUrl: session.appUrl,
|
|
2647
2815
|
sessionToken: session.sessionToken
|
|
@@ -2710,7 +2878,7 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2710
2878
|
return 1;
|
|
2711
2879
|
}
|
|
2712
2880
|
console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
|
|
2713
|
-
const { runRawPtyPassthrough } = await import("./raw-pty-
|
|
2881
|
+
const { runRawPtyPassthrough } = await import("./raw-pty-Ci2qFR9F.mjs");
|
|
2714
2882
|
const result = await runRawPtyPassthrough({
|
|
2715
2883
|
stdin: process.stdin,
|
|
2716
2884
|
stdout: process.stdout,
|
|
@@ -2722,40 +2890,151 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2722
2890
|
return result.code;
|
|
2723
2891
|
}
|
|
2724
2892
|
|
|
2893
|
+
//#endregion
|
|
2894
|
+
//#region src/chat/sandbox/fs-client.ts
|
|
2895
|
+
function wsBase(appUrl) {
|
|
2896
|
+
const base = appUrl.replace(/\/+$/, "");
|
|
2897
|
+
if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
|
|
2898
|
+
if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
|
|
2899
|
+
return `wss://${base}`;
|
|
2900
|
+
}
|
|
2901
|
+
function toU8(data) {
|
|
2902
|
+
if (Buffer.isBuffer(data)) return new Uint8Array(data);
|
|
2903
|
+
if (Array.isArray(data)) return new Uint8Array(Buffer.concat(data));
|
|
2904
|
+
return new Uint8Array(Buffer.from(data));
|
|
2905
|
+
}
|
|
2906
|
+
var SandboxFsClient = class SandboxFsClient {
|
|
2907
|
+
ws;
|
|
2908
|
+
ready;
|
|
2909
|
+
nextReqId = 1;
|
|
2910
|
+
pending = /* @__PURE__ */ new Map();
|
|
2911
|
+
closedReason = null;
|
|
2912
|
+
constructor(opts) {
|
|
2913
|
+
const spec = {
|
|
2914
|
+
mode: "fs",
|
|
2915
|
+
agentId: opts.agentId
|
|
2916
|
+
};
|
|
2917
|
+
const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
|
|
2918
|
+
for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
|
|
2919
|
+
this.ws = new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } });
|
|
2920
|
+
this.ws.binaryType = "nodebuffer";
|
|
2921
|
+
this.ready = new Promise((resolve, reject) => {
|
|
2922
|
+
this.ws.once("open", () => resolve());
|
|
2923
|
+
this.ws.once("error", (err) => reject(err));
|
|
2924
|
+
});
|
|
2925
|
+
this.ws.on("message", (data, isBinary) => {
|
|
2926
|
+
if (!isBinary) return;
|
|
2927
|
+
const res = decodeFsResponse(toU8(data));
|
|
2928
|
+
if (!res) return;
|
|
2929
|
+
const p = this.pending.get(res.reqId);
|
|
2930
|
+
if (!p) return;
|
|
2931
|
+
this.pending.delete(res.reqId);
|
|
2932
|
+
if (res.status === "error") p.reject(new Error(res.message));
|
|
2933
|
+
else p.resolve({
|
|
2934
|
+
result: res.result,
|
|
2935
|
+
blob: res.blob
|
|
2936
|
+
});
|
|
2937
|
+
});
|
|
2938
|
+
const failAll = (reason) => {
|
|
2939
|
+
this.closedReason = reason;
|
|
2940
|
+
for (const [, p] of this.pending) p.reject(new Error(reason));
|
|
2941
|
+
this.pending.clear();
|
|
2942
|
+
};
|
|
2943
|
+
this.ws.on("error", (err) => failAll(err.message));
|
|
2944
|
+
this.ws.on("close", () => failAll(this.closedReason ?? "the sandbox filesystem connection closed"));
|
|
2945
|
+
}
|
|
2946
|
+
/** Open and wait for the channel to be ready to accept requests. */
|
|
2947
|
+
static async connect(opts) {
|
|
2948
|
+
const client = new SandboxFsClient(opts);
|
|
2949
|
+
await client.ready;
|
|
2950
|
+
return client;
|
|
2951
|
+
}
|
|
2952
|
+
request(req, blob) {
|
|
2953
|
+
if (this.closedReason) return Promise.reject(new Error(this.closedReason));
|
|
2954
|
+
const reqId = this.nextReqId++;
|
|
2955
|
+
return new Promise((resolve, reject) => {
|
|
2956
|
+
this.pending.set(reqId, {
|
|
2957
|
+
resolve,
|
|
2958
|
+
reject
|
|
2959
|
+
});
|
|
2960
|
+
try {
|
|
2961
|
+
this.ws.send(encodeFsRequest(reqId, req, blob));
|
|
2962
|
+
} catch (error) {
|
|
2963
|
+
this.pending.delete(reqId);
|
|
2964
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
async list(path) {
|
|
2969
|
+
const { result } = await this.request({
|
|
2970
|
+
op: "list",
|
|
2971
|
+
path
|
|
2972
|
+
}, null);
|
|
2973
|
+
const entries = result["entries"];
|
|
2974
|
+
if (!Array.isArray(entries)) return [];
|
|
2975
|
+
return entries;
|
|
2976
|
+
}
|
|
2977
|
+
async stat(path) {
|
|
2978
|
+
const { result } = await this.request({
|
|
2979
|
+
op: "stat",
|
|
2980
|
+
path
|
|
2981
|
+
}, null);
|
|
2982
|
+
return result["stat"];
|
|
2983
|
+
}
|
|
2984
|
+
async read(path, range) {
|
|
2985
|
+
const req = range ? {
|
|
2986
|
+
op: "read",
|
|
2987
|
+
path,
|
|
2988
|
+
offset: range.offset,
|
|
2989
|
+
length: range.length
|
|
2990
|
+
} : {
|
|
2991
|
+
op: "read",
|
|
2992
|
+
path
|
|
2993
|
+
};
|
|
2994
|
+
const { blob } = await this.request(req, null);
|
|
2995
|
+
return blob;
|
|
2996
|
+
}
|
|
2997
|
+
async write(path, data) {
|
|
2998
|
+
await this.request({
|
|
2999
|
+
op: "write",
|
|
3000
|
+
path
|
|
3001
|
+
}, data);
|
|
3002
|
+
}
|
|
3003
|
+
async mkdir(path) {
|
|
3004
|
+
await this.request({
|
|
3005
|
+
op: "mkdir",
|
|
3006
|
+
path
|
|
3007
|
+
}, null);
|
|
3008
|
+
}
|
|
3009
|
+
async rename(oldPath, newPath) {
|
|
3010
|
+
await this.request({
|
|
3011
|
+
op: "rename",
|
|
3012
|
+
oldPath,
|
|
3013
|
+
newPath
|
|
3014
|
+
}, null);
|
|
3015
|
+
}
|
|
3016
|
+
async remove(path) {
|
|
3017
|
+
await this.request({
|
|
3018
|
+
op: "remove",
|
|
3019
|
+
path
|
|
3020
|
+
}, null);
|
|
3021
|
+
}
|
|
3022
|
+
async exists(path) {
|
|
3023
|
+
const { result } = await this.request({
|
|
3024
|
+
op: "exists",
|
|
3025
|
+
path
|
|
3026
|
+
}, null);
|
|
3027
|
+
return result["exists"] === true;
|
|
3028
|
+
}
|
|
3029
|
+
close() {
|
|
3030
|
+
this.closedReason ??= "closed by caller";
|
|
3031
|
+
this.ws.close();
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
3034
|
+
|
|
2725
3035
|
//#endregion
|
|
2726
3036
|
//#region src/commands/fs.ts
|
|
2727
3037
|
/**
|
|
2728
|
-
* POSIX-quote one word so the remote shell treats it as a single token. Wraps
|
|
2729
|
-
* `shell-quote`'s `quote` (already a repo dependency, used elsewhere in the
|
|
2730
|
-
* CLI) rather than hand-rolling the escaping — it is the well-tested source of
|
|
2731
|
-
* truth for making a path/arg with spaces, quotes, `$`, `;`, globs, etc. inert
|
|
2732
|
-
* when interpolated into an `sh -c` line.
|
|
2733
|
-
*/
|
|
2734
|
-
function shellQuote(word) {
|
|
2735
|
-
return quote([word]);
|
|
2736
|
-
}
|
|
2737
|
-
const MISSING_FILE_EXIT = 44;
|
|
2738
|
-
/**
|
|
2739
|
-
* Build the remote shell command that base64-encodes a file to stdout. Exits
|
|
2740
|
-
* {@link MISSING_FILE_EXIT} when the path doesn't exist, 1 when it's a
|
|
2741
|
-
* directory, so the caller can tell "create new" from a genuine error.
|
|
2742
|
-
*/
|
|
2743
|
-
function buildReadCommand(path) {
|
|
2744
|
-
const q = shellQuote(path);
|
|
2745
|
-
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}`)}`;
|
|
2746
|
-
}
|
|
2747
|
-
/**
|
|
2748
|
-
* Build the remote shell command that decodes `b64` and writes it to `path`
|
|
2749
|
-
* atomically (temp file + `mv`) so a dropped connection can't truncate the
|
|
2750
|
-
* original in place. `token` is a caller-supplied unique suffix for the temp
|
|
2751
|
-
* file — generated in JS (not the shell's `$$`, which shell-quote would escape
|
|
2752
|
-
* to a literal, so every write would collide on the same temp name).
|
|
2753
|
-
*/
|
|
2754
|
-
function buildWriteCommand(path, b64, token) {
|
|
2755
|
-
const tmp = `${path}.skydive-edit.${token}`;
|
|
2756
|
-
return `sh -c ${shellQuote(`printf %s ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)} && mv -f ${shellQuote(tmp)} ${shellQuote(path)}`)}`;
|
|
2757
|
-
}
|
|
2758
|
-
/**
|
|
2759
3038
|
* Resolve the editor command from an explicit override, then $VISUAL, $EDITOR,
|
|
2760
3039
|
* then `vi`. Split so `EDITOR="code --wait"` (command + flags) is honored.
|
|
2761
3040
|
*/
|
|
@@ -2769,17 +3048,17 @@ function resolveEditor(override, env) {
|
|
|
2769
3048
|
/**
|
|
2770
3049
|
* `skydive fs edit <path>` — edit a file that lives on the agent's computer
|
|
2771
3050
|
* (its sandbox) with your LOCAL editor. It reads the remote file down over the
|
|
2772
|
-
*
|
|
2773
|
-
* changes back when you save and close — so you edit a remote file as if
|
|
2774
|
-
* were local, without opening the chat TUI.
|
|
3051
|
+
* multiplexed `fs` sandbox-stream channel, opens it in `$EDITOR`, and writes
|
|
3052
|
+
* your changes back when you save and close — so you edit a remote file as if
|
|
3053
|
+
* it were local, without opening the chat TUI.
|
|
2775
3054
|
*
|
|
2776
|
-
*
|
|
2777
|
-
*
|
|
2778
|
-
*
|
|
2779
|
-
* embedded
|
|
2780
|
-
*
|
|
2781
|
-
*
|
|
2782
|
-
*
|
|
3055
|
+
* It rides the first-class `fs` relay channel ({@link SandboxFsClient}): file
|
|
3056
|
+
* bytes move as the frame's trailing binary blob in BOTH directions, so binary
|
|
3057
|
+
* content, trailing newlines, and CRLF are preserved and — unlike the old
|
|
3058
|
+
* exec-mode path that embedded a base64 payload in the upgrade URL — a large
|
|
3059
|
+
* file no longer blows past the WebSocket URL / request-header length limit.
|
|
3060
|
+
* Access gating (EDIT on the agent + the `sandbox-fs-enabled` kill switch) is
|
|
3061
|
+
* enforced by the relay, exactly as for `skydive sandbox`.
|
|
2783
3062
|
*/
|
|
2784
3063
|
const fsEditCommand = {
|
|
2785
3064
|
command: "edit <path>",
|
|
@@ -2798,8 +3077,8 @@ const fsEditCommand = {
|
|
|
2798
3077
|
handler: async (argv) => {
|
|
2799
3078
|
const session = requireSession(argv);
|
|
2800
3079
|
const remotePath = argv.path;
|
|
2801
|
-
const { createRestClient } = await import("./rest-
|
|
2802
|
-
const { resolveAgent } = await import("./print-
|
|
3080
|
+
const { createRestClient } = await import("./rest-Do3mPVpC.mjs");
|
|
3081
|
+
const { resolveAgent } = await import("./print-Dc-xRapT.mjs");
|
|
2803
3082
|
const client = createRestClient({
|
|
2804
3083
|
appUrl: session.appUrl,
|
|
2805
3084
|
sessionToken: session.sessionToken
|
|
@@ -2814,148 +3093,63 @@ const fsEditCommand = {
|
|
|
2814
3093
|
printError(error instanceof Error ? error.message : String(error));
|
|
2815
3094
|
process.exit(1);
|
|
2816
3095
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
if (read.kind === "error") {
|
|
2827
|
-
printError(read.message);
|
|
3096
|
+
let fs;
|
|
3097
|
+
try {
|
|
3098
|
+
fs = await SandboxFsClient.connect({
|
|
3099
|
+
appUrl: session.appUrl,
|
|
3100
|
+
sessionToken: session.sessionToken,
|
|
3101
|
+
agentId: agent.id
|
|
3102
|
+
});
|
|
3103
|
+
} catch (error) {
|
|
3104
|
+
printError(`Could not open the sandbox filesystem channel: ${error instanceof Error ? error.message : String(error)}`);
|
|
2828
3105
|
process.exit(1);
|
|
2829
3106
|
}
|
|
2830
|
-
const
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
3107
|
+
const cleanup = async (dir) => {
|
|
3108
|
+
await rm(dir, {
|
|
3109
|
+
recursive: true,
|
|
3110
|
+
force: true
|
|
3111
|
+
}).catch((error) => {
|
|
3112
|
+
console.error(`Note: could not remove temp dir ${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3113
|
+
});
|
|
3114
|
+
};
|
|
3115
|
+
let local = null;
|
|
3116
|
+
let dir = null;
|
|
2834
3117
|
try {
|
|
3118
|
+
console.error(`Reading ${remotePath} from ${agent.name}'s sandbox…`);
|
|
3119
|
+
const isNew = !await fs.exists(remotePath);
|
|
3120
|
+
const original = isNew ? Buffer.alloc(0) : Buffer.from(await fs.read(remotePath, null));
|
|
3121
|
+
if (isNew) console.error(`${remotePath} does not exist yet — it will be created when you save.`);
|
|
3122
|
+
dir = await mkdtemp(join(tmpdir(), "skydive-edit-"));
|
|
3123
|
+
const base = remotePath.split("/").pop() || "file";
|
|
3124
|
+
local = join(dir, base);
|
|
2835
3125
|
await writeFile(local, original);
|
|
2836
3126
|
if (!await openEditor(argv.editor, local)) {
|
|
2837
3127
|
printError("Editor exited non-zero; not saving any changes.");
|
|
3128
|
+
fs.close();
|
|
3129
|
+
await cleanup(dir);
|
|
2838
3130
|
process.exit(1);
|
|
2839
3131
|
}
|
|
2840
3132
|
const edited = await readFile(local);
|
|
2841
3133
|
if (edited.equals(original)) {
|
|
2842
3134
|
console.error("No changes — nothing written.");
|
|
3135
|
+
fs.close();
|
|
3136
|
+
await cleanup(dir);
|
|
2843
3137
|
process.exit(0);
|
|
2844
3138
|
}
|
|
2845
3139
|
console.error(`Writing changes back to ${remotePath}…`);
|
|
2846
|
-
|
|
2847
|
-
...opts,
|
|
2848
|
-
path: remotePath,
|
|
2849
|
-
content: edited
|
|
2850
|
-
});
|
|
2851
|
-
if (wrote.kind === "error") {
|
|
2852
|
-
printError(`${wrote.message}\nYour edited copy is kept at ${local} so you don't lose it.`);
|
|
2853
|
-
process.exit(1);
|
|
2854
|
-
}
|
|
3140
|
+
await fs.write(remotePath, new Uint8Array(edited));
|
|
2855
3141
|
console.error("Saved.");
|
|
3142
|
+
fs.close();
|
|
3143
|
+
await cleanup(dir);
|
|
2856
3144
|
process.exit(0);
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
}).catch((error) => {
|
|
2862
|
-
console.error(`Note: could not remove temp dir ${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2863
|
-
});
|
|
3145
|
+
} catch (error) {
|
|
3146
|
+
fs.close();
|
|
3147
|
+
printError(`${error instanceof Error ? error.message : String(error)}${local ? `\nYour edited copy is kept at ${local} so you don't lose it.` : ""}`);
|
|
3148
|
+
process.exit(1);
|
|
2864
3149
|
}
|
|
2865
3150
|
}
|
|
2866
3151
|
};
|
|
2867
3152
|
/**
|
|
2868
|
-
* Read a remote file as bytes. Runs `base64` on the file in the sandbox and
|
|
2869
|
-
* decodes the streamed stdout locally. Distinguishes "file is missing" (exit
|
|
2870
|
-
* code 44, so `edit` can create it) from a real read failure.
|
|
2871
|
-
*/
|
|
2872
|
-
function readRemote({ session, agentId, path }) {
|
|
2873
|
-
const command = buildReadCommand(path);
|
|
2874
|
-
const chunks = [];
|
|
2875
|
-
const stderr = [];
|
|
2876
|
-
return new Promise((resolve) => {
|
|
2877
|
-
SandboxStream.open({
|
|
2878
|
-
mode: "exec",
|
|
2879
|
-
appUrl: session.appUrl,
|
|
2880
|
-
sessionToken: session.sessionToken,
|
|
2881
|
-
agentId,
|
|
2882
|
-
command,
|
|
2883
|
-
onEvent: (e) => {
|
|
2884
|
-
switch (e.type) {
|
|
2885
|
-
case "data":
|
|
2886
|
-
chunks.push(Buffer.from(e.bytes));
|
|
2887
|
-
break;
|
|
2888
|
-
case "exit":
|
|
2889
|
-
if (e.code === MISSING_FILE_EXIT) resolve({
|
|
2890
|
-
kind: "missing",
|
|
2891
|
-
content: Buffer.alloc(0)
|
|
2892
|
-
});
|
|
2893
|
-
else if (e.code === 0) resolve({
|
|
2894
|
-
kind: "ok",
|
|
2895
|
-
content: Buffer.from(Buffer.concat(chunks).toString("utf8").replace(/\s+/g, ""), "base64")
|
|
2896
|
-
});
|
|
2897
|
-
else resolve({
|
|
2898
|
-
kind: "error",
|
|
2899
|
-
message: stderr.join("").trim() || `Could not read the file (exit ${e.code}).`
|
|
2900
|
-
});
|
|
2901
|
-
break;
|
|
2902
|
-
case "error":
|
|
2903
|
-
stderr.push(e.message);
|
|
2904
|
-
break;
|
|
2905
|
-
case "close":
|
|
2906
|
-
resolve({
|
|
2907
|
-
kind: "error",
|
|
2908
|
-
message: e.failure || "The connection to the sandbox closed before the file could be read."
|
|
2909
|
-
});
|
|
2910
|
-
break;
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
});
|
|
2914
|
-
});
|
|
2915
|
-
}
|
|
2916
|
-
/**
|
|
2917
|
-
* Write bytes to a remote file. Exec has no stdin, so the base64 payload is
|
|
2918
|
-
* embedded in the command and decoded on the far side. Writes atomically via a
|
|
2919
|
-
* temp file + `mv` so a dropped connection mid-write can't leave a truncated
|
|
2920
|
-
* file in place of the original.
|
|
2921
|
-
*/
|
|
2922
|
-
function writeRemote({ session, agentId, path, content }) {
|
|
2923
|
-
const token = randomBytes(6).toString("hex");
|
|
2924
|
-
const command = buildWriteCommand(path, content.toString("base64"), token);
|
|
2925
|
-
const stderr = [];
|
|
2926
|
-
return new Promise((resolve) => {
|
|
2927
|
-
SandboxStream.open({
|
|
2928
|
-
mode: "exec",
|
|
2929
|
-
appUrl: session.appUrl,
|
|
2930
|
-
sessionToken: session.sessionToken,
|
|
2931
|
-
agentId,
|
|
2932
|
-
command,
|
|
2933
|
-
onEvent: (e) => {
|
|
2934
|
-
switch (e.type) {
|
|
2935
|
-
case "data":
|
|
2936
|
-
stderr.push(Buffer.from(e.bytes).toString("utf8"));
|
|
2937
|
-
break;
|
|
2938
|
-
case "exit":
|
|
2939
|
-
resolve(e.code === 0 ? { kind: "ok" } : {
|
|
2940
|
-
kind: "error",
|
|
2941
|
-
message: stderr.join("").trim() || `Could not write the file (exit ${e.code}).`
|
|
2942
|
-
});
|
|
2943
|
-
break;
|
|
2944
|
-
case "error":
|
|
2945
|
-
stderr.push(e.message);
|
|
2946
|
-
break;
|
|
2947
|
-
case "close":
|
|
2948
|
-
resolve({
|
|
2949
|
-
kind: "error",
|
|
2950
|
-
message: e.failure || "The connection to the sandbox closed before the file was written."
|
|
2951
|
-
});
|
|
2952
|
-
break;
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
});
|
|
2956
|
-
});
|
|
2957
|
-
}
|
|
2958
|
-
/**
|
|
2959
3153
|
* Open `file` in the caller's editor and resolve to whether it exited cleanly.
|
|
2960
3154
|
* Precedence: explicit `--editor`, then $VISUAL, $EDITOR, then `vi`. The editor
|
|
2961
3155
|
* inherits the real TTY so full-screen editors (vim/nano) work.
|
|
@@ -2978,7 +3172,7 @@ function openEditor(override, file) {
|
|
|
2978
3172
|
*
|
|
2979
3173
|
* - `fs edit <path>` edit one remote file with your local editor (shipped).
|
|
2980
3174
|
* - `fs mount <dir>` mount the whole sandbox filesystem locally (coming in a
|
|
2981
|
-
* later PR, on
|
|
3175
|
+
* later PR, on the same first-class `fs` relay channel).
|
|
2982
3176
|
*
|
|
2983
3177
|
* Grouping these under one command keeps the file surface discoverable and
|
|
2984
3178
|
* leaves room to grow (`fs ls`, `fs cat`, `fs pull`) without adding more
|
|
@@ -3266,7 +3460,27 @@ function resolveSelfPath(execPath = process.execPath) {
|
|
|
3266
3460
|
|
|
3267
3461
|
//#endregion
|
|
3268
3462
|
//#region src/update-check/sources.ts
|
|
3463
|
+
/**
|
|
3464
|
+
* How this CLI was installed, which decides whether `skydive update` (and the
|
|
3465
|
+
* background update check) self-updates the binary from the release CDN or
|
|
3466
|
+
* hands off to the package manager.
|
|
3467
|
+
*
|
|
3468
|
+
* Two signals, runtime env first:
|
|
3469
|
+
* - `SKYDIVE_CLI_INSTALL_SOURCE` env var — set by the npm launcher
|
|
3470
|
+
* (src/launcher.ts) to `package-manager` before it execs the compiled
|
|
3471
|
+
* binary. The binary itself is `--define`'d to `binary`, but when it was
|
|
3472
|
+
* launched via the npm-installed launcher, npm owns its bytes and must not
|
|
3473
|
+
* be self-mutated — so the env override wins.
|
|
3474
|
+
* - The `SKYDIVE_CLI_INSTALL_SOURCE` compile-time constant — stamped
|
|
3475
|
+
* `binary` into the standalone binary, undefined in the npm JS bundle.
|
|
3476
|
+
*
|
|
3477
|
+
* Any value other than exactly `binary` (including unset) means
|
|
3478
|
+
* package-manager, the safe default: we never self-update something we can't
|
|
3479
|
+
* prove is a standalone binary.
|
|
3480
|
+
*/
|
|
3269
3481
|
function resolveInstallSource() {
|
|
3482
|
+
const runtime = typeof process !== "undefined" ? process.env?.["SKYDIVE_CLI_INSTALL_SOURCE"] : void 0;
|
|
3483
|
+
if (runtime !== void 0) return runtime === "binary" ? "binary" : "package-manager";
|
|
3270
3484
|
return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
|
|
3271
3485
|
}
|
|
3272
3486
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
@@ -3680,6 +3894,11 @@ function resolvePreference(key) {
|
|
|
3680
3894
|
}
|
|
3681
3895
|
return pref;
|
|
3682
3896
|
}
|
|
3897
|
+
/** Human rendering of a preference value; unset string preferences (null)
|
|
3898
|
+
* print as empty rather than the string "null". */
|
|
3899
|
+
function formatValue(value) {
|
|
3900
|
+
return value === null ? "" : String(value);
|
|
3901
|
+
}
|
|
3683
3902
|
/** One row's worth of state for a preference, used by `list` and `get`. */
|
|
3684
3903
|
function snapshot(pref) {
|
|
3685
3904
|
return {
|
|
@@ -3689,7 +3908,7 @@ function snapshot(pref) {
|
|
|
3689
3908
|
describe: pref.describe
|
|
3690
3909
|
};
|
|
3691
3910
|
}
|
|
3692
|
-
const listCommand = {
|
|
3911
|
+
const listCommand$1 = {
|
|
3693
3912
|
command: "list",
|
|
3694
3913
|
aliases: ["ls"],
|
|
3695
3914
|
describe: "Show every editable preference, its value, and its source",
|
|
@@ -3706,7 +3925,7 @@ const listCommand = {
|
|
|
3706
3925
|
"Description"
|
|
3707
3926
|
], rows.map((r) => [
|
|
3708
3927
|
r.key,
|
|
3709
|
-
|
|
3928
|
+
formatValue(r.value),
|
|
3710
3929
|
r.source,
|
|
3711
3930
|
r.describe
|
|
3712
3931
|
]));
|
|
@@ -3727,7 +3946,7 @@ const getCommand = {
|
|
|
3727
3946
|
output(argv, snapshot(pref));
|
|
3728
3947
|
return;
|
|
3729
3948
|
}
|
|
3730
|
-
console.log(
|
|
3949
|
+
console.log(formatValue(pref.read()));
|
|
3731
3950
|
}
|
|
3732
3951
|
};
|
|
3733
3952
|
const setCommand = {
|
|
@@ -3745,17 +3964,16 @@ const setCommand = {
|
|
|
3745
3964
|
}),
|
|
3746
3965
|
handler: (argv) => {
|
|
3747
3966
|
const pref = resolvePreference(argv.key);
|
|
3748
|
-
const
|
|
3749
|
-
if (
|
|
3750
|
-
printError(`${pref.key}: ${
|
|
3967
|
+
const result = pref.set(argv.value ?? "");
|
|
3968
|
+
if (result.isErr()) {
|
|
3969
|
+
printError(`${pref.key}: ${result.error}`);
|
|
3751
3970
|
process.exit(1);
|
|
3752
3971
|
}
|
|
3753
|
-
pref.write(parsed.value);
|
|
3754
3972
|
if (argv.json) {
|
|
3755
3973
|
output(argv, snapshot(pref));
|
|
3756
3974
|
return;
|
|
3757
3975
|
}
|
|
3758
|
-
if (!argv.quiet) console.log(`Set ${pref.key} = ${
|
|
3976
|
+
if (!argv.quiet) console.log(`Set ${pref.key} = ${formatValue(pref.read())}`);
|
|
3759
3977
|
}
|
|
3760
3978
|
};
|
|
3761
3979
|
const unsetCommand = {
|
|
@@ -3775,10 +3993,13 @@ const unsetCommand = {
|
|
|
3775
3993
|
output(argv, snapshot(pref));
|
|
3776
3994
|
return;
|
|
3777
3995
|
}
|
|
3778
|
-
if (!argv.quiet)
|
|
3996
|
+
if (!argv.quiet) {
|
|
3997
|
+
const fallback = pref.read();
|
|
3998
|
+
console.log(fallback === null ? `Unset ${pref.key}` : `Unset ${pref.key} (now ${formatValue(fallback)} by default)`);
|
|
3999
|
+
}
|
|
3779
4000
|
}
|
|
3780
4001
|
};
|
|
3781
|
-
const pathCommand = {
|
|
4002
|
+
const pathCommand$1 = {
|
|
3782
4003
|
command: "path",
|
|
3783
4004
|
describe: "Print the on-disk config file path",
|
|
3784
4005
|
handler: (argv) => {
|
|
@@ -3792,14 +4013,172 @@ const pathCommand = {
|
|
|
3792
4013
|
const configCommand = {
|
|
3793
4014
|
command: "config",
|
|
3794
4015
|
describe: "View and set CLI preferences (safe, hand-editable keys only)",
|
|
3795
|
-
builder: (y) => y.command(listCommand).command(getCommand).command(setCommand).command(unsetCommand).command(pathCommand),
|
|
4016
|
+
builder: (y) => y.command(listCommand$1).command(getCommand).command(setCommand).command(unsetCommand).command(pathCommand$1),
|
|
4017
|
+
handler: (argv) => listCommand$1.handler(argv)
|
|
4018
|
+
};
|
|
4019
|
+
|
|
4020
|
+
//#endregion
|
|
4021
|
+
//#region src/commands/logs.ts
|
|
4022
|
+
const ISSUE_REPOSITORY_URL = "https://github.com/Create-Inc/skydive-cli";
|
|
4023
|
+
function issueUrl(archivePath) {
|
|
4024
|
+
const url = new URL(`${ISSUE_REPOSITORY_URL}/issues/new`);
|
|
4025
|
+
url.searchParams.set("title", "CLI issue: ");
|
|
4026
|
+
url.searchParams.set("body", [
|
|
4027
|
+
"## What happened?",
|
|
4028
|
+
"",
|
|
4029
|
+
"<!-- Tell us what you were doing and what went wrong. -->",
|
|
4030
|
+
"",
|
|
4031
|
+
"## Diagnostics",
|
|
4032
|
+
"",
|
|
4033
|
+
`Please attach \`${path.basename(archivePath)}\` to this issue.`
|
|
4034
|
+
].join("\n"));
|
|
4035
|
+
return url.toString();
|
|
4036
|
+
}
|
|
4037
|
+
async function listLogSessions(activeSessionPath = profileDir()) {
|
|
4038
|
+
const root = getAutomaticLogsDir();
|
|
4039
|
+
try {
|
|
4040
|
+
const entries = await fs.promises.readdir(root, { withFileTypes: true });
|
|
4041
|
+
return (await Promise.all(entries.filter((entry) => entry.isDirectory() && path.join(root, entry.name) !== activeSessionPath).map(async (entry) => {
|
|
4042
|
+
const sessionPath = path.join(root, entry.name);
|
|
4043
|
+
return {
|
|
4044
|
+
name: entry.name,
|
|
4045
|
+
path: sessionPath,
|
|
4046
|
+
modifiedAt: (await fs.promises.stat(sessionPath)).mtime.toISOString()
|
|
4047
|
+
};
|
|
4048
|
+
}))).sort((a, b) => b.name.localeCompare(a.name));
|
|
4049
|
+
} catch (_error) {
|
|
4050
|
+
return [];
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
async function resolveSession(name) {
|
|
4054
|
+
const sessions = await listLogSessions();
|
|
4055
|
+
const session = name ? sessions.find((candidate) => candidate.name === name) : sessions[0];
|
|
4056
|
+
if (session) return session;
|
|
4057
|
+
printError(name ? `Log session "${name}" was not found. Run \`skydive logs list\`.` : "No diagnostic logs found yet.");
|
|
4058
|
+
process.exit(1);
|
|
4059
|
+
}
|
|
4060
|
+
async function offerIssueReport(archivePath) {
|
|
4061
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4062
|
+
console.log(`Report an issue: ${ISSUE_REPOSITORY_URL}`);
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
const prompt = createInterface$1({
|
|
4066
|
+
input: process.stdin,
|
|
4067
|
+
output: process.stdout
|
|
4068
|
+
});
|
|
4069
|
+
try {
|
|
4070
|
+
if ((await prompt.question("Open a prefilled GitHub issue in your browser? [Y/n] ")).trim().toLowerCase() === "n") {
|
|
4071
|
+
console.log(`File an issue: ${ISSUE_REPOSITORY_URL}`);
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
await open(issueUrl(archivePath));
|
|
4075
|
+
console.log(`Attach the diagnostics archive: ${archivePath}`);
|
|
4076
|
+
} finally {
|
|
4077
|
+
prompt.close();
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
async function createArchive(source, destination) {
|
|
4081
|
+
await new Promise((resolve, reject) => {
|
|
4082
|
+
const child = spawn("tar", [
|
|
4083
|
+
"-czf",
|
|
4084
|
+
destination,
|
|
4085
|
+
"-C",
|
|
4086
|
+
path.dirname(source),
|
|
4087
|
+
path.basename(source)
|
|
4088
|
+
], { stdio: [
|
|
4089
|
+
"ignore",
|
|
4090
|
+
"ignore",
|
|
4091
|
+
"pipe"
|
|
4092
|
+
] });
|
|
4093
|
+
const stderr = [];
|
|
4094
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
4095
|
+
child.once("error", reject);
|
|
4096
|
+
child.once("close", (code) => {
|
|
4097
|
+
if (code === 0) {
|
|
4098
|
+
resolve();
|
|
4099
|
+
return;
|
|
4100
|
+
}
|
|
4101
|
+
reject(new Error(Buffer.concat(stderr).toString("utf8").trim() || "Could not archive logs."));
|
|
4102
|
+
});
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
4105
|
+
const listCommand = {
|
|
4106
|
+
command: "list",
|
|
4107
|
+
aliases: ["ls"],
|
|
4108
|
+
describe: "List recent diagnostic sessions",
|
|
4109
|
+
handler: async (argv) => {
|
|
4110
|
+
const sessions = await listLogSessions();
|
|
4111
|
+
if (argv.json) {
|
|
4112
|
+
output(argv, sessions);
|
|
4113
|
+
return;
|
|
4114
|
+
}
|
|
4115
|
+
if (sessions.length === 0) {
|
|
4116
|
+
console.log("No diagnostic logs found yet.");
|
|
4117
|
+
return;
|
|
4118
|
+
}
|
|
4119
|
+
printTable(["Session", "Modified"], sessions.map((session) => [session.name, session.modifiedAt]));
|
|
4120
|
+
}
|
|
4121
|
+
};
|
|
4122
|
+
const pathCommand = {
|
|
4123
|
+
command: "path [session]",
|
|
4124
|
+
describe: "Print a diagnostic session directory (latest by default)",
|
|
4125
|
+
builder: (y) => y.positional("session", {
|
|
4126
|
+
type: "string",
|
|
4127
|
+
describe: "Session name from `skydive logs list`"
|
|
4128
|
+
}),
|
|
4129
|
+
handler: async (argv) => {
|
|
4130
|
+
const session = await resolveSession(argv.session);
|
|
4131
|
+
if (argv.json) {
|
|
4132
|
+
output(argv, session);
|
|
4133
|
+
return;
|
|
4134
|
+
}
|
|
4135
|
+
console.log(session.path);
|
|
4136
|
+
}
|
|
4137
|
+
};
|
|
4138
|
+
const exportCommand = {
|
|
4139
|
+
command: "export [session]",
|
|
4140
|
+
describe: "Archive a diagnostic session for sharing (latest by default)",
|
|
4141
|
+
builder: (y) => y.positional("session", {
|
|
4142
|
+
type: "string",
|
|
4143
|
+
describe: "Session name from `skydive logs list`"
|
|
4144
|
+
}).option("output", {
|
|
4145
|
+
alias: "o",
|
|
4146
|
+
type: "string",
|
|
4147
|
+
describe: "Archive path (defaults to the current directory)"
|
|
4148
|
+
}),
|
|
4149
|
+
handler: async (argv) => {
|
|
4150
|
+
const session = await resolveSession(argv.session);
|
|
4151
|
+
const destination = path.resolve(argv.output ?? `skydive-logs-${session.name}.tar.gz`);
|
|
4152
|
+
try {
|
|
4153
|
+
await createArchive(session.path, destination);
|
|
4154
|
+
} catch (error) {
|
|
4155
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
4156
|
+
process.exit(1);
|
|
4157
|
+
}
|
|
4158
|
+
if (argv.json) {
|
|
4159
|
+
output(argv, {
|
|
4160
|
+
path: destination,
|
|
4161
|
+
session: session.name,
|
|
4162
|
+
issueUrl: issueUrl(destination),
|
|
4163
|
+
repositoryUrl: ISSUE_REPOSITORY_URL
|
|
4164
|
+
});
|
|
4165
|
+
return;
|
|
4166
|
+
}
|
|
4167
|
+
console.log(destination);
|
|
4168
|
+
await offerIssueReport(destination);
|
|
4169
|
+
}
|
|
4170
|
+
};
|
|
4171
|
+
const logsCommand = {
|
|
4172
|
+
command: "logs",
|
|
4173
|
+
describe: "Find and export recent local diagnostic logs",
|
|
4174
|
+
builder: (y) => y.command(listCommand).command(pathCommand).command(exportCommand),
|
|
3796
4175
|
handler: (argv) => listCommand.handler(argv)
|
|
3797
4176
|
};
|
|
3798
4177
|
|
|
3799
4178
|
//#endregion
|
|
3800
4179
|
//#region src/cli.ts
|
|
3801
4180
|
function createCli(argv) {
|
|
3802
|
-
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", {
|
|
4181
|
+
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", {
|
|
3803
4182
|
type: "boolean",
|
|
3804
4183
|
default: false,
|
|
3805
4184
|
global: true,
|
|
@@ -3813,7 +4192,7 @@ function createCli(argv) {
|
|
|
3813
4192
|
type: "string",
|
|
3814
4193
|
global: true,
|
|
3815
4194
|
describe: "Override API base URL"
|
|
3816
|
-
}).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) => {
|
|
4195
|
+
}).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) => {
|
|
3817
4196
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
3818
4197
|
process.exit(1);
|
|
3819
4198
|
});
|
|
@@ -4026,7 +4405,7 @@ function setupUpdateCheck(argv) {
|
|
|
4026
4405
|
|
|
4027
4406
|
//#endregion
|
|
4028
4407
|
//#region src/changelog.generated.ts
|
|
4029
|
-
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";
|
|
4408
|
+
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- `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- `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";
|
|
4030
4409
|
|
|
4031
4410
|
//#endregion
|
|
4032
4411
|
//#region src/whats-new.ts
|
|
@@ -4169,7 +4548,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
|
|
|
4169
4548
|
process.exit(0);
|
|
4170
4549
|
}
|
|
4171
4550
|
if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
|
|
4172
|
-
const { runPortalDaemon } = await import("./daemon-
|
|
4551
|
+
const { runPortalDaemon } = await import("./daemon-mLypjXdu.mjs");
|
|
4173
4552
|
runPortalDaemon(process.argv);
|
|
4174
4553
|
} else runCli();
|
|
4175
4554
|
function runCli() {
|