@xevy/heny-connect 0.1.0
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/README.md +32 -0
- package/bin/heny-connect.mjs +74 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @xevy/heny-connect
|
|
2
|
+
|
|
3
|
+
Heny Connect pairs a computer with a Heny workspace and keeps the device's availability current.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 20 or newer
|
|
8
|
+
- A six-digit pairing code from the Heny **Desktop** page
|
|
9
|
+
|
|
10
|
+
## Pair a computer
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx @xevy/heny-connect pair --code 123456 --server https://heny.vyte.dev --run
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The pairing code works once and expires after 15 minutes. Pairing state is stored in `.heny-connect.json` in the current user's home directory.
|
|
17
|
+
|
|
18
|
+
## Run an existing pairing
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx @xevy/heny-connect run
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The process sends a heartbeat every 30 seconds. Keep it running for the device to remain available in Heny.
|
|
25
|
+
|
|
26
|
+
## Check status
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @xevy/heny-connect status
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Heny Connect currently provides device registration and availability reporting. Browser and shell execution are planned capabilities.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Heny Connect — pairs this computer with a Heny workspace and keeps it
|
|
4
|
+
* reporting in. Node 20+; no dependencies.
|
|
5
|
+
*
|
|
6
|
+
* npx @xevy/heny-connect pair --code 123456 --server https://heny.example
|
|
7
|
+
* npx @xevy/heny-connect run
|
|
8
|
+
* npx @xevy/heny-connect status
|
|
9
|
+
*
|
|
10
|
+
* Pairing state is stored in ~/.heny-connect.json (mode 600).
|
|
11
|
+
*/
|
|
12
|
+
import { chmod, readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { homedir, hostname, platform, release } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const STATE_FILE = join(process.env.HENY_CONNECT_HOME || homedir(), ".heny-connect.json");
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const command = args[0] || "help";
|
|
19
|
+
const opt = (name, fallback) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : fallback; };
|
|
20
|
+
|
|
21
|
+
function systemLabel() {
|
|
22
|
+
const os = platform();
|
|
23
|
+
if (os === "win32") return `Windows ${release().startsWith("10.0.2") ? "11" : release()}`;
|
|
24
|
+
if (os === "darwin") return `macOS ${release()}`;
|
|
25
|
+
return `${os} ${release()}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function loadState() { try { return JSON.parse(await readFile(STATE_FILE, "utf8")); } catch { return null; } }
|
|
29
|
+
async function saveState(state) { await writeFile(STATE_FILE, JSON.stringify(state, null, 2)); await chmod(STATE_FILE, 0o600).catch(() => undefined); }
|
|
30
|
+
|
|
31
|
+
async function call(server, path, body, token) {
|
|
32
|
+
const res = await fetch(new URL(path, server), { method: "POST", headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify(body), signal: AbortSignal.timeout(20_000) });
|
|
33
|
+
const text = await res.text();
|
|
34
|
+
let parsed; try { parsed = JSON.parse(text); } catch { parsed = { raw: text }; }
|
|
35
|
+
if (!res.ok) throw new Error(parsed?.error?.message || `HTTP ${res.status}`);
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function pair() {
|
|
40
|
+
const code = opt("code"); const server = opt("server", process.env.HENY_SERVER);
|
|
41
|
+
if (!code || !server) { console.error("Usage: heny-connect pair --code 123456 --server https://heny.example"); process.exitCode = 2; return; }
|
|
42
|
+
const result = await call(server, "/api/devices/pair", { code, system: systemLabel(), hostname: hostname(), capabilities: ["browser", "shell"] });
|
|
43
|
+
await saveState({ server, token: result.token, deviceId: result.deviceId, workspace: result.workspace, pairedAt: new Date().toISOString() });
|
|
44
|
+
console.log(`Paired with ${result.workspace?.name ?? "workspace"} as device ${result.deviceId}.`);
|
|
45
|
+
if (!args.includes("--once") && args.includes("--run")) await run();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function heartbeat(state, detail) {
|
|
49
|
+
return call(state.server, "/api/devices/heartbeat", { state: "available", detail }, state.token);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function run() {
|
|
53
|
+
const state = await loadState();
|
|
54
|
+
if (!state) { console.error("Not paired. Run: heny-connect pair --code … --server …"); process.exitCode = 2; return; }
|
|
55
|
+
const every = Number(opt("every", 30)) * 1000;
|
|
56
|
+
const beats = Number(opt("beats", 0)); let count = 0;
|
|
57
|
+
const tick = async () => {
|
|
58
|
+
try { await heartbeat(state, `${hostname()}: browser ready`); count += 1; console.log(`${new Date().toISOString()} heartbeat ok (${count})`); }
|
|
59
|
+
catch (err) { console.error(`${new Date().toISOString()} heartbeat failed: ${err.message}`); if (/Unknown device token/.test(err.message)) process.exit(3); }
|
|
60
|
+
if (beats && count >= beats) process.exit(0);
|
|
61
|
+
};
|
|
62
|
+
await tick();
|
|
63
|
+
if (!beats || count < beats) setInterval(tick, every);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function status() {
|
|
67
|
+
const state = await loadState();
|
|
68
|
+
if (!state) { console.log("Not paired."); return; }
|
|
69
|
+
const res = await fetch(new URL("/api/devices/me", state.server), { headers: { Authorization: `Bearer ${state.token}` }, signal: AbortSignal.timeout(20_000) });
|
|
70
|
+
console.log(res.ok ? JSON.stringify(await res.json(), null, 2) : `Server answered HTTP ${res.status}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const commands = { pair, run, status, help: async () => console.log("Commands: pair --code <6 digits> --server <url> [--run] | run [--every 30] [--beats N] | status") };
|
|
74
|
+
(commands[command] || commands.help)().catch((err) => { console.error(err.message); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xevy/heny-connect",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pair a computer with Heny and keep its device presence online.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"heny-connect": "bin/heny-connect.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test test/*.node-test.mjs"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/xevytech/heny.ai.git",
|
|
25
|
+
"directory": "packages/connect"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://heny.vyte.dev",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/xevytech/heny.ai/issues"
|
|
30
|
+
},
|
|
31
|
+
"license": "UNLICENSED",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"heny",
|
|
34
|
+
"device-agent",
|
|
35
|
+
"desktop-agent"
|
|
36
|
+
]
|
|
37
|
+
}
|