skydive-cli 0.2.0 → 0.3.0-beta.773
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 +42 -0
- package/README.md +114 -22
- package/dist/js/bin.mjs +1261 -128
- package/dist/js/{boot-CGaXUEer.mjs → boot-BOgO_O-u.mjs} +2573 -199
- package/dist/js/chat/tui/chunks/grammars/assets/bash.highlights.scm +56 -0
- package/dist/js/chat/tui/chunks/grammars/assets/bash.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/c.highlights.scm +81 -0
- package/dist/js/chat/tui/chunks/grammars/assets/c.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/cpp.highlights.scm +70 -0
- package/dist/js/chat/tui/chunks/grammars/assets/cpp.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/css.highlights.scm +76 -0
- package/dist/js/chat/tui/chunks/grammars/assets/css.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/go.highlights.scm +123 -0
- package/dist/js/chat/tui/chunks/grammars/assets/go.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/html.highlights.scm +13 -0
- package/dist/js/chat/tui/chunks/grammars/assets/html.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/java.highlights.scm +149 -0
- package/dist/js/chat/tui/chunks/grammars/assets/java.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/json.highlights.scm +16 -0
- package/dist/js/chat/tui/chunks/grammars/assets/json.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/lua.highlights.scm +201 -0
- package/dist/js/chat/tui/chunks/grammars/assets/lua.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/python.highlights.scm +137 -0
- package/dist/js/chat/tui/chunks/grammars/assets/python.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/ruby.highlights.scm +154 -0
- package/dist/js/chat/tui/chunks/grammars/assets/ruby.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/rust.highlights.scm +161 -0
- package/dist/js/chat/tui/chunks/grammars/assets/rust.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/toml.highlights.scm +53 -0
- package/dist/js/chat/tui/chunks/grammars/assets/toml.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/assets/yaml.highlights.scm +79 -0
- package/dist/js/chat/tui/chunks/grammars/assets/yaml.wasm +0 -0
- package/dist/js/chat/tui/chunks/grammars/grammars.ts +146 -0
- package/dist/js/{client-B3ZhhF7e.mjs → client-Dc7GZ3PG.mjs} +148 -8
- package/dist/js/client-DuwxEDG4.mjs +5 -0
- package/dist/js/daemon-Ch_MSDmQ.mjs +6 -0
- package/dist/js/daemon-Co4CtpXZ.mjs +607 -0
- package/dist/js/{theme-CuQhvqzN.mjs → install-BE0gnaLl.mjs} +256 -186
- package/dist/js/{print-BZH0b3KB.mjs → print-CPCFci60.mjs} +2 -2
- package/dist/js/{print-Bin4u16d.mjs → print-Cd-bVSEJ.mjs} +224 -10
- package/dist/js/{print-share-D7OSxvE2.mjs → print-share-7qfpQtWK.mjs} +3 -3
- package/dist/js/{raw-pty-GAvxm2ol.mjs → raw-pty-BcjbTjHJ.mjs} +1 -1
- package/dist/js/raw-pty-ChUHav4d.mjs +5 -0
- package/dist/js/{rest-CamHVOce.mjs → rest-BlN_uWmL.mjs} +150 -19
- package/dist/js/rest-CDTXCmUb.mjs +4 -0
- package/package.json +3 -1
- package/dist/js/api-CDTKq_5Q.mjs +0 -145
- package/dist/js/client-D6NAkL9e.mjs +0 -6
- package/dist/js/raw-pty-2_VA1kw_.mjs +0 -5
- package/dist/js/rest-COkLEZOB.mjs +0 -4
- package/dist/js/util-CeisaZVY.mjs +0 -15
- /package/dist/js/{client-CpEvH2Pq.mjs → client-DfcJFEbh.mjs} +0 -0
- /package/dist/js/{output-B4cW10Ph.mjs → output-DYzzdXYV.mjs} +0 -0
package/dist/js/bin.mjs
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
6
|
-
import { a as registerPortalDevice,
|
|
7
|
-
import {
|
|
2
|
+
import { C as setActiveWorkspace, S as listWorkspaces, T as version, _ as themes, b as getActiveWorkspaceId, o as brandHelpArt, t as installCrashHandler, w as name, x as getSessionIdentity, y as ensureActiveOrganization } from "./install-BE0gnaLl.mjs";
|
|
3
|
+
import { A as resolveAppUrl, C as getPromptHistoryPath, D as getStoredApiKeyId, E as getShareMachineDefault, F as saveConfig, I as saveSession, M as resolveManagementAuth, N as resolveSession, O as getStoredApiKeyWorkspaceName, R as setLastSeenVersion, S as getLastSeenVersion, _ as API_KEY_PREFIX, b as deleteConfig, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as resolveConfig, k as getUpdateCheckDisabled, v as DEFAULT_API_URL, x as getConfigPath } from "./print-Cd-bVSEJ.mjs";
|
|
4
|
+
import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
|
|
5
|
+
import { n as createRestClient, t as HttpError } from "./rest-BlN_uWmL.mjs";
|
|
6
|
+
import { a as registerPortalDevice, i as grantPortalAccess, n as fetchPortalDevices, o as revokePortalAccess, r as findThisDevice, s as machineIdentity } from "./client-Dc7GZ3PG.mjs";
|
|
7
|
+
import { c as PORTAL_DAEMON_FLAG, i as queryDaemonStatus, l as daemonPaths, n as ensureDaemonRunning, o as stopDaemon } from "./daemon-Co4CtpXZ.mjs";
|
|
8
|
+
import { t as SandboxStream } from "./client-DfcJFEbh.mjs";
|
|
8
9
|
import { hideBin } from "yargs/helpers";
|
|
9
10
|
import yargs from "yargs";
|
|
10
|
-
import { hostname } from "node:os";
|
|
11
|
+
import os, { hostname } from "node:os";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { err, ok } from "neverthrow";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
import open from "open";
|
|
16
|
+
import fs from "node:fs";
|
|
15
17
|
import { spawn, spawnSync } from "node:child_process";
|
|
16
18
|
import { createHash } from "node:crypto";
|
|
17
|
-
import fs from "node:fs";
|
|
18
19
|
import zlib from "node:zlib";
|
|
20
|
+
import fsp from "node:fs/promises";
|
|
19
21
|
import semver from "semver";
|
|
22
|
+
import { createInterface } from "node:readline";
|
|
20
23
|
|
|
21
|
-
//#region package.json
|
|
22
|
-
var name = "skydive-cli";
|
|
23
|
-
var version = "0.2.0";
|
|
24
|
-
|
|
25
|
-
//#endregion
|
|
26
24
|
//#region src/types.ts
|
|
27
25
|
const NON_INTERACTIVE_ENV_VARS = [
|
|
28
26
|
"CI",
|
|
@@ -715,6 +713,308 @@ const authCommand = {
|
|
|
715
713
|
handler: () => {}
|
|
716
714
|
};
|
|
717
715
|
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/commands/completion.ts
|
|
718
|
+
/**
|
|
719
|
+
* The argv sentinel yargs answers with completion candidates for a partially
|
|
720
|
+
* typed command line. The scripts below are the only callers; `bin.ts` also
|
|
721
|
+
* matches on it to keep notices off the user's prompt (see isCompletionProbe).
|
|
722
|
+
*/
|
|
723
|
+
const COMPLETION_PROBE_FLAG = "--get-yargs-completions";
|
|
724
|
+
const SUPPORTED_SHELLS = [
|
|
725
|
+
"bash",
|
|
726
|
+
"zsh",
|
|
727
|
+
"fish"
|
|
728
|
+
];
|
|
729
|
+
/**
|
|
730
|
+
* True when this invocation is a shell asking for completion candidates
|
|
731
|
+
* rather than a user running a command. Such a run must print candidates and
|
|
732
|
+
* nothing else: stderr is still a TTY inside a completion function, so an
|
|
733
|
+
* update notice would be drawn onto the prompt line, and the what's-new
|
|
734
|
+
* notice is one-shot — a TAB press would consume it unread.
|
|
735
|
+
*/
|
|
736
|
+
function isCompletionProbe(argv) {
|
|
737
|
+
return argv.includes(COMPLETION_PROBE_FLAG);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Blank the word the cursor is still on before yargs sees a probe.
|
|
741
|
+
*
|
|
742
|
+
* yargs' completion walk treats *every* token in the probe as a finished
|
|
743
|
+
* command, the word under the cursor included: it scans the args for one that
|
|
744
|
+
* names a command, descends into that command's builder, and answers from
|
|
745
|
+
* there. So a fully typed command name answers with nothing —
|
|
746
|
+
* `skydive conv<TAB>` descends into the `conv` alias of `conversations` and
|
|
747
|
+
* emits an empty list, which leaves an alias that can never be expanded and a
|
|
748
|
+
* `skydive agents<TAB>` that won't even add its trailing space. In bash it is
|
|
749
|
+
* worse than a no-op: `complete -o default` reads the empty list as "no
|
|
750
|
+
* candidates" and offers local *filenames* instead.
|
|
751
|
+
*
|
|
752
|
+
* The three scripts all send the partial word as the last token — that is
|
|
753
|
+
* yargs' wire format, not something we chose — so drop it here and let the
|
|
754
|
+
* shell filter the full candidate list by prefix. That is already what happens
|
|
755
|
+
* for any partial that doesn't happen to name a command (`skydive conve<TAB>`
|
|
756
|
+
* gets all 13 commands back today and bash/zsh/fish narrow them), so this only
|
|
757
|
+
* removes the special case, it doesn't change the contract.
|
|
758
|
+
*
|
|
759
|
+
* Two tokens are left alone:
|
|
760
|
+
* - one starting with `-`, because yargs uses exactly that to decide between
|
|
761
|
+
* offering options and offering subcommands, and
|
|
762
|
+
* - the command name itself, so a probe never degrades to an empty script name.
|
|
763
|
+
*/
|
|
764
|
+
function normalizeProbeArgv(argv) {
|
|
765
|
+
const flag = argv.indexOf(COMPLETION_PROBE_FLAG);
|
|
766
|
+
if (flag === -1 || argv.length - flag < 3) return argv;
|
|
767
|
+
const last = argv[argv.length - 1] ?? "";
|
|
768
|
+
if (last === "" || last.startsWith("-")) return argv;
|
|
769
|
+
return [...argv.slice(0, -1), ""];
|
|
770
|
+
}
|
|
771
|
+
const blockStart = (name) => `###-begin-${name}-completions-###`;
|
|
772
|
+
const blockEnd = (name) => `###-end-${name}-completions-###`;
|
|
773
|
+
/**
|
|
774
|
+
* Emit a completion script that asks the CLI itself for candidates on every
|
|
775
|
+
* TAB, rather than baking today's command tree into the script. bash and zsh
|
|
776
|
+
* are adapted from yargs' own templates
|
|
777
|
+
* (node_modules/yargs/build/lib/completion-templates.js); fish is
|
|
778
|
+
* hand-written, as yargs ships no fish template. Differences from yargs:
|
|
779
|
+
*
|
|
780
|
+
* - The shell is an explicit argument, not inferred from `$SHELL` at generate
|
|
781
|
+
* time, so `skydive completion zsh` is meaningful from any shell.
|
|
782
|
+
* - The script calls `skydive` off PATH instead of the absolute path of the
|
|
783
|
+
* generating process, which would go stale on every version bump under a
|
|
784
|
+
* version manager.
|
|
785
|
+
* - The probe's wire format is pinned. yargs picks it from `SHELL`/`ZSH_NAME`
|
|
786
|
+
* — plain lines normally, `value:description` when either says "zsh" — so
|
|
787
|
+
* a bash user whose login shell is zsh would otherwise get descriptions
|
|
788
|
+
* stuffed into COMPREPLY. bash asks for plain lines; zsh and fish ask for
|
|
789
|
+
* the descriptive format and render the descriptions.
|
|
790
|
+
*/
|
|
791
|
+
function renderCompletionScript(shell, commandName = "skydive") {
|
|
792
|
+
if (shell === "zsh") return zshScript(commandName);
|
|
793
|
+
if (shell === "fish") return fishScript(commandName);
|
|
794
|
+
return bashScript(commandName);
|
|
795
|
+
}
|
|
796
|
+
function bashScript(name) {
|
|
797
|
+
const fn = `_${name}_completions`;
|
|
798
|
+
return `${blockStart(name)}
|
|
799
|
+
#
|
|
800
|
+
# ${name} command completion script, generated by \`${name} completion bash\`.
|
|
801
|
+
# Installed by \`${name} completion install bash\`; edits inside this block
|
|
802
|
+
# are overwritten on the next install.
|
|
803
|
+
#
|
|
804
|
+
${fn}()
|
|
805
|
+
{
|
|
806
|
+
local cur_word args candidates
|
|
807
|
+
|
|
808
|
+
cur_word="\${COMP_WORDS[COMP_CWORD]}"
|
|
809
|
+
args=("\${COMP_WORDS[@]}")
|
|
810
|
+
|
|
811
|
+
candidates=$(SHELL=bash ZSH_NAME= ${name} ${COMPLETION_PROBE_FLAG} "\${args[@]}" 2>/dev/null)
|
|
812
|
+
|
|
813
|
+
COMPREPLY=( $(compgen -W "\${candidates}" -- "\${cur_word}") )
|
|
814
|
+
|
|
815
|
+
return 0
|
|
816
|
+
}
|
|
817
|
+
complete -o bashdefault -o default -F ${fn} ${name}
|
|
818
|
+
${blockEnd(name)}
|
|
819
|
+
`;
|
|
820
|
+
}
|
|
821
|
+
function zshScript(name) {
|
|
822
|
+
const fn = `_${name}_completions`;
|
|
823
|
+
return `${blockStart(name)}
|
|
824
|
+
#
|
|
825
|
+
# ${name} command completion script, generated by \`${name} completion zsh\`.
|
|
826
|
+
# Installed by \`${name} completion install zsh\`; edits inside this block
|
|
827
|
+
# are overwritten on the next install.
|
|
828
|
+
#
|
|
829
|
+
${fn}()
|
|
830
|
+
{
|
|
831
|
+
local reply
|
|
832
|
+
local si=$IFS
|
|
833
|
+
IFS=$'\\n' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" SHELL=zsh ${name} ${COMPLETION_PROBE_FLAG} "\${words[@]}" 2>/dev/null))
|
|
834
|
+
IFS=$si
|
|
835
|
+
_describe 'values' reply
|
|
836
|
+
}
|
|
837
|
+
# compdef only exists once compinit has run. Sourcing this block from a .zshrc
|
|
838
|
+
# that never calls compinit (or calls it later) would otherwise fail with
|
|
839
|
+
# "command not found: compdef".
|
|
840
|
+
#
|
|
841
|
+
# -i is load-bearing: a bare compinit that finds an insecure directory on fpath
|
|
842
|
+
# stops to ask "Ignore insecure directories and continue?", and where it cannot
|
|
843
|
+
# open the terminal it prints "compinit: initialization aborted" and defines no
|
|
844
|
+
# compdef at all -- so the line below fails and the user gets no completions.
|
|
845
|
+
# An insecure fpath entry is ordinary, not exotic: Homebrew's
|
|
846
|
+
# share/zsh/site-functions is writable by the installing user. -i skips those
|
|
847
|
+
# directories and carries on, which beats prompting from someone's shell
|
|
848
|
+
# startup.
|
|
849
|
+
if ! type compdef >/dev/null 2>&1; then
|
|
850
|
+
autoload -Uz compinit && compinit -i
|
|
851
|
+
fi
|
|
852
|
+
compdef ${fn} ${name}
|
|
853
|
+
${blockEnd(name)}
|
|
854
|
+
`;
|
|
855
|
+
}
|
|
856
|
+
function fishScript(name) {
|
|
857
|
+
const fn = `__${name}_completions`;
|
|
858
|
+
return `${blockStart(name)}
|
|
859
|
+
#
|
|
860
|
+
# ${name} command completion script, generated by \`${name} completion fish\`.
|
|
861
|
+
# Installed by \`${name} completion install fish\`; edits are overwritten on
|
|
862
|
+
# the next install.
|
|
863
|
+
#
|
|
864
|
+
function ${fn}
|
|
865
|
+
set -l tokens (commandline --current-process --tokenize --cut-at-cursor)
|
|
866
|
+
set -l current (commandline --current-token --cut-at-cursor)
|
|
867
|
+
|
|
868
|
+
for line in (ZSH_NAME=zsh ${name} ${COMPLETION_PROBE_FLAG} $tokens "$current" 2>/dev/null)
|
|
869
|
+
set -l masked (string replace --all -- '\\:' \\x01 $line)
|
|
870
|
+
set -l parts (string split --max 1 -- ':' $masked)
|
|
871
|
+
set -l value (string replace --all -- \\x01 ':' $parts[1])
|
|
872
|
+
|
|
873
|
+
# Filter by the partial token ourselves. fish 3.3 and older (Ubuntu
|
|
874
|
+
# 22.04 LTS ships 3.3.1) skip their own matching for any candidate that
|
|
875
|
+
# carries a description, so \`${name} ag<TAB>\` there offers every
|
|
876
|
+
# command instead of the one that matches -- and a description is the
|
|
877
|
+
# whole reason this script asks for the descriptive wire format. Newer
|
|
878
|
+
# fish filters, so this is a no-op on it. Compared by length rather
|
|
879
|
+
# than \`string match\` so a token holding a glob character stays
|
|
880
|
+
# literal. Prefix matching is also what bash's \`compgen -W\` and zsh's
|
|
881
|
+
# \`_describe\` do, so the three shells now agree.
|
|
882
|
+
if test (string sub --length (string length -- "$current") -- "$value") != "$current"
|
|
883
|
+
continue
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
if set -q parts[2]
|
|
887
|
+
printf '%s\\t%s\\n' $value (string replace --all -- \\x01 ':' $parts[2])
|
|
888
|
+
else
|
|
889
|
+
printf '%s\\n' $value
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# -f: only our candidates, no filename fallback. -k: keep the CLI's own order
|
|
895
|
+
# instead of fish's alphabetical sort.
|
|
896
|
+
complete -c ${name} -f -k -a '(${fn})'
|
|
897
|
+
${blockEnd(name)}
|
|
898
|
+
`;
|
|
899
|
+
}
|
|
900
|
+
const defaultInstallEnv = () => ({
|
|
901
|
+
home: os.homedir(),
|
|
902
|
+
platform: process.platform,
|
|
903
|
+
env: process.env,
|
|
904
|
+
exists: fs.existsSync
|
|
905
|
+
});
|
|
906
|
+
/**
|
|
907
|
+
* The shell to install for when the user didn't name one. `$SHELL` is the
|
|
908
|
+
* only signal available: a completion install runs as a child process, so the
|
|
909
|
+
* invoking shell's own variables (`$ZSH_VERSION`, `$FISH_VERSION`) aren't
|
|
910
|
+
* visible here.
|
|
911
|
+
*/
|
|
912
|
+
function detectShell(env = process.env) {
|
|
913
|
+
const shell = env["SHELL"];
|
|
914
|
+
if (!shell) return null;
|
|
915
|
+
const name = path.basename(shell);
|
|
916
|
+
return SUPPORTED_SHELLS.find((candidate) => name === candidate) ?? null;
|
|
917
|
+
}
|
|
918
|
+
function completionTarget(shell, deps = defaultInstallEnv()) {
|
|
919
|
+
const { home, platform, env, exists } = deps;
|
|
920
|
+
if (shell === "fish") {
|
|
921
|
+
const configHome = env["XDG_CONFIG_HOME"] || path.join(home, ".config");
|
|
922
|
+
return {
|
|
923
|
+
path: path.join(configHome, "fish", "completions", "skydive.fish"),
|
|
924
|
+
mode: "file"
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
if (shell === "zsh") {
|
|
928
|
+
const zdotdir = env["ZDOTDIR"] || home;
|
|
929
|
+
return {
|
|
930
|
+
path: path.join(zdotdir, ".zshrc"),
|
|
931
|
+
mode: "block"
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const rc = path.join(home, ".bashrc");
|
|
935
|
+
const profile = path.join(home, ".bash_profile");
|
|
936
|
+
const preferred = platform === "darwin" ? profile : rc;
|
|
937
|
+
const alternate = platform === "darwin" ? rc : profile;
|
|
938
|
+
if (!exists(preferred) && exists(alternate)) return {
|
|
939
|
+
path: alternate,
|
|
940
|
+
mode: "block"
|
|
941
|
+
};
|
|
942
|
+
return {
|
|
943
|
+
path: preferred,
|
|
944
|
+
mode: "block"
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* The startup file's contents with our block added or refreshed. Idempotent:
|
|
949
|
+
* a second install rewrites the block in place rather than appending a
|
|
950
|
+
* duplicate, which is also how an upgraded CLI ships a newer script.
|
|
951
|
+
*/
|
|
952
|
+
function spliceCompletionBlock(existing, script, commandName = "skydive") {
|
|
953
|
+
const start = existing.indexOf(blockStart(commandName));
|
|
954
|
+
const endMarker = blockEnd(commandName);
|
|
955
|
+
const end = existing.indexOf(endMarker);
|
|
956
|
+
if (start !== -1 && end > start) {
|
|
957
|
+
const before = existing.slice(0, start);
|
|
958
|
+
const after = existing.slice(end + endMarker.length).replace(/^\n/, "");
|
|
959
|
+
return `${before}${script.trimEnd()}\n${after}`;
|
|
960
|
+
}
|
|
961
|
+
return `${existing}${existing === "" || existing.endsWith("\n\n") ? "" : "\n"}\n${script}`;
|
|
962
|
+
}
|
|
963
|
+
function installCompletionScript(shell, deps = defaultInstallEnv()) {
|
|
964
|
+
const target = completionTarget(shell, deps);
|
|
965
|
+
const script = renderCompletionScript(shell);
|
|
966
|
+
const existing = deps.exists(target.path) ? fs.readFileSync(target.path, "utf8") : "";
|
|
967
|
+
const contents = target.mode === "file" ? script : spliceCompletionBlock(existing, script);
|
|
968
|
+
fs.mkdirSync(path.dirname(target.path), { recursive: true });
|
|
969
|
+
fs.writeFileSync(target.path, contents, "utf8");
|
|
970
|
+
return {
|
|
971
|
+
shell,
|
|
972
|
+
path: target.path,
|
|
973
|
+
updated: existing.includes(blockStart("skydive"))
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
/** What the user has to do before completions work in the shell they're in. */
|
|
977
|
+
function activationHint(result) {
|
|
978
|
+
return result.shell === "fish" ? "Open a new fish shell to pick them up (fish autoloads completions)." : `Open a new shell, or run: source ${result.path}`;
|
|
979
|
+
}
|
|
980
|
+
const installCommand$1 = {
|
|
981
|
+
command: "install [shell]",
|
|
982
|
+
describe: "Install completions into your shell's startup file",
|
|
983
|
+
builder: (y) => y.positional("shell", {
|
|
984
|
+
type: "string",
|
|
985
|
+
describe: "Shell to install for (default: $SHELL)"
|
|
986
|
+
}).example("skydive completion install", "Install for the current shell").example("skydive completion install fish", "Install for a named shell"),
|
|
987
|
+
handler: async (argv) => {
|
|
988
|
+
const shell = argv.shell ? SUPPORTED_SHELLS.find((candidate) => candidate === argv.shell) : detectShell();
|
|
989
|
+
if (!shell) throw new Error(argv.shell ? `No completions for ${argv.shell}. Supported: ${SUPPORTED_SHELLS.join(", ")}.` : `Could not tell which shell to install for from $SHELL. Name one: ${SUPPORTED_SHELLS.join(", ")}.`);
|
|
990
|
+
let result;
|
|
991
|
+
try {
|
|
992
|
+
result = installCompletionScript(shell);
|
|
993
|
+
} catch (error) {
|
|
994
|
+
throw new Error(`Could not write ${shell} completions: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
995
|
+
}
|
|
996
|
+
if (argv.json) {
|
|
997
|
+
output(argv, result);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
console.log(`${result.updated ? "Updated" : "Installed"} ${result.shell} completions in ${result.path}`);
|
|
1001
|
+
if (!argv.quiet) console.log(activationHint(result));
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
const completionCommand = {
|
|
1005
|
+
command: "completion [shell]",
|
|
1006
|
+
describe: "Print the shell completion script for bash, zsh, or fish",
|
|
1007
|
+
builder: (y) => y.positional("shell", {
|
|
1008
|
+
type: "string",
|
|
1009
|
+
choices: SUPPORTED_SHELLS,
|
|
1010
|
+
describe: "Shell to generate completions for"
|
|
1011
|
+
}).command(installCommand$1).example("skydive completion install", "Set up completions for your shell").example("eval \"$(skydive completion zsh)\"", "Enable completions for this shell only"),
|
|
1012
|
+
handler: async (argv) => {
|
|
1013
|
+
if (!argv.shell) throw new Error(`Specify a shell (${SUPPORTED_SHELLS.join(", ")}), or run \`skydive completion install\` to set up $SHELL.`);
|
|
1014
|
+
process.stdout.write(renderCompletionScript(argv.shell));
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
|
|
718
1018
|
//#endregion
|
|
719
1019
|
//#region src/commands/session.ts
|
|
720
1020
|
/**
|
|
@@ -1200,7 +1500,7 @@ const importCommand = {
|
|
|
1200
1500
|
process.exit(1);
|
|
1201
1501
|
}
|
|
1202
1502
|
}
|
|
1203
|
-
const { runChat } = await import("./boot-
|
|
1503
|
+
const { runChat } = await import("./boot-BOgO_O-u.mjs");
|
|
1204
1504
|
await runChat({
|
|
1205
1505
|
appUrl,
|
|
1206
1506
|
sessionToken: session.value.sessionToken,
|
|
@@ -1209,6 +1509,7 @@ const importCommand = {
|
|
|
1209
1509
|
notifications: true,
|
|
1210
1510
|
agentSelector: argv.agent ?? null,
|
|
1211
1511
|
conversationId: null,
|
|
1512
|
+
newConversation: false,
|
|
1212
1513
|
seedPrompt: buildImportSeedPrompt(process.cwd())
|
|
1213
1514
|
});
|
|
1214
1515
|
}
|
|
@@ -1226,7 +1527,7 @@ async function runImportPrintMode({ argv, appUrl }) {
|
|
|
1226
1527
|
printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1227
1528
|
process.exit(1);
|
|
1228
1529
|
}
|
|
1229
|
-
const { connectMachineShare } = await import("./print-share-
|
|
1530
|
+
const { connectMachineShare } = await import("./print-share-7qfpQtWK.mjs");
|
|
1230
1531
|
const machineShare = await connectMachineShare({
|
|
1231
1532
|
appUrl,
|
|
1232
1533
|
sessionToken: session.value.sessionToken,
|
|
@@ -1234,7 +1535,7 @@ async function runImportPrintMode({ argv, appUrl }) {
|
|
|
1234
1535
|
});
|
|
1235
1536
|
const extra = (argv.print ?? "").trim();
|
|
1236
1537
|
const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
|
|
1237
|
-
const { runPrint } = await import("./print-
|
|
1538
|
+
const { runPrint } = await import("./print-CPCFci60.mjs");
|
|
1238
1539
|
try {
|
|
1239
1540
|
const result = await runPrint({
|
|
1240
1541
|
appUrl,
|
|
@@ -1449,6 +1750,97 @@ const secretsCommand = {
|
|
|
1449
1750
|
handler: () => {}
|
|
1450
1751
|
};
|
|
1451
1752
|
|
|
1753
|
+
//#endregion
|
|
1754
|
+
//#region src/auth/agent-workspace.ts
|
|
1755
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1756
|
+
/**
|
|
1757
|
+
* Cap on concurrent per-workspace probes. An account can be in a lot of
|
|
1758
|
+
* workspaces; firing one request per workspace all at once is a burst the
|
|
1759
|
+
* API (and the user's uplink) doesn't need. Four keeps the common case
|
|
1760
|
+
* (a handful of workspaces) effectively parallel while bounding the worst.
|
|
1761
|
+
*/
|
|
1762
|
+
const PROBE_CONCURRENCY = 4;
|
|
1763
|
+
/**
|
|
1764
|
+
* `Promise.all` with at most `limit` tasks in flight — a dependency-free
|
|
1765
|
+
* stand-in for p-queue sized to this one call site. Results keep input
|
|
1766
|
+
* order.
|
|
1767
|
+
*/
|
|
1768
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1769
|
+
const results = [];
|
|
1770
|
+
const queue = items.entries();
|
|
1771
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1772
|
+
for (const [index, item] of queue) results[index] = await fn(item);
|
|
1773
|
+
});
|
|
1774
|
+
await Promise.all(workers);
|
|
1775
|
+
return results;
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Whether the agent is visible in one workspace, via the typed rest client
|
|
1779
|
+
* scoped with `x-workspace-id` (a per-request override — the session's
|
|
1780
|
+
* persisted active workspace is untouched, so probing is side-effect free).
|
|
1781
|
+
* A miss (404 unknown/hidden, 403 other org) or a network failure is simply
|
|
1782
|
+
* "not here" — if the API is truly unreachable the main request path fails
|
|
1783
|
+
* right after with its own (better) error.
|
|
1784
|
+
*/
|
|
1785
|
+
async function probeAgent({ appUrl, sessionToken, agentId, workspaceId }) {
|
|
1786
|
+
const client = createRestClient({
|
|
1787
|
+
appUrl,
|
|
1788
|
+
sessionToken,
|
|
1789
|
+
...workspaceId ? { workspaceId } : {}
|
|
1790
|
+
});
|
|
1791
|
+
try {
|
|
1792
|
+
return { name: (await client.getAgent({ agentId })).name };
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
if (error instanceof HttpError) return null;
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async function resolveAgentWorkspace({ appUrl, sessionToken, agentSelector }) {
|
|
1799
|
+
if (!agentSelector || !uuidPattern.test(agentSelector)) return { kind: "active" };
|
|
1800
|
+
if (await probeAgent({
|
|
1801
|
+
appUrl,
|
|
1802
|
+
sessionToken,
|
|
1803
|
+
agentId: agentSelector,
|
|
1804
|
+
workspaceId: null
|
|
1805
|
+
})) return { kind: "active" };
|
|
1806
|
+
const [workspaces, activeId] = await Promise.all([listWorkspaces({
|
|
1807
|
+
appUrl,
|
|
1808
|
+
sessionToken
|
|
1809
|
+
}), getActiveWorkspaceId({
|
|
1810
|
+
appUrl,
|
|
1811
|
+
sessionToken
|
|
1812
|
+
})]);
|
|
1813
|
+
if (workspaces.isErr()) return {
|
|
1814
|
+
kind: "list-failed",
|
|
1815
|
+
message: workspaces.error.message
|
|
1816
|
+
};
|
|
1817
|
+
const found = (await mapWithConcurrency(workspaces.value.filter((w) => activeId.isErr() || w.id !== activeId.value), PROBE_CONCURRENCY, async (workspace) => ({
|
|
1818
|
+
workspace,
|
|
1819
|
+
hit: await probeAgent({
|
|
1820
|
+
appUrl,
|
|
1821
|
+
sessionToken,
|
|
1822
|
+
agentId: agentSelector,
|
|
1823
|
+
workspaceId: workspace.id
|
|
1824
|
+
})
|
|
1825
|
+
}))).find((p) => p.hit !== null);
|
|
1826
|
+
if (!found?.hit) return { kind: "not-found" };
|
|
1827
|
+
const switched = await setActiveWorkspace({
|
|
1828
|
+
appUrl,
|
|
1829
|
+
sessionToken,
|
|
1830
|
+
organizationId: found.workspace.id
|
|
1831
|
+
});
|
|
1832
|
+
if (switched.isErr()) return {
|
|
1833
|
+
kind: "switch-failed",
|
|
1834
|
+
workspace: found.workspace,
|
|
1835
|
+
message: switched.error.message
|
|
1836
|
+
};
|
|
1837
|
+
return {
|
|
1838
|
+
kind: "switched",
|
|
1839
|
+
workspace: found.workspace,
|
|
1840
|
+
agentName: found.hit.name
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1452
1844
|
//#endregion
|
|
1453
1845
|
//#region src/commands/chat.ts
|
|
1454
1846
|
const chatCommand = {
|
|
@@ -1465,6 +1857,10 @@ const chatCommand = {
|
|
|
1465
1857
|
alias: "resume",
|
|
1466
1858
|
type: "string",
|
|
1467
1859
|
describe: "Continue an existing conversation by id. With -p, the one-shot lands in that conversation; without -p, the TUI opens directly into it (the agent is resolved from the conversation, no --agent needed)."
|
|
1860
|
+
}).option("new", {
|
|
1861
|
+
type: "boolean",
|
|
1862
|
+
default: false,
|
|
1863
|
+
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."
|
|
1468
1864
|
}).option("share-machine", {
|
|
1469
1865
|
type: "boolean",
|
|
1470
1866
|
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."
|
|
@@ -1475,7 +1871,7 @@ const chatCommand = {
|
|
|
1475
1871
|
type: "boolean",
|
|
1476
1872
|
default: true,
|
|
1477
1873
|
describe: "Show a desktop notification when a run finishes or needs your input while this terminal is unfocused (use --no-notify to disable)"
|
|
1478
|
-
}).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)").example("skydive chat --resume 0197e0f3-…", "Reopen a previous conversation in the TUI (the id is printed when you quit a chat)"),
|
|
1874
|
+
}).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)").example("skydive chat --agent grace --new", "Start a fresh conversation with Grace directly (skips the conversation list)").example("skydive chat --resume 0197e0f3-…", "Reopen a previous conversation in the TUI (the id is printed when you quit a chat)"),
|
|
1479
1875
|
handler: async (argv) => {
|
|
1480
1876
|
const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
|
|
1481
1877
|
if (argv.print !== void 0) {
|
|
@@ -1511,7 +1907,12 @@ const chatCommand = {
|
|
|
1511
1907
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
1512
1908
|
process.exit(1);
|
|
1513
1909
|
}
|
|
1514
|
-
|
|
1910
|
+
await ensureAgentWorkspace({
|
|
1911
|
+
appUrl,
|
|
1912
|
+
sessionToken: session.value.sessionToken,
|
|
1913
|
+
agentSelector: argv.agent ?? null
|
|
1914
|
+
});
|
|
1915
|
+
const { runChat } = await import("./boot-BOgO_O-u.mjs");
|
|
1515
1916
|
await runChat({
|
|
1516
1917
|
appUrl,
|
|
1517
1918
|
sessionToken: session.value.sessionToken,
|
|
@@ -1521,6 +1922,7 @@ const chatCommand = {
|
|
|
1521
1922
|
notifications: argv.notify,
|
|
1522
1923
|
agentSelector: argv.agent ?? null,
|
|
1523
1924
|
conversationId: argv.conversation ?? null,
|
|
1925
|
+
newConversation: resolveNewConversation(argv),
|
|
1524
1926
|
seedPrompt: null
|
|
1525
1927
|
});
|
|
1526
1928
|
}
|
|
@@ -1533,13 +1935,55 @@ const chatCommand = {
|
|
|
1533
1935
|
function resolveShareMachine(argv) {
|
|
1534
1936
|
return argv["share-machine"] ?? getShareMachineDefault();
|
|
1535
1937
|
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Whether this invocation should boot straight into a fresh conversation:
|
|
1940
|
+
* `--new` requests it, but `--resume <id>` always wins (it pins an existing
|
|
1941
|
+
* conversation, so "new" is meaningless alongside it).
|
|
1942
|
+
*/
|
|
1943
|
+
function resolveNewConversation(argv) {
|
|
1944
|
+
return argv.new === true && !argv.conversation;
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Cross-workspace `--agent <uuid>` handling shared by the TUI and -p paths:
|
|
1948
|
+
* on a hit in another workspace, switch to it and say so on stderr (stdout
|
|
1949
|
+
* may carry the -p reply/JSON envelope); when the id matches no workspace on
|
|
1950
|
+
* the account, fail with the two realistic fixes instead of the misleading
|
|
1951
|
+
* "no agent matches" roster error.
|
|
1952
|
+
*/
|
|
1953
|
+
async function ensureAgentWorkspace({ appUrl, sessionToken, agentSelector }) {
|
|
1954
|
+
const resolution = await resolveAgentWorkspace({
|
|
1955
|
+
appUrl,
|
|
1956
|
+
sessionToken,
|
|
1957
|
+
agentSelector
|
|
1958
|
+
});
|
|
1959
|
+
if (resolution.kind === "active") return;
|
|
1960
|
+
if (resolution.kind === "switched") {
|
|
1961
|
+
console.error(`Switched workspace to ${resolution.workspace.name} (${resolution.workspace.slug}) — ${resolution.agentName} lives there.`);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (resolution.kind === "list-failed") {
|
|
1965
|
+
printError(`No agent with id ${agentSelector} in the current workspace, and listing your other workspaces failed (${resolution.message}), so it couldn't be searched for elsewhere. Retry, or run \`skydive workspace switch <slug>\` if you know where it lives.`);
|
|
1966
|
+
process.exit(1);
|
|
1967
|
+
}
|
|
1968
|
+
if (resolution.kind === "switch-failed") {
|
|
1969
|
+
printError(`Agent found in workspace ${resolution.workspace.name}, but switching to it failed: ${resolution.message}. Run \`skydive workspace switch ${resolution.workspace.slug}\` and retry.`);
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1972
|
+
printError(`No agent with id ${agentSelector} in any workspace on this account. It may belong to a different account — run \`skydive auth login\` with the right one — or the agent may have been archived.`);
|
|
1973
|
+
process.exit(1);
|
|
1974
|
+
}
|
|
1536
1975
|
async function runPrintMode({ argv, appUrl }) {
|
|
1537
1976
|
const session = resolveSession({ appUrl });
|
|
1538
1977
|
if (session.isErr()) {
|
|
1539
1978
|
printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1540
1979
|
process.exit(1);
|
|
1541
1980
|
}
|
|
1542
|
-
const { runPrint, readStdin } = await import("./print-
|
|
1981
|
+
const { runPrint, readStdin } = await import("./print-CPCFci60.mjs");
|
|
1982
|
+
await ensureAgentWorkspace({
|
|
1983
|
+
appUrl,
|
|
1984
|
+
sessionToken: session.value.sessionToken,
|
|
1985
|
+
agentSelector: argv.agent ?? null
|
|
1986
|
+
});
|
|
1543
1987
|
let prompt = (argv.print ?? "").trim();
|
|
1544
1988
|
if (!prompt) {
|
|
1545
1989
|
if (process.stdin.isTTY) {
|
|
@@ -1554,7 +1998,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
1554
1998
|
}
|
|
1555
1999
|
let machineShare = null;
|
|
1556
2000
|
if (resolveShareMachine(argv)) {
|
|
1557
|
-
const { connectMachineShare } = await import("./print-share-
|
|
2001
|
+
const { connectMachineShare } = await import("./print-share-7qfpQtWK.mjs");
|
|
1558
2002
|
machineShare = await connectMachineShare({
|
|
1559
2003
|
appUrl,
|
|
1560
2004
|
sessionToken: session.value.sessionToken,
|
|
@@ -1613,7 +2057,7 @@ const getCommand = {
|
|
|
1613
2057
|
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1614
2058
|
process.exit(1);
|
|
1615
2059
|
}
|
|
1616
|
-
const { messageGet } = await import("./print-
|
|
2060
|
+
const { messageGet } = await import("./print-CPCFci60.mjs");
|
|
1617
2061
|
try {
|
|
1618
2062
|
const result = await messageGet({
|
|
1619
2063
|
appUrl,
|
|
@@ -1810,7 +2254,7 @@ const switchCommand = {
|
|
|
1810
2254
|
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.");
|
|
1811
2255
|
process.exit(1);
|
|
1812
2256
|
}
|
|
1813
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
2257
|
+
const { runWorkspacePicker } = await import("./boot-BOgO_O-u.mjs");
|
|
1814
2258
|
await runWorkspacePicker(session);
|
|
1815
2259
|
return;
|
|
1816
2260
|
}
|
|
@@ -1888,7 +2332,7 @@ const openCommand = {
|
|
|
1888
2332
|
const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
1889
2333
|
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
1890
2334
|
const { machineName } = machineIdentity();
|
|
1891
|
-
const { PortalClient } = await import("./client-
|
|
2335
|
+
const { PortalClient } = await import("./client-DuwxEDG4.mjs");
|
|
1892
2336
|
let lastLine = "";
|
|
1893
2337
|
let signalConnected;
|
|
1894
2338
|
const connected = new Promise((resolve) => {
|
|
@@ -1897,7 +2341,7 @@ const openCommand = {
|
|
|
1897
2341
|
const client = new PortalClient({
|
|
1898
2342
|
appUrl: session.appUrl,
|
|
1899
2343
|
sessionToken: session.sessionToken,
|
|
1900
|
-
cwd,
|
|
2344
|
+
resolveCwd: () => cwd,
|
|
1901
2345
|
onState: (state) => {
|
|
1902
2346
|
if (state.status === "connected") signalConnected();
|
|
1903
2347
|
const line = state.status === "error" ? `portal: connection error: ${state.error ?? "unknown"} — retrying` : `portal: ${state.status}`;
|
|
@@ -2012,10 +2456,111 @@ function buildDeviceTable(devices, agents, thisMachineName) {
|
|
|
2012
2456
|
])
|
|
2013
2457
|
};
|
|
2014
2458
|
}
|
|
2459
|
+
/**
|
|
2460
|
+
* `skydive portal daemon <cmd>` — operator control of the per-host portal daemon
|
|
2461
|
+
* (the process that owns the single portal connection and routes exec per
|
|
2462
|
+
* conversation). Distinct from the agent-facing `platform portal`: this manages
|
|
2463
|
+
* the local daemon a human runs, so it lives on the skydive CLI only.
|
|
2464
|
+
*/
|
|
2465
|
+
const daemonStatusCommand = {
|
|
2466
|
+
command: "status",
|
|
2467
|
+
describe: "Show the portal daemon: running state, connection, and routing map",
|
|
2468
|
+
handler: async (argv) => {
|
|
2469
|
+
const status = await queryDaemonStatus(requireSession(argv).appUrl);
|
|
2470
|
+
if (argv.json) {
|
|
2471
|
+
output(argv, status ?? { running: false });
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
if (!status) {
|
|
2475
|
+
console.log("portal daemon: not running");
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
const cwdCount = Object.keys(status.cwds).length;
|
|
2479
|
+
console.log(`portal daemon: running (pid ${status.pid})`);
|
|
2480
|
+
console.log(` app: ${status.appUrl}`);
|
|
2481
|
+
console.log(` connection: ${status.portal.status}${status.portal.error ? ` (${status.portal.error})` : ""}`);
|
|
2482
|
+
if (status.portal.machineName) console.log(` machine: ${status.portal.friendlyName}`);
|
|
2483
|
+
console.log(` attached: ${status.clientCount} CLI session(s)`);
|
|
2484
|
+
console.log(` granted: ${status.portal.grantedAgentIds.length} agent(s)`);
|
|
2485
|
+
console.log(` routing: ${cwdCount} conversation(s)`);
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
const daemonStopCommand = {
|
|
2489
|
+
command: "stop",
|
|
2490
|
+
describe: "Stop the portal daemon, cutting every attached session and live exec/tunnel",
|
|
2491
|
+
handler: async (argv) => {
|
|
2492
|
+
const result = await stopDaemon(requireSession(argv).appUrl);
|
|
2493
|
+
if (argv.json) {
|
|
2494
|
+
output(argv, { result });
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
console.log(result === "not-running" ? "portal daemon: not running" : result === "failed" ? "portal daemon: could not be stopped" : `portal daemon: stopped${result === "killed" ? " (force-killed)" : ""}`);
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
const daemonStartCommand = {
|
|
2501
|
+
command: "start",
|
|
2502
|
+
describe: "Start the portal daemon if it is not already running",
|
|
2503
|
+
handler: async (argv) => {
|
|
2504
|
+
const session = requireSession(argv);
|
|
2505
|
+
await ensureDaemonRunning(session.appUrl);
|
|
2506
|
+
const status = await queryDaemonStatus(session.appUrl);
|
|
2507
|
+
if (argv.json) {
|
|
2508
|
+
output(argv, {
|
|
2509
|
+
running: !!status,
|
|
2510
|
+
pid: status?.pid ?? null
|
|
2511
|
+
});
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
console.log(status ? `portal daemon: running (pid ${status.pid})` : "portal daemon: failed to start");
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
const daemonRestartCommand = {
|
|
2518
|
+
command: "restart",
|
|
2519
|
+
describe: "Stop and restart the portal daemon (e.g. to pick up a new build)",
|
|
2520
|
+
handler: async (argv) => {
|
|
2521
|
+
const session = requireSession(argv);
|
|
2522
|
+
await stopDaemon(session.appUrl);
|
|
2523
|
+
await ensureDaemonRunning(session.appUrl);
|
|
2524
|
+
const status = await queryDaemonStatus(session.appUrl);
|
|
2525
|
+
if (argv.json) {
|
|
2526
|
+
output(argv, {
|
|
2527
|
+
running: !!status,
|
|
2528
|
+
pid: status?.pid ?? null
|
|
2529
|
+
});
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
console.log(status ? `portal daemon: restarted (pid ${status.pid})` : "portal daemon: failed to restart");
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
const daemonLogsCommand = {
|
|
2536
|
+
command: "logs",
|
|
2537
|
+
describe: "Print the portal daemon's log",
|
|
2538
|
+
builder: (y) => y.option("follow", {
|
|
2539
|
+
alias: "f",
|
|
2540
|
+
type: "boolean",
|
|
2541
|
+
default: false,
|
|
2542
|
+
describe: "Follow the log (like tail -f)"
|
|
2543
|
+
}),
|
|
2544
|
+
handler: async (argv) => {
|
|
2545
|
+
const { logPath } = daemonPaths(requireSession(argv).appUrl);
|
|
2546
|
+
const child = spawn("tail", argv.follow === true ? ["-f", logPath] : [
|
|
2547
|
+
"-n",
|
|
2548
|
+
"200",
|
|
2549
|
+
logPath
|
|
2550
|
+
], { stdio: "inherit" });
|
|
2551
|
+
await new Promise((resolve) => child.on("close", () => resolve()));
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
const daemonCommand = {
|
|
2555
|
+
command: "daemon",
|
|
2556
|
+
describe: "Control the local portal daemon (status, stop, start, restart, logs)",
|
|
2557
|
+
builder: (y) => y.command(daemonStatusCommand).command(daemonStopCommand).command(daemonStartCommand).command(daemonRestartCommand).command(daemonLogsCommand).demandCommand(1, "Specify a subcommand: status, stop, start, restart, logs"),
|
|
2558
|
+
handler: () => {}
|
|
2559
|
+
};
|
|
2015
2560
|
const portalCommand = {
|
|
2016
2561
|
command: "portal",
|
|
2017
2562
|
describe: "Open this machine's portal to agents and manage their access",
|
|
2018
|
-
builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status"),
|
|
2563
|
+
builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, daemon"),
|
|
2019
2564
|
handler: () => {}
|
|
2020
2565
|
};
|
|
2021
2566
|
|
|
@@ -2060,8 +2605,8 @@ const sandboxCommand = {
|
|
|
2060
2605
|
}).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`"),
|
|
2061
2606
|
handler: async (argv) => {
|
|
2062
2607
|
const session = requireSession(argv);
|
|
2063
|
-
const { createRestClient } = await import("./rest-
|
|
2064
|
-
const { resolveAgent } = await import("./print-
|
|
2608
|
+
const { createRestClient } = await import("./rest-CDTXCmUb.mjs");
|
|
2609
|
+
const { resolveAgent } = await import("./print-CPCFci60.mjs");
|
|
2065
2610
|
const client = createRestClient({
|
|
2066
2611
|
appUrl: session.appUrl,
|
|
2067
2612
|
sessionToken: session.sessionToken
|
|
@@ -2130,7 +2675,7 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2130
2675
|
return 1;
|
|
2131
2676
|
}
|
|
2132
2677
|
console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
|
|
2133
|
-
const { runRawPtyPassthrough } = await import("./raw-pty-
|
|
2678
|
+
const { runRawPtyPassthrough } = await import("./raw-pty-ChUHav4d.mjs");
|
|
2134
2679
|
const result = await runRawPtyPassthrough({
|
|
2135
2680
|
stdin: process.stdin,
|
|
2136
2681
|
stdout: process.stdout,
|
|
@@ -2142,10 +2687,679 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2142
2687
|
return result.code;
|
|
2143
2688
|
}
|
|
2144
2689
|
|
|
2690
|
+
//#endregion
|
|
2691
|
+
//#region src/update-check/manifest.ts
|
|
2692
|
+
/**
|
|
2693
|
+
* The release manifest the binary pipeline publishes
|
|
2694
|
+
* (`.github/workflows/release-skydive-cli-binaries.yml`) and the api serves
|
|
2695
|
+
* (`apps/anyone/api/src/routes/cli-releases.ts`). The update *check* only
|
|
2696
|
+
* needs `version` (see ./sources.ts), but the self-updater
|
|
2697
|
+
* (`../commands/update.ts`) needs the per-target byte pointer + integrity
|
|
2698
|
+
* digest too, so the full shape lives here and both consume it.
|
|
2699
|
+
*
|
|
2700
|
+
* Kept deliberately in sync with the api's `manifestSchema`: a field the api
|
|
2701
|
+
* validates but we drop here is fine, but a field we *require* that the api
|
|
2702
|
+
* doesn't emit would make every install fail — so required fields mirror the
|
|
2703
|
+
* api exactly (name, version, channel, per-target key/sha256/size).
|
|
2704
|
+
*/
|
|
2705
|
+
const releaseManifestSchema = z.object({
|
|
2706
|
+
name: z.string().min(1),
|
|
2707
|
+
version: z.string().min(1),
|
|
2708
|
+
channel: z.string().min(1),
|
|
2709
|
+
pubDate: z.string().optional(),
|
|
2710
|
+
targets: z.record(z.object({
|
|
2711
|
+
key: z.string().min(1),
|
|
2712
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
2713
|
+
size: z.number().int().positive()
|
|
2714
|
+
}))
|
|
2715
|
+
});
|
|
2716
|
+
/**
|
|
2717
|
+
* The four release targets the pipeline builds. Named `<os>-<arch>` to match
|
|
2718
|
+
* the manifest's `targets` keys and the install script's `$OS-$ARCH`.
|
|
2719
|
+
*/
|
|
2720
|
+
const RELEASE_TARGETS = [
|
|
2721
|
+
"darwin-arm64",
|
|
2722
|
+
"darwin-x64",
|
|
2723
|
+
"linux-arm64",
|
|
2724
|
+
"linux-x64"
|
|
2725
|
+
];
|
|
2726
|
+
function isReleaseTarget(value) {
|
|
2727
|
+
return RELEASE_TARGETS.some((target) => target === value);
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Map this host's `process.platform` / `process.arch` to a release target,
|
|
2731
|
+
* or null when we don't publish a binary for it. Mirrors the install
|
|
2732
|
+
* script's `uname -s` / `uname -m` normalization (arm64/aarch64 -> arm64,
|
|
2733
|
+
* x86_64/amd64 -> x64) so a CLI self-update resolves the same object a fresh
|
|
2734
|
+
* `curl | sh` install would.
|
|
2735
|
+
*/
|
|
2736
|
+
function hostReleaseTarget(platform, arch) {
|
|
2737
|
+
const os = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : null;
|
|
2738
|
+
if (!os) return null;
|
|
2739
|
+
const cpu = arch === "arm64" || arch === "aarch64" ? "arm64" : arch === "x64" || arch === "x86_64" || arch === "amd64" ? "x64" : null;
|
|
2740
|
+
if (!cpu) return null;
|
|
2741
|
+
const target = `${os}-${cpu}`;
|
|
2742
|
+
return isReleaseTarget(target) ? target : null;
|
|
2743
|
+
}
|
|
2744
|
+
function lookupTarget(manifest, platform, arch) {
|
|
2745
|
+
const target = hostReleaseTarget(platform, arch);
|
|
2746
|
+
if (!target) return {
|
|
2747
|
+
ok: false,
|
|
2748
|
+
reason: "unsupported-host",
|
|
2749
|
+
target: null
|
|
2750
|
+
};
|
|
2751
|
+
const entry = manifest.targets[target];
|
|
2752
|
+
if (!entry) return {
|
|
2753
|
+
ok: false,
|
|
2754
|
+
reason: "missing-from-manifest",
|
|
2755
|
+
target
|
|
2756
|
+
};
|
|
2757
|
+
return {
|
|
2758
|
+
ok: true,
|
|
2759
|
+
target,
|
|
2760
|
+
entry
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
//#endregion
|
|
2765
|
+
//#region src/update-check/versions.ts
|
|
2766
|
+
/** Canary builds carry a prerelease suffix (`X.Y.Z-beta.N`, synthesized in
|
|
2767
|
+
* CI); a plain semver is a stable release. An unparseable version defaults
|
|
2768
|
+
* to stable. */
|
|
2769
|
+
function resolveChannel(version) {
|
|
2770
|
+
return (semver.prerelease(version, { loose: true })?.length ?? 0) > 0 ? "canary" : "stable";
|
|
2771
|
+
}
|
|
2772
|
+
/** npm dist-tag for a channel (see release-skydive-cli.yml). */
|
|
2773
|
+
function distTagForChannel(channel) {
|
|
2774
|
+
return channel === "canary" ? "beta" : "latest";
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* True when `candidate` is a strictly newer release than `current`, per
|
|
2778
|
+
* semver precedence (the `semver` package, including prerelease ordering).
|
|
2779
|
+
* Unparseable input is never newer, so garbage from the registry can't
|
|
2780
|
+
* produce a notice.
|
|
2781
|
+
*/
|
|
2782
|
+
function isNewerVersion(candidate, current) {
|
|
2783
|
+
if (!semver.valid(candidate, { loose: true })) return false;
|
|
2784
|
+
if (!semver.valid(current, { loose: true })) return false;
|
|
2785
|
+
return semver.gt(candidate, current, { loose: true });
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
//#endregion
|
|
2789
|
+
//#region src/update-check/install.ts
|
|
2790
|
+
/**
|
|
2791
|
+
* The self-updater for compiled-binary installs. The update *check*
|
|
2792
|
+
* (./index.ts) is notify-only; this is what a user-invoked `skydive update`
|
|
2793
|
+
* (../commands/update.ts) runs to actually replace the binary.
|
|
2794
|
+
*
|
|
2795
|
+
* package-manager installs are never self-mutated here — that's the install
|
|
2796
|
+
* mode's own tool's job, and silently reaching into a global npm/yarn/pnpm
|
|
2797
|
+
* prefix is exactly the footgun the existing notice policy avoids. They still
|
|
2798
|
+
* go through the same {@link planUpdate} version comparison (an install that
|
|
2799
|
+
* skips it has no way to know it is already current); only the *action*
|
|
2800
|
+
* differs — the command prints, or offers to run, the upgrade command.
|
|
2801
|
+
*
|
|
2802
|
+
* The dangerous part is replacing a running executable in place, so the flow
|
|
2803
|
+
* is split: a pure {@link planUpdate} decides *whether* and *what* (testable
|
|
2804
|
+
* with no IO), and {@link downloadAndVerify} / {@link applyBinary} do the IO
|
|
2805
|
+
* with integrity checks and an atomic rename.
|
|
2806
|
+
*/
|
|
2807
|
+
/** Cap so a wrong URL or a hung CDN can't wedge `skydive update` forever. */
|
|
2808
|
+
const DOWNLOAD_TIMEOUT_MS = 6e4;
|
|
2809
|
+
/**
|
|
2810
|
+
* The single version comparison every install mode goes through: only a
|
|
2811
|
+
* strictly newer release is an update. `allowSameVersion` (`--force`) and
|
|
2812
|
+
* `allowDowngrade` (rollback) are the two explicit escape hatches.
|
|
2813
|
+
*/
|
|
2814
|
+
function wantsInstall(opts) {
|
|
2815
|
+
const newer = isNewerVersion(opts.latestVersion, opts.currentVersion);
|
|
2816
|
+
const same = opts.latestVersion === opts.currentVersion;
|
|
2817
|
+
return newer || same && opts.allowSameVersion === true || !newer && !same && opts.allowDowngrade === true;
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Decide what `skydive update` should do, given the running version, the
|
|
2821
|
+
* install source, the channel's newest release, and this host. Pure — no
|
|
2822
|
+
* network, no filesystem — so every branch is unit-testable.
|
|
2823
|
+
*
|
|
2824
|
+
* `latestVersion` is the channel's current version for a **package-manager**
|
|
2825
|
+
* install (an npm dist-tag lookup). A binary install reads it off `manifest`
|
|
2826
|
+
* instead, because there the manifest is both the version pointer and the
|
|
2827
|
+
* byte source. Either way it must be a real fetched answer: a package-manager
|
|
2828
|
+
* install that skips the comparison is what made `skydive update` announce
|
|
2829
|
+
* "0.2.0 → 0.2.0" on every invocation.
|
|
2830
|
+
*
|
|
2831
|
+
* `allowSameVersion` forces a reinstall of the current version (the `--force`
|
|
2832
|
+
* escape hatch for a corrupt binary); `allowDowngrade` lets an explicit
|
|
2833
|
+
* pinned/older manifest install over a newer running binary (rollback).
|
|
2834
|
+
*/
|
|
2835
|
+
function planUpdate(opts) {
|
|
2836
|
+
const { currentVersion, source, manifest, platform, arch } = opts;
|
|
2837
|
+
if (source !== "binary") {
|
|
2838
|
+
const { latestVersion } = opts;
|
|
2839
|
+
if (!latestVersion || !wantsInstall({
|
|
2840
|
+
...opts,
|
|
2841
|
+
latestVersion
|
|
2842
|
+
})) return {
|
|
2843
|
+
kind: "up-to-date",
|
|
2844
|
+
currentVersion
|
|
2845
|
+
};
|
|
2846
|
+
return {
|
|
2847
|
+
kind: "unsupported-source",
|
|
2848
|
+
source,
|
|
2849
|
+
latestVersion
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
if (!manifest) return {
|
|
2853
|
+
kind: "up-to-date",
|
|
2854
|
+
currentVersion
|
|
2855
|
+
};
|
|
2856
|
+
if (!wantsInstall({
|
|
2857
|
+
...opts,
|
|
2858
|
+
latestVersion: manifest.version
|
|
2859
|
+
})) return {
|
|
2860
|
+
kind: "up-to-date",
|
|
2861
|
+
currentVersion
|
|
2862
|
+
};
|
|
2863
|
+
const lookup = lookupTarget(manifest, platform, arch);
|
|
2864
|
+
if (!lookup.ok) return lookup.reason === "unsupported-host" ? {
|
|
2865
|
+
kind: "unsupported-host",
|
|
2866
|
+
platform,
|
|
2867
|
+
arch
|
|
2868
|
+
} : {
|
|
2869
|
+
kind: "missing-from-manifest",
|
|
2870
|
+
target: lookup.target,
|
|
2871
|
+
version: manifest.version
|
|
2872
|
+
};
|
|
2873
|
+
return {
|
|
2874
|
+
kind: "update",
|
|
2875
|
+
target: lookup.target,
|
|
2876
|
+
version: manifest.version,
|
|
2877
|
+
key: lookup.entry.key,
|
|
2878
|
+
sha256: lookup.entry.sha256,
|
|
2879
|
+
size: lookup.entry.size
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
var UpdateError = class extends Error {};
|
|
2883
|
+
/** Read a Node errno string (`EACCES`, `EROFS`, ...) off an unknown thrown
|
|
2884
|
+
* value without a type assertion — the property is validated before it's read. */
|
|
2885
|
+
function errnoCode(error) {
|
|
2886
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
2887
|
+
const { code } = error;
|
|
2888
|
+
if (typeof code === "string") return code;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
/** Lowercase hex sha256 of a buffer, for comparing against the manifest. */
|
|
2892
|
+
function sha256Hex(bytes) {
|
|
2893
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Download the release object and verify its digest before it ever touches
|
|
2897
|
+
* the install path. Returns the raw bytes; the caller writes them atomically.
|
|
2898
|
+
* Throws {@link UpdateError} (with an actionable message) on any HTTP,
|
|
2899
|
+
* timeout, or integrity failure — never a bare fetch error.
|
|
2900
|
+
*/
|
|
2901
|
+
async function downloadAndVerify(opts) {
|
|
2902
|
+
const url = `${opts.downloadBaseUrl}/${opts.key}`;
|
|
2903
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
2904
|
+
const signal = opts.signal ?? AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS);
|
|
2905
|
+
let response;
|
|
2906
|
+
try {
|
|
2907
|
+
response = await doFetch(url, {
|
|
2908
|
+
signal,
|
|
2909
|
+
redirect: "follow"
|
|
2910
|
+
});
|
|
2911
|
+
} catch (error) {
|
|
2912
|
+
throw new UpdateError(`download failed from ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2913
|
+
}
|
|
2914
|
+
if (!response.ok) throw new UpdateError(`download failed from ${url}: HTTP ${response.status}`);
|
|
2915
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
2916
|
+
if (bytes.byteLength === 0) throw new UpdateError(`download from ${url} was empty`);
|
|
2917
|
+
const actual = sha256Hex(bytes);
|
|
2918
|
+
if (actual !== opts.expectedSha256) throw new UpdateError(`sha256 mismatch (expected ${opts.expectedSha256}, got ${actual}) — the download may be corrupt; re-run \`skydive update\``);
|
|
2919
|
+
return bytes;
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Atomically replace the executable at `destPath` with `bytes`. The temp file
|
|
2923
|
+
* is written in the *same directory* as the target so the final `rename` is
|
|
2924
|
+
* atomic (rename across filesystems is not) — a crash mid-update leaves either
|
|
2925
|
+
* the old binary or the new one, never a truncated file.
|
|
2926
|
+
*
|
|
2927
|
+
* On a locked destination (Windows, or a read-only install dir) the rename
|
|
2928
|
+
* fails; we surface an actionable UpdateError instead of a raw errno. macOS
|
|
2929
|
+
* and Linux happily rename over a running executable (the old inode stays
|
|
2930
|
+
* live for the current process), which is what makes in-place self-update
|
|
2931
|
+
* safe here.
|
|
2932
|
+
*/
|
|
2933
|
+
async function applyBinary(destPath, bytes) {
|
|
2934
|
+
const dir = path.dirname(destPath);
|
|
2935
|
+
const tmp = path.join(dir, `.skydive-update-${process.pid}-${Date.now()}.tmp`);
|
|
2936
|
+
try {
|
|
2937
|
+
await fsp.writeFile(tmp, bytes, { mode: 493 });
|
|
2938
|
+
await fsp.chmod(tmp, 493);
|
|
2939
|
+
await fsp.rename(tmp, destPath);
|
|
2940
|
+
} catch (error) {
|
|
2941
|
+
await fsp.rm(tmp, { force: true }).catch(() => {});
|
|
2942
|
+
const code = errnoCode(error);
|
|
2943
|
+
if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new UpdateError(`cannot write ${destPath} (permission denied). Re-run with the right permissions, or reinstall with the install script.`);
|
|
2944
|
+
throw new UpdateError(`failed to install the new binary at ${destPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Resolve the path of the currently running executable to overwrite. For a
|
|
2949
|
+
* `bun --compile` binary that's `process.execPath`; we resolve symlinks so a
|
|
2950
|
+
* `~/.local/bin/skydive` symlink into a versioned store updates the real
|
|
2951
|
+
* file, not the link. Returns null when the path can't be determined (e.g. a
|
|
2952
|
+
* non-binary context), which the command treats as "can't self-update".
|
|
2953
|
+
*/
|
|
2954
|
+
function resolveSelfPath(execPath = process.execPath) {
|
|
2955
|
+
if (!execPath) return null;
|
|
2956
|
+
try {
|
|
2957
|
+
return fs.realpathSync(execPath);
|
|
2958
|
+
} catch (_error) {
|
|
2959
|
+
return execPath;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
//#endregion
|
|
2964
|
+
//#region src/update-check/sources.ts
|
|
2965
|
+
function resolveInstallSource() {
|
|
2966
|
+
return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
|
|
2967
|
+
}
|
|
2968
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2969
|
+
const npmDistTagsSchema = z.record(z.string(), z.unknown());
|
|
2970
|
+
const binaryManifestSchema = z.object({ version: z.unknown().optional() });
|
|
2971
|
+
/** npm dist-tags for the published package (`skydive-cli`). */
|
|
2972
|
+
const npmReleaseSource = {
|
|
2973
|
+
async fetchLatestVersion(channel, { signal }) {
|
|
2974
|
+
const response = await fetch(`${NPM_REGISTRY_URL}/-/package/${name}/dist-tags`, {
|
|
2975
|
+
signal,
|
|
2976
|
+
headers: { accept: "application/json" }
|
|
2977
|
+
});
|
|
2978
|
+
if (!response.ok) return null;
|
|
2979
|
+
const tags = npmDistTagsSchema.safeParse(await response.json());
|
|
2980
|
+
if (!tags.success) return null;
|
|
2981
|
+
const version = tags.data[distTagForChannel(channel)];
|
|
2982
|
+
return typeof version === "string" ? version : null;
|
|
2983
|
+
},
|
|
2984
|
+
async fetchManifest() {
|
|
2985
|
+
return null;
|
|
2986
|
+
}
|
|
2987
|
+
};
|
|
2988
|
+
/**
|
|
2989
|
+
* Release CDN (CloudFront over the releases bucket, infra: CliReleasesCdn).
|
|
2990
|
+
* Serves the channel pointers the release workflow uploads
|
|
2991
|
+
* (`channels/{stable,canary}.json`, cached max-age=60) alongside the
|
|
2992
|
+
* binaries. The daily poll goes here, not to the api, so a fleet of
|
|
2993
|
+
* installed binaries puts no load on — and takes no dependency on — the api.
|
|
2994
|
+
*/
|
|
2995
|
+
const RELEASE_CDN_URL = "https://dl.skydive.com";
|
|
2996
|
+
/** Channel manifest on the release CDN, for compiled binaries. The same
|
|
2997
|
+
* document the api's channel route serves (see
|
|
2998
|
+
* apps/anyone/api/src/routes/cli-releases.ts, which reads it from the
|
|
2999
|
+
* bucket this CDN fronts). */
|
|
3000
|
+
const binaryReleaseSource = {
|
|
3001
|
+
async fetchLatestVersion(channel, { signal }) {
|
|
3002
|
+
const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
|
|
3003
|
+
signal,
|
|
3004
|
+
headers: { accept: "application/json" }
|
|
3005
|
+
});
|
|
3006
|
+
if (!response.ok) return null;
|
|
3007
|
+
const manifest = binaryManifestSchema.safeParse(await response.json());
|
|
3008
|
+
if (!manifest.success) return null;
|
|
3009
|
+
return typeof manifest.data.version === "string" ? manifest.data.version : null;
|
|
3010
|
+
},
|
|
3011
|
+
async fetchManifest(channel, { signal }) {
|
|
3012
|
+
const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
|
|
3013
|
+
signal,
|
|
3014
|
+
headers: { accept: "application/json" }
|
|
3015
|
+
});
|
|
3016
|
+
if (!response.ok) return null;
|
|
3017
|
+
const manifest = releaseManifestSchema.safeParse(await response.json());
|
|
3018
|
+
return manifest.success ? manifest.data : null;
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
function releaseSourceForInstall(source) {
|
|
3022
|
+
return source === "binary" ? binaryReleaseSource : npmReleaseSource;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
//#endregion
|
|
3026
|
+
//#region src/update-check/notice.ts
|
|
3027
|
+
/** The install-mode-specific action line. A package-manager install is never
|
|
3028
|
+
* self-mutated — we only tell the user what to run. A binary install has a
|
|
3029
|
+
* built-in self-updater (`skydive update`, ../commands/update.ts), so that's
|
|
3030
|
+
* the action; the curl re-install stays the documented fallback. */
|
|
3031
|
+
function renderUpdateCommand(channel, source) {
|
|
3032
|
+
if (source === "binary") return channel === "canary" ? "skydive update --channel canary" : "skydive update";
|
|
3033
|
+
return `npm install -g ${name}@${distTagForChannel(channel)}`;
|
|
3034
|
+
}
|
|
3035
|
+
function renderUpdateNotice(opts) {
|
|
3036
|
+
return `\nUpdate available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun ${renderUpdateCommand(opts.channel, opts.source)}\n`;
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Whether this invocation may print the notice. Pure so it's testable; the
|
|
3040
|
+
* inputs are raw pre-yargs argv (parsing hasn't happened when this runs) and
|
|
3041
|
+
* stderr's TTY-ness. `--json`/`--quiet` go to stdout, and the notice goes to
|
|
3042
|
+
* stderr — but scripts commonly capture 2>&1, so machine-readable modes
|
|
3043
|
+
* suppress it entirely rather than risk corrupting piped output.
|
|
3044
|
+
*/
|
|
3045
|
+
function shouldNotify(opts) {
|
|
3046
|
+
if (!opts.stderrIsTTY) return false;
|
|
3047
|
+
if (opts.argv.includes("--json") || opts.argv.includes("--quiet")) return false;
|
|
3048
|
+
return true;
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
//#endregion
|
|
3052
|
+
//#region src/update-check/package-manager.ts
|
|
3053
|
+
const KNOWN = [
|
|
3054
|
+
"npm",
|
|
3055
|
+
"yarn",
|
|
3056
|
+
"pnpm",
|
|
3057
|
+
"bun"
|
|
3058
|
+
];
|
|
3059
|
+
function isKnown(value) {
|
|
3060
|
+
return KNOWN.some((m) => m === value);
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Parse the manager name out of an `npm_config_user_agent` string. npm, yarn,
|
|
3064
|
+
* pnpm and bun all set it to `"<manager>/<version> ..."` when they spawn a
|
|
3065
|
+
* script, which is the most reliable signal for how *this* process was
|
|
3066
|
+
* launched. Returns null when the var is absent or unrecognized.
|
|
3067
|
+
*/
|
|
3068
|
+
function parseUserAgent(userAgent) {
|
|
3069
|
+
if (!userAgent) return null;
|
|
3070
|
+
const name = userAgent.trim().split("/")[0]?.toLowerCase();
|
|
3071
|
+
return name && isKnown(name) ? name : null;
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Best-effort detection of the manager that owns this install. Order of
|
|
3075
|
+
* signals, most to least reliable:
|
|
3076
|
+
* 1. `npm_config_user_agent` — set by the manager that spawned us.
|
|
3077
|
+
* 2. the executable path — a global bin under a `pnpm`/`yarn`/`bun` store
|
|
3078
|
+
* names the manager even when we weren't spawned by it (e.g. the user
|
|
3079
|
+
* typed `skydive` directly).
|
|
3080
|
+
* Falls back to `npm`, the documented default install path, so the command we
|
|
3081
|
+
* show is always runnable — the prompt still lets the user decline and copy a
|
|
3082
|
+
* different one.
|
|
3083
|
+
*/
|
|
3084
|
+
function detectPackageManager(opts) {
|
|
3085
|
+
const fromUa = parseUserAgent(opts.env["npm_config_user_agent"]);
|
|
3086
|
+
if (fromUa) return fromUa;
|
|
3087
|
+
const exec = opts.execPath.toLowerCase();
|
|
3088
|
+
if (exec.includes("pnpm")) return "pnpm";
|
|
3089
|
+
if (exec.includes("yarn")) return "yarn";
|
|
3090
|
+
if (exec.includes("bun")) return "bun";
|
|
3091
|
+
return "npm";
|
|
3092
|
+
}
|
|
3093
|
+
/**
|
|
3094
|
+
* The global-install command (as argv) for a manager + channel. Each manager's
|
|
3095
|
+
* global-add spelling differs; the version target is the channel's dist-tag
|
|
3096
|
+
* (`latest`/`beta`), matching what the release workflow publishes.
|
|
3097
|
+
*/
|
|
3098
|
+
function installCommand(manager, channel) {
|
|
3099
|
+
const spec = `${name}@${distTagForChannel(channel)}`;
|
|
3100
|
+
switch (manager) {
|
|
3101
|
+
case "npm": return [
|
|
3102
|
+
"npm",
|
|
3103
|
+
"install",
|
|
3104
|
+
"-g",
|
|
3105
|
+
spec
|
|
3106
|
+
];
|
|
3107
|
+
case "pnpm": return [
|
|
3108
|
+
"pnpm",
|
|
3109
|
+
"add",
|
|
3110
|
+
"-g",
|
|
3111
|
+
spec
|
|
3112
|
+
];
|
|
3113
|
+
case "bun": return [
|
|
3114
|
+
"bun",
|
|
3115
|
+
"add",
|
|
3116
|
+
"-g",
|
|
3117
|
+
spec
|
|
3118
|
+
];
|
|
3119
|
+
case "yarn": return [
|
|
3120
|
+
"yarn",
|
|
3121
|
+
"global",
|
|
3122
|
+
"add",
|
|
3123
|
+
spec
|
|
3124
|
+
];
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
/** The command as a copy-pasteable string for prompts and notices. */
|
|
3128
|
+
function installCommandString(manager, channel) {
|
|
3129
|
+
return installCommand(manager, channel).join(" ");
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
//#endregion
|
|
3133
|
+
//#region src/update-check/pm-update.ts
|
|
3134
|
+
/**
|
|
3135
|
+
* The interactive upgrade path for a package-manager install: instead of only
|
|
3136
|
+
* printing "run npm install -g …", offer to run it now, and on confirm run
|
|
3137
|
+
* that exact command and relaunch the CLI on the new version.
|
|
3138
|
+
*
|
|
3139
|
+
* We never silently mutate a package-manager install (the manager owns it),
|
|
3140
|
+
* but an explicit prompt the user answers with a keypress is honest consent —
|
|
3141
|
+
* and it runs the *same* command we detected + display, so what you approve is
|
|
3142
|
+
* what executes. Non-interactive contexts (no TTY, --json/--quiet, CI) skip
|
|
3143
|
+
* the prompt entirely and fall back to the printed notice.
|
|
3144
|
+
*/
|
|
3145
|
+
/** Whether it's safe to show an interactive prompt at all. */
|
|
3146
|
+
function canPromptInteractively(opts) {
|
|
3147
|
+
if (opts.json || opts.quiet) return false;
|
|
3148
|
+
if (!opts.stdinIsTTY || !opts.stdoutIsTTY) return false;
|
|
3149
|
+
if (opts.env["CI"] || opts.env["SKYDIVE_NO_UPDATE_CHECK"]) return false;
|
|
3150
|
+
return true;
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Interpret a raw prompt answer. Default (empty / Enter) is **yes** — the
|
|
3154
|
+
* prompt renders `[Y/n]`, so a bare Enter accepts. Anything starting with `n`
|
|
3155
|
+
* (case-insensitive) is no; any other non-empty input is treated as no too,
|
|
3156
|
+
* so a fat-fingered answer never runs an install the user didn't mean.
|
|
3157
|
+
*/
|
|
3158
|
+
function interpretYesNo(answer) {
|
|
3159
|
+
const a = answer.trim().toLowerCase();
|
|
3160
|
+
if (a === "") return true;
|
|
3161
|
+
return a === "y" || a === "yes";
|
|
3162
|
+
}
|
|
3163
|
+
/** Ask a `[Y/n]` question on the TTY and resolve to the boolean answer. */
|
|
3164
|
+
async function promptYesNo(question) {
|
|
3165
|
+
const rl = createInterface({
|
|
3166
|
+
input: process.stdin,
|
|
3167
|
+
output: process.stdout
|
|
3168
|
+
});
|
|
3169
|
+
try {
|
|
3170
|
+
return interpretYesNo(await new Promise((resolve) => {
|
|
3171
|
+
rl.question(question, resolve);
|
|
3172
|
+
}));
|
|
3173
|
+
} finally {
|
|
3174
|
+
rl.close();
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* Heading shared by the prompt and the printed instruction, so the two can't
|
|
3179
|
+
* disagree about what is happening. `--force` asks to reinstall the version
|
|
3180
|
+
* already running, and calling that an update is exactly the lie this path
|
|
3181
|
+
* used to tell on every invocation — so it gets its own wording.
|
|
3182
|
+
*/
|
|
3183
|
+
function renderPmUpdateHeading(opts) {
|
|
3184
|
+
return opts.latestVersion === opts.currentVersion ? `Reinstalling ${opts.currentVersion}.` : `Update available: ${opts.currentVersion} \u2192 ${opts.latestVersion}`;
|
|
3185
|
+
}
|
|
3186
|
+
/**
|
|
3187
|
+
* Offer + run the package-manager upgrade. Returns a result the caller turns
|
|
3188
|
+
* into output and an exit/relaunch decision — kept separate from the actual
|
|
3189
|
+
* relaunch so it's testable without re-execing the test runner.
|
|
3190
|
+
*/
|
|
3191
|
+
async function offerPackageManagerUpdate(opts) {
|
|
3192
|
+
const manager = detectPackageManager({
|
|
3193
|
+
env: opts.env,
|
|
3194
|
+
execPath: opts.execPath
|
|
3195
|
+
});
|
|
3196
|
+
const command = installCommand(manager, opts.channel);
|
|
3197
|
+
const commandStr = installCommandString(manager, opts.channel);
|
|
3198
|
+
if (!opts.interactive) return { status: "skipped-noninteractive" };
|
|
3199
|
+
if (!await (opts.ask ?? promptYesNo)(`${renderPmUpdateHeading(opts)}\nRun \`${commandStr}\` now? [Y/n] `)) return { status: "declined" };
|
|
3200
|
+
const { status } = (opts.run ?? ((cmd) => {
|
|
3201
|
+
const [bin, ...args] = cmd;
|
|
3202
|
+
return { status: spawnSync(bin ?? "", args, { stdio: "inherit" }).status };
|
|
3203
|
+
}))(command);
|
|
3204
|
+
if (status !== 0) return {
|
|
3205
|
+
status: "failed",
|
|
3206
|
+
code: status,
|
|
3207
|
+
command: commandStr
|
|
3208
|
+
};
|
|
3209
|
+
return {
|
|
3210
|
+
status: "installed",
|
|
3211
|
+
command: commandStr
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Relaunch the CLI after a successful in-place upgrade so the user lands back
|
|
3216
|
+
* on their command running the new version. Uses the same interpreter + argv
|
|
3217
|
+
* the current process was launched with; detaches so the parent can exit
|
|
3218
|
+
* cleanly. Never throws — a failed relaunch just means the user re-runs
|
|
3219
|
+
* manually, which the caller's message covers.
|
|
3220
|
+
*/
|
|
3221
|
+
function relaunchCli(argv = process.argv) {
|
|
3222
|
+
try {
|
|
3223
|
+
const [, script, ...rest] = argv;
|
|
3224
|
+
spawn(process.execPath, script ? [script, ...rest] : rest, { stdio: "inherit" }).on("exit", (code) => process.exit(code ?? 0));
|
|
3225
|
+
} catch (_error) {
|
|
3226
|
+
process.exit(0);
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
//#endregion
|
|
3231
|
+
//#region src/commands/update.ts
|
|
3232
|
+
/**
|
|
3233
|
+
* `skydive update` — the user-invoked self-updater the update *check*
|
|
3234
|
+
* (../update-check/index.ts) has always pointed at. Notify-only remains the
|
|
3235
|
+
* automatic behavior; this is the explicit "do it now" action.
|
|
3236
|
+
*
|
|
3237
|
+
* Binary installs self-update in place (download -> verify sha256 -> atomic
|
|
3238
|
+
* replace); package-manager installs are never self-mutated (the notice
|
|
3239
|
+
* policy) — we print the exact upgrade command instead. `--check` is a dry
|
|
3240
|
+
* run; `--force` reinstalls the current version (repair a corrupt binary).
|
|
3241
|
+
*/
|
|
3242
|
+
const FETCH_TIMEOUT_MS$1 = 15e3;
|
|
3243
|
+
function normalizeChannel(raw, currentVersion) {
|
|
3244
|
+
if (raw === "stable" || raw === "canary") return raw;
|
|
3245
|
+
return resolveChannel(currentVersion);
|
|
3246
|
+
}
|
|
3247
|
+
/** Human-readable line for a plan outcome. Pure so the handler stays thin. */
|
|
3248
|
+
function describePlan(plan, channel, isCheck) {
|
|
3249
|
+
switch (plan.kind) {
|
|
3250
|
+
case "up-to-date": return `Already on the latest ${channel} release (${plan.currentVersion}).`;
|
|
3251
|
+
case "unsupported-source": return `${renderPmUpdateHeading({
|
|
3252
|
+
currentVersion: version,
|
|
3253
|
+
latestVersion: plan.latestVersion
|
|
3254
|
+
})}\nThis is a ${plan.source} install, so update through your package manager:\n ${renderUpdateCommand(channel, plan.source)}`;
|
|
3255
|
+
case "unsupported-host": return `No Skydive binary is published for ${plan.platform}/${plan.arch}.`;
|
|
3256
|
+
case "missing-from-manifest": return `The ${channel} ${plan.version} release has no binary for your platform (${plan.target}).`;
|
|
3257
|
+
case "update": return isCheck ? `Update available: ${version} \u2192 ${plan.version} (${channel}, ${plan.target}).\nRun \`skydive update\` to install it.` : `Updating ${version} \u2192 ${plan.version} (${channel}, ${plan.target})...`;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
const updateCommand = {
|
|
3261
|
+
command: "update",
|
|
3262
|
+
describe: "Update the Skydive CLI to the latest release",
|
|
3263
|
+
builder: (y) => y.option("channel", {
|
|
3264
|
+
type: "string",
|
|
3265
|
+
choices: ["stable", "canary"],
|
|
3266
|
+
describe: "Release channel to update to (default: this build's channel)"
|
|
3267
|
+
}).option("check", {
|
|
3268
|
+
type: "boolean",
|
|
3269
|
+
default: false,
|
|
3270
|
+
describe: "Only report whether an update is available; don't install"
|
|
3271
|
+
}).option("force", {
|
|
3272
|
+
type: "boolean",
|
|
3273
|
+
default: false,
|
|
3274
|
+
describe: "Reinstall the current version even if already up to date (repairs a corrupt binary)"
|
|
3275
|
+
}).example("skydive update", "Install the latest release").example("skydive update --check", "See if an update is available").example("skydive update --channel canary", "Switch to the canary line"),
|
|
3276
|
+
handler: async (argv) => {
|
|
3277
|
+
const currentVersion = version;
|
|
3278
|
+
const source = resolveInstallSource();
|
|
3279
|
+
const channel = normalizeChannel(argv.channel, currentVersion);
|
|
3280
|
+
const releaseSource = releaseSourceForInstall(source);
|
|
3281
|
+
const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS$1);
|
|
3282
|
+
const manifest = source === "binary" ? await releaseSource.fetchManifest(channel, { signal }).catch(() => null) : null;
|
|
3283
|
+
const latestVersion = source === "binary" ? manifest?.version ?? null : await releaseSource.fetchLatestVersion(channel, { signal }).catch(() => null);
|
|
3284
|
+
if (latestVersion === null) {
|
|
3285
|
+
if (argv.json) {
|
|
3286
|
+
output(argv, {
|
|
3287
|
+
status: "unavailable",
|
|
3288
|
+
channel,
|
|
3289
|
+
currentVersion
|
|
3290
|
+
});
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
throw new UpdateError(source === "binary" ? `couldn't reach the ${channel} release channel — check your connection and try again.` : `couldn't reach the npm registry to look up the ${channel} release — check your connection and try again.`);
|
|
3294
|
+
}
|
|
3295
|
+
const plan = planUpdate({
|
|
3296
|
+
currentVersion,
|
|
3297
|
+
source,
|
|
3298
|
+
latestVersion,
|
|
3299
|
+
manifest,
|
|
3300
|
+
platform: process.platform,
|
|
3301
|
+
arch: process.arch,
|
|
3302
|
+
allowSameVersion: argv.force
|
|
3303
|
+
});
|
|
3304
|
+
if (argv.json) {
|
|
3305
|
+
output(argv, {
|
|
3306
|
+
plan,
|
|
3307
|
+
channel,
|
|
3308
|
+
currentVersion,
|
|
3309
|
+
dryRun: argv.check
|
|
3310
|
+
});
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
if (plan.kind === "unsupported-source" && !argv.check) {
|
|
3314
|
+
const interactive = canPromptInteractively({
|
|
3315
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
3316
|
+
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
3317
|
+
json: argv.json,
|
|
3318
|
+
quiet: argv.quiet,
|
|
3319
|
+
env: process.env
|
|
3320
|
+
});
|
|
3321
|
+
const result = await offerPackageManagerUpdate({
|
|
3322
|
+
channel,
|
|
3323
|
+
currentVersion,
|
|
3324
|
+
latestVersion: plan.latestVersion,
|
|
3325
|
+
env: process.env,
|
|
3326
|
+
execPath: process.execPath,
|
|
3327
|
+
interactive
|
|
3328
|
+
});
|
|
3329
|
+
switch (result.status) {
|
|
3330
|
+
case "installed":
|
|
3331
|
+
console.log(`Updated via \`${result.command}\`. Relaunching \`skydive\`...`);
|
|
3332
|
+
relaunchCli();
|
|
3333
|
+
return;
|
|
3334
|
+
case "failed": throw new UpdateError(`\`${result.command}\` exited with code ${result.code ?? "unknown"}. Try running it yourself.`);
|
|
3335
|
+
case "declined":
|
|
3336
|
+
console.log(describePlan(plan, channel, false));
|
|
3337
|
+
return;
|
|
3338
|
+
case "skipped-noninteractive":
|
|
3339
|
+
console.log(describePlan(plan, channel, false));
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
if (plan.kind !== "update" || argv.check) {
|
|
3344
|
+
console.log(describePlan(plan, channel, argv.check));
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
const selfPath = resolveSelfPath();
|
|
3348
|
+
if (!selfPath) throw new UpdateError("couldn't determine this binary's path to replace it — reinstall with the install script instead.");
|
|
3349
|
+
console.log(describePlan(plan, channel, false));
|
|
3350
|
+
await applyBinary(selfPath, await downloadAndVerify({
|
|
3351
|
+
downloadBaseUrl: RELEASE_CDN_URL,
|
|
3352
|
+
key: plan.key,
|
|
3353
|
+
expectedSha256: plan.sha256
|
|
3354
|
+
}));
|
|
3355
|
+
console.log(`Updated to ${plan.version}. Run \`skydive --version\` to confirm.`);
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
|
|
2145
3359
|
//#endregion
|
|
2146
3360
|
//#region src/cli.ts
|
|
2147
3361
|
function createCli(argv) {
|
|
2148
|
-
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 keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch 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)").option("json", {
|
|
3362
|
+
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 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)").option("json", {
|
|
2149
3363
|
type: "boolean",
|
|
2150
3364
|
default: false,
|
|
2151
3365
|
global: true,
|
|
@@ -2159,7 +3373,7 @@ function createCli(argv) {
|
|
|
2159
3373
|
type: "string",
|
|
2160
3374
|
global: true,
|
|
2161
3375
|
describe: "Override API base URL"
|
|
2162
|
-
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(importCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
3376
|
+
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(importCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).command(updateCommand).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) => {
|
|
2163
3377
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
2164
3378
|
process.exit(1);
|
|
2165
3379
|
});
|
|
@@ -2271,100 +3485,6 @@ function isCheckDue(lastCheckedAt, now = Date.now()) {
|
|
|
2271
3485
|
return now - then >= CHECK_INTERVAL_MS;
|
|
2272
3486
|
}
|
|
2273
3487
|
|
|
2274
|
-
//#endregion
|
|
2275
|
-
//#region src/update-check/versions.ts
|
|
2276
|
-
/** Canary builds carry a prerelease suffix (`X.Y.Z-beta.N`, synthesized in
|
|
2277
|
-
* CI); a plain semver is a stable release. An unparseable version defaults
|
|
2278
|
-
* to stable. */
|
|
2279
|
-
function resolveChannel(version) {
|
|
2280
|
-
return (semver.prerelease(version, { loose: true })?.length ?? 0) > 0 ? "canary" : "stable";
|
|
2281
|
-
}
|
|
2282
|
-
/** npm dist-tag for a channel (see release-skydive-cli.yml). */
|
|
2283
|
-
function distTagForChannel(channel) {
|
|
2284
|
-
return channel === "canary" ? "beta" : "latest";
|
|
2285
|
-
}
|
|
2286
|
-
/**
|
|
2287
|
-
* True when `candidate` is a strictly newer release than `current`, per
|
|
2288
|
-
* semver precedence (the `semver` package, including prerelease ordering).
|
|
2289
|
-
* Unparseable input is never newer, so garbage from the registry can't
|
|
2290
|
-
* produce a notice.
|
|
2291
|
-
*/
|
|
2292
|
-
function isNewerVersion(candidate, current) {
|
|
2293
|
-
if (!semver.valid(candidate, { loose: true })) return false;
|
|
2294
|
-
if (!semver.valid(current, { loose: true })) return false;
|
|
2295
|
-
return semver.gt(candidate, current, { loose: true });
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
//#endregion
|
|
2299
|
-
//#region src/update-check/notice.ts
|
|
2300
|
-
/** The install-mode-specific action line. A package-manager install is never
|
|
2301
|
-
* self-mutated — we only tell the user what to run. */
|
|
2302
|
-
function renderUpdateCommand(channel, source) {
|
|
2303
|
-
if (source === "binary") return `curl -fsSL ${DEFAULT_WEB_URL}/api/v1/cli/install.sh | ${channel === "canary" ? "SKYDIVE_CHANNEL=canary " : ""}sh`;
|
|
2304
|
-
return `npm install -g ${name}@${distTagForChannel(channel)}`;
|
|
2305
|
-
}
|
|
2306
|
-
function renderUpdateNotice(opts) {
|
|
2307
|
-
return `\nUpdate available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun ${renderUpdateCommand(opts.channel, opts.source)}\n`;
|
|
2308
|
-
}
|
|
2309
|
-
/**
|
|
2310
|
-
* Whether this invocation may print the notice. Pure so it's testable; the
|
|
2311
|
-
* inputs are raw pre-yargs argv (parsing hasn't happened when this runs) and
|
|
2312
|
-
* stderr's TTY-ness. `--json`/`--quiet` go to stdout, and the notice goes to
|
|
2313
|
-
* stderr — but scripts commonly capture 2>&1, so machine-readable modes
|
|
2314
|
-
* suppress it entirely rather than risk corrupting piped output.
|
|
2315
|
-
*/
|
|
2316
|
-
function shouldNotify(opts) {
|
|
2317
|
-
if (!opts.stderrIsTTY) return false;
|
|
2318
|
-
if (opts.argv.includes("--json") || opts.argv.includes("--quiet")) return false;
|
|
2319
|
-
return true;
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
//#endregion
|
|
2323
|
-
//#region src/update-check/sources.ts
|
|
2324
|
-
function resolveInstallSource() {
|
|
2325
|
-
return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
|
|
2326
|
-
}
|
|
2327
|
-
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2328
|
-
const npmDistTagsSchema = z.record(z.string(), z.unknown());
|
|
2329
|
-
const binaryManifestSchema = z.object({ version: z.unknown().optional() });
|
|
2330
|
-
/** npm dist-tags for the published package (`skydive-cli`). */
|
|
2331
|
-
const npmReleaseSource = { async fetchLatestVersion(channel, { signal }) {
|
|
2332
|
-
const response = await fetch(`${NPM_REGISTRY_URL}/-/package/${name}/dist-tags`, {
|
|
2333
|
-
signal,
|
|
2334
|
-
headers: { accept: "application/json" }
|
|
2335
|
-
});
|
|
2336
|
-
if (!response.ok) return null;
|
|
2337
|
-
const tags = npmDistTagsSchema.safeParse(await response.json());
|
|
2338
|
-
if (!tags.success) return null;
|
|
2339
|
-
const version = tags.data[distTagForChannel(channel)];
|
|
2340
|
-
return typeof version === "string" ? version : null;
|
|
2341
|
-
} };
|
|
2342
|
-
/**
|
|
2343
|
-
* Release CDN (CloudFront over the releases bucket, infra: CliReleasesCdn).
|
|
2344
|
-
* Serves the channel pointers the release workflow uploads
|
|
2345
|
-
* (`channels/{stable,canary}.json`, cached max-age=60) alongside the
|
|
2346
|
-
* binaries. The daily poll goes here, not to the api, so a fleet of
|
|
2347
|
-
* installed binaries puts no load on — and takes no dependency on — the api.
|
|
2348
|
-
*/
|
|
2349
|
-
const RELEASE_CDN_URL = "https://dl.skydive.com";
|
|
2350
|
-
/** Channel manifest on the release CDN, for compiled binaries. The same
|
|
2351
|
-
* document the api's channel route serves (see
|
|
2352
|
-
* apps/anyone/api/src/routes/cli-releases.ts, which reads it from the
|
|
2353
|
-
* bucket this CDN fronts). */
|
|
2354
|
-
const binaryReleaseSource = { async fetchLatestVersion(channel, { signal }) {
|
|
2355
|
-
const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
|
|
2356
|
-
signal,
|
|
2357
|
-
headers: { accept: "application/json" }
|
|
2358
|
-
});
|
|
2359
|
-
if (!response.ok) return null;
|
|
2360
|
-
const manifest = binaryManifestSchema.safeParse(await response.json());
|
|
2361
|
-
if (!manifest.success) return null;
|
|
2362
|
-
return typeof manifest.data.version === "string" ? manifest.data.version : null;
|
|
2363
|
-
} };
|
|
2364
|
-
function releaseSourceForInstall(source) {
|
|
2365
|
-
return source === "binary" ? binaryReleaseSource : npmReleaseSource;
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
3488
|
//#endregion
|
|
2369
3489
|
//#region src/update-check/index.ts
|
|
2370
3490
|
/**
|
|
@@ -2466,7 +3586,7 @@ function setupUpdateCheck(argv) {
|
|
|
2466
3586
|
|
|
2467
3587
|
//#endregion
|
|
2468
3588
|
//#region src/changelog.generated.ts
|
|
2469
|
-
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## [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";
|
|
3589
|
+
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## [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";
|
|
2470
3590
|
|
|
2471
3591
|
//#endregion
|
|
2472
3592
|
//#region src/whats-new.ts
|
|
@@ -2602,13 +3722,26 @@ function maybePrintWhatsNew({ stderrIsTTY = process.stderr.isTTY ?? false, nonIn
|
|
|
2602
3722
|
|
|
2603
3723
|
//#endregion
|
|
2604
3724
|
//#region src/bin.ts
|
|
3725
|
+
installCrashHandler();
|
|
2605
3726
|
if (process.argv.includes(UPDATE_WORKER_FLAG)) {
|
|
2606
3727
|
await runUpdateCheckWorker();
|
|
2607
3728
|
process.exit(0);
|
|
2608
3729
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
3730
|
+
if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
|
|
3731
|
+
const { runPortalDaemon } = await import("./daemon-Ch_MSDmQ.mjs");
|
|
3732
|
+
runPortalDaemon(process.argv);
|
|
3733
|
+
} else runCli();
|
|
3734
|
+
function runCli() {
|
|
3735
|
+
const completionProbe = isCompletionProbe(process.argv);
|
|
3736
|
+
if (!completionProbe) {
|
|
3737
|
+
maybePrintWhatsNew();
|
|
3738
|
+
setupUpdateCheck(hideBin(process.argv));
|
|
3739
|
+
}
|
|
3740
|
+
const resolved = resolveArgv(normalizeProbeArgv(hideBin(process.argv)));
|
|
3741
|
+
const raw = hideBin(process.argv);
|
|
3742
|
+
if (!completionProbe && (raw.includes("--help") || raw.includes("-h")) && !raw.some((a) => !a.startsWith("-")) && !raw.includes("--json") && !raw.includes("--quiet")) process.stdout.write(brandHelpArt());
|
|
3743
|
+
createCli(resolved).parse();
|
|
3744
|
+
}
|
|
2612
3745
|
|
|
2613
3746
|
//#endregion
|
|
2614
3747
|
export { };
|