@hera-al/standardnode 1.0.4 → 1.0.6
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/dist/commands/browser-proxy.js +1 -164
- package/dist/commands/shell.js +1 -62
- package/dist/config.js +1 -141
- package/dist/gateway-link.js +1 -315
- package/dist/index.js +1 -169
- package/dist/logger.js +1 -64
- package/package.json +4 -2
|
@@ -1,164 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { extname } from "node:path";
|
|
3
|
-
const TAG = "BrowserProxy";
|
|
4
|
-
const MAX_FILE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
5
|
-
// ---------- MIME detection (extension-based) ----------
|
|
6
|
-
const MIME_MAP = {
|
|
7
|
-
".png": "image/png",
|
|
8
|
-
".jpg": "image/jpeg",
|
|
9
|
-
".jpeg": "image/jpeg",
|
|
10
|
-
".gif": "image/gif",
|
|
11
|
-
".webp": "image/webp",
|
|
12
|
-
".svg": "image/svg+xml",
|
|
13
|
-
".pdf": "application/pdf",
|
|
14
|
-
".html": "text/html",
|
|
15
|
-
".json": "application/json",
|
|
16
|
-
".txt": "text/plain",
|
|
17
|
-
".zip": "application/zip",
|
|
18
|
-
".mp4": "video/mp4",
|
|
19
|
-
".webm": "video/webm",
|
|
20
|
-
};
|
|
21
|
-
function detectMime(filePath) {
|
|
22
|
-
return MIME_MAP[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
23
|
-
}
|
|
24
|
-
// ---------- Helpers ----------
|
|
25
|
-
/** Extract file paths from a browser response (path, imagePath, download.path) */
|
|
26
|
-
function collectFilePaths(payload) {
|
|
27
|
-
const paths = new Set();
|
|
28
|
-
const obj = typeof payload === "object" && payload !== null
|
|
29
|
-
? payload
|
|
30
|
-
: null;
|
|
31
|
-
if (!obj)
|
|
32
|
-
return [];
|
|
33
|
-
if (typeof obj.path === "string" && obj.path.trim()) {
|
|
34
|
-
paths.add(obj.path.trim());
|
|
35
|
-
}
|
|
36
|
-
if (typeof obj.imagePath === "string" && obj.imagePath.trim()) {
|
|
37
|
-
paths.add(obj.imagePath.trim());
|
|
38
|
-
}
|
|
39
|
-
const download = obj.download;
|
|
40
|
-
if (download && typeof download === "object") {
|
|
41
|
-
const dlPath = download.path;
|
|
42
|
-
if (typeof dlPath === "string" && dlPath.trim()) {
|
|
43
|
-
paths.add(dlPath.trim());
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return [...paths];
|
|
47
|
-
}
|
|
48
|
-
/** Read a file from disk, check size, encode as base64 */
|
|
49
|
-
async function readProxyFile(filePath) {
|
|
50
|
-
const st = await stat(filePath).catch(() => null);
|
|
51
|
-
if (!st || !st.isFile())
|
|
52
|
-
return null;
|
|
53
|
-
if (st.size > MAX_FILE_BYTES) {
|
|
54
|
-
throw new Error(`File exceeds ${Math.round(MAX_FILE_BYTES / (1024 * 1024))}MB: ${filePath}`);
|
|
55
|
-
}
|
|
56
|
-
const buffer = await readFile(filePath);
|
|
57
|
-
return {
|
|
58
|
-
path: filePath,
|
|
59
|
-
base64: buffer.toString("base64"),
|
|
60
|
-
mimeType: detectMime(filePath),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
// ---------- Proxy handler ----------
|
|
64
|
-
export function createBrowserProxy(controlPort, allowProfiles, log) {
|
|
65
|
-
const baseUrl = `http://127.0.0.1:${controlPort}`;
|
|
66
|
-
function isProfileAllowed(profile) {
|
|
67
|
-
if (allowProfiles.length === 0)
|
|
68
|
-
return true;
|
|
69
|
-
if (!profile)
|
|
70
|
-
return false;
|
|
71
|
-
return allowProfiles.includes(profile.trim());
|
|
72
|
-
}
|
|
73
|
-
async function handleProxy(params) {
|
|
74
|
-
const pathValue = typeof params.path === "string" ? params.path.trim() : "";
|
|
75
|
-
if (!pathValue) {
|
|
76
|
-
throw new Error("INVALID_REQUEST: path required");
|
|
77
|
-
}
|
|
78
|
-
const method = (typeof params.method === "string" ? params.method.toUpperCase() : "GET");
|
|
79
|
-
const path = pathValue.startsWith("/") ? pathValue : `/${pathValue}`;
|
|
80
|
-
const requestedProfile = typeof params.profile === "string" ? params.profile.trim() : "";
|
|
81
|
-
// Profile allowlist check
|
|
82
|
-
if (allowProfiles.length > 0 && path !== "/profiles") {
|
|
83
|
-
const profileToCheck = requestedProfile || "default";
|
|
84
|
-
if (!isProfileAllowed(profileToCheck)) {
|
|
85
|
-
throw new Error("INVALID_REQUEST: browser profile not allowed");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Build URL with query params
|
|
89
|
-
const url = new URL(path, baseUrl);
|
|
90
|
-
if (requestedProfile) {
|
|
91
|
-
url.searchParams.set("profile", requestedProfile);
|
|
92
|
-
}
|
|
93
|
-
const rawQuery = params.query ?? {};
|
|
94
|
-
for (const [key, value] of Object.entries(rawQuery)) {
|
|
95
|
-
if (value === undefined || value === null)
|
|
96
|
-
continue;
|
|
97
|
-
url.searchParams.set(key, String(value));
|
|
98
|
-
}
|
|
99
|
-
log.info(TAG, `proxy: ${method} ${url.pathname}${url.search}`);
|
|
100
|
-
// HTTP fetch to local browser server
|
|
101
|
-
const fetchOpts = { method };
|
|
102
|
-
if (params.body !== undefined && method !== "GET") {
|
|
103
|
-
fetchOpts.headers = { "Content-Type": "application/json" };
|
|
104
|
-
fetchOpts.body = JSON.stringify(params.body);
|
|
105
|
-
}
|
|
106
|
-
const timeoutMs = Math.max(1000, Math.min(120_000, params.timeoutMs ?? 30_000));
|
|
107
|
-
const controller = new AbortController();
|
|
108
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
109
|
-
fetchOpts.signal = controller.signal;
|
|
110
|
-
let response;
|
|
111
|
-
try {
|
|
112
|
-
response = await fetch(url.toString(), fetchOpts);
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
throw new Error(`Browser server unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
-
}
|
|
117
|
-
finally {
|
|
118
|
-
clearTimeout(timer);
|
|
119
|
-
}
|
|
120
|
-
let result;
|
|
121
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
122
|
-
if (contentType.includes("application/json")) {
|
|
123
|
-
result = await response.json();
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
result = await response.text();
|
|
127
|
-
}
|
|
128
|
-
if (!response.ok) {
|
|
129
|
-
const message = result && typeof result === "object" && "error" in result
|
|
130
|
-
? String(result.error)
|
|
131
|
-
: `HTTP ${response.status}`;
|
|
132
|
-
throw new Error(message);
|
|
133
|
-
}
|
|
134
|
-
// Filter profiles by allowlist if applicable
|
|
135
|
-
if (allowProfiles.length > 0 && path === "/profiles") {
|
|
136
|
-
const obj = typeof result === "object" && result !== null ? result : {};
|
|
137
|
-
const profiles = Array.isArray(obj.profiles) ? obj.profiles : [];
|
|
138
|
-
obj.profiles = profiles.filter((entry) => {
|
|
139
|
-
if (!entry || typeof entry !== "object")
|
|
140
|
-
return false;
|
|
141
|
-
const name = entry.name;
|
|
142
|
-
return typeof name === "string" && allowProfiles.includes(name);
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
// Collect and read files (screenshots, PDFs, downloads)
|
|
146
|
-
let files;
|
|
147
|
-
const paths = collectFilePaths(result);
|
|
148
|
-
if (paths.length > 0) {
|
|
149
|
-
const loaded = await Promise.all(paths.map(async (p) => {
|
|
150
|
-
const file = await readProxyFile(p);
|
|
151
|
-
if (!file) {
|
|
152
|
-
throw new Error(`Browser proxy file not found: ${p}`);
|
|
153
|
-
}
|
|
154
|
-
return file;
|
|
155
|
-
}));
|
|
156
|
-
if (loaded.length > 0) {
|
|
157
|
-
files = loaded;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return files ? { result, files } : { result };
|
|
161
|
-
}
|
|
162
|
-
return { handleProxy };
|
|
163
|
-
}
|
|
164
|
-
//# sourceMappingURL=browser-proxy.js.map
|
|
1
|
+
import{readFile as t,stat as e}from"node:fs/promises";import{extname as r}from"node:path";const o=10485760,n={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".svg":"image/svg+xml",".pdf":"application/pdf",".html":"text/html",".json":"application/json",".txt":"text/plain",".zip":"application/zip",".mp4":"video/mp4",".webm":"video/webm"};function i(t){return n[r(t).toLowerCase()]??"application/octet-stream"}export function createBrowserProxy(r,n,a){const s=`http://127.0.0.1:${r}`;return{handleProxy:async function(r){const p="string"==typeof r.path?r.path.trim():"";if(!p)throw new Error("INVALID_REQUEST: path required");const l="string"==typeof r.method?r.method.toUpperCase():"GET",c=p.startsWith("/")?p:`/${p}`,f="string"==typeof r.profile?r.profile.trim():"";if(n.length>0&&"/profiles"!==c){if(m=f||"default",!(0===n.length||m&&n.includes(m.trim())))throw new Error("INVALID_REQUEST: browser profile not allowed")}var m;const h=new URL(c,s);f&&h.searchParams.set("profile",f);const g=r.query??{};for(const[t,e]of Object.entries(g))null!=e&&h.searchParams.set(t,String(e));a.info("BrowserProxy",`proxy: ${l} ${h.pathname}${h.search}`);const u={method:l};void 0!==r.body&&"GET"!==l&&(u.headers={"Content-Type":"application/json"},u.body=JSON.stringify(r.body));const w=Math.max(1e3,Math.min(12e4,r.timeoutMs??3e4)),d=new AbortController,y=setTimeout(()=>d.abort(),w);let b,j,x;u.signal=d.signal;try{b=await fetch(h.toString(),u)}catch(t){throw new Error(`Browser server unreachable: ${t instanceof Error?t.message:String(t)}`)}finally{clearTimeout(y)}if(j=(b.headers.get("content-type")??"").includes("application/json")?await b.json():await b.text(),!b.ok){const t=j&&"object"==typeof j&&"error"in j?String(j.error):`HTTP ${b.status}`;throw new Error(t)}if(n.length>0&&"/profiles"===c){const t="object"==typeof j&&null!==j?j:{},e=Array.isArray(t.profiles)?t.profiles:[];t.profiles=e.filter(t=>{if(!t||"object"!=typeof t)return!1;const e=t.name;return"string"==typeof e&&n.includes(e)})}const E=function(t){const e=new Set,r="object"==typeof t&&null!==t?t:null;if(!r)return[];"string"==typeof r.path&&r.path.trim()&&e.add(r.path.trim()),"string"==typeof r.imagePath&&r.imagePath.trim()&&e.add(r.imagePath.trim());const o=r.download;if(o&&"object"==typeof o){const t=o.path;"string"==typeof t&&t.trim()&&e.add(t.trim())}return[...e]}(j);if(E.length>0){const r=await Promise.all(E.map(async r=>{const n=await async function(r){const n=await e(r).catch(()=>null);if(!n||!n.isFile())return null;if(n.size>o)throw new Error(`File exceeds ${Math.round(10)}MB: ${r}`);return{path:r,base64:(await t(r)).toString("base64"),mimeType:i(r)}}(r);if(!n)throw new Error(`Browser proxy file not found: ${r}`);return n}));r.length>0&&(x=r)}return x?{result:j,files:x}:{result:j}}}}
|
package/dist/commands/shell.js
CHANGED
|
@@ -1,62 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
const TAG = "Shell";
|
|
3
|
-
export function createShellCommands(config, log) {
|
|
4
|
-
const shellConfig = config.commands.shell;
|
|
5
|
-
function validateCommand(cmd) {
|
|
6
|
-
if (!shellConfig.enabled) {
|
|
7
|
-
throw new Error("Shell commands are disabled");
|
|
8
|
-
}
|
|
9
|
-
if (shellConfig.allowlist.length > 0 && !shellConfig.allowlist.includes(cmd)) {
|
|
10
|
-
throw new Error(`Command "${cmd}" is not in the allowlist`);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
async function shellRun(params) {
|
|
14
|
-
validateCommand(params.cmd);
|
|
15
|
-
log.info(TAG, `run: ${params.cmd} ${(params.args ?? []).join(" ")}`);
|
|
16
|
-
const timeout = params.timeout ?? shellConfig.timeout;
|
|
17
|
-
return new Promise((resolve) => {
|
|
18
|
-
const proc = spawn(params.cmd, params.args ?? [], {
|
|
19
|
-
cwd: params.cwd,
|
|
20
|
-
env: params.env ? { ...process.env, ...params.env } : undefined,
|
|
21
|
-
timeout,
|
|
22
|
-
shell: false,
|
|
23
|
-
});
|
|
24
|
-
const stdoutChunks = [];
|
|
25
|
-
const stderrChunks = [];
|
|
26
|
-
proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
27
|
-
proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
28
|
-
proc.on("close", (code) => {
|
|
29
|
-
log.info(TAG, `run: ${params.cmd} exited with code ${code}`);
|
|
30
|
-
resolve({
|
|
31
|
-
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
32
|
-
stderr: Buffer.concat(stderrChunks).toString("utf-8"),
|
|
33
|
-
exitCode: code,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
proc.on("error", (err) => {
|
|
37
|
-
log.error(TAG, `run: ${params.cmd} error: ${err.message}`);
|
|
38
|
-
resolve({
|
|
39
|
-
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
40
|
-
stderr: err.message,
|
|
41
|
-
exitCode: 1,
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
async function shellWhich(params) {
|
|
47
|
-
validateCommand(params.cmd);
|
|
48
|
-
log.debug(TAG, `which: ${params.cmd}`);
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
execFile("which", [params.cmd], (err, stdout) => {
|
|
51
|
-
if (err) {
|
|
52
|
-
resolve({ path: null });
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
resolve({ path: stdout.trim() });
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
return { shellRun, shellWhich };
|
|
61
|
-
}
|
|
62
|
-
//# sourceMappingURL=shell.js.map
|
|
1
|
+
import{spawn as e,execFile as t}from"node:child_process";const o="Shell";export function createShellCommands(n,r){const s=n.commands.shell;function c(e){if(!s.enabled)throw new Error("Shell commands are disabled");if(s.allowlist.length>0&&!s.allowlist.includes(e))throw new Error(`Command "${e}" is not in the allowlist`)}return{shellRun:async function(t){c(t.cmd),r.info(o,`run: ${t.cmd} ${(t.args??[]).join(" ")}`);const n=t.timeout??s.timeout;return new Promise(s=>{const c=e(t.cmd,t.args??[],{cwd:t.cwd,env:t.env?{...process.env,...t.env}:void 0,timeout:n,shell:!1}),i=[],d=[];c.stdout.on("data",e=>i.push(e)),c.stderr.on("data",e=>d.push(e)),c.on("close",e=>{r.info(o,`run: ${t.cmd} exited with code ${e}`),s({stdout:Buffer.concat(i).toString("utf-8"),stderr:Buffer.concat(d).toString("utf-8"),exitCode:e})}),c.on("error",e=>{r.error(o,`run: ${t.cmd} error: ${e.message}`),s({stdout:Buffer.concat(i).toString("utf-8"),stderr:e.message,exitCode:1})})})},shellWhich:async function(e){return c(e.cmd),r.debug(o,`which: ${e.cmd}`),new Promise(o=>{t("which",[e.cmd],(e,t)=>{o(e?{path:null}:{path:t.trim()})})})}}}
|
package/dist/config.js
CHANGED
|
@@ -1,141 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve, join, dirname } from "node:path";
|
|
3
|
-
import { hostname as osHostname } from "node:os";
|
|
4
|
-
import { randomUUID, randomBytes } from "node:crypto";
|
|
5
|
-
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
const ConfigSchema = z.object({
|
|
8
|
-
node: z.object({
|
|
9
|
-
id: z.string().default(""),
|
|
10
|
-
displayName: z.string().default(""),
|
|
11
|
-
}),
|
|
12
|
-
gateway: z.object({
|
|
13
|
-
enabled: z.boolean().default(true),
|
|
14
|
-
url: z.string().default("ws://localhost:3001/nostromo/ws/nodes"),
|
|
15
|
-
token: z.string().default(""),
|
|
16
|
-
signature: z.string().default(""),
|
|
17
|
-
reconnectMs: z.number().default(5000),
|
|
18
|
-
}),
|
|
19
|
-
logs: z.object({
|
|
20
|
-
dir: z.string().default("./logs-stdnode"),
|
|
21
|
-
}).default({ dir: "./logs-stdnode" }),
|
|
22
|
-
commands: z.object({
|
|
23
|
-
shell: z.object({
|
|
24
|
-
enabled: z.boolean().default(true),
|
|
25
|
-
allowlist: z.array(z.string()).default([]),
|
|
26
|
-
timeout: z.number().default(30000),
|
|
27
|
-
}),
|
|
28
|
-
}),
|
|
29
|
-
browser: z.object({
|
|
30
|
-
enabled: z.boolean().default(false),
|
|
31
|
-
controlPort: z.number().default(3002),
|
|
32
|
-
headless: z.boolean().default(false),
|
|
33
|
-
noSandbox: z.boolean().default(false),
|
|
34
|
-
attachOnly: z.boolean().default(false),
|
|
35
|
-
executablePath: z.string().optional(),
|
|
36
|
-
allowProfiles: z.array(z.string()).default([]),
|
|
37
|
-
}).default({
|
|
38
|
-
enabled: false,
|
|
39
|
-
controlPort: 3002,
|
|
40
|
-
headless: false,
|
|
41
|
-
noSandbox: false,
|
|
42
|
-
attachOnly: false,
|
|
43
|
-
allowProfiles: [],
|
|
44
|
-
}),
|
|
45
|
-
});
|
|
46
|
-
const DEFAULT_CONFIG_NAME = "config.stdnode.yaml";
|
|
47
|
-
const DEFAULT_GATEWAY_URL = "ws://localhost:3001/nostromo/ws/nodes";
|
|
48
|
-
/**
|
|
49
|
-
* Try to read the Hera server config.yaml from the parent directory
|
|
50
|
-
* and extract Nostromo port + basePath to build the gateway URL.
|
|
51
|
-
* Returns the auto-detected URL or the default.
|
|
52
|
-
*/
|
|
53
|
-
function detectGatewayUrl(configDir) {
|
|
54
|
-
const serverConfigPath = join(dirname(configDir), "config.yaml");
|
|
55
|
-
try {
|
|
56
|
-
if (!existsSync(serverConfigPath))
|
|
57
|
-
return DEFAULT_GATEWAY_URL;
|
|
58
|
-
const raw = readFileSync(serverConfigPath, "utf-8");
|
|
59
|
-
const parsed = yamlParse(raw);
|
|
60
|
-
const port = parsed?.nostromo?.port ?? 3001;
|
|
61
|
-
const basePath = parsed?.nostromo?.basePath ?? "/nostromo";
|
|
62
|
-
const host = parsed?.host ?? "localhost";
|
|
63
|
-
return `ws://${host}:${port}${basePath}/ws/nodes`;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return DEFAULT_GATEWAY_URL;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
export function loadConfig(opts) {
|
|
70
|
-
const path = resolve(opts?.configPath ?? DEFAULT_CONFIG_NAME);
|
|
71
|
-
// If config doesn't exist, create a minimal one
|
|
72
|
-
if (!existsSync(path)) {
|
|
73
|
-
writeFileSync(path, yamlStringify({
|
|
74
|
-
node: { id: "", displayName: "" },
|
|
75
|
-
gateway: { enabled: true, url: "ws://localhost:3001/nostromo/ws/nodes" },
|
|
76
|
-
logs: { dir: "./logs-stdnode" },
|
|
77
|
-
commands: { shell: { enabled: true, allowlist: [], timeout: 30000 } },
|
|
78
|
-
browser: { enabled: false, controlPort: 3002 },
|
|
79
|
-
}), "utf-8");
|
|
80
|
-
}
|
|
81
|
-
const raw = readFileSync(path, "utf-8");
|
|
82
|
-
const parsed = yamlParse(raw);
|
|
83
|
-
const config = ConfigSchema.parse(parsed);
|
|
84
|
-
// Auto-generate node ID and signature if empty
|
|
85
|
-
let needsWrite = false;
|
|
86
|
-
const updated = yamlParse(raw) ?? {};
|
|
87
|
-
if (!config.node.id) {
|
|
88
|
-
config.node.id = randomUUID();
|
|
89
|
-
if (!updated.node)
|
|
90
|
-
updated.node = {};
|
|
91
|
-
updated.node.id = config.node.id;
|
|
92
|
-
needsWrite = true;
|
|
93
|
-
}
|
|
94
|
-
if (!config.gateway.signature) {
|
|
95
|
-
config.gateway.signature = randomBytes(64).toString("hex");
|
|
96
|
-
if (!updated.gateway)
|
|
97
|
-
updated.gateway = {};
|
|
98
|
-
updated.gateway.signature = config.gateway.signature;
|
|
99
|
-
needsWrite = true;
|
|
100
|
-
}
|
|
101
|
-
// Default displayName to hostname
|
|
102
|
-
if (!config.node.displayName) {
|
|
103
|
-
config.node.displayName = osHostname();
|
|
104
|
-
if (!updated.node)
|
|
105
|
-
updated.node = {};
|
|
106
|
-
updated.node.displayName = config.node.displayName;
|
|
107
|
-
needsWrite = true;
|
|
108
|
-
}
|
|
109
|
-
// Auto-detect gateway URL from ../config.yaml if still default
|
|
110
|
-
if (!opts?.ws && config.gateway.url === DEFAULT_GATEWAY_URL) {
|
|
111
|
-
const detected = detectGatewayUrl(resolve(path, ".."));
|
|
112
|
-
if (detected !== DEFAULT_GATEWAY_URL) {
|
|
113
|
-
config.gateway.url = detected;
|
|
114
|
-
if (!updated.gateway)
|
|
115
|
-
updated.gateway = {};
|
|
116
|
-
updated.gateway.url = detected;
|
|
117
|
-
needsWrite = true;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// CLI overrides: --ws (takes priority over auto-detect)
|
|
121
|
-
if (opts?.ws) {
|
|
122
|
-
config.gateway.url = opts.ws;
|
|
123
|
-
if (!updated.gateway)
|
|
124
|
-
updated.gateway = {};
|
|
125
|
-
updated.gateway.url = opts.ws;
|
|
126
|
-
needsWrite = true;
|
|
127
|
-
}
|
|
128
|
-
// CLI overrides: --name
|
|
129
|
-
if (opts?.name) {
|
|
130
|
-
config.node.displayName = opts.name;
|
|
131
|
-
if (!updated.node)
|
|
132
|
-
updated.node = {};
|
|
133
|
-
updated.node.displayName = opts.name;
|
|
134
|
-
needsWrite = true;
|
|
135
|
-
}
|
|
136
|
-
if (needsWrite) {
|
|
137
|
-
writeFileSync(path, yamlStringify(updated), "utf-8");
|
|
138
|
-
}
|
|
139
|
-
return config;
|
|
140
|
-
}
|
|
141
|
-
//# sourceMappingURL=config.js.map
|
|
1
|
+
import{readFileSync as e,writeFileSync as o,existsSync as a}from"node:fs";import{resolve as t,join as n,dirname as l}from"node:path";import{hostname as s}from"node:os";import{randomUUID as d,randomBytes as r}from"node:crypto";import{parse as i,stringify as u}from"yaml";import{z as g}from"zod";const m=g.object({node:g.object({id:g.string().default(""),displayName:g.string().default("")}),gateway:g.object({enabled:g.boolean().default(!0),url:g.string().default("ws://localhost:3001/nostromo/ws/nodes"),token:g.string().default(""),signature:g.string().default(""),reconnectMs:g.number().default(5e3)}),logs:g.object({dir:g.string().default("./logs-stdnode")}).default({dir:"./logs-stdnode"}),commands:g.object({shell:g.object({enabled:g.boolean().default(!0),allowlist:g.array(g.string()).default([]),timeout:g.number().default(3e4)})}),browser:g.object({enabled:g.boolean().default(!1),controlPort:g.number().default(3002),headless:g.boolean().default(!1),noSandbox:g.boolean().default(!1),attachOnly:g.boolean().default(!1),executablePath:g.string().optional(),allowProfiles:g.array(g.string()).default([])}).default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,allowProfiles:[]})}),f="ws://localhost:3001/nostromo/ws/nodes";export function loadConfig(g){const c=t(g?.configPath??"config.stdnode.yaml");a(c)||o(c,u({node:{id:"",displayName:""},gateway:{enabled:!0,url:"ws://localhost:3001/nostromo/ws/nodes"},logs:{dir:"./logs-stdnode"},commands:{shell:{enabled:!0,allowlist:[],timeout:3e4}},browser:{enabled:!1,controlPort:3002}}),"utf-8");const w=e(c,"utf-8"),y=i(w),b=m.parse(y);let p=!1;const h=i(w)??{};if(b.node.id||(b.node.id=d(),h.node||(h.node={}),h.node.id=b.node.id,p=!0),b.gateway.signature||(b.gateway.signature=r(64).toString("hex"),h.gateway||(h.gateway={}),h.gateway.signature=b.gateway.signature,p=!0),b.node.displayName||(b.node.displayName=s(),h.node||(h.node={}),h.node.displayName=b.node.displayName,p=!0),!g?.ws&&b.gateway.url===f){const o=function(o){const t=n(l(o),"config.yaml");try{if(!a(t))return f;const o=e(t,"utf-8"),n=i(o),l=n?.nostromo?.port??3001,s=n?.nostromo?.basePath??"/nostromo";return`ws://${n?.host??"localhost"}:${l}${s}/ws/nodes`}catch{return f}}(t(c,".."));o!==f&&(b.gateway.url=o,h.gateway||(h.gateway={}),h.gateway.url=o,p=!0)}return g?.ws&&(b.gateway.url=g.ws,h.gateway||(h.gateway={}),h.gateway.url=g.ws,p=!0),g?.name&&(b.node.displayName=g.name,h.node||(h.node={}),h.node.displayName=g.name,p=!0),p&&o(c,u(h),"utf-8"),b}
|
package/dist/gateway-link.js
CHANGED
|
@@ -1,315 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { resolve, dirname } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { platform, hostname, arch } from "node:os";
|
|
7
|
-
import { createShellCommands } from "./commands/shell.js";
|
|
8
|
-
import { createBrowserProxy } from "./commands/browser-proxy.js";
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const TAG = "Gateway";
|
|
11
|
-
const RESET = "\x1b[0m";
|
|
12
|
-
const BOLD = "\x1b[1m";
|
|
13
|
-
const DIM = "\x1b[2m";
|
|
14
|
-
const fg = (n) => `\x1b[38;5;${n}m`;
|
|
15
|
-
const GREEN = fg(82);
|
|
16
|
-
const RED = fg(196);
|
|
17
|
-
const YELLOW = fg(220);
|
|
18
|
-
export class GatewayLink {
|
|
19
|
-
config;
|
|
20
|
-
log;
|
|
21
|
-
ws = null;
|
|
22
|
-
heartbeatTimer = null;
|
|
23
|
-
reconnectTimer = null;
|
|
24
|
-
stopped = false;
|
|
25
|
-
paired = false;
|
|
26
|
-
shell;
|
|
27
|
-
browserProcess = null;
|
|
28
|
-
browserProxy = null;
|
|
29
|
-
browserReady = false;
|
|
30
|
-
constructor(config, log) {
|
|
31
|
-
this.config = config;
|
|
32
|
-
this.log = log;
|
|
33
|
-
this.shell = createShellCommands(config, log);
|
|
34
|
-
}
|
|
35
|
-
start() {
|
|
36
|
-
if (this.config.browser.enabled) {
|
|
37
|
-
this.startBrowserServer();
|
|
38
|
-
}
|
|
39
|
-
if (!this.config.gateway.enabled)
|
|
40
|
-
return;
|
|
41
|
-
this.stopped = false;
|
|
42
|
-
this.connect();
|
|
43
|
-
}
|
|
44
|
-
stop() {
|
|
45
|
-
this.stopped = true;
|
|
46
|
-
this.clearTimers();
|
|
47
|
-
this.stopBrowserServer();
|
|
48
|
-
if (this.ws) {
|
|
49
|
-
this.ws.close();
|
|
50
|
-
this.ws = null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// ---------- Browser server subprocess ----------
|
|
54
|
-
startBrowserServer() {
|
|
55
|
-
const cfg = this.config.browser;
|
|
56
|
-
// Try to resolve @hera-al/browser-server package
|
|
57
|
-
let scriptPath = null;
|
|
58
|
-
try {
|
|
59
|
-
// import.meta.resolve returns a file:// URL pointing to the main entry
|
|
60
|
-
// (dist/server/browser-server.js) — standalone is in the same directory
|
|
61
|
-
const resolved = import.meta.resolve("@hera-al/browser-server");
|
|
62
|
-
const entryDir = dirname(fileURLToPath(resolved));
|
|
63
|
-
const ext = resolved.endsWith(".ts") ? ".ts" : ".js";
|
|
64
|
-
// The main export points to dist/server/browser-server.js,
|
|
65
|
-
// so standalone.js is in the same directory (not server/ below it).
|
|
66
|
-
scriptPath = resolve(entryDir, `standalone${ext}`);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
// Fallback: monorepo / dev layout
|
|
70
|
-
const isTs = import.meta.url.endsWith(".ts");
|
|
71
|
-
const ext = isTs ? ".ts" : ".js";
|
|
72
|
-
const srcOrDist = isTs ? "src" : "dist";
|
|
73
|
-
const fallback = resolve(__dirname, `../../browser-server/${srcOrDist}/server/standalone${ext}`);
|
|
74
|
-
if (existsSync(fallback))
|
|
75
|
-
scriptPath = fallback;
|
|
76
|
-
}
|
|
77
|
-
if (!scriptPath || !existsSync(scriptPath)) {
|
|
78
|
-
this.log.warn(TAG, "Browser capability requires @hera-al/browser-server. Install it with: npm install @hera-al/browser-server");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const args = [
|
|
82
|
-
...process.execArgv,
|
|
83
|
-
scriptPath,
|
|
84
|
-
"--port", String(cfg.controlPort),
|
|
85
|
-
];
|
|
86
|
-
if (cfg.headless)
|
|
87
|
-
args.push("--headless", "true");
|
|
88
|
-
if (cfg.noSandbox)
|
|
89
|
-
args.push("--noSandbox", "true");
|
|
90
|
-
if (cfg.attachOnly)
|
|
91
|
-
args.push("--attachOnly", "true");
|
|
92
|
-
if (cfg.executablePath)
|
|
93
|
-
args.push("--executablePath", cfg.executablePath);
|
|
94
|
-
this.log.info(TAG, `Starting browser server on port ${cfg.controlPort}...`);
|
|
95
|
-
const child = spawn(process.execPath, args, {
|
|
96
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
97
|
-
env: process.env,
|
|
98
|
-
});
|
|
99
|
-
child.stdout?.on("data", (data) => {
|
|
100
|
-
const line = data.toString().trim();
|
|
101
|
-
if (line)
|
|
102
|
-
this.log.info("Browser", line);
|
|
103
|
-
if (line.includes("listening")) {
|
|
104
|
-
this.browserReady = true;
|
|
105
|
-
this.log.info(TAG, "Browser server is ready");
|
|
106
|
-
console.log(` ${GREEN}${BOLD}🌐 Browser server${RESET} listening on 127.0.0.1:${cfg.controlPort}`);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
child.stderr?.on("data", (data) => {
|
|
110
|
-
const line = data.toString().trim();
|
|
111
|
-
if (line)
|
|
112
|
-
this.log.warn("Browser", line);
|
|
113
|
-
});
|
|
114
|
-
child.on("exit", (code) => {
|
|
115
|
-
this.log.info(TAG, `Browser server exited with code ${code}`);
|
|
116
|
-
this.browserProcess = null;
|
|
117
|
-
this.browserReady = false;
|
|
118
|
-
});
|
|
119
|
-
// Safety net: kill the child if this process exits for any reason
|
|
120
|
-
// (crash, uncaught exception, etc.). The 'exit' event is synchronous
|
|
121
|
-
// and fires even on unhandled errors — only SIGKILL bypasses it.
|
|
122
|
-
const exitHandler = () => {
|
|
123
|
-
try {
|
|
124
|
-
child.kill("SIGTERM");
|
|
125
|
-
}
|
|
126
|
-
catch { /* already dead */ }
|
|
127
|
-
};
|
|
128
|
-
process.on("exit", exitHandler);
|
|
129
|
-
child.on("exit", () => {
|
|
130
|
-
process.removeListener("exit", exitHandler);
|
|
131
|
-
});
|
|
132
|
-
this.browserProcess = child;
|
|
133
|
-
this.browserProxy = createBrowserProxy(cfg.controlPort, cfg.allowProfiles, this.log);
|
|
134
|
-
}
|
|
135
|
-
stopBrowserServer() {
|
|
136
|
-
if (this.browserProcess) {
|
|
137
|
-
this.browserProcess.kill("SIGTERM");
|
|
138
|
-
this.browserProcess = null;
|
|
139
|
-
this.browserReady = false;
|
|
140
|
-
this.browserProxy = null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
connect() {
|
|
144
|
-
if (this.stopped)
|
|
145
|
-
return;
|
|
146
|
-
const url = this.config.gateway.url;
|
|
147
|
-
this.log.info(TAG, `Connecting to ${url}...`);
|
|
148
|
-
this.ws = new WebSocket(url);
|
|
149
|
-
this.ws.on("open", () => {
|
|
150
|
-
this.log.info(TAG, "Connected to gateway");
|
|
151
|
-
this.sendHello();
|
|
152
|
-
this.startHeartbeat();
|
|
153
|
-
});
|
|
154
|
-
this.ws.on("message", (data) => {
|
|
155
|
-
try {
|
|
156
|
-
const msg = JSON.parse(data.toString());
|
|
157
|
-
this.handleMessage(msg);
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
// Ignore malformed messages
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
this.ws.on("close", () => {
|
|
164
|
-
this.log.warn(TAG, "Disconnected from gateway");
|
|
165
|
-
this.paired = false;
|
|
166
|
-
this.clearTimers();
|
|
167
|
-
this.scheduleReconnect();
|
|
168
|
-
});
|
|
169
|
-
this.ws.on("error", (err) => {
|
|
170
|
-
this.log.error(TAG, `WebSocket error: ${err.message}`);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
sendHello() {
|
|
174
|
-
const capabilities = [];
|
|
175
|
-
const commands = [];
|
|
176
|
-
if (this.config.commands.shell.enabled) {
|
|
177
|
-
capabilities.push("shell");
|
|
178
|
-
commands.push("shell.run", "shell.which");
|
|
179
|
-
}
|
|
180
|
-
if (this.config.browser.enabled) {
|
|
181
|
-
capabilities.push("browser");
|
|
182
|
-
commands.push("browser.proxy");
|
|
183
|
-
}
|
|
184
|
-
this.send({
|
|
185
|
-
type: "hello",
|
|
186
|
-
nodeId: this.config.node.id,
|
|
187
|
-
displayName: this.config.node.displayName,
|
|
188
|
-
platform: platform(),
|
|
189
|
-
arch: arch(),
|
|
190
|
-
hostname: hostname(),
|
|
191
|
-
signature: this.config.gateway.signature,
|
|
192
|
-
capabilities,
|
|
193
|
-
commands,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
startHeartbeat() {
|
|
197
|
-
this.heartbeatTimer = setInterval(() => {
|
|
198
|
-
this.send({ type: "ping" });
|
|
199
|
-
}, 30000);
|
|
200
|
-
}
|
|
201
|
-
handleMessage(msg) {
|
|
202
|
-
switch (msg.type) {
|
|
203
|
-
case "pong":
|
|
204
|
-
break;
|
|
205
|
-
case "pairing_status":
|
|
206
|
-
this.handlePairingStatus(msg.status);
|
|
207
|
-
break;
|
|
208
|
-
case "command":
|
|
209
|
-
this.handleCommand(msg);
|
|
210
|
-
break;
|
|
211
|
-
default:
|
|
212
|
-
this.log.debug(TAG, `Received: ${msg.type}`);
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
handlePairingStatus(status) {
|
|
217
|
-
switch (status) {
|
|
218
|
-
case "pending":
|
|
219
|
-
this.log.info(TAG, "Pairing pending — waiting for admin approval");
|
|
220
|
-
console.log(` ${YELLOW}${BOLD}⏳ Pairing pending${RESET} ${DIM}— waiting for admin approval on Nostromo${RESET}`);
|
|
221
|
-
this.paired = false;
|
|
222
|
-
break;
|
|
223
|
-
case "approved":
|
|
224
|
-
this.log.info(TAG, "Pairing approved — node is active");
|
|
225
|
-
console.log(` ${GREEN}${BOLD}✔ Paired!${RESET} Node is active and ready to receive commands.`);
|
|
226
|
-
this.paired = true;
|
|
227
|
-
break;
|
|
228
|
-
case "revoked":
|
|
229
|
-
this.log.warn(TAG, "Pairing revoked — disconnecting");
|
|
230
|
-
console.log(` ${RED}${BOLD}✖ Pairing revoked.${RESET} The node has been disconnected by the admin.`);
|
|
231
|
-
this.paired = false;
|
|
232
|
-
this.stop();
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
async handleCommand(msg) {
|
|
237
|
-
const id = msg.id;
|
|
238
|
-
const command = msg.command;
|
|
239
|
-
const params = msg.params;
|
|
240
|
-
if (!id || !command) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (!this.paired) {
|
|
244
|
-
this.send({
|
|
245
|
-
type: "command_result",
|
|
246
|
-
id,
|
|
247
|
-
ok: false,
|
|
248
|
-
error: "Node is not paired — awaiting approval",
|
|
249
|
-
});
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
this.log.info(TAG, `Command: ${command} (${id})`);
|
|
253
|
-
try {
|
|
254
|
-
let result;
|
|
255
|
-
switch (command) {
|
|
256
|
-
case "shell.run":
|
|
257
|
-
result = await this.shell.shellRun(params);
|
|
258
|
-
break;
|
|
259
|
-
case "shell.which":
|
|
260
|
-
result = await this.shell.shellWhich(params);
|
|
261
|
-
break;
|
|
262
|
-
case "browser.proxy":
|
|
263
|
-
if (!this.browserProxy) {
|
|
264
|
-
this.send({ type: "command_result", id, ok: false, error: "Browser is not enabled" });
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (!this.browserReady) {
|
|
268
|
-
this.send({ type: "command_result", id, ok: false, error: "Browser server not ready" });
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
result = await this.browserProxy.handleProxy(params);
|
|
272
|
-
break;
|
|
273
|
-
default:
|
|
274
|
-
this.log.warn(TAG, `Unknown command: ${command}`);
|
|
275
|
-
this.send({
|
|
276
|
-
type: "command_result",
|
|
277
|
-
id,
|
|
278
|
-
ok: false,
|
|
279
|
-
error: `Unknown command: ${command}`,
|
|
280
|
-
});
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
this.log.info(TAG, `Command ${id} completed`);
|
|
284
|
-
this.send({ type: "command_result", id, ok: true, result });
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
288
|
-
this.log.error(TAG, `Command ${id} failed: ${message}`);
|
|
289
|
-
this.send({ type: "command_result", id, ok: false, error: message });
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
send(data) {
|
|
293
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
294
|
-
this.ws.send(JSON.stringify(data));
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
scheduleReconnect() {
|
|
298
|
-
if (this.stopped)
|
|
299
|
-
return;
|
|
300
|
-
const delay = this.config.gateway.reconnectMs;
|
|
301
|
-
this.log.info(TAG, `Reconnecting in ${delay}ms...`);
|
|
302
|
-
this.reconnectTimer = setTimeout(() => this.connect(), delay);
|
|
303
|
-
}
|
|
304
|
-
clearTimers() {
|
|
305
|
-
if (this.heartbeatTimer) {
|
|
306
|
-
clearInterval(this.heartbeatTimer);
|
|
307
|
-
this.heartbeatTimer = null;
|
|
308
|
-
}
|
|
309
|
-
if (this.reconnectTimer) {
|
|
310
|
-
clearTimeout(this.reconnectTimer);
|
|
311
|
-
this.reconnectTimer = null;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
//# sourceMappingURL=gateway-link.js.map
|
|
1
|
+
import e from"ws";import{spawn as s}from"node:child_process";import{existsSync as r}from"node:fs";import{resolve as t,dirname as o}from"node:path";import{fileURLToPath as i}from"node:url";import{platform as n,hostname as a,arch as l}from"node:os";import{createShellCommands as h}from"./commands/shell.js";import{createBrowserProxy as c}from"./commands/browser-proxy.js";const d=o(i(import.meta.url)),m="Gateway",p="[0m",g="[1m",w=e=>`[38;5;${e}m`,u=w(82),b=w(196),f=w(220);export class GatewayLink{config;log;ws=null;heartbeatTimer=null;reconnectTimer=null;stopped=!1;paired=!1;shell;browserProcess=null;browserProxy=null;browserReady=!1;constructor(e,s){this.config=e,this.log=s,this.shell=h(e,s)}start(){this.config.browser.enabled&&this.startBrowserServer(),this.config.gateway.enabled&&(this.stopped=!1,this.connect())}stop(){this.stopped=!0,this.clearTimers(),this.stopBrowserServer(),this.ws&&(this.ws.close(),this.ws=null)}startBrowserServer(){const e=this.config.browser;let n=null;try{const e=import.meta.resolve("@hera-al/browser-server"),s=o(i(e)),r=e.endsWith(".ts")?".ts":".js";n=t(s,`standalone${r}`)}catch{const e=import.meta.url.endsWith(".ts"),s=t(d,`../../browser-server/${e?"src":"dist"}/server/standalone${e?".ts":".js"}`);r(s)&&(n=s)}if(!n||!r(n))return void this.log.warn(m,"Browser capability requires @hera-al/browser-server. Install it with: npm install @hera-al/browser-server");const a=[...process.execArgv,n,"--port",String(e.controlPort)];e.headless&&a.push("--headless","true"),e.noSandbox&&a.push("--noSandbox","true"),e.attachOnly&&a.push("--attachOnly","true"),e.executablePath&&a.push("--executablePath",e.executablePath),this.log.info(m,`Starting browser server on port ${e.controlPort}...`);const l=s(process.execPath,a,{stdio:["ignore","pipe","pipe"],env:process.env});l.stdout?.on("data",s=>{const r=s.toString().trim();r&&this.log.info("Browser",r),r.includes("listening")&&(this.browserReady=!0,this.log.info(m,"Browser server is ready"),console.log(` ${u}${g}🌐 Browser server${p} listening on 127.0.0.1:${e.controlPort}`))}),l.stderr?.on("data",e=>{const s=e.toString().trim();s&&this.log.warn("Browser",s)}),l.on("exit",e=>{this.log.info(m,`Browser server exited with code ${e}`),this.browserProcess=null,this.browserReady=!1});const h=()=>{try{l.kill("SIGTERM")}catch{}};process.on("exit",h),l.on("exit",()=>{process.removeListener("exit",h)}),this.browserProcess=l,this.browserProxy=c(e.controlPort,e.allowProfiles,this.log)}stopBrowserServer(){this.browserProcess&&(this.browserProcess.kill("SIGTERM"),this.browserProcess=null,this.browserReady=!1,this.browserProxy=null)}connect(){if(this.stopped)return;const s=this.config.gateway.url;this.log.info(m,`Connecting to ${s}...`),this.ws=new e(s),this.ws.on("open",()=>{this.log.info(m,"Connected to gateway"),this.sendHello(),this.startHeartbeat()}),this.ws.on("message",e=>{try{const s=JSON.parse(e.toString());this.handleMessage(s)}catch{}}),this.ws.on("close",()=>{this.log.warn(m,"Disconnected from gateway"),this.paired=!1,this.clearTimers(),this.scheduleReconnect()}),this.ws.on("error",e=>{this.log.error(m,`WebSocket error: ${e.message}`)})}sendHello(){const e=[],s=[];this.config.commands.shell.enabled&&(e.push("shell"),s.push("shell.run","shell.which")),this.config.browser.enabled&&(e.push("browser"),s.push("browser.proxy")),this.send({type:"hello",nodeId:this.config.node.id,displayName:this.config.node.displayName,platform:n(),arch:l(),hostname:a(),signature:this.config.gateway.signature,capabilities:e,commands:s})}startHeartbeat(){this.heartbeatTimer=setInterval(()=>{this.send({type:"ping"})},3e4)}handleMessage(e){switch(e.type){case"pong":break;case"pairing_status":this.handlePairingStatus(e.status);break;case"command":this.handleCommand(e);break;default:this.log.debug(m,`Received: ${e.type}`)}}handlePairingStatus(e){switch(e){case"pending":this.log.info(m,"Pairing pending — waiting for admin approval"),console.log(` ${f}${g}⏳ Pairing pending${p} [2m— waiting for admin approval on Nostromo${p}`),this.paired=!1;break;case"approved":this.log.info(m,"Pairing approved — node is active"),console.log(` ${u}${g}✔ Paired!${p} Node is active and ready to receive commands.`),this.paired=!0;break;case"revoked":this.log.warn(m,"Pairing revoked — disconnecting"),console.log(` ${b}${g}✖ Pairing revoked.${p} The node has been disconnected by the admin.`),this.paired=!1,this.stop()}}async handleCommand(e){const s=e.id,r=e.command,t=e.params;if(s&&r)if(this.paired){this.log.info(m,`Command: ${r} (${s})`);try{let e;switch(r){case"shell.run":e=await this.shell.shellRun(t);break;case"shell.which":e=await this.shell.shellWhich(t);break;case"browser.proxy":if(!this.browserProxy)return void this.send({type:"command_result",id:s,ok:!1,error:"Browser is not enabled"});if(!this.browserReady)return void this.send({type:"command_result",id:s,ok:!1,error:"Browser server not ready"});e=await this.browserProxy.handleProxy(t);break;default:return this.log.warn(m,`Unknown command: ${r}`),void this.send({type:"command_result",id:s,ok:!1,error:`Unknown command: ${r}`})}this.log.info(m,`Command ${s} completed`),this.send({type:"command_result",id:s,ok:!0,result:e})}catch(e){const r=e instanceof Error?e.message:String(e);this.log.error(m,`Command ${s} failed: ${r}`),this.send({type:"command_result",id:s,ok:!1,error:r})}}else this.send({type:"command_result",id:s,ok:!1,error:"Node is not paired — awaiting approval"})}send(s){this.ws?.readyState===e.OPEN&&this.ws.send(JSON.stringify(s))}scheduleReconnect(){if(this.stopped)return;const e=this.config.gateway.reconnectMs;this.log.info(m,`Reconnecting in ${e}ms...`),this.reconnectTimer=setTimeout(()=>this.connect(),e)}clearTimers(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}}
|
package/dist/index.js
CHANGED
|
@@ -1,170 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { resolve, dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { loadConfig } from "./config.js";
|
|
6
|
-
import { GatewayLink } from "./gateway-link.js";
|
|
7
|
-
import { Logger } from "./logger.js";
|
|
8
|
-
// ─── ANSI helpers ──────────────────────────────────────────────
|
|
9
|
-
const RESET = "\x1b[0m";
|
|
10
|
-
const BOLD = "\x1b[1m";
|
|
11
|
-
const DIM = "\x1b[2m";
|
|
12
|
-
const fg = (n) => `\x1b[38;5;${n}m`;
|
|
13
|
-
// ─── Palette (same gem tones as installer) ─────────────────────
|
|
14
|
-
const C = {
|
|
15
|
-
pk: 205,
|
|
16
|
-
hp: 199,
|
|
17
|
-
mg: 200,
|
|
18
|
-
dp: 197,
|
|
19
|
-
rd: 196,
|
|
20
|
-
dr: 161,
|
|
21
|
-
};
|
|
22
|
-
// ─── Banner ────────────────────────────────────────────────────
|
|
23
|
-
function printBanner(name, id, ws, browserPort) {
|
|
24
|
-
console.log("");
|
|
25
|
-
console.log(` ${fg(C.hp)}${BOLD} ███████╗████████╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗${RESET}`);
|
|
26
|
-
console.log(` ${fg(C.hp)}${BOLD} ██╔════╝╚══██╔══╝██╔══██╗ ████╗ ██║██╔═══██╗██╔══██╗██╔════╝${RESET}`);
|
|
27
|
-
console.log(` ${fg(C.mg)}${BOLD} ███████╗ ██║ ██║ ██║ ██╔██╗ ██║██║ ██║██║ ██║█████╗ ${RESET}`);
|
|
28
|
-
console.log(` ${fg(C.dp)}${BOLD} ╚════██║ ██║ ██║ ██║ ██║╚██╗██║██║ ██║██║ ██║██╔══╝ ${RESET}`);
|
|
29
|
-
console.log(` ${fg(C.rd)}${BOLD} ███████║ ██║ ██████╔╝ ██║ ╚████║╚██████╔╝██████╔╝███████╗${RESET}`);
|
|
30
|
-
console.log(` ${fg(C.dr)}${BOLD} ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝${RESET}`);
|
|
31
|
-
console.log("");
|
|
32
|
-
console.log(` ${fg(C.pk)}${BOLD}Hera StandardNode${RESET}`);
|
|
33
|
-
console.log(` ${DIM}${"─".repeat(50)}${RESET}`);
|
|
34
|
-
console.log(` ${DIM}Name${RESET} ${BOLD}${name}${RESET}`);
|
|
35
|
-
console.log(` ${DIM}ID${RESET} ${DIM}${id}${RESET}`);
|
|
36
|
-
console.log(` ${DIM}Gateway${RESET} ${DIM}${ws}${RESET}`);
|
|
37
|
-
if (browserPort) {
|
|
38
|
-
console.log(` ${DIM}Browser${RESET} ${DIM}127.0.0.1:${browserPort}${RESET}`);
|
|
39
|
-
}
|
|
40
|
-
console.log("");
|
|
41
|
-
}
|
|
42
|
-
// ─── Help ──────────────────────────────────────────────────────
|
|
43
|
-
function printHelp() {
|
|
44
|
-
console.log("");
|
|
45
|
-
console.log(` ${fg(C.pk)}${BOLD}Hera StandardNode${RESET}`);
|
|
46
|
-
console.log("");
|
|
47
|
-
console.log(` ${BOLD}Usage:${RESET} npx tsx src/index.ts [options]`);
|
|
48
|
-
console.log("");
|
|
49
|
-
console.log(` ${BOLD}Options:${RESET}`);
|
|
50
|
-
console.log(` --ws <url> WebSocket URL of the Hera gateway`);
|
|
51
|
-
console.log(` --name <name> Display name for this node (default: hostname)`);
|
|
52
|
-
console.log(` --config <path> Path to config file (default: config.stdnode.yaml)`);
|
|
53
|
-
console.log(` --init Copy example config to current directory and exit`);
|
|
54
|
-
console.log(` --help Show this help message`);
|
|
55
|
-
console.log("");
|
|
56
|
-
console.log(` ${BOLD}WebSocket URL format:${RESET}`);
|
|
57
|
-
console.log(` ws[s]://<hostname>:<port><basePath>/ws/nodes`);
|
|
58
|
-
console.log("");
|
|
59
|
-
console.log(` The ${BOLD}<port>${RESET} is the Nostromo UI port configured on the server`);
|
|
60
|
-
console.log(` (default 3001). The ${BOLD}<basePath>${RESET} is the Nostromo base path`);
|
|
61
|
-
console.log(` (default /nostromo).`);
|
|
62
|
-
console.log("");
|
|
63
|
-
console.log(` ${BOLD}Examples:${RESET}`);
|
|
64
|
-
console.log(` --ws ws://localhost:3001/nostromo/ws/nodes`);
|
|
65
|
-
console.log(` --ws wss://myhost.tail12345.ts.net:3001/nostromo/ws/nodes`);
|
|
66
|
-
console.log("");
|
|
67
|
-
console.log(` ${BOLD}Auto-detect:${RESET}`);
|
|
68
|
-
console.log(` If ../config.yaml exists (Hera server config), the node reads`);
|
|
69
|
-
console.log(` the Nostromo port and basePath from it and auto-configures the`);
|
|
70
|
-
console.log(` gateway URL. The --ws flag overrides auto-detection.`);
|
|
71
|
-
console.log("");
|
|
72
|
-
}
|
|
73
|
-
// ─── CLI arg parsing ───────────────────────────────────────────
|
|
74
|
-
function parseArgs() {
|
|
75
|
-
const args = process.argv.slice(2);
|
|
76
|
-
const opts = {};
|
|
77
|
-
for (let i = 0; i < args.length; i++) {
|
|
78
|
-
const arg = args[i];
|
|
79
|
-
if (arg === "--help" || arg === "-h") {
|
|
80
|
-
opts.help = true;
|
|
81
|
-
}
|
|
82
|
-
else if (arg === "--init") {
|
|
83
|
-
opts.init = true;
|
|
84
|
-
}
|
|
85
|
-
else if (arg === "--config" && args[i + 1]) {
|
|
86
|
-
opts.configPath = args[++i];
|
|
87
|
-
}
|
|
88
|
-
else if (arg === "--ws" && args[i + 1]) {
|
|
89
|
-
opts.ws = args[++i];
|
|
90
|
-
}
|
|
91
|
-
else if (arg === "--name" && args[i + 1]) {
|
|
92
|
-
opts.name = args[++i];
|
|
93
|
-
}
|
|
94
|
-
else if (!arg.startsWith("--") && !opts.configPath) {
|
|
95
|
-
// Legacy: first positional arg is config path
|
|
96
|
-
opts.configPath = arg;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return opts;
|
|
100
|
-
}
|
|
101
|
-
// ─── Init ─────────────────────────────────────────────────────
|
|
102
|
-
function initConfig() {
|
|
103
|
-
const dest = resolve("config.stdnode.yaml");
|
|
104
|
-
if (existsSync(dest)) {
|
|
105
|
-
console.log(`\n ${fg(C.rd)}${BOLD}config.stdnode.yaml already exists${RESET} — not overwriting.\n`);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
// Resolve the example config bundled with the package
|
|
109
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
110
|
-
const pkgRoot = dirname(dirname(thisFile)); // up from dist/ or src/
|
|
111
|
-
const example = join(pkgRoot, "config.stdnode.example.yaml");
|
|
112
|
-
if (!existsSync(example)) {
|
|
113
|
-
console.log(`\n ${fg(C.rd)}${BOLD}Example config not found${RESET} — run from a proper install.\n`);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
copyFileSync(example, dest);
|
|
117
|
-
console.log(`\n ${fg(C.pk)}${BOLD}Created${RESET} config.stdnode.yaml`);
|
|
118
|
-
console.log(` ${DIM}Edit it, then run: hera-stdnode${RESET}\n`);
|
|
119
|
-
}
|
|
120
|
-
// ─── Main ──────────────────────────────────────────────────────
|
|
121
|
-
function main() {
|
|
122
|
-
const opts = parseArgs();
|
|
123
|
-
if (opts.help) {
|
|
124
|
-
printHelp();
|
|
125
|
-
process.exit(0);
|
|
126
|
-
}
|
|
127
|
-
if (opts.init) {
|
|
128
|
-
initConfig();
|
|
129
|
-
process.exit(0);
|
|
130
|
-
}
|
|
131
|
-
const config = loadConfig(opts);
|
|
132
|
-
const log = new Logger(config.logs.dir);
|
|
133
|
-
printBanner(config.node.displayName, config.node.id, config.gateway.url, config.browser.enabled ? config.browser.controlPort : undefined);
|
|
134
|
-
log.info("Node", `Starting — ${config.node.displayName} (${config.node.id})`);
|
|
135
|
-
log.info("Node", `Gateway: ${config.gateway.url}`);
|
|
136
|
-
log.info("Node", `Logs dir: ${config.logs.dir}`);
|
|
137
|
-
// Connect to gateway
|
|
138
|
-
const gateway = new GatewayLink(config, log);
|
|
139
|
-
gateway.start();
|
|
140
|
-
// Graceful shutdown
|
|
141
|
-
const shutdown = () => {
|
|
142
|
-
console.log(`\n ${DIM}Shutting down...${RESET}`);
|
|
143
|
-
log.info("Node", "Shutting down");
|
|
144
|
-
gateway.stop();
|
|
145
|
-
process.exit(0);
|
|
146
|
-
};
|
|
147
|
-
process.on("SIGINT", shutdown);
|
|
148
|
-
process.on("SIGTERM", shutdown);
|
|
149
|
-
// Parent-liveness monitor: when launched from OSXNode, OSXNODE_PARENT=1
|
|
150
|
-
// is set. Poll the parent PID — when it disappears, shut down so
|
|
151
|
-
// browser-server and Chrome are cleaned up. Standalone launches are
|
|
152
|
-
// unaffected. Polling is more reliable than stdin-pipe EOF because
|
|
153
|
-
// posix_spawn may leak the pipe's write-end to the child.
|
|
154
|
-
if (process.env.OSXNODE_PARENT) {
|
|
155
|
-
const parentPid = process.ppid;
|
|
156
|
-
const parentCheck = setInterval(() => {
|
|
157
|
-
try {
|
|
158
|
-
process.kill(parentPid, 0); // signal 0 = existence check
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
clearInterval(parentCheck);
|
|
162
|
-
log.info("Node", `Parent (pid ${parentPid}) exited, shutting down`);
|
|
163
|
-
shutdown();
|
|
164
|
-
}
|
|
165
|
-
}, 2000);
|
|
166
|
-
parentCheck.unref(); // don't keep the event loop alive just for this
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
main();
|
|
170
|
-
//# sourceMappingURL=index.js.map
|
|
2
|
+
import{copyFileSync as o,existsSync as e}from"node:fs";import{resolve as n,dirname as s,join as l}from"node:path";import{fileURLToPath as t}from"node:url";import{loadConfig as c}from"./config.js";import{GatewayLink as r}from"./gateway-link.js";import{Logger as a}from"./logger.js";const i="[0m",g="[1m",$="[2m",d=o=>`[38;5;${o}m`,f=205,m=199,p=200,h=197,w=196,u=161;!function(){const y=function(){const o=process.argv.slice(2),e={};for(let n=0;n<o.length;n++){const s=o[n];"--help"===s||"-h"===s?e.help=!0:"--init"===s?e.init=!0:"--config"===s&&o[n+1]?e.configPath=o[++n]:"--ws"===s&&o[n+1]?e.ws=o[++n]:"--name"===s&&o[n+1]?e.name=o[++n]:s.startsWith("--")||e.configPath||(e.configPath=s)}return e}();y.help&&(console.log(""),console.log(` ${d(f)}${g}Hera StandardNode${i}`),console.log(""),console.log(` ${g}Usage:${i} npx tsx src/index.ts [options]`),console.log(""),console.log(` ${g}Options:${i}`),console.log(" --ws <url> WebSocket URL of the Hera gateway"),console.log(" --name <name> Display name for this node (default: hostname)"),console.log(" --config <path> Path to config file (default: config.stdnode.yaml)"),console.log(" --init Copy example config to current directory and exit"),console.log(" --help Show this help message"),console.log(""),console.log(` ${g}WebSocket URL format:${i}`),console.log(" ws[s]://<hostname>:<port><basePath>/ws/nodes"),console.log(""),console.log(` The ${g}<port>${i} is the Nostromo UI port configured on the server`),console.log(` (default 3001). The ${g}<basePath>${i} is the Nostromo base path`),console.log(" (default /nostromo)."),console.log(""),console.log(` ${g}Examples:${i}`),console.log(" --ws ws://localhost:3001/nostromo/ws/nodes"),console.log(" --ws wss://myhost.tail12345.ts.net:3001/nostromo/ws/nodes"),console.log(""),console.log(` ${g}Auto-detect:${i}`),console.log(" If ../config.yaml exists (Hera server config), the node reads"),console.log(" the Nostromo port and basePath from it and auto-configures the"),console.log(" gateway URL. The --ws flag overrides auto-detection."),console.log(""),process.exit(0)),y.init&&(!function(){const c=n("config.stdnode.yaml");if(e(c))return void console.log(`\n ${d(w)}${g}config.stdnode.yaml already exists${i} — not overwriting.\n`);const r=t(import.meta.url),a=s(s(r)),m=l(a,"config.stdnode.example.yaml");e(m)?(o(m,c),console.log(`\n ${d(f)}${g}Created${i} config.stdnode.yaml`),console.log(` ${$}Edit it, then run: hera-stdnode${i}\n`)):console.log(`\n ${d(w)}${g}Example config not found${i} — run from a proper install.\n`)}(),process.exit(0));const N=c(y),x=new a(N.logs.dir);var v,S,P,b;v=N.node.displayName,S=N.node.id,P=N.gateway.url,b=N.browser.enabled?N.browser.controlPort:void 0,console.log(""),console.log(` ${d(m)}${g} ███████╗████████╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗${i}`),console.log(` ${d(m)}${g} ██╔════╝╚══██╔══╝██╔══██╗ ████╗ ██║██╔═══██╗██╔══██╗██╔════╝${i}`),console.log(` ${d(p)}${g} ███████╗ ██║ ██║ ██║ ██╔██╗ ██║██║ ██║██║ ██║█████╗ ${i}`),console.log(` ${d(h)}${g} ╚════██║ ██║ ██║ ██║ ██║╚██╗██║██║ ██║██║ ██║██╔══╝ ${i}`),console.log(` ${d(w)}${g} ███████║ ██║ ██████╔╝ ██║ ╚████║╚██████╔╝██████╔╝███████╗${i}`),console.log(` ${d(u)}${g} ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝${i}`),console.log(""),console.log(` ${d(f)}${g}Hera StandardNode${i}`),console.log(` ${$}${"─".repeat(50)}${i}`),console.log(` ${$}Name${i} ${g}${v}${i}`),console.log(` ${$}ID${i} ${$}${S}${i}`),console.log(` ${$}Gateway${i} ${$}${P}${i}`),b&&console.log(` ${$}Browser${i} ${$}127.0.0.1:${b}${i}`),console.log(""),x.info("Node",`Starting — ${N.node.displayName} (${N.node.id})`),x.info("Node",`Gateway: ${N.gateway.url}`),x.info("Node",`Logs dir: ${N.logs.dir}`);const I=new r(N,x);I.start();const E=()=>{console.log(`\n ${$}Shutting down...${i}`),x.info("Node","Shutting down"),I.stop(),process.exit(0)};if(process.on("SIGINT",E),process.on("SIGTERM",E),process.env.OSXNODE_PARENT){const o=process.ppid,e=setInterval(()=>{try{process.kill(o,0)}catch{clearInterval(e),x.info("Node",`Parent (pid ${o}) exited, shutting down`),E()}},2e3);e.unref()}}();
|
package/dist/logger.js
CHANGED
|
@@ -1,64 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { resolve, join } from "node:path";
|
|
3
|
-
const MAX_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
4
|
-
const MAX_FILES = 9;
|
|
5
|
-
export class Logger {
|
|
6
|
-
dir;
|
|
7
|
-
filePath;
|
|
8
|
-
constructor(logsDir) {
|
|
9
|
-
this.dir = resolve(logsDir);
|
|
10
|
-
if (!existsSync(this.dir)) {
|
|
11
|
-
mkdirSync(this.dir, { recursive: true });
|
|
12
|
-
}
|
|
13
|
-
this.filePath = join(this.dir, "stdnode.log");
|
|
14
|
-
}
|
|
15
|
-
info(tag, msg) {
|
|
16
|
-
this.write("INFO", tag, msg);
|
|
17
|
-
}
|
|
18
|
-
warn(tag, msg) {
|
|
19
|
-
this.write("WARN", tag, msg);
|
|
20
|
-
}
|
|
21
|
-
error(tag, msg) {
|
|
22
|
-
this.write("ERROR", tag, msg);
|
|
23
|
-
}
|
|
24
|
-
debug(tag, msg) {
|
|
25
|
-
this.write("DEBUG", tag, msg);
|
|
26
|
-
}
|
|
27
|
-
write(level, tag, msg) {
|
|
28
|
-
const ts = new Date().toISOString();
|
|
29
|
-
const line = `${ts} [${level}] [${tag}] ${msg}\n`;
|
|
30
|
-
try {
|
|
31
|
-
this.rotate();
|
|
32
|
-
appendFileSync(this.filePath, line, "utf-8");
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// Silently ignore write errors
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
rotate() {
|
|
39
|
-
try {
|
|
40
|
-
if (!existsSync(this.filePath))
|
|
41
|
-
return;
|
|
42
|
-
const stat = statSync(this.filePath);
|
|
43
|
-
if (stat.size < MAX_SIZE)
|
|
44
|
-
return;
|
|
45
|
-
// Shift existing rotated files: stdnode.9.log -> delete, 8->9, ... 1->2
|
|
46
|
-
for (let i = MAX_FILES; i >= 1; i--) {
|
|
47
|
-
const src = join(this.dir, `stdnode.${i}.log`);
|
|
48
|
-
if (!existsSync(src))
|
|
49
|
-
continue;
|
|
50
|
-
if (i === MAX_FILES) {
|
|
51
|
-
// oldest file — just let it be overwritten
|
|
52
|
-
}
|
|
53
|
-
const dst = join(this.dir, `stdnode.${i + 1}.log`);
|
|
54
|
-
renameSync(src, dst);
|
|
55
|
-
}
|
|
56
|
-
// Current -> .1
|
|
57
|
-
renameSync(this.filePath, join(this.dir, "stdnode.1.log"));
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// Ignore rotation errors
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
//# sourceMappingURL=logger.js.map
|
|
1
|
+
import{existsSync as t,mkdirSync as i,appendFileSync as r,renameSync as e,statSync as s}from"node:fs";import{resolve as o,join as h}from"node:path";export class Logger{dir;filePath;constructor(r){this.dir=o(r),t(this.dir)||i(this.dir,{recursive:!0}),this.filePath=h(this.dir,"stdnode.log")}info(t,i){this.write("INFO",t,i)}warn(t,i){this.write("WARN",t,i)}error(t,i){this.write("ERROR",t,i)}debug(t,i){this.write("DEBUG",t,i)}write(t,i,e){const s=`${(new Date).toISOString()} [${t}] [${i}] ${e}\n`;try{this.rotate(),r(this.filePath,s,"utf-8")}catch{}}rotate(){try{if(!t(this.filePath))return;if(s(this.filePath).size<10485760)return;for(let i=9;i>=1;i--){const r=h(this.dir,`stdnode.${i}.log`);if(!t(r))continue;const s=h(this.dir,`stdnode.${i+1}.log`);e(r,s)}e(this.filePath,h(this.dir,"stdnode.1.log"))}catch{}}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hera-al/standardnode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Hera StandardNode — remote execution node for Hera gateway",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "TGP <heralife.dev@gmail.com>",
|
|
@@ -38,9 +38,11 @@
|
|
|
38
38
|
"start": "tsx src/index.ts",
|
|
39
39
|
"dev": "tsx watch src/index.ts",
|
|
40
40
|
"build": "tsc",
|
|
41
|
+
"minify": "find dist -name '*.map' -delete && find dist -name '*.js' -exec terser {} --module --compress --mangle -o {} \\;",
|
|
42
|
+
"build:prod": "rm -rf dist && npm run build && npm run minify",
|
|
41
43
|
"node": "node dist/index.js",
|
|
42
44
|
"help": "tsx src/index.ts --help",
|
|
43
|
-
"prepublishOnly": "npm run build"
|
|
45
|
+
"prepublishOnly": "npm run build:prod"
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
46
48
|
"ws": "^8.18.0",
|