ai-remote 0.4.14 → 0.4.16

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.
@@ -0,0 +1,173 @@
1
+ import {
2
+ SshSession,
3
+ TcpTransport
4
+ } from "./cli-chunk-S2TWL5LF.mjs";
5
+
6
+ // src/cli/shell.ts
7
+ function bareUsername(username) {
8
+ const withoutDomain = username.includes("\\") ? username.split("\\").pop() : username;
9
+ return withoutDomain.split("@")[0].trim();
10
+ }
11
+ var Shell = class {
12
+ options;
13
+ session;
14
+ ready = false;
15
+ /** True once the connection has ended. A closed shell never reopens; a new one is made. */
16
+ closed = false;
17
+ lastError = "";
18
+ #sinks = /* @__PURE__ */ new Set();
19
+ /** What the shell has printed, so a viewer attaching late sees the scrollback. */
20
+ #scrollback = [];
21
+ #scrollbackBytes = 0;
22
+ constructor(options) {
23
+ this.options = options;
24
+ this.session = new SshSession(`tcp://${options.host}:${options.port}`, {
25
+ username: options.username,
26
+ password: options.password ?? "",
27
+ identities: options.identities ?? [],
28
+ columns: options.columns ?? 120,
29
+ rows: options.rows ?? 30,
30
+ // Every host this reaches was named by the person running the command,
31
+ // on their own network. There is no stored known_hosts to compare with
32
+ // yet, so nothing useful can be decided here.
33
+ verifyHost: async () => true,
34
+ openTransport: () => new TcpTransport(options.host, options.port)
35
+ });
36
+ this.session.addEventListener("data", (event) => {
37
+ const bytes = event.detail.display ?? event.detail.bytes;
38
+ if (!bytes?.length) return;
39
+ this.#remember(bytes);
40
+ for (const sink of this.#sinks) sink(bytes);
41
+ });
42
+ this.session.addEventListener("ready", () => {
43
+ this.ready = true;
44
+ });
45
+ this.session.addEventListener("error", (event) => {
46
+ this.lastError = event.detail?.message || "";
47
+ });
48
+ this.session.addEventListener("close", () => {
49
+ this.ready = false;
50
+ this.closed = true;
51
+ });
52
+ }
53
+ /** Everything printed so far, capped so a long-running shell cannot grow forever. */
54
+ get scrollback() {
55
+ const total = this.#scrollback.reduce((sum, chunk) => sum + chunk.length, 0);
56
+ const out = new Uint8Array(total);
57
+ let offset = 0;
58
+ for (const chunk of this.#scrollback) {
59
+ out.set(chunk, offset);
60
+ offset += chunk.length;
61
+ }
62
+ return out;
63
+ }
64
+ /** Called when the connection ends, however it ends. Returns an unsubscribe. */
65
+ onClose(listener) {
66
+ if (this.closed) {
67
+ queueMicrotask(listener);
68
+ return () => {
69
+ };
70
+ }
71
+ this.session.addEventListener("close", listener);
72
+ return () => this.session.removeEventListener("close", listener);
73
+ }
74
+ onData(sink) {
75
+ this.#sinks.add(sink);
76
+ return () => this.#sinks.delete(sink);
77
+ }
78
+ connect(timeoutMs = 3e4) {
79
+ return new Promise((resolve, reject) => {
80
+ let settled = false;
81
+ const finish = (error) => {
82
+ if (settled) return;
83
+ settled = true;
84
+ clearTimeout(timer);
85
+ if (error) reject(error);
86
+ else resolve();
87
+ };
88
+ const timer = setTimeout(
89
+ () => finish(new Error(`The SSH host did not open a shell within ${Math.round(timeoutMs / 1e3)}s.`)),
90
+ timeoutMs
91
+ );
92
+ this.session.addEventListener("ready", () => finish());
93
+ this.session.addEventListener("close", (event) => {
94
+ finish(new Error(this.lastError || event.detail?.message || "The SSH connection closed during sign-in."));
95
+ });
96
+ this.session.connect();
97
+ });
98
+ }
99
+ /**
100
+ * Run one command and wait for its output and exit status.
101
+ *
102
+ * The first command against a Windows host usually fails: the engine frames
103
+ * commands POSIX-style to find where the output starts and ends, cmd.exe
104
+ * rejects that, and the engine notes the shell family and asks to be called
105
+ * again. That is a detail of how the shell was discovered, not something a
106
+ * caller did wrong, so the retry happens here instead of reaching them.
107
+ *
108
+ * The wait in front is the same idea one step earlier. "Connected" and "able
109
+ * to take a command" are not the same moment on Windows: OpenSSH hands out
110
+ * cmd.exe, the engine swaps it for PowerShell, and readiness is withdrawn in
111
+ * between. A caller arriving in that window used to be told the shell was
112
+ * still starting -- true, and useless, because the only thing to do about it
113
+ * is wait. So it waits.
114
+ */
115
+ async runCommand(command) {
116
+ await this.#waitForPrompt();
117
+ let result;
118
+ try {
119
+ result = await this.session.runCommand(command);
120
+ } catch (error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ if (!/run the command again|not a POSIX shell/i.test(message)) throw error;
123
+ await this.#waitForPrompt();
124
+ result = await this.session.runCommand(command);
125
+ }
126
+ return {
127
+ output: result.output ?? "",
128
+ exitStatus: result.exitStatus ?? null,
129
+ timedOut: Boolean(result.timedOut),
130
+ durationMs: result.durationMs ?? null
131
+ };
132
+ }
133
+ /**
134
+ * Wait until the shell will accept a framed command.
135
+ *
136
+ * Returns on the first check when the shell is already settled, which is the
137
+ * usual case, so this costs a waiting caller nothing.
138
+ */
139
+ async #waitForPrompt(timeoutMs = 15e3) {
140
+ const deadline = Date.now() + timeoutMs;
141
+ while (Date.now() < deadline) {
142
+ if (this.session.shellOpen && this.session.readySignaled !== false) return;
143
+ await new Promise((resolve) => setTimeout(resolve, 100));
144
+ }
145
+ throw new Error("The SSH shell did not become ready for a command.");
146
+ }
147
+ /** Raw keystrokes, for an interactive terminal. */
148
+ write(text) {
149
+ this.session.write(text);
150
+ }
151
+ resize(columns, rows) {
152
+ this.session.resize?.(columns, rows);
153
+ }
154
+ disconnect() {
155
+ try {
156
+ this.session.disconnect();
157
+ } catch {
158
+ }
159
+ }
160
+ #remember(bytes) {
161
+ const CAP = 256 * 1024;
162
+ this.#scrollback.push(bytes);
163
+ this.#scrollbackBytes += bytes.length;
164
+ while (this.#scrollbackBytes > CAP && this.#scrollback.length > 1) {
165
+ this.#scrollbackBytes -= this.#scrollback.shift().length;
166
+ }
167
+ }
168
+ };
169
+
170
+ export {
171
+ bareUsername,
172
+ Shell
173
+ };