ai-remote 0.3.0 → 0.4.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 +69 -20
- package/dist/cli.mjs +244 -85
- package/package.json +7 -4
- package/src/cli/cli.ts +17 -6
- package/src/cli/daemon.ts +19 -4
- package/src/cli/generated/viewer-bundle.ts +1 -1
- package/src/cli/native/window.swift +151 -0
- package/src/cli/shell.ts +13 -0
- package/src/cli/viewer-client/main.ts +38 -15
- package/src/cli/viewer-page.ts +92 -56
- package/src/cli/viewer.ts +47 -20
- package/src/cli/window.ts +140 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-remote",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "RDP, VNC and SSH protocol engines that run in Node, a Worker or a browser -- plus `npx ai-remote`, a CLI that drives a machine and shows you what it is doing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ai-remote": "
|
|
8
|
-
"remotectl": "
|
|
7
|
+
"ai-remote": "dist/cli.mjs",
|
|
8
|
+
"remotectl": "dist/cli.mjs"
|
|
9
9
|
},
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"author": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "https://github.com/guocity/vnc-node-worker.git"
|
|
18
|
+
"url": "git+https://github.com/guocity/vnc-node-worker.git"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"remote-desktop",
|
|
@@ -88,5 +88,8 @@
|
|
|
88
88
|
},
|
|
89
89
|
"engines": {
|
|
90
90
|
"node": ">=22"
|
|
91
|
+
},
|
|
92
|
+
"optionalDependencies": {
|
|
93
|
+
"@webviewjs/webview": "^0.4.3"
|
|
91
94
|
}
|
|
92
95
|
}
|
package/src/cli/cli.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
import { basename, resolve as resolvePath } from 'node:path';
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
12
|
import { readFileSync, readdirSync, existsSync, openSync, unlinkSync } from 'node:fs';
|
|
13
|
-
import { Shell } from './shell';
|
|
13
|
+
import { bareUsername, Shell } from './shell';
|
|
14
14
|
import { runDaemon } from './daemon';
|
|
15
|
+
import { runWindow } from './window';
|
|
15
16
|
import { request } from './ipc';
|
|
16
17
|
import { ensureRunDir, logPath, metaPath, RUN_DIR, sessionName, socketPath } from './paths';
|
|
17
18
|
|
|
@@ -59,7 +60,8 @@ Steps for "do", separated by semicolons:
|
|
|
59
60
|
Options
|
|
60
61
|
-u, --user NAME account on the host
|
|
61
62
|
-d, --domain NAME Windows domain or computer name
|
|
62
|
-
--ssh-user NAME
|
|
63
|
+
--ssh-user NAME only if the terminal signs in as someone else;
|
|
64
|
+
by default it reuses --user and the same password
|
|
63
65
|
--ssh-port N SSH port (default: 22)
|
|
64
66
|
-s, --security MODE auto | nla | tls | rdp (default: auto)
|
|
65
67
|
-W, --width N desktop width (default: 1280)
|
|
@@ -186,6 +188,8 @@ async function startSession(args: Args, name: string, host: string, port: number
|
|
|
186
188
|
];
|
|
187
189
|
if (has(args, 'no-view', 'headless')) daemonArgs.push('--no-view');
|
|
188
190
|
if (has(args, 'no-open')) daemonArgs.push('--no-open');
|
|
191
|
+
if (has(args, 'tab')) daemonArgs.push('--tab');
|
|
192
|
+
if (has(args, 'fullscreen')) daemonArgs.push('--fullscreen');
|
|
189
193
|
|
|
190
194
|
// Its output goes to a file rather than nowhere: a session that fails to
|
|
191
195
|
// start is the one moment its log is worth having.
|
|
@@ -273,6 +277,8 @@ async function main(): Promise<void> {
|
|
|
273
277
|
viewPort: Number(flag(args, 'view-port') ?? 7373),
|
|
274
278
|
sshUsername: flag(args, 'ssh-user') ?? flag(args, 'u', 'user') ?? '',
|
|
275
279
|
sshPort: Number(flag(args, 'ssh-port') ?? 22),
|
|
280
|
+
tab: has(args, 'tab'),
|
|
281
|
+
fullscreen: has(args, 'fullscreen'),
|
|
276
282
|
// Zero by default: a session stays open until it is closed. An agent
|
|
277
283
|
// that pauses to think is not an agent that has finished.
|
|
278
284
|
idleMs: Math.max(0, Number(flag(args, 'idle') ?? 0)) * 60_000,
|
|
@@ -280,6 +286,9 @@ async function main(): Promise<void> {
|
|
|
280
286
|
return;
|
|
281
287
|
}
|
|
282
288
|
|
|
289
|
+
// The hidden command the native window runs as.
|
|
290
|
+
if (args.command === '__window') { await runWindow(process.argv.slice(3)); return; }
|
|
291
|
+
|
|
283
292
|
if (args.command === 'shell') { await interactiveShell(args, host); return; }
|
|
284
293
|
|
|
285
294
|
// Everything else runs against a session: the one already open, or a new one.
|
|
@@ -296,7 +305,7 @@ async function main(): Promise<void> {
|
|
|
296
305
|
if (!has(args, 'no-view', 'headless') && !status.viewer) {
|
|
297
306
|
Object.assign(status, await ask(name, 'view-open', { launch: !has(args, 'no-open') }));
|
|
298
307
|
}
|
|
299
|
-
Object.assign(report, { viewer: status.viewer });
|
|
308
|
+
Object.assign(report, { viewer: status.viewer, window: status.window, viewers: status.viewers });
|
|
300
309
|
break;
|
|
301
310
|
|
|
302
311
|
case 'view': {
|
|
@@ -376,7 +385,7 @@ async function interactiveShell(args: Args, host: string): Promise<void> {
|
|
|
376
385
|
const shell = new Shell({
|
|
377
386
|
host,
|
|
378
387
|
port: Number(flag(args, 'ssh-port') ?? 22),
|
|
379
|
-
username: flag(args, 'ssh-user') ?? flag(args, 'u', 'user') ?? '',
|
|
388
|
+
username: flag(args, 'ssh-user') ?? bareUsername(flag(args, 'u', 'user') ?? ''),
|
|
380
389
|
password: password(),
|
|
381
390
|
columns: process.stdout.columns ?? 120,
|
|
382
391
|
rows: process.stdout.rows ?? 30,
|
|
@@ -423,8 +432,10 @@ function describe(command: string, report: Record<string, unknown>): string {
|
|
|
423
432
|
const where = report.reused ? 'the open session' : 'a new session';
|
|
424
433
|
|
|
425
434
|
if (command === 'open') {
|
|
426
|
-
|
|
427
|
-
|
|
435
|
+
const window = report.window === 'window' ? 'A window is open on it.'
|
|
436
|
+
: report.window === 'tab' ? 'It opened in your browser.'
|
|
437
|
+
: report.viewer ? `Open it at ${report.viewer}` : 'No window (headless).';
|
|
438
|
+
return `${report.reused ? 'Already connected' : 'Connected'} to ${report.host}:${report.port} at ${size}. ${window}`
|
|
428
439
|
+ `\nRun more commands against it, then \`${NAME} close ${report.host}\` when you are done.`;
|
|
429
440
|
}
|
|
430
441
|
if (command === 'view') {
|
package/src/cli/daemon.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import net from 'node:net';
|
|
14
14
|
import { unlinkSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
15
|
import { RdpSession, type SessionOptions } from './session';
|
|
16
|
-
import { Shell } from './shell';
|
|
16
|
+
import { bareUsername, Shell } from './shell';
|
|
17
17
|
import { downscale, encodePng } from './png';
|
|
18
18
|
import { startViewer, type Viewer } from './viewer';
|
|
19
19
|
import { readMessages, writeMessage, type Request } from './ipc';
|
|
@@ -26,6 +26,10 @@ export interface DaemonOptions extends SessionOptions {
|
|
|
26
26
|
viewPort: number;
|
|
27
27
|
/** Account for the terminal, when one is wanted. Blank leaves SSH unopened. */
|
|
28
28
|
sshUsername: string;
|
|
29
|
+
/** Open in the default browser rather than a window of its own. */
|
|
30
|
+
tab: boolean;
|
|
31
|
+
/** Start the window already filling the display. */
|
|
32
|
+
fullscreen: boolean;
|
|
29
33
|
sshPort: number;
|
|
30
34
|
idleMs: number;
|
|
31
35
|
}
|
|
@@ -43,6 +47,10 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
|
|
|
43
47
|
* The terminal is opened on first use rather than at connect time. Most
|
|
44
48
|
* sessions never ask for one, and a second sign-in that nobody wanted is a
|
|
45
49
|
* second chance to lock an account out.
|
|
50
|
+
*
|
|
51
|
+
* It signs in as the desktop's own account and password by default. Needing a
|
|
52
|
+
* separate flag to get a terminal on the machine you are already looking at
|
|
53
|
+
* is a question with an obvious answer, so it is not asked.
|
|
46
54
|
*/
|
|
47
55
|
function openShell(): Promise<Shell> {
|
|
48
56
|
if (shell) return Promise.resolve(shell);
|
|
@@ -52,7 +60,7 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
|
|
|
52
60
|
const started = new Shell({
|
|
53
61
|
host: options.host,
|
|
54
62
|
port: options.sshPort,
|
|
55
|
-
username: options.sshUsername || options.username,
|
|
63
|
+
username: options.sshUsername || bareUsername(options.username),
|
|
56
64
|
password: options.password,
|
|
57
65
|
});
|
|
58
66
|
await started.connect();
|
|
@@ -100,7 +108,12 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
|
|
|
100
108
|
if (launch) viewer.launch();
|
|
101
109
|
return viewer;
|
|
102
110
|
}
|
|
103
|
-
viewer = await startViewer(session, options.viewPort, {
|
|
111
|
+
viewer = await startViewer(session, options.viewPort, {
|
|
112
|
+
launch,
|
|
113
|
+
openShell,
|
|
114
|
+
tab: options.tab,
|
|
115
|
+
fullscreen: options.fullscreen,
|
|
116
|
+
});
|
|
104
117
|
writeMeta();
|
|
105
118
|
return viewer;
|
|
106
119
|
}
|
|
@@ -143,7 +156,7 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
|
|
|
143
156
|
switch (message.op) {
|
|
144
157
|
case 'view-open': {
|
|
145
158
|
const opened = await openViewer(args.launch !== false);
|
|
146
|
-
return { viewer: opened.url };
|
|
159
|
+
return { viewer: opened.url, window: opened.window?.kind ?? null };
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
case 'view-close':
|
|
@@ -158,6 +171,8 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
|
|
|
158
171
|
height: session.framebuffer.height,
|
|
159
172
|
connected: session.connected,
|
|
160
173
|
viewer: viewer?.url ?? null,
|
|
174
|
+
window: viewer?.window?.kind ?? null,
|
|
175
|
+
viewers: viewer?.viewers ?? 0,
|
|
161
176
|
shell: shell ? 'open' : shellStarting ? 'opening' : 'closed',
|
|
162
177
|
pid: process.pid,
|
|
163
178
|
};
|