Skip to content
Open
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
23 changes: 23 additions & 0 deletions examples/cjs-consumer/pnpm-lock.yaml

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

24 changes: 24 additions & 0 deletions examples/executor-tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Executor Tools Example

Demonstrates executor tool invocation in just-bash. Sandboxed JavaScript code running in `js-exec` calls tools that fetch from real public APIs — no API keys needed.

## Run

```bash
cd examples/executor-tools
pnpm install
pnpm start
```

## What it shows

**Part 1 — Inline tools** (no `@executor/sdk` required):
1. **GraphQL tools** — Countries API queries exposed as `tools.countries.*`
2. **Utility tools** — `tools.util.timestamp()`, `tools.util.random()`
3. **Cross-tool scripts** — one js-exec script calling tools from multiple namespaces
4. **Tools + filesystem** — fetch data via tools, write to virtual fs, read with bash commands
5. **Error handling** — tool errors propagate as catchable exceptions

**Part 2 — Native SDK source discovery** (requires `@executor/sdk`):
- Shows the `executor.setup` callback pattern for auto-discovering tools from OpenAPI specs, GraphQL schemas, and MCP servers
- Includes `onToolApproval` for controlling which operations are allowed
183 changes: 183 additions & 0 deletions examples/executor-tools/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* Executor Tools Example
*
* Demonstrates executor tool invocation in just-bash:
* 1. Inline tools — defined in the Bash constructor
* 2. Native SDK source discovery — OpenAPI and GraphQL auto-discovered tools
*
* No API keys required. Uses:
* - httpbin.org (OpenAPI) — echo/utility API
* - countries.trevorblades.com (GraphQL) — country data
*
* Run with: npx tsx main.ts
*/

import { Bash } from "just-bash";

// ── Part 1: Inline tools (no SDK) ─────────────────────────────────

console.log("=== Part 1: Inline Tools ===\n");

const bash = new Bash({
executionLimits: { maxJsTimeoutMs: 60000 },
executor: {
tools: {
// GraphQL tool — queries countries.trevorblades.com
"countries.list": {
description: "List countries, optionally filtered by continent code",
execute: async (args?: { continent?: string }) => {
const filter = args?.continent
? `(filter: { continent: { eq: "${args.continent}" } })`
: "";
const query = `{ countries${filter} { code name capital emoji } }`;
const res = await fetch(
"https://countries.trevorblades.com/graphql",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
},
);
const json = (await res.json()) as { data: { countries: unknown[] } };
return json.data.countries;
},
},

"countries.get": {
description: "Get a single country by ISO code",
execute: async (args: { code: string }) => {
const query = `{ country(code: "${args.code}") { name capital currency emoji languages { name } continent { name } } }`;
const res = await fetch(
"https://countries.trevorblades.com/graphql",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
},
);
const json = (await res.json()) as { data: { country: unknown } };
return json.data.country;
},
},

// Simple sync tools
"util.timestamp": {
description: "Current Unix timestamp",
execute: () => ({ ts: Math.floor(Date.now() / 1000) }),
},
"util.random": {
description: "Random number between min and max",
execute: (args: { min?: number; max?: number }) => ({
value: Math.floor(
Math.random() * ((args.max ?? 100) - (args.min ?? 0)) +
(args.min ?? 0),
),
}),
},
},
},
});

// 1. List European countries
console.log("1. European countries:");
let r = await bash.exec(`js-exec -c '
const countries = await tools.countries.list({ continent: "EU" });
console.log(countries.length + " countries in Europe");
for (const c of countries.slice(0, 5)) {
console.log(" " + c.emoji + " " + c.name + " — " + c.capital);
}
console.log(" ...");
'`);
console.log(r.stdout);

// 2. Country detail
console.log("2. Country detail:");
r = await bash.exec(`js-exec -c '
const c = await tools.countries.get({ code: "JP" });
console.log(c.emoji + " " + c.name);
console.log(" Capital: " + c.capital);
console.log(" Currency: " + c.currency);
console.log(" Continent: " + c.continent.name);
console.log(" Languages: " + c.languages.map(l => l.name).join(", "));
'`);
console.log(r.stdout);

// 3. Mix tools from different sources
console.log("3. Cross-tool script:");
r = await bash.exec(`js-exec -c '
const ts = await tools.util.timestamp();
const rand = await tools.util.random({ min: 0, max: 249 });
const all = await tools.countries.list();
const pick = all[rand.value];

console.log("Report at " + ts.ts);
console.log("Random country #" + rand.value + ": " + pick.emoji + " " + pick.name);
'`);
console.log(r.stdout);

// 4. Tools + virtual filesystem
console.log("4. Fetch → write to fs → read with bash:");
r = await bash.exec(`js-exec -c '
const fs = require("fs");
const countries = await tools.countries.list({ continent: "SA" });
const csv = "code,name,capital\\n" +
countries.map(c => c.code + "," + c.name + "," + c.capital).join("\\n");
fs.writeFileSync("/tmp/south-america.csv", csv);
console.log("Wrote " + countries.length + " rows to /tmp/south-america.csv");
'`);
console.log(r.stdout);

r = await bash.exec("cat /tmp/south-america.csv | head -5");
console.log(" " + r.stdout.split("\n").join("\n "));

// 5. Error handling
console.log("5. Error handling:");
r = await bash.exec(`js-exec -c '
try {
await tools.countries.get({ code: "NOPE" });
} catch (e) {
console.error("Caught: " + e.message);
}
console.log("Script continued after error");
'`);
console.log(r.stdout);
if (r.stderr) console.log(" stderr: " + r.stderr);

// ── Part 2: Native SDK source discovery ───────────────────────────

console.log("=== Part 2: Native SDK Source Discovery ===\n");
console.log(
"When @executor/sdk is configured via executor.setup, tools are\n" +
"auto-discovered from OpenAPI specs, GraphQL schemas, and MCP servers.\n",
);
console.log("Example Bash constructor:\n");
console.log(` const bash = new Bash({
executor: {
setup: async (sdk) => {
await sdk.sources.add({
kind: "openapi",
endpoint: "https://petstore3.swagger.io/api/v3",
specUrl: "https://petstore3.swagger.io/api/v3/openapi.json",
name: "petstore",
});
await sdk.sources.add({
kind: "graphql",
endpoint: "https://countries.trevorblades.com/graphql",
name: "countries",
});
},
onToolApproval: async (req) => {
if (req.operationKind === "read") return { approved: true };
return { approved: false, reason: "writes not allowed" };
},
},
});

// Tools are auto-discovered from the specs:
await bash.exec(\`js-exec -c '
const pets = await tools.petstore.findPetsByStatus({ status: "available" });
const country = await tools.countries.country({ code: "US" });
'\`);
`);

console.log("Done!");
12 changes: 12 additions & 0 deletions examples/executor-tools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "executor-tools-example",
"version": "1.0.0",
"description": "Example of executor tools in just-bash — inline tools + OpenAPI via fetch",
"type": "module",
"scripts": {
"start": "npx tsx main.ts"
},
"dependencies": {
"just-bash": "link:../.."
}
}
10 changes: 10 additions & 0 deletions examples/executor-tools/pnpm-lock.yaml

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

12 changes: 12 additions & 0 deletions examples/executor-tools/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["*.ts"]
}
6 changes: 5 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["src/cli/just-bash.ts"],
"entry": [
"src/cli/just-bash.ts",
"src/commands/js-exec/executor-adapter.ts",
"src/executor-init.ts"
],
"project": ["src/**/*.ts"],
"ignore": [
"src/**/*.test.ts",
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "just-bash",
"version": "2.14.0",
"version": "2.15.1-executor.0",
"description": "A simulated bash environment with virtual filesystem",
"repository": {
"type": "git",
Expand Down Expand Up @@ -44,6 +44,7 @@
"dist/sandbox/*.d.ts",
"dist/utils/*.d.ts",
"vendor/cpython-emscripten/",
"vendor/executor/",
"README.md",
"dist/AGENTS.md"
],
Expand All @@ -58,10 +59,10 @@
"build": "rm -rf dist && tsc && pnpm build:lib && pnpm build:lib:cjs && pnpm build:browser && pnpm build:cli && pnpm build:shell && pnpm build:worker && pnpm build:clean && cp dist/index.d.ts dist/index.d.cts && sed '1,/^-->/d' AGENTS.npm.md > dist/AGENTS.md",
"build:clean": "find dist -name '*.test.js' -delete && find dist -name '*.test.d.ts' -delete",
"build:worker": "esbuild src/commands/python3/worker.ts --bundle --platform=node --format=esm --outfile=src/commands/python3/worker.js --external:../../../vendor/cpython-emscripten/* && cp src/commands/python3/worker.js dist/commands/python3/worker.js && mkdir -p dist/bin/chunks && cp src/commands/python3/worker.js dist/bin/chunks/worker.js && mkdir -p dist/bundle/chunks && cp src/commands/python3/worker.js dist/bundle/chunks/worker.js && esbuild src/commands/js-exec/worker.ts --bundle --platform=node --format=esm --outfile=src/commands/js-exec/worker.js --external:quickjs-emscripten && cp src/commands/js-exec/worker.js dist/commands/js-exec/worker.js && cp src/commands/js-exec/worker.js dist/bin/chunks/js-exec-worker.js && cp src/commands/js-exec/worker.js dist/bundle/chunks/js-exec-worker.js",
"build:lib": "esbuild dist/index.js --bundle --splitting --platform=node --format=esm --minify --outdir=dist/bundle --chunk-names=chunks/[name]-[hash] --external:diff --external:minimatch --external:sprintf-js --external:turndown --external:sql.js --external:quickjs-emscripten --external:@mongodb-js/zstd --external:node-liblzma --external:compressjs",
"build:lib:cjs": "esbuild dist/index.js --bundle --platform=node --format=cjs --minify --outfile=dist/bundle/index.cjs --external:diff --external:minimatch --external:sprintf-js --external:turndown --external:sql.js --external:quickjs-emscripten --external:@mongodb-js/zstd --external:node-liblzma --external:compressjs",
"build:lib": "esbuild dist/index.js --bundle --splitting --platform=node --format=esm --minify --outdir=dist/bundle --chunk-names=chunks/[name]-[hash] --external:diff --external:minimatch --external:sprintf-js --external:turndown --external:sql.js --external:quickjs-emscripten --external:@mongodb-js/zstd --external:node-liblzma --external:compressjs --external:effect --external:@effect/platform --external:@effect/platform-node --external:../vendor/executor/*",
"build:lib:cjs": "esbuild dist/index.js --bundle --platform=node --format=cjs --minify --outfile=dist/bundle/index.cjs --external:diff --external:minimatch --external:sprintf-js --external:turndown --external:sql.js --external:quickjs-emscripten --external:@mongodb-js/zstd --external:node-liblzma --external:compressjs --external:effect --external:@effect/platform --external:@effect/platform-node --external:../vendor/executor/*",
"build:browser": "esbuild dist/browser.js --bundle --platform=browser --format=esm --minify --outfile=dist/bundle/browser.js --external:diff --external:minimatch --external:sprintf-js --external:turndown --external:node:zlib --external:@mongodb-js/zstd --external:node-liblzma --external:compressjs --define:__BROWSER__=true --alias:node:dns=./src/shims/browser-unsupported.js",
"build:cli": "esbuild dist/cli/just-bash.js --bundle --splitting --platform=node --format=esm --minify --outdir=dist/bin --entry-names=[name] --chunk-names=chunks/[name]-[hash] --banner:js='#!/usr/bin/env node' --external:sql.js --external:quickjs-emscripten --external:@mongodb-js/zstd --external:node-liblzma --external:compressjs",
"build:cli": "esbuild dist/cli/just-bash.js --bundle --splitting --platform=node --format=esm --minify --outdir=dist/bin --entry-names=[name] --chunk-names=chunks/[name]-[hash] --banner:js='#!/usr/bin/env node' --external:sql.js --external:quickjs-emscripten --external:@mongodb-js/zstd --external:node-liblzma --external:compressjs --external:effect --external:@effect/platform --external:@effect/platform-node --external:../vendor/executor/*",
"build:shell": "esbuild dist/cli/shell.js --bundle --splitting --platform=node --format=esm --minify --outdir=dist/bin/shell --entry-names=[name] --chunk-names=chunks/[name]-[hash] --banner:js='#!/usr/bin/env node' --external:sql.js --external:quickjs-emscripten --external:@mongodb-js/zstd --external:node-liblzma --external:compressjs",
"prepublishOnly": "pnpm validate",
"validate": "pnpm lint && pnpm knip && pnpm typecheck && pnpm build && pnpm check:worker-sync && pnpm test:run && pnpm test:wasm && pnpm test:dist && pnpm test:examples",
Expand Down Expand Up @@ -107,6 +108,7 @@
"dependencies": {
"compressjs": "^1.0.3",
"diff": "^8.0.2",
"effect": "^3.21.0",
"fast-xml-parser": "^5.3.3",
"file-type": "^21.2.0",
"ini": "^6.0.0",
Expand Down
Loading
Loading