@zonease/happy 0.6.39 → 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/{api-CwOCQeLH.cjs → api-C7FyJfZ1.cjs} +87 -7
- package/dist/{api-CIZKyfSn.mjs → api-D6WL40-U.mjs} +86 -6
- package/dist/{auth-BIleVCw0.cjs → auth-C7ZScWEw.cjs} +2 -2
- package/dist/{auth-Cas9DR3b.cjs → auth-DCsIoZLT.cjs} +4 -4
- package/dist/{auth-0VnlXT9f.mjs → auth-DO9GexAR.mjs} +4 -4
- package/dist/{auth-BE8dD20T.mjs → auth-DxJ3VvrX.mjs} +2 -2
- package/dist/{browser-Dll5hxOV.mjs → browser-AoVGRX7y.mjs} +1 -1
- package/dist/{browser-Cc08m7of.cjs → browser-DzgO4oaD.cjs} +1 -1
- package/dist/{claudeLocal-WkgLbl1a.cjs → claudeLocal-C6BdzhDx.cjs} +1 -1
- package/dist/{claudeLocal-CPT3zcIU.mjs → claudeLocal-CWNGpJ0-.mjs} +1 -1
- package/dist/{config-Draa-5XC.cjs → config-CclB3QW9.cjs} +1 -1
- package/dist/{config-rTT4qTU5.mjs → config-DZk-mJ0K.mjs} +1 -1
- package/dist/{connect-BfC1tYP_.mjs → connect-BXLUKJ5P.mjs} +4 -4
- package/dist/{connect-CJZ73syK.cjs → connect-k9CqDJ-T.cjs} +4 -4
- package/dist/{controlClient-DZMIsKsT.cjs → controlClient-B23M-WPp.cjs} +10 -1
- package/dist/{controlClient-CM_QcJnB.mjs → controlClient-DB5tDU7V.mjs} +10 -2
- package/dist/deviceKeys-BTdo34Uz.mjs +175 -0
- package/dist/deviceKeys-C540DcXl.cjs +182 -0
- package/dist/{doctor-Cya9YSA0.cjs → doctor-BQGLhrcI.cjs} +2 -2
- package/dist/{doctor-DZYB6IVc.mjs → doctor-CU4JLGQ1.mjs} +2 -2
- package/dist/{downloadImage-DfSFLb-h.mjs → downloadImage-DZXI3mXL.mjs} +3 -3
- package/dist/{downloadImage-MQMrUUxS.cjs → downloadImage-cQ1Z7hkC.cjs} +5 -5
- package/dist/{enroll-C52x8oly.mjs → enroll-B55BQR56.mjs} +3 -3
- package/dist/{enroll-BxlbVftP.cjs → enroll-Bo-CNio1.cjs} +3 -3
- package/dist/{evidence-CjWBG5rX.cjs → evidence-DQp8QWcv.cjs} +1 -1
- package/dist/{evidence-Ccd9iYcj.mjs → evidence-DTYRAaOZ.mjs} +1 -1
- package/dist/{formatImageMessage-DbyG8c2j.cjs → formatImageMessage-CixDYY6G.cjs} +1 -1
- package/dist/{formatImageMessage-B4lSxlk2.mjs → formatImageMessage-DbKSSjMx.mjs} +1 -1
- package/dist/index.cjs +42 -42
- package/dist/index.mjs +42 -42
- package/dist/{install-DAeCn8ae.mjs → install-B1e6rdy7.mjs} +2 -2
- package/dist/{install-oRAjVLjP.mjs → install-CgUPGIoF.mjs} +1 -1
- package/dist/{install-C_cp8b2-.cjs → install-D7Ir7ciZ.cjs} +2 -2
- package/dist/{install-BokHRgFW.cjs → install-DSDufaPd.cjs} +1 -1
- package/dist/lib.cjs +3 -3
- package/dist/lib.d.cts +77 -0
- package/dist/lib.d.mts +77 -0
- package/dist/lib.mjs +3 -3
- package/dist/{loadableToolOutput-Cuygmd4K.mjs → loadableToolOutput-Bfei9wPq.mjs} +3 -3
- package/dist/{loadableToolOutput-DDCJjywt.cjs → loadableToolOutput-CbLGtvST.cjs} +3 -3
- package/dist/{persistence-JLUn_Nou.cjs → persistence-CBTtC_dm.cjs} +1 -1
- package/dist/{persistence-BFjvtUad.mjs → persistence-DKfF81pd.mjs} +2 -2
- package/dist/{run-DBZqN6Kn.mjs → run-BVZsMNwN.mjs} +45 -9
- package/dist/{run-BDn2aE3Q.cjs → run-CURx7LhK.cjs} +45 -9
- package/dist/{runClaude-DNHvERX0.mjs → runClaude-6Gi0sYQy.mjs} +14 -14
- package/dist/{runClaude-uaKxPZPm.cjs → runClaude-CL3Kt5ex.cjs} +15 -15
- package/dist/{runCodex-C-RTM0wA.mjs → runCodex-ClwEgJ7M.mjs} +13 -13
- package/dist/{runCodex-82UrZLme.cjs → runCodex-YKyVBkJ3.cjs} +13 -13
- package/dist/{runGemini-Bm6-4Xm2.mjs → runGemini-BPWxVr60.mjs} +15 -15
- package/dist/{runGemini-CXMr4Jwb.cjs → runGemini-Xz2W1spn.cjs} +15 -15
- package/dist/{runOneShot-ioARRHrx.mjs → runOneShot-CdCWiiLr.mjs} +2 -2
- package/dist/{runOneShot-1B6RMYT7.cjs → runOneShot-DjZyCvDI.cjs} +2 -2
- package/dist/{serve-CwibwU6N.mjs → serve-B48JttQ3.mjs} +6 -6
- package/dist/{serve-BtdAhkIE.cjs → serve-B8vaWQA0.cjs} +6 -6
- package/dist/{session-2sU4Bu1R.mjs → session-BXboDn2V.mjs} +5 -5
- package/dist/{session-D--F87zL.cjs → session-BZO25zxp.cjs} +5 -5
- package/dist/{sessionKeyCache-D2BwO91C.mjs → sessionKeyCache-B6cGadYL.mjs} +1 -1
- package/dist/{sessionKeyCache-4Tp8pJ3N.cjs → sessionKeyCache-CDmexJ9p.cjs} +1 -1
- package/dist/{sessionList-BCSw3qYt.mjs → sessionList-CgAYZpNj.mjs} +2 -2
- package/dist/{sessionList-ICJsjNtJ.cjs → sessionList-lQ-nqEhc.cjs} +2 -2
- package/dist/{sessionSummary-Bkz52BPh.cjs → sessionSummary-CcORiQSq.cjs} +3 -3
- package/dist/{sessionSummary-lHGBWQSb.mjs → sessionSummary-DCWQoaDP.mjs} +3 -3
- package/dist/{spawnHappyCLI-D_V-AAoP.cjs → spawnHappyCLI-KiazE3kL.cjs} +1 -1
- package/dist/{spawnHappyCLI-CoORX0YW.mjs → spawnHappyCLI-yBIP_Oqi.mjs} +1 -1
- package/dist/{ssh-DL_AkRAD.mjs → ssh-q5uZhqBf.mjs} +134 -103
- package/dist/{ssh-BTOlhhxi.cjs → ssh-yMllSHu5.cjs} +138 -106
- package/dist/{task-CSpaC9HX.cjs → task-_nr7J19j.cjs} +6 -6
- package/dist/{task-CQzrHRZe.mjs → task-wNX1N2i4.mjs} +6 -6
- package/dist/{uninstall-BiKyRAb1.cjs → uninstall-Buaz7d-X.cjs} +2 -2
- package/dist/{uninstall-nFMFGUVM.mjs → uninstall-Dxf6_4mQ.mjs} +2 -2
- package/dist/{update-Dw8pgsAT.cjs → update-CGlIfa8e.cjs} +3 -3
- package/dist/{update-BcqRHXf2.mjs → update-CnnoX2cV.mjs} +3 -3
- package/package.json +1 -1
- package/dist/deviceExec-CU1R9CRb.cjs +0 -84
- package/dist/deviceExec-D93AOVV4.mjs +0 -81
|
@@ -1,105 +1,27 @@
|
|
|
1
1
|
import { io } from 'socket.io-client';
|
|
2
|
-
import { c as configuration,
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import { hostname } from 'node:os';
|
|
10
|
-
import { decryptWithEphemeralKey } from './auth-BE8dD20T.mjs';
|
|
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';
|
|
11
9
|
import 'zod';
|
|
12
10
|
import 'node:crypto';
|
|
11
|
+
import 'tweetnacl';
|
|
13
12
|
import 'chalk';
|
|
14
13
|
import 'fs';
|
|
14
|
+
import 'axios';
|
|
15
|
+
import './auth-DxJ3VvrX.mjs';
|
|
15
16
|
import 'qrcode-terminal';
|
|
16
17
|
import './time-DgsBLJA_.mjs';
|
|
17
|
-
import './browser-
|
|
18
|
+
import './browser-AoVGRX7y.mjs';
|
|
18
19
|
import 'open';
|
|
19
20
|
import 'react';
|
|
20
21
|
import 'ink';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function deviceKeysFile() {
|
|
25
|
-
return join(configuration.happyHomeDir, "device-keys.json");
|
|
26
|
-
}
|
|
27
|
-
async function readDeviceKeys() {
|
|
28
|
-
const file = deviceKeysFile();
|
|
29
|
-
if (!existsSync(file)) return {};
|
|
30
|
-
try {
|
|
31
|
-
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
32
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
33
|
-
} catch {
|
|
34
|
-
return {};
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
async function writeDeviceDirectory(entries) {
|
|
38
|
-
const file = deviceKeysFile();
|
|
39
|
-
await mkdir(dirname(file), { recursive: true });
|
|
40
|
-
const keys = await readDeviceKeys();
|
|
41
|
-
for (const entry of entries) {
|
|
42
|
-
keys[entry.id] = { name: entry.name, key: entry.key, variant: entry.variant ?? "dataKey" };
|
|
43
|
-
}
|
|
44
|
-
await writeFile(file, JSON.stringify(keys, null, 2), { mode: 384 });
|
|
45
|
-
}
|
|
46
|
-
async function readCachedDeviceKey(machineId) {
|
|
47
|
-
const keys = await readDeviceKeys();
|
|
48
|
-
const stored = keys[machineId];
|
|
49
|
-
if (!stored?.key) return null;
|
|
50
|
-
return { key: decodeBase64(stored.key), variant: stored.variant ?? "dataKey" };
|
|
51
|
-
}
|
|
52
|
-
async function readCachedDeviceNames() {
|
|
53
|
-
const keys = await readDeviceKeys();
|
|
54
|
-
return Object.fromEntries(Object.entries(keys).map(([id, entry]) => [id, entry.name]));
|
|
55
|
-
}
|
|
56
|
-
async function ensureDeviceKey(credentials, machineId, onPending) {
|
|
57
|
-
const cached = await readCachedDeviceKey(machineId);
|
|
58
|
-
if (cached) return cached;
|
|
59
|
-
const seed = tweetnacl.randomBytes(32);
|
|
60
|
-
const secretKey = tweetnacl.hash(seed).slice(0, 32);
|
|
61
|
-
const publicKey = tweetnacl.box.keyPair.fromSecretKey(secretKey).publicKey;
|
|
62
|
-
const created = await axios.post(
|
|
63
|
-
`${configuration.serverUrl}/v1/devices/key-requests`,
|
|
64
|
-
{ machineId, publicKey: encodeBase64(publicKey), label: `happy ssh from ${hostname()}` },
|
|
65
|
-
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
66
|
-
);
|
|
67
|
-
onPending?.({ id: created.data.id });
|
|
68
|
-
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
69
|
-
while (Date.now() < deadline) {
|
|
70
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
71
|
-
const status = await axios.get(
|
|
72
|
-
`${configuration.serverUrl}/v1/devices/key-requests/${created.data.id}`,
|
|
73
|
-
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
74
|
-
);
|
|
75
|
-
if (status.data.expired) {
|
|
76
|
-
throw new Error("Authorization request expired. Run the command again.");
|
|
77
|
-
}
|
|
78
|
-
if (status.data.approved && status.data.response) {
|
|
79
|
-
const opened = decryptWithEphemeralKey(decodeBase64(status.data.response), secretKey);
|
|
80
|
-
if (!opened) {
|
|
81
|
-
throw new Error("Could not open the device directory returned by the app");
|
|
82
|
-
}
|
|
83
|
-
let parsed;
|
|
84
|
-
try {
|
|
85
|
-
parsed = JSON.parse(Buffer.from(opened).toString("utf8"));
|
|
86
|
-
} catch {
|
|
87
|
-
throw new Error("Device directory from the app was malformed");
|
|
88
|
-
}
|
|
89
|
-
const devices = parsed.devices ?? [];
|
|
90
|
-
if (devices.length === 0) {
|
|
91
|
-
throw new Error("The app returned an empty device directory");
|
|
92
|
-
}
|
|
93
|
-
await writeDeviceDirectory(devices);
|
|
94
|
-
const target = devices.find((device) => device.id === machineId);
|
|
95
|
-
if (!target) {
|
|
96
|
-
throw new Error("The app did not include this device in the directory");
|
|
97
|
-
}
|
|
98
|
-
return { key: decodeBase64(target.key), variant: target.variant ?? "dataKey" };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
throw new Error("Timed out waiting for approval in the Happy app");
|
|
102
|
-
}
|
|
22
|
+
import './projectPath-BTBIMB57.mjs';
|
|
23
|
+
import 'path';
|
|
24
|
+
import 'url';
|
|
103
25
|
|
|
104
26
|
function matchDevice(devices, query) {
|
|
105
27
|
const lowered = query.trim().toLowerCase();
|
|
@@ -112,7 +34,12 @@ function parseSshArgs(args) {
|
|
|
112
34
|
const command = separator >= 0 ? args.slice(separator + 1).join(" ") : null;
|
|
113
35
|
let target = null;
|
|
114
36
|
let timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS;
|
|
37
|
+
let stream = false;
|
|
115
38
|
for (let i = 0; i < head.length; i++) {
|
|
39
|
+
if (head[i] === "--stream" || head[i] === "-s") {
|
|
40
|
+
stream = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
116
43
|
if (head[i] === "--timeout" || head[i] === "-t") {
|
|
117
44
|
const seconds = Number(head[++i]);
|
|
118
45
|
if (Number.isFinite(seconds) && seconds > 0) timeoutMs = seconds * 1e3;
|
|
@@ -120,22 +47,131 @@ function parseSshArgs(args) {
|
|
|
120
47
|
}
|
|
121
48
|
if (!target && !head[i].startsWith("-")) target = head[i];
|
|
122
49
|
}
|
|
123
|
-
return { target, command: command && command.trim() ? command : null, timeoutMs };
|
|
50
|
+
return { target, command: command && command.trim() ? command : null, timeoutMs, stream };
|
|
124
51
|
}
|
|
125
|
-
async function
|
|
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) {
|
|
126
74
|
const { devices } = await listDevicesWithNames(credentials);
|
|
127
75
|
const device = matchDevice(devices, query);
|
|
128
76
|
if (!device) {
|
|
129
77
|
console.error(`No enrolled device matches "${query}". Run "happy ssh" with no arguments to list devices.`);
|
|
130
|
-
return
|
|
78
|
+
return null;
|
|
131
79
|
}
|
|
132
80
|
if (!device.active) {
|
|
133
81
|
console.error(`Device "${device.name}" is offline.`);
|
|
134
|
-
return
|
|
82
|
+
return null;
|
|
135
83
|
}
|
|
136
84
|
const { key, variant } = await ensureDeviceKey(credentials, device.id, () => {
|
|
137
85
|
console.error("Waiting for approval in the Happy app (Devices \u2192 pending request)...");
|
|
138
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) {
|
|
139
175
|
const socket = io(configuration.serverUrl, {
|
|
140
176
|
auth: { token: credentials.token, clientType: "user-scoped" },
|
|
141
177
|
path: "/v1/updates",
|
|
@@ -153,12 +189,7 @@ async function runOnDevice(credentials, query, command, options = {}) {
|
|
|
153
189
|
// matchDevice already read the listing and rejected offline devices.
|
|
154
190
|
skipMachineLookup: true
|
|
155
191
|
});
|
|
156
|
-
|
|
157
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
158
|
-
if (result.error && !result.error.startsWith("Command failed:")) {
|
|
159
|
-
console.error(result.error);
|
|
160
|
-
}
|
|
161
|
-
return result.exitCode ?? (result.success ? 0 : 1);
|
|
192
|
+
return reportResult(result);
|
|
162
193
|
} finally {
|
|
163
194
|
socket.close();
|
|
164
195
|
}
|
|
@@ -325,4 +356,4 @@ async function sshPickAndConnect(credentials) {
|
|
|
325
356
|
return await sshDevice(credentials, chosen.id);
|
|
326
357
|
}
|
|
327
358
|
|
|
328
|
-
export { parseSshArgs, runOnDevice, sshDevice, sshPickAndConnect };
|
|
359
|
+
export { parseSshArgs, pickCachedDeviceId, runOnDevice, sshDevice, sshPickAndConnect };
|
|
@@ -1,107 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var socket_ioClient = require('socket.io-client');
|
|
4
|
-
var logger = require('./persistence-
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
var os = require('node:os');
|
|
12
|
-
var auth = require('./auth-BIleVCw0.cjs');
|
|
4
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
5
|
+
var deviceKeys = require('./deviceKeys-C540DcXl.cjs');
|
|
6
|
+
var controlClient = require('./controlClient-B23M-WPp.cjs');
|
|
7
|
+
require('node:fs/promises');
|
|
8
|
+
require('node:fs');
|
|
9
|
+
require('node:os');
|
|
10
|
+
require('node:path');
|
|
13
11
|
require('zod');
|
|
14
12
|
require('node:crypto');
|
|
13
|
+
require('tweetnacl');
|
|
15
14
|
require('chalk');
|
|
16
15
|
require('fs');
|
|
16
|
+
require('axios');
|
|
17
|
+
require('./auth-C7ZScWEw.cjs');
|
|
17
18
|
require('qrcode-terminal');
|
|
18
19
|
require('./time-Bj2HRkKH.cjs');
|
|
19
|
-
require('./browser-
|
|
20
|
+
require('./browser-DzgO4oaD.cjs');
|
|
20
21
|
require('open');
|
|
21
22
|
require('react');
|
|
22
23
|
require('ink');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
function deviceKeysFile() {
|
|
27
|
-
return path.join(logger.configuration.happyHomeDir, "device-keys.json");
|
|
28
|
-
}
|
|
29
|
-
async function readDeviceKeys() {
|
|
30
|
-
const file = deviceKeysFile();
|
|
31
|
-
if (!fs.existsSync(file)) return {};
|
|
32
|
-
try {
|
|
33
|
-
const parsed = JSON.parse(await promises.readFile(file, "utf8"));
|
|
34
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
35
|
-
} catch {
|
|
36
|
-
return {};
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async function writeDeviceDirectory(entries) {
|
|
40
|
-
const file = deviceKeysFile();
|
|
41
|
-
await promises.mkdir(path.dirname(file), { recursive: true });
|
|
42
|
-
const keys = await readDeviceKeys();
|
|
43
|
-
for (const entry of entries) {
|
|
44
|
-
keys[entry.id] = { name: entry.name, key: entry.key, variant: entry.variant ?? "dataKey" };
|
|
45
|
-
}
|
|
46
|
-
await promises.writeFile(file, JSON.stringify(keys, null, 2), { mode: 384 });
|
|
47
|
-
}
|
|
48
|
-
async function readCachedDeviceKey(machineId) {
|
|
49
|
-
const keys = await readDeviceKeys();
|
|
50
|
-
const stored = keys[machineId];
|
|
51
|
-
if (!stored?.key) return null;
|
|
52
|
-
return { key: logger.decodeBase64(stored.key), variant: stored.variant ?? "dataKey" };
|
|
53
|
-
}
|
|
54
|
-
async function readCachedDeviceNames() {
|
|
55
|
-
const keys = await readDeviceKeys();
|
|
56
|
-
return Object.fromEntries(Object.entries(keys).map(([id, entry]) => [id, entry.name]));
|
|
57
|
-
}
|
|
58
|
-
async function ensureDeviceKey(credentials, machineId, onPending) {
|
|
59
|
-
const cached = await readCachedDeviceKey(machineId);
|
|
60
|
-
if (cached) return cached;
|
|
61
|
-
const seed = tweetnacl.randomBytes(32);
|
|
62
|
-
const secretKey = tweetnacl.hash(seed).slice(0, 32);
|
|
63
|
-
const publicKey = tweetnacl.box.keyPair.fromSecretKey(secretKey).publicKey;
|
|
64
|
-
const created = await axios.post(
|
|
65
|
-
`${logger.configuration.serverUrl}/v1/devices/key-requests`,
|
|
66
|
-
{ machineId, publicKey: logger.encodeBase64(publicKey), label: `happy ssh from ${os.hostname()}` },
|
|
67
|
-
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
68
|
-
);
|
|
69
|
-
onPending?.({ id: created.data.id });
|
|
70
|
-
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
71
|
-
while (Date.now() < deadline) {
|
|
72
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
73
|
-
const status = await axios.get(
|
|
74
|
-
`${logger.configuration.serverUrl}/v1/devices/key-requests/${created.data.id}`,
|
|
75
|
-
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
76
|
-
);
|
|
77
|
-
if (status.data.expired) {
|
|
78
|
-
throw new Error("Authorization request expired. Run the command again.");
|
|
79
|
-
}
|
|
80
|
-
if (status.data.approved && status.data.response) {
|
|
81
|
-
const opened = auth.decryptWithEphemeralKey(logger.decodeBase64(status.data.response), secretKey);
|
|
82
|
-
if (!opened) {
|
|
83
|
-
throw new Error("Could not open the device directory returned by the app");
|
|
84
|
-
}
|
|
85
|
-
let parsed;
|
|
86
|
-
try {
|
|
87
|
-
parsed = JSON.parse(Buffer.from(opened).toString("utf8"));
|
|
88
|
-
} catch {
|
|
89
|
-
throw new Error("Device directory from the app was malformed");
|
|
90
|
-
}
|
|
91
|
-
const devices = parsed.devices ?? [];
|
|
92
|
-
if (devices.length === 0) {
|
|
93
|
-
throw new Error("The app returned an empty device directory");
|
|
94
|
-
}
|
|
95
|
-
await writeDeviceDirectory(devices);
|
|
96
|
-
const target = devices.find((device) => device.id === machineId);
|
|
97
|
-
if (!target) {
|
|
98
|
-
throw new Error("The app did not include this device in the directory");
|
|
99
|
-
}
|
|
100
|
-
return { key: logger.decodeBase64(target.key), variant: target.variant ?? "dataKey" };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
throw new Error("Timed out waiting for approval in the Happy app");
|
|
104
|
-
}
|
|
24
|
+
require('./projectPath-ChLhdOyt.cjs');
|
|
25
|
+
require('path');
|
|
26
|
+
require('url');
|
|
105
27
|
|
|
106
28
|
function matchDevice(devices, query) {
|
|
107
29
|
const lowered = query.trim().toLowerCase();
|
|
@@ -114,7 +36,12 @@ function parseSshArgs(args) {
|
|
|
114
36
|
const command = separator >= 0 ? args.slice(separator + 1).join(" ") : null;
|
|
115
37
|
let target = null;
|
|
116
38
|
let timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS;
|
|
39
|
+
let stream = false;
|
|
117
40
|
for (let i = 0; i < head.length; i++) {
|
|
41
|
+
if (head[i] === "--stream" || head[i] === "-s") {
|
|
42
|
+
stream = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
118
45
|
if (head[i] === "--timeout" || head[i] === "-t") {
|
|
119
46
|
const seconds = Number(head[++i]);
|
|
120
47
|
if (Number.isFinite(seconds) && seconds > 0) timeoutMs = seconds * 1e3;
|
|
@@ -122,22 +49,131 @@ function parseSshArgs(args) {
|
|
|
122
49
|
}
|
|
123
50
|
if (!target && !head[i].startsWith("-")) target = head[i];
|
|
124
51
|
}
|
|
125
|
-
return { target, command: command && command.trim() ? command : null, timeoutMs };
|
|
52
|
+
return { target, command: command && command.trim() ? command : null, timeoutMs, stream };
|
|
126
53
|
}
|
|
127
|
-
async function
|
|
54
|
+
async function resolveFromCache(query) {
|
|
55
|
+
const names = await deviceKeys.readCachedDeviceNames();
|
|
56
|
+
const id = pickCachedDeviceId(names, query);
|
|
57
|
+
if (!id) return null;
|
|
58
|
+
const material = await deviceKeys.readCachedDeviceKey(id);
|
|
59
|
+
return material ? { device: { id, name: names[id] }, ...material } : null;
|
|
60
|
+
}
|
|
61
|
+
function pickCachedDeviceId(names, query) {
|
|
62
|
+
if (names[query]) return query;
|
|
63
|
+
const lowered = query.trim().toLowerCase();
|
|
64
|
+
for (const test of [
|
|
65
|
+
(name) => name === lowered,
|
|
66
|
+
(name) => name.startsWith(lowered),
|
|
67
|
+
(name) => name.includes(lowered)
|
|
68
|
+
]) {
|
|
69
|
+
const matches = Object.keys(names).filter((id) => test(names[id].toLowerCase()));
|
|
70
|
+
if (matches.length === 1) return matches[0];
|
|
71
|
+
if (matches.length > 1) return null;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
async function resolveFromServer(credentials, query) {
|
|
128
76
|
const { devices } = await listDevicesWithNames(credentials);
|
|
129
77
|
const device = matchDevice(devices, query);
|
|
130
78
|
if (!device) {
|
|
131
79
|
console.error(`No enrolled device matches "${query}". Run "happy ssh" with no arguments to list devices.`);
|
|
132
|
-
return
|
|
80
|
+
return null;
|
|
133
81
|
}
|
|
134
82
|
if (!device.active) {
|
|
135
83
|
console.error(`Device "${device.name}" is offline.`);
|
|
136
|
-
return
|
|
84
|
+
return null;
|
|
137
85
|
}
|
|
138
|
-
const { key, variant } = await ensureDeviceKey(credentials, device.id, () => {
|
|
86
|
+
const { key, variant } = await deviceKeys.ensureDeviceKey(credentials, device.id, () => {
|
|
139
87
|
console.error("Waiting for approval in the Happy app (Devices \u2192 pending request)...");
|
|
140
88
|
});
|
|
89
|
+
await deviceKeys.pruneDeviceAliases(device.id, device.name);
|
|
90
|
+
return { device, key, variant };
|
|
91
|
+
}
|
|
92
|
+
async function runOnDevice(credentials, query, command, options = {}) {
|
|
93
|
+
const cached = await resolveFromCache(query);
|
|
94
|
+
const resolved = cached ?? await resolveFromServer(credentials, query);
|
|
95
|
+
if (!resolved) return 1;
|
|
96
|
+
if (options.stream) {
|
|
97
|
+
return await streamOnResolved(credentials, resolved, command, options);
|
|
98
|
+
}
|
|
99
|
+
const viaDaemon = await controlClient.execOnDeviceViaDaemon(
|
|
100
|
+
resolved.device.id,
|
|
101
|
+
command,
|
|
102
|
+
options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS
|
|
103
|
+
).catch(() => null);
|
|
104
|
+
if (viaDaemon) {
|
|
105
|
+
return reportResult(viaDaemon);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return await execOnResolved(credentials, resolved, command, options);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (!cached) throw error;
|
|
111
|
+
const fresh = await resolveFromServer(credentials, query);
|
|
112
|
+
if (!fresh) return 1;
|
|
113
|
+
return await execOnResolved(credentials, fresh, command, options);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function reportResult(result) {
|
|
117
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
118
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
119
|
+
if (result.error && !result.error.startsWith("Command failed:")) {
|
|
120
|
+
console.error(result.error);
|
|
121
|
+
}
|
|
122
|
+
return result.exitCode ?? (result.success ? 0 : 1);
|
|
123
|
+
}
|
|
124
|
+
async function streamOnResolved(credentials, { device, key, variant }, command, options) {
|
|
125
|
+
const socket = socket_ioClient.io(logger.configuration.serverUrl, {
|
|
126
|
+
auth: { token: credentials.token, clientType: "user-scoped" },
|
|
127
|
+
path: "/v1/updates",
|
|
128
|
+
transports: ["websocket"]
|
|
129
|
+
});
|
|
130
|
+
await new Promise((resolve, reject) => {
|
|
131
|
+
socket.once("connect", () => resolve());
|
|
132
|
+
socket.once("connect_error", reject);
|
|
133
|
+
});
|
|
134
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
135
|
+
try {
|
|
136
|
+
const answer = await socket.timeout(3e4).emitWithAck("rpc-call", {
|
|
137
|
+
method: `${device.id}:pty-start`,
|
|
138
|
+
params: logger.encodeBase64(logger.encrypt(key, variant, {
|
|
139
|
+
cols: process.stdout.columns || 80,
|
|
140
|
+
rows: process.stdout.rows || 24,
|
|
141
|
+
exec: { command, timeoutMs }
|
|
142
|
+
}))
|
|
143
|
+
});
|
|
144
|
+
if (!answer?.ok) throw new Error(answer?.error || "pty-start failed");
|
|
145
|
+
const started = logger.decrypt(key, variant, logger.decodeBase64(answer.result));
|
|
146
|
+
if (!started?.ok || !started.ptyId) {
|
|
147
|
+
throw new Error(started?.error ?? "Device refused to start the command");
|
|
148
|
+
}
|
|
149
|
+
return await new Promise((resolve) => {
|
|
150
|
+
const execId = started.ptyId;
|
|
151
|
+
const guard = setTimeout(() => resolve(1), timeoutMs + 3e4);
|
|
152
|
+
socket.on("pty-output", (envelope) => {
|
|
153
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
|
|
154
|
+
try {
|
|
155
|
+
const frame = logger.decrypt(key, variant, logger.decodeBase64(envelope.data));
|
|
156
|
+
if (typeof frame?.chunk !== "string") return;
|
|
157
|
+
const target = frame.stream === "stderr" ? process.stderr : process.stdout;
|
|
158
|
+
target.write(Buffer.from(frame.chunk, "base64"));
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
socket.on("pty-exit", (envelope) => {
|
|
163
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
|
|
164
|
+
clearTimeout(guard);
|
|
165
|
+
resolve(typeof envelope.exitCode === "number" ? envelope.exitCode : 1);
|
|
166
|
+
});
|
|
167
|
+
socket.on("disconnect", () => {
|
|
168
|
+
clearTimeout(guard);
|
|
169
|
+
resolve(1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
} finally {
|
|
173
|
+
socket.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function execOnResolved(credentials, { device, key, variant }, command, options) {
|
|
141
177
|
const socket = socket_ioClient.io(logger.configuration.serverUrl, {
|
|
142
178
|
auth: { token: credentials.token, clientType: "user-scoped" },
|
|
143
179
|
path: "/v1/updates",
|
|
@@ -148,19 +184,14 @@ async function runOnDevice(credentials, query, command, options = {}) {
|
|
|
148
184
|
socket.once("connect_error", reject);
|
|
149
185
|
});
|
|
150
186
|
try {
|
|
151
|
-
const result = await
|
|
187
|
+
const result = await deviceKeys.deviceExec(socket, credentials, device.id, command, {
|
|
152
188
|
deviceKeyBase64: logger.encodeBase64(key),
|
|
153
189
|
deviceKeyVariant: variant,
|
|
154
190
|
timeout: options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
|
|
155
191
|
// matchDevice already read the listing and rejected offline devices.
|
|
156
192
|
skipMachineLookup: true
|
|
157
193
|
});
|
|
158
|
-
|
|
159
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
160
|
-
if (result.error && !result.error.startsWith("Command failed:")) {
|
|
161
|
-
console.error(result.error);
|
|
162
|
-
}
|
|
163
|
-
return result.exitCode ?? (result.success ? 0 : 1);
|
|
194
|
+
return reportResult(result);
|
|
164
195
|
} finally {
|
|
165
196
|
socket.close();
|
|
166
197
|
}
|
|
@@ -176,7 +207,7 @@ async function sshDevice(credentials, query) {
|
|
|
176
207
|
console.error(`Device "${device.name}" is offline.`);
|
|
177
208
|
return 1;
|
|
178
209
|
}
|
|
179
|
-
const { key, variant } = await ensureDeviceKey(credentials, device.id, () => {
|
|
210
|
+
const { key, variant } = await deviceKeys.ensureDeviceKey(credentials, device.id, () => {
|
|
180
211
|
console.log("Waiting for approval in the Happy app (Devices \u2192 pending request)...");
|
|
181
212
|
});
|
|
182
213
|
const socket = socket_ioClient.io(logger.configuration.serverUrl, {
|
|
@@ -300,7 +331,7 @@ function pickDevice(devices) {
|
|
|
300
331
|
});
|
|
301
332
|
}
|
|
302
333
|
async function listDevicesWithNames(credentials) {
|
|
303
|
-
const [devices, names] = await Promise.all([
|
|
334
|
+
const [devices, names] = await Promise.all([deviceKeys.listDevices(credentials), deviceKeys.readCachedDeviceNames()]);
|
|
304
335
|
let named = false;
|
|
305
336
|
const withNames = devices.map((device) => {
|
|
306
337
|
const cached = names[device.id];
|
|
@@ -328,6 +359,7 @@ async function sshPickAndConnect(credentials) {
|
|
|
328
359
|
}
|
|
329
360
|
|
|
330
361
|
exports.parseSshArgs = parseSshArgs;
|
|
362
|
+
exports.pickCachedDeviceId = pickCachedDeviceId;
|
|
331
363
|
exports.runOnDevice = runOnDevice;
|
|
332
364
|
exports.sshDevice = sshDevice;
|
|
333
365
|
exports.sshPickAndConnect = sshPickAndConnect;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
4
|
var path = require('node:path');
|
|
5
|
-
var logger = require('./persistence-
|
|
6
|
-
var sessionList = require('./sessionList-
|
|
7
|
-
var sessionSummary = require('./sessionSummary-
|
|
5
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
6
|
+
var sessionList = require('./sessionList-lQ-nqEhc.cjs');
|
|
7
|
+
var sessionSummary = require('./sessionSummary-CcORiQSq.cjs');
|
|
8
8
|
var workflowCommon = require('./workflowCommon-Bl568-rt.cjs');
|
|
9
9
|
require('node:fs/promises');
|
|
10
10
|
require('node:fs');
|
|
@@ -14,14 +14,14 @@ require('node:crypto');
|
|
|
14
14
|
require('tweetnacl');
|
|
15
15
|
require('fs');
|
|
16
16
|
require('axios');
|
|
17
|
-
require('./auth-
|
|
17
|
+
require('./auth-C7ZScWEw.cjs');
|
|
18
18
|
require('qrcode-terminal');
|
|
19
19
|
require('./time-Bj2HRkKH.cjs');
|
|
20
|
-
require('./browser-
|
|
20
|
+
require('./browser-DzgO4oaD.cjs');
|
|
21
21
|
require('open');
|
|
22
22
|
require('react');
|
|
23
23
|
require('ink');
|
|
24
|
-
require('./sessionKeyCache-
|
|
24
|
+
require('./sessionKeyCache-CDmexJ9p.cjs');
|
|
25
25
|
|
|
26
26
|
function printTaskHelp() {
|
|
27
27
|
console.log(`
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import { r as readCredentials } from './persistence-
|
|
4
|
-
import { r as runSessionList } from './sessionList-
|
|
5
|
-
import { r as runSessionSummary } from './sessionSummary-
|
|
3
|
+
import { r as readCredentials } from './persistence-DKfF81pd.mjs';
|
|
4
|
+
import { r as runSessionList } from './sessionList-CgAYZpNj.mjs';
|
|
5
|
+
import { r as runSessionSummary } from './sessionSummary-DCWQoaDP.mjs';
|
|
6
6
|
import { p as parseWorkflowArgs, s as stringFlag, b as booleanFlag, n as numberFlag, a as printOrWriteOutput } from './workflowCommon-B0d01Y_a.mjs';
|
|
7
7
|
import 'node:fs/promises';
|
|
8
8
|
import 'node:fs';
|
|
@@ -12,14 +12,14 @@ import 'node:crypto';
|
|
|
12
12
|
import 'tweetnacl';
|
|
13
13
|
import 'fs';
|
|
14
14
|
import 'axios';
|
|
15
|
-
import './auth-
|
|
15
|
+
import './auth-DxJ3VvrX.mjs';
|
|
16
16
|
import 'qrcode-terminal';
|
|
17
17
|
import './time-DgsBLJA_.mjs';
|
|
18
|
-
import './browser-
|
|
18
|
+
import './browser-AoVGRX7y.mjs';
|
|
19
19
|
import 'open';
|
|
20
20
|
import 'react';
|
|
21
21
|
import 'ink';
|
|
22
|
-
import './sessionKeyCache-
|
|
22
|
+
import './sessionKeyCache-B6cGadYL.mjs';
|
|
23
23
|
|
|
24
24
|
function printTaskHelp() {
|
|
25
25
|
console.log(`
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var logger = require('./persistence-
|
|
3
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
4
4
|
var fs = require('fs');
|
|
5
5
|
var child_process = require('child_process');
|
|
6
6
|
var path = require('path');
|
|
7
7
|
var os = require('os');
|
|
8
8
|
var systemdScope = require('./systemdScope-DW3-UoKB.cjs');
|
|
9
|
-
var controlClient = require('./controlClient-
|
|
9
|
+
var controlClient = require('./controlClient-B23M-WPp.cjs');
|
|
10
10
|
require('node:fs/promises');
|
|
11
11
|
require('node:fs');
|
|
12
12
|
require('node:os');
|