@demicodes/host-local 0.10.3 → 0.11.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/dist/index.d.mts +3 -8
- package/dist/index.mjs +26 -30
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { AgentHarness, AgentServer, AgentServerSessionOptions } from "@demicodes/agent";
|
|
2
|
+
import { resolveDemiHome } from "@demicodes/provider/credentials-pool";
|
|
2
3
|
import { BashEnvironmentOptions, Host, HostFileSystem, HostProcess, HostStore } from "@demicodes/shell";
|
|
3
4
|
import { Provider } from "@demicodes/provider";
|
|
4
|
-
|
|
5
5
|
//#region src/command-bridge.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Dispatch script shared by every generated command-name symlink.
|
|
8
8
|
* Reads the invoked name from the symlink path, applies the stdin grace
|
|
9
9
|
* contract from docs/command-bridge.md, and POSTs /run on the UDS socket.
|
|
10
10
|
*/
|
|
11
|
-
declare const COMMAND_BRIDGE_SHIM_SOURCE = "#!/usr/bin/env node\nconst { request } = require('node:http')\nconst { basename } = require('node:path')\n\n// Stdin policy (docs/command-bridge.md): if no byte arrives within the grace\n// window, proceed with empty stdin. Once any byte arrives, clear the timer\n// and read until EOF with no time cap. Late data after empty-stdin dispatch\n// is reported on stderr and not pretended to have been delivered.\nconst STDIN_GRACE_MS = 300\n\nfunction readStdin() {\n if (process.stdin.isTTY) return Promise.resolve('')\n return new Promise((resolve, reject) => {\n const chunks = []\n let resolved = false\n let timedOut = false\n let timer\n const cleanup = () => {\n clearTimeout(timer)\n process.stdin.removeListener('data', onData)\n process.stdin.removeListener('end', onEnd)\n process.stdin.removeListener('error', onError)\n }\n const onData = (chunk) => {\n if (timedOut) {\n process.stderr.write(\n 'command bridge: ' + chunk.length + ' byte(s) of stdin arrived after the ' + STDIN_GRACE_MS +\n 'ms grace period elapsed; the command already ran with empty stdin\\n',\n )\n cleanup()\n return\n }\n clearTimeout(timer)\n chunks.push(chunk)\n }\n const onEnd = () => {\n if (timedOut) {\n cleanup()\n return\n }\n resolved = true\n cleanup()\n resolve(Buffer.concat(chunks).toString('utf8'))\n }\n const onError = (error) => {\n if (resolved || timedOut) return\n resolved = true\n cleanup()\n reject(error)\n }\n timer = setTimeout(() => {\n timedOut = true\n resolved = true\n resolve('')\n }, STDIN_GRACE_MS)\n process.stdin.on('data', onData)\n process.stdin.on('end', onEnd)\n process.stdin.on('error', onError)\n })\n}\n\nfunction postRun(socketPath, body) {\n return new Promise((resolve, reject) => {\n const req = request(\n { socketPath, path: '/run', method: 'POST', headers: { 'content-type': 'application/json' } },\n (res) => {\n const chunks = []\n res.on('data', (chunk) => chunks.push(chunk))\n res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: Buffer.concat(chunks).toString('utf8') }))\n },\n )\n req.on('error', reject)\n req.end(body)\n })\n}\n\nasync function main() {\n const socketPath = process.env.DEMI_COMMAND_BRIDGE_SOCK\n
|
|
11
|
+
declare const COMMAND_BRIDGE_SHIM_SOURCE = "#!/usr/bin/env node\nconst { request } = require('node:http')\nconst { basename } = require('node:path')\n\n// Stdin policy (docs/command-bridge.md): if no byte arrives within the grace\n// window, proceed with empty stdin. Once any byte arrives, clear the timer\n// and read until EOF with no time cap. Late data after empty-stdin dispatch\n// is reported on stderr and not pretended to have been delivered.\nconst STDIN_GRACE_MS = 300\n\nfunction readStdin() {\n if (process.stdin.isTTY) return Promise.resolve('')\n return new Promise((resolve, reject) => {\n const chunks = []\n let resolved = false\n let timedOut = false\n let timer\n const cleanup = () => {\n clearTimeout(timer)\n process.stdin.removeListener('data', onData)\n process.stdin.removeListener('end', onEnd)\n process.stdin.removeListener('error', onError)\n }\n const onData = (chunk) => {\n if (timedOut) {\n process.stderr.write(\n 'command bridge: ' + chunk.length + ' byte(s) of stdin arrived after the ' + STDIN_GRACE_MS +\n 'ms grace period elapsed; the command already ran with empty stdin\\n',\n )\n cleanup()\n return\n }\n clearTimeout(timer)\n chunks.push(chunk)\n }\n const onEnd = () => {\n if (timedOut) {\n cleanup()\n return\n }\n resolved = true\n cleanup()\n resolve(Buffer.concat(chunks).toString('utf8'))\n }\n const onError = (error) => {\n if (resolved || timedOut) return\n resolved = true\n cleanup()\n reject(error)\n }\n timer = setTimeout(() => {\n timedOut = true\n resolved = true\n resolve('')\n }, STDIN_GRACE_MS)\n process.stdin.on('data', onData)\n process.stdin.on('end', onEnd)\n process.stdin.on('error', onError)\n })\n}\n\nfunction postRun(socketPath, body) {\n return new Promise((resolve, reject) => {\n const req = request(\n { socketPath, path: '/run', method: 'POST', headers: { 'content-type': 'application/json' } },\n (res) => {\n const chunks = []\n res.on('data', (chunk) => chunks.push(chunk))\n res.on('end', () => resolve({ statusCode: res.statusCode || 0, body: Buffer.concat(chunks).toString('utf8') }))\n },\n )\n req.on('error', reject)\n req.end(body)\n })\n}\n\nasync function main() {\n const socketPath = process.env.DEMI_COMMAND_BRIDGE_SOCK\n const shellId = process.env.DEMI_SHELL_ID\n if (!socketPath || !shellId) {\n process.stderr.write('command bridge: DEMI_COMMAND_BRIDGE_SOCK / shell id not set in this shell\\n')\n process.exit(1)\n }\n const name = basename(process.argv[1] || '')\n const args = process.argv.slice(2)\n const stdin = await readStdin()\n const body = JSON.stringify({ shellId, name, args, cwd: process.cwd(), stdin })\n\n const response = await postRun(socketPath, body)\n if (response.statusCode !== 200) {\n let message = response.body\n try {\n message = JSON.parse(response.body).error || message\n } catch {}\n process.stderr.write('command bridge: ' + message + '\\n')\n process.exit(1)\n }\n const result = JSON.parse(response.body)\n if (result.stdout) {\n // Binary final streams arrive base64-encoded; write raw bytes so external\n // pipes (demi read a.png | ffmpeg -i - ...) stay byte-clean.\n process.stdout.write(result.stdoutEncoding === 'base64' ? Buffer.from(result.stdout, 'base64') : result.stdout)\n }\n if (result.stderr) process.stderr.write(result.stderr)\n process.exit(result.exitCode)\n}\n\nmain().catch((error) => {\n process.stderr.write('command bridge: ' + (error && error.message ? error.message : String(error)) + '\\n')\n process.exit(1)\n})\n";
|
|
12
12
|
interface CommandBridgeOptions {
|
|
13
13
|
socketPath: string;
|
|
14
14
|
}
|
|
@@ -42,11 +42,6 @@ declare function bridgeBinDirFor(stateDir: string): string;
|
|
|
42
42
|
declare function materializeCommandBridgeShims(options: MaterializeCommandBridgeShimsOptions): Promise<string>;
|
|
43
43
|
//#endregion
|
|
44
44
|
//#region src/demi-home.d.ts
|
|
45
|
-
/**
|
|
46
|
-
* Demi local state root (not the workspace cwd).
|
|
47
|
-
* Override with `DEMI_HOME` or `createLocalAgentServer({ stateDir })`, else `~/.demi`.
|
|
48
|
-
*/
|
|
49
|
-
declare function resolveDemiHome(explicit?: string): string;
|
|
50
45
|
/** Fixed layout: `<stateDir>/bridges/<id>.sock` (short id for macOS AF_UNIX limits). */
|
|
51
46
|
declare function defaultBridgeSocketPath(stateDir: string, serverId?: string): string;
|
|
52
47
|
//#endregion
|
|
@@ -97,7 +92,7 @@ interface LocalAgentServerHandle {
|
|
|
97
92
|
* bridge **on by default**.
|
|
98
93
|
*
|
|
99
94
|
* Owns the Node-only bridge transport (UDS + PATH shims under `stateDir`).
|
|
100
|
-
* AgentServer only receives a generic `
|
|
95
|
+
* AgentServer only receives a generic `prepareShell` hook and exposes
|
|
101
96
|
* `runCommandLine` — it never sees bin dirs or sockets.
|
|
102
97
|
*
|
|
103
98
|
* Default layout under `stateDir` (`~/.demi` or `$DEMI_HOME`):
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
-
import { AgentServer,
|
|
4
|
+
import { AgentServer, RunCommandLineCommandNotRegisteredError, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError } from "@demicodes/agent";
|
|
5
5
|
import { encodeUtf8, errorMessage, isFileNotFoundError, parsePortableJson, stringifyPortableJson } from "@demicodes/utils";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveDemiHome } from "@demicodes/provider/credentials-pool";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
+
import { homedir } from "node:os";
|
|
9
10
|
import { appendFile, chmod, cp, link, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, utimes, writeFile } from "node:fs/promises";
|
|
10
11
|
//#region src/command-bridge.ts
|
|
11
12
|
/**
|
|
@@ -91,16 +92,15 @@ function postRun(socketPath, body) {
|
|
|
91
92
|
|
|
92
93
|
async function main() {
|
|
93
94
|
const socketPath = process.env.DEMI_COMMAND_BRIDGE_SOCK
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
process.stderr.write('command bridge: DEMI_COMMAND_BRIDGE_SOCK / session id not set in this shell\\n')
|
|
95
|
+
const shellId = process.env.DEMI_SHELL_ID
|
|
96
|
+
if (!socketPath || !shellId) {
|
|
97
|
+
process.stderr.write('command bridge: DEMI_COMMAND_BRIDGE_SOCK / shell id not set in this shell\\n')
|
|
98
98
|
process.exit(1)
|
|
99
99
|
}
|
|
100
100
|
const name = basename(process.argv[1] || '')
|
|
101
101
|
const args = process.argv.slice(2)
|
|
102
102
|
const stdin = await readStdin()
|
|
103
|
-
const body = JSON.stringify({
|
|
103
|
+
const body = JSON.stringify({ shellId, name, args, cwd: process.cwd(), stdin })
|
|
104
104
|
|
|
105
105
|
const response = await postRun(socketPath, body)
|
|
106
106
|
if (response.statusCode !== 200) {
|
|
@@ -112,7 +112,11 @@ async function main() {
|
|
|
112
112
|
process.exit(1)
|
|
113
113
|
}
|
|
114
114
|
const result = JSON.parse(response.body)
|
|
115
|
-
if (result.stdout)
|
|
115
|
+
if (result.stdout) {
|
|
116
|
+
// Binary final streams arrive base64-encoded; write raw bytes so external
|
|
117
|
+
// pipes (demi read a.png | ffmpeg -i - ...) stay byte-clean.
|
|
118
|
+
process.stdout.write(result.stdoutEncoding === 'base64' ? Buffer.from(result.stdout, 'base64') : result.stdout)
|
|
119
|
+
}
|
|
116
120
|
if (result.stderr) process.stderr.write(result.stderr)
|
|
117
121
|
process.exit(result.exitCode)
|
|
118
122
|
}
|
|
@@ -148,7 +152,7 @@ async function handleRun(server, req, res) {
|
|
|
148
152
|
return;
|
|
149
153
|
}
|
|
150
154
|
if (!isRunRequestBody(body)) {
|
|
151
|
-
sendJson(res, 400, { error: "bad request: expected {
|
|
155
|
+
sendJson(res, 400, { error: "bad request: expected { shellId, name, args, cwd, stdin }" });
|
|
152
156
|
return;
|
|
153
157
|
}
|
|
154
158
|
const controller = new AbortController();
|
|
@@ -158,7 +162,7 @@ async function handleRun(server, req, res) {
|
|
|
158
162
|
};
|
|
159
163
|
req.on("close", onClosedEarly);
|
|
160
164
|
try {
|
|
161
|
-
const result = await server.runCommandLine(body.
|
|
165
|
+
const result = await server.runCommandLine(body.shellId, body.name, body.args, {
|
|
162
166
|
cwd: body.cwd,
|
|
163
167
|
stdin: body.stdin,
|
|
164
168
|
signal: controller.signal
|
|
@@ -173,14 +177,15 @@ async function handleRun(server, req, res) {
|
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
179
|
function statusForError(error) {
|
|
176
|
-
if (error instanceof
|
|
180
|
+
if (error instanceof RunCommandLineShellNotFoundError) return 404;
|
|
181
|
+
if (error instanceof RunCommandLineCommandNotRegisteredError) return 404;
|
|
177
182
|
if (error instanceof RunCommandLineTimeoutError) return 504;
|
|
178
183
|
return 500;
|
|
179
184
|
}
|
|
180
185
|
function isRunRequestBody(value) {
|
|
181
186
|
if (value === null || typeof value !== "object") return false;
|
|
182
187
|
const record = value;
|
|
183
|
-
return typeof record.
|
|
188
|
+
return typeof record.shellId === "string" && typeof record.name === "string" && Array.isArray(record.args) && record.args.every((arg) => typeof arg === "string") && typeof record.cwd === "string" && typeof record.stdin === "string";
|
|
184
189
|
}
|
|
185
190
|
function readJsonBody(req) {
|
|
186
191
|
return new Promise((resolve, reject) => {
|
|
@@ -222,7 +227,7 @@ const DISPATCH_PACKAGE_JSON = "{\"type\":\"commonjs\"}\n";
|
|
|
222
227
|
* component under Host.fs, so it is validated here rather than trusted.
|
|
223
228
|
*/
|
|
224
229
|
function assertPathSafeSessionId(agentSessionId) {
|
|
225
|
-
if (agentSessionId.length === 0 || agentSessionId.includes("/") || agentSessionId.includes("\\") || agentSessionId === "..") throw new Error(`Command bridge: agentSessionId "${agentSessionId}" is not safe to use as a path segment`);
|
|
230
|
+
if (agentSessionId.length === 0 || agentSessionId.includes("\0") || agentSessionId.includes("/") || agentSessionId.includes("\\") || agentSessionId === "." || agentSessionId === "..") throw new Error(`Command bridge: agentSessionId "${agentSessionId}" is not safe to use as a path segment`);
|
|
226
231
|
}
|
|
227
232
|
/** Fixed relative layout under stateDir. */
|
|
228
233
|
function bridgeBinDirFor(stateDir) {
|
|
@@ -256,16 +261,6 @@ async function materializeCommandBridgeShims(options) {
|
|
|
256
261
|
}
|
|
257
262
|
//#endregion
|
|
258
263
|
//#region src/demi-home.ts
|
|
259
|
-
/**
|
|
260
|
-
* Demi local state root (not the workspace cwd).
|
|
261
|
-
* Override with `DEMI_HOME` or `createLocalAgentServer({ stateDir })`, else `~/.demi`.
|
|
262
|
-
*/
|
|
263
|
-
function resolveDemiHome(explicit) {
|
|
264
|
-
if (explicit && explicit.trim()) return resolve(explicit.trim());
|
|
265
|
-
const fromEnv = process.env.DEMI_HOME;
|
|
266
|
-
if (fromEnv && fromEnv.trim()) return resolve(fromEnv.trim());
|
|
267
|
-
return join(homedir(), ".demi");
|
|
268
|
-
}
|
|
269
264
|
/** Fixed layout: `<stateDir>/bridges/<id>.sock` (short id for macOS AF_UNIX limits). */
|
|
270
265
|
function defaultBridgeSocketPath(stateDir, serverId = randomUUID().replace(/-/g, "").slice(0, 12)) {
|
|
271
266
|
return join(stateDir, "bridges", `${serverId}.sock`);
|
|
@@ -279,7 +274,8 @@ var LocalHostStore = class {
|
|
|
279
274
|
}
|
|
280
275
|
async readJson(key) {
|
|
281
276
|
try {
|
|
282
|
-
|
|
277
|
+
const content = await readFile(this.pathForKey(key), "utf8");
|
|
278
|
+
return parsePortableJson(content);
|
|
283
279
|
} catch (error) {
|
|
284
280
|
if (isFileNotFoundError(error)) return null;
|
|
285
281
|
throw error;
|
|
@@ -501,16 +497,16 @@ var LocalHostFileSystem = class {
|
|
|
501
497
|
return readdir(target);
|
|
502
498
|
}
|
|
503
499
|
async mkdir(path, options) {
|
|
504
|
-
await mkdir(this.resolvePath(path, options?.cwd), { recursive: options?.recursive });
|
|
500
|
+
await mkdir(this.resolvePath(path, options?.cwd), { recursive: options?.recursive === true });
|
|
505
501
|
}
|
|
506
502
|
async rm(path, options) {
|
|
507
503
|
await rm(this.resolvePath(path, options?.cwd), {
|
|
508
|
-
recursive: options?.recursive,
|
|
509
|
-
force: options?.force
|
|
504
|
+
recursive: options?.recursive === true,
|
|
505
|
+
force: options?.force === true
|
|
510
506
|
});
|
|
511
507
|
}
|
|
512
508
|
async cp(path, destination, options) {
|
|
513
|
-
await cp(this.resolvePath(path, options?.cwd), this.resolvePath(destination, options?.cwd), { recursive: options?.recursive });
|
|
509
|
+
await cp(this.resolvePath(path, options?.cwd), this.resolvePath(destination, options?.cwd), { recursive: options?.recursive === true });
|
|
514
510
|
}
|
|
515
511
|
async mv(path, destination, options) {
|
|
516
512
|
await rename(this.resolvePath(path, options?.cwd), this.resolvePath(destination, options?.cwd));
|
|
@@ -582,7 +578,7 @@ async function* streamBytes(stream) {
|
|
|
582
578
|
* bridge **on by default**.
|
|
583
579
|
*
|
|
584
580
|
* Owns the Node-only bridge transport (UDS + PATH shims under `stateDir`).
|
|
585
|
-
* AgentServer only receives a generic `
|
|
581
|
+
* AgentServer only receives a generic `prepareShell` hook and exposes
|
|
586
582
|
* `runCommandLine` — it never sees bin dirs or sockets.
|
|
587
583
|
*
|
|
588
584
|
* Default layout under `stateDir` (`~/.demi` or `$DEMI_HOME`):
|
|
@@ -615,7 +611,7 @@ function createLocalAgentServer(options) {
|
|
|
615
611
|
if (bridgeEnabled && socketPath && stateDir) {
|
|
616
612
|
const bridgeStateDir = stateDir;
|
|
617
613
|
const bridgeSocketPath = socketPath;
|
|
618
|
-
serverOptions.
|
|
614
|
+
serverOptions.prepareShell = async ({ host, agentSessionId, commandNames, shell }) => {
|
|
619
615
|
const shimDir = await materializeCommandBridgeShims({
|
|
620
616
|
host,
|
|
621
617
|
agentSessionId,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demicodes/host-local",
|
|
3
3
|
"description": "Node LocalHost and open-box local AgentServer assembly (command bridge on by default).",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@demicodes/agent": "^0.
|
|
15
|
-
"@demicodes/provider": "^0.
|
|
16
|
-
"@demicodes/shell": "^0.
|
|
17
|
-
"@demicodes/utils": "^0.
|
|
14
|
+
"@demicodes/agent": "^0.11.0",
|
|
15
|
+
"@demicodes/provider": "^0.11.0",
|
|
16
|
+
"@demicodes/shell": "^0.11.0",
|
|
17
|
+
"@demicodes/utils": "^0.11.0"
|
|
18
18
|
},
|
|
19
19
|
"license": "Apache-2.0",
|
|
20
20
|
"main": "./dist/index.mjs",
|