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/dist/cli.mjs CHANGED
@@ -13,21 +13,108 @@ import {
13
13
  metaPath,
14
14
  sessionName,
15
15
  socketPath
16
- } from "./cli-chunk-OZG2CODR.mjs";
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 Boolean(process.stdin.isTTY) && !process.env.AI_REMOTE_NO_PROMPT;
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 = Boolean(input.isRaw);
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-26D3F6EN.mjs");
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-ZILT4XCR.mjs");
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 (/^Unknown operation/.test(message)) {
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-ZKAXEHQV.mjs");
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-ZKAXEHQV.mjs");
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-3BBFQCML.mjs");
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-ZKAXEHQV.mjs");
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
- await tell(session, "script", { script: has(args, "double") ? `${step}; ${step}` : step });
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, "script", { script: `type ${text}` });
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
- await tell(session, "script", { script: args.rest.map((code) => `key ${code}`).join("; ") });
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 tell(session, "script", { script }));
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-45XRTE7O.mjs");
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-ZILT4XCR.mjs");
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 password2 = await passwordFor(session.host, port, username);
998
- if (!password2 && session.port !== port) {
999
- password2 = await passwordFor(session.host, session.port, username);
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 (!password2 && !identities.length) {
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
- password2 = await promptFor(session.host, port, username);
1014
- asked = Boolean(password2);
1114
+ sshPassword = await promptFor(session.host, port, username);
1115
+ asked = Boolean(sshPassword);
1015
1116
  }
1016
- return { username, port, identities, password: password2, asked };
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-BOF44DWQ.mjs");
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-45XRTE7O.mjs");
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" : `${size}`;
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 `${summary.name} now has a screen over VNC, ${size}.${summary.viewer ? ` Watch it at ${summary.viewer}` : ""}
1235
- \`${NAME} shot\`, \`${NAME} click X,Y\` and \`${NAME} type\` now work on it.${summary.saved ? `
1236
- The password is saved in ${summary.saved}.` : ""}`;
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 === "key") return `Pressed ${summary.keys.join(", ")}.`;
1263
- if (command === "do") return `Ran ${summary.steps.length} steps: ${summary.steps.join(" \u2192 ")}`;
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) => {