ai-remote 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -20
- package/dist/cli.mjs +311 -130
- package/dist/index.js +1 -1
- package/dist/{protocols/index.d.ts → protocols.d.ts} +2 -0
- package/dist/protocols.js +24 -0
- package/package.json +15 -10
- package/dist/protocols/index.js +0 -3
- package/dist/protocols/ssh/session.d.ts +0 -66
- package/dist/protocols/ssh/session.js +0 -17
- package/dist/protocols/ssh/terminal.d.ts +0 -46
- package/dist/protocols/ssh/terminal.js +0 -6
- package/dist/protocols/types.d.ts +0 -127
- package/dist/protocols/types.js +0 -0
- package/dist/shared/connection.d.ts +0 -81
- package/dist/shared/connection.js +0 -1
- package/dist/shared/device.d.ts +0 -26
- package/dist/shared/device.js +0 -0
- package/dist/shared/hosts.d.ts +0 -63
- package/dist/shared/hosts.js +0 -1
- package/dist/shared/icons.d.ts +0 -46
- package/dist/shared/icons.js +0 -1
- package/dist/shared/protocol.d.ts +0 -28
- package/dist/shared/protocol.js +0 -1
- package/dist/shared/signals.d.ts +0 -37
- package/dist/shared/signals.js +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli/cli.ts
|
|
4
4
|
import { basename, resolve as resolvePath } from "node:path";
|
|
5
5
|
import { spawn as spawn2 } from "node:child_process";
|
|
6
|
-
import { readFileSync
|
|
6
|
+
import { readFileSync as readFileSync2, openSync } from "node:fs";
|
|
7
7
|
|
|
8
8
|
// src/protocols/ssh/messages.ts
|
|
9
9
|
var MSG = {
|
|
@@ -2827,8 +2827,8 @@ var Tls12Client = class {
|
|
|
2827
2827
|
case HANDSHAKE.FINISHED: {
|
|
2828
2828
|
const expected = await this.#verifyData("server finished");
|
|
2829
2829
|
const actual = body.subarray(0, 12);
|
|
2830
|
-
const
|
|
2831
|
-
if (!
|
|
2830
|
+
const matches2 = expected.length === actual.length && expected.every((byte, i) => byte === actual[i]);
|
|
2831
|
+
if (!matches2) throw new Error("TLS: the host Finished message did not verify");
|
|
2832
2832
|
this.transcript.push(message);
|
|
2833
2833
|
this.established = true;
|
|
2834
2834
|
this.resolveHandshake({
|
|
@@ -7569,6 +7569,7 @@ function sessionName(host, port, explicit) {
|
|
|
7569
7569
|
var socketPath = (name) => join(RUN_DIR, `${name}.sock`);
|
|
7570
7570
|
var metaPath = (name) => join(RUN_DIR, `${name}.json`);
|
|
7571
7571
|
var logPath = (name) => join(RUN_DIR, `${name}.log`);
|
|
7572
|
+
var currentPath = () => join(ROOT, "current");
|
|
7572
7573
|
|
|
7573
7574
|
// src/cli/daemon.ts
|
|
7574
7575
|
async function runDaemon(options) {
|
|
@@ -7606,7 +7607,11 @@ async function runDaemon(options) {
|
|
|
7606
7607
|
viewer: viewer?.url ?? null,
|
|
7607
7608
|
width: session.framebuffer.width || size.width,
|
|
7608
7609
|
height: session.framebuffer.height || size.height,
|
|
7609
|
-
startedAt
|
|
7610
|
+
startedAt,
|
|
7611
|
+
// So `shell` and `exec` need no flags either: the account the terminal
|
|
7612
|
+
// signs in as was settled when the session was opened.
|
|
7613
|
+
sshUser: options.sshUsername || bareUsername(options.username),
|
|
7614
|
+
sshPort: options.sshPort
|
|
7610
7615
|
}, null, 2));
|
|
7611
7616
|
}
|
|
7612
7617
|
let viewer = null;
|
|
@@ -7674,6 +7679,8 @@ async function runDaemon(options) {
|
|
|
7674
7679
|
window: viewer?.window?.kind ?? null,
|
|
7675
7680
|
viewers: viewer?.viewers ?? 0,
|
|
7676
7681
|
shell: shell ? "open" : shellStarting ? "opening" : "closed",
|
|
7682
|
+
sshUser: options.sshUsername || bareUsername(options.username),
|
|
7683
|
+
sshPort: options.sshPort,
|
|
7677
7684
|
pid: process.pid
|
|
7678
7685
|
};
|
|
7679
7686
|
case "shot": {
|
|
@@ -7797,10 +7804,89 @@ async function runScript(session, script) {
|
|
|
7797
7804
|
return done;
|
|
7798
7805
|
}
|
|
7799
7806
|
|
|
7807
|
+
// src/cli/sessions.ts
|
|
7808
|
+
import { existsSync as existsSync2, readFileSync, readdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7809
|
+
function liveSessions() {
|
|
7810
|
+
ensureRunDir();
|
|
7811
|
+
return readdirSync(RUN_DIR).filter((file) => file.endsWith(".json")).flatMap((file) => {
|
|
7812
|
+
try {
|
|
7813
|
+
const meta = JSON.parse(readFileSync(`${RUN_DIR}/${file}`, "utf8"));
|
|
7814
|
+
try {
|
|
7815
|
+
process.kill(meta.pid, 0);
|
|
7816
|
+
} catch {
|
|
7817
|
+
cleanup(meta.name);
|
|
7818
|
+
return [];
|
|
7819
|
+
}
|
|
7820
|
+
return [meta];
|
|
7821
|
+
} catch {
|
|
7822
|
+
return [];
|
|
7823
|
+
}
|
|
7824
|
+
}).toSorted((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
7825
|
+
}
|
|
7826
|
+
function cleanup(name) {
|
|
7827
|
+
for (const file of [socketPath(name), metaPath(name), logPath(name)]) {
|
|
7828
|
+
try {
|
|
7829
|
+
if (existsSync2(file)) unlinkSync2(file);
|
|
7830
|
+
} catch {
|
|
7831
|
+
}
|
|
7832
|
+
}
|
|
7833
|
+
forgetCurrent(name);
|
|
7834
|
+
}
|
|
7835
|
+
function setCurrent(name) {
|
|
7836
|
+
ensureRunDir();
|
|
7837
|
+
writeFileSync2(currentPath(), `${name}
|
|
7838
|
+
`, { mode: 384 });
|
|
7839
|
+
}
|
|
7840
|
+
function readCurrent() {
|
|
7841
|
+
try {
|
|
7842
|
+
return readFileSync(currentPath(), "utf8").trim() || null;
|
|
7843
|
+
} catch {
|
|
7844
|
+
return null;
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7847
|
+
function forgetCurrent(name) {
|
|
7848
|
+
if (name && readCurrent() !== name) return;
|
|
7849
|
+
try {
|
|
7850
|
+
if (existsSync2(currentPath())) unlinkSync2(currentPath());
|
|
7851
|
+
} catch {
|
|
7852
|
+
}
|
|
7853
|
+
}
|
|
7854
|
+
var matches = (meta, asked) => meta.name === asked || meta.host === asked || `${meta.host}:${meta.port}` === asked;
|
|
7855
|
+
function pickSession(open, asked, current, program = "ai-remote") {
|
|
7856
|
+
if (asked) {
|
|
7857
|
+
const named = open.find((meta) => matches(meta, asked));
|
|
7858
|
+
return named ? { session: named } : {
|
|
7859
|
+
problem: `No open session matches "${asked}".${listing(open, program)}`
|
|
7860
|
+
};
|
|
7861
|
+
}
|
|
7862
|
+
if (!open.length) {
|
|
7863
|
+
return { problem: `No session is open. Start one with \`${program} open <host[:port]>\`.` };
|
|
7864
|
+
}
|
|
7865
|
+
const marked = current ? open.find((meta) => meta.name === current) : void 0;
|
|
7866
|
+
if (marked) return { session: marked };
|
|
7867
|
+
if (open.length === 1) return { session: open[0] };
|
|
7868
|
+
return {
|
|
7869
|
+
problem: `${open.length} sessions are open and none of them is the current one. Pass \`--session NAME\`.${listing(open, program)}`
|
|
7870
|
+
};
|
|
7871
|
+
}
|
|
7872
|
+
function listing(open, program) {
|
|
7873
|
+
if (!open.length) return ` Nothing is open -- \`${program} open <host[:port]>\` starts a session.`;
|
|
7874
|
+
return `
|
|
7875
|
+
Open: ${open.map((meta) => `${meta.name} (${meta.host}:${meta.port})`).join(", ")}`;
|
|
7876
|
+
}
|
|
7877
|
+
function freeName(base, taken) {
|
|
7878
|
+
const used = new Set(taken);
|
|
7879
|
+
if (!used.has(base)) return base;
|
|
7880
|
+
for (let suffix = 2; ; suffix++) {
|
|
7881
|
+
const candidate = `${base}_${suffix}`;
|
|
7882
|
+
if (!used.has(candidate)) return candidate;
|
|
7883
|
+
}
|
|
7884
|
+
}
|
|
7885
|
+
|
|
7800
7886
|
// src/cli/cli.ts
|
|
7801
7887
|
function readLog(name) {
|
|
7802
7888
|
try {
|
|
7803
|
-
return
|
|
7889
|
+
return readFileSync2(logPath(name), "utf8").split("\n").filter((line) => line.trim() && !/^\s/.test(line) && !line.startsWith("[RDP] ")).slice(-4).join("\n");
|
|
7804
7890
|
} catch {
|
|
7805
7891
|
return "";
|
|
7806
7892
|
}
|
|
@@ -7811,40 +7897,56 @@ var NAME = (() => {
|
|
|
7811
7897
|
})();
|
|
7812
7898
|
var USAGE = `${NAME} -- drive a remote desktop, with a window you can watch
|
|
7813
7899
|
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
${NAME}
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
${NAME}
|
|
7821
|
-
${NAME}
|
|
7822
|
-
${NAME}
|
|
7823
|
-
${NAME}
|
|
7824
|
-
${NAME}
|
|
7825
|
-
${NAME}
|
|
7900
|
+
One command names the machine:
|
|
7901
|
+
|
|
7902
|
+
${NAME} open <host[:port]> connect, show a window, and stay open
|
|
7903
|
+
|
|
7904
|
+
The rest drive that session, and take no host:
|
|
7905
|
+
|
|
7906
|
+
${NAME} shot screenshot the desktop
|
|
7907
|
+
${NAME} click X,Y click, --button right|middle, --double
|
|
7908
|
+
${NAME} type "text" type into whatever has focus
|
|
7909
|
+
${NAME} key <Code> press keys, e.g. MetaLeft, ControlLeft+KeyA
|
|
7910
|
+
${NAME} exec "cmd" run a command in the terminal (SSH)
|
|
7911
|
+
${NAME} shell an interactive terminal
|
|
7912
|
+
${NAME} do "steps" several actions in one go
|
|
7913
|
+
${NAME} view open the window on the session
|
|
7914
|
+
${NAME} view --close close the window, session keeps running
|
|
7915
|
+
${NAME} status what the session is doing
|
|
7916
|
+
${NAME} list every session that is open
|
|
7917
|
+
${NAME} close close it; --all closes every session
|
|
7918
|
+
${NAME} probe [<host[:port]>] is anything listening?
|
|
7826
7919
|
|
|
7827
7920
|
Steps for "do", separated by semicolons:
|
|
7828
7921
|
move X,Y \xB7 click X,Y[,right|middle] \xB7 dblclick X,Y \xB7 scroll X,Y,DX,DY
|
|
7829
7922
|
type TEXT \xB7 key Code[+Code...] \xB7 cad \xB7 wait MS \xB7 shot FILE
|
|
7830
7923
|
|
|
7831
|
-
|
|
7924
|
+
Sessions
|
|
7925
|
+
The session opened last is the one commands use. --session NAME picks
|
|
7926
|
+
another, and a session answers to its name, its host, or host:port.
|
|
7927
|
+
A session stays open until it is closed; \`${NAME} list\` shows them.
|
|
7928
|
+
|
|
7929
|
+
Options for open
|
|
7832
7930
|
-u, --user NAME account on the host
|
|
7833
7931
|
-d, --domain NAME Windows domain or computer name
|
|
7932
|
+
--session NAME name this session (default: host_port)
|
|
7834
7933
|
--ssh-user NAME only if the terminal signs in as someone else;
|
|
7835
7934
|
by default it reuses --user and the same password
|
|
7836
7935
|
--ssh-port N SSH port (default: 22)
|
|
7837
7936
|
-s, --security MODE auto | nla | tls | rdp (default: auto)
|
|
7838
7937
|
-W, --width N desktop width (default: 1280)
|
|
7839
7938
|
-H, --height N desktop height (default: 800)
|
|
7840
|
-
-o, --out FILE where a screenshot goes (default: screen.png)
|
|
7841
|
-
--max-edge N shrink a screenshot to fit N
|
|
7842
7939
|
--view-port N port for the window (default: 7373)
|
|
7843
7940
|
--idle MINUTES close an unused session (default: 0, never)
|
|
7844
7941
|
--headless no window (same as --no-view)
|
|
7845
7942
|
--no-view no window
|
|
7846
7943
|
--no-open window, but do not launch a browser
|
|
7847
|
-
--no-reuse a
|
|
7944
|
+
--no-reuse a second session beside one that is already open
|
|
7945
|
+
|
|
7946
|
+
Options for the rest
|
|
7947
|
+
--session NAME which session to drive (default: the last opened)
|
|
7948
|
+
-o, --out FILE where a screenshot goes (default: screen.png)
|
|
7949
|
+
--max-edge N shrink a screenshot to fit N
|
|
7848
7950
|
--json machine-readable result on stdout
|
|
7849
7951
|
|
|
7850
7952
|
The password comes from AI_REMOTE_PASSWORD (or REMOTECTL_PASSWORD). There is no
|
|
@@ -7871,6 +7973,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
7871
7973
|
"view-port",
|
|
7872
7974
|
"idle",
|
|
7873
7975
|
"name",
|
|
7976
|
+
"session",
|
|
7874
7977
|
"ssh-user",
|
|
7875
7978
|
"ssh-port",
|
|
7876
7979
|
"settle"
|
|
@@ -7888,7 +7991,7 @@ function parse(argv) {
|
|
|
7888
7991
|
if (VALUE_FLAGS.has(name)) flags[name] = argv[++i] ?? "";
|
|
7889
7992
|
else flags[name] = true;
|
|
7890
7993
|
}
|
|
7891
|
-
return { command: positional[0] ?? "",
|
|
7994
|
+
return { command: positional[0] ?? "", rest: positional.slice(1), flags };
|
|
7892
7995
|
}
|
|
7893
7996
|
var flag = (args, ...names) => {
|
|
7894
7997
|
for (const name of names) if (typeof args.flags[name] === "string") return args.flags[name];
|
|
@@ -7908,29 +8011,38 @@ var CliError = class extends Error {
|
|
|
7908
8011
|
code;
|
|
7909
8012
|
};
|
|
7910
8013
|
var password = () => process.env.AI_REMOTE_PASSWORD ?? process.env.REMOTECTL_PASSWORD ?? "";
|
|
7911
|
-
function
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
const meta = JSON.parse(readFileSync(`${RUN_DIR}/${file}`, "utf8"));
|
|
7916
|
-
try {
|
|
7917
|
-
process.kill(meta.pid, 0);
|
|
7918
|
-
} catch {
|
|
7919
|
-
cleanup(meta.name);
|
|
7920
|
-
return [];
|
|
7921
|
-
}
|
|
7922
|
-
return [meta];
|
|
7923
|
-
} catch {
|
|
7924
|
-
return [];
|
|
7925
|
-
}
|
|
7926
|
-
});
|
|
8014
|
+
function currentSession(args) {
|
|
8015
|
+
const chosen = pickSession(liveSessions(), flag(args, "session", "name"), readCurrent(), NAME);
|
|
8016
|
+
if ("problem" in chosen) throw new CliError(chosen.problem, 12);
|
|
8017
|
+
return chosen.session;
|
|
7927
8018
|
}
|
|
7928
|
-
function
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
}
|
|
7933
|
-
|
|
8019
|
+
function refuseTarget(command, token) {
|
|
8020
|
+
const address = /^(\d{1,3}\.){3}\d{1,3}(:\d+)?$/.test(token) || /^[\w-]+(\.[\w-]+)+(:\d+)?$/.test(token) || /^localhost(:\d+)?$/i.test(token);
|
|
8021
|
+
throw new CliError(
|
|
8022
|
+
address ? `\`${command}\` does not take a host: it drives the session \`${NAME} open\` left running.
|
|
8023
|
+
${NAME} open ${token} then ${NAME} ${command} ...
|
|
8024
|
+
--session NAME picks between several; \`${NAME} list\` shows what is open.` : `\`${command}\` takes no argument: it acts on the session \`${NAME} open\` left running.
|
|
8025
|
+
${NAME} ${command} --session ${token} acts on another one
|
|
8026
|
+
\`${NAME} list\` shows what is open.`,
|
|
8027
|
+
12
|
|
8028
|
+
);
|
|
8029
|
+
}
|
|
8030
|
+
var TAKES_NOTHING = /* @__PURE__ */ new Set(["shot", "view", "status", "shell", "close"]);
|
|
8031
|
+
var COORDINATE = /^-?\d+\s*,\s*-?\d+$/;
|
|
8032
|
+
function refuseLegacyTarget(args) {
|
|
8033
|
+
const { command, rest } = args;
|
|
8034
|
+
if (TAKES_NOTHING.has(command) && rest.length) refuseTarget(command, rest[0]);
|
|
8035
|
+
const spec = flag(args, "at") ?? rest[0];
|
|
8036
|
+
if (command === "click" && spec && !COORDINATE.test(spec) && rest[1] && COORDINATE.test(rest[1])) {
|
|
8037
|
+
refuseTarget("click", spec);
|
|
8038
|
+
}
|
|
8039
|
+
if (command === "key" && rest[0] && /[.:]/.test(rest[0])) refuseTarget("key", rest[0]);
|
|
8040
|
+
}
|
|
8041
|
+
function refuseSessionHost(args, command, session) {
|
|
8042
|
+
if (args.rest.length < 2) return;
|
|
8043
|
+
const token = args.rest[0].replace(/^\w+:\/\//, "");
|
|
8044
|
+
if (token === session.host || token === `${session.host}:${session.port}` || token === session.name) {
|
|
8045
|
+
refuseTarget(command, args.rest[0]);
|
|
7934
8046
|
}
|
|
7935
8047
|
}
|
|
7936
8048
|
async function ask(name, op, args = {}) {
|
|
@@ -7942,6 +8054,14 @@ async function ask(name, op, args = {}) {
|
|
|
7942
8054
|
if (!response.ok) throw new CliError(response.error ?? `The session refused "${op}".`, response.code ?? 1);
|
|
7943
8055
|
return response.result;
|
|
7944
8056
|
}
|
|
8057
|
+
async function tell(session, op, args = {}) {
|
|
8058
|
+
const result = await ask(session.name, op, args);
|
|
8059
|
+
if (result === null) {
|
|
8060
|
+
cleanup(session.name);
|
|
8061
|
+
throw new CliError(`The session ${session.name} is no longer answering. It has been cleaned up.`, 12);
|
|
8062
|
+
}
|
|
8063
|
+
return result;
|
|
8064
|
+
}
|
|
7945
8065
|
async function startSession(args, name, host, port) {
|
|
7946
8066
|
const daemonArgs = [
|
|
7947
8067
|
process.argv[1],
|
|
@@ -7965,8 +8085,10 @@ async function startSession(args, name, host, port) {
|
|
|
7965
8085
|
flag(args, "ssh-port") ?? "22",
|
|
7966
8086
|
"--view-port",
|
|
7967
8087
|
flag(args, "view-port") ?? "7373",
|
|
8088
|
+
// Zero unless asked for: a session stays open until it is closed. An agent
|
|
8089
|
+
// that pauses to think is not an agent that has finished.
|
|
7968
8090
|
"--idle",
|
|
7969
|
-
flag(args, "idle") ?? "
|
|
8091
|
+
flag(args, "idle") ?? "0"
|
|
7970
8092
|
];
|
|
7971
8093
|
if (has(args, "no-view", "headless")) daemonArgs.push("--no-view");
|
|
7972
8094
|
if (has(args, "no-open")) daemonArgs.push("--no-open");
|
|
@@ -8000,38 +8122,84 @@ async function main() {
|
|
|
8000
8122
|
return;
|
|
8001
8123
|
}
|
|
8002
8124
|
const json = has(args, "json");
|
|
8003
|
-
if (args.command === "
|
|
8125
|
+
if (args.command === "__session") {
|
|
8126
|
+
const { host, port } = splitTarget(args.rest[0] ?? "", 3389);
|
|
8127
|
+
await runDaemon({
|
|
8128
|
+
name: sessionName(host, port, flag(args, "name", "session")),
|
|
8129
|
+
host,
|
|
8130
|
+
port,
|
|
8131
|
+
username: flag(args, "u", "user") ?? "",
|
|
8132
|
+
password: password(),
|
|
8133
|
+
domain: flag(args, "d", "domain") ?? "",
|
|
8134
|
+
width: Number(flag(args, "W", "width") ?? 1280),
|
|
8135
|
+
height: Number(flag(args, "H", "height") ?? 800),
|
|
8136
|
+
security: flag(args, "s", "security") ?? "auto",
|
|
8137
|
+
clientName: "ai-remote",
|
|
8138
|
+
view: !has(args, "no-view", "headless"),
|
|
8139
|
+
launchBrowser: !has(args, "no-open"),
|
|
8140
|
+
viewPort: Number(flag(args, "view-port") ?? 7373),
|
|
8141
|
+
sshUsername: flag(args, "ssh-user") ?? flag(args, "u", "user") ?? "",
|
|
8142
|
+
sshPort: Number(flag(args, "ssh-port") ?? 22),
|
|
8143
|
+
tab: has(args, "tab"),
|
|
8144
|
+
fullscreen: has(args, "fullscreen"),
|
|
8145
|
+
idleMs: Math.max(0, Number(flag(args, "idle") ?? 0)) * 6e4
|
|
8146
|
+
});
|
|
8147
|
+
return;
|
|
8148
|
+
}
|
|
8149
|
+
if (args.command === "__window") {
|
|
8150
|
+
await runWindow(process.argv.slice(3));
|
|
8151
|
+
return;
|
|
8152
|
+
}
|
|
8153
|
+
if (args.command === "list") {
|
|
8004
8154
|
const sessions = liveSessions();
|
|
8155
|
+
const current = readCurrent();
|
|
8005
8156
|
if (json) {
|
|
8006
|
-
process.stdout.write(`${JSON.stringify({ sessions })}
|
|
8157
|
+
process.stdout.write(`${JSON.stringify({ current, sessions })}
|
|
8007
8158
|
`);
|
|
8008
8159
|
return;
|
|
8009
8160
|
}
|
|
8010
8161
|
if (!sessions.length) {
|
|
8011
|
-
process.stdout.write(
|
|
8162
|
+
process.stdout.write(`No sessions are open. \`${NAME} open <host>\` starts one.
|
|
8163
|
+
`);
|
|
8012
8164
|
return;
|
|
8013
8165
|
}
|
|
8014
8166
|
for (const meta of sessions) {
|
|
8015
|
-
process.stdout.write(`${meta.name} ${meta.host}:${meta.port} ${meta.width}x${meta.height} pid ${meta.pid}${meta.viewer ? ` ${meta.viewer}` : ""}
|
|
8167
|
+
process.stdout.write(`${meta.name === current ? "*" : " "} ${meta.name} ${meta.host}:${meta.port} ${meta.width}x${meta.height} pid ${meta.pid}${meta.viewer ? ` ${meta.viewer}` : ""}
|
|
8016
8168
|
`);
|
|
8017
8169
|
}
|
|
8018
8170
|
return;
|
|
8019
8171
|
}
|
|
8020
|
-
if (
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
const
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8172
|
+
if (args.command === "open") {
|
|
8173
|
+
const target = args.rest[0];
|
|
8174
|
+
if (!target) throw new CliError(`Name the machine to connect to: \`${NAME} open <host[:port]>\``, 12);
|
|
8175
|
+
const { host, port } = splitTarget(target, 3389);
|
|
8176
|
+
const asked = flag(args, "session", "name");
|
|
8177
|
+
const base = sessionName(host, port, asked);
|
|
8178
|
+
const name2 = has(args, "no-reuse") && !asked ? freeName(base, liveSessions().map((meta) => meta.name)) : base;
|
|
8179
|
+
let status = has(args, "no-reuse") ? null : await ask(name2, "status");
|
|
8180
|
+
const reused = Boolean(status);
|
|
8181
|
+
if (!status) status = await startSession(args, name2, host, port);
|
|
8182
|
+
if (!has(args, "no-view", "headless") && !status.viewer) {
|
|
8183
|
+
Object.assign(status, await ask(name2, "view-open", { launch: !has(args, "no-open") }));
|
|
8184
|
+
}
|
|
8185
|
+
setCurrent(name2);
|
|
8186
|
+
report(json, "open", {
|
|
8187
|
+
host,
|
|
8188
|
+
port,
|
|
8189
|
+
name: name2,
|
|
8190
|
+
reused,
|
|
8191
|
+
width: status.width,
|
|
8192
|
+
height: status.height,
|
|
8193
|
+
viewer: status.viewer,
|
|
8194
|
+
window: status.window,
|
|
8195
|
+
viewers: status.viewers
|
|
8196
|
+
});
|
|
8032
8197
|
return;
|
|
8033
8198
|
}
|
|
8034
8199
|
if (args.command === "probe") {
|
|
8200
|
+
const target = args.rest[0];
|
|
8201
|
+
const where = target ? splitTarget(target, 3389) : currentSession(args);
|
|
8202
|
+
const { host, port } = where;
|
|
8035
8203
|
const { reachable, ms, detail } = await probe(host, port);
|
|
8036
8204
|
if (json) process.stdout.write(`${JSON.stringify({ host, port, reachable, ms, detail })}
|
|
8037
8205
|
`);
|
|
@@ -8040,106 +8208,103 @@ async function main() {
|
|
|
8040
8208
|
if (!reachable) throw new CliError(detail, 10);
|
|
8041
8209
|
return;
|
|
8042
8210
|
}
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
view: !has(args, "no-view", "headless"),
|
|
8056
|
-
launchBrowser: !has(args, "no-open"),
|
|
8057
|
-
viewPort: Number(flag(args, "view-port") ?? 7373),
|
|
8058
|
-
sshUsername: flag(args, "ssh-user") ?? flag(args, "u", "user") ?? "",
|
|
8059
|
-
sshPort: Number(flag(args, "ssh-port") ?? 22),
|
|
8060
|
-
tab: has(args, "tab"),
|
|
8061
|
-
fullscreen: has(args, "fullscreen"),
|
|
8062
|
-
// Zero by default: a session stays open until it is closed. An agent
|
|
8063
|
-
// that pauses to think is not an agent that has finished.
|
|
8064
|
-
idleMs: Math.max(0, Number(flag(args, "idle") ?? 0)) * 6e4
|
|
8065
|
-
});
|
|
8211
|
+
refuseLegacyTarget(args);
|
|
8212
|
+
if (args.command === "close") {
|
|
8213
|
+
const targets = has(args, "all") ? liveSessions().map((meta) => meta.name) : [currentSession(args).name];
|
|
8214
|
+
for (const target of targets) {
|
|
8215
|
+
await ask(target, "close").catch(() => null);
|
|
8216
|
+
cleanup(target);
|
|
8217
|
+
process.stdout.write(`Closed ${target}.
|
|
8218
|
+
`);
|
|
8219
|
+
}
|
|
8220
|
+
if (!targets.length) process.stdout.write("No sessions are open.\n");
|
|
8221
|
+
const left = liveSessions();
|
|
8222
|
+
if (!readCurrent() && left.length) setCurrent(left[left.length - 1].name);
|
|
8066
8223
|
return;
|
|
8067
8224
|
}
|
|
8068
|
-
if (args.command === "
|
|
8069
|
-
|
|
8225
|
+
if (args.command === "status" && !liveSessions().length) {
|
|
8226
|
+
if (json) process.stdout.write(`${JSON.stringify({ ok: true, sessions: [] })}
|
|
8227
|
+
`);
|
|
8228
|
+
else process.stdout.write("No sessions are open.\n");
|
|
8070
8229
|
return;
|
|
8071
8230
|
}
|
|
8231
|
+
const session = currentSession(args);
|
|
8232
|
+
const { name } = session;
|
|
8072
8233
|
if (args.command === "shell") {
|
|
8073
|
-
await interactiveShell(args,
|
|
8234
|
+
await interactiveShell(args, session);
|
|
8074
8235
|
return;
|
|
8075
8236
|
}
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8237
|
+
const summary = {
|
|
8238
|
+
host: session.host,
|
|
8239
|
+
port: session.port,
|
|
8240
|
+
name,
|
|
8241
|
+
width: session.width,
|
|
8242
|
+
height: session.height
|
|
8243
|
+
};
|
|
8080
8244
|
switch (args.command) {
|
|
8081
|
-
case "
|
|
8082
|
-
|
|
8083
|
-
Object.assign(status, await ask(name, "view-open", { launch: !has(args, "no-open") }));
|
|
8084
|
-
}
|
|
8085
|
-
Object.assign(report, { viewer: status.viewer, window: status.window, viewers: status.viewers });
|
|
8245
|
+
case "status": {
|
|
8246
|
+
Object.assign(summary, await tell(session, "status"));
|
|
8086
8247
|
break;
|
|
8248
|
+
}
|
|
8087
8249
|
case "view": {
|
|
8088
8250
|
if (has(args, "close")) {
|
|
8089
|
-
const { closed } = await
|
|
8090
|
-
Object.assign(
|
|
8251
|
+
const { closed } = await tell(session, "view-close");
|
|
8252
|
+
Object.assign(summary, { viewer: null, closed });
|
|
8091
8253
|
break;
|
|
8092
8254
|
}
|
|
8093
|
-
Object.assign(
|
|
8255
|
+
Object.assign(summary, await tell(session, "view-open", { launch: !has(args, "no-open") }));
|
|
8094
8256
|
break;
|
|
8095
8257
|
}
|
|
8096
8258
|
case "shot": {
|
|
8097
8259
|
const file = resolvePath(flag(args, "o", "out") ?? "screen.png");
|
|
8098
8260
|
const maxEdge = Number(flag(args, "max-edge") ?? 0);
|
|
8099
|
-
Object.assign(
|
|
8261
|
+
Object.assign(summary, await tell(session, "shot", { file, maxEdge }));
|
|
8100
8262
|
break;
|
|
8101
8263
|
}
|
|
8102
8264
|
case "click": {
|
|
8103
8265
|
const spec = flag(args, "at") ?? args.rest[0];
|
|
8104
|
-
if (!spec) throw new CliError(
|
|
8266
|
+
if (!spec) throw new CliError(`click needs a position, e.g. \`${NAME} click 640,400\``, 12);
|
|
8105
8267
|
const [x, y] = spec.split(",").map(Number);
|
|
8106
8268
|
const button2 = flag(args, "button") ?? "left";
|
|
8107
8269
|
const step = `click ${x},${y}${button2 === "left" ? "" : `,${button2}`}`;
|
|
8108
|
-
await
|
|
8109
|
-
Object.assign(
|
|
8270
|
+
await tell(session, "script", { script: has(args, "double") ? `${step}; ${step}` : step });
|
|
8271
|
+
Object.assign(summary, { clicked: { x, y, button: button2 } });
|
|
8110
8272
|
break;
|
|
8111
8273
|
}
|
|
8112
8274
|
case "type": {
|
|
8275
|
+
refuseSessionHost(args, "type", session);
|
|
8113
8276
|
const text = args.rest.join(" ");
|
|
8114
8277
|
if (!text) throw new CliError("type needs some text", 12);
|
|
8115
|
-
await
|
|
8116
|
-
Object.assign(
|
|
8278
|
+
await tell(session, "script", { script: `type ${text}` });
|
|
8279
|
+
Object.assign(summary, { typed: text.length });
|
|
8117
8280
|
break;
|
|
8118
8281
|
}
|
|
8119
8282
|
case "key": {
|
|
8120
8283
|
if (!args.rest.length) throw new CliError("key needs at least one code, e.g. MetaLeft", 12);
|
|
8121
|
-
await
|
|
8122
|
-
Object.assign(
|
|
8284
|
+
await tell(session, "script", { script: args.rest.map((code) => `key ${code}`).join("; ") });
|
|
8285
|
+
Object.assign(summary, { keys: args.rest });
|
|
8123
8286
|
break;
|
|
8124
8287
|
}
|
|
8125
8288
|
case "do": {
|
|
8289
|
+
refuseSessionHost(args, "do", session);
|
|
8126
8290
|
const script = args.rest.join(" ");
|
|
8127
8291
|
if (!script) throw new CliError('do needs a script, e.g. "key MetaLeft; wait 800; shot s.png"', 12);
|
|
8128
|
-
Object.assign(
|
|
8292
|
+
Object.assign(summary, await tell(session, "script", { script }));
|
|
8129
8293
|
break;
|
|
8130
8294
|
}
|
|
8131
8295
|
case "exec": {
|
|
8296
|
+
refuseSessionHost(args, "exec", session);
|
|
8132
8297
|
const command = args.rest.join(" ");
|
|
8133
8298
|
if (!command) throw new CliError("exec needs a command to run", 12);
|
|
8134
|
-
const result = await
|
|
8135
|
-
Object.assign(
|
|
8299
|
+
const result = await tell(session, "exec", { command });
|
|
8300
|
+
Object.assign(summary, result);
|
|
8136
8301
|
if (!json) {
|
|
8137
8302
|
process.stdout.write(result.output.endsWith("\n") ? result.output : `${result.output}
|
|
8138
8303
|
`);
|
|
8139
8304
|
if (result.exitStatus) process.stderr.write(`exit ${result.exitStatus}
|
|
8140
8305
|
`);
|
|
8141
8306
|
}
|
|
8142
|
-
if (json) process.stdout.write(`${JSON.stringify({ ok: result.exitStatus === 0, ...
|
|
8307
|
+
if (json) process.stdout.write(`${JSON.stringify({ ok: result.exitStatus === 0, ...summary })}
|
|
8143
8308
|
`);
|
|
8144
8309
|
process.exit(result.exitStatus === 0 ? 0 : 1);
|
|
8145
8310
|
return;
|
|
@@ -8147,16 +8312,29 @@ async function main() {
|
|
|
8147
8312
|
default:
|
|
8148
8313
|
throw new CliError(`Unknown command "${args.command}".`, 12);
|
|
8149
8314
|
}
|
|
8150
|
-
|
|
8315
|
+
report(json, args.command, summary);
|
|
8316
|
+
}
|
|
8317
|
+
function report(json, command, summary) {
|
|
8318
|
+
if (json) process.stdout.write(`${JSON.stringify({ ok: true, ...summary })}
|
|
8151
8319
|
`);
|
|
8152
|
-
else process.stdout.write(`${describe(
|
|
8320
|
+
else process.stdout.write(`${describe(command, summary)}
|
|
8153
8321
|
`);
|
|
8154
8322
|
}
|
|
8155
|
-
async function interactiveShell(args,
|
|
8323
|
+
async function interactiveShell(args, session) {
|
|
8324
|
+
const username = bareUsername(flag(args, "ssh-user") ?? session.sshUser ?? "");
|
|
8325
|
+
if (!username) {
|
|
8326
|
+
throw new CliError(
|
|
8327
|
+
`The session ${session.name} has no account for a terminal. Open it with -u, or pass --ssh-user here.`,
|
|
8328
|
+
12
|
|
8329
|
+
);
|
|
8330
|
+
}
|
|
8331
|
+
if (!password()) {
|
|
8332
|
+
throw new CliError("The terminal signs in for itself, so it needs AI_REMOTE_PASSWORD in this shell.", 11);
|
|
8333
|
+
}
|
|
8156
8334
|
const shell = new Shell({
|
|
8157
|
-
host,
|
|
8158
|
-
port: Number(flag(args, "ssh-port") ?? 22),
|
|
8159
|
-
username
|
|
8335
|
+
host: session.host,
|
|
8336
|
+
port: Number(flag(args, "ssh-port") ?? session.sshPort ?? 22),
|
|
8337
|
+
username,
|
|
8160
8338
|
password: password(),
|
|
8161
8339
|
columns: process.stdout.columns ?? 120,
|
|
8162
8340
|
rows: process.stdout.rows ?? 30
|
|
@@ -8194,26 +8372,29 @@ async function probe(host, port) {
|
|
|
8194
8372
|
socket.on("error", (error) => done(false, error.message));
|
|
8195
8373
|
});
|
|
8196
8374
|
}
|
|
8197
|
-
function describe(command,
|
|
8198
|
-
const size = `${
|
|
8199
|
-
const where = report.reused ? "the open session" : "a new session";
|
|
8375
|
+
function describe(command, summary) {
|
|
8376
|
+
const size = `${summary.width}x${summary.height}`;
|
|
8200
8377
|
if (command === "open") {
|
|
8201
|
-
const window =
|
|
8202
|
-
return `${
|
|
8203
|
-
Run more commands
|
|
8378
|
+
const window = summary.window === "window" ? "A window is open on it." : summary.window === "tab" ? "It opened in your browser." : summary.viewer ? `Open it at ${summary.viewer}` : "No window (headless).";
|
|
8379
|
+
return `${summary.reused ? "Already connected" : "Connected"} to ${summary.host}:${summary.port} at ${size}. ${window}
|
|
8380
|
+
Session ${summary.name}. Run more commands with no host, then \`${NAME} close\` when you are done.`;
|
|
8381
|
+
}
|
|
8382
|
+
if (command === "status") {
|
|
8383
|
+
return `${summary.name} ${summary.host}:${summary.port} ${size} ${summary.connected ? "connected" : "disconnected"} terminal ${summary.shell}
|
|
8384
|
+
${summary.viewer ? `Window: ${summary.viewer} (${summary.viewers} watching)` : "No window."}`;
|
|
8204
8385
|
}
|
|
8205
8386
|
if (command === "view") {
|
|
8206
|
-
if (
|
|
8207
|
-
return
|
|
8387
|
+
if (summary.viewer) return `Window: ${summary.viewer}`;
|
|
8388
|
+
return summary.closed ? "Closed the window. The session is still open." : "There was no window open.";
|
|
8208
8389
|
}
|
|
8209
|
-
if (command === "shot") return `Wrote ${
|
|
8390
|
+
if (command === "shot") return `Wrote ${summary.file} (${summary.width}x${summary.height}) from ${summary.name}.`;
|
|
8210
8391
|
if (command === "click") {
|
|
8211
|
-
const { x, y } =
|
|
8392
|
+
const { x, y } = summary.clicked;
|
|
8212
8393
|
return `Clicked (${x}, ${y}) on a ${size} desktop.`;
|
|
8213
8394
|
}
|
|
8214
|
-
if (command === "type") return `Typed ${
|
|
8215
|
-
if (command === "key") return `Pressed ${
|
|
8216
|
-
if (command === "do") return `Ran ${
|
|
8395
|
+
if (command === "type") return `Typed ${summary.typed} characters.`;
|
|
8396
|
+
if (command === "key") return `Pressed ${summary.keys.join(", ")}.`;
|
|
8397
|
+
if (command === "do") return `Ran ${summary.steps.length} steps: ${summary.steps.join(" \u2192 ")}`;
|
|
8217
8398
|
return "Done.";
|
|
8218
8399
|
}
|
|
8219
8400
|
main().catch((error) => {
|