Skip to content

Commit 2db918f

Browse files
committed
Add ZenFsAdapter for just-bash to operate on zen-fs filesystem
- Created ZenFsAdapter that implements IFileSystem interface - Removed redundant tests that only tested one library - Added tests demonstrating just-bash writing to zen-fs filesystem - Tests verify writes via just-bash are visible from zen-fs perspective
1 parent ab5a3e1 commit 2db918f

3 files changed

Lines changed: 264 additions & 84 deletions

File tree

Lines changed: 78 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,113 @@
11
import { Bash } from 'just-bash';
22
import { fs as zenfs, InMemory, configure } from '@zenfs/core';
3+
import { ZenFsAdapter } from '../src/ZenFsAdapter';
34

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

13-
it('supports complex file operations', async () => {
14-
const bash = new Bash();
15-
await bash.exec('mkdir -p /tmp/project/src');
16-
await bash.exec('echo "console.log(42)" > /tmp/project/src/index.js');
17-
await bash.exec('cp /tmp/project/src/index.js /tmp/project/src/backup.js');
18-
const result = await bash.exec('cat /tmp/project/src/backup.js');
19-
expect(result.stdout).toBe('console.log(42)\n');
20-
});
8+
beforeEach(async () => {
9+
await configure({ mounts: { '/': InMemory } });
10+
adapter = new ZenFsAdapter();
11+
zenfs.mkdirSync('/tmp', { recursive: true });
12+
zenfs.mkdirSync('/home', { recursive: true });
2113
});
2214

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

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

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

42-
describe('interoperability patterns', () => {
43-
beforeEach(async () => {
44-
await configure({ mounts: { '/': InMemory } });
25+
it('creates directories via just-bash, verifies via zen-fs', async () => {
26+
const bash = new Bash({ fs: adapter });
27+
28+
await bash.exec('mkdir -p /home/user/projects/myapp');
29+
await bash.exec('echo "# My App" > /home/user/projects/myapp/README.md');
30+
31+
expect(zenfs.existsSync('/home/user/projects/myapp')).toBe(true);
32+
expect(zenfs.existsSync('/home/user/projects/myapp/README.md')).toBe(true);
33+
expect(zenfs.readFileSync('/home/user/projects/myapp/README.md', 'utf-8')).toBe('# My App\n');
4534
});
4635

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

51-
const bash = new Bash({
52-
files: {
53-
'/workspace/input.txt': zenfs.readFileSync('/workspace/input.txt', 'utf-8')
54-
}
55-
});
39+
await bash.exec('echo "line1" > /tmp/log.txt');
40+
await bash.exec('echo "line2" >> /tmp/log.txt');
41+
await bash.exec('echo "line3" >> /tmp/log.txt');
5642

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

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

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

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

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

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

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

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

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

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

102-
describe('parallel usage patterns', () => {
103-
it('maintains separate filesystems for isolation', async () => {
104-
await configure({ mounts: { '/': InMemory } });
79+
describe('bidirectional file operations', () => {
80+
it('zen-fs writes, just-bash reads and processes', async () => {
81+
zenfs.writeFileSync('/tmp/data.csv', 'name,age\nAlice,30\nBob,25\nCharlie,35');
82+
83+
const bash = new Bash({ fs: adapter });
84+
const result = await bash.exec('cat /tmp/data.csv | grep -c ","');
85+
86+
expect(result.stdout.trim()).toBe('4');
87+
});
88+
89+
it('just-bash processes, zen-fs stores result', async () => {
90+
zenfs.writeFileSync('/tmp/numbers.txt', '10\n20\n30\n40\n50\n');
91+
92+
const bash = new Bash({ fs: adapter });
93+
await bash.exec('cat /tmp/numbers.txt | wc -l > /tmp/count.txt');
94+
95+
const count = zenfs.readFileSync('/tmp/count.txt', 'utf-8');
96+
expect(count.trim()).toBe('5');
97+
});
10598

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

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

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

115-
zenfs.writeFileSync('/shared.txt', 'shared data');
116-
expect(zenfs.readFileSync('/shared.txt', 'utf-8')).toBe('shared data');
109+
expect(zenfs.readFileSync('/workspace/output/host.txt', 'utf-8').trim()).toBe('localhost');
110+
expect(zenfs.readFileSync('/workspace/output/port.txt', 'utf-8').trim()).toBe('5432');
117111
});
118112
});
119113
});
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { fs as zenfs } from '@zenfs/core';
2+
import * as path from 'path';
3+
4+
export interface FsStat {
5+
isFile: boolean;
6+
isDirectory: boolean;
7+
isSymbolicLink: boolean;
8+
mode: number;
9+
size: number;
10+
mtime: Date;
11+
}
12+
13+
export interface MkdirOptions {
14+
recursive?: boolean;
15+
}
16+
17+
export interface RmOptions {
18+
recursive?: boolean;
19+
force?: boolean;
20+
}
21+
22+
export interface CpOptions {
23+
recursive?: boolean;
24+
}
25+
26+
export interface ReadFileOptions {
27+
encoding?: string | null;
28+
}
29+
30+
export interface WriteFileOptions {
31+
encoding?: string;
32+
}
33+
34+
export type FileContent = string | Uint8Array;
35+
36+
export class ZenFsAdapter {
37+
async readFile(filePath: string, options?: ReadFileOptions | string): Promise<string> {
38+
const encoding = typeof options === 'string' ? options : options?.encoding ?? 'utf-8';
39+
return zenfs.readFileSync(filePath, encoding as BufferEncoding);
40+
}
41+
42+
async readFileBuffer(filePath: string): Promise<Uint8Array> {
43+
const buffer = zenfs.readFileSync(filePath);
44+
if (typeof buffer === 'string') {
45+
return new TextEncoder().encode(buffer);
46+
}
47+
return new Uint8Array(buffer);
48+
}
49+
50+
async writeFile(filePath: string, content: FileContent, options?: WriteFileOptions | string): Promise<void> {
51+
this.ensureParentDirs(filePath);
52+
zenfs.writeFileSync(filePath, content);
53+
}
54+
55+
async appendFile(filePath: string, content: FileContent, options?: WriteFileOptions | string): Promise<void> {
56+
this.ensureParentDirs(filePath);
57+
zenfs.appendFileSync(filePath, content);
58+
}
59+
60+
async exists(filePath: string): Promise<boolean> {
61+
return zenfs.existsSync(filePath);
62+
}
63+
64+
async stat(filePath: string): Promise<FsStat> {
65+
const stats = zenfs.statSync(filePath);
66+
return {
67+
isFile: stats.isFile(),
68+
isDirectory: stats.isDirectory(),
69+
isSymbolicLink: stats.isSymbolicLink(),
70+
mode: stats.mode,
71+
size: stats.size,
72+
mtime: stats.mtime,
73+
};
74+
}
75+
76+
async lstat(filePath: string): Promise<FsStat> {
77+
const stats = zenfs.lstatSync(filePath);
78+
return {
79+
isFile: stats.isFile(),
80+
isDirectory: stats.isDirectory(),
81+
isSymbolicLink: stats.isSymbolicLink(),
82+
mode: stats.mode,
83+
size: stats.size,
84+
mtime: stats.mtime,
85+
};
86+
}
87+
88+
async mkdir(dirPath: string, options?: MkdirOptions): Promise<void> {
89+
zenfs.mkdirSync(dirPath, { recursive: options?.recursive });
90+
}
91+
92+
async readdir(dirPath: string): Promise<string[]> {
93+
return zenfs.readdirSync(dirPath) as string[];
94+
}
95+
96+
async rm(filePath: string, options?: RmOptions): Promise<void> {
97+
try {
98+
zenfs.rmSync(filePath, { recursive: options?.recursive, force: options?.force });
99+
} catch (e) {
100+
if (!options?.force) throw e;
101+
}
102+
}
103+
104+
async cp(src: string, dest: string, options?: CpOptions): Promise<void> {
105+
const srcStat = zenfs.statSync(src);
106+
if (srcStat.isDirectory()) {
107+
if (!options?.recursive) {
108+
throw new Error(`EISDIR: is a directory, cp '${src}'`);
109+
}
110+
zenfs.mkdirSync(dest, { recursive: true });
111+
const entries = zenfs.readdirSync(src) as string[];
112+
for (const entry of entries) {
113+
await this.cp(path.posix.join(src, entry), path.posix.join(dest, entry), options);
114+
}
115+
} else {
116+
this.ensureParentDirs(dest);
117+
zenfs.copyFileSync(src, dest);
118+
}
119+
}
120+
121+
async mv(src: string, dest: string): Promise<void> {
122+
zenfs.renameSync(src, dest);
123+
}
124+
125+
resolvePath(base: string, filePath: string): string {
126+
if (filePath.startsWith('/')) {
127+
return path.posix.normalize(filePath);
128+
}
129+
return path.posix.normalize(path.posix.join(base, filePath));
130+
}
131+
132+
getAllPaths(): string[] {
133+
const paths: string[] = [];
134+
this.walkDir('/', paths);
135+
return paths;
136+
}
137+
138+
private walkDir(dir: string, paths: string[]): void {
139+
paths.push(dir);
140+
try {
141+
const entries = zenfs.readdirSync(dir) as string[];
142+
for (const entry of entries) {
143+
const fullPath = dir === '/' ? `/${entry}` : `${dir}/${entry}`;
144+
try {
145+
const stats = zenfs.lstatSync(fullPath);
146+
if (stats.isDirectory()) {
147+
this.walkDir(fullPath, paths);
148+
} else {
149+
paths.push(fullPath);
150+
}
151+
} catch {
152+
paths.push(fullPath);
153+
}
154+
}
155+
} catch {
156+
// Directory not readable, skip
157+
}
158+
}
159+
160+
async chmod(filePath: string, mode: number): Promise<void> {
161+
zenfs.chmodSync(filePath, mode);
162+
}
163+
164+
async symlink(target: string, linkPath: string): Promise<void> {
165+
this.ensureParentDirs(linkPath);
166+
zenfs.symlinkSync(target, linkPath);
167+
}
168+
169+
async link(existingPath: string, newPath: string): Promise<void> {
170+
this.ensureParentDirs(newPath);
171+
zenfs.linkSync(existingPath, newPath);
172+
}
173+
174+
async readlink(linkPath: string): Promise<string> {
175+
const result = zenfs.readlinkSync(linkPath);
176+
return typeof result === 'string' ? result : result.toString('utf-8');
177+
}
178+
179+
private ensureParentDirs(filePath: string): void {
180+
const dir = path.posix.dirname(filePath);
181+
if (dir && dir !== '/' && !zenfs.existsSync(dir)) {
182+
zenfs.mkdirSync(dir, { recursive: true });
183+
}
184+
}
185+
}

packages/zen-bash/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ZenFsAdapter } from './ZenFsAdapter';

0 commit comments

Comments
 (0)