@zonease/happy 0.6.39 → 0.6.41

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 (78) hide show
  1. package/bin/happy.mjs +10 -0
  2. package/dist/{api-CIZKyfSn.mjs → api-K2-jr9ar.mjs} +86 -6
  3. package/dist/{api-CwOCQeLH.cjs → api-XeHlmApX.cjs} +87 -7
  4. package/dist/{auth-BIleVCw0.cjs → auth-BQvEuSs5.cjs} +2 -2
  5. package/dist/{auth-Cas9DR3b.cjs → auth-BbvPcnkY.cjs} +4 -4
  6. package/dist/{auth-0VnlXT9f.mjs → auth-CjLkoM3m.mjs} +4 -4
  7. package/dist/{auth-BE8dD20T.mjs → auth-DS8fzlin.mjs} +2 -2
  8. package/dist/{browser-Dll5hxOV.mjs → browser-C05Ak6RL.mjs} +1 -1
  9. package/dist/{browser-Cc08m7of.cjs → browser-C05SP_Tt.cjs} +1 -1
  10. package/dist/{claudeLocal-WkgLbl1a.cjs → claudeLocal-CpDhLCcZ.cjs} +1 -1
  11. package/dist/{claudeLocal-CPT3zcIU.mjs → claudeLocal-DJNRlHIC.mjs} +1 -1
  12. package/dist/{config-Draa-5XC.cjs → config-B0mns9fF.cjs} +1 -1
  13. package/dist/{config-rTT4qTU5.mjs → config-DWWtjKQx.mjs} +1 -1
  14. package/dist/{connect-CJZ73syK.cjs → connect-BxCScVNX.cjs} +4 -4
  15. package/dist/{connect-BfC1tYP_.mjs → connect-DXQC1CJe.mjs} +4 -4
  16. package/dist/{controlClient-DZMIsKsT.cjs → controlClient-DgELDZgM.cjs} +10 -1
  17. package/dist/{controlClient-CM_QcJnB.mjs → controlClient-DgjVRN8D.mjs} +10 -2
  18. package/dist/deviceKeys-BpIAWz13.cjs +182 -0
  19. package/dist/deviceKeys-CejCdWpw.mjs +175 -0
  20. package/dist/{doctor-DZYB6IVc.mjs → doctor-B-LCMVN9.mjs} +3 -3
  21. package/dist/{doctor-Cya9YSA0.cjs → doctor-DznasgP3.cjs} +3 -3
  22. package/dist/{downloadImage-MQMrUUxS.cjs → downloadImage-B_ygLAXY.cjs} +5 -5
  23. package/dist/{downloadImage-DfSFLb-h.mjs → downloadImage-ia5SzlCV.mjs} +3 -3
  24. package/dist/{enroll-BxlbVftP.cjs → enroll-BvfpRUGJ.cjs} +3 -3
  25. package/dist/{enroll-C52x8oly.mjs → enroll-HjUqyGnD.mjs} +3 -3
  26. package/dist/{evidence-Ccd9iYcj.mjs → evidence-BEQd1dCh.mjs} +1 -1
  27. package/dist/{evidence-CjWBG5rX.cjs → evidence-CVgTjBrq.cjs} +1 -1
  28. package/dist/{formatImageMessage-DbyG8c2j.cjs → formatImageMessage-CmJTP_hU.cjs} +1 -1
  29. package/dist/{formatImageMessage-B4lSxlk2.mjs → formatImageMessage-DVs57t3x.mjs} +1 -1
  30. package/dist/index.cjs +58 -43
  31. package/dist/index.mjs +58 -43
  32. package/dist/{install-C_cp8b2-.cjs → install-2Yl_L582.cjs} +2 -2
  33. package/dist/{install-oRAjVLjP.mjs → install-B3mMGISS.mjs} +1 -1
  34. package/dist/{install-BokHRgFW.cjs → install-Ck22tYHP.cjs} +1 -1
  35. package/dist/{install-DAeCn8ae.mjs → install-UTz_vYvB.mjs} +2 -2
  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-4EvraaNY.mjs} +3 -3
  41. package/dist/{loadableToolOutput-DDCJjywt.cjs → loadableToolOutput-BZYlaqHn.cjs} +3 -3
  42. package/dist/{persistence-BFjvtUad.mjs → persistence-CIUFyOwz.mjs} +2 -2
  43. package/dist/{persistence-JLUn_Nou.cjs → persistence-DrfN7TzY.cjs} +1 -1
  44. package/dist/push-CCOXCcM_.cjs +80 -0
  45. package/dist/push-USXj_pv0.mjs +78 -0
  46. package/dist/{run-BDn2aE3Q.cjs → run-D3CopmEb.cjs} +45 -9
  47. package/dist/{run-DBZqN6Kn.mjs → run-Dy6KX6m-.mjs} +45 -9
  48. package/dist/{runClaude-DNHvERX0.mjs → runClaude-CWux2RDq.mjs} +14 -14
  49. package/dist/{runClaude-uaKxPZPm.cjs → runClaude-aHzvx6Ha.cjs} +15 -15
  50. package/dist/{runCodex-C-RTM0wA.mjs → runCodex-D0ZxSj3O.mjs} +13 -13
  51. package/dist/{runCodex-82UrZLme.cjs → runCodex-G-Aq531U.cjs} +13 -13
  52. package/dist/{runGemini-Bm6-4Xm2.mjs → runGemini-DZC2i6vA.mjs} +15 -15
  53. package/dist/{runGemini-CXMr4Jwb.cjs → runGemini-DxGgDAIM.cjs} +15 -15
  54. package/dist/{runOneShot-1B6RMYT7.cjs → runOneShot-BgT-JNMA.cjs} +2 -2
  55. package/dist/{runOneShot-ioARRHrx.mjs → runOneShot-cxlk7VKA.mjs} +2 -2
  56. package/dist/{serve-CwibwU6N.mjs → serve-CKyvJNs0.mjs} +6 -6
  57. package/dist/{serve-BtdAhkIE.cjs → serve-Dr3wPrtK.cjs} +6 -6
  58. package/dist/{session-D--F87zL.cjs → session-dYzbOo_W.cjs} +5 -5
  59. package/dist/{session-2sU4Bu1R.mjs → session-vf2ZjFto.mjs} +5 -5
  60. package/dist/{sessionKeyCache-D2BwO91C.mjs → sessionKeyCache-D2u3JUys.mjs} +1 -1
  61. package/dist/{sessionKeyCache-4Tp8pJ3N.cjs → sessionKeyCache-dDquDzn0.cjs} +1 -1
  62. package/dist/{sessionList-BCSw3qYt.mjs → sessionList-C1k2PNg_.mjs} +2 -2
  63. package/dist/{sessionList-ICJsjNtJ.cjs → sessionList-DmmtC7uR.cjs} +2 -2
  64. package/dist/{sessionSummary-lHGBWQSb.mjs → sessionSummary-5a1QtFVS.mjs} +3 -3
  65. package/dist/{sessionSummary-Bkz52BPh.cjs → sessionSummary-DGLoVOxd.cjs} +3 -3
  66. package/dist/{spawnHappyCLI-D_V-AAoP.cjs → spawnHappyCLI-CarRI4GM.cjs} +1 -1
  67. package/dist/{spawnHappyCLI-CoORX0YW.mjs → spawnHappyCLI-D3EkTFIn.mjs} +1 -1
  68. package/dist/{ssh-DL_AkRAD.mjs → ssh-B0hvEYIl.mjs} +137 -103
  69. package/dist/{ssh-BTOlhhxi.cjs → ssh-CBgVwKsB.cjs} +142 -106
  70. package/dist/{task-CSpaC9HX.cjs → task-CMunPKCV.cjs} +6 -6
  71. package/dist/{task-CQzrHRZe.mjs → task-CRo6W-3_.mjs} +6 -6
  72. package/dist/{uninstall-nFMFGUVM.mjs → uninstall-BALE2up0.mjs} +2 -2
  73. package/dist/{uninstall-BiKyRAb1.cjs → uninstall-BJ3eEGM4.cjs} +2 -2
  74. package/dist/{update-Dw8pgsAT.cjs → update-Bb3iijy8.cjs} +3 -3
  75. package/dist/{update-BcqRHXf2.mjs → update-BfjtLrhO.mjs} +3 -3
  76. package/package.json +1 -1
  77. package/dist/deviceExec-CU1R9CRb.cjs +0 -84
  78. package/dist/deviceExec-D93AOVV4.mjs +0 -81
@@ -3,7 +3,7 @@
3
3
  var child_process = require('child_process');
4
4
  var path = require('node:path');
5
5
  var projectPath = require('./projectPath-ChLhdOyt.cjs');
6
- var logger = require('./persistence-JLUn_Nou.cjs');
6
+ var logger = require('./persistence-DrfN7TzY.cjs');
7
7
  var fs = require('node:fs');
8
8
 
9
9
  let cachedRuntime = null;
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { join } from 'node:path';
3
3
  import { p as projectPath } from './projectPath-BTBIMB57.mjs';
4
- import { l as logger } from './persistence-BFjvtUad.mjs';
4
+ import { l as logger } from './persistence-CIUFyOwz.mjs';
5
5
  import { existsSync } from 'node:fs';
6
6
 
7
7
  let cachedRuntime = null;
@@ -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, g as decrypt, h as decodeBase64, e as encodeBase64, f as encrypt } from './persistence-CIUFyOwz.mjs';
3
+ import { e as ensureDeviceKey, l as listDevices, r as readCachedDeviceNames, a as readCachedDeviceKey, p as pruneDeviceAliases, d as deviceExec } from './deviceKeys-CejCdWpw.mjs';
4
+ import { execOnDeviceViaDaemon } from './controlClient-DgjVRN8D.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-DS8fzlin.mjs';
15
16
  import 'qrcode-terminal';
16
17
  import './time-DgsBLJA_.mjs';
17
- import './browser-Dll5hxOV.mjs';
18
+ import './browser-C05Ak6RL.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,134 @@ 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 resolveDeviceForCommand(credentials, query) {
91
+ return await resolveFromCache(query) ?? await resolveFromServer(credentials, query);
92
+ }
93
+ async function runOnDevice(credentials, query, command, options = {}) {
94
+ const cached = await resolveFromCache(query);
95
+ const resolved = cached ?? await resolveFromServer(credentials, query);
96
+ if (!resolved) return 1;
97
+ if (options.stream) {
98
+ return await streamOnResolved(credentials, resolved, command, options);
99
+ }
100
+ const viaDaemon = await execOnDeviceViaDaemon(
101
+ resolved.device.id,
102
+ command,
103
+ options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS
104
+ ).catch(() => null);
105
+ if (viaDaemon) {
106
+ return reportResult(viaDaemon);
107
+ }
108
+ try {
109
+ return await execOnResolved(credentials, resolved, command, options);
110
+ } catch (error) {
111
+ if (!cached) throw error;
112
+ const fresh = await resolveFromServer(credentials, query);
113
+ if (!fresh) return 1;
114
+ return await execOnResolved(credentials, fresh, command, options);
115
+ }
116
+ }
117
+ function reportResult(result) {
118
+ if (result.stdout) process.stdout.write(result.stdout);
119
+ if (result.stderr) process.stderr.write(result.stderr);
120
+ if (result.error && !result.error.startsWith("Command failed:")) {
121
+ console.error(result.error);
122
+ }
123
+ return result.exitCode ?? (result.success ? 0 : 1);
124
+ }
125
+ async function streamOnResolved(credentials, { device, key, variant }, command, options) {
126
+ const socket = io(configuration.serverUrl, {
127
+ auth: { token: credentials.token, clientType: "user-scoped" },
128
+ path: "/v1/updates",
129
+ transports: ["websocket"]
130
+ });
131
+ await new Promise((resolve, reject) => {
132
+ socket.once("connect", () => resolve());
133
+ socket.once("connect_error", reject);
134
+ });
135
+ const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
136
+ try {
137
+ const answer = await socket.timeout(3e4).emitWithAck("rpc-call", {
138
+ method: `${device.id}:pty-start`,
139
+ params: encodeBase64(encrypt(key, variant, {
140
+ cols: process.stdout.columns || 80,
141
+ rows: process.stdout.rows || 24,
142
+ exec: { command, timeoutMs }
143
+ }))
144
+ });
145
+ if (!answer?.ok) throw new Error(answer?.error || "pty-start failed");
146
+ const started = decrypt(key, variant, decodeBase64(answer.result));
147
+ if (!started?.ok || !started.ptyId) {
148
+ throw new Error(started?.error ?? "Device refused to start the command");
149
+ }
150
+ return await new Promise((resolve) => {
151
+ const execId = started.ptyId;
152
+ const guard = setTimeout(() => resolve(1), timeoutMs + 3e4);
153
+ socket.on("pty-output", (envelope) => {
154
+ if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
155
+ try {
156
+ const frame = decrypt(key, variant, decodeBase64(envelope.data));
157
+ if (typeof frame?.chunk !== "string") return;
158
+ const target = frame.stream === "stderr" ? process.stderr : process.stdout;
159
+ target.write(Buffer.from(frame.chunk, "base64"));
160
+ } catch {
161
+ }
162
+ });
163
+ socket.on("pty-exit", (envelope) => {
164
+ if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
165
+ clearTimeout(guard);
166
+ resolve(typeof envelope.exitCode === "number" ? envelope.exitCode : 1);
167
+ });
168
+ socket.on("disconnect", () => {
169
+ clearTimeout(guard);
170
+ resolve(1);
171
+ });
172
+ });
173
+ } finally {
174
+ socket.close();
175
+ }
176
+ }
177
+ async function execOnResolved(credentials, { device, key, variant }, command, options) {
139
178
  const socket = io(configuration.serverUrl, {
140
179
  auth: { token: credentials.token, clientType: "user-scoped" },
141
180
  path: "/v1/updates",
@@ -153,12 +192,7 @@ async function runOnDevice(credentials, query, command, options = {}) {
153
192
  // matchDevice already read the listing and rejected offline devices.
154
193
  skipMachineLookup: true
155
194
  });
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);
195
+ return reportResult(result);
162
196
  } finally {
163
197
  socket.close();
164
198
  }
@@ -325,4 +359,4 @@ async function sshPickAndConnect(credentials) {
325
359
  return await sshDevice(credentials, chosen.id);
326
360
  }
327
361
 
328
- export { parseSshArgs, runOnDevice, sshDevice, sshPickAndConnect };
362
+ export { parseSshArgs, pickCachedDeviceId, resolveDeviceForCommand, 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-DrfN7TzY.cjs');
5
+ var deviceKeys = require('./deviceKeys-BpIAWz13.cjs');
6
+ var controlClient = require('./controlClient-DgELDZgM.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-BQvEuSs5.cjs');
17
18
  require('qrcode-terminal');
18
19
  require('./time-Bj2HRkKH.cjs');
19
- require('./browser-Cc08m7of.cjs');
20
+ require('./browser-C05SP_Tt.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,134 @@ 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 resolveDeviceForCommand(credentials, query) {
93
+ return await resolveFromCache(query) ?? await resolveFromServer(credentials, query);
94
+ }
95
+ async function runOnDevice(credentials, query, command, options = {}) {
96
+ const cached = await resolveFromCache(query);
97
+ const resolved = cached ?? await resolveFromServer(credentials, query);
98
+ if (!resolved) return 1;
99
+ if (options.stream) {
100
+ return await streamOnResolved(credentials, resolved, command, options);
101
+ }
102
+ const viaDaemon = await controlClient.execOnDeviceViaDaemon(
103
+ resolved.device.id,
104
+ command,
105
+ options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS
106
+ ).catch(() => null);
107
+ if (viaDaemon) {
108
+ return reportResult(viaDaemon);
109
+ }
110
+ try {
111
+ return await execOnResolved(credentials, resolved, command, options);
112
+ } catch (error) {
113
+ if (!cached) throw error;
114
+ const fresh = await resolveFromServer(credentials, query);
115
+ if (!fresh) return 1;
116
+ return await execOnResolved(credentials, fresh, command, options);
117
+ }
118
+ }
119
+ function reportResult(result) {
120
+ if (result.stdout) process.stdout.write(result.stdout);
121
+ if (result.stderr) process.stderr.write(result.stderr);
122
+ if (result.error && !result.error.startsWith("Command failed:")) {
123
+ console.error(result.error);
124
+ }
125
+ return result.exitCode ?? (result.success ? 0 : 1);
126
+ }
127
+ async function streamOnResolved(credentials, { device, key, variant }, command, options) {
128
+ const socket = socket_ioClient.io(logger.configuration.serverUrl, {
129
+ auth: { token: credentials.token, clientType: "user-scoped" },
130
+ path: "/v1/updates",
131
+ transports: ["websocket"]
132
+ });
133
+ await new Promise((resolve, reject) => {
134
+ socket.once("connect", () => resolve());
135
+ socket.once("connect_error", reject);
136
+ });
137
+ const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
138
+ try {
139
+ const answer = await socket.timeout(3e4).emitWithAck("rpc-call", {
140
+ method: `${device.id}:pty-start`,
141
+ params: logger.encodeBase64(logger.encrypt(key, variant, {
142
+ cols: process.stdout.columns || 80,
143
+ rows: process.stdout.rows || 24,
144
+ exec: { command, timeoutMs }
145
+ }))
146
+ });
147
+ if (!answer?.ok) throw new Error(answer?.error || "pty-start failed");
148
+ const started = logger.decrypt(key, variant, logger.decodeBase64(answer.result));
149
+ if (!started?.ok || !started.ptyId) {
150
+ throw new Error(started?.error ?? "Device refused to start the command");
151
+ }
152
+ return await new Promise((resolve) => {
153
+ const execId = started.ptyId;
154
+ const guard = setTimeout(() => resolve(1), timeoutMs + 3e4);
155
+ socket.on("pty-output", (envelope) => {
156
+ if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
157
+ try {
158
+ const frame = logger.decrypt(key, variant, logger.decodeBase64(envelope.data));
159
+ if (typeof frame?.chunk !== "string") return;
160
+ const target = frame.stream === "stderr" ? process.stderr : process.stdout;
161
+ target.write(Buffer.from(frame.chunk, "base64"));
162
+ } catch {
163
+ }
164
+ });
165
+ socket.on("pty-exit", (envelope) => {
166
+ if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
167
+ clearTimeout(guard);
168
+ resolve(typeof envelope.exitCode === "number" ? envelope.exitCode : 1);
169
+ });
170
+ socket.on("disconnect", () => {
171
+ clearTimeout(guard);
172
+ resolve(1);
173
+ });
174
+ });
175
+ } finally {
176
+ socket.close();
177
+ }
178
+ }
179
+ async function execOnResolved(credentials, { device, key, variant }, command, options) {
141
180
  const socket = socket_ioClient.io(logger.configuration.serverUrl, {
142
181
  auth: { token: credentials.token, clientType: "user-scoped" },
143
182
  path: "/v1/updates",
@@ -148,19 +187,14 @@ async function runOnDevice(credentials, query, command, options = {}) {
148
187
  socket.once("connect_error", reject);
149
188
  });
150
189
  try {
151
- const result = await deviceExec.deviceExec(socket, credentials, device.id, command, {
190
+ const result = await deviceKeys.deviceExec(socket, credentials, device.id, command, {
152
191
  deviceKeyBase64: logger.encodeBase64(key),
153
192
  deviceKeyVariant: variant,
154
193
  timeout: options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
155
194
  // matchDevice already read the listing and rejected offline devices.
156
195
  skipMachineLookup: true
157
196
  });
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);
197
+ return reportResult(result);
164
198
  } finally {
165
199
  socket.close();
166
200
  }
@@ -176,7 +210,7 @@ async function sshDevice(credentials, query) {
176
210
  console.error(`Device "${device.name}" is offline.`);
177
211
  return 1;
178
212
  }
179
- const { key, variant } = await ensureDeviceKey(credentials, device.id, () => {
213
+ const { key, variant } = await deviceKeys.ensureDeviceKey(credentials, device.id, () => {
180
214
  console.log("Waiting for approval in the Happy app (Devices \u2192 pending request)...");
181
215
  });
182
216
  const socket = socket_ioClient.io(logger.configuration.serverUrl, {
@@ -300,7 +334,7 @@ function pickDevice(devices) {
300
334
  });
301
335
  }
302
336
  async function listDevicesWithNames(credentials) {
303
- const [devices, names] = await Promise.all([deviceExec.listDevices(credentials), readCachedDeviceNames()]);
337
+ const [devices, names] = await Promise.all([deviceKeys.listDevices(credentials), deviceKeys.readCachedDeviceNames()]);
304
338
  let named = false;
305
339
  const withNames = devices.map((device) => {
306
340
  const cached = names[device.id];
@@ -328,6 +362,8 @@ async function sshPickAndConnect(credentials) {
328
362
  }
329
363
 
330
364
  exports.parseSshArgs = parseSshArgs;
365
+ exports.pickCachedDeviceId = pickCachedDeviceId;
366
+ exports.resolveDeviceForCommand = resolveDeviceForCommand;
331
367
  exports.runOnDevice = runOnDevice;
332
368
  exports.sshDevice = sshDevice;
333
369
  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-DrfN7TzY.cjs');
6
+ var sessionList = require('./sessionList-DmmtC7uR.cjs');
7
+ var sessionSummary = require('./sessionSummary-DGLoVOxd.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-BQvEuSs5.cjs');
18
18
  require('qrcode-terminal');
19
19
  require('./time-Bj2HRkKH.cjs');
20
- require('./browser-Cc08m7of.cjs');
20
+ require('./browser-C05SP_Tt.cjs');
21
21
  require('open');
22
22
  require('react');
23
23
  require('ink');
24
- require('./sessionKeyCache-4Tp8pJ3N.cjs');
24
+ require('./sessionKeyCache-dDquDzn0.cjs');
25
25
 
26
26
  function printTaskHelp() {
27
27
  console.log(`