@zonease/happy 0.6.38 → 0.6.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/happy.mjs +10 -0
- package/dist/{types-DvTsFaqR.cjs → api-C7FyJfZ1.cjs} +400 -1964
- package/dist/{types-BWpRU1VN.mjs → api-D6WL40-U.mjs} +220 -1729
- package/dist/auth-C7ZScWEw.cjs +274 -0
- package/dist/auth-DCsIoZLT.cjs +183 -0
- package/dist/auth-DO9GexAR.mjs +181 -0
- package/dist/auth-DxJ3VvrX.mjs +270 -0
- package/dist/browser-AoVGRX7y.mjs +20 -0
- package/dist/browser-DzgO4oaD.cjs +22 -0
- package/dist/claudeLocal-C6BdzhDx.cjs +352 -0
- package/dist/claudeLocal-CWNGpJ0-.mjs +344 -0
- package/dist/codex/happyMcpStdioBridge.cjs +4 -1
- package/dist/codex/happyMcpStdioBridge.mjs +4 -1
- package/dist/common-BdNgghUJ.mjs +75 -0
- package/dist/common-CZvSRU4p.cjs +83 -0
- package/dist/{config-0Xnc8ARM.cjs → config-CclB3QW9.cjs} +21 -34
- package/dist/{config-_GW4YBCP.mjs → config-DZk-mJ0K.mjs} +7 -20
- package/dist/connect-BXLUKJ5P.mjs +679 -0
- package/dist/connect-k9CqDJ-T.cjs +681 -0
- package/dist/{constants-B4x1K7by.cjs → constants-E_UWYTo3.cjs} +2 -2
- package/dist/{constants-BO9OAFOT.mjs → constants-pmXZGibX.mjs} +1 -1
- package/dist/controlClient-B23M-WPp.cjs +177 -0
- package/dist/controlClient-DB5tDU7V.mjs +167 -0
- package/dist/deviceKeys-BTdo34Uz.mjs +175 -0
- package/dist/deviceKeys-C540DcXl.cjs +182 -0
- package/dist/diagnose-B3D1a_v0.cjs +60 -0
- package/dist/diagnose-CuRKXxt1.mjs +58 -0
- package/dist/doctor-BQGLhrcI.cjs +214 -0
- package/dist/doctor-C08m5Z9f.cjs +78 -0
- package/dist/doctor-CU4JLGQ1.mjs +211 -0
- package/dist/doctor-ON7pN5G5.mjs +74 -0
- package/dist/downloadImage-DZXI3mXL.mjs +887 -0
- package/dist/downloadImage-cQ1Z7hkC.cjs +897 -0
- package/dist/enroll-B55BQR56.mjs +113 -0
- package/dist/enroll-Bo-CNio1.cjs +115 -0
- package/dist/evidence-DQp8QWcv.cjs +129 -0
- package/dist/evidence-DTYRAaOZ.mjs +127 -0
- package/dist/formatImageMessage-CixDYY6G.cjs +46 -0
- package/dist/formatImageMessage-DbKSSjMx.mjs +43 -0
- package/dist/github-DKcyF1zp.mjs +57 -0
- package/dist/github-h9GhJJ_C.cjs +59 -0
- package/dist/index-CDTNgfUf.cjs +578 -0
- package/dist/index-CPKdvi7J.mjs +554 -0
- package/dist/index.cjs +812 -40
- package/dist/index.mjs +813 -40
- package/dist/install-B1e6rdy7.mjs +94 -0
- package/dist/install-CgUPGIoF.mjs +95 -0
- package/dist/install-D7Ir7ciZ.cjs +96 -0
- package/dist/install-DSDufaPd.cjs +98 -0
- package/dist/lib.cjs +18 -12
- package/dist/lib.d.cts +77 -0
- package/dist/lib.d.mts +77 -0
- package/dist/lib.mjs +13 -7
- package/dist/{loadableToolOutput-va3RLplh.mjs → loadableToolOutput-Bfei9wPq.mjs} +4 -2
- package/dist/{loadableToolOutput-5vdQB98M.cjs → loadableToolOutput-CbLGtvST.cjs} +63 -61
- package/dist/mcpToolSchemas-CjeTfrAs.mjs +62 -0
- package/dist/mcpToolSchemas-DK42yoZ_.cjs +64 -0
- package/dist/persistence-CBTtC_dm.cjs +996 -0
- package/dist/persistence-DKfF81pd.mjs +944 -0
- package/dist/projectPath-BTBIMB57.mjs +10 -0
- package/dist/projectPath-ChLhdOyt.cjs +13 -0
- package/dist/prompt-BTWDZZO0.cjs +25 -0
- package/dist/prompt-BsAZsPbo.mjs +22 -0
- package/dist/release-BMEzNONE.mjs +168 -0
- package/dist/release-EhOJeAZe.cjs +171 -0
- package/dist/run-BVZsMNwN.mjs +2164 -0
- package/dist/run-CURx7LhK.cjs +2188 -0
- package/dist/runClaude-6Gi0sYQy.mjs +3763 -0
- package/dist/runClaude-CL3Kt5ex.cjs +3766 -0
- package/dist/{runCodex-DiKzBf9q.mjs → runCodex-ClwEgJ7M.mjs} +36 -20
- package/dist/{runCodex-skq0j05d.cjs → runCodex-YKyVBkJ3.cjs} +169 -153
- package/dist/{runGemini-BeqrVq8R.mjs → runGemini-BPWxVr60.mjs} +34 -17
- package/dist/{runGemini-CVp-CEIh.cjs → runGemini-Xz2W1spn.cjs} +212 -195
- package/dist/{runOneShot-ltCpHbBB.mjs → runOneShot-CdCWiiLr.mjs} +14 -38
- package/dist/{runOneShot-BQNkNW61.cjs → runOneShot-DjZyCvDI.cjs} +31 -55
- package/dist/{serve-BNdoEDVx.mjs → serve-B48JttQ3.mjs} +12 -35
- package/dist/{serve-BECdtivY.cjs → serve-B8vaWQA0.cjs} +35 -58
- package/dist/session-BXboDn2V.mjs +108 -0
- package/dist/session-BZO25zxp.cjs +110 -0
- package/dist/sessionKeyCache-B6cGadYL.mjs +33 -0
- package/dist/sessionKeyCache-CDmexJ9p.cjs +36 -0
- package/dist/sessionList-CgAYZpNj.mjs +51 -0
- package/dist/sessionList-lQ-nqEhc.cjs +54 -0
- package/dist/sessionSummary-CcORiQSq.cjs +393 -0
- package/dist/sessionSummary-DCWQoaDP.mjs +387 -0
- package/dist/spawnHappyCLI-KiazE3kL.cjs +70 -0
- package/dist/spawnHappyCLI-yBIP_Oqi.mjs +66 -0
- package/dist/ssh-q5uZhqBf.mjs +359 -0
- package/dist/ssh-yMllSHu5.cjs +365 -0
- package/dist/systemdScope-BeOtAkhk.mjs +25 -0
- package/dist/systemdScope-DW3-UoKB.cjs +29 -0
- package/dist/task-_nr7J19j.cjs +167 -0
- package/dist/task-wNX1N2i4.mjs +165 -0
- package/dist/time-Bj2HRkKH.cjs +33 -0
- package/dist/time-DgsBLJA_.mjs +29 -0
- package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
- package/dist/trimIdent-BAjmv_TG.mjs +20 -0
- package/dist/types-BoJh4Qfv.mjs +57 -0
- package/dist/types-DWeB7aqh.cjs +59 -0
- package/dist/uninstall-Buaz7d-X.cjs +77 -0
- package/dist/uninstall-Dxf6_4mQ.mjs +75 -0
- package/dist/update-CGlIfa8e.cjs +111 -0
- package/dist/update-CnnoX2cV.mjs +109 -0
- package/dist/workflowCommon-B0d01Y_a.mjs +56 -0
- package/dist/workflowCommon-Bl568-rt.cjs +62 -0
- package/dist/workflowReports-BEWuQesA.cjs +113 -0
- package/dist/workflowReports-Dd8-4-8f.mjs +108 -0
- package/package.json +1 -1
- package/dist/index-BbCDDvoB.cjs +0 -11376
- package/dist/index-vvZXdswy.mjs +0 -11336
- package/dist/mcpToolSchemas-CdPXJQmb.mjs +0 -175
- package/dist/mcpToolSchemas-f-kttjFw.cjs +0 -186
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
4
|
+
var projectPath = require('./projectPath-ChLhdOyt.cjs');
|
|
5
|
+
var fs = require('fs');
|
|
6
|
+
var path = require('path');
|
|
7
|
+
require('node:fs/promises');
|
|
8
|
+
require('node:fs');
|
|
9
|
+
require('node:os');
|
|
10
|
+
require('node:path');
|
|
11
|
+
require('zod');
|
|
12
|
+
require('node:crypto');
|
|
13
|
+
require('tweetnacl');
|
|
14
|
+
require('chalk');
|
|
15
|
+
require('url');
|
|
16
|
+
|
|
17
|
+
async function daemonPost(path, body) {
|
|
18
|
+
const state = await logger.readDaemonState();
|
|
19
|
+
if (!state?.httpPort) {
|
|
20
|
+
const errorMessage = "No daemon running, no state file found";
|
|
21
|
+
logger.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
22
|
+
return {
|
|
23
|
+
error: errorMessage
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
process.kill(state.pid, 0);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const errorMessage = "Daemon is not running, file is stale";
|
|
30
|
+
logger.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
31
|
+
return {
|
|
32
|
+
error: errorMessage
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const timeout = process.env.HAPPY_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPPY_DAEMON_HTTP_TIMEOUT) : 1e4;
|
|
37
|
+
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
body: JSON.stringify(body || {}),
|
|
41
|
+
// Mostly increased for stress test
|
|
42
|
+
signal: AbortSignal.timeout(timeout)
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const errorMessage = `Request failed: ${path}, HTTP ${response.status}`;
|
|
46
|
+
logger.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
47
|
+
return {
|
|
48
|
+
error: errorMessage
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return await response.json();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const errorMessage = `Request failed: ${path}, ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
54
|
+
logger.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
55
|
+
return {
|
|
56
|
+
error: errorMessage
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
61
|
+
return await daemonPost("/session-started", {
|
|
62
|
+
sessionId,
|
|
63
|
+
metadata
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function execOnDeviceViaDaemon(deviceId, command, timeoutMs) {
|
|
67
|
+
const response = await daemonPost("/device-exec", { deviceId, command, timeoutMs });
|
|
68
|
+
if (!response || response.error || response.ok !== true) {
|
|
69
|
+
logger.logger.debug(`[CONTROL CLIENT] Device exec via daemon unavailable: ${response?.error ?? "no daemon"}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return response.result;
|
|
73
|
+
}
|
|
74
|
+
async function listDaemonSessions() {
|
|
75
|
+
const result = await daemonPost("/list");
|
|
76
|
+
return result.children || [];
|
|
77
|
+
}
|
|
78
|
+
async function stopDaemonSession(sessionId) {
|
|
79
|
+
const result = await daemonPost("/stop-session", { sessionId });
|
|
80
|
+
return result.success || false;
|
|
81
|
+
}
|
|
82
|
+
async function stopDaemonHttp() {
|
|
83
|
+
await daemonPost("/stop");
|
|
84
|
+
}
|
|
85
|
+
async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
86
|
+
const state = await logger.readDaemonState();
|
|
87
|
+
if (!state) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
process.kill(state.pid, 0);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
logger.logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
95
|
+
await cleanupDaemonState();
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function isDaemonRunningCurrentlyInstalledHappyVersion() {
|
|
100
|
+
logger.logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
101
|
+
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
102
|
+
if (!runningDaemon) {
|
|
103
|
+
logger.logger.debug("[DAEMON CONTROL] No daemon running, returning false");
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const state = await logger.readDaemonState();
|
|
107
|
+
if (!state) {
|
|
108
|
+
logger.logger.debug("[DAEMON CONTROL] No daemon state found, returning false");
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const packageJsonPath = path.join(projectPath.projectPath(), "package.json");
|
|
113
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
114
|
+
const currentCliVersion = packageJson.version;
|
|
115
|
+
logger.logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
|
116
|
+
return currentCliVersion === state.startedWithCliVersion;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function cleanupDaemonState() {
|
|
123
|
+
try {
|
|
124
|
+
await logger.clearDaemonState();
|
|
125
|
+
logger.logger.debug("[DAEMON RUN] Daemon state file removed");
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logger.logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function stopDaemon() {
|
|
131
|
+
try {
|
|
132
|
+
const state = await logger.readDaemonState();
|
|
133
|
+
if (!state) {
|
|
134
|
+
logger.logger.debug("No daemon state found");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
logger.logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
138
|
+
try {
|
|
139
|
+
await stopDaemonHttp();
|
|
140
|
+
await waitForProcessDeath(state.pid, 2e3);
|
|
141
|
+
logger.logger.debug("Daemon stopped gracefully via HTTP");
|
|
142
|
+
return;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger.logger.debug("HTTP stop failed, will force kill", error);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
process.kill(state.pid, "SIGKILL");
|
|
148
|
+
logger.logger.debug("Force killed daemon");
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.logger.debug("Daemon already dead");
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.logger.debug("Error stopping daemon", error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function waitForProcessDeath(pid, timeout) {
|
|
157
|
+
const start = Date.now();
|
|
158
|
+
while (Date.now() - start < timeout) {
|
|
159
|
+
try {
|
|
160
|
+
process.kill(pid, 0);
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw new Error("Process did not die within timeout");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
exports.checkIfDaemonRunningAndCleanupStaleState = checkIfDaemonRunningAndCleanupStaleState;
|
|
170
|
+
exports.cleanupDaemonState = cleanupDaemonState;
|
|
171
|
+
exports.execOnDeviceViaDaemon = execOnDeviceViaDaemon;
|
|
172
|
+
exports.isDaemonRunningCurrentlyInstalledHappyVersion = isDaemonRunningCurrentlyInstalledHappyVersion;
|
|
173
|
+
exports.listDaemonSessions = listDaemonSessions;
|
|
174
|
+
exports.notifyDaemonSessionStarted = notifyDaemonSessionStarted;
|
|
175
|
+
exports.stopDaemon = stopDaemon;
|
|
176
|
+
exports.stopDaemonHttp = stopDaemonHttp;
|
|
177
|
+
exports.stopDaemonSession = stopDaemonSession;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { l as logger, j as readDaemonState, n as clearDaemonState } from './persistence-DKfF81pd.mjs';
|
|
2
|
+
import { p as projectPath } from './projectPath-BTBIMB57.mjs';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import 'node:fs/promises';
|
|
6
|
+
import 'node:fs';
|
|
7
|
+
import 'node:os';
|
|
8
|
+
import 'node:path';
|
|
9
|
+
import 'zod';
|
|
10
|
+
import 'node:crypto';
|
|
11
|
+
import 'tweetnacl';
|
|
12
|
+
import 'chalk';
|
|
13
|
+
import 'url';
|
|
14
|
+
|
|
15
|
+
async function daemonPost(path, body) {
|
|
16
|
+
const state = await readDaemonState();
|
|
17
|
+
if (!state?.httpPort) {
|
|
18
|
+
const errorMessage = "No daemon running, no state file found";
|
|
19
|
+
logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
20
|
+
return {
|
|
21
|
+
error: errorMessage
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
process.kill(state.pid, 0);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const errorMessage = "Daemon is not running, file is stale";
|
|
28
|
+
logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
29
|
+
return {
|
|
30
|
+
error: errorMessage
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const timeout = process.env.HAPPY_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPPY_DAEMON_HTTP_TIMEOUT) : 1e4;
|
|
35
|
+
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify(body || {}),
|
|
39
|
+
// Mostly increased for stress test
|
|
40
|
+
signal: AbortSignal.timeout(timeout)
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorMessage = `Request failed: ${path}, HTTP ${response.status}`;
|
|
44
|
+
logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
45
|
+
return {
|
|
46
|
+
error: errorMessage
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return await response.json();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const errorMessage = `Request failed: ${path}, ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
52
|
+
logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
53
|
+
return {
|
|
54
|
+
error: errorMessage
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
59
|
+
return await daemonPost("/session-started", {
|
|
60
|
+
sessionId,
|
|
61
|
+
metadata
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function execOnDeviceViaDaemon(deviceId, command, timeoutMs) {
|
|
65
|
+
const response = await daemonPost("/device-exec", { deviceId, command, timeoutMs });
|
|
66
|
+
if (!response || response.error || response.ok !== true) {
|
|
67
|
+
logger.debug(`[CONTROL CLIENT] Device exec via daemon unavailable: ${response?.error ?? "no daemon"}`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return response.result;
|
|
71
|
+
}
|
|
72
|
+
async function listDaemonSessions() {
|
|
73
|
+
const result = await daemonPost("/list");
|
|
74
|
+
return result.children || [];
|
|
75
|
+
}
|
|
76
|
+
async function stopDaemonSession(sessionId) {
|
|
77
|
+
const result = await daemonPost("/stop-session", { sessionId });
|
|
78
|
+
return result.success || false;
|
|
79
|
+
}
|
|
80
|
+
async function stopDaemonHttp() {
|
|
81
|
+
await daemonPost("/stop");
|
|
82
|
+
}
|
|
83
|
+
async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
84
|
+
const state = await readDaemonState();
|
|
85
|
+
if (!state) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
process.kill(state.pid, 0);
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
93
|
+
await cleanupDaemonState();
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function isDaemonRunningCurrentlyInstalledHappyVersion() {
|
|
98
|
+
logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
99
|
+
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
100
|
+
if (!runningDaemon) {
|
|
101
|
+
logger.debug("[DAEMON CONTROL] No daemon running, returning false");
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const state = await readDaemonState();
|
|
105
|
+
if (!state) {
|
|
106
|
+
logger.debug("[DAEMON CONTROL] No daemon state found, returning false");
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const packageJsonPath = join(projectPath(), "package.json");
|
|
111
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
112
|
+
const currentCliVersion = packageJson.version;
|
|
113
|
+
logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
|
114
|
+
return currentCliVersion === state.startedWithCliVersion;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function cleanupDaemonState() {
|
|
121
|
+
try {
|
|
122
|
+
await clearDaemonState();
|
|
123
|
+
logger.debug("[DAEMON RUN] Daemon state file removed");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function stopDaemon() {
|
|
129
|
+
try {
|
|
130
|
+
const state = await readDaemonState();
|
|
131
|
+
if (!state) {
|
|
132
|
+
logger.debug("No daemon state found");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
136
|
+
try {
|
|
137
|
+
await stopDaemonHttp();
|
|
138
|
+
await waitForProcessDeath(state.pid, 2e3);
|
|
139
|
+
logger.debug("Daemon stopped gracefully via HTTP");
|
|
140
|
+
return;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logger.debug("HTTP stop failed, will force kill", error);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
process.kill(state.pid, "SIGKILL");
|
|
146
|
+
logger.debug("Force killed daemon");
|
|
147
|
+
} catch (error) {
|
|
148
|
+
logger.debug("Daemon already dead");
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logger.debug("Error stopping daemon", error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function waitForProcessDeath(pid, timeout) {
|
|
155
|
+
const start = Date.now();
|
|
156
|
+
while (Date.now() - start < timeout) {
|
|
157
|
+
try {
|
|
158
|
+
process.kill(pid, 0);
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
160
|
+
} catch {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw new Error("Process did not die within timeout");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { checkIfDaemonRunningAndCleanupStaleState, cleanupDaemonState, execOnDeviceViaDaemon, isDaemonRunningCurrentlyInstalledHappyVersion, listDaemonSessions, notifyDaemonSessionStarted, stopDaemon, stopDaemonHttp, stopDaemonSession };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { f as decodeBase64, g as encodeBase64, h as encrypt, e as decrypt, c as configuration } from './persistence-DKfF81pd.mjs';
|
|
3
|
+
import { decryptWithEphemeralKey } from './auth-DxJ3VvrX.mjs';
|
|
4
|
+
import tweetnacl from 'tweetnacl';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { writeFile, mkdir, readFile } from 'node:fs/promises';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { hostname } from 'node:os';
|
|
9
|
+
|
|
10
|
+
function resolveMachineKey(credentials, dataEncryptionKey) {
|
|
11
|
+
if (credentials.encryption.type === "legacy") {
|
|
12
|
+
return { key: credentials.encryption.secret, variant: "legacy" };
|
|
13
|
+
}
|
|
14
|
+
if (!dataEncryptionKey) return null;
|
|
15
|
+
const blob = decodeBase64(dataEncryptionKey);
|
|
16
|
+
if (blob.length < 1 || blob[0] !== 0) return null;
|
|
17
|
+
const opened = decryptWithEphemeralKey(blob.slice(1), credentials.encryption.machineKey);
|
|
18
|
+
return opened ? { key: opened, variant: "dataKey" } : null;
|
|
19
|
+
}
|
|
20
|
+
async function fetchMachines(credentials) {
|
|
21
|
+
const response = await axios.get(
|
|
22
|
+
`${configuration.serverUrl}/v1/machines`,
|
|
23
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
24
|
+
);
|
|
25
|
+
const data = response.data;
|
|
26
|
+
return Array.isArray(data) ? data : data.machines ?? [];
|
|
27
|
+
}
|
|
28
|
+
async function listDevices(credentials) {
|
|
29
|
+
const rows = await fetchMachines(credentials);
|
|
30
|
+
return rows.map((row) => {
|
|
31
|
+
const resolved = resolveMachineKey(credentials, row.dataEncryptionKey);
|
|
32
|
+
let metadata = null;
|
|
33
|
+
if (resolved && row.metadata) {
|
|
34
|
+
try {
|
|
35
|
+
metadata = decrypt(resolved.key, resolved.variant, decodeBase64(row.metadata));
|
|
36
|
+
} catch {
|
|
37
|
+
metadata = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
name: row.displayName || metadata?.displayName || metadata?.host || row.id.slice(0, 12),
|
|
43
|
+
description: row.description ?? null,
|
|
44
|
+
platform: metadata?.platform ?? null,
|
|
45
|
+
active: row.active,
|
|
46
|
+
lastActiveAt: row.activeAt ?? row.lastActiveAt ?? 0
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function deviceExec(socket, credentials, deviceId, command, options = {}) {
|
|
51
|
+
const row = options.skipMachineLookup ? null : await (async () => {
|
|
52
|
+
const rows = await fetchMachines(credentials);
|
|
53
|
+
const found = rows.find((candidate) => candidate.id === deviceId);
|
|
54
|
+
if (!found) {
|
|
55
|
+
throw new Error(`Device ${deviceId} not found`);
|
|
56
|
+
}
|
|
57
|
+
if (!found.active) {
|
|
58
|
+
throw new Error(`Device ${deviceId} is offline`);
|
|
59
|
+
}
|
|
60
|
+
return found;
|
|
61
|
+
})();
|
|
62
|
+
const resolved = options.deviceKeyBase64 ? { key: decodeBase64(options.deviceKeyBase64), variant: options.deviceKeyVariant ?? "dataKey" } : resolveMachineKey(credentials, row.dataEncryptionKey);
|
|
63
|
+
if (!resolved) {
|
|
64
|
+
throw new Error(`Cannot resolve encryption key for device ${deviceId}. Re-select the device in the app.`);
|
|
65
|
+
}
|
|
66
|
+
const timeout = options.timeout ?? 6e4;
|
|
67
|
+
const params = encodeBase64(encrypt(resolved.key, resolved.variant, {
|
|
68
|
+
command,
|
|
69
|
+
cwd: options.cwd,
|
|
70
|
+
timeout
|
|
71
|
+
}));
|
|
72
|
+
const answer = await socket.timeout(timeout + 5e3).emitWithAck("rpc-call", {
|
|
73
|
+
method: `${deviceId}:bash`,
|
|
74
|
+
params
|
|
75
|
+
});
|
|
76
|
+
if (!answer?.ok) {
|
|
77
|
+
throw new Error(answer?.error || "Device RPC failed");
|
|
78
|
+
}
|
|
79
|
+
const decrypted = decrypt(resolved.key, resolved.variant, decodeBase64(answer.result));
|
|
80
|
+
if (!decrypted) {
|
|
81
|
+
throw new Error("Failed to decrypt device response");
|
|
82
|
+
}
|
|
83
|
+
return decrypted;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
87
|
+
const POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
88
|
+
function deviceKeysFile() {
|
|
89
|
+
return join(configuration.happyHomeDir, "device-keys.json");
|
|
90
|
+
}
|
|
91
|
+
async function readDeviceKeys() {
|
|
92
|
+
const file = deviceKeysFile();
|
|
93
|
+
if (!existsSync(file)) return {};
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
96
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function writeDeviceDirectory(entries) {
|
|
102
|
+
const file = deviceKeysFile();
|
|
103
|
+
await mkdir(dirname(file), { recursive: true });
|
|
104
|
+
const keys = {};
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
keys[entry.id] = { name: entry.name, key: entry.key, variant: entry.variant ?? "dataKey" };
|
|
107
|
+
}
|
|
108
|
+
await writeFile(file, JSON.stringify(keys, null, 2), { mode: 384 });
|
|
109
|
+
}
|
|
110
|
+
async function pruneDeviceAliases(id, name) {
|
|
111
|
+
const keys = await readDeviceKeys();
|
|
112
|
+
const stale = Object.keys(keys).filter((candidate) => candidate !== id && keys[candidate].name === name);
|
|
113
|
+
if (stale.length === 0) return;
|
|
114
|
+
for (const candidate of stale) delete keys[candidate];
|
|
115
|
+
await writeFile(deviceKeysFile(), JSON.stringify(keys, null, 2), { mode: 384 });
|
|
116
|
+
}
|
|
117
|
+
async function readCachedDeviceKey(machineId) {
|
|
118
|
+
const keys = await readDeviceKeys();
|
|
119
|
+
const stored = keys[machineId];
|
|
120
|
+
if (!stored?.key) return null;
|
|
121
|
+
return { key: decodeBase64(stored.key), variant: stored.variant ?? "dataKey" };
|
|
122
|
+
}
|
|
123
|
+
async function readCachedDeviceNames() {
|
|
124
|
+
const keys = await readDeviceKeys();
|
|
125
|
+
return Object.fromEntries(Object.entries(keys).map(([id, entry]) => [id, entry.name]));
|
|
126
|
+
}
|
|
127
|
+
async function ensureDeviceKey(credentials, machineId, onPending) {
|
|
128
|
+
const cached = await readCachedDeviceKey(machineId);
|
|
129
|
+
if (cached) return cached;
|
|
130
|
+
const seed = tweetnacl.randomBytes(32);
|
|
131
|
+
const secretKey = tweetnacl.hash(seed).slice(0, 32);
|
|
132
|
+
const publicKey = tweetnacl.box.keyPair.fromSecretKey(secretKey).publicKey;
|
|
133
|
+
const created = await axios.post(
|
|
134
|
+
`${configuration.serverUrl}/v1/devices/key-requests`,
|
|
135
|
+
{ machineId, publicKey: encodeBase64(publicKey), label: `happy ssh from ${hostname()}` },
|
|
136
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
137
|
+
);
|
|
138
|
+
onPending?.({ id: created.data.id });
|
|
139
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
140
|
+
while (Date.now() < deadline) {
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
142
|
+
const status = await axios.get(
|
|
143
|
+
`${configuration.serverUrl}/v1/devices/key-requests/${created.data.id}`,
|
|
144
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
145
|
+
);
|
|
146
|
+
if (status.data.expired) {
|
|
147
|
+
throw new Error("Authorization request expired. Run the command again.");
|
|
148
|
+
}
|
|
149
|
+
if (status.data.approved && status.data.response) {
|
|
150
|
+
const opened = decryptWithEphemeralKey(decodeBase64(status.data.response), secretKey);
|
|
151
|
+
if (!opened) {
|
|
152
|
+
throw new Error("Could not open the device directory returned by the app");
|
|
153
|
+
}
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(Buffer.from(opened).toString("utf8"));
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error("Device directory from the app was malformed");
|
|
159
|
+
}
|
|
160
|
+
const devices = parsed.devices ?? [];
|
|
161
|
+
if (devices.length === 0) {
|
|
162
|
+
throw new Error("The app returned an empty device directory");
|
|
163
|
+
}
|
|
164
|
+
await writeDeviceDirectory(devices);
|
|
165
|
+
const target = devices.find((device) => device.id === machineId);
|
|
166
|
+
if (!target) {
|
|
167
|
+
throw new Error("The app did not include this device in the directory");
|
|
168
|
+
}
|
|
169
|
+
return { key: decodeBase64(target.key), variant: target.variant ?? "dataKey" };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw new Error("Timed out waiting for approval in the Happy app");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { readCachedDeviceKey as a, deviceExec as d, ensureDeviceKey as e, listDevices as l, pruneDeviceAliases as p, readCachedDeviceNames as r };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
5
|
+
var auth = require('./auth-C7ZScWEw.cjs');
|
|
6
|
+
var tweetnacl = require('tweetnacl');
|
|
7
|
+
var fs = require('node:fs');
|
|
8
|
+
var promises = require('node:fs/promises');
|
|
9
|
+
var path = require('node:path');
|
|
10
|
+
var os = require('node:os');
|
|
11
|
+
|
|
12
|
+
function resolveMachineKey(credentials, dataEncryptionKey) {
|
|
13
|
+
if (credentials.encryption.type === "legacy") {
|
|
14
|
+
return { key: credentials.encryption.secret, variant: "legacy" };
|
|
15
|
+
}
|
|
16
|
+
if (!dataEncryptionKey) return null;
|
|
17
|
+
const blob = logger.decodeBase64(dataEncryptionKey);
|
|
18
|
+
if (blob.length < 1 || blob[0] !== 0) return null;
|
|
19
|
+
const opened = auth.decryptWithEphemeralKey(blob.slice(1), credentials.encryption.machineKey);
|
|
20
|
+
return opened ? { key: opened, variant: "dataKey" } : null;
|
|
21
|
+
}
|
|
22
|
+
async function fetchMachines(credentials) {
|
|
23
|
+
const response = await axios.get(
|
|
24
|
+
`${logger.configuration.serverUrl}/v1/machines`,
|
|
25
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
26
|
+
);
|
|
27
|
+
const data = response.data;
|
|
28
|
+
return Array.isArray(data) ? data : data.machines ?? [];
|
|
29
|
+
}
|
|
30
|
+
async function listDevices(credentials) {
|
|
31
|
+
const rows = await fetchMachines(credentials);
|
|
32
|
+
return rows.map((row) => {
|
|
33
|
+
const resolved = resolveMachineKey(credentials, row.dataEncryptionKey);
|
|
34
|
+
let metadata = null;
|
|
35
|
+
if (resolved && row.metadata) {
|
|
36
|
+
try {
|
|
37
|
+
metadata = logger.decrypt(resolved.key, resolved.variant, logger.decodeBase64(row.metadata));
|
|
38
|
+
} catch {
|
|
39
|
+
metadata = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
id: row.id,
|
|
44
|
+
name: row.displayName || metadata?.displayName || metadata?.host || row.id.slice(0, 12),
|
|
45
|
+
description: row.description ?? null,
|
|
46
|
+
platform: metadata?.platform ?? null,
|
|
47
|
+
active: row.active,
|
|
48
|
+
lastActiveAt: row.activeAt ?? row.lastActiveAt ?? 0
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function deviceExec(socket, credentials, deviceId, command, options = {}) {
|
|
53
|
+
const row = options.skipMachineLookup ? null : await (async () => {
|
|
54
|
+
const rows = await fetchMachines(credentials);
|
|
55
|
+
const found = rows.find((candidate) => candidate.id === deviceId);
|
|
56
|
+
if (!found) {
|
|
57
|
+
throw new Error(`Device ${deviceId} not found`);
|
|
58
|
+
}
|
|
59
|
+
if (!found.active) {
|
|
60
|
+
throw new Error(`Device ${deviceId} is offline`);
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
})();
|
|
64
|
+
const resolved = options.deviceKeyBase64 ? { key: logger.decodeBase64(options.deviceKeyBase64), variant: options.deviceKeyVariant ?? "dataKey" } : resolveMachineKey(credentials, row.dataEncryptionKey);
|
|
65
|
+
if (!resolved) {
|
|
66
|
+
throw new Error(`Cannot resolve encryption key for device ${deviceId}. Re-select the device in the app.`);
|
|
67
|
+
}
|
|
68
|
+
const timeout = options.timeout ?? 6e4;
|
|
69
|
+
const params = logger.encodeBase64(logger.encrypt(resolved.key, resolved.variant, {
|
|
70
|
+
command,
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
timeout
|
|
73
|
+
}));
|
|
74
|
+
const answer = await socket.timeout(timeout + 5e3).emitWithAck("rpc-call", {
|
|
75
|
+
method: `${deviceId}:bash`,
|
|
76
|
+
params
|
|
77
|
+
});
|
|
78
|
+
if (!answer?.ok) {
|
|
79
|
+
throw new Error(answer?.error || "Device RPC failed");
|
|
80
|
+
}
|
|
81
|
+
const decrypted = logger.decrypt(resolved.key, resolved.variant, logger.decodeBase64(answer.result));
|
|
82
|
+
if (!decrypted) {
|
|
83
|
+
throw new Error("Failed to decrypt device response");
|
|
84
|
+
}
|
|
85
|
+
return decrypted;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
89
|
+
const POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
90
|
+
function deviceKeysFile() {
|
|
91
|
+
return path.join(logger.configuration.happyHomeDir, "device-keys.json");
|
|
92
|
+
}
|
|
93
|
+
async function readDeviceKeys() {
|
|
94
|
+
const file = deviceKeysFile();
|
|
95
|
+
if (!fs.existsSync(file)) return {};
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(await promises.readFile(file, "utf8"));
|
|
98
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
99
|
+
} catch {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function writeDeviceDirectory(entries) {
|
|
104
|
+
const file = deviceKeysFile();
|
|
105
|
+
await promises.mkdir(path.dirname(file), { recursive: true });
|
|
106
|
+
const keys = {};
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
keys[entry.id] = { name: entry.name, key: entry.key, variant: entry.variant ?? "dataKey" };
|
|
109
|
+
}
|
|
110
|
+
await promises.writeFile(file, JSON.stringify(keys, null, 2), { mode: 384 });
|
|
111
|
+
}
|
|
112
|
+
async function pruneDeviceAliases(id, name) {
|
|
113
|
+
const keys = await readDeviceKeys();
|
|
114
|
+
const stale = Object.keys(keys).filter((candidate) => candidate !== id && keys[candidate].name === name);
|
|
115
|
+
if (stale.length === 0) return;
|
|
116
|
+
for (const candidate of stale) delete keys[candidate];
|
|
117
|
+
await promises.writeFile(deviceKeysFile(), JSON.stringify(keys, null, 2), { mode: 384 });
|
|
118
|
+
}
|
|
119
|
+
async function readCachedDeviceKey(machineId) {
|
|
120
|
+
const keys = await readDeviceKeys();
|
|
121
|
+
const stored = keys[machineId];
|
|
122
|
+
if (!stored?.key) return null;
|
|
123
|
+
return { key: logger.decodeBase64(stored.key), variant: stored.variant ?? "dataKey" };
|
|
124
|
+
}
|
|
125
|
+
async function readCachedDeviceNames() {
|
|
126
|
+
const keys = await readDeviceKeys();
|
|
127
|
+
return Object.fromEntries(Object.entries(keys).map(([id, entry]) => [id, entry.name]));
|
|
128
|
+
}
|
|
129
|
+
async function ensureDeviceKey(credentials, machineId, onPending) {
|
|
130
|
+
const cached = await readCachedDeviceKey(machineId);
|
|
131
|
+
if (cached) return cached;
|
|
132
|
+
const seed = tweetnacl.randomBytes(32);
|
|
133
|
+
const secretKey = tweetnacl.hash(seed).slice(0, 32);
|
|
134
|
+
const publicKey = tweetnacl.box.keyPair.fromSecretKey(secretKey).publicKey;
|
|
135
|
+
const created = await axios.post(
|
|
136
|
+
`${logger.configuration.serverUrl}/v1/devices/key-requests`,
|
|
137
|
+
{ machineId, publicKey: logger.encodeBase64(publicKey), label: `happy ssh from ${os.hostname()}` },
|
|
138
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
139
|
+
);
|
|
140
|
+
onPending?.({ id: created.data.id });
|
|
141
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
142
|
+
while (Date.now() < deadline) {
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
144
|
+
const status = await axios.get(
|
|
145
|
+
`${logger.configuration.serverUrl}/v1/devices/key-requests/${created.data.id}`,
|
|
146
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
147
|
+
);
|
|
148
|
+
if (status.data.expired) {
|
|
149
|
+
throw new Error("Authorization request expired. Run the command again.");
|
|
150
|
+
}
|
|
151
|
+
if (status.data.approved && status.data.response) {
|
|
152
|
+
const opened = auth.decryptWithEphemeralKey(logger.decodeBase64(status.data.response), secretKey);
|
|
153
|
+
if (!opened) {
|
|
154
|
+
throw new Error("Could not open the device directory returned by the app");
|
|
155
|
+
}
|
|
156
|
+
let parsed;
|
|
157
|
+
try {
|
|
158
|
+
parsed = JSON.parse(Buffer.from(opened).toString("utf8"));
|
|
159
|
+
} catch {
|
|
160
|
+
throw new Error("Device directory from the app was malformed");
|
|
161
|
+
}
|
|
162
|
+
const devices = parsed.devices ?? [];
|
|
163
|
+
if (devices.length === 0) {
|
|
164
|
+
throw new Error("The app returned an empty device directory");
|
|
165
|
+
}
|
|
166
|
+
await writeDeviceDirectory(devices);
|
|
167
|
+
const target = devices.find((device) => device.id === machineId);
|
|
168
|
+
if (!target) {
|
|
169
|
+
throw new Error("The app did not include this device in the directory");
|
|
170
|
+
}
|
|
171
|
+
return { key: logger.decodeBase64(target.key), variant: target.variant ?? "dataKey" };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
throw new Error("Timed out waiting for approval in the Happy app");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
exports.deviceExec = deviceExec;
|
|
178
|
+
exports.ensureDeviceKey = ensureDeviceKey;
|
|
179
|
+
exports.listDevices = listDevices;
|
|
180
|
+
exports.pruneDeviceAliases = pruneDeviceAliases;
|
|
181
|
+
exports.readCachedDeviceKey = readCachedDeviceKey;
|
|
182
|
+
exports.readCachedDeviceNames = readCachedDeviceNames;
|