@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.
Files changed (76) hide show
  1. package/bin/happy.mjs +10 -0
  2. package/dist/{api-CwOCQeLH.cjs → api-C7FyJfZ1.cjs} +87 -7
  3. package/dist/{api-CIZKyfSn.mjs → api-D6WL40-U.mjs} +86 -6
  4. package/dist/{auth-BIleVCw0.cjs → auth-C7ZScWEw.cjs} +2 -2
  5. package/dist/{auth-Cas9DR3b.cjs → auth-DCsIoZLT.cjs} +4 -4
  6. package/dist/{auth-0VnlXT9f.mjs → auth-DO9GexAR.mjs} +4 -4
  7. package/dist/{auth-BE8dD20T.mjs → auth-DxJ3VvrX.mjs} +2 -2
  8. package/dist/{browser-Dll5hxOV.mjs → browser-AoVGRX7y.mjs} +1 -1
  9. package/dist/{browser-Cc08m7of.cjs → browser-DzgO4oaD.cjs} +1 -1
  10. package/dist/{claudeLocal-WkgLbl1a.cjs → claudeLocal-C6BdzhDx.cjs} +1 -1
  11. package/dist/{claudeLocal-CPT3zcIU.mjs → claudeLocal-CWNGpJ0-.mjs} +1 -1
  12. package/dist/{config-Draa-5XC.cjs → config-CclB3QW9.cjs} +1 -1
  13. package/dist/{config-rTT4qTU5.mjs → config-DZk-mJ0K.mjs} +1 -1
  14. package/dist/{connect-BfC1tYP_.mjs → connect-BXLUKJ5P.mjs} +4 -4
  15. package/dist/{connect-CJZ73syK.cjs → connect-k9CqDJ-T.cjs} +4 -4
  16. package/dist/{controlClient-DZMIsKsT.cjs → controlClient-B23M-WPp.cjs} +10 -1
  17. package/dist/{controlClient-CM_QcJnB.mjs → controlClient-DB5tDU7V.mjs} +10 -2
  18. package/dist/deviceKeys-BTdo34Uz.mjs +175 -0
  19. package/dist/deviceKeys-C540DcXl.cjs +182 -0
  20. package/dist/{doctor-Cya9YSA0.cjs → doctor-BQGLhrcI.cjs} +2 -2
  21. package/dist/{doctor-DZYB6IVc.mjs → doctor-CU4JLGQ1.mjs} +2 -2
  22. package/dist/{downloadImage-DfSFLb-h.mjs → downloadImage-DZXI3mXL.mjs} +3 -3
  23. package/dist/{downloadImage-MQMrUUxS.cjs → downloadImage-cQ1Z7hkC.cjs} +5 -5
  24. package/dist/{enroll-C52x8oly.mjs → enroll-B55BQR56.mjs} +3 -3
  25. package/dist/{enroll-BxlbVftP.cjs → enroll-Bo-CNio1.cjs} +3 -3
  26. package/dist/{evidence-CjWBG5rX.cjs → evidence-DQp8QWcv.cjs} +1 -1
  27. package/dist/{evidence-Ccd9iYcj.mjs → evidence-DTYRAaOZ.mjs} +1 -1
  28. package/dist/{formatImageMessage-DbyG8c2j.cjs → formatImageMessage-CixDYY6G.cjs} +1 -1
  29. package/dist/{formatImageMessage-B4lSxlk2.mjs → formatImageMessage-DbKSSjMx.mjs} +1 -1
  30. package/dist/index.cjs +42 -42
  31. package/dist/index.mjs +42 -42
  32. package/dist/{install-DAeCn8ae.mjs → install-B1e6rdy7.mjs} +2 -2
  33. package/dist/{install-oRAjVLjP.mjs → install-CgUPGIoF.mjs} +1 -1
  34. package/dist/{install-C_cp8b2-.cjs → install-D7Ir7ciZ.cjs} +2 -2
  35. package/dist/{install-BokHRgFW.cjs → install-DSDufaPd.cjs} +1 -1
  36. package/dist/lib.cjs +3 -3
  37. package/dist/lib.d.cts +77 -0
  38. package/dist/lib.d.mts +77 -0
  39. package/dist/lib.mjs +3 -3
  40. package/dist/{loadableToolOutput-Cuygmd4K.mjs → loadableToolOutput-Bfei9wPq.mjs} +3 -3
  41. package/dist/{loadableToolOutput-DDCJjywt.cjs → loadableToolOutput-CbLGtvST.cjs} +3 -3
  42. package/dist/{persistence-JLUn_Nou.cjs → persistence-CBTtC_dm.cjs} +1 -1
  43. package/dist/{persistence-BFjvtUad.mjs → persistence-DKfF81pd.mjs} +2 -2
  44. package/dist/{run-DBZqN6Kn.mjs → run-BVZsMNwN.mjs} +45 -9
  45. package/dist/{run-BDn2aE3Q.cjs → run-CURx7LhK.cjs} +45 -9
  46. package/dist/{runClaude-DNHvERX0.mjs → runClaude-6Gi0sYQy.mjs} +14 -14
  47. package/dist/{runClaude-uaKxPZPm.cjs → runClaude-CL3Kt5ex.cjs} +15 -15
  48. package/dist/{runCodex-C-RTM0wA.mjs → runCodex-ClwEgJ7M.mjs} +13 -13
  49. package/dist/{runCodex-82UrZLme.cjs → runCodex-YKyVBkJ3.cjs} +13 -13
  50. package/dist/{runGemini-Bm6-4Xm2.mjs → runGemini-BPWxVr60.mjs} +15 -15
  51. package/dist/{runGemini-CXMr4Jwb.cjs → runGemini-Xz2W1spn.cjs} +15 -15
  52. package/dist/{runOneShot-ioARRHrx.mjs → runOneShot-CdCWiiLr.mjs} +2 -2
  53. package/dist/{runOneShot-1B6RMYT7.cjs → runOneShot-DjZyCvDI.cjs} +2 -2
  54. package/dist/{serve-CwibwU6N.mjs → serve-B48JttQ3.mjs} +6 -6
  55. package/dist/{serve-BtdAhkIE.cjs → serve-B8vaWQA0.cjs} +6 -6
  56. package/dist/{session-2sU4Bu1R.mjs → session-BXboDn2V.mjs} +5 -5
  57. package/dist/{session-D--F87zL.cjs → session-BZO25zxp.cjs} +5 -5
  58. package/dist/{sessionKeyCache-D2BwO91C.mjs → sessionKeyCache-B6cGadYL.mjs} +1 -1
  59. package/dist/{sessionKeyCache-4Tp8pJ3N.cjs → sessionKeyCache-CDmexJ9p.cjs} +1 -1
  60. package/dist/{sessionList-BCSw3qYt.mjs → sessionList-CgAYZpNj.mjs} +2 -2
  61. package/dist/{sessionList-ICJsjNtJ.cjs → sessionList-lQ-nqEhc.cjs} +2 -2
  62. package/dist/{sessionSummary-Bkz52BPh.cjs → sessionSummary-CcORiQSq.cjs} +3 -3
  63. package/dist/{sessionSummary-lHGBWQSb.mjs → sessionSummary-DCWQoaDP.mjs} +3 -3
  64. package/dist/{spawnHappyCLI-D_V-AAoP.cjs → spawnHappyCLI-KiazE3kL.cjs} +1 -1
  65. package/dist/{spawnHappyCLI-CoORX0YW.mjs → spawnHappyCLI-yBIP_Oqi.mjs} +1 -1
  66. package/dist/{ssh-DL_AkRAD.mjs → ssh-q5uZhqBf.mjs} +134 -103
  67. package/dist/{ssh-BTOlhhxi.cjs → ssh-yMllSHu5.cjs} +138 -106
  68. package/dist/{task-CSpaC9HX.cjs → task-_nr7J19j.cjs} +6 -6
  69. package/dist/{task-CQzrHRZe.mjs → task-wNX1N2i4.mjs} +6 -6
  70. package/dist/{uninstall-BiKyRAb1.cjs → uninstall-Buaz7d-X.cjs} +2 -2
  71. package/dist/{uninstall-nFMFGUVM.mjs → uninstall-Dxf6_4mQ.mjs} +2 -2
  72. package/dist/{update-Dw8pgsAT.cjs → update-CGlIfa8e.cjs} +3 -3
  73. package/dist/{update-BcqRHXf2.mjs → update-CnnoX2cV.mjs} +3 -3
  74. package/package.json +1 -1
  75. package/dist/deviceExec-CU1R9CRb.cjs +0 -84
  76. package/dist/deviceExec-D93AOVV4.mjs +0 -81
@@ -1,105 +1,27 @@
1
1
  import { io } from 'socket.io-client';
2
- import { c as configuration, f as encodeBase64, e as decodeBase64, h as decrypt, g as encrypt } from './persistence-BFjvtUad.mjs';
3
- import { d as deviceExec, l as listDevices } from './deviceExec-D93AOVV4.mjs';
4
- import axios from 'axios';
5
- import tweetnacl from 'tweetnacl';
6
- import { existsSync } from 'node:fs';
7
- import { mkdir, writeFile, readFile } from 'node:fs/promises';
8
- import { dirname, join } from 'node:path';
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-Dll5hxOV.mjs';
18
+ import './browser-AoVGRX7y.mjs';
18
19
  import 'open';
19
20
  import 'react';
20
21
  import 'ink';
21
-
22
- const POLL_INTERVAL_MS = 2e3;
23
- const POLL_TIMEOUT_MS = 10 * 60 * 1e3;
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 runOnDevice(credentials, query, command, options = {}) {
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 1;
78
+ return null;
131
79
  }
132
80
  if (!device.active) {
133
81
  console.error(`Device "${device.name}" is offline.`);
134
- return 1;
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
- if (result.stdout) process.stdout.write(result.stdout);
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-JLUn_Nou.cjs');
5
- var deviceExec = require('./deviceExec-CU1R9CRb.cjs');
6
- var axios = require('axios');
7
- var tweetnacl = require('tweetnacl');
8
- var fs = require('node:fs');
9
- var promises = require('node:fs/promises');
10
- var path = require('node:path');
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-Cc08m7of.cjs');
20
+ require('./browser-DzgO4oaD.cjs');
20
21
  require('open');
21
22
  require('react');
22
23
  require('ink');
23
-
24
- const POLL_INTERVAL_MS = 2e3;
25
- const POLL_TIMEOUT_MS = 10 * 60 * 1e3;
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 runOnDevice(credentials, query, command, options = {}) {
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 1;
80
+ return null;
133
81
  }
134
82
  if (!device.active) {
135
83
  console.error(`Device "${device.name}" is offline.`);
136
- return 1;
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 deviceExec.deviceExec(socket, credentials, device.id, command, {
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
- if (result.stdout) process.stdout.write(result.stdout);
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([deviceExec.listDevices(credentials), readCachedDeviceNames()]);
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-JLUn_Nou.cjs');
6
- var sessionList = require('./sessionList-ICJsjNtJ.cjs');
7
- var sessionSummary = require('./sessionSummary-Bkz52BPh.cjs');
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-BIleVCw0.cjs');
17
+ require('./auth-C7ZScWEw.cjs');
18
18
  require('qrcode-terminal');
19
19
  require('./time-Bj2HRkKH.cjs');
20
- require('./browser-Cc08m7of.cjs');
20
+ require('./browser-DzgO4oaD.cjs');
21
21
  require('open');
22
22
  require('react');
23
23
  require('ink');
24
- require('./sessionKeyCache-4Tp8pJ3N.cjs');
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-BFjvtUad.mjs';
4
- import { r as runSessionList } from './sessionList-BCSw3qYt.mjs';
5
- import { r as runSessionSummary } from './sessionSummary-lHGBWQSb.mjs';
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-BE8dD20T.mjs';
15
+ import './auth-DxJ3VvrX.mjs';
16
16
  import 'qrcode-terminal';
17
17
  import './time-DgsBLJA_.mjs';
18
- import './browser-Dll5hxOV.mjs';
18
+ import './browser-AoVGRX7y.mjs';
19
19
  import 'open';
20
20
  import 'react';
21
21
  import 'ink';
22
- import './sessionKeyCache-D2BwO91C.mjs';
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-JLUn_Nou.cjs');
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-DZMIsKsT.cjs');
9
+ var controlClient = require('./controlClient-B23M-WPp.cjs');
10
10
  require('node:fs/promises');
11
11
  require('node:fs');
12
12
  require('node:os');