@demicodes/host-local 0.5.0 → 0.6.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 +2 -2
- package/dist/index.mjs +11 -13
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -8,7 +8,7 @@ import { Provider } from "@demicodes/provider";
|
|
|
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
|
}
|
|
@@ -92,7 +92,7 @@ interface LocalAgentServerHandle {
|
|
|
92
92
|
* bridge **on by default**.
|
|
93
93
|
*
|
|
94
94
|
* Owns the Node-only bridge transport (UDS + PATH shims under `stateDir`).
|
|
95
|
-
* AgentServer only receives a generic `
|
|
95
|
+
* AgentServer only receives a generic `prepareShell` hook and exposes
|
|
96
96
|
* `runCommandLine` — it never sees bin dirs or sockets.
|
|
97
97
|
*
|
|
98
98
|
* Default layout under `stateDir` (`~/.demi` or `$DEMI_HOME`):
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, RunCommandLineCommandNotRegisteredError,
|
|
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
7
|
import { resolveDemiHome } from "@demicodes/provider/credentials-pool";
|
|
@@ -92,17 +92,15 @@ function postRun(socketPath, body) {
|
|
|
92
92
|
|
|
93
93
|
async function main() {
|
|
94
94
|
const socketPath = process.env.DEMI_COMMAND_BRIDGE_SOCK
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!socketPath || !commandScopeId) {
|
|
99
|
-
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')
|
|
100
98
|
process.exit(1)
|
|
101
99
|
}
|
|
102
100
|
const name = basename(process.argv[1] || '')
|
|
103
101
|
const args = process.argv.slice(2)
|
|
104
102
|
const stdin = await readStdin()
|
|
105
|
-
const body = JSON.stringify({
|
|
103
|
+
const body = JSON.stringify({ shellId, name, args, cwd: process.cwd(), stdin })
|
|
106
104
|
|
|
107
105
|
const response = await postRun(socketPath, body)
|
|
108
106
|
if (response.statusCode !== 200) {
|
|
@@ -154,7 +152,7 @@ async function handleRun(server, req, res) {
|
|
|
154
152
|
return;
|
|
155
153
|
}
|
|
156
154
|
if (!isRunRequestBody(body)) {
|
|
157
|
-
sendJson(res, 400, { error: "bad request: expected {
|
|
155
|
+
sendJson(res, 400, { error: "bad request: expected { shellId, name, args, cwd, stdin }" });
|
|
158
156
|
return;
|
|
159
157
|
}
|
|
160
158
|
const controller = new AbortController();
|
|
@@ -164,7 +162,7 @@ async function handleRun(server, req, res) {
|
|
|
164
162
|
};
|
|
165
163
|
req.on("close", onClosedEarly);
|
|
166
164
|
try {
|
|
167
|
-
const result = await server.runCommandLine(body.
|
|
165
|
+
const result = await server.runCommandLine(body.shellId, body.name, body.args, {
|
|
168
166
|
cwd: body.cwd,
|
|
169
167
|
stdin: body.stdin,
|
|
170
168
|
signal: controller.signal
|
|
@@ -179,7 +177,7 @@ async function handleRun(server, req, res) {
|
|
|
179
177
|
}
|
|
180
178
|
}
|
|
181
179
|
function statusForError(error) {
|
|
182
|
-
if (error instanceof
|
|
180
|
+
if (error instanceof RunCommandLineShellNotFoundError) return 404;
|
|
183
181
|
if (error instanceof RunCommandLineCommandNotRegisteredError) return 404;
|
|
184
182
|
if (error instanceof RunCommandLineTimeoutError) return 504;
|
|
185
183
|
return 500;
|
|
@@ -187,7 +185,7 @@ function statusForError(error) {
|
|
|
187
185
|
function isRunRequestBody(value) {
|
|
188
186
|
if (value === null || typeof value !== "object") return false;
|
|
189
187
|
const record = value;
|
|
190
|
-
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";
|
|
191
189
|
}
|
|
192
190
|
function readJsonBody(req) {
|
|
193
191
|
return new Promise((resolve, reject) => {
|
|
@@ -579,7 +577,7 @@ async function* streamBytes(stream) {
|
|
|
579
577
|
* bridge **on by default**.
|
|
580
578
|
*
|
|
581
579
|
* Owns the Node-only bridge transport (UDS + PATH shims under `stateDir`).
|
|
582
|
-
* AgentServer only receives a generic `
|
|
580
|
+
* AgentServer only receives a generic `prepareShell` hook and exposes
|
|
583
581
|
* `runCommandLine` — it never sees bin dirs or sockets.
|
|
584
582
|
*
|
|
585
583
|
* Default layout under `stateDir` (`~/.demi` or `$DEMI_HOME`):
|
|
@@ -612,7 +610,7 @@ function createLocalAgentServer(options) {
|
|
|
612
610
|
if (bridgeEnabled && socketPath && stateDir) {
|
|
613
611
|
const bridgeStateDir = stateDir;
|
|
614
612
|
const bridgeSocketPath = socketPath;
|
|
615
|
-
serverOptions.
|
|
613
|
+
serverOptions.prepareShell = async ({ host, agentSessionId, commandNames, shell }) => {
|
|
616
614
|
const shimDir = await materializeCommandBridgeShims({
|
|
617
615
|
host,
|
|
618
616
|
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.6.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.6.0",
|
|
15
|
+
"@demicodes/provider": "^0.6.0",
|
|
16
|
+
"@demicodes/shell": "^0.6.0",
|
|
17
|
+
"@demicodes/utils": "^0.6.0"
|
|
18
18
|
},
|
|
19
19
|
"license": "Apache-2.0",
|
|
20
20
|
"main": "./dist/index.mjs",
|