From 1bc18d2acce1eb4def9e58fba3acd4f9859ef1cf Mon Sep 17 00:00:00 2001 From: Peter Isberg Date: Thu, 18 Jun 2026 21:09:51 +0200 Subject: [PATCH] Fix dropped stderr, bind exec server to localhost, repair typecheck - Exec endpoint merged stdout+stderr so error output is visible (was stdout-only) - Bind Bun.serve to 127.0.0.1 by default (HOST overridable); avoids exposing arbitrary command execution to the LAN - Add "types": ["bun"] to tsconfig so the project typechecks (0 tsc errors) - Reconcile bun.lock with package.json typescript ~6.0.3 - Add integration test asserting the exec endpoint surfaces stderr Co-Authored-By: Claude Opus 4.8 (1M context) --- bashful.test.ts | 13 +++++++++++++ bashful.ts | 32 ++++++++++++++++++++++++++++---- bun.lock | 4 ++-- tsconfig.json | 3 +++ 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/bashful.test.ts b/bashful.test.ts index 3a631bb..c59fc0f 100644 --- a/bashful.test.ts +++ b/bashful.test.ts @@ -266,4 +266,17 @@ describe('Integration: HTTP Server Routing', () => { const res = await fetch(`${baseUrl}/unknown`); expect(res.status).toBe(404); }); + + test('exec endpoint surfaces stderr output (not just stdout)', async () => { + // `bun --unknown-flag-xyz` fails and writes its diagnostic to stderr. + // Previously only stdout was returned, so this came back empty. + const res = await fetch(`${baseUrl}/bun`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ _args: ['--this-flag-does-not-exist-xyz'] }) + }); + expect(res.status).toBe(200); + const text = await res.text(); + expect(text.trim().length).toBeGreaterThan(0); + }); }); diff --git a/bashful.ts b/bashful.ts index 62229bd..a40a296 100644 --- a/bashful.ts +++ b/bashful.ts @@ -311,11 +311,15 @@ if (import.meta.main) { `; const PORT = parseInt(process.env.PORT || '3000', 10); + // Bind to localhost by default: this server executes arbitrary CLI commands, + // so it must not be exposed to the network unless deliberately opted in. + const HOST = process.env.HOST || '127.0.0.1'; const commandMap = new Map(commands.map(c => [c.name, c.schema])); const serializedSchemas = new Map(commands.map(c => [c.name, JSON.stringify(c.schema, null, 2)])); const server = Bun.serve({ port: PORT, + hostname: HOST, async fetch(req) { const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -366,7 +370,27 @@ if (import.meta.main) { if (isDebug) console.log(`[Bashful] Executing: ${cmdName} ${cliArgs.join(' ')}`); const proc = safeSpawn([cmdName, ...cliArgs], { stdout: 'pipe', stderr: 'pipe' }); - return new Response(proc.stdout, { + + // Merge stdout + stderr into a single stream so error output (and + // tools that write to stderr) is visible, while preserving streaming. + const merged = new ReadableStream({ + start(controller) { + const pump = async (stream: ReadableStream | undefined | null) => { + if (!stream) return; + const reader = stream.getReader(); + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + controller.enqueue(value); + } + }; + Promise.all([pump(proc.stdout), pump(proc.stderr)]) + .then(() => controller.close()) + .catch((err) => controller.error(err)); + } + }); + + return new Response(merged, { headers: { ...corsHeaders, 'Content-Type': 'text/plain' } }); } catch (e: any) { @@ -389,9 +413,9 @@ if (import.meta.main) { if (isDebug) { console.log(`[Bashful] Server listening on port ${server.port}`); for (const { name } of commands) { - console.log(` - UI: GET http://localhost:${server.port}/`); - console.log(` - Schema: GET http://localhost:${server.port}/${name}/schema`); - console.log(` - Exec: POST http://localhost:${server.port}/${name}`); + console.log(` - UI: GET http://${HOST}:${server.port}/`); + console.log(` - Schema: GET http://${HOST}:${server.port}/${name}/schema`); + console.log(` - Exec: POST http://${HOST}:${server.port}/${name}`); } console.timeEnd('Bashful Startup'); } diff --git a/bun.lock b/bun.lock index 2f2cfa0..3673c7c 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "react-example", "devDependencies": { "@types/bun": "^1.1.14", - "typescript": "~5.8.2", + "typescript": "~6.0.3", }, }, }, @@ -17,7 +17,7 @@ "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], } diff --git a/tsconfig.json b/tsconfig.json index d88f175..79c7654 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,9 @@ "DOM.Iterable" ], "skipLibCheck": true, + "types": [ + "bun" + ], "moduleResolution": "bundler", "isolatedModules": true, "moduleDetection": "force",