shell-dsl 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,6 +31,7 @@ bun add shell-dsl memfs
31
31
 
32
32
  - **Sandboxed execution** — No host OS access; all commands run in-process
33
33
  - **Virtual filesystem** — Uses memfs for complete isolation from the real filesystem
34
+ - **Real filesystem** — Optional sandboxed access to real files with path containment and permissions
34
35
  - **Explicit command registry** — Only registered commands can execute
35
36
  - **Automatic escaping** — Interpolated values are escaped by default for safety
36
37
  - **POSIX-inspired syntax** — Pipes, redirects, control flow operators, and more
@@ -440,7 +441,7 @@ import { builtinCommands } from "shell-dsl/commands";
440
441
  Or import individually:
441
442
 
442
443
  ```ts
443
- import { echo, cat, grep, wc, cp, mv, touch, tee } from "shell-dsl/commands";
444
+ import { echo, cat, grep, wc, cp, mv, touch, tee, tree } from "shell-dsl/commands";
444
445
  ```
445
446
 
446
447
  | Command | Description |
@@ -461,6 +462,7 @@ import { echo, cat, grep, wc, cp, mv, touch, tee } from "shell-dsl/commands";
461
462
  | `mv` | Move/rename files/directories (`-n` no-clobber) |
462
463
  | `touch` | Create empty files or update timestamps (`-c` no-create) |
463
464
  | `tee` | Duplicate stdin to stdout and files (`-a` append) |
465
+ | `tree` | Display directory structure as tree (`-a` all, `-d` dirs only, `-L <n>` depth) |
464
466
  | `test` / `[` | File and string tests (`-f`, `-d`, `-e`, `-z`, `-n`, `=`, `!=`) |
465
467
  | `true` | Exit with code 0 |
466
468
  | `false` | Exit with code 1 |
@@ -508,6 +510,86 @@ interface VirtualFS {
508
510
  }
509
511
  ```
510
512
 
513
+ ## Real Filesystem Access
514
+
515
+ For scenarios where you need to access the real filesystem with sandboxing, use `FileSystem` or `ReadOnlyFileSystem`:
516
+
517
+ ```ts
518
+ import { createShellDSL, FileSystem } from "shell-dsl";
519
+ import { builtinCommands } from "shell-dsl/commands";
520
+
521
+ // Mount a directory with permission rules
522
+ const fs = new FileSystem("./project", {
523
+ ".env": "excluded", // Cannot read or write
524
+ ".git/**": "excluded", // Block entire directory
525
+ "config/**": "read-only", // Can read, cannot write
526
+ "src/**": "read-write", // Full access (default)
527
+ });
528
+
529
+ const sh = createShellDSL({
530
+ fs,
531
+ cwd: "/",
532
+ env: {},
533
+ commands: builtinCommands,
534
+ });
535
+
536
+ await sh`cat /src/index.ts`.text(); // Works
537
+ await sh`cat /.env`.text(); // Throws: excluded
538
+ await sh`echo "x" > /config/app.json`; // Throws: read-only
539
+ ```
540
+
541
+ ### Permission Types
542
+
543
+ | Permission | Read | Write |
544
+ |------------|------|-------|
545
+ | `"read-write"` | Yes | Yes |
546
+ | `"read-only"` | Yes | No |
547
+ | `"excluded"` | No | No |
548
+
549
+ ### Rule Specificity
550
+
551
+ When multiple rules match, the most specific wins:
552
+
553
+ 1. More path segments: `a/b/c` beats `a/b`
554
+ 2. Literal beats wildcard: `config/app.json` beats `config/*`
555
+ 3. Single wildcard beats double: `src/*` beats `src/**`
556
+
557
+ ```ts
558
+ const fs = new FileSystem("./project", {
559
+ "**": "read-only", // Default: read-only
560
+ "src/**": "read-write", // Override for src/
561
+ "src/generated/**": "excluded", // But not generated files
562
+ });
563
+ ```
564
+
565
+ ### ReadOnlyFileSystem
566
+
567
+ Convenience class that defaults all paths to read-only:
568
+
569
+ ```ts
570
+ import { ReadOnlyFileSystem } from "shell-dsl";
571
+
572
+ const fs = new ReadOnlyFileSystem("./docs");
573
+
574
+ // All writes blocked by default
575
+ await fs.writeFile("/file.txt", "x"); // Throws: read-only
576
+
577
+ // Can still exclude or allow specific paths
578
+ const fs2 = new ReadOnlyFileSystem("./docs", {
579
+ "drafts/**": "read-write", // Allow writes here
580
+ ".internal/**": "excluded", // Block completely
581
+ });
582
+ ```
583
+
584
+ ### Full System Access
585
+
586
+ Omit the mount path for unrestricted access, but this is the same as just passing `fs` from `node:fs`:
587
+
588
+ ```ts
589
+ const fs = new FileSystem(); // Full filesystem access same as fs from node:fs
590
+ ```
591
+
592
+
511
593
  ## Low-Level API
512
594
 
513
595
  For advanced use cases (custom tooling, AST inspection):
@@ -569,6 +651,9 @@ import type {
569
651
  ExecResult,
570
652
  ShellConfig,
571
653
  RawValue,
654
+ Permission,
655
+ PermissionRules,
656
+ UnderlyingFS,
572
657
  } from "shell-dsl";
573
658
  ```
574
659
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "shell-dsl",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "commonjs"
5
5
  }
@@ -29,9 +29,13 @@ var __export = (target, all) => {
29
29
  // src/fs/index.ts
30
30
  var exports_fs = {};
31
31
  __export(exports_fs, {
32
- createVirtualFS: () => import_memfs_adapter.createVirtualFS
32
+ createVirtualFS: () => import_memfs_adapter.createVirtualFS,
33
+ ReadOnlyFileSystem: () => import_readonly_fs.ReadOnlyFileSystem,
34
+ FileSystem: () => import_real_fs.FileSystem
33
35
  });
34
36
  module.exports = __toCommonJS(exports_fs);
35
37
  var import_memfs_adapter = require("./memfs-adapter.cjs");
38
+ var import_real_fs = require("./real-fs.cjs");
39
+ var import_readonly_fs = require("./readonly-fs.cjs");
36
40
 
37
- //# debugId=EFC1FC4A0309E57D64756E2164756E21
41
+ //# debugId=AC77355F139E98D864756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/fs/index.ts"],
4
4
  "sourcesContent": [
5
- "export { createVirtualFS } from \"./memfs-adapter.cjs\";\n"
5
+ "export { createVirtualFS } from \"./memfs-adapter.cjs\";\nexport { FileSystem, type Permission, type PermissionRules, type UnderlyingFS } from \"./real-fs.cjs\";\nexport { ReadOnlyFileSystem } from \"./readonly-fs.cjs\";\n"
6
6
  ],
7
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAgC,IAAhC;",
8
- "debugId": "EFC1FC4A0309E57D64756E2164756E21",
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAgC,IAAhC;AACqF,IAArF;AACmC,IAAnC;",
8
+ "debugId": "AC77355F139E98D864756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -0,0 +1,47 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
6
+ var __toCommonJS = (from) => {
7
+ var entry = __moduleCache.get(from), desc;
8
+ if (entry)
9
+ return entry;
10
+ entry = __defProp({}, "__esModule", { value: true });
11
+ if (from && typeof from === "object" || typeof from === "function")
12
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
+ get: () => from[key],
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ }));
16
+ __moduleCache.set(from, entry);
17
+ return entry;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+
29
+ // src/fs/readonly-fs.ts
30
+ var exports_readonly_fs = {};
31
+ __export(exports_readonly_fs, {
32
+ ReadOnlyFileSystem: () => ReadOnlyFileSystem
33
+ });
34
+ module.exports = __toCommonJS(exports_readonly_fs);
35
+ var import_real_fs = require("./real-fs.cjs");
36
+
37
+ class ReadOnlyFileSystem extends import_real_fs.FileSystem {
38
+ constructor(mountPath, permissions, fs) {
39
+ const mergedPermissions = {
40
+ "**": "read-only",
41
+ ...permissions
42
+ };
43
+ super(mountPath, mergedPermissions, fs);
44
+ }
45
+ }
46
+
47
+ //# debugId=EA698A711F5E3A5564756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/fs/readonly-fs.ts"],
4
+ "sourcesContent": [
5
+ "import { FileSystem, type PermissionRules, type UnderlyingFS } from \"./real-fs.cjs\";\n\nexport class ReadOnlyFileSystem extends FileSystem {\n constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS) {\n // Merge user permissions with base read-only rule\n const mergedPermissions: PermissionRules = {\n \"**\": \"read-only\",\n ...permissions,\n };\n super(mountPath, mergedPermissions, fs);\n }\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAoE,IAApE;AAAA;AAEO,MAAM,2BAA2B,0BAAW;AAAA,EACjD,WAAW,CAAC,WAAoB,aAA+B,IAAmB;AAAA,IAEhF,MAAM,oBAAqC;AAAA,MACzC,MAAM;AAAA,SACH;AAAA,IACL;AAAA,IACA,MAAM,WAAW,mBAAmB,EAAE;AAAA;AAE1C;",
8
+ "debugId": "EA698A711F5E3A5564756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,311 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
19
+ var __toCommonJS = (from) => {
20
+ var entry = __moduleCache.get(from), desc;
21
+ if (entry)
22
+ return entry;
23
+ entry = __defProp({}, "__esModule", { value: true });
24
+ if (from && typeof from === "object" || typeof from === "function")
25
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
26
+ get: () => from[key],
27
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
28
+ }));
29
+ __moduleCache.set(from, entry);
30
+ return entry;
31
+ };
32
+ var __export = (target, all) => {
33
+ for (var name in all)
34
+ __defProp(target, name, {
35
+ get: all[name],
36
+ enumerable: true,
37
+ configurable: true,
38
+ set: (newValue) => all[name] = () => newValue
39
+ });
40
+ };
41
+
42
+ // src/fs/real-fs.ts
43
+ var exports_real_fs = {};
44
+ __export(exports_real_fs, {
45
+ FileSystem: () => FileSystem
46
+ });
47
+ module.exports = __toCommonJS(exports_real_fs);
48
+ var path = __toESM(require("path"));
49
+ var nodeFs = __toESM(require("node:fs/promises"));
50
+ var defaultFS = { promises: nodeFs };
51
+
52
+ class FileSystem {
53
+ mountBase;
54
+ rules;
55
+ underlyingFs;
56
+ constructor(mountPath, permissions, fs) {
57
+ this.mountBase = mountPath ? path.resolve(mountPath) : null;
58
+ this.rules = this.compileRules(permissions ?? {});
59
+ this.underlyingFs = fs ?? defaultFS;
60
+ }
61
+ compileRules(permissions) {
62
+ return Object.entries(permissions).map(([pattern, permission]) => ({
63
+ pattern,
64
+ permission,
65
+ specificity: this.calculateSpecificity(pattern)
66
+ })).sort((a, b) => b.specificity - a.specificity);
67
+ }
68
+ calculateSpecificity(pattern) {
69
+ const segments = pattern.split("/").filter(Boolean);
70
+ let score = segments.length * 1000;
71
+ for (const seg of segments) {
72
+ if (seg === "**")
73
+ score += 0;
74
+ else if (seg.includes("*"))
75
+ score += 1;
76
+ else
77
+ score += 10;
78
+ }
79
+ return score;
80
+ }
81
+ getPermission(virtualPath) {
82
+ const normalized = virtualPath.replace(/^\/+/, "");
83
+ for (const rule of this.rules) {
84
+ if (this.matchGlob(rule.pattern, normalized)) {
85
+ return rule.permission;
86
+ }
87
+ }
88
+ return "read-write";
89
+ }
90
+ matchGlob(pattern, filePath) {
91
+ const regex = pattern.split("/").map((seg) => {
92
+ if (seg === "**")
93
+ return ".*";
94
+ return seg.replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
95
+ }).join("/");
96
+ return new RegExp(`^${regex}$`).test(filePath);
97
+ }
98
+ checkPermission(virtualPath, operation) {
99
+ const perm = this.getPermission(virtualPath);
100
+ if (perm === "excluded") {
101
+ throw new Error(`Access denied: "${virtualPath}" is excluded`);
102
+ }
103
+ if (operation === "write" && perm === "read-only") {
104
+ throw new Error(`Access denied: "${virtualPath}" is read-only`);
105
+ }
106
+ }
107
+ resolveSafePath(virtualPath) {
108
+ if (this.mountBase === null) {
109
+ return path.resolve(virtualPath);
110
+ }
111
+ const segments = virtualPath.split("/").filter(Boolean);
112
+ let depth = 0;
113
+ for (const seg of segments) {
114
+ if (seg === "..") {
115
+ depth--;
116
+ if (depth < 0) {
117
+ throw new Error(`Path traversal blocked: "${virtualPath}" escapes mount point`);
118
+ }
119
+ } else if (seg !== ".") {
120
+ depth++;
121
+ }
122
+ }
123
+ const normalized = path.normalize(virtualPath);
124
+ const relativePath = normalized.startsWith("/") ? normalized.slice(1) : normalized;
125
+ const realPath = path.join(this.mountBase, relativePath);
126
+ const resolved = path.resolve(realPath);
127
+ if (!resolved.startsWith(this.mountBase + path.sep) && resolved !== this.mountBase) {
128
+ throw new Error(`Path traversal blocked: "${virtualPath}" escapes mount point`);
129
+ }
130
+ return resolved;
131
+ }
132
+ async readFile(filePath) {
133
+ this.checkPermission(filePath, "read");
134
+ const realPath = this.resolveSafePath(filePath);
135
+ const content = await this.underlyingFs.promises.readFile(realPath);
136
+ return Buffer.from(content);
137
+ }
138
+ async readdir(dirPath) {
139
+ this.checkPermission(dirPath, "read");
140
+ const realPath = this.resolveSafePath(dirPath);
141
+ const entries = await this.underlyingFs.promises.readdir(realPath);
142
+ return entries.map(String);
143
+ }
144
+ async stat(filePath) {
145
+ this.checkPermission(filePath, "read");
146
+ const realPath = this.resolveSafePath(filePath);
147
+ const stats = await this.underlyingFs.promises.stat(realPath);
148
+ return {
149
+ isFile: () => stats.isFile(),
150
+ isDirectory: () => stats.isDirectory(),
151
+ size: stats.size,
152
+ mtime: stats.mtime
153
+ };
154
+ }
155
+ async exists(filePath) {
156
+ try {
157
+ this.checkPermission(filePath, "read");
158
+ const realPath = this.resolveSafePath(filePath);
159
+ await this.underlyingFs.promises.stat(realPath);
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+ async writeFile(filePath, data) {
166
+ this.checkPermission(filePath, "write");
167
+ const realPath = this.resolveSafePath(filePath);
168
+ await this.underlyingFs.promises.writeFile(realPath, data);
169
+ }
170
+ async appendFile(filePath, data) {
171
+ this.checkPermission(filePath, "write");
172
+ const realPath = this.resolveSafePath(filePath);
173
+ await this.underlyingFs.promises.appendFile(realPath, data);
174
+ }
175
+ async mkdir(dirPath, opts) {
176
+ this.checkPermission(dirPath, "write");
177
+ const realPath = this.resolveSafePath(dirPath);
178
+ await this.underlyingFs.promises.mkdir(realPath, opts);
179
+ }
180
+ async rm(filePath, opts) {
181
+ this.checkPermission(filePath, "write");
182
+ const realPath = this.resolveSafePath(filePath);
183
+ await this.underlyingFs.promises.rm(realPath, opts);
184
+ }
185
+ resolve(...paths) {
186
+ return path.resolve("/", ...paths);
187
+ }
188
+ dirname(filePath) {
189
+ return path.dirname(filePath);
190
+ }
191
+ basename(filePath) {
192
+ return path.basename(filePath);
193
+ }
194
+ async glob(pattern, opts) {
195
+ const cwd = opts?.cwd ?? "/";
196
+ this.checkPermission(cwd, "read");
197
+ const matches = await this.expandGlob(pattern, cwd);
198
+ return matches.filter((p) => this.getPermission(p) !== "excluded").sort();
199
+ }
200
+ async expandGlob(pattern, cwd) {
201
+ const patterns = this.expandBraces(pattern);
202
+ const allMatches = [];
203
+ for (const pat of patterns) {
204
+ const matches = await this.matchPattern(pat, cwd);
205
+ allMatches.push(...matches);
206
+ }
207
+ return [...new Set(allMatches)].sort();
208
+ }
209
+ expandBraces(pattern) {
210
+ const braceMatch = pattern.match(/\{([^{}]+)\}/);
211
+ if (!braceMatch)
212
+ return [pattern];
213
+ const before = pattern.slice(0, braceMatch.index);
214
+ const after = pattern.slice(braceMatch.index + braceMatch[0].length);
215
+ const options = braceMatch[1].split(",");
216
+ const results = [];
217
+ for (const opt of options) {
218
+ const expanded = this.expandBraces(before + opt + after);
219
+ results.push(...expanded);
220
+ }
221
+ return results;
222
+ }
223
+ async matchPattern(pattern, cwd) {
224
+ const parts = pattern.split("/").filter((p) => p !== "");
225
+ const isAbsolute = pattern.startsWith("/");
226
+ const startDir = isAbsolute ? "/" : cwd;
227
+ return this.matchParts(parts, startDir);
228
+ }
229
+ async matchParts(parts, currentPath) {
230
+ if (parts.length === 0) {
231
+ return [currentPath];
232
+ }
233
+ const [part, ...rest] = parts;
234
+ if (part === "**") {
235
+ const results = [];
236
+ const withoutStar = await this.matchParts(rest, currentPath);
237
+ results.push(...withoutStar);
238
+ try {
239
+ const realPath = this.resolveSafePath(currentPath);
240
+ const entries = await this.underlyingFs.promises.readdir(realPath);
241
+ for (const entry of entries) {
242
+ const entryPath = path.posix.join(currentPath, String(entry));
243
+ try {
244
+ const entryRealPath = this.resolveSafePath(entryPath);
245
+ const stat = await this.underlyingFs.promises.stat(entryRealPath);
246
+ if (stat.isDirectory()) {
247
+ const subMatches = await this.matchParts(parts, entryPath);
248
+ results.push(...subMatches);
249
+ }
250
+ } catch {}
251
+ }
252
+ } catch {}
253
+ return results;
254
+ }
255
+ const regex = this.globToRegex(part);
256
+ try {
257
+ const realPath = this.resolveSafePath(currentPath);
258
+ const entries = await this.underlyingFs.promises.readdir(realPath);
259
+ const results = [];
260
+ for (const entry of entries) {
261
+ const entryName = String(entry);
262
+ if (regex.test(entryName)) {
263
+ const entryPath = path.posix.join(currentPath, entryName);
264
+ if (rest.length === 0) {
265
+ results.push(entryPath);
266
+ } else {
267
+ try {
268
+ const entryRealPath = this.resolveSafePath(entryPath);
269
+ const stat = await this.underlyingFs.promises.stat(entryRealPath);
270
+ if (stat.isDirectory()) {
271
+ const subMatches = await this.matchParts(rest, entryPath);
272
+ results.push(...subMatches);
273
+ }
274
+ } catch {}
275
+ }
276
+ }
277
+ }
278
+ return results;
279
+ } catch {
280
+ return [];
281
+ }
282
+ }
283
+ globToRegex(pattern) {
284
+ let regex = "^";
285
+ for (let i = 0;i < pattern.length; i++) {
286
+ const char = pattern[i];
287
+ if (char === "*") {
288
+ regex += "[^/]*";
289
+ } else if (char === "?") {
290
+ regex += "[^/]";
291
+ } else if (char === "[") {
292
+ let j = i + 1;
293
+ let classContent = "";
294
+ while (j < pattern.length && pattern[j] !== "]") {
295
+ classContent += pattern[j];
296
+ j++;
297
+ }
298
+ regex += `[${classContent}]`;
299
+ i = j;
300
+ } else if (".+^${}()|\\".includes(char)) {
301
+ regex += "\\" + char;
302
+ } else {
303
+ regex += char;
304
+ }
305
+ }
306
+ regex += "$";
307
+ return new RegExp(regex);
308
+ }
309
+ }
310
+
311
+ //# debugId=FE5125D36453662C64756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/fs/real-fs.ts"],
4
+ "sourcesContent": [
5
+ "import * as path from \"path\";\nimport * as nodeFs from \"node:fs/promises\";\nimport type { VirtualFS, FileStat } from \"../types.cjs\";\n\nexport type Permission = \"read-write\" | \"read-only\" | \"excluded\";\nexport type PermissionRules = Record<string, Permission>;\n\n// Minimal interface for the underlying fs (compatible with node:fs and memfs)\nexport interface UnderlyingFS {\n promises: {\n readFile(path: string): Promise<Buffer | Uint8Array | string>;\n readdir(path: string): Promise<string[]>;\n stat(path: string): Promise<{\n isFile(): boolean;\n isDirectory(): boolean;\n size: number;\n mtime: Date;\n }>;\n writeFile(path: string, data: Buffer | string): Promise<void>;\n appendFile(path: string, data: Buffer | string): Promise<void>;\n mkdir(path: string, opts?: { recursive?: boolean }): Promise<string | undefined | void>;\n rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void>;\n };\n}\n\n// Default: use real node:fs\nconst defaultFS: UnderlyingFS = { promises: nodeFs };\n\ninterface CompiledRule {\n pattern: string;\n permission: Permission;\n specificity: number;\n}\n\nexport class FileSystem implements VirtualFS {\n private readonly mountBase: string | null;\n private readonly rules: CompiledRule[];\n protected readonly underlyingFs: UnderlyingFS;\n\n constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS) {\n this.mountBase = mountPath ? path.resolve(mountPath) : null;\n this.rules = this.compileRules(permissions ?? {});\n this.underlyingFs = fs ?? defaultFS;\n }\n\n private compileRules(permissions: PermissionRules): CompiledRule[] {\n return Object.entries(permissions)\n .map(([pattern, permission]) => ({\n pattern,\n permission,\n specificity: this.calculateSpecificity(pattern),\n }))\n .sort((a, b) => b.specificity - a.specificity); // highest first\n }\n\n private calculateSpecificity(pattern: string): number {\n const segments = pattern.split(\"/\").filter(Boolean);\n let score = segments.length * 1000; // segment count is primary\n\n for (const seg of segments) {\n if (seg === \"**\") score += 0;\n else if (seg.includes(\"*\")) score += 1;\n else score += 10; // literal segment\n }\n return score;\n }\n\n protected getPermission(virtualPath: string): Permission {\n const normalized = virtualPath.replace(/^\\/+/, \"\"); // strip leading slashes\n\n for (const rule of this.rules) {\n if (this.matchGlob(rule.pattern, normalized)) {\n return rule.permission;\n }\n }\n return \"read-write\"; // default\n }\n\n private matchGlob(pattern: string, filePath: string): boolean {\n // Convert glob to regex\n // ** matches any path segments, * matches within segment\n const regex = pattern\n .split(\"/\")\n .map((seg) => {\n if (seg === \"**\") return \".*\";\n return seg.replace(/\\*/g, \"[^/]*\").replace(/\\?/g, \"[^/]\");\n })\n .join(\"/\");\n return new RegExp(`^${regex}$`).test(filePath);\n }\n\n protected checkPermission(virtualPath: string, operation: \"read\" | \"write\"): void {\n const perm = this.getPermission(virtualPath);\n\n if (perm === \"excluded\") {\n throw new Error(`Access denied: \"${virtualPath}\" is excluded`);\n }\n if (operation === \"write\" && perm === \"read-only\") {\n throw new Error(`Access denied: \"${virtualPath}\" is read-only`);\n }\n }\n\n private resolveSafePath(virtualPath: string): string {\n if (this.mountBase === null) {\n return path.resolve(virtualPath);\n }\n\n // Check for path traversal by tracking depth\n const segments = virtualPath.split(\"/\").filter(Boolean);\n let depth = 0;\n for (const seg of segments) {\n if (seg === \"..\") {\n depth--;\n if (depth < 0) {\n throw new Error(`Path traversal blocked: \"${virtualPath}\" escapes mount point`);\n }\n } else if (seg !== \".\") {\n depth++;\n }\n }\n\n const normalized = path.normalize(virtualPath);\n const relativePath = normalized.startsWith(\"/\") ? normalized.slice(1) : normalized;\n const realPath = path.join(this.mountBase, relativePath);\n const resolved = path.resolve(realPath);\n\n // Double-check containment (defense in depth)\n if (!resolved.startsWith(this.mountBase + path.sep) && resolved !== this.mountBase) {\n throw new Error(`Path traversal blocked: \"${virtualPath}\" escapes mount point`);\n }\n\n return resolved;\n }\n\n // Read operations\n async readFile(filePath: string): Promise<Buffer> {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const content = await this.underlyingFs.promises.readFile(realPath);\n return Buffer.from(content);\n }\n\n async readdir(dirPath: string): Promise<string[]> {\n this.checkPermission(dirPath, \"read\");\n const realPath = this.resolveSafePath(dirPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n return entries.map(String);\n }\n\n async stat(filePath: string): Promise<FileStat> {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const stats = await this.underlyingFs.promises.stat(realPath);\n return {\n isFile: () => stats.isFile(),\n isDirectory: () => stats.isDirectory(),\n size: stats.size,\n mtime: stats.mtime,\n };\n }\n\n async exists(filePath: string): Promise<boolean> {\n try {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.stat(realPath);\n return true;\n } catch {\n return false;\n }\n }\n\n // Write operations\n async writeFile(filePath: string, data: Buffer | string): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.writeFile(realPath, data);\n }\n\n async appendFile(filePath: string, data: Buffer | string): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.appendFile(realPath, data);\n }\n\n async mkdir(dirPath: string, opts?: { recursive?: boolean }): Promise<void> {\n this.checkPermission(dirPath, \"write\");\n const realPath = this.resolveSafePath(dirPath);\n await this.underlyingFs.promises.mkdir(realPath, opts);\n }\n\n async rm(filePath: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.rm(realPath, opts);\n }\n\n // Path utilities (no permission check needed)\n resolve(...paths: string[]): string {\n return path.resolve(\"/\", ...paths);\n }\n\n dirname(filePath: string): string {\n return path.dirname(filePath);\n }\n\n basename(filePath: string): string {\n return path.basename(filePath);\n }\n\n // Glob with permission filtering\n async glob(pattern: string, opts?: { cwd?: string }): Promise<string[]> {\n const cwd = opts?.cwd ?? \"/\";\n this.checkPermission(cwd, \"read\");\n\n const matches = await this.expandGlob(pattern, cwd);\n\n // Filter out excluded paths\n return matches.filter((p) => this.getPermission(p) !== \"excluded\").sort();\n }\n\n // Glob expansion (similar to memfs-adapter implementation)\n private async expandGlob(pattern: string, cwd: string): Promise<string[]> {\n // Handle brace expansion first\n const patterns = this.expandBraces(pattern);\n const allMatches: string[] = [];\n\n for (const pat of patterns) {\n const matches = await this.matchPattern(pat, cwd);\n allMatches.push(...matches);\n }\n\n // Remove duplicates and sort\n return [...new Set(allMatches)].sort();\n }\n\n private expandBraces(pattern: string): string[] {\n const braceMatch = pattern.match(/\\{([^{}]+)\\}/);\n if (!braceMatch) return [pattern];\n\n const before = pattern.slice(0, braceMatch.index);\n const after = pattern.slice(braceMatch.index! + braceMatch[0].length);\n const options = braceMatch[1]!.split(\",\");\n\n const results: string[] = [];\n for (const opt of options) {\n const expanded = this.expandBraces(before + opt + after);\n results.push(...expanded);\n }\n return results;\n }\n\n private async matchPattern(pattern: string, cwd: string): Promise<string[]> {\n const parts = pattern.split(\"/\").filter((p) => p !== \"\");\n const isAbsolute = pattern.startsWith(\"/\");\n const startDir = isAbsolute ? \"/\" : cwd;\n\n return this.matchParts(parts, startDir);\n }\n\n private async matchParts(parts: string[], currentPath: string): Promise<string[]> {\n if (parts.length === 0) {\n return [currentPath];\n }\n\n const [part, ...rest] = parts;\n\n // Handle ** (recursive glob)\n if (part === \"**\") {\n const results: string[] = [];\n\n // Match current directory\n const withoutStar = await this.matchParts(rest, currentPath);\n results.push(...withoutStar);\n\n // Recurse into subdirectories\n try {\n const realPath = this.resolveSafePath(currentPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n for (const entry of entries) {\n const entryPath = path.posix.join(currentPath, String(entry));\n try {\n const entryRealPath = this.resolveSafePath(entryPath);\n const stat = await this.underlyingFs.promises.stat(entryRealPath);\n if (stat.isDirectory()) {\n const subMatches = await this.matchParts(parts, entryPath);\n results.push(...subMatches);\n }\n } catch {\n // Skip inaccessible entries\n }\n }\n } catch {\n // Directory not readable\n }\n\n return results;\n }\n\n // Handle regular glob patterns\n const regex = this.globToRegex(part!);\n\n try {\n const realPath = this.resolveSafePath(currentPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n const results: string[] = [];\n\n for (const entry of entries) {\n const entryName = String(entry);\n if (regex.test(entryName)) {\n const entryPath = path.posix.join(currentPath, entryName);\n if (rest.length === 0) {\n results.push(entryPath);\n } else {\n try {\n const entryRealPath = this.resolveSafePath(entryPath);\n const stat = await this.underlyingFs.promises.stat(entryRealPath);\n if (stat.isDirectory()) {\n const subMatches = await this.matchParts(rest, entryPath);\n results.push(...subMatches);\n }\n } catch {\n // Skip inaccessible entries\n }\n }\n }\n }\n\n return results;\n } catch {\n return [];\n }\n }\n\n private globToRegex(pattern: string): RegExp {\n let regex = \"^\";\n for (let i = 0; i < pattern.length; i++) {\n const char = pattern[i]!;\n if (char === \"*\") {\n regex += \"[^/]*\";\n } else if (char === \"?\") {\n regex += \"[^/]\";\n } else if (char === \"[\") {\n // Character class\n let j = i + 1;\n let classContent = \"\";\n while (j < pattern.length && pattern[j] !== \"]\") {\n classContent += pattern[j];\n j++;\n }\n regex += `[${classContent}]`;\n i = j;\n } else if (\".+^${}()|\\\\\".includes(char)) {\n regex += \"\\\\\" + char;\n } else {\n regex += char;\n }\n }\n regex += \"$\";\n return new RegExp(regex);\n }\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "FE5125D36453662C64756E2164756E21",
9
+ "names": []
10
+ }
@@ -55,13 +55,15 @@ __export(exports_src, {
55
55
  ShellPromise: () => import_shell_promise.ShellPromise,
56
56
  ShellError: () => import_errors.ShellError,
57
57
  ShellDSL: () => import_shell_dsl.ShellDSL,
58
+ ReadOnlyFileSystem: () => import_fs2.ReadOnlyFileSystem,
58
59
  PipeBuffer: () => import_io2.PipeBuffer,
59
60
  Parser: () => import_parser.Parser,
60
61
  ParseError: () => import_errors.ParseError,
61
62
  OutputCollectorImpl: () => import_io2.OutputCollectorImpl,
62
63
  Lexer: () => import_lexer.Lexer,
63
64
  LexError: () => import_errors.LexError,
64
- Interpreter: () => import_interpreter.Interpreter
65
+ Interpreter: () => import_interpreter.Interpreter,
66
+ FileSystem: () => import_fs2.FileSystem
65
67
  });
66
68
  module.exports = __toCommonJS(exports_src);
67
69
  var import_shell_dsl = require("./shell-dsl.cjs");
@@ -73,8 +75,9 @@ var import_parser = require("./parser/index.cjs");
73
75
  var import_parser2 = require("./parser/index.cjs");
74
76
  var import_interpreter = require("./interpreter/index.cjs");
75
77
  var import_fs = require("./fs/index.cjs");
78
+ var import_fs2 = require("./fs/index.cjs");
76
79
  var import_io = require("./io/index.cjs");
77
80
  var import_io2 = require("./io/index.cjs");
78
81
  var import_utils = require("./utils/index.cjs");
79
82
 
80
- //# debugId=CA28B33C8C95E9F064756E2164756E21
83
+ //# debugId=308511120A1E796164756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../src/index.ts"],
4
4
  "sourcesContent": [
5
- "// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.cjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.cjs\";\n\n// Types\nexport type {\n VirtualFS,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n RawValue,\n} from \"./types.cjs\";\nexport { isRawValue } from \"./types.cjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.cjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.cjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.cjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.cjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n LiteralNode,\n VariableNode,\n SubstitutionNode,\n GlobNode,\n ConcatNode,\n} from \"./parser/index.cjs\";\nexport {\n isCommandNode,\n isPipelineNode,\n isAndNode,\n isOrNode,\n isSequenceNode,\n isLiteralNode,\n isVariableNode,\n isSubstitutionNode,\n isGlobNode,\n isConcatNode,\n} from \"./parser/index.cjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions } from \"./interpreter/index.cjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.cjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.cjs\";\nexport { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from \"./io/index.cjs\";\n\n// Utilities\nexport { escape, escapeForInterpolation } from \"./utils/index.cjs\";\n"
5
+ "// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.cjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.cjs\";\n\n// Types\nexport type {\n VirtualFS,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n RawValue,\n} from \"./types.cjs\";\nexport { isRawValue } from \"./types.cjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.cjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.cjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.cjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.cjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n LiteralNode,\n VariableNode,\n SubstitutionNode,\n GlobNode,\n ConcatNode,\n} from \"./parser/index.cjs\";\nexport {\n isCommandNode,\n isPipelineNode,\n isAndNode,\n isOrNode,\n isSequenceNode,\n isLiteralNode,\n isVariableNode,\n isSubstitutionNode,\n isGlobNode,\n isConcatNode,\n} from \"./parser/index.cjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions } from \"./interpreter/index.cjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.cjs\";\nexport {\n FileSystem,\n ReadOnlyFileSystem,\n type Permission,\n type PermissionRules,\n type UnderlyingFS,\n} from \"./fs/index.cjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.cjs\";\nexport { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from \"./io/index.cjs\";\n\n// Utilities\nexport { escape, escapeForInterpolation } from \"./utils/index.cjs\";\n"
6
6
  ],
7
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACuD,IAAvD;AACuD,IAAvD;AAgB2B,IAA3B;AAGiD,IAAjD;AAG0C,IAA1C;AAI8B,IAA9B;AA0BO,IAXP;AAcqD,IAArD;AAGgC,IAAhC;AAGuC,IAAvC;AACwF,IAAxF;AAG+C,IAA/C;",
8
- "debugId": "CA28B33C8C95E9F064756E2164756E21",
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACuD,IAAvD;AACuD,IAAvD;AAgB2B,IAA3B;AAGiD,IAAjD;AAG0C,IAA1C;AAI8B,IAA9B;AA0BO,IAXP;AAcqD,IAArD;AAGgC,IAAhC;AAOO,IANP;AASuC,IAAvC;AACwF,IAAxF;AAG+C,IAA/C;",
8
+ "debugId": "308511120A1E796164756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "shell-dsl",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module"
5
5
  }
@@ -1,7 +1,11 @@
1
1
  // src/fs/index.ts
2
2
  import { createVirtualFS } from "./memfs-adapter.mjs";
3
+ import { FileSystem } from "./real-fs.mjs";
4
+ import { ReadOnlyFileSystem } from "./readonly-fs.mjs";
3
5
  export {
4
- createVirtualFS
6
+ createVirtualFS,
7
+ ReadOnlyFileSystem,
8
+ FileSystem
5
9
  };
6
10
 
7
- //# debugId=6662852DAB957AC264756E2164756E21
11
+ //# debugId=1175A4F96BC2DD3F64756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/fs/index.ts"],
4
4
  "sourcesContent": [
5
- "export { createVirtualFS } from \"./memfs-adapter.mjs\";\n"
5
+ "export { createVirtualFS } from \"./memfs-adapter.mjs\";\nexport { FileSystem, type Permission, type PermissionRules, type UnderlyingFS } from \"./real-fs.mjs\";\nexport { ReadOnlyFileSystem } from \"./readonly-fs.mjs\";\n"
6
6
  ],
7
- "mappings": ";AAAA;",
8
- "debugId": "6662852DAB957AC264756E2164756E21",
7
+ "mappings": ";AAAA;AACA;AACA;",
8
+ "debugId": "1175A4F96BC2DD3F64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -0,0 +1,17 @@
1
+ // src/fs/readonly-fs.ts
2
+ import { FileSystem } from "./real-fs.mjs";
3
+
4
+ class ReadOnlyFileSystem extends FileSystem {
5
+ constructor(mountPath, permissions, fs) {
6
+ const mergedPermissions = {
7
+ "**": "read-only",
8
+ ...permissions
9
+ };
10
+ super(mountPath, mergedPermissions, fs);
11
+ }
12
+ }
13
+ export {
14
+ ReadOnlyFileSystem
15
+ };
16
+
17
+ //# debugId=1565D7691D56BD6B64756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/fs/readonly-fs.ts"],
4
+ "sourcesContent": [
5
+ "import { FileSystem, type PermissionRules, type UnderlyingFS } from \"./real-fs.mjs\";\n\nexport class ReadOnlyFileSystem extends FileSystem {\n constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS) {\n // Merge user permissions with base read-only rule\n const mergedPermissions: PermissionRules = {\n \"**\": \"read-only\",\n ...permissions,\n };\n super(mountPath, mergedPermissions, fs);\n }\n}\n"
6
+ ],
7
+ "mappings": ";AAAA;AAAA;AAEO,MAAM,2BAA2B,WAAW;AAAA,EACjD,WAAW,CAAC,WAAoB,aAA+B,IAAmB;AAAA,IAEhF,MAAM,oBAAqC;AAAA,MACzC,MAAM;AAAA,SACH;AAAA,IACL;AAAA,IACA,MAAM,WAAW,mBAAmB,EAAE;AAAA;AAE1C;",
8
+ "debugId": "1565D7691D56BD6B64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,268 @@
1
+ // src/fs/real-fs.ts
2
+ import * as path from "path";
3
+ import * as nodeFs from "node:fs/promises";
4
+ var defaultFS = { promises: nodeFs };
5
+
6
+ class FileSystem {
7
+ mountBase;
8
+ rules;
9
+ underlyingFs;
10
+ constructor(mountPath, permissions, fs) {
11
+ this.mountBase = mountPath ? path.resolve(mountPath) : null;
12
+ this.rules = this.compileRules(permissions ?? {});
13
+ this.underlyingFs = fs ?? defaultFS;
14
+ }
15
+ compileRules(permissions) {
16
+ return Object.entries(permissions).map(([pattern, permission]) => ({
17
+ pattern,
18
+ permission,
19
+ specificity: this.calculateSpecificity(pattern)
20
+ })).sort((a, b) => b.specificity - a.specificity);
21
+ }
22
+ calculateSpecificity(pattern) {
23
+ const segments = pattern.split("/").filter(Boolean);
24
+ let score = segments.length * 1000;
25
+ for (const seg of segments) {
26
+ if (seg === "**")
27
+ score += 0;
28
+ else if (seg.includes("*"))
29
+ score += 1;
30
+ else
31
+ score += 10;
32
+ }
33
+ return score;
34
+ }
35
+ getPermission(virtualPath) {
36
+ const normalized = virtualPath.replace(/^\/+/, "");
37
+ for (const rule of this.rules) {
38
+ if (this.matchGlob(rule.pattern, normalized)) {
39
+ return rule.permission;
40
+ }
41
+ }
42
+ return "read-write";
43
+ }
44
+ matchGlob(pattern, filePath) {
45
+ const regex = pattern.split("/").map((seg) => {
46
+ if (seg === "**")
47
+ return ".*";
48
+ return seg.replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
49
+ }).join("/");
50
+ return new RegExp(`^${regex}$`).test(filePath);
51
+ }
52
+ checkPermission(virtualPath, operation) {
53
+ const perm = this.getPermission(virtualPath);
54
+ if (perm === "excluded") {
55
+ throw new Error(`Access denied: "${virtualPath}" is excluded`);
56
+ }
57
+ if (operation === "write" && perm === "read-only") {
58
+ throw new Error(`Access denied: "${virtualPath}" is read-only`);
59
+ }
60
+ }
61
+ resolveSafePath(virtualPath) {
62
+ if (this.mountBase === null) {
63
+ return path.resolve(virtualPath);
64
+ }
65
+ const segments = virtualPath.split("/").filter(Boolean);
66
+ let depth = 0;
67
+ for (const seg of segments) {
68
+ if (seg === "..") {
69
+ depth--;
70
+ if (depth < 0) {
71
+ throw new Error(`Path traversal blocked: "${virtualPath}" escapes mount point`);
72
+ }
73
+ } else if (seg !== ".") {
74
+ depth++;
75
+ }
76
+ }
77
+ const normalized = path.normalize(virtualPath);
78
+ const relativePath = normalized.startsWith("/") ? normalized.slice(1) : normalized;
79
+ const realPath = path.join(this.mountBase, relativePath);
80
+ const resolved = path.resolve(realPath);
81
+ if (!resolved.startsWith(this.mountBase + path.sep) && resolved !== this.mountBase) {
82
+ throw new Error(`Path traversal blocked: "${virtualPath}" escapes mount point`);
83
+ }
84
+ return resolved;
85
+ }
86
+ async readFile(filePath) {
87
+ this.checkPermission(filePath, "read");
88
+ const realPath = this.resolveSafePath(filePath);
89
+ const content = await this.underlyingFs.promises.readFile(realPath);
90
+ return Buffer.from(content);
91
+ }
92
+ async readdir(dirPath) {
93
+ this.checkPermission(dirPath, "read");
94
+ const realPath = this.resolveSafePath(dirPath);
95
+ const entries = await this.underlyingFs.promises.readdir(realPath);
96
+ return entries.map(String);
97
+ }
98
+ async stat(filePath) {
99
+ this.checkPermission(filePath, "read");
100
+ const realPath = this.resolveSafePath(filePath);
101
+ const stats = await this.underlyingFs.promises.stat(realPath);
102
+ return {
103
+ isFile: () => stats.isFile(),
104
+ isDirectory: () => stats.isDirectory(),
105
+ size: stats.size,
106
+ mtime: stats.mtime
107
+ };
108
+ }
109
+ async exists(filePath) {
110
+ try {
111
+ this.checkPermission(filePath, "read");
112
+ const realPath = this.resolveSafePath(filePath);
113
+ await this.underlyingFs.promises.stat(realPath);
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+ async writeFile(filePath, data) {
120
+ this.checkPermission(filePath, "write");
121
+ const realPath = this.resolveSafePath(filePath);
122
+ await this.underlyingFs.promises.writeFile(realPath, data);
123
+ }
124
+ async appendFile(filePath, data) {
125
+ this.checkPermission(filePath, "write");
126
+ const realPath = this.resolveSafePath(filePath);
127
+ await this.underlyingFs.promises.appendFile(realPath, data);
128
+ }
129
+ async mkdir(dirPath, opts) {
130
+ this.checkPermission(dirPath, "write");
131
+ const realPath = this.resolveSafePath(dirPath);
132
+ await this.underlyingFs.promises.mkdir(realPath, opts);
133
+ }
134
+ async rm(filePath, opts) {
135
+ this.checkPermission(filePath, "write");
136
+ const realPath = this.resolveSafePath(filePath);
137
+ await this.underlyingFs.promises.rm(realPath, opts);
138
+ }
139
+ resolve(...paths) {
140
+ return path.resolve("/", ...paths);
141
+ }
142
+ dirname(filePath) {
143
+ return path.dirname(filePath);
144
+ }
145
+ basename(filePath) {
146
+ return path.basename(filePath);
147
+ }
148
+ async glob(pattern, opts) {
149
+ const cwd = opts?.cwd ?? "/";
150
+ this.checkPermission(cwd, "read");
151
+ const matches = await this.expandGlob(pattern, cwd);
152
+ return matches.filter((p) => this.getPermission(p) !== "excluded").sort();
153
+ }
154
+ async expandGlob(pattern, cwd) {
155
+ const patterns = this.expandBraces(pattern);
156
+ const allMatches = [];
157
+ for (const pat of patterns) {
158
+ const matches = await this.matchPattern(pat, cwd);
159
+ allMatches.push(...matches);
160
+ }
161
+ return [...new Set(allMatches)].sort();
162
+ }
163
+ expandBraces(pattern) {
164
+ const braceMatch = pattern.match(/\{([^{}]+)\}/);
165
+ if (!braceMatch)
166
+ return [pattern];
167
+ const before = pattern.slice(0, braceMatch.index);
168
+ const after = pattern.slice(braceMatch.index + braceMatch[0].length);
169
+ const options = braceMatch[1].split(",");
170
+ const results = [];
171
+ for (const opt of options) {
172
+ const expanded = this.expandBraces(before + opt + after);
173
+ results.push(...expanded);
174
+ }
175
+ return results;
176
+ }
177
+ async matchPattern(pattern, cwd) {
178
+ const parts = pattern.split("/").filter((p) => p !== "");
179
+ const isAbsolute = pattern.startsWith("/");
180
+ const startDir = isAbsolute ? "/" : cwd;
181
+ return this.matchParts(parts, startDir);
182
+ }
183
+ async matchParts(parts, currentPath) {
184
+ if (parts.length === 0) {
185
+ return [currentPath];
186
+ }
187
+ const [part, ...rest] = parts;
188
+ if (part === "**") {
189
+ const results = [];
190
+ const withoutStar = await this.matchParts(rest, currentPath);
191
+ results.push(...withoutStar);
192
+ try {
193
+ const realPath = this.resolveSafePath(currentPath);
194
+ const entries = await this.underlyingFs.promises.readdir(realPath);
195
+ for (const entry of entries) {
196
+ const entryPath = path.posix.join(currentPath, String(entry));
197
+ try {
198
+ const entryRealPath = this.resolveSafePath(entryPath);
199
+ const stat = await this.underlyingFs.promises.stat(entryRealPath);
200
+ if (stat.isDirectory()) {
201
+ const subMatches = await this.matchParts(parts, entryPath);
202
+ results.push(...subMatches);
203
+ }
204
+ } catch {}
205
+ }
206
+ } catch {}
207
+ return results;
208
+ }
209
+ const regex = this.globToRegex(part);
210
+ try {
211
+ const realPath = this.resolveSafePath(currentPath);
212
+ const entries = await this.underlyingFs.promises.readdir(realPath);
213
+ const results = [];
214
+ for (const entry of entries) {
215
+ const entryName = String(entry);
216
+ if (regex.test(entryName)) {
217
+ const entryPath = path.posix.join(currentPath, entryName);
218
+ if (rest.length === 0) {
219
+ results.push(entryPath);
220
+ } else {
221
+ try {
222
+ const entryRealPath = this.resolveSafePath(entryPath);
223
+ const stat = await this.underlyingFs.promises.stat(entryRealPath);
224
+ if (stat.isDirectory()) {
225
+ const subMatches = await this.matchParts(rest, entryPath);
226
+ results.push(...subMatches);
227
+ }
228
+ } catch {}
229
+ }
230
+ }
231
+ }
232
+ return results;
233
+ } catch {
234
+ return [];
235
+ }
236
+ }
237
+ globToRegex(pattern) {
238
+ let regex = "^";
239
+ for (let i = 0;i < pattern.length; i++) {
240
+ const char = pattern[i];
241
+ if (char === "*") {
242
+ regex += "[^/]*";
243
+ } else if (char === "?") {
244
+ regex += "[^/]";
245
+ } else if (char === "[") {
246
+ let j = i + 1;
247
+ let classContent = "";
248
+ while (j < pattern.length && pattern[j] !== "]") {
249
+ classContent += pattern[j];
250
+ j++;
251
+ }
252
+ regex += `[${classContent}]`;
253
+ i = j;
254
+ } else if (".+^${}()|\\".includes(char)) {
255
+ regex += "\\" + char;
256
+ } else {
257
+ regex += char;
258
+ }
259
+ }
260
+ regex += "$";
261
+ return new RegExp(regex);
262
+ }
263
+ }
264
+ export {
265
+ FileSystem
266
+ };
267
+
268
+ //# debugId=78CB3041E0AD3DDE64756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/fs/real-fs.ts"],
4
+ "sourcesContent": [
5
+ "import * as path from \"path\";\nimport * as nodeFs from \"node:fs/promises\";\nimport type { VirtualFS, FileStat } from \"../types.mjs\";\n\nexport type Permission = \"read-write\" | \"read-only\" | \"excluded\";\nexport type PermissionRules = Record<string, Permission>;\n\n// Minimal interface for the underlying fs (compatible with node:fs and memfs)\nexport interface UnderlyingFS {\n promises: {\n readFile(path: string): Promise<Buffer | Uint8Array | string>;\n readdir(path: string): Promise<string[]>;\n stat(path: string): Promise<{\n isFile(): boolean;\n isDirectory(): boolean;\n size: number;\n mtime: Date;\n }>;\n writeFile(path: string, data: Buffer | string): Promise<void>;\n appendFile(path: string, data: Buffer | string): Promise<void>;\n mkdir(path: string, opts?: { recursive?: boolean }): Promise<string | undefined | void>;\n rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void>;\n };\n}\n\n// Default: use real node:fs\nconst defaultFS: UnderlyingFS = { promises: nodeFs };\n\ninterface CompiledRule {\n pattern: string;\n permission: Permission;\n specificity: number;\n}\n\nexport class FileSystem implements VirtualFS {\n private readonly mountBase: string | null;\n private readonly rules: CompiledRule[];\n protected readonly underlyingFs: UnderlyingFS;\n\n constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS) {\n this.mountBase = mountPath ? path.resolve(mountPath) : null;\n this.rules = this.compileRules(permissions ?? {});\n this.underlyingFs = fs ?? defaultFS;\n }\n\n private compileRules(permissions: PermissionRules): CompiledRule[] {\n return Object.entries(permissions)\n .map(([pattern, permission]) => ({\n pattern,\n permission,\n specificity: this.calculateSpecificity(pattern),\n }))\n .sort((a, b) => b.specificity - a.specificity); // highest first\n }\n\n private calculateSpecificity(pattern: string): number {\n const segments = pattern.split(\"/\").filter(Boolean);\n let score = segments.length * 1000; // segment count is primary\n\n for (const seg of segments) {\n if (seg === \"**\") score += 0;\n else if (seg.includes(\"*\")) score += 1;\n else score += 10; // literal segment\n }\n return score;\n }\n\n protected getPermission(virtualPath: string): Permission {\n const normalized = virtualPath.replace(/^\\/+/, \"\"); // strip leading slashes\n\n for (const rule of this.rules) {\n if (this.matchGlob(rule.pattern, normalized)) {\n return rule.permission;\n }\n }\n return \"read-write\"; // default\n }\n\n private matchGlob(pattern: string, filePath: string): boolean {\n // Convert glob to regex\n // ** matches any path segments, * matches within segment\n const regex = pattern\n .split(\"/\")\n .map((seg) => {\n if (seg === \"**\") return \".*\";\n return seg.replace(/\\*/g, \"[^/]*\").replace(/\\?/g, \"[^/]\");\n })\n .join(\"/\");\n return new RegExp(`^${regex}$`).test(filePath);\n }\n\n protected checkPermission(virtualPath: string, operation: \"read\" | \"write\"): void {\n const perm = this.getPermission(virtualPath);\n\n if (perm === \"excluded\") {\n throw new Error(`Access denied: \"${virtualPath}\" is excluded`);\n }\n if (operation === \"write\" && perm === \"read-only\") {\n throw new Error(`Access denied: \"${virtualPath}\" is read-only`);\n }\n }\n\n private resolveSafePath(virtualPath: string): string {\n if (this.mountBase === null) {\n return path.resolve(virtualPath);\n }\n\n // Check for path traversal by tracking depth\n const segments = virtualPath.split(\"/\").filter(Boolean);\n let depth = 0;\n for (const seg of segments) {\n if (seg === \"..\") {\n depth--;\n if (depth < 0) {\n throw new Error(`Path traversal blocked: \"${virtualPath}\" escapes mount point`);\n }\n } else if (seg !== \".\") {\n depth++;\n }\n }\n\n const normalized = path.normalize(virtualPath);\n const relativePath = normalized.startsWith(\"/\") ? normalized.slice(1) : normalized;\n const realPath = path.join(this.mountBase, relativePath);\n const resolved = path.resolve(realPath);\n\n // Double-check containment (defense in depth)\n if (!resolved.startsWith(this.mountBase + path.sep) && resolved !== this.mountBase) {\n throw new Error(`Path traversal blocked: \"${virtualPath}\" escapes mount point`);\n }\n\n return resolved;\n }\n\n // Read operations\n async readFile(filePath: string): Promise<Buffer> {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const content = await this.underlyingFs.promises.readFile(realPath);\n return Buffer.from(content);\n }\n\n async readdir(dirPath: string): Promise<string[]> {\n this.checkPermission(dirPath, \"read\");\n const realPath = this.resolveSafePath(dirPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n return entries.map(String);\n }\n\n async stat(filePath: string): Promise<FileStat> {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const stats = await this.underlyingFs.promises.stat(realPath);\n return {\n isFile: () => stats.isFile(),\n isDirectory: () => stats.isDirectory(),\n size: stats.size,\n mtime: stats.mtime,\n };\n }\n\n async exists(filePath: string): Promise<boolean> {\n try {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.stat(realPath);\n return true;\n } catch {\n return false;\n }\n }\n\n // Write operations\n async writeFile(filePath: string, data: Buffer | string): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.writeFile(realPath, data);\n }\n\n async appendFile(filePath: string, data: Buffer | string): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.appendFile(realPath, data);\n }\n\n async mkdir(dirPath: string, opts?: { recursive?: boolean }): Promise<void> {\n this.checkPermission(dirPath, \"write\");\n const realPath = this.resolveSafePath(dirPath);\n await this.underlyingFs.promises.mkdir(realPath, opts);\n }\n\n async rm(filePath: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.rm(realPath, opts);\n }\n\n // Path utilities (no permission check needed)\n resolve(...paths: string[]): string {\n return path.resolve(\"/\", ...paths);\n }\n\n dirname(filePath: string): string {\n return path.dirname(filePath);\n }\n\n basename(filePath: string): string {\n return path.basename(filePath);\n }\n\n // Glob with permission filtering\n async glob(pattern: string, opts?: { cwd?: string }): Promise<string[]> {\n const cwd = opts?.cwd ?? \"/\";\n this.checkPermission(cwd, \"read\");\n\n const matches = await this.expandGlob(pattern, cwd);\n\n // Filter out excluded paths\n return matches.filter((p) => this.getPermission(p) !== \"excluded\").sort();\n }\n\n // Glob expansion (similar to memfs-adapter implementation)\n private async expandGlob(pattern: string, cwd: string): Promise<string[]> {\n // Handle brace expansion first\n const patterns = this.expandBraces(pattern);\n const allMatches: string[] = [];\n\n for (const pat of patterns) {\n const matches = await this.matchPattern(pat, cwd);\n allMatches.push(...matches);\n }\n\n // Remove duplicates and sort\n return [...new Set(allMatches)].sort();\n }\n\n private expandBraces(pattern: string): string[] {\n const braceMatch = pattern.match(/\\{([^{}]+)\\}/);\n if (!braceMatch) return [pattern];\n\n const before = pattern.slice(0, braceMatch.index);\n const after = pattern.slice(braceMatch.index! + braceMatch[0].length);\n const options = braceMatch[1]!.split(\",\");\n\n const results: string[] = [];\n for (const opt of options) {\n const expanded = this.expandBraces(before + opt + after);\n results.push(...expanded);\n }\n return results;\n }\n\n private async matchPattern(pattern: string, cwd: string): Promise<string[]> {\n const parts = pattern.split(\"/\").filter((p) => p !== \"\");\n const isAbsolute = pattern.startsWith(\"/\");\n const startDir = isAbsolute ? \"/\" : cwd;\n\n return this.matchParts(parts, startDir);\n }\n\n private async matchParts(parts: string[], currentPath: string): Promise<string[]> {\n if (parts.length === 0) {\n return [currentPath];\n }\n\n const [part, ...rest] = parts;\n\n // Handle ** (recursive glob)\n if (part === \"**\") {\n const results: string[] = [];\n\n // Match current directory\n const withoutStar = await this.matchParts(rest, currentPath);\n results.push(...withoutStar);\n\n // Recurse into subdirectories\n try {\n const realPath = this.resolveSafePath(currentPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n for (const entry of entries) {\n const entryPath = path.posix.join(currentPath, String(entry));\n try {\n const entryRealPath = this.resolveSafePath(entryPath);\n const stat = await this.underlyingFs.promises.stat(entryRealPath);\n if (stat.isDirectory()) {\n const subMatches = await this.matchParts(parts, entryPath);\n results.push(...subMatches);\n }\n } catch {\n // Skip inaccessible entries\n }\n }\n } catch {\n // Directory not readable\n }\n\n return results;\n }\n\n // Handle regular glob patterns\n const regex = this.globToRegex(part!);\n\n try {\n const realPath = this.resolveSafePath(currentPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n const results: string[] = [];\n\n for (const entry of entries) {\n const entryName = String(entry);\n if (regex.test(entryName)) {\n const entryPath = path.posix.join(currentPath, entryName);\n if (rest.length === 0) {\n results.push(entryPath);\n } else {\n try {\n const entryRealPath = this.resolveSafePath(entryPath);\n const stat = await this.underlyingFs.promises.stat(entryRealPath);\n if (stat.isDirectory()) {\n const subMatches = await this.matchParts(rest, entryPath);\n results.push(...subMatches);\n }\n } catch {\n // Skip inaccessible entries\n }\n }\n }\n }\n\n return results;\n } catch {\n return [];\n }\n }\n\n private globToRegex(pattern: string): RegExp {\n let regex = \"^\";\n for (let i = 0; i < pattern.length; i++) {\n const char = pattern[i]!;\n if (char === \"*\") {\n regex += \"[^/]*\";\n } else if (char === \"?\") {\n regex += \"[^/]\";\n } else if (char === \"[\") {\n // Character class\n let j = i + 1;\n let classContent = \"\";\n while (j < pattern.length && pattern[j] !== \"]\") {\n classContent += pattern[j];\n j++;\n }\n regex += `[${classContent}]`;\n i = j;\n } else if (\".+^${}()|\\\\\".includes(char)) {\n regex += \"\\\\\" + char;\n } else {\n regex += char;\n }\n }\n regex += \"$\";\n return new RegExp(regex);\n }\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "78CB3041E0AD3DDE64756E2164756E21",
9
+ "names": []
10
+ }
@@ -19,6 +19,10 @@ import {
19
19
  } from "./parser/index.mjs";
20
20
  import { Interpreter } from "./interpreter/index.mjs";
21
21
  import { createVirtualFS } from "./fs/index.mjs";
22
+ import {
23
+ FileSystem,
24
+ ReadOnlyFileSystem
25
+ } from "./fs/index.mjs";
22
26
  import { createStdin, StdinImpl } from "./io/index.mjs";
23
27
  import { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from "./io/index.mjs";
24
28
  import { escape, escapeForInterpolation } from "./utils/index.mjs";
@@ -49,13 +53,15 @@ export {
49
53
  ShellPromise,
50
54
  ShellError,
51
55
  ShellDSL,
56
+ ReadOnlyFileSystem,
52
57
  PipeBuffer,
53
58
  Parser,
54
59
  ParseError,
55
60
  OutputCollectorImpl,
56
61
  Lexer,
57
62
  LexError,
58
- Interpreter
63
+ Interpreter,
64
+ FileSystem
59
65
  };
60
66
 
61
- //# debugId=F0817E6C4D09F0A764756E2164756E21
67
+ //# debugId=4DB7B709124A1C5464756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../src/index.ts"],
4
4
  "sourcesContent": [
5
- "// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.mjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.mjs\";\n\n// Types\nexport type {\n VirtualFS,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n RawValue,\n} from \"./types.mjs\";\nexport { isRawValue } from \"./types.mjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.mjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.mjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.mjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.mjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n LiteralNode,\n VariableNode,\n SubstitutionNode,\n GlobNode,\n ConcatNode,\n} from \"./parser/index.mjs\";\nexport {\n isCommandNode,\n isPipelineNode,\n isAndNode,\n isOrNode,\n isSequenceNode,\n isLiteralNode,\n isVariableNode,\n isSubstitutionNode,\n isGlobNode,\n isConcatNode,\n} from \"./parser/index.mjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions } from \"./interpreter/index.mjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.mjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.mjs\";\nexport { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from \"./io/index.mjs\";\n\n// Utilities\nexport { escape, escapeForInterpolation } from \"./utils/index.mjs\";\n"
5
+ "// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.mjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.mjs\";\n\n// Types\nexport type {\n VirtualFS,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n RawValue,\n} from \"./types.mjs\";\nexport { isRawValue } from \"./types.mjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.mjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.mjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.mjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.mjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n LiteralNode,\n VariableNode,\n SubstitutionNode,\n GlobNode,\n ConcatNode,\n} from \"./parser/index.mjs\";\nexport {\n isCommandNode,\n isPipelineNode,\n isAndNode,\n isOrNode,\n isSequenceNode,\n isLiteralNode,\n isVariableNode,\n isSubstitutionNode,\n isGlobNode,\n isConcatNode,\n} from \"./parser/index.mjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions } from \"./interpreter/index.mjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.mjs\";\nexport {\n FileSystem,\n ReadOnlyFileSystem,\n type Permission,\n type PermissionRules,\n type UnderlyingFS,\n} from \"./fs/index.mjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.mjs\";\nexport { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from \"./io/index.mjs\";\n\n// Utilities\nexport { escape, escapeForInterpolation } from \"./utils/index.mjs\";\n"
6
6
  ],
7
- "mappings": ";AACA;AACA;AAgBA;AAGA;AAGA;AAIA;AAeA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA;AAGA;AAGA;AACA;AAGA;",
8
- "debugId": "F0817E6C4D09F0A764756E2164756E21",
7
+ "mappings": ";AACA;AACA;AAgBA;AAGA;AAGA;AAIA;AAeA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA;AAGA;AACA;AAAA;AAAA;AAAA;AASA;AACA;AAGA;",
8
+ "debugId": "4DB7B709124A1C5464756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -17,4 +17,5 @@ export { touch } from "./touch.ts";
17
17
  export { cp } from "./cp.ts";
18
18
  export { mv } from "./mv.ts";
19
19
  export { tee } from "./tee.ts";
20
+ export { tree } from "./tree.ts";
20
21
  export declare const builtinCommands: Record<string, Command>;
@@ -0,0 +1,2 @@
1
+ import type { Command } from "../src/types.ts";
2
+ export declare const tree: Command;
@@ -1 +1,3 @@
1
1
  export { createVirtualFS } from "./memfs-adapter.ts";
2
+ export { FileSystem, type Permission, type PermissionRules, type UnderlyingFS } from "./real-fs.ts";
3
+ export { ReadOnlyFileSystem } from "./readonly-fs.ts";
@@ -0,0 +1,4 @@
1
+ import { FileSystem, type PermissionRules, type UnderlyingFS } from "./real-fs.ts";
2
+ export declare class ReadOnlyFileSystem extends FileSystem {
3
+ constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS);
4
+ }
@@ -0,0 +1,60 @@
1
+ import type { VirtualFS, FileStat } from "../types.ts";
2
+ export type Permission = "read-write" | "read-only" | "excluded";
3
+ export type PermissionRules = Record<string, Permission>;
4
+ export interface UnderlyingFS {
5
+ promises: {
6
+ readFile(path: string): Promise<Buffer | Uint8Array | string>;
7
+ readdir(path: string): Promise<string[]>;
8
+ stat(path: string): Promise<{
9
+ isFile(): boolean;
10
+ isDirectory(): boolean;
11
+ size: number;
12
+ mtime: Date;
13
+ }>;
14
+ writeFile(path: string, data: Buffer | string): Promise<void>;
15
+ appendFile(path: string, data: Buffer | string): Promise<void>;
16
+ mkdir(path: string, opts?: {
17
+ recursive?: boolean;
18
+ }): Promise<string | undefined | void>;
19
+ rm(path: string, opts?: {
20
+ recursive?: boolean;
21
+ force?: boolean;
22
+ }): Promise<void>;
23
+ };
24
+ }
25
+ export declare class FileSystem implements VirtualFS {
26
+ private readonly mountBase;
27
+ private readonly rules;
28
+ protected readonly underlyingFs: UnderlyingFS;
29
+ constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS);
30
+ private compileRules;
31
+ private calculateSpecificity;
32
+ protected getPermission(virtualPath: string): Permission;
33
+ private matchGlob;
34
+ protected checkPermission(virtualPath: string, operation: "read" | "write"): void;
35
+ private resolveSafePath;
36
+ readFile(filePath: string): Promise<Buffer>;
37
+ readdir(dirPath: string): Promise<string[]>;
38
+ stat(filePath: string): Promise<FileStat>;
39
+ exists(filePath: string): Promise<boolean>;
40
+ writeFile(filePath: string, data: Buffer | string): Promise<void>;
41
+ appendFile(filePath: string, data: Buffer | string): Promise<void>;
42
+ mkdir(dirPath: string, opts?: {
43
+ recursive?: boolean;
44
+ }): Promise<void>;
45
+ rm(filePath: string, opts?: {
46
+ recursive?: boolean;
47
+ force?: boolean;
48
+ }): Promise<void>;
49
+ resolve(...paths: string[]): string;
50
+ dirname(filePath: string): string;
51
+ basename(filePath: string): string;
52
+ glob(pattern: string, opts?: {
53
+ cwd?: string;
54
+ }): Promise<string[]>;
55
+ private expandGlob;
56
+ private expandBraces;
57
+ private matchPattern;
58
+ private matchParts;
59
+ private globToRegex;
60
+ }
@@ -10,6 +10,7 @@ export type { ASTNode, Redirect, CommandNode, PipelineNode, AndNode, OrNode, Seq
10
10
  export { isCommandNode, isPipelineNode, isAndNode, isOrNode, isSequenceNode, isLiteralNode, isVariableNode, isSubstitutionNode, isGlobNode, isConcatNode, } from "./parser/index.ts";
11
11
  export { Interpreter, type InterpreterOptions } from "./interpreter/index.ts";
12
12
  export { createVirtualFS } from "./fs/index.ts";
13
+ export { FileSystem, ReadOnlyFileSystem, type Permission, type PermissionRules, type UnderlyingFS, } from "./fs/index.ts";
13
14
  export { createStdin, StdinImpl } from "./io/index.ts";
14
15
  export { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from "./io/index.ts";
15
16
  export { escape, escapeForInterpolation } from "./utils/index.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-dsl",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A sandboxed shell-style DSL for running scriptable command pipelines in-process without host OS access",
5
5
  "author": "ricsam <oss@ricsam.dev>",
6
6
  "license": "MIT",