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
162 changes: 78 additions & 84 deletions packages/zen-bash/__tests__/combined.test.ts
Original file line number Diff line number Diff line change
@@ -1,119 +1,113 @@
import { Bash } from 'just-bash';
import { fs as zenfs, InMemory, configure } from '@zenfs/core';
import { ZenFsAdapter } from '../src/ZenFsAdapter';

describe('zen-bash: combining just-bash with zen-fs', () => {
describe('using just-bash VirtualFs (default)', () => {
it('executes bash commands with built-in virtual filesystem', async () => {
const bash = new Bash();
await bash.exec('echo "hello from just-bash" > /tmp/test.txt');
const result = await bash.exec('cat /tmp/test.txt');
expect(result.stdout).toBe('hello from just-bash\n');
});
describe('zen-bash: just-bash with zen-fs filesystem', () => {
let adapter: ZenFsAdapter;

it('supports complex file operations', async () => {
const bash = new Bash();
await bash.exec('mkdir -p /tmp/project/src');
await bash.exec('echo "console.log(42)" > /tmp/project/src/index.js');
await bash.exec('cp /tmp/project/src/index.js /tmp/project/src/backup.js');
const result = await bash.exec('cat /tmp/project/src/backup.js');
expect(result.stdout).toBe('console.log(42)\n');
});
beforeEach(async () => {
await configure({ mounts: { '/': InMemory } });
adapter = new ZenFsAdapter();
zenfs.mkdirSync('/tmp', { recursive: true });
zenfs.mkdirSync('/home', { recursive: true });
});

describe('using zen-fs InMemory backend', () => {
beforeEach(async () => {
await configure({ mounts: { '/': InMemory } });
});
describe('just-bash operating on zen-fs filesystem via adapter', () => {
it('writes a file via just-bash, reads it via zen-fs', async () => {
const bash = new Bash({ fs: adapter });

it('performs file operations with zen-fs', async () => {
zenfs.writeFileSync('/data.txt', 'zen-fs content');
const content = zenfs.readFileSync('/data.txt', 'utf-8');
expect(content).toBe('zen-fs content');
});
await bash.exec('echo "hello from bash" > /tmp/test.txt');

it('creates directories and files', async () => {
zenfs.mkdirSync('/project/src', { recursive: true });
zenfs.writeFileSync('/project/src/main.ts', 'export const x = 1;');
const files = zenfs.readdirSync('/project/src');
expect(files).toContain('main.ts');
const content = zenfs.readFileSync('/tmp/test.txt', 'utf-8');
expect(content).toBe('hello from bash\n');
});
});

describe('interoperability patterns', () => {
beforeEach(async () => {
await configure({ mounts: { '/': InMemory } });
it('creates directories via just-bash, verifies via zen-fs', async () => {
const bash = new Bash({ fs: adapter });

await bash.exec('mkdir -p /home/user/projects/myapp');
await bash.exec('echo "# My App" > /home/user/projects/myapp/README.md');

expect(zenfs.existsSync('/home/user/projects/myapp')).toBe(true);
expect(zenfs.existsSync('/home/user/projects/myapp/README.md')).toBe(true);
expect(zenfs.readFileSync('/home/user/projects/myapp/README.md', 'utf-8')).toBe('# My App\n');
});

it('prepares files with zen-fs, processes with just-bash', async () => {
zenfs.mkdirSync('/workspace', { recursive: true });
zenfs.writeFileSync('/workspace/input.txt', 'line1\nline2\nline3\n');
it('appends to a file via just-bash, verifies via zen-fs', async () => {
const bash = new Bash({ fs: adapter });

const bash = new Bash({
files: {
'/workspace/input.txt': zenfs.readFileSync('/workspace/input.txt', 'utf-8')
}
});
await bash.exec('echo "line1" > /tmp/log.txt');
await bash.exec('echo "line2" >> /tmp/log.txt');
await bash.exec('echo "line3" >> /tmp/log.txt');

const result = await bash.exec('wc -l < /workspace/input.txt');
expect(result.stdout.trim()).toBe('3');
const content = zenfs.readFileSync('/tmp/log.txt', 'utf-8');
expect(content).toBe('line1\nline2\nline3\n');
});

it('uses just-bash for text processing, zen-fs for storage', async () => {
const bash = new Bash({
files: { '/data/users.json': '[{"name":"Alice"},{"name":"Bob"}]' }
});
it('copies files via just-bash, verifies via zen-fs', async () => {
const bash = new Bash({ fs: adapter });

const result = await bash.exec('cat /data/users.json | grep -o \'"name":"[^"]*"\' | wc -l');
expect(result.stdout.trim()).toBe('2');
await bash.exec('echo "original content" > /tmp/original.txt');
await bash.exec('cp /tmp/original.txt /tmp/copy.txt');

zenfs.mkdirSync('/processed', { recursive: true });
zenfs.writeFileSync('/processed/count.txt', result.stdout.trim());
expect(zenfs.readFileSync('/processed/count.txt', 'utf-8')).toBe('2');
expect(zenfs.existsSync('/tmp/copy.txt')).toBe(true);
expect(zenfs.readFileSync('/tmp/copy.txt', 'utf-8')).toBe('original content\n');
});

it('combines bash scripting with zen-fs file management', async () => {
zenfs.mkdirSync('/scripts', { recursive: true });
zenfs.writeFileSync('/scripts/config.env', 'APP_NAME=myapp\nAPP_VERSION=1.0.0');
it('moves files via just-bash, verifies via zen-fs', async () => {
const bash = new Bash({ fs: adapter });

const bash = new Bash({
files: {
'/scripts/config.env': zenfs.readFileSync('/scripts/config.env', 'utf-8')
}
});
await bash.exec('echo "movable content" > /tmp/source.txt');
await bash.exec('mv /tmp/source.txt /tmp/destination.txt');

const nameResult = await bash.exec('grep APP_NAME /scripts/config.env | cut -d= -f2');
const versionResult = await bash.exec('grep APP_VERSION /scripts/config.env | cut -d= -f2');
expect(zenfs.existsSync('/tmp/source.txt')).toBe(false);
expect(zenfs.existsSync('/tmp/destination.txt')).toBe(true);
expect(zenfs.readFileSync('/tmp/destination.txt', 'utf-8')).toBe('movable content\n');
});

expect(nameResult.stdout.trim()).toBe('myapp');
expect(versionResult.stdout.trim()).toBe('1.0.0');
it('removes files via just-bash, verifies via zen-fs', async () => {
const bash = new Bash({ fs: adapter });

zenfs.mkdirSync('/output', { recursive: true });
zenfs.writeFileSync('/output/app-info.json', JSON.stringify({
name: nameResult.stdout.trim(),
version: versionResult.stdout.trim()
}));
await bash.exec('echo "temporary" > /tmp/temp.txt');
expect(zenfs.existsSync('/tmp/temp.txt')).toBe(true);

const appInfo = JSON.parse(zenfs.readFileSync('/output/app-info.json', 'utf-8'));
expect(appInfo.name).toBe('myapp');
expect(appInfo.version).toBe('1.0.0');
await bash.exec('rm /tmp/temp.txt');
expect(zenfs.existsSync('/tmp/temp.txt')).toBe(false);
});
});

describe('parallel usage patterns', () => {
it('maintains separate filesystems for isolation', async () => {
await configure({ mounts: { '/': InMemory } });
describe('bidirectional file operations', () => {
it('zen-fs writes, just-bash reads and processes', async () => {
zenfs.writeFileSync('/tmp/data.csv', 'name,age\nAlice,30\nBob,25\nCharlie,35');

const bash = new Bash({ fs: adapter });
const result = await bash.exec('cat /tmp/data.csv | grep -c ","');

expect(result.stdout.trim()).toBe('4');
});

it('just-bash processes, zen-fs stores result', async () => {
zenfs.writeFileSync('/tmp/numbers.txt', '10\n20\n30\n40\n50\n');

const bash = new Bash({ fs: adapter });
await bash.exec('cat /tmp/numbers.txt | wc -l > /tmp/count.txt');

const count = zenfs.readFileSync('/tmp/count.txt', 'utf-8');
expect(count.trim()).toBe('5');
});

const bash1 = new Bash({ files: { '/config.txt': 'env=dev' } });
const bash2 = new Bash({ files: { '/config.txt': 'env=prod' } });
it('complex workflow: zen-fs setup, bash transform, zen-fs verify', async () => {
zenfs.mkdirSync('/workspace/input', { recursive: true });
zenfs.mkdirSync('/workspace/output', { recursive: true });
zenfs.writeFileSync('/workspace/input/config.env', 'DB_HOST=localhost\nDB_PORT=5432\nDB_NAME=mydb');

const result1 = await bash1.exec('cat /config.txt');
const result2 = await bash2.exec('cat /config.txt');
const bash = new Bash({ fs: adapter });

expect(result1.stdout).toBe('env=dev');
expect(result2.stdout).toBe('env=prod');
await bash.exec('grep DB_HOST /workspace/input/config.env | cut -d= -f2 > /workspace/output/host.txt');
await bash.exec('grep DB_PORT /workspace/input/config.env | cut -d= -f2 > /workspace/output/port.txt');

zenfs.writeFileSync('/shared.txt', 'shared data');
expect(zenfs.readFileSync('/shared.txt', 'utf-8')).toBe('shared data');
expect(zenfs.readFileSync('/workspace/output/host.txt', 'utf-8').trim()).toBe('localhost');
expect(zenfs.readFileSync('/workspace/output/port.txt', 'utf-8').trim()).toBe('5432');
});
});
});
185 changes: 185 additions & 0 deletions packages/zen-bash/src/ZenFsAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { fs as zenfs } from '@zenfs/core';
import * as path from 'path';

export interface FsStat {
isFile: boolean;
isDirectory: boolean;
isSymbolicLink: boolean;
mode: number;
size: number;
mtime: Date;
}

export interface MkdirOptions {
recursive?: boolean;
}

export interface RmOptions {
recursive?: boolean;
force?: boolean;
}

export interface CpOptions {
recursive?: boolean;
}

export interface ReadFileOptions {
encoding?: string | null;
}

export interface WriteFileOptions {
encoding?: string;
}

export type FileContent = string | Uint8Array;

export class ZenFsAdapter {
async readFile(filePath: string, options?: ReadFileOptions | string): Promise<string> {
const encoding = typeof options === 'string' ? options : options?.encoding ?? 'utf-8';
return zenfs.readFileSync(filePath, encoding as BufferEncoding);
}

async readFileBuffer(filePath: string): Promise<Uint8Array> {
const buffer = zenfs.readFileSync(filePath);
if (typeof buffer === 'string') {
return new TextEncoder().encode(buffer);
}
return new Uint8Array(buffer);
}

async writeFile(filePath: string, content: FileContent, options?: WriteFileOptions | string): Promise<void> {
this.ensureParentDirs(filePath);
zenfs.writeFileSync(filePath, content);
}

async appendFile(filePath: string, content: FileContent, options?: WriteFileOptions | string): Promise<void> {
this.ensureParentDirs(filePath);
zenfs.appendFileSync(filePath, content);
}

async exists(filePath: string): Promise<boolean> {
return zenfs.existsSync(filePath);
}

async stat(filePath: string): Promise<FsStat> {
const stats = zenfs.statSync(filePath);
return {
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
isSymbolicLink: stats.isSymbolicLink(),
mode: stats.mode,
size: stats.size,
mtime: stats.mtime,
};
}

async lstat(filePath: string): Promise<FsStat> {
const stats = zenfs.lstatSync(filePath);
return {
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
isSymbolicLink: stats.isSymbolicLink(),
mode: stats.mode,
size: stats.size,
mtime: stats.mtime,
};
}

async mkdir(dirPath: string, options?: MkdirOptions): Promise<void> {
zenfs.mkdirSync(dirPath, { recursive: options?.recursive });
}

async readdir(dirPath: string): Promise<string[]> {
return zenfs.readdirSync(dirPath) as string[];
}

async rm(filePath: string, options?: RmOptions): Promise<void> {
try {
zenfs.rmSync(filePath, { recursive: options?.recursive, force: options?.force });
} catch (e) {
if (!options?.force) throw e;
}
}

async cp(src: string, dest: string, options?: CpOptions): Promise<void> {
const srcStat = zenfs.statSync(src);
if (srcStat.isDirectory()) {
if (!options?.recursive) {
throw new Error(`EISDIR: is a directory, cp '${src}'`);
}
zenfs.mkdirSync(dest, { recursive: true });
const entries = zenfs.readdirSync(src) as string[];
for (const entry of entries) {
await this.cp(path.posix.join(src, entry), path.posix.join(dest, entry), options);
}
} else {
this.ensureParentDirs(dest);
zenfs.copyFileSync(src, dest);
}
}

async mv(src: string, dest: string): Promise<void> {
zenfs.renameSync(src, dest);
}

resolvePath(base: string, filePath: string): string {
if (filePath.startsWith('/')) {
return path.posix.normalize(filePath);
}
return path.posix.normalize(path.posix.join(base, filePath));
}

getAllPaths(): string[] {
const paths: string[] = [];
this.walkDir('/', paths);
return paths;
}

private walkDir(dir: string, paths: string[]): void {
paths.push(dir);
try {
const entries = zenfs.readdirSync(dir) as string[];
for (const entry of entries) {
const fullPath = dir === '/' ? `/${entry}` : `${dir}/${entry}`;
try {
const stats = zenfs.lstatSync(fullPath);
if (stats.isDirectory()) {
this.walkDir(fullPath, paths);
} else {
paths.push(fullPath);
}
} catch {
paths.push(fullPath);
}
}
} catch {
// Directory not readable, skip
}
}

async chmod(filePath: string, mode: number): Promise<void> {
zenfs.chmodSync(filePath, mode);
}

async symlink(target: string, linkPath: string): Promise<void> {
this.ensureParentDirs(linkPath);
zenfs.symlinkSync(target, linkPath);
}

async link(existingPath: string, newPath: string): Promise<void> {
this.ensureParentDirs(newPath);
zenfs.linkSync(existingPath, newPath);
}

async readlink(linkPath: string): Promise<string> {
const result = zenfs.readlinkSync(linkPath);
return typeof result === 'string' ? result : result.toString('utf-8');
}

private ensureParentDirs(filePath: string): void {
const dir = path.posix.dirname(filePath);
if (dir && dir !== '/' && !zenfs.existsSync(dir)) {
zenfs.mkdirSync(dir, { recursive: true });
}
}
}
1 change: 1 addition & 0 deletions packages/zen-bash/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ZenFsAdapter } from './ZenFsAdapter';