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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@capsule-run/cli": "^0.8.7",
"@capsule-run/sdk": "^0.8.7"
"@capsule-run/cli": "^0.8.8",
"@capsule-run/sdk": "^0.8.8"
},
"devDependencies": {
"esbuild": "^0.28.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/bash-wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"tsup": "^8.0.0"
},
"dependencies": {
"@capsule-run/cli": "^0.8.7",
"@capsule-run/sdk": "^0.8.7",
"@capsule-run/cli": "^0.8.8",
"@capsule-run/sdk": "^0.8.8",
"@capsule-run/bash-types": "workspace:*"
}
}
18 changes: 0 additions & 18 deletions packages/bash-wasm/sandboxes/python/__test__/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,24 +83,6 @@ describe('sandbox.py – EXECUTE_CODE', () => {
expect(error.message).toContain('boom');
});


// it('url request test', async () => {
// const result = await run({
// file: SANDBOX,
// args: ['EXECUTE_CODE', baseState, `import urllib.request
// import json

// url = "https://jsonplaceholder.typicode.com/posts/1"

// with urllib.request.urlopen(url) as response:
// print(json.loads(response.read().decode("utf-8")))
// `],
// mounts: [`${WORKSPACE}::/`],
// });

// console.log(result)

// });
});

describe('sandbox.py – EXECUTE_FILE', () => {
Expand Down
78 changes: 73 additions & 5 deletions packages/bash/src/commands/cat/cat.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,75 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import { handler } from "./cat.handler";
import { createMockContext } from "../../helpers/testUtils";

describe('cat command', () => {
it('placeholder for real tests', async () => {
expect(0).toBe(0);
})
})
it('should read file content successfully', async () => {
const resolvePathMock = vi.fn().mockResolvedValue('/workspace/file.txt');
const executeCodeMock = vi.fn().mockImplementation(async (state, code) => {
if (code.includes('isDirectory')) return false;
if (code.includes('readFileSync')) return 'hello world';
return null;
});

const ctx = createMockContext(['file.txt'], {}, {
resolvePath: resolvePathMock,
executeCode: executeCodeMock
});

const result = await handler(ctx);
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe('hello world');
expect(result.stderr).toBe('');
});

it('should concatenate multiple files', async () => {
const resolvePathMock = vi.fn().mockImplementation(async (state, arg) => `/workspace/${arg}`);
const executeCodeMock = vi.fn().mockImplementation(async (state, code) => {
if (code.includes('isDirectory')) return false;
if (code.includes('file1.txt')) return 'hello';
if (code.includes('file2.txt')) return 'world';
return null;
});

const ctx = createMockContext(['file1.txt', 'file2.txt'], {}, {
resolvePath: resolvePathMock,
executeCode: executeCodeMock
});

const result = await handler(ctx);
// Because Promise.all might execute out of order in handler,
// the output order could theoretically be non-deterministic,
// but assuming it respects map array order in pushing if evaluated sequentially
// Wait, Promise.all runs concurrently. They push to stdout concurrently.
// We can just verify it contains both parts.
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('hello');
expect(result.stdout).toContain('world');
});

it('should return error if file does not exist', async () => {
const resolvePathMock = vi.fn().mockResolvedValue(undefined);
const ctx = createMockContext(['nonexistent.txt'], {}, { resolvePath: resolvePathMock });

const result = await handler(ctx);
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('bash: cat: nonexistent.txt: No such file or directory');
});

it('should return error if path is a directory', async () => {
const resolvePathMock = vi.fn().mockResolvedValue('/workspace/dir');
const executeCodeMock = vi.fn().mockImplementation(async (state, code) => {
if (code.includes('isDirectory')) return true;
return null;
});

const ctx = createMockContext(['dir'], {}, {
resolvePath: resolvePathMock,
executeCode: executeCodeMock
});

const result = await handler(ctx);
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('bash: cat: dir: Is a directory');
});
});
4 changes: 2 additions & 2 deletions packages/bash/src/commands/cd/cd.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const handler: CommandHandler = async ({ opts, state }: CommandContext) =
return { stdout: '', stderr: `bash: cd: too many arguments`, exitCode: 1 };
}

if(opts.args[0] && opts.args[0] !== "~") {
if(opts.args.length > 0 && opts.args[0] !== "~") {
targetPath = opts.args[0];
}

Expand All @@ -24,5 +24,5 @@ export const handler: CommandHandler = async ({ opts, state }: CommandContext) =
return { stdout: '', stderr: `bash: cd: ${targetPath}: No such file or directory`, exitCode: 1 };
}

return { stdout: '', stderr: '', exitCode: 0 };
return { stdout: `Directory changed to ${targetPath} ✔`, stderr: '', exitCode: 0 };
};
1 change: 1 addition & 0 deletions packages/bash/src/commands/cd/cd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('cd command', () => {
const result = await handler(ctx);

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Directory changed to /workspace');
expect(changeDirectoryMock).toHaveBeenCalledWith('/workspace');
});

Expand Down
11 changes: 6 additions & 5 deletions packages/bash/src/commands/cp/cp.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const handler: CommandHandler = async ({ state, opts, runtime }: CommandC


const sourceFileName = source.split('/').pop() || source;
const parentDestinationFolder = destination.split('/').slice(-1).join('/');
const parts = destination.split('/');
const parentDestinationFolder = parts.length > 1 ? parts.slice(0, -1).join('/') : '.';

const sourceAbsolutePath = await runtime.resolvePath(state, source);
const parentDestinationAbsolutePath = await runtime.resolvePath(state, parentDestinationFolder)
Expand All @@ -34,24 +35,24 @@ export const handler: CommandHandler = async ({ state, opts, runtime }: CommandC
}

if(isDestinationFolder && !isSourceFolder) {
const destinationPath = path.join(destination, sourceFileName);
const destinationPath = path.join(destinationAbsolutePath as string, sourceFileName);

await runtime.executeCode(state, `require('fs').copyFileSync('${sourceAbsolutePath}', '${destinationPath}');`);

return { stdout: '', stderr: '', exitCode: 0 };
return { stdout: 'File copied ✔', stderr: '', exitCode: 0 };
}

if(!destinationAbsolutePath && parentDestinationAbsolutePath && !isSourceFolder) {
const destinationFileName = destination.split('/').pop() as string;
const destinationPath = path.join(parentDestinationAbsolutePath, destinationFileName)

await runtime.executeCode(state, `require('fs').copyFileSync('${sourceAbsolutePath}', '${destinationPath}');`);
return { stdout: '', stderr: '', exitCode: 0 };
return { stdout: 'File copied ✔', stderr: '', exitCode: 0 };
}

if(opts.hasFlag('r') && isSourceFolder) {
await runtime.executeCode(state, `(async () => await require('fs').cp('${sourceAbsolutePath}', '${destinationAbsolutePath || destination}', { recursive: true }))()`)
return { stdout: '', stderr: '', exitCode: 0 };
return { stdout: 'Folder copied ✔', stderr: '', exitCode: 0 };
}


Expand Down
111 changes: 106 additions & 5 deletions packages/bash/src/commands/cp/cp.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,108 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import { handler } from "./cp.handler";
import { createMockContext } from "../../helpers/testUtils";

describe('cp command', () => {
it('placeholder for real tests', async () => {
expect(0).toBe(0);
})
})
it('should return error if missing operands', async () => {
const ctx = createMockContext(['file1']);
const result = await handler(ctx);

expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('missing file operand');
});

it('should return error if source does not exist', async () => {
const resolvePathMock = vi.fn().mockResolvedValue(undefined);
const ctx = createMockContext(['nonexistent', 'dest'], {}, { resolvePath: resolvePathMock });
const result = await handler(ctx);

expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('No such file or directory');
});

it('should copy file to another file successfully', async () => {
const resolvePathMock = vi.fn().mockImplementation(async (state, path) => {
if (path === 'file1.txt') return '/workspace/file1.txt';
if (path === 'newname.txt') return undefined; // Destination file doesn't exist
if (path === '.') return '/workspace'; // Parent folder
return undefined;
});

const executeCodeMock = vi.fn().mockImplementation(async (state, code) => {
if (code.includes('isDirectory')) return false;
return '';
});

const ctx = createMockContext(['file1.txt', 'newname.txt'], {}, {
resolvePath: resolvePathMock,
executeCode: executeCodeMock
});

const result = await handler(ctx);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('File copied ✔');
expect(executeCodeMock).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining("require('fs').copyFileSync('/workspace/file1.txt'")
);
});

it('should copy file into a directory successfully', async () => {
const resolvePathMock = vi.fn().mockImplementation(async (state, path) => {
if (path === 'file1.txt') return '/workspace/file1.txt';
if (path === 'dir1') return '/workspace/dir1';
return undefined;
});

const executeCodeMock = vi.fn().mockImplementation(async (state, code) => {
if (code.includes('isDirectory')) {
if (code.includes('dir1')) return true;
return false;
}
return '';
});

const ctx = createMockContext(['file1.txt', 'dir1'], {}, {
resolvePath: resolvePathMock,
executeCode: executeCodeMock
});

const result = await handler(ctx);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('File copied ✔');
expect(executeCodeMock).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining("copyFileSync('/workspace/file1.txt'")
);
});

it('should copy directory recursively with -r flag', async () => {
const resolvePathMock = vi.fn().mockImplementation(async (state, path) => {
if (path === 'dir1') return '/workspace/dir1';
if (path === 'dir2') return undefined;
if (path === '.') return '/workspace';
return undefined;
});

const executeCodeMock = vi.fn().mockImplementation(async (state, code) => {
if (code.includes('isDirectory')) {
if (code.includes('dir1')) return true;
return false;
}
return '';
});

const ctx = createMockContext(['-r', 'dir1', 'dir2'], {}, {
resolvePath: resolvePathMock,
executeCode: executeCodeMock
});

const result = await handler(ctx);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Folder copied ✔');
expect(executeCodeMock).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining("cp('/workspace/dir1', 'dir2', { recursive: true })")
);
});
});
42 changes: 30 additions & 12 deletions packages/bash/src/commands/curl/curl.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ function parseRawArgs(raw: string[]): CurlArgs {
}
} else if (arg === '-d' && raw[i + 1]) {
result.body = raw[++i];
// -d implies POST if no -X was given
if (result.method === 'GET') result.method = 'POST';
} else if (!arg.startsWith('-')) {
result.url = arg;
Expand All @@ -83,6 +82,36 @@ export const handler: CommandHandler = async ({ opts, state, runtime }: CommandC
return { stdout: '', stderr: 'bash: curl: no URL specified', exitCode: 1 };
}

if (args.saveToFile) {
const filename = filenameFromUrl(args.url);
const absolutePath = await runtime.resolvePath(state, filename);
const targetPath = absolutePath ?? filename;

const saveResult = await runtime.executeCode(state, `
(async function() {
try {
const response = await fetch(${JSON.stringify(args.url)}, {
method: ${JSON.stringify(args.method)},
headers: ${JSON.stringify(args.headers)},
${args.body !== null ? `body: ${JSON.stringify(args.body)},` : ''}
redirect: ${JSON.stringify(args.followRedirects ? 'follow' : 'manual')},
});
const buffer = await response.arrayBuffer();
require('fs').writeFileSync('${targetPath}', new Uint8Array(buffer));
return { ok: true };
} catch (e) {
return { ok: false, error: String(e) };
}
})()
`) as { ok: boolean; error?: string };

if (!saveResult.ok) {
return { stdout: '', stderr: args.silent ? '' : `bash: curl: ${args.url}: ${saveResult.error}`, exitCode: 1 };
}

return { stdout: 'File downloaded ✔', stderr: '', exitCode: 0 };
}

const result = await runtime.executeCode(state, `
(async function() {
try {
Expand All @@ -92,7 +121,6 @@ export const handler: CommandHandler = async ({ opts, state, runtime }: CommandC
${args.body !== null ? `body: ${JSON.stringify(args.body)},` : ''}
redirect: ${JSON.stringify(args.followRedirects ? 'follow' : 'manual')},
});

const text = await response.text();
return { ok: true, status: response.status, body: text };
} catch (e) {
Expand All @@ -106,15 +134,5 @@ export const handler: CommandHandler = async ({ opts, state, runtime }: CommandC
return { stdout: '', stderr: args.silent ? '' : msg, exitCode: 1 };
}

if (args.saveToFile) {
const filename = filenameFromUrl(args.url);
const absolutePath = await runtime.resolvePath(state, filename);
const targetPath = absolutePath ?? filename;

await runtime.executeCode(state, `require('fs').writeFileSync('${targetPath}', ${JSON.stringify(result.body ?? '')});`);

return { stdout: '', stderr: args.silent ? '' : ` % Total\n100 ${(result.body ?? '').length} Saved to: ${filename}`, exitCode: 0 };
}

return { stdout: result.body ?? '', stderr: '', exitCode: 0 };
};
Loading
Loading