From fdc2ce255b15576822498bda75068fb3632b49a2 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Fri, 24 Apr 2026 11:54:25 +0200 Subject: [PATCH 1/5] refactor: delegate filesystem snapshotting to runtime execution instead of local fs module --- packages/bash/src/core/executor.ts | 55 +++++++++++++++++------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/bash/src/core/executor.ts b/packages/bash/src/core/executor.ts index 86eb7a5..1f0e749 100644 --- a/packages/bash/src/core/executor.ts +++ b/packages/bash/src/core/executor.ts @@ -1,5 +1,4 @@ import path from 'path'; -import fs from 'fs'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; @@ -24,28 +23,36 @@ export class Executor { private readonly state: State, ) {} - private snapshotFs(root: string): FsSnapshot { - const snapshot: FsSnapshot = {}; - - const walk = (dir: string) => { - try { - for (const entry of fs.readdirSync(dir)) { - const fullPath = path.join(dir, entry); - try { - const stat = fs.statSync(fullPath); - if (stat.isDirectory()) { - snapshot[fullPath.slice(root.length + 1) + '/'] = stat.mtimeMs; - walk(fullPath); - } else { - snapshot[fullPath.slice(root.length + 1)] = stat.mtimeMs; - } - } catch {} - } - } catch {} - }; + private async snapshotFs(root: string): Promise { + const code = ` + const fs = require('fs'); + const path = require('path'); + const snapshot = {}; + const walk = (dir) => { + try { + for (const entry of fs.readdirSync(dir)) { + const fullPath = path.join(dir, entry); + try { + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + snapshot[fullPath.slice(${JSON.stringify(root)}.length + 1) + '/'] = stat.mtimeMs; + walk(fullPath); + } else { + snapshot[fullPath.slice(${JSON.stringify(root)}.length + 1)] = stat.mtimeMs; + } + } catch {} + } + } catch {} + }; + walk(${JSON.stringify(root)}); + return snapshot; + `; - walk(root); - return snapshot; + try { + return await this.runtime.executeCode(this.state, code) as FsSnapshot; + } catch { + return {}; + } } private cwdRoot(): string { @@ -171,7 +178,7 @@ export class Executor { const command = await this.searchCommandHandler(name); const root = this.cwdRoot(); - const before = this.snapshotFs(root); + const before = await this.snapshotFs(root); if (!command) { result = { stdout: '', stderr: `bash: ${name}: command not found`, exitCode: 127, durationMs: Date.now() - start }; @@ -266,7 +273,7 @@ export class Executor { } } - const after = this.snapshotFs(root); + const after = await this.snapshotFs(root); const diff = this.diffSnapshots(before, after); const durationMs = Date.now() - start; From 51c2668022f279df94eb9813bac8cb0a3213da6a Mon Sep 17 00:00:00 2001 From: Mavdol Date: Fri, 24 Apr 2026 20:09:38 +0200 Subject: [PATCH 2/5] refactor: improve filesystem snapshotting, command handling, and touch output while adding local sdk override --- packages/bash/src/commands/ls/ls.handler.ts | 3 ++- packages/bash/src/commands/mv/mv.handler.ts | 2 +- .../bash/src/commands/touch/touch.handler.ts | 7 ++++-- packages/bash/src/core/executor.ts | 22 +++++++------------ 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/bash/src/commands/ls/ls.handler.ts b/packages/bash/src/commands/ls/ls.handler.ts index f1a6e3e..7e237db 100644 --- a/packages/bash/src/commands/ls/ls.handler.ts +++ b/packages/bash/src/commands/ls/ls.handler.ts @@ -45,6 +45,7 @@ export const handler: CommandHandler = async ({ opts, state, runtime }: CommandC if (!a.startsWith(".") && b.startsWith(".")) return 1; return a.localeCompare(b); }); + } catch (e) { stderr.push(`bash: ls: cannot access '${arg}': No such file or directory`); exitCode = 1; @@ -86,7 +87,7 @@ export const handler: CommandHandler = async ({ opts, state, runtime }: CommandC const time = `${months[date.getMonth()]} ${padDate} ${timeStr}`; return `${permissions} ${hardlink} ${user} ${group} ${size} ${time} ${filename}`; - } catch (err) { + } catch { return; } }))).filter((file): file is string => file !== undefined); diff --git a/packages/bash/src/commands/mv/mv.handler.ts b/packages/bash/src/commands/mv/mv.handler.ts index c0c9922..da175b5 100644 --- a/packages/bash/src/commands/mv/mv.handler.ts +++ b/packages/bash/src/commands/mv/mv.handler.ts @@ -39,7 +39,7 @@ export const handler: CommandHandler = async ({ state, opts, runtime }: CommandC (async () => { ${isSourceFolder ? `await fs.cp('${sourceAbsolutePath}', '${destinationAbsolutePath}', {recursive: true });` : - `await fs.copyFile('${sourceAbsolutePath}', '${path.join(destinationAbsolutePath as string, sourceFileName)}');` + `await fs.cp('${sourceAbsolutePath}', '${path.join(destinationAbsolutePath as string, sourceFileName)}');` } fs.rmSync('${sourceAbsolutePath}', ${isSourceFolder ? '{ recursive: true }' : '{}'}); })() diff --git a/packages/bash/src/commands/touch/touch.handler.ts b/packages/bash/src/commands/touch/touch.handler.ts index e76b0fc..e202488 100644 --- a/packages/bash/src/commands/touch/touch.handler.ts +++ b/packages/bash/src/commands/touch/touch.handler.ts @@ -15,7 +15,6 @@ export const handler: CommandHandler = async ({ state, opts, runtime }: CommandC const segments = arg.split('/'); const parentFolder = segments.length > 1 ? segments.slice(0, -1).join('/') : '.'; - const parentFolderAbsolutePath = (await runtime.resolvePath(state, parentFolder)); if (!parentFolderAbsolutePath) { @@ -32,5 +31,9 @@ export const handler: CommandHandler = async ({ state, opts, runtime }: CommandC } })) - return { stdout: 'File created ✔', stderr: stderr.join('\n'), exitCode: stderr.length > 0 ? 1 : 0 }; + if (stderr.length === 0) { + return { stdout: `${opts.args.length > 1 ? opts.args.length + ' Files' : 'File'} created ✔`, stderr: stderr.join('\n'), exitCode: 0 }; + } + + return { stdout: "", stderr: stderr.join('\n'), exitCode: 1 }; } diff --git a/packages/bash/src/core/executor.ts b/packages/bash/src/core/executor.ts index 1f0e749..4a57372 100644 --- a/packages/bash/src/core/executor.ts +++ b/packages/bash/src/core/executor.ts @@ -23,10 +23,12 @@ export class Executor { private readonly state: State, ) {} - private async snapshotFs(root: string): Promise { + private async snapshotFs(): Promise { + const sandboxRoot = this.state.cwd || '/'; const code = ` const fs = require('fs'); const path = require('path'); + const root = ${JSON.stringify(sandboxRoot)}; const snapshot = {}; const walk = (dir) => { try { @@ -35,16 +37,16 @@ export class Executor { try { const stat = fs.statSync(fullPath); if (stat.isDirectory()) { - snapshot[fullPath.slice(${JSON.stringify(root)}.length + 1) + '/'] = stat.mtimeMs; + snapshot[fullPath.slice(root.length + 1) + '/'] = stat.mtimeMs; walk(fullPath); } else { - snapshot[fullPath.slice(${JSON.stringify(root)}.length + 1)] = stat.mtimeMs; + snapshot[fullPath.slice(root.length + 1)] = stat.mtimeMs; } } catch {} } } catch {} }; - walk(${JSON.stringify(root)}); + walk(root); return snapshot; `; @@ -55,13 +57,6 @@ export class Executor { } } - private cwdRoot(): string { - const workspace = this.runtime.hostWorkspace; - if (!workspace) return ''; - const relativeCwd = this.state.cwd.replace(/^\//, ''); - return relativeCwd ? path.join(workspace, relativeCwd) : workspace; - } - private diffSnapshots(before: FsSnapshot, after: FsSnapshot): { created: string[]; modified: string[]; deleted: string[] } { const created: string[] = []; const modified: string[] = []; @@ -177,8 +172,7 @@ export class Executor { const opts = parsedCommandOptions(args); const command = await this.searchCommandHandler(name); - const root = this.cwdRoot(); - const before = await this.snapshotFs(root); + const before = await this.snapshotFs(); if (!command) { result = { stdout: '', stderr: `bash: ${name}: command not found`, exitCode: 127, durationMs: Date.now() - start }; @@ -273,7 +267,7 @@ export class Executor { } } - const after = await this.snapshotFs(root); + const after = await this.snapshotFs(); const diff = this.diffSnapshots(before, after); const durationMs = Date.now() - start; From 8057b660b23823c87d3855f62ebc09bc1629edeb Mon Sep 17 00:00:00 2001 From: Mavdol Date: Fri, 24 Apr 2026 20:28:49 +0200 Subject: [PATCH 3/5] refactor: pass snapshot root as argument to executor --- packages/bash/src/core/executor.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/bash/src/core/executor.ts b/packages/bash/src/core/executor.ts index 4a57372..cd4ecec 100644 --- a/packages/bash/src/core/executor.ts +++ b/packages/bash/src/core/executor.ts @@ -23,8 +23,8 @@ export class Executor { private readonly state: State, ) {} - private async snapshotFs(): Promise { - const sandboxRoot = this.state.cwd || '/'; + private async snapshotFs(root: string): Promise { + const sandboxRoot = root; const code = ` const fs = require('fs'); const path = require('path'); @@ -172,7 +172,8 @@ export class Executor { const opts = parsedCommandOptions(args); const command = await this.searchCommandHandler(name); - const before = await this.snapshotFs(); + const snapshotRoot = this.state.cwd || '/'; + const before = await this.snapshotFs(snapshotRoot); if (!command) { result = { stdout: '', stderr: `bash: ${name}: command not found`, exitCode: 127, durationMs: Date.now() - start }; @@ -267,7 +268,7 @@ export class Executor { } } - const after = await this.snapshotFs(); + const after = await this.snapshotFs(snapshotRoot); const diff = this.diffSnapshots(before, after); const durationMs = Date.now() - start; From 9533e58810e063c7c8993916971034dfddeaf540 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Fri, 24 Apr 2026 22:31:49 +0200 Subject: [PATCH 4/5] update @capsule-run dependencies to version 0.8.10 --- package.json | 4 +- packages/bash-wasm/package.json | 4 +- pnpm-lock.yaml | 72 ++++++++++++++++----------------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index b20bc9e..af918d7 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "url": "https://github.com/capsulerun/bash.git" }, "dependencies": { - "@capsule-run/cli": "^0.8.9", - "@capsule-run/sdk": "^0.8.9" + "@capsule-run/cli": "^0.8.10", + "@capsule-run/sdk": "^0.8.10" }, "devDependencies": { "@types/node": "^25.6.0", diff --git a/packages/bash-wasm/package.json b/packages/bash-wasm/package.json index 6add6ce..acc5643 100644 --- a/packages/bash-wasm/package.json +++ b/packages/bash-wasm/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "@capsule-run/bash-types": "workspace:*", - "@capsule-run/sdk": "^0.8.9", - "@capsule-run/cli": "^0.8.9" + "@capsule-run/sdk": "^0.8.10", + "@capsule-run/cli": "^0.8.10" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5249d22..9f755ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: dependencies: '@capsule-run/cli': - specifier: ^0.8.9 - version: 0.8.9 + specifier: ^0.8.10 + version: 0.8.10 '@capsule-run/sdk': - specifier: ^0.8.9 - version: 0.8.9(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0) + specifier: ^0.8.10 + version: 0.8.10(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0) devDependencies: '@types/node': specifier: ^25.6.0 @@ -97,11 +97,11 @@ importers: specifier: workspace:* version: link:../bash-types '@capsule-run/cli': - specifier: ^0.8.9 - version: 0.8.9 + specifier: ^0.8.10 + version: 0.8.10 '@capsule-run/sdk': - specifier: ^0.8.9 - version: 0.8.9(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0) + specifier: ^0.8.10 + version: 0.8.10(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0) devDependencies: tsup: specifier: ^8.0.0 @@ -161,8 +161,8 @@ packages: resolution: {integrity: sha512-JPRYUTD8v1QUsZ5eqhCtQR7amOTugjV2ofSjFv1/zAGksf4AZUoCFYiKTQ61E+hKUVNJKIdYLOw+stGqAL9qAg==} hasBin: true - '@bytecodealliance/jco@1.18.0': - resolution: {integrity: sha512-IbWpq0iN6v95IwgqwctSSHkMo3EOIlsMJ+GGTPOoVhgJBKYKNqQVDfGqzJ+K57RDjoxpKVceVyN+4204wGBpxQ==} + '@bytecodealliance/jco@1.19.0': + resolution: {integrity: sha512-I57cVbL24/u/zCBwHq7D9PyIMP81hFFYF4hL/pW5biRGVLQAZuwEAUaEmghOouyt77bU2ExqscP2wkLvr3nfDw==} hasBin: true '@bytecodealliance/preview2-shim@0.17.9': @@ -213,37 +213,37 @@ packages: engines: {node: '>=16'} hasBin: true - '@capsule-run/cli-darwin-arm64@0.8.9': - resolution: {integrity: sha512-L5cWhCa5elZ0gejD1f3zF66h58QqCCW/6KfNk0cRt2o/EtNSHjwpD9oLkKE8k3OkkJVMLlnCrlYKP0uI+OG4ig==} + '@capsule-run/cli-darwin-arm64@0.8.10': + resolution: {integrity: sha512-wOJlLPVnvEV0mlpdZti1wn6gZI1f+zNFSlbC1i8iy7col97i2VU3yxYxK0ZnGMeTwB+1gDzC8ctILd+CFWINEQ==} cpu: [arm64] os: [darwin] hasBin: true - '@capsule-run/cli-darwin-x64@0.8.9': - resolution: {integrity: sha512-NWWVOxbpL1vFag4h/PYioqjH1deAiTThxS4JVoctO/809nJUxi+JJaWrJdu6TspzrTN8nOrlG/mIfXMfdCuk8A==} + '@capsule-run/cli-darwin-x64@0.8.10': + resolution: {integrity: sha512-muXiVJULeX4p6WyfZ8rIt7FuyDfyPkaWexP0WS3UdJV4vnAa8gpPXZH5zUZrDxLdxUpvHRj7BrHbKq6k2WLj6g==} cpu: [x64] os: [darwin] hasBin: true - '@capsule-run/cli-linux-x64@0.8.9': - resolution: {integrity: sha512-MGLxM2rS5/uVrSCkkR8CkLXmxmXwmyJ7L5GWa3rtF2W0GHZ4tjW9lHGf8ihSUG2c081w3C8xGyXSp0JUfr8zuw==} + '@capsule-run/cli-linux-x64@0.8.10': + resolution: {integrity: sha512-lhqxYqWfMP1kkXbOVQMcvYdOR6RYvdHGj4Adcrs+TJmz2Yg6aOXFSsNw9UpTwa7c/TwmpjXHSbnd2EDJx9rdag==} cpu: [x64] os: [linux] hasBin: true - '@capsule-run/cli-win32-x64@0.8.9': - resolution: {integrity: sha512-8brGYTQ7Qi9TLxO2qSgPMVL3BMc6aO5IKvL9xc16voFQgmxmIVSq9963bkYfICS24tcdadMLTcCvt9TkT3LBXw==} + '@capsule-run/cli-win32-x64@0.8.10': + resolution: {integrity: sha512-a34mBOyTbAFhDoIZgSO4r/Pn6rgoUOU4eV9D05CJcrt+pedwrXJoreap+T0IX1P4mOQOpQttwBSOIXtmFh4MMQ==} cpu: [x64] os: [win32] hasBin: true - '@capsule-run/cli@0.8.9': - resolution: {integrity: sha512-WPLBxy4aKCu7weYlQPPBbOvkcGb1eaNUlzW4F88qzThOQoLMxh3G/VTHfSSsvdJw2j9gzZYIrtS0wlb6FsV0UQ==} + '@capsule-run/cli@0.8.10': + resolution: {integrity: sha512-QeX3HwVfCOOvFhXV6L4rbndvfszTVJe0ZXiqEUVUmzwRg4Ehi8jxnh5iJ9mr80NIBH6IPV4tM77EYk9v5Z7OGQ==} engines: {node: '>=18'} hasBin: true - '@capsule-run/sdk@0.8.9': - resolution: {integrity: sha512-FPg9A9MgWTA35MuvulOft5BfeuaHnW6HIIZUSEnI6Mr+GDxkIBl2CUueIRx7JAdmm/CFYjzG2FMAg2F/7vQNOA==} + '@capsule-run/sdk@0.8.10': + resolution: {integrity: sha512-7G20+dhj+1jNm49ZFRmtCkbdemIzb0Dy62fXUdk/eDafqBiooF3IhEy8WaLdiO3jTQboa1qiQbwcw0oWVT+liw==} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: @@ -2471,7 +2471,7 @@ snapshots: '@bytecodealliance/componentize-js@0.19.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@bytecodealliance/jco': 1.18.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@bytecodealliance/jco': 1.19.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@bytecodealliance/wizer': 10.0.0 es-module-lexer: 1.7.0 oxc-parser: 0.76.0 @@ -2481,7 +2481,7 @@ snapshots: '@bytecodealliance/componentize-js@0.20.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@bytecodealliance/jco': 1.18.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@bytecodealliance/jco': 1.19.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@bytecodealliance/weval': 0.4.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@bytecodealliance/wizer': 10.0.0 es-module-lexer: 1.7.0 @@ -2490,7 +2490,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - '@bytecodealliance/jco@1.18.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@bytecodealliance/jco@1.19.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@bytecodealliance/componentize-js': 0.20.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@bytecodealliance/componentize-js-0-19-3': '@bytecodealliance/componentize-js@0.19.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)' @@ -2543,28 +2543,28 @@ snapshots: '@bytecodealliance/wizer-linux-x64': 10.0.0 '@bytecodealliance/wizer-win32-x64': 10.0.0 - '@capsule-run/cli-darwin-arm64@0.8.9': + '@capsule-run/cli-darwin-arm64@0.8.10': optional: true - '@capsule-run/cli-darwin-x64@0.8.9': + '@capsule-run/cli-darwin-x64@0.8.10': optional: true - '@capsule-run/cli-linux-x64@0.8.9': + '@capsule-run/cli-linux-x64@0.8.10': optional: true - '@capsule-run/cli-win32-x64@0.8.9': + '@capsule-run/cli-win32-x64@0.8.10': optional: true - '@capsule-run/cli@0.8.9': + '@capsule-run/cli@0.8.10': optionalDependencies: - '@capsule-run/cli-darwin-arm64': 0.8.9 - '@capsule-run/cli-darwin-x64': 0.8.9 - '@capsule-run/cli-linux-x64': 0.8.9 - '@capsule-run/cli-win32-x64': 0.8.9 + '@capsule-run/cli-darwin-arm64': 0.8.10 + '@capsule-run/cli-darwin-x64': 0.8.10 + '@capsule-run/cli-linux-x64': 0.8.10 + '@capsule-run/cli-win32-x64': 0.8.10 - '@capsule-run/sdk@0.8.9(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)': + '@capsule-run/sdk@0.8.10(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)': dependencies: - '@bytecodealliance/jco': 1.18.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@bytecodealliance/jco': 1.19.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) esbuild: 0.27.7 typescript: 5.9.3 unenv: 2.0.0-rc.24 From 84981f221ca8852dcd705c3029065cdb51053b51 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Fri, 24 Apr 2026 22:43:34 +0200 Subject: [PATCH 5/5] test: update mv command test to use fs.cp and configure vitest execution script --- packages/bash/package.json | 2 +- packages/bash/src/commands/mv/mv.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bash/package.json b/packages/bash/package.json index c9a145d..be9cce0 100644 --- a/packages/bash/package.json +++ b/packages/bash/package.json @@ -15,7 +15,7 @@ ".": "./src/index.ts" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest run" }, "license": "Apache-2.0", "packageManager": "pnpm@10.21.0", diff --git a/packages/bash/src/commands/mv/mv.test.ts b/packages/bash/src/commands/mv/mv.test.ts index 9a7a43b..24d406e 100644 --- a/packages/bash/src/commands/mv/mv.test.ts +++ b/packages/bash/src/commands/mv/mv.test.ts @@ -76,7 +76,7 @@ describe('mv command', () => { expect(result.stdout).toContain('File moved ✔'); expect(executeCodeMock).toHaveBeenCalledWith( expect.anything(), - expect.stringContaining("fs.copyFile('/workspace/file1.txt'") + expect.stringContaining("fs.cp('/workspace/file1.txt'") ); });