ai-remote 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/SKILL.md +123 -13
- package/dist/{cli-chunk-Q33YGIDO.mjs → cli-chunk-L6E2YYPQ.mjs} +19 -5
- package/dist/{cli-chunk-F2TSY3YW.mjs → cli-chunk-P4645KKB.mjs} +3 -3
- package/dist/{cli-chunk-OZG2CODR.mjs → cli-chunk-TKKENOSJ.mjs} +1 -1
- package/dist/{cli-chunk-S2TWL5LF.mjs → cli-chunk-UD2YES3P.mjs} +107 -39
- package/dist/cli-chunk-WWORJ2NE.mjs +72 -0
- package/dist/{cli-copy-BOF44DWQ.mjs → cli-copy-2HQU2E7D.mjs} +18 -5
- package/dist/{cli-daemon-3BBFQCML.mjs → cli-daemon-3L36TBIB.mjs} +852 -274
- package/dist/{cli-identities-ZILT4XCR.mjs → cli-identities-GBQWYF2O.mjs} +4 -4
- package/dist/cli-known-hosts-UU427T4N.mjs +12 -0
- package/dist/{cli-secrets-26D3F6EN.mjs → cli-secrets-BMYFPDB2.mjs} +1 -1
- package/dist/cli-shell-JEC6BEME.mjs +11 -0
- package/dist/{cli-window-ZKAXEHQV.mjs → cli-window-AWXZ5NJX.mjs} +2 -2
- package/dist/cli.mjs +219 -109
- package/dist/computer.d.ts +77 -0
- package/dist/computer.js +3 -0
- package/dist/index.d.ts +80 -1
- package/dist/index.js +1 -1
- package/dist/protocols.d.ts +247 -17
- package/dist/protocols.js +16 -16
- package/package.json +6 -1
- package/dist/cli-shell-45XRTE7O.mjs +0 -10
package/dist/cli.mjs
CHANGED
|
@@ -13,21 +13,108 @@ import {
|
|
|
13
13
|
metaPath,
|
|
14
14
|
sessionName,
|
|
15
15
|
socketPath
|
|
16
|
-
} from "./cli-chunk-
|
|
16
|
+
} from "./cli-chunk-TKKENOSJ.mjs";
|
|
17
17
|
|
|
18
18
|
// src/cli/cli.ts
|
|
19
19
|
import { resolve as resolvePath } from "node:path";
|
|
20
20
|
import { readFileSync as readFileSync2, openSync } from "node:fs";
|
|
21
21
|
|
|
22
|
+
// src/cli/target.ts
|
|
23
|
+
function splitTarget(target, fallbackPort) {
|
|
24
|
+
const stripped = target.replace(/^\w+:\/\//, "");
|
|
25
|
+
const at = stripped.lastIndexOf("@");
|
|
26
|
+
const account = at === -1 ? "" : stripped.slice(0, at);
|
|
27
|
+
const [host, port] = (at === -1 ? stripped : stripped.slice(at + 1)).split(":");
|
|
28
|
+
return { account, host, port: port ? Number(port) : fallbackPort };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/cli/args.ts
|
|
32
|
+
var CliError = class extends Error {
|
|
33
|
+
constructor(message, code) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.code = code;
|
|
36
|
+
}
|
|
37
|
+
code;
|
|
38
|
+
};
|
|
39
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
40
|
+
"u",
|
|
41
|
+
"user",
|
|
42
|
+
"d",
|
|
43
|
+
"domain",
|
|
44
|
+
"s",
|
|
45
|
+
"security",
|
|
46
|
+
"W",
|
|
47
|
+
"width",
|
|
48
|
+
"H",
|
|
49
|
+
"height",
|
|
50
|
+
"o",
|
|
51
|
+
"out",
|
|
52
|
+
"at",
|
|
53
|
+
"button",
|
|
54
|
+
"max-edge",
|
|
55
|
+
"view-port",
|
|
56
|
+
"idle",
|
|
57
|
+
"name",
|
|
58
|
+
"session",
|
|
59
|
+
"ssh-user",
|
|
60
|
+
"ssh-port",
|
|
61
|
+
"settle",
|
|
62
|
+
"vnc-port",
|
|
63
|
+
"port",
|
|
64
|
+
"keys",
|
|
65
|
+
"region"
|
|
66
|
+
]);
|
|
67
|
+
var OPTIONAL_VALUE_FLAGS = { shot: /\.(png|jpe?g)$/i };
|
|
68
|
+
function parse(argv) {
|
|
69
|
+
const flags = {};
|
|
70
|
+
const positional = [];
|
|
71
|
+
const identities = [];
|
|
72
|
+
for (let i = 0; i < argv.length; i++) {
|
|
73
|
+
const token = argv[i];
|
|
74
|
+
if (!token.startsWith("-") || token === "-") {
|
|
75
|
+
positional.push(token);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const split = token.indexOf("=");
|
|
79
|
+
const name = (split === -1 ? token : token.slice(0, split)).replace(/^--?/, "");
|
|
80
|
+
const attached = split === -1 ? void 0 : token.slice(split + 1);
|
|
81
|
+
if (name === "i" || name === "identity") {
|
|
82
|
+
identities.push(attached ?? argv[++i] ?? "");
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (attached !== void 0) {
|
|
86
|
+
if (!VALUE_FLAGS.has(name) && !(name in OPTIONAL_VALUE_FLAGS)) {
|
|
87
|
+
throw new CliError(`--${name} takes no value, so --${name}=... is not a thing.`, 12);
|
|
88
|
+
}
|
|
89
|
+
flags[name] = attached;
|
|
90
|
+
} else if (VALUE_FLAGS.has(name)) flags[name] = argv[++i] ?? "";
|
|
91
|
+
else if (name in OPTIONAL_VALUE_FLAGS) {
|
|
92
|
+
const next = argv[i + 1];
|
|
93
|
+
flags[name] = next !== void 0 && OPTIONAL_VALUE_FLAGS[name].test(next) ? argv[++i] : true;
|
|
94
|
+
} else flags[name] = true;
|
|
95
|
+
}
|
|
96
|
+
return { command: positional[0] ?? "", rest: positional.slice(1), flags, identities };
|
|
97
|
+
}
|
|
98
|
+
var flag = (args, ...names) => {
|
|
99
|
+
for (const name of names) if (typeof args.flags[name] === "string") return args.flags[name];
|
|
100
|
+
return void 0;
|
|
101
|
+
};
|
|
102
|
+
var has = (args, ...names) => names.some((name) => args.flags[name] === true);
|
|
103
|
+
var shotFile = (args) => {
|
|
104
|
+
const value = args.flags.shot;
|
|
105
|
+
if (value === void 0) return void 0;
|
|
106
|
+
return typeof value === "string" && value || flag(args, "o", "out") || "screen.png";
|
|
107
|
+
};
|
|
108
|
+
|
|
22
109
|
// src/cli/prompt.ts
|
|
23
110
|
function canPrompt() {
|
|
24
|
-
return
|
|
111
|
+
return process.stdin.isTTY && !process.env.AI_REMOTE_NO_PROMPT;
|
|
25
112
|
}
|
|
26
113
|
function askPassword(label) {
|
|
27
114
|
if (!canPrompt()) return Promise.resolve("");
|
|
28
115
|
return new Promise((resolve, reject) => {
|
|
29
116
|
const input = process.stdin;
|
|
30
|
-
const wasRaw =
|
|
117
|
+
const wasRaw = input.isRaw;
|
|
31
118
|
const bytes = [];
|
|
32
119
|
const done = (finish) => {
|
|
33
120
|
input.off("data", onData);
|
|
@@ -171,6 +258,7 @@ The rest drive that session, and take no host:
|
|
|
171
258
|
${NAME} clipboard [on|off] share this machine's clipboard both ways
|
|
172
259
|
${NAME} cp <src> <dst> copy files; a remote path starts with ':'
|
|
173
260
|
${NAME} do "steps" several actions in one go
|
|
261
|
+
${NAME} batch actions.json structured actions; - reads JSON from stdin
|
|
174
262
|
${NAME} vnc give this session a screen over VNC
|
|
175
263
|
(--vnc-port N, default 5900)
|
|
176
264
|
${NAME} view open the window on the session
|
|
@@ -180,6 +268,8 @@ The rest drive that session, and take no host:
|
|
|
180
268
|
${NAME} list every session open, and every password kept
|
|
181
269
|
${NAME} saved machines with a password kept
|
|
182
270
|
${NAME} forget [<host[:port]>] throw one away; --all throws all
|
|
271
|
+
${NAME} hosts SSH host keys this machine has accepted
|
|
272
|
+
${NAME} forget-host <host[:port]> stop trusting one, after a rebuild
|
|
183
273
|
${NAME} close close it; --all closes every session
|
|
184
274
|
${NAME} probe [<host[:port]>] is anything listening?
|
|
185
275
|
${NAME} --version which version this is
|
|
@@ -219,6 +309,10 @@ Options for open
|
|
|
219
309
|
--side-panel open the SSH terminal docked on the right
|
|
220
310
|
--audio play the remote computer's sound in the window (RDP)
|
|
221
311
|
--no-clipboard do not share this machine's clipboard (shared by default)
|
|
312
|
+
--insecure-host-key
|
|
313
|
+
accept an SSH host key that does not match the one on
|
|
314
|
+
file. For a machine you rebuild often, and nothing else:
|
|
315
|
+
a changed key is otherwise refused, which is the point
|
|
222
316
|
--keys MODE adapt | literal (default: adapt)
|
|
223
317
|
adapt swaps Control and Command when one end is a Mac,
|
|
224
318
|
so Command-C copies on Windows and Control-C copies on
|
|
@@ -238,7 +332,14 @@ Options for the rest
|
|
|
238
332
|
-r, --recursive for cp: copy a directory
|
|
239
333
|
--reconnect for exec: a new terminal first, then the command
|
|
240
334
|
-o, --out FILE where a screenshot goes (default: screen.png)
|
|
335
|
+
--shot [FILE.png] for click, type, key, do and batch: screenshot the
|
|
336
|
+
steps produced, in the same command. Waits for the
|
|
337
|
+
frame they caused rather than a fixed delay, and says
|
|
338
|
+
so when nothing on screen changed. Bare --shot writes
|
|
339
|
+
to -o, or to screen.png; a name that is not a .png
|
|
340
|
+
has to be given as --shot=NAME
|
|
241
341
|
--max-edge N shrink a screenshot to fit N
|
|
342
|
+
--region X,Y,W,H capture a desktop region; --json reports its origin
|
|
242
343
|
--json machine-readable result on stdout
|
|
243
344
|
|
|
244
345
|
Signing in
|
|
@@ -260,63 +361,6 @@ Signing in
|
|
|
260
361
|
|
|
261
362
|
Exit codes: 0 ok \xB7 10 unreachable \xB7 11 auth rejected \xB7 12 bad usage \xB7 13 timeout
|
|
262
363
|
`;
|
|
263
|
-
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
264
|
-
"u",
|
|
265
|
-
"user",
|
|
266
|
-
"d",
|
|
267
|
-
"domain",
|
|
268
|
-
"s",
|
|
269
|
-
"security",
|
|
270
|
-
"W",
|
|
271
|
-
"width",
|
|
272
|
-
"H",
|
|
273
|
-
"height",
|
|
274
|
-
"o",
|
|
275
|
-
"out",
|
|
276
|
-
"at",
|
|
277
|
-
"button",
|
|
278
|
-
"max-edge",
|
|
279
|
-
"view-port",
|
|
280
|
-
"idle",
|
|
281
|
-
"name",
|
|
282
|
-
"session",
|
|
283
|
-
"ssh-user",
|
|
284
|
-
"ssh-port",
|
|
285
|
-
"settle",
|
|
286
|
-
"vnc-port",
|
|
287
|
-
"port",
|
|
288
|
-
"keys"
|
|
289
|
-
]);
|
|
290
|
-
function parse(argv) {
|
|
291
|
-
const flags = {};
|
|
292
|
-
const positional = [];
|
|
293
|
-
const identities = [];
|
|
294
|
-
for (let i = 0; i < argv.length; i++) {
|
|
295
|
-
const token = argv[i];
|
|
296
|
-
if (!token.startsWith("-") || token === "-") {
|
|
297
|
-
positional.push(token);
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
const name = token.replace(/^--?/, "");
|
|
301
|
-
if (name === "i" || name === "identity") {
|
|
302
|
-
identities.push(argv[++i] ?? "");
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (VALUE_FLAGS.has(name)) flags[name] = argv[++i] ?? "";
|
|
306
|
-
else flags[name] = true;
|
|
307
|
-
}
|
|
308
|
-
return { command: positional[0] ?? "", rest: positional.slice(1), flags, identities };
|
|
309
|
-
}
|
|
310
|
-
var flag = (args, ...names) => {
|
|
311
|
-
for (const name of names) if (typeof args.flags[name] === "string") return args.flags[name];
|
|
312
|
-
return void 0;
|
|
313
|
-
};
|
|
314
|
-
var has = (args, ...names) => names.some((name) => args.flags[name] === true);
|
|
315
|
-
function splitTarget(target, fallbackPort) {
|
|
316
|
-
const stripped = target.replace(/^\w+:\/\//, "");
|
|
317
|
-
const [host, port] = stripped.split(":");
|
|
318
|
-
return { host, port: port ? Number(port) : fallbackPort };
|
|
319
|
-
}
|
|
320
364
|
var PORTS = { rdp: 3389, vnc: 5900, ssh: 22 };
|
|
321
365
|
function modeFor(args, target) {
|
|
322
366
|
if (has(args, "ssh", "ssh-only", "terminal")) return "ssh";
|
|
@@ -328,19 +372,12 @@ function modeFor(args, target) {
|
|
|
328
372
|
return "rdp";
|
|
329
373
|
}
|
|
330
374
|
var defaultPort = (mode) => PORTS[mode];
|
|
331
|
-
var CliError = class extends Error {
|
|
332
|
-
constructor(message, code) {
|
|
333
|
-
super(message);
|
|
334
|
-
this.code = code;
|
|
335
|
-
}
|
|
336
|
-
code;
|
|
337
|
-
};
|
|
338
375
|
var password = () => process.env.AI_REMOTE_PASSWORD ?? "";
|
|
339
376
|
function splitKey(key) {
|
|
340
377
|
const at = key.lastIndexOf("@");
|
|
341
378
|
return at === -1 ? ["", key] : [key.slice(0, at), key.slice(at + 1)];
|
|
342
379
|
}
|
|
343
|
-
var secrets = () => import("./cli-secrets-
|
|
380
|
+
var secrets = () => import("./cli-secrets-BMYFPDB2.mjs");
|
|
344
381
|
async function passwordFor(host, port, username) {
|
|
345
382
|
const exported = password();
|
|
346
383
|
if (exported) return exported;
|
|
@@ -361,7 +398,7 @@ async function promptFor(host, port, username, note = "") {
|
|
|
361
398
|
}
|
|
362
399
|
function identityLoader(paths) {
|
|
363
400
|
return async () => {
|
|
364
|
-
const { loadIdentities } = await import("./cli-identities-
|
|
401
|
+
const { loadIdentities } = await import("./cli-identities-GBQWYF2O.mjs");
|
|
365
402
|
const { identities, skipped } = await loadIdentities(paths);
|
|
366
403
|
for (const note of skipped) console.log(`[ssh] skipped ${note}`);
|
|
367
404
|
console.log(identities.length ? `[ssh] keys: ${identities.map((identity) => identity.comment).join(", ")}` : "[ssh] no keys to offer");
|
|
@@ -384,7 +421,7 @@ function refuseTarget(command, token) {
|
|
|
384
421
|
12
|
|
385
422
|
);
|
|
386
423
|
}
|
|
387
|
-
var DESKTOP_ONLY = /* @__PURE__ */ new Set(["shot", "click", "type", "key", "do"]);
|
|
424
|
+
var DESKTOP_ONLY = /* @__PURE__ */ new Set(["shot", "click", "type", "key", "do", "batch"]);
|
|
388
425
|
var TAKES_NOTHING = /* @__PURE__ */ new Set(["shot", "view", "status", "shell", "close", "vnc", "reconnect"]);
|
|
389
426
|
var COORDINATE = /^-?\d+\s*,\s*-?\d+$/;
|
|
390
427
|
function refuseLegacyTarget(args) {
|
|
@@ -415,7 +452,7 @@ async function ask(name, op, args = {}, timeoutMs) {
|
|
|
415
452
|
async function tell(session, op, args = {}, timeoutMs) {
|
|
416
453
|
const result = await ask(session.name, op, args, timeoutMs).catch((error) => {
|
|
417
454
|
const message = error instanceof Error ? error.message : String(error);
|
|
418
|
-
if (
|
|
455
|
+
if (message.startsWith("Unknown operation")) {
|
|
419
456
|
throw new CliError(
|
|
420
457
|
`The session ${session.name} does not support \`${op}\`: it was started by an older version of ${NAME} and is still running it.
|
|
421
458
|
${NAME} close --session ${session.name} then open it again`,
|
|
@@ -430,13 +467,13 @@ async function tell(session, op, args = {}, timeoutMs) {
|
|
|
430
467
|
}
|
|
431
468
|
return result;
|
|
432
469
|
}
|
|
433
|
-
async function startSession(args, name, host, port, secret) {
|
|
470
|
+
async function startSession(args, name, host, port, secret, account = "") {
|
|
434
471
|
const mode = modeFor(args, `${host}:${port}`);
|
|
435
472
|
const explicitWidth = flag(args, "W", "width");
|
|
436
473
|
const explicitHeight = flag(args, "H", "height");
|
|
437
474
|
let fullscreenSize = null;
|
|
438
475
|
if (mode === "rdp" && has(args, "fullscreen") && (!explicitWidth || !explicitHeight)) {
|
|
439
|
-
const { detectCurrentDisplaySize } = await import("./cli-window-
|
|
476
|
+
const { detectCurrentDisplaySize } = await import("./cli-window-AWXZ5NJX.mjs");
|
|
440
477
|
const display = await detectCurrentDisplaySize();
|
|
441
478
|
if (display) fullscreenSize = fullscreenRdpSize(display, has(args, "side-panel"));
|
|
442
479
|
}
|
|
@@ -453,7 +490,7 @@ async function startSession(args, name, host, port, secret) {
|
|
|
453
490
|
"--name",
|
|
454
491
|
name,
|
|
455
492
|
"-u",
|
|
456
|
-
flag(args, "u", "user") ??
|
|
493
|
+
flag(args, "u", "user") ?? account,
|
|
457
494
|
"-d",
|
|
458
495
|
flag(args, "d", "domain") ?? "",
|
|
459
496
|
"-s",
|
|
@@ -463,7 +500,7 @@ async function startSession(args, name, host, port, secret) {
|
|
|
463
500
|
"-H",
|
|
464
501
|
height,
|
|
465
502
|
"--ssh-user",
|
|
466
|
-
flag(args, "ssh-user") ?? flag(args, "u", "user") ??
|
|
503
|
+
flag(args, "ssh-user") ?? flag(args, "u", "user") ?? account,
|
|
467
504
|
// In a terminal session the target port is the SSH port; there is no other.
|
|
468
505
|
"--ssh-port",
|
|
469
506
|
flag(args, "ssh-port") ?? (mode === "ssh" ? String(port) : "22"),
|
|
@@ -481,6 +518,7 @@ async function startSession(args, name, host, port, secret) {
|
|
|
481
518
|
if (has(args, "no-view", "headless")) daemonArgs.push("--no-view");
|
|
482
519
|
if (has(args, "no-open")) daemonArgs.push("--no-open");
|
|
483
520
|
if (has(args, "no-clipboard")) daemonArgs.push("--no-clipboard");
|
|
521
|
+
if (has(args, "insecure-host-key")) daemonArgs.push("--insecure-host-key");
|
|
484
522
|
if (has(args, "watch-only", "observe")) daemonArgs.push("--watch-only");
|
|
485
523
|
if (has(args, "tab")) daemonArgs.push("--tab");
|
|
486
524
|
if (has(args, "fullscreen")) daemonArgs.push("--fullscreen");
|
|
@@ -559,7 +597,7 @@ async function main() {
|
|
|
559
597
|
}
|
|
560
598
|
const json = has(args, "json");
|
|
561
599
|
if (args.command === "__display-size") {
|
|
562
|
-
const { readCurrentDisplaySize } = await import("./cli-window-
|
|
600
|
+
const { readCurrentDisplaySize } = await import("./cli-window-AWXZ5NJX.mjs");
|
|
563
601
|
const size = await readCurrentDisplaySize();
|
|
564
602
|
if (size) process.stdout.write(`${JSON.stringify(size)}
|
|
565
603
|
`);
|
|
@@ -568,7 +606,7 @@ async function main() {
|
|
|
568
606
|
if (args.command === "__session") {
|
|
569
607
|
const mode = modeFor(args, args.rest[0]);
|
|
570
608
|
const { host, port } = splitTarget(args.rest[0] ?? "", defaultPort(mode));
|
|
571
|
-
const { runDaemon } = await import("./cli-daemon-
|
|
609
|
+
const { runDaemon } = await import("./cli-daemon-3L36TBIB.mjs");
|
|
572
610
|
await runDaemon({
|
|
573
611
|
name: sessionName(host, port, flag(args, "name", "session")),
|
|
574
612
|
host,
|
|
@@ -596,12 +634,13 @@ async function main() {
|
|
|
596
634
|
audio: has(args, "audio", "sound") && mode === "rdp" && !has(args, "no-view", "headless"),
|
|
597
635
|
idleMs: Math.max(0, Number(flag(args, "idle") ?? 0)) * 6e4,
|
|
598
636
|
keys: flag(args, "keys") === "literal" ? "literal" : "adapt",
|
|
599
|
-
clipboard: !has(args, "no-clipboard")
|
|
637
|
+
clipboard: !has(args, "no-clipboard"),
|
|
638
|
+
insecureHostKey: has(args, "insecure-host-key")
|
|
600
639
|
});
|
|
601
640
|
return;
|
|
602
641
|
}
|
|
603
642
|
if (args.command === "__window") {
|
|
604
|
-
const { runWindow } = await import("./cli-window-
|
|
643
|
+
const { runWindow } = await import("./cli-window-AWXZ5NJX.mjs");
|
|
605
644
|
await runWindow(process.argv.slice(3));
|
|
606
645
|
return;
|
|
607
646
|
}
|
|
@@ -653,11 +692,11 @@ Saved, nothing open (\`${NAME} open <host>\` needs no password):
|
|
|
653
692
|
throw new CliError(`--keys takes "adapt" or "literal", not "${keys}".`, 12);
|
|
654
693
|
}
|
|
655
694
|
const mode = modeFor(args, target);
|
|
656
|
-
const { host, port } = splitTarget(target, defaultPort(mode));
|
|
695
|
+
const { account: targetAccount, host, port } = splitTarget(target, defaultPort(mode));
|
|
657
696
|
const asked = flag(args, "session", "name");
|
|
658
697
|
const base = sessionName(host, port, asked);
|
|
659
698
|
const name2 = has(args, "no-reuse") && !asked ? freeName(base, liveSessions().map((meta) => meta.name)) : base;
|
|
660
|
-
const account = flag(args, "u", "user") ??
|
|
699
|
+
const account = flag(args, "u", "user") ?? targetAccount;
|
|
661
700
|
const prompts = prompting(args);
|
|
662
701
|
let secret = await passwordFor(host, port, account);
|
|
663
702
|
let prompted = false;
|
|
@@ -669,7 +708,7 @@ Saved, nothing open (\`${NAME} open <host>\` needs no password):
|
|
|
669
708
|
const reused = Boolean(status);
|
|
670
709
|
if (!status) {
|
|
671
710
|
try {
|
|
672
|
-
status = await startSession(args, name2, host, port, secret);
|
|
711
|
+
status = await startSession(args, name2, host, port, secret, account);
|
|
673
712
|
} catch (error) {
|
|
674
713
|
const refused = error instanceof CliError && error.code === 11;
|
|
675
714
|
if (!refused || prompted || !prompts) throw error;
|
|
@@ -683,7 +722,7 @@ Saved, nothing open (\`${NAME} open <host>\` needs no password):
|
|
|
683
722
|
cleanup(name2);
|
|
684
723
|
secret = typed;
|
|
685
724
|
prompted = true;
|
|
686
|
-
status = await startSession(args, name2, host, port, secret);
|
|
725
|
+
status = await startSession(args, name2, host, port, secret, account);
|
|
687
726
|
}
|
|
688
727
|
}
|
|
689
728
|
const saved = has(args, "save") && secret ? await keepSecret(host, port, account, secret) : null;
|
|
@@ -760,6 +799,36 @@ Saved, nothing open (\`${NAME} open <host>\` needs no password):
|
|
|
760
799
|
}
|
|
761
800
|
return;
|
|
762
801
|
}
|
|
802
|
+
if (args.command === "hosts") {
|
|
803
|
+
const { knownHosts } = await import("./cli-known-hosts-UU427T4N.mjs");
|
|
804
|
+
const entries = knownHosts();
|
|
805
|
+
if (json) {
|
|
806
|
+
process.stdout.write(`${JSON.stringify({ hosts: entries })}
|
|
807
|
+
`);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (!entries.length) {
|
|
811
|
+
process.stdout.write("No SSH host keys have been accepted yet.\n");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
for (const entry of entries) {
|
|
815
|
+
process.stdout.write(` ${entry.address.padEnd(28)} ${entry.fingerprint} ${entry.keyType} first seen ${entry.firstSeen.slice(0, 10)}
|
|
816
|
+
`);
|
|
817
|
+
}
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (args.command === "forget-host") {
|
|
821
|
+
const { forgetHostKey } = await import("./cli-known-hosts-UU427T4N.mjs");
|
|
822
|
+
const target = args.rest[0];
|
|
823
|
+
if (!target) throw new CliError(`\`${NAME} forget-host <host[:port]>\` needs a machine to forget.`, 2);
|
|
824
|
+
const where = splitTarget(target, 22);
|
|
825
|
+
const address = `${where.host}:${where.port}`;
|
|
826
|
+
const forgotten = forgetHostKey(address);
|
|
827
|
+
process.stdout.write(forgotten ? `Forgot the host key for ${address}. The next connection records a new one.
|
|
828
|
+
` : `No host key was on file for ${address}. \`${NAME} hosts\` lists what is.
|
|
829
|
+
`);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
763
832
|
if (args.command === "forget") {
|
|
764
833
|
const { forgetAll, forgetSecret, secretKey } = await secrets();
|
|
765
834
|
if (has(args, "all")) {
|
|
@@ -899,6 +968,22 @@ Open the host again without --ssh for a desktop.`,
|
|
|
899
968
|
width: session.width,
|
|
900
969
|
height: session.height
|
|
901
970
|
};
|
|
971
|
+
const runSteps = async (script) => {
|
|
972
|
+
const shot = shotFile(args);
|
|
973
|
+
if (shot === void 0) return tell(session, "script", { script });
|
|
974
|
+
return tell(session, "act", {
|
|
975
|
+
script,
|
|
976
|
+
file: resolvePath(shot),
|
|
977
|
+
maxEdge: Number(flag(args, "max-edge") ?? 0),
|
|
978
|
+
region: flag(args, "region")
|
|
979
|
+
});
|
|
980
|
+
};
|
|
981
|
+
const observation = (shot) => shot === void 0 ? { observe: false } : {
|
|
982
|
+
observe: true,
|
|
983
|
+
file: resolvePath(shot),
|
|
984
|
+
region: flag(args, "region"),
|
|
985
|
+
maxEdge: Number(flag(args, "max-edge") ?? 0)
|
|
986
|
+
};
|
|
902
987
|
switch (args.command) {
|
|
903
988
|
case "status": {
|
|
904
989
|
Object.assign(summary, await tell(session, "status", {}, 1e4));
|
|
@@ -919,7 +1004,7 @@ Open the host again without --ssh for a desktop.`,
|
|
|
919
1004
|
case "shot": {
|
|
920
1005
|
const file = resolvePath(flag(args, "o", "out") ?? "screen.png");
|
|
921
1006
|
const maxEdge = Number(flag(args, "max-edge") ?? 0);
|
|
922
|
-
Object.assign(summary, await tell(session, "shot", { file, maxEdge }));
|
|
1007
|
+
Object.assign(summary, await tell(session, "shot", { file, maxEdge, region: flag(args, "region") }));
|
|
923
1008
|
break;
|
|
924
1009
|
}
|
|
925
1010
|
case "click": {
|
|
@@ -928,7 +1013,7 @@ Open the host again without --ssh for a desktop.`,
|
|
|
928
1013
|
const [x, y] = spec.split(",").map(Number);
|
|
929
1014
|
const button = flag(args, "button") ?? "left";
|
|
930
1015
|
const step = `click ${x},${y}${button === "left" ? "" : `,${button}`}`;
|
|
931
|
-
|
|
1016
|
+
Object.assign(summary, await runSteps(has(args, "double") ? `${step}; ${step}` : step));
|
|
932
1017
|
Object.assign(summary, { clicked: { x, y, button } });
|
|
933
1018
|
break;
|
|
934
1019
|
}
|
|
@@ -936,13 +1021,16 @@ Open the host again without --ssh for a desktop.`,
|
|
|
936
1021
|
refuseSessionHost(args, "type", session);
|
|
937
1022
|
const text = args.rest.join(" ");
|
|
938
1023
|
if (!text) throw new CliError("type needs some text", 12);
|
|
939
|
-
await tell(session, "
|
|
1024
|
+
Object.assign(summary, await tell(session, "batch", {
|
|
1025
|
+
actions: [{ type: "type", text }],
|
|
1026
|
+
...observation(shotFile(args))
|
|
1027
|
+
}));
|
|
940
1028
|
Object.assign(summary, { typed: text.length });
|
|
941
1029
|
break;
|
|
942
1030
|
}
|
|
943
1031
|
case "key": {
|
|
944
1032
|
if (!args.rest.length) throw new CliError("key needs at least one code, e.g. MetaLeft", 12);
|
|
945
|
-
|
|
1033
|
+
Object.assign(summary, await runSteps(args.rest.map((code) => `key ${code}`).join("; ")));
|
|
946
1034
|
Object.assign(summary, { keys: args.rest });
|
|
947
1035
|
break;
|
|
948
1036
|
}
|
|
@@ -950,7 +1038,20 @@ Open the host again without --ssh for a desktop.`,
|
|
|
950
1038
|
refuseSessionHost(args, "do", session);
|
|
951
1039
|
const script = args.rest.join(" ");
|
|
952
1040
|
if (!script) throw new CliError('do needs a script, e.g. "key MetaLeft; wait 800; shot s.png"', 12);
|
|
953
|
-
Object.assign(summary, await
|
|
1041
|
+
Object.assign(summary, await runSteps(script));
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
case "batch": {
|
|
1045
|
+
const file = args.rest[0];
|
|
1046
|
+
if (!file || args.rest.length !== 1) throw new CliError("batch needs a JSON file containing an action array; use - for stdin.", 12);
|
|
1047
|
+
const source = readFileSync2(file === "-" ? 0 : resolvePath(file), "utf8");
|
|
1048
|
+
let actions;
|
|
1049
|
+
try {
|
|
1050
|
+
actions = JSON.parse(source);
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
throw new CliError(`${file === "-" ? "stdin" : file} is not valid JSON: ${error.message}`, 12);
|
|
1053
|
+
}
|
|
1054
|
+
Object.assign(summary, await tell(session, "batch", { actions, ...observation(shotFile(args)) }));
|
|
954
1055
|
break;
|
|
955
1056
|
}
|
|
956
1057
|
case "exec": {
|
|
@@ -983,7 +1084,7 @@ function report(json, command, summary) {
|
|
|
983
1084
|
`);
|
|
984
1085
|
}
|
|
985
1086
|
async function sshLogin(args, session, what) {
|
|
986
|
-
const { bareUsername } = await import("./cli-shell-
|
|
1087
|
+
const { bareUsername } = await import("./cli-shell-JEC6BEME.mjs");
|
|
987
1088
|
const username = bareUsername(flag(args, "ssh-user") ?? session.sshUser ?? "");
|
|
988
1089
|
if (!username) {
|
|
989
1090
|
throw new CliError(
|
|
@@ -991,15 +1092,15 @@ async function sshLogin(args, session, what) {
|
|
|
991
1092
|
12
|
|
992
1093
|
);
|
|
993
1094
|
}
|
|
994
|
-
const { loadIdentities } = await import("./cli-identities-
|
|
1095
|
+
const { loadIdentities } = await import("./cli-identities-GBQWYF2O.mjs");
|
|
995
1096
|
const { identities, skipped } = await loadIdentities(args.identities);
|
|
996
1097
|
const port = Number(flag(args, "ssh-port") ?? session.sshPort ?? 22);
|
|
997
|
-
let
|
|
998
|
-
if (!
|
|
999
|
-
|
|
1098
|
+
let sshPassword = await passwordFor(session.host, port, username);
|
|
1099
|
+
if (!sshPassword && session.port !== port) {
|
|
1100
|
+
sshPassword = await passwordFor(session.host, session.port, username);
|
|
1000
1101
|
}
|
|
1001
1102
|
let asked = false;
|
|
1002
|
-
if (!
|
|
1103
|
+
if (!sshPassword && !identities.length) {
|
|
1003
1104
|
if (!prompting(args)) {
|
|
1004
1105
|
throw new CliError(
|
|
1005
1106
|
`${what} signs in for itself, and this shell has nothing for it to use.
|
|
@@ -1010,10 +1111,10 @@ Keys passed over: ${skipped.join("; ")}` : ""),
|
|
|
1010
1111
|
11
|
|
1011
1112
|
);
|
|
1012
1113
|
}
|
|
1013
|
-
|
|
1014
|
-
asked = Boolean(
|
|
1114
|
+
sshPassword = await promptFor(session.host, port, username);
|
|
1115
|
+
asked = Boolean(sshPassword);
|
|
1015
1116
|
}
|
|
1016
|
-
return { username, port, identities, password:
|
|
1117
|
+
return { username, port, identities, password: sshPassword, asked };
|
|
1017
1118
|
}
|
|
1018
1119
|
function resolveEndpoint(args, endpoint) {
|
|
1019
1120
|
if (!endpoint.remote) return { endpoint, session: null };
|
|
@@ -1065,7 +1166,7 @@ async function copyFiles(args, json) {
|
|
|
1065
1166
|
upload,
|
|
1066
1167
|
walkLocal,
|
|
1067
1168
|
walkRemote
|
|
1068
|
-
} = await import("./cli-copy-
|
|
1169
|
+
} = await import("./cli-copy-2HQU2E7D.mjs");
|
|
1069
1170
|
const from = resolveEndpoint(args, parseEndpoint(args.rest[0]));
|
|
1070
1171
|
const to = resolveEndpoint(args, parseEndpoint(args.rest[1]));
|
|
1071
1172
|
if (!from.endpoint.remote && !to.endpoint.remote) {
|
|
@@ -1087,7 +1188,8 @@ async function copyFiles(args, json) {
|
|
|
1087
1188
|
port: login.port,
|
|
1088
1189
|
username: login.username,
|
|
1089
1190
|
password: login.password,
|
|
1090
|
-
identities: login.identities
|
|
1191
|
+
identities: login.identities,
|
|
1192
|
+
insecureHostKey: has(args, "insecure-host-key")
|
|
1091
1193
|
});
|
|
1092
1194
|
await connection.connect().catch((error) => {
|
|
1093
1195
|
throw new CliError(error.message, /password|auth|denied|refused the/i.test(error.message) ? 11 : 10);
|
|
@@ -1143,7 +1245,7 @@ async function copyFiles(args, json) {
|
|
|
1143
1245
|
}
|
|
1144
1246
|
}
|
|
1145
1247
|
async function interactiveShell(args, session) {
|
|
1146
|
-
const { Shell } = await import("./cli-shell-
|
|
1248
|
+
const { Shell } = await import("./cli-shell-JEC6BEME.mjs");
|
|
1147
1249
|
const login = await sshLogin(args, session, "a terminal");
|
|
1148
1250
|
const { username, port, identities } = login;
|
|
1149
1251
|
const prompts = prompting(args);
|
|
@@ -1156,6 +1258,7 @@ async function interactiveShell(args, session) {
|
|
|
1156
1258
|
username,
|
|
1157
1259
|
password: secret,
|
|
1158
1260
|
identities,
|
|
1261
|
+
insecureHostKey: has(args, "insecure-host-key"),
|
|
1159
1262
|
columns: process.stdout.columns ?? 120,
|
|
1160
1263
|
rows: process.stdout.rows ?? 30
|
|
1161
1264
|
});
|
|
@@ -1211,11 +1314,11 @@ async function probe(host, port) {
|
|
|
1211
1314
|
});
|
|
1212
1315
|
}
|
|
1213
1316
|
function describe(command, summary) {
|
|
1214
|
-
const size = `${summary.width}x${summary.height}`;
|
|
1317
|
+
const size = `${summary.desktopWidth ?? summary.width}x${summary.desktopHeight ?? summary.height}`;
|
|
1215
1318
|
const terminalOnly = summary.mode === "ssh";
|
|
1216
1319
|
if (command === "open") {
|
|
1217
1320
|
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).";
|
|
1218
|
-
const shape = terminalOnly ? "a terminal" :
|
|
1321
|
+
const shape = terminalOnly ? "a terminal" : size;
|
|
1219
1322
|
const next = terminalOnly ? `
|
|
1220
1323
|
Session ${summary.name}. \`${NAME} exec "cmd"\` runs something there, \`${NAME} shell\` opens it, \`${NAME} close\` ends it.` : `
|
|
1221
1324
|
Session ${summary.name}. Run more commands with no host, then \`${NAME} close\` when you are done.`;
|
|
@@ -1231,9 +1334,14 @@ ${summary.viewer ? `Window: ${summary.viewer} (${summary.viewers} watching)` : "
|
|
|
1231
1334
|
if (summary.already) {
|
|
1232
1335
|
return `${summary.name} already has a screen (${summary.mode}), ${size}.`;
|
|
1233
1336
|
}
|
|
1234
|
-
return
|
|
1235
|
-
|
|
1236
|
-
|
|
1337
|
+
return [
|
|
1338
|
+
`${summary.name} now has a screen over VNC, ${size}.`,
|
|
1339
|
+
summary.viewer ? ` Watch it at ${summary.viewer}` : "",
|
|
1340
|
+
`
|
|
1341
|
+
\`${NAME} shot\`, \`${NAME} click X,Y\` and \`${NAME} type\` now work on it.`,
|
|
1342
|
+
summary.saved ? `
|
|
1343
|
+
The password is saved in ${summary.saved}.` : ""
|
|
1344
|
+
].join("");
|
|
1237
1345
|
}
|
|
1238
1346
|
if (command === "view") {
|
|
1239
1347
|
if (summary.viewer) return `Window: ${summary.viewer}`;
|
|
@@ -1254,13 +1362,15 @@ The password is saved in ${summary.saved}.` : ""}`;
|
|
|
1254
1362
|
return `${summary.replaced ? "Replaced" : "Opened"} the terminal on ${summary.name}. It is a new login, so it sees whatever the PATH says now -- and has forgotten the working directory and any exported variables.`;
|
|
1255
1363
|
}
|
|
1256
1364
|
if (command === "shot") return `Wrote ${summary.file} (${summary.width}x${summary.height}) from ${summary.name}.`;
|
|
1365
|
+
const looked = summary.file ? ` Wrote ${summary.file} (${summary.width}x${summary.height})` + (summary.changed === false ? ", though nothing on screen changed" : "") + (summary.quiet === false ? "; the screen was still moving" : "") + "." : "";
|
|
1257
1366
|
if (command === "click") {
|
|
1258
1367
|
const { x, y } = summary.clicked;
|
|
1259
|
-
return `Clicked (${x}, ${y}) on a ${size} desktop
|
|
1368
|
+
return `Clicked (${x}, ${y}) on a ${size} desktop.${looked}`;
|
|
1260
1369
|
}
|
|
1261
|
-
if (command === "type") return `Typed ${summary.typed} characters
|
|
1262
|
-
if (command === "
|
|
1263
|
-
if (command === "
|
|
1370
|
+
if (command === "type") return `Typed ${summary.typed} characters.${looked}`;
|
|
1371
|
+
if (command === "batch") return `Ran ${summary.actions} actions (${summary.clicks} clicks) in ${summary.dispatchMs} ms of dispatch.${looked}`;
|
|
1372
|
+
if (command === "key") return `Pressed ${summary.keys.join(", ")}.${looked}`;
|
|
1373
|
+
if (command === "do") return `Ran ${summary.steps.length} steps: ${summary.steps.join(" \u2192 ")}${looked}`;
|
|
1264
1374
|
return "Done.";
|
|
1265
1375
|
}
|
|
1266
1376
|
main().catch((error) => {
|