@hera-al/standardnode 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TGP / Hera Artificial Life
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # @hera-al/standardnode
2
+
3
+ > **Part of [Hera Artificial Life](https://github.com/hera-artificial-life/hera-al)** — an opinionated AI assistant platform that runs locally on your machine. This package is the remote execution agent that connects your machines to the Hera gateway.
4
+
5
+ Lightweight remote execution node that connects to a Hera gateway via WebSocket. Once paired, it can execute shell commands and proxy browser automation requests dispatched by the central server.
6
+
7
+ ## Features
8
+
9
+ - **WebSocket connection** to Hera gateway (Nostromo) with automatic reconnect
10
+ - **Signature-based pairing** — each node has a unique cryptographic identity, approved by an admin
11
+ - **Shell execution** — run commands remotely with optional allowlist and timeout
12
+ - **Browser proxy** — forward requests to a local [`@hera-al/browser-server`](https://www.npmjs.com/package/@hera-al/browser-server) instance (optional)
13
+ - **Auto-detection** — discovers the gateway URL automatically when running inside the Hera project
14
+ - **YAML configuration** — auto-generated on first run, CLI flags override and persist
15
+ - **File logging** — append-only with automatic rotation at 10 MB
16
+ - **Multi-platform** — macOS, Linux, Windows (Node.js ≥ 18)
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install @hera-al/standardnode
22
+ ```
23
+
24
+ ### With browser automation (optional)
25
+
26
+ ```bash
27
+ npm install @hera-al/standardnode @hera-al/browser-server
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ ### CLI
33
+
34
+ ```bash
35
+ npx hera-stdnode --ws ws://localhost:3001/nostromo/ws/nodes
36
+ ```
37
+
38
+ All CLI flags:
39
+
40
+ | Flag | Default | Description |
41
+ | ---------- | ---------------------- | ------------------------------------ |
42
+ | `--ws` | *(auto-detected)* | WebSocket URL of the Hera gateway |
43
+ | `--name` | *(OS hostname)* | Display name for this node |
44
+ | `--config` | `config.stdnode.yaml` | Path to configuration file |
45
+ | `--init` | — | Copy example config to current directory and exit |
46
+ | `--help` | — | Show help message |
47
+
48
+ Values passed via CLI are persisted to the config file.
49
+
50
+ ### First run
51
+
52
+ ```bash
53
+ # Option A: just launch — config is auto-generated with sensible defaults
54
+ hera-stdnode --ws wss://yourserver:3001/nostromo/ws/nodes
55
+
56
+ # Option B: generate a commented config first, edit it, then launch
57
+ hera-stdnode --init
58
+ # → creates config.stdnode.yaml in the current directory
59
+ vi config.stdnode.yaml
60
+ hera-stdnode
61
+ ```
62
+
63
+ ### From source
64
+
65
+ ```bash
66
+ git clone <repo>
67
+ cd standardnode
68
+ npm install
69
+ npm start -- --ws ws://localhost:3001/nostromo/ws/nodes
70
+ ```
71
+
72
+ ## WebSocket URL format
73
+
74
+ ```
75
+ ws[s]://<hostname>:<port><basePath>/ws/nodes
76
+ ```
77
+
78
+ | Segment | Description | Default |
79
+ | ------------ | ----------------------------------------------- | ------------ |
80
+ | `<hostname>` | Server address or Tailscale DNS name | `localhost` |
81
+ | `<port>` | Nostromo UI port configured on the Hera server | `3001` |
82
+ | `<basePath>` | Nostromo base path configured on the Hera server| `/nostromo` |
83
+
84
+ Use `wss://` when connecting through Tailscale Serve or any TLS-terminated proxy.
85
+
86
+ ## Configuration
87
+
88
+ On first run a `config.stdnode.yaml` is created with a unique node ID and cryptographic signature. You can also copy the bundled `config.stdnode.example.yaml` as a starting point.
89
+
90
+ ### node
91
+
92
+ | Key | Type | Default | Description |
93
+ | ------------- | ------ | -------------- | ------------------------------------ |
94
+ | `id` | string | *(generated)* | Unique node identifier (UUID) |
95
+ | `displayName` | string | *(hostname)* | Human-readable name shown in Nostromo|
96
+
97
+ ### gateway
98
+
99
+ | Key | Type | Default | Description |
100
+ | ------------- | ------- | -------------------------------------- | -------------------------------- |
101
+ | `enabled` | boolean | `true` | Enable gateway connection |
102
+ | `url` | string | `ws://localhost:3001/nostromo/ws/nodes` | WebSocket endpoint |
103
+ | `token` | string | `""` | Authentication token (reserved) |
104
+ | `signature` | string | *(generated)* | Node signature for pairing |
105
+ | `reconnectMs` | number | `5000` | Reconnect delay in milliseconds |
106
+
107
+ ### commands.shell
108
+
109
+ | Key | Type | Default | Description |
110
+ | ----------- | -------- | ------- | --------------------------------------------- |
111
+ | `enabled` | boolean | `true` | Enable shell command execution |
112
+ | `allowlist` | string[] | `[]` | If non-empty, only these commands are allowed |
113
+ | `timeout` | number | `30000` | Command timeout in milliseconds |
114
+
115
+ ### browser
116
+
117
+ | Key | Type | Default | Description |
118
+ | ---------------- | -------- | ------- | ---------------------------------------------- |
119
+ | `enabled` | boolean | `false` | Enable browser automation proxy |
120
+ | `controlPort` | number | `3002` | Port for the browser HTTP control server |
121
+ | `headless` | boolean | `false` | Run Chrome in headless mode |
122
+ | `noSandbox` | boolean | `false` | Disable Chrome sandbox (CI/Docker) |
123
+ | `attachOnly` | boolean | `false` | Only attach to an already-running Chrome |
124
+ | `executablePath` | string | — | Custom Chrome/Brave/Edge executable path |
125
+ | `allowProfiles` | string[] | `[]` | If non-empty, only these profiles are allowed |
126
+
127
+ ### logs
128
+
129
+ | Key | Type | Default | Description |
130
+ | ----- | ------ | ---------------- | ------------------------------ |
131
+ | `dir` | string | `./logs-stdnode` | Directory for log files |
132
+
133
+ Log files are written as `stdnode.log` with rotation at 10 MB (up to 9 rotated files).
134
+
135
+ ## Pairing flow
136
+
137
+ ```
138
+ ┌──────────┐ ┌──────────┐ ┌───────────┐
139
+ │ Node │──hello──▶│ Nostromo │ │ Admin │
140
+ │ │ │ (gateway)│◀─approve─│ (web UI) │
141
+ │ │◀─status─│ │ │ │
142
+ └──────────┘ └──────────┘ └───────────┘
143
+ ```
144
+
145
+ 1. Node connects and sends a `hello` message with ID, signature, and capabilities
146
+ 2. Nostromo shows the node as **pending** in the admin UI
147
+ 3. An admin approves (or revokes) the node
148
+ 4. Once approved, the node starts receiving and executing commands
149
+ 5. If the connection drops, the node reconnects automatically after `reconnectMs`
150
+
151
+ ## Supported commands
152
+
153
+ | Command | Description |
154
+ | --------------- | --------------------------------------------------------- |
155
+ | `shell.run` | Execute a shell command (`cmd` + `args` array) |
156
+ | `shell.which` | Resolve a binary path |
157
+ | `browser.proxy` | Forward an HTTP request to the local browser-server |
158
+
159
+ ### shell.run
160
+
161
+ ```json
162
+ {
163
+ "cmd": "/usr/bin/ls",
164
+ "args": ["-la", "/tmp"],
165
+ "cwd": "/home/user",
166
+ "timeout": 10000,
167
+ "env": { "NODE_ENV": "production" }
168
+ }
169
+ ```
170
+
171
+ Returns `{ stdout, stderr, exitCode }`.
172
+
173
+ > **Security:** Commands are spawned with `shell: false` — no shell injection possible. Use the `allowlist` to restrict which binaries can be executed.
174
+
175
+ ### browser.proxy
176
+
177
+ Requires `@hera-al/browser-server` installed and `browser.enabled: true` in config.
178
+
179
+ ```json
180
+ {
181
+ "method": "GET",
182
+ "path": "/snapshot",
183
+ "query": { "mode": "text" },
184
+ "profile": "default",
185
+ "timeoutMs": 30000
186
+ }
187
+ ```
188
+
189
+ Returns the browser-server response. File results (screenshots, PDFs) are automatically base64-encoded.
190
+
191
+ ## Architecture
192
+
193
+ ```
194
+ ┌─────────────────────────────────────────────────┐
195
+ │ Hera Gateway (Nostromo) │
196
+ └────────────────────┬────────────────────────────┘
197
+ │ WebSocket
198
+ ┌──────────┴──────────┐
199
+ │ StandardNode │
200
+ │ ┌────────────────┐ │
201
+ │ │ Gateway Link │ │ ← connection, pairing, heartbeat
202
+ │ └───────┬────────┘ │
203
+ │ ┌─────┴─────┐ │
204
+ │ │ │ │
205
+ │ Shell Browser │
206
+ │ Runner Proxy │
207
+ │ │ │ │
208
+ │ ▼ ▼ │
209
+ │ spawn() fetch() │ ← local processes / browser-server
210
+ └─────────────────────┘
211
+ ```
212
+
213
+ - **Gateway Link** — WebSocket client with reconnect, heartbeat (30s), and command dispatch
214
+ - **Shell Runner** — secure process spawning with allowlist, timeout, env isolation
215
+ - **Browser Proxy** — HTTP proxy to local `@hera-al/browser-server` with file detection and profile filtering
216
+
217
+ ## Auto-detection
218
+
219
+ When running inside the Hera project directory (i.e. `standardnode/` is a subfolder), the node reads `../config.yaml` on first run and auto-configures the gateway URL from the Nostromo settings. The `--ws` flag always takes priority.
220
+
221
+ ## NPM scripts
222
+
223
+ | Script | Description |
224
+ | --------------- | -------------------------------------------- |
225
+ | `npm start` | Run via tsx (development, no build needed) |
226
+ | `npm run dev` | Run with file watcher (hot reload) |
227
+ | `npm run build` | Compile TypeScript to `dist/` |
228
+ | `npm run node` | Run the compiled build (`node dist/index.js`)|
229
+ | `npm run help` | Show CLI help |
230
+
231
+ All scripts forward extra flags: `npm start -- --ws <url> --name <name>`.
232
+
233
+ ## Requirements
234
+
235
+ - Node.js ≥ 18
236
+ - macOS, Linux, or Windows
237
+ - A running Hera gateway with Nostromo enabled
238
+
239
+ ## License
240
+
241
+ [MIT](./LICENSE) — © 2026 TGP / Hera Artificial Life
@@ -0,0 +1,44 @@
1
+ # StandardNode configuration
2
+ # Copy this file to config.stdnode.yaml and adjust as needed.
3
+
4
+ node:
5
+ # Auto-generated on first run if left empty
6
+ id: ""
7
+ # Defaults to OS hostname if left empty
8
+ displayName: ""
9
+
10
+ gateway:
11
+ enabled: true
12
+ # WebSocket URL of the Hera gateway (Nostromo)
13
+ url: ws://localhost:3001/nostromo/ws/nodes
14
+ token: ""
15
+ # Auto-generated on first run if left empty
16
+ signature: ""
17
+ reconnectMs: 5000
18
+
19
+ logs:
20
+ # Directory for log files (created automatically)
21
+ dir: ./logs-stdnode
22
+
23
+ commands:
24
+ shell:
25
+ enabled: true
26
+ # If non-empty, only these commands are allowed
27
+ allowlist: []
28
+ timeout: 30000
29
+
30
+ # Browser automation via CDP (Chrome DevTools Protocol)
31
+ browser:
32
+ enabled: false
33
+ # Port for the browser HTTP control server
34
+ controlPort: 3002
35
+ # Run Chrome in headless mode
36
+ headless: false
37
+ # Disable Chrome sandbox (needed in some Linux environments)
38
+ noSandbox: false
39
+ # Only attach to an already-running Chrome (don't launch one)
40
+ attachOnly: false
41
+ # Custom Chrome/Brave/Edge executable path
42
+ # executablePath: /usr/bin/google-chrome
43
+ # If non-empty, only these browser profiles are allowed
44
+ allowProfiles: []
@@ -0,0 +1,22 @@
1
+ import type { Logger } from "../logger.js";
2
+ export interface BrowserProxyParams {
3
+ method?: string;
4
+ path?: string;
5
+ query?: Record<string, string | number | boolean | null | undefined>;
6
+ body?: unknown;
7
+ timeoutMs?: number;
8
+ profile?: string;
9
+ }
10
+ export interface BrowserProxyFile {
11
+ path: string;
12
+ base64: string;
13
+ mimeType?: string;
14
+ }
15
+ export interface BrowserProxyResult {
16
+ result: unknown;
17
+ files?: BrowserProxyFile[];
18
+ }
19
+ export declare function createBrowserProxy(controlPort: number, allowProfiles: string[], log: Logger): {
20
+ handleProxy: (params: BrowserProxyParams) => Promise<BrowserProxyResult>;
21
+ };
22
+ //# sourceMappingURL=browser-proxy.d.ts.map
@@ -0,0 +1,164 @@
1
+ import { readFile, stat } from "node:fs/promises";
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
@@ -0,0 +1,25 @@
1
+ import type { NodeConfig } from "../config.js";
2
+ import type { Logger } from "../logger.js";
3
+ export interface ShellRunParams {
4
+ cmd: string;
5
+ args?: string[];
6
+ cwd?: string;
7
+ timeout?: number;
8
+ env?: Record<string, string>;
9
+ }
10
+ export interface ShellRunResult {
11
+ stdout: string;
12
+ stderr: string;
13
+ exitCode: number | null;
14
+ }
15
+ export interface ShellWhichParams {
16
+ cmd: string;
17
+ }
18
+ export interface ShellWhichResult {
19
+ path: string | null;
20
+ }
21
+ export declare function createShellCommands(config: NodeConfig, log: Logger): {
22
+ shellRun: (params: ShellRunParams) => Promise<ShellRunResult>;
23
+ shellWhich: (params: ShellWhichParams) => Promise<ShellWhichResult>;
24
+ };
25
+ //# sourceMappingURL=shell.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { spawn, execFile } from "node:child_process";
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
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ declare const ConfigSchema: z.ZodObject<{
3
+ node: z.ZodObject<{
4
+ id: z.ZodDefault<z.ZodString>;
5
+ displayName: z.ZodDefault<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ gateway: z.ZodObject<{
8
+ enabled: z.ZodDefault<z.ZodBoolean>;
9
+ url: z.ZodDefault<z.ZodString>;
10
+ token: z.ZodDefault<z.ZodString>;
11
+ signature: z.ZodDefault<z.ZodString>;
12
+ reconnectMs: z.ZodDefault<z.ZodNumber>;
13
+ }, z.core.$strip>;
14
+ logs: z.ZodDefault<z.ZodObject<{
15
+ dir: z.ZodDefault<z.ZodString>;
16
+ }, z.core.$strip>>;
17
+ commands: z.ZodObject<{
18
+ shell: z.ZodObject<{
19
+ enabled: z.ZodDefault<z.ZodBoolean>;
20
+ allowlist: z.ZodDefault<z.ZodArray<z.ZodString>>;
21
+ timeout: z.ZodDefault<z.ZodNumber>;
22
+ }, z.core.$strip>;
23
+ }, z.core.$strip>;
24
+ browser: z.ZodDefault<z.ZodObject<{
25
+ enabled: z.ZodDefault<z.ZodBoolean>;
26
+ controlPort: z.ZodDefault<z.ZodNumber>;
27
+ headless: z.ZodDefault<z.ZodBoolean>;
28
+ noSandbox: z.ZodDefault<z.ZodBoolean>;
29
+ attachOnly: z.ZodDefault<z.ZodBoolean>;
30
+ executablePath: z.ZodOptional<z.ZodString>;
31
+ allowProfiles: z.ZodDefault<z.ZodArray<z.ZodString>>;
32
+ }, z.core.$strip>>;
33
+ }, z.core.$strip>;
34
+ export type NodeConfig = z.infer<typeof ConfigSchema>;
35
+ export declare function loadConfig(opts?: {
36
+ configPath?: string;
37
+ ws?: string;
38
+ name?: string;
39
+ }): NodeConfig;
40
+ export {};
41
+ //# sourceMappingURL=config.d.ts.map