@zonease/happy 0.6.38 → 0.6.40
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/bin/happy.mjs +10 -0
- package/dist/{types-DvTsFaqR.cjs → api-C7FyJfZ1.cjs} +400 -1964
- package/dist/{types-BWpRU1VN.mjs → api-D6WL40-U.mjs} +220 -1729
- package/dist/auth-C7ZScWEw.cjs +274 -0
- package/dist/auth-DCsIoZLT.cjs +183 -0
- package/dist/auth-DO9GexAR.mjs +181 -0
- package/dist/auth-DxJ3VvrX.mjs +270 -0
- package/dist/browser-AoVGRX7y.mjs +20 -0
- package/dist/browser-DzgO4oaD.cjs +22 -0
- package/dist/claudeLocal-C6BdzhDx.cjs +352 -0
- package/dist/claudeLocal-CWNGpJ0-.mjs +344 -0
- package/dist/codex/happyMcpStdioBridge.cjs +4 -1
- package/dist/codex/happyMcpStdioBridge.mjs +4 -1
- package/dist/common-BdNgghUJ.mjs +75 -0
- package/dist/common-CZvSRU4p.cjs +83 -0
- package/dist/{config-0Xnc8ARM.cjs → config-CclB3QW9.cjs} +21 -34
- package/dist/{config-_GW4YBCP.mjs → config-DZk-mJ0K.mjs} +7 -20
- package/dist/connect-BXLUKJ5P.mjs +679 -0
- package/dist/connect-k9CqDJ-T.cjs +681 -0
- package/dist/{constants-B4x1K7by.cjs → constants-E_UWYTo3.cjs} +2 -2
- package/dist/{constants-BO9OAFOT.mjs → constants-pmXZGibX.mjs} +1 -1
- package/dist/controlClient-B23M-WPp.cjs +177 -0
- package/dist/controlClient-DB5tDU7V.mjs +167 -0
- package/dist/deviceKeys-BTdo34Uz.mjs +175 -0
- package/dist/deviceKeys-C540DcXl.cjs +182 -0
- package/dist/diagnose-B3D1a_v0.cjs +60 -0
- package/dist/diagnose-CuRKXxt1.mjs +58 -0
- package/dist/doctor-BQGLhrcI.cjs +214 -0
- package/dist/doctor-C08m5Z9f.cjs +78 -0
- package/dist/doctor-CU4JLGQ1.mjs +211 -0
- package/dist/doctor-ON7pN5G5.mjs +74 -0
- package/dist/downloadImage-DZXI3mXL.mjs +887 -0
- package/dist/downloadImage-cQ1Z7hkC.cjs +897 -0
- package/dist/enroll-B55BQR56.mjs +113 -0
- package/dist/enroll-Bo-CNio1.cjs +115 -0
- package/dist/evidence-DQp8QWcv.cjs +129 -0
- package/dist/evidence-DTYRAaOZ.mjs +127 -0
- package/dist/formatImageMessage-CixDYY6G.cjs +46 -0
- package/dist/formatImageMessage-DbKSSjMx.mjs +43 -0
- package/dist/github-DKcyF1zp.mjs +57 -0
- package/dist/github-h9GhJJ_C.cjs +59 -0
- package/dist/index-CDTNgfUf.cjs +578 -0
- package/dist/index-CPKdvi7J.mjs +554 -0
- package/dist/index.cjs +812 -40
- package/dist/index.mjs +813 -40
- package/dist/install-B1e6rdy7.mjs +94 -0
- package/dist/install-CgUPGIoF.mjs +95 -0
- package/dist/install-D7Ir7ciZ.cjs +96 -0
- package/dist/install-DSDufaPd.cjs +98 -0
- package/dist/lib.cjs +18 -12
- package/dist/lib.d.cts +77 -0
- package/dist/lib.d.mts +77 -0
- package/dist/lib.mjs +13 -7
- package/dist/{loadableToolOutput-va3RLplh.mjs → loadableToolOutput-Bfei9wPq.mjs} +4 -2
- package/dist/{loadableToolOutput-5vdQB98M.cjs → loadableToolOutput-CbLGtvST.cjs} +63 -61
- package/dist/mcpToolSchemas-CjeTfrAs.mjs +62 -0
- package/dist/mcpToolSchemas-DK42yoZ_.cjs +64 -0
- package/dist/persistence-CBTtC_dm.cjs +996 -0
- package/dist/persistence-DKfF81pd.mjs +944 -0
- package/dist/projectPath-BTBIMB57.mjs +10 -0
- package/dist/projectPath-ChLhdOyt.cjs +13 -0
- package/dist/prompt-BTWDZZO0.cjs +25 -0
- package/dist/prompt-BsAZsPbo.mjs +22 -0
- package/dist/release-BMEzNONE.mjs +168 -0
- package/dist/release-EhOJeAZe.cjs +171 -0
- package/dist/run-BVZsMNwN.mjs +2164 -0
- package/dist/run-CURx7LhK.cjs +2188 -0
- package/dist/runClaude-6Gi0sYQy.mjs +3763 -0
- package/dist/runClaude-CL3Kt5ex.cjs +3766 -0
- package/dist/{runCodex-DiKzBf9q.mjs → runCodex-ClwEgJ7M.mjs} +36 -20
- package/dist/{runCodex-skq0j05d.cjs → runCodex-YKyVBkJ3.cjs} +169 -153
- package/dist/{runGemini-BeqrVq8R.mjs → runGemini-BPWxVr60.mjs} +34 -17
- package/dist/{runGemini-CVp-CEIh.cjs → runGemini-Xz2W1spn.cjs} +212 -195
- package/dist/{runOneShot-ltCpHbBB.mjs → runOneShot-CdCWiiLr.mjs} +14 -38
- package/dist/{runOneShot-BQNkNW61.cjs → runOneShot-DjZyCvDI.cjs} +31 -55
- package/dist/{serve-BNdoEDVx.mjs → serve-B48JttQ3.mjs} +12 -35
- package/dist/{serve-BECdtivY.cjs → serve-B8vaWQA0.cjs} +35 -58
- package/dist/session-BXboDn2V.mjs +108 -0
- package/dist/session-BZO25zxp.cjs +110 -0
- package/dist/sessionKeyCache-B6cGadYL.mjs +33 -0
- package/dist/sessionKeyCache-CDmexJ9p.cjs +36 -0
- package/dist/sessionList-CgAYZpNj.mjs +51 -0
- package/dist/sessionList-lQ-nqEhc.cjs +54 -0
- package/dist/sessionSummary-CcORiQSq.cjs +393 -0
- package/dist/sessionSummary-DCWQoaDP.mjs +387 -0
- package/dist/spawnHappyCLI-KiazE3kL.cjs +70 -0
- package/dist/spawnHappyCLI-yBIP_Oqi.mjs +66 -0
- package/dist/ssh-q5uZhqBf.mjs +359 -0
- package/dist/ssh-yMllSHu5.cjs +365 -0
- package/dist/systemdScope-BeOtAkhk.mjs +25 -0
- package/dist/systemdScope-DW3-UoKB.cjs +29 -0
- package/dist/task-_nr7J19j.cjs +167 -0
- package/dist/task-wNX1N2i4.mjs +165 -0
- package/dist/time-Bj2HRkKH.cjs +33 -0
- package/dist/time-DgsBLJA_.mjs +29 -0
- package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
- package/dist/trimIdent-BAjmv_TG.mjs +20 -0
- package/dist/types-BoJh4Qfv.mjs +57 -0
- package/dist/types-DWeB7aqh.cjs +59 -0
- package/dist/uninstall-Buaz7d-X.cjs +77 -0
- package/dist/uninstall-Dxf6_4mQ.mjs +75 -0
- package/dist/update-CGlIfa8e.cjs +111 -0
- package/dist/update-CnnoX2cV.mjs +109 -0
- package/dist/workflowCommon-B0d01Y_a.mjs +56 -0
- package/dist/workflowCommon-Bl568-rt.cjs +62 -0
- package/dist/workflowReports-BEWuQesA.cjs +113 -0
- package/dist/workflowReports-Dd8-4-8f.mjs +108 -0
- package/package.json +1 -1
- package/dist/index-BbCDDvoB.cjs +0 -11376
- package/dist/index-vvZXdswy.mjs +0 -11336
- package/dist/mcpToolSchemas-CdPXJQmb.mjs +0 -175
- package/dist/mcpToolSchemas-f-kttjFw.cjs +0 -186
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { io } from 'socket.io-client';
|
|
2
|
+
import { c as configuration, e as decrypt, f as decodeBase64, g as encodeBase64, h as encrypt } from './persistence-DKfF81pd.mjs';
|
|
3
|
+
import { e as ensureDeviceKey, l as listDevices, r as readCachedDeviceNames, a as readCachedDeviceKey, p as pruneDeviceAliases, d as deviceExec } from './deviceKeys-BTdo34Uz.mjs';
|
|
4
|
+
import { execOnDeviceViaDaemon } from './controlClient-DB5tDU7V.mjs';
|
|
5
|
+
import 'node:fs/promises';
|
|
6
|
+
import 'node:fs';
|
|
7
|
+
import 'node:os';
|
|
8
|
+
import 'node:path';
|
|
9
|
+
import 'zod';
|
|
10
|
+
import 'node:crypto';
|
|
11
|
+
import 'tweetnacl';
|
|
12
|
+
import 'chalk';
|
|
13
|
+
import 'fs';
|
|
14
|
+
import 'axios';
|
|
15
|
+
import './auth-DxJ3VvrX.mjs';
|
|
16
|
+
import 'qrcode-terminal';
|
|
17
|
+
import './time-DgsBLJA_.mjs';
|
|
18
|
+
import './browser-AoVGRX7y.mjs';
|
|
19
|
+
import 'open';
|
|
20
|
+
import 'react';
|
|
21
|
+
import 'ink';
|
|
22
|
+
import './projectPath-BTBIMB57.mjs';
|
|
23
|
+
import 'path';
|
|
24
|
+
import 'url';
|
|
25
|
+
|
|
26
|
+
function matchDevice(devices, query) {
|
|
27
|
+
const lowered = query.trim().toLowerCase();
|
|
28
|
+
return devices.find((device) => device.id === query) ?? devices.find((device) => device.name.toLowerCase() === lowered) ?? devices.find((device) => device.name.toLowerCase().startsWith(lowered)) ?? devices.find((device) => device.name.toLowerCase().includes(lowered)) ?? null;
|
|
29
|
+
}
|
|
30
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 3e5;
|
|
31
|
+
function parseSshArgs(args) {
|
|
32
|
+
const separator = args.indexOf("--");
|
|
33
|
+
const head = separator >= 0 ? args.slice(0, separator) : args;
|
|
34
|
+
const command = separator >= 0 ? args.slice(separator + 1).join(" ") : null;
|
|
35
|
+
let target = null;
|
|
36
|
+
let timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS;
|
|
37
|
+
let stream = false;
|
|
38
|
+
for (let i = 0; i < head.length; i++) {
|
|
39
|
+
if (head[i] === "--stream" || head[i] === "-s") {
|
|
40
|
+
stream = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (head[i] === "--timeout" || head[i] === "-t") {
|
|
44
|
+
const seconds = Number(head[++i]);
|
|
45
|
+
if (Number.isFinite(seconds) && seconds > 0) timeoutMs = seconds * 1e3;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!target && !head[i].startsWith("-")) target = head[i];
|
|
49
|
+
}
|
|
50
|
+
return { target, command: command && command.trim() ? command : null, timeoutMs, stream };
|
|
51
|
+
}
|
|
52
|
+
async function resolveFromCache(query) {
|
|
53
|
+
const names = await readCachedDeviceNames();
|
|
54
|
+
const id = pickCachedDeviceId(names, query);
|
|
55
|
+
if (!id) return null;
|
|
56
|
+
const material = await readCachedDeviceKey(id);
|
|
57
|
+
return material ? { device: { id, name: names[id] }, ...material } : null;
|
|
58
|
+
}
|
|
59
|
+
function pickCachedDeviceId(names, query) {
|
|
60
|
+
if (names[query]) return query;
|
|
61
|
+
const lowered = query.trim().toLowerCase();
|
|
62
|
+
for (const test of [
|
|
63
|
+
(name) => name === lowered,
|
|
64
|
+
(name) => name.startsWith(lowered),
|
|
65
|
+
(name) => name.includes(lowered)
|
|
66
|
+
]) {
|
|
67
|
+
const matches = Object.keys(names).filter((id) => test(names[id].toLowerCase()));
|
|
68
|
+
if (matches.length === 1) return matches[0];
|
|
69
|
+
if (matches.length > 1) return null;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
async function resolveFromServer(credentials, query) {
|
|
74
|
+
const { devices } = await listDevicesWithNames(credentials);
|
|
75
|
+
const device = matchDevice(devices, query);
|
|
76
|
+
if (!device) {
|
|
77
|
+
console.error(`No enrolled device matches "${query}". Run "happy ssh" with no arguments to list devices.`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (!device.active) {
|
|
81
|
+
console.error(`Device "${device.name}" is offline.`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const { key, variant } = await ensureDeviceKey(credentials, device.id, () => {
|
|
85
|
+
console.error("Waiting for approval in the Happy app (Devices \u2192 pending request)...");
|
|
86
|
+
});
|
|
87
|
+
await pruneDeviceAliases(device.id, device.name);
|
|
88
|
+
return { device, key, variant };
|
|
89
|
+
}
|
|
90
|
+
async function runOnDevice(credentials, query, command, options = {}) {
|
|
91
|
+
const cached = await resolveFromCache(query);
|
|
92
|
+
const resolved = cached ?? await resolveFromServer(credentials, query);
|
|
93
|
+
if (!resolved) return 1;
|
|
94
|
+
if (options.stream) {
|
|
95
|
+
return await streamOnResolved(credentials, resolved, command, options);
|
|
96
|
+
}
|
|
97
|
+
const viaDaemon = await execOnDeviceViaDaemon(
|
|
98
|
+
resolved.device.id,
|
|
99
|
+
command,
|
|
100
|
+
options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS
|
|
101
|
+
).catch(() => null);
|
|
102
|
+
if (viaDaemon) {
|
|
103
|
+
return reportResult(viaDaemon);
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return await execOnResolved(credentials, resolved, command, options);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (!cached) throw error;
|
|
109
|
+
const fresh = await resolveFromServer(credentials, query);
|
|
110
|
+
if (!fresh) return 1;
|
|
111
|
+
return await execOnResolved(credentials, fresh, command, options);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function reportResult(result) {
|
|
115
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
116
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
117
|
+
if (result.error && !result.error.startsWith("Command failed:")) {
|
|
118
|
+
console.error(result.error);
|
|
119
|
+
}
|
|
120
|
+
return result.exitCode ?? (result.success ? 0 : 1);
|
|
121
|
+
}
|
|
122
|
+
async function streamOnResolved(credentials, { device, key, variant }, command, options) {
|
|
123
|
+
const socket = io(configuration.serverUrl, {
|
|
124
|
+
auth: { token: credentials.token, clientType: "user-scoped" },
|
|
125
|
+
path: "/v1/updates",
|
|
126
|
+
transports: ["websocket"]
|
|
127
|
+
});
|
|
128
|
+
await new Promise((resolve, reject) => {
|
|
129
|
+
socket.once("connect", () => resolve());
|
|
130
|
+
socket.once("connect_error", reject);
|
|
131
|
+
});
|
|
132
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
133
|
+
try {
|
|
134
|
+
const answer = await socket.timeout(3e4).emitWithAck("rpc-call", {
|
|
135
|
+
method: `${device.id}:pty-start`,
|
|
136
|
+
params: encodeBase64(encrypt(key, variant, {
|
|
137
|
+
cols: process.stdout.columns || 80,
|
|
138
|
+
rows: process.stdout.rows || 24,
|
|
139
|
+
exec: { command, timeoutMs }
|
|
140
|
+
}))
|
|
141
|
+
});
|
|
142
|
+
if (!answer?.ok) throw new Error(answer?.error || "pty-start failed");
|
|
143
|
+
const started = decrypt(key, variant, decodeBase64(answer.result));
|
|
144
|
+
if (!started?.ok || !started.ptyId) {
|
|
145
|
+
throw new Error(started?.error ?? "Device refused to start the command");
|
|
146
|
+
}
|
|
147
|
+
return await new Promise((resolve) => {
|
|
148
|
+
const execId = started.ptyId;
|
|
149
|
+
const guard = setTimeout(() => resolve(1), timeoutMs + 3e4);
|
|
150
|
+
socket.on("pty-output", (envelope) => {
|
|
151
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
|
|
152
|
+
try {
|
|
153
|
+
const frame = decrypt(key, variant, decodeBase64(envelope.data));
|
|
154
|
+
if (typeof frame?.chunk !== "string") return;
|
|
155
|
+
const target = frame.stream === "stderr" ? process.stderr : process.stdout;
|
|
156
|
+
target.write(Buffer.from(frame.chunk, "base64"));
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
socket.on("pty-exit", (envelope) => {
|
|
161
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
|
|
162
|
+
clearTimeout(guard);
|
|
163
|
+
resolve(typeof envelope.exitCode === "number" ? envelope.exitCode : 1);
|
|
164
|
+
});
|
|
165
|
+
socket.on("disconnect", () => {
|
|
166
|
+
clearTimeout(guard);
|
|
167
|
+
resolve(1);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
} finally {
|
|
171
|
+
socket.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function execOnResolved(credentials, { device, key, variant }, command, options) {
|
|
175
|
+
const socket = io(configuration.serverUrl, {
|
|
176
|
+
auth: { token: credentials.token, clientType: "user-scoped" },
|
|
177
|
+
path: "/v1/updates",
|
|
178
|
+
transports: ["websocket"]
|
|
179
|
+
});
|
|
180
|
+
await new Promise((resolve, reject) => {
|
|
181
|
+
socket.once("connect", () => resolve());
|
|
182
|
+
socket.once("connect_error", reject);
|
|
183
|
+
});
|
|
184
|
+
try {
|
|
185
|
+
const result = await deviceExec(socket, credentials, device.id, command, {
|
|
186
|
+
deviceKeyBase64: encodeBase64(key),
|
|
187
|
+
deviceKeyVariant: variant,
|
|
188
|
+
timeout: options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
|
|
189
|
+
// matchDevice already read the listing and rejected offline devices.
|
|
190
|
+
skipMachineLookup: true
|
|
191
|
+
});
|
|
192
|
+
return reportResult(result);
|
|
193
|
+
} finally {
|
|
194
|
+
socket.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function sshDevice(credentials, query) {
|
|
198
|
+
const { devices } = await listDevicesWithNames(credentials);
|
|
199
|
+
const device = matchDevice(devices, query);
|
|
200
|
+
if (!device) {
|
|
201
|
+
console.error(`No enrolled device matches "${query}". Run "happy ssh" with no arguments to list devices.`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
if (!device.active) {
|
|
205
|
+
console.error(`Device "${device.name}" is offline.`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
const { key, variant } = await ensureDeviceKey(credentials, device.id, () => {
|
|
209
|
+
console.log("Waiting for approval in the Happy app (Devices \u2192 pending request)...");
|
|
210
|
+
});
|
|
211
|
+
const socket = io(configuration.serverUrl, {
|
|
212
|
+
auth: { token: credentials.token, clientType: "user-scoped" },
|
|
213
|
+
path: "/v1/updates",
|
|
214
|
+
transports: ["websocket"]
|
|
215
|
+
});
|
|
216
|
+
await new Promise((resolve, reject) => {
|
|
217
|
+
socket.once("connect", () => resolve());
|
|
218
|
+
socket.once("connect_error", reject);
|
|
219
|
+
});
|
|
220
|
+
const rpc = async (method, params) => {
|
|
221
|
+
const answer = await socket.timeout(3e4).emitWithAck("rpc-call", {
|
|
222
|
+
method: `${device.id}:${method}`,
|
|
223
|
+
params: encodeBase64(encrypt(key, variant, params))
|
|
224
|
+
});
|
|
225
|
+
if (!answer?.ok) throw new Error(answer?.error || `${method} failed`);
|
|
226
|
+
return decrypt(key, variant, decodeBase64(answer.result));
|
|
227
|
+
};
|
|
228
|
+
const cols = process.stdout.columns || 80;
|
|
229
|
+
const rows = process.stdout.rows || 24;
|
|
230
|
+
const started = await rpc("pty-start", { cols, rows });
|
|
231
|
+
if (!started?.ok || !started.ptyId) {
|
|
232
|
+
socket.close();
|
|
233
|
+
console.error(`Could not open a shell on "${device.name}": ${started?.error ?? "unknown error"}`);
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
const ptyId = started.ptyId;
|
|
237
|
+
let exitCode = 0;
|
|
238
|
+
const cleanup = () => {
|
|
239
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
240
|
+
process.stdin.pause();
|
|
241
|
+
socket.close();
|
|
242
|
+
};
|
|
243
|
+
await new Promise((resolve) => {
|
|
244
|
+
socket.on("pty-output", (envelope) => {
|
|
245
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== ptyId) return;
|
|
246
|
+
try {
|
|
247
|
+
const data = decrypt(key, variant, decodeBase64(envelope.data));
|
|
248
|
+
if (typeof data === "string") process.stdout.write(Buffer.from(data, "base64"));
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
socket.on("pty-exit", (envelope) => {
|
|
253
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== ptyId) return;
|
|
254
|
+
exitCode = typeof envelope.exitCode === "number" ? envelope.exitCode : 0;
|
|
255
|
+
resolve();
|
|
256
|
+
});
|
|
257
|
+
socket.on("disconnect", () => resolve());
|
|
258
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
259
|
+
process.stdin.resume();
|
|
260
|
+
process.stdin.on("data", (chunk) => {
|
|
261
|
+
socket.volatile.emit("pty-input", {
|
|
262
|
+
sessionId: device.id,
|
|
263
|
+
ptyId,
|
|
264
|
+
data: encodeBase64(encrypt(key, variant, chunk.toString("utf8")))
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
process.stdout.on("resize", () => {
|
|
268
|
+
void rpc("pty-resize", {
|
|
269
|
+
ptyId,
|
|
270
|
+
cols: process.stdout.columns || 80,
|
|
271
|
+
rows: process.stdout.rows || 24
|
|
272
|
+
}).catch(() => {
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
await rpc("pty-close", { ptyId }).catch(() => {
|
|
277
|
+
});
|
|
278
|
+
cleanup();
|
|
279
|
+
return exitCode;
|
|
280
|
+
}
|
|
281
|
+
function pickDevice(devices) {
|
|
282
|
+
const selectable = devices.filter((device) => device.active);
|
|
283
|
+
if (selectable.length === 0) {
|
|
284
|
+
console.log("No devices are online right now.");
|
|
285
|
+
return Promise.resolve(null);
|
|
286
|
+
}
|
|
287
|
+
if (selectable.length === 1) {
|
|
288
|
+
return Promise.resolve(selectable[0]);
|
|
289
|
+
}
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
let index = 0;
|
|
292
|
+
let rendered = 0;
|
|
293
|
+
const render = () => {
|
|
294
|
+
if (rendered > 0) process.stdout.write(`\x1B[${rendered}A`);
|
|
295
|
+
const lines = selectable.map((device, position) => {
|
|
296
|
+
const marker = position === index ? "\x1B[36m>\x1B[0m" : " ";
|
|
297
|
+
const name = position === index ? `\x1B[1m${device.name}\x1B[0m` : device.name;
|
|
298
|
+
return `\x1B[2K ${marker} ${name}${device.platform ? ` \x1B[2m${device.platform}\x1B[0m` : ""}`;
|
|
299
|
+
});
|
|
300
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
301
|
+
rendered = lines.length;
|
|
302
|
+
};
|
|
303
|
+
const finish = (device) => {
|
|
304
|
+
process.stdin.off("data", onData);
|
|
305
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
306
|
+
process.stdin.pause();
|
|
307
|
+
resolve(device);
|
|
308
|
+
};
|
|
309
|
+
const onData = (chunk) => {
|
|
310
|
+
const key = chunk.toString();
|
|
311
|
+
if (key === "\x1B[A" || key === "k") {
|
|
312
|
+
index = (index - 1 + selectable.length) % selectable.length;
|
|
313
|
+
render();
|
|
314
|
+
} else if (key === "\x1B[B" || key === "j") {
|
|
315
|
+
index = (index + 1) % selectable.length;
|
|
316
|
+
render();
|
|
317
|
+
} else if (key === "\r" || key === "\n") {
|
|
318
|
+
finish(selectable[index]);
|
|
319
|
+
} else if (key === "" || key === "\x1B" || key === "q") {
|
|
320
|
+
console.log("Cancelled.");
|
|
321
|
+
finish(null);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
console.log("Select a device (\u2191/\u2193, Enter to connect, q to cancel):");
|
|
325
|
+
render();
|
|
326
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
327
|
+
process.stdin.resume();
|
|
328
|
+
process.stdin.on("data", onData);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
async function listDevicesWithNames(credentials) {
|
|
332
|
+
const [devices, names] = await Promise.all([listDevices(credentials), readCachedDeviceNames()]);
|
|
333
|
+
let named = false;
|
|
334
|
+
const withNames = devices.map((device) => {
|
|
335
|
+
const cached = names[device.id];
|
|
336
|
+
if (cached) named = true;
|
|
337
|
+
return cached ? { ...device, name: cached } : device;
|
|
338
|
+
});
|
|
339
|
+
return { devices: withNames, named };
|
|
340
|
+
}
|
|
341
|
+
async function sshPickAndConnect(credentials) {
|
|
342
|
+
const { devices } = await listDevicesWithNames(credentials);
|
|
343
|
+
if (devices.length === 0) {
|
|
344
|
+
console.log("No devices enrolled yet. Add one from Happy \u2192 Devices.");
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
if (!process.stdin.isTTY) {
|
|
348
|
+
for (const device of devices) {
|
|
349
|
+
console.log(`${device.active ? "online " : "offline"} ${device.name}${device.platform ? ` (${device.platform})` : ""}`);
|
|
350
|
+
}
|
|
351
|
+
console.log("\nUsage: happy ssh <device>");
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
const chosen = await pickDevice(devices);
|
|
355
|
+
if (!chosen) return 0;
|
|
356
|
+
return await sshDevice(credentials, chosen.id);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export { parseSshArgs, pickCachedDeviceId, runOnDevice, sshDevice, sshPickAndConnect };
|