Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions bashful.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
32 changes: 28 additions & 4 deletions bashful.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,15 @@ if (import.meta.main) {
</html>`;

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': '*',
Expand Down Expand Up @@ -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<Uint8Array>({
start(controller) {
const pump = async (stream: ReadableStream<Uint8Array> | 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) {
Expand All @@ -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');
}
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"bun"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
Expand Down