shell-dsl 0.0.30 → 0.0.32
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 +27 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/commands/mv/mv.cjs +7 -1
- package/dist/cjs/src/commands/mv/mv.cjs.map +3 -3
- package/dist/cjs/src/commands/tree/tree.cjs +64 -27
- package/dist/cjs/src/commands/tree/tree.cjs.map +3 -3
- package/dist/cjs/src/fs/memfs-adapter.cjs +32 -110
- package/dist/cjs/src/fs/memfs-adapter.cjs.map +3 -3
- package/dist/cjs/src/fs/real-fs.cjs +34 -112
- package/dist/cjs/src/fs/real-fs.cjs.map +3 -3
- package/dist/cjs/src/fs/special-files.cjs +98 -0
- package/dist/cjs/src/fs/special-files.cjs.map +10 -0
- package/dist/cjs/src/index.cjs +2 -1
- package/dist/cjs/src/index.cjs.map +3 -3
- package/dist/cjs/src/interpreter/interpreter.cjs +9 -8
- package/dist/cjs/src/interpreter/interpreter.cjs.map +3 -3
- package/dist/cjs/src/utils/glob.cjs +129 -0
- package/dist/cjs/src/utils/glob.cjs.map +10 -0
- package/dist/cjs/src/utils/index.cjs +3 -1
- package/dist/cjs/src/utils/index.cjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/src/commands/mv/mv.mjs +7 -1
- package/dist/mjs/src/commands/mv/mv.mjs.map +3 -3
- package/dist/mjs/src/commands/tree/tree.mjs +64 -27
- package/dist/mjs/src/commands/tree/tree.mjs.map +3 -3
- package/dist/mjs/src/fs/memfs-adapter.mjs +38 -110
- package/dist/mjs/src/fs/memfs-adapter.mjs.map +3 -3
- package/dist/mjs/src/fs/real-fs.mjs +40 -112
- package/dist/mjs/src/fs/real-fs.mjs.map +3 -3
- package/dist/mjs/src/fs/special-files.mjs +58 -0
- package/dist/mjs/src/fs/special-files.mjs.map +10 -0
- package/dist/mjs/src/index.mjs +3 -2
- package/dist/mjs/src/index.mjs.map +2 -2
- package/dist/mjs/src/interpreter/interpreter.mjs +9 -8
- package/dist/mjs/src/interpreter/interpreter.mjs.map +3 -3
- package/dist/mjs/src/utils/glob.mjs +89 -0
- package/dist/mjs/src/utils/glob.mjs.map +10 -0
- package/dist/mjs/src/utils/index.mjs +3 -1
- package/dist/mjs/src/utils/index.mjs.map +3 -3
- package/dist/types/src/fs/real-fs.d.ts +0 -5
- package/dist/types/src/fs/special-files.d.ts +8 -0
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/utils/glob.d.ts +6 -0
- package/dist/types/src/utils/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// src/fs/real-fs.ts
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as nodeFs from "node:fs/promises";
|
|
4
|
+
import { globVirtualFS } from "../utils/glob.mjs";
|
|
5
|
+
import {
|
|
6
|
+
discardsSpecialFileWrites,
|
|
7
|
+
existsSpecialFile,
|
|
8
|
+
getSpecialPathError,
|
|
9
|
+
readSpecialFile,
|
|
10
|
+
statSpecialFile
|
|
11
|
+
} from "./special-files.mjs";
|
|
4
12
|
var defaultFS = { promises: nodeFs };
|
|
5
13
|
var nodePathOps = {
|
|
6
14
|
separator: path.sep,
|
|
@@ -99,6 +107,10 @@ class FileSystem {
|
|
|
99
107
|
return resolved;
|
|
100
108
|
}
|
|
101
109
|
async readFile(filePath, encoding) {
|
|
110
|
+
const specialContent = readSpecialFile(filePath, encoding);
|
|
111
|
+
if (specialContent !== undefined) {
|
|
112
|
+
return specialContent;
|
|
113
|
+
}
|
|
102
114
|
this.checkPermission(filePath, "read");
|
|
103
115
|
const realPath = this.resolveSafePath(filePath);
|
|
104
116
|
const content = await this.underlyingFs.promises.readFile(realPath);
|
|
@@ -106,12 +118,20 @@ class FileSystem {
|
|
|
106
118
|
return encoding ? buf.toString(encoding) : buf;
|
|
107
119
|
}
|
|
108
120
|
async readdir(dirPath) {
|
|
121
|
+
const specialError = getSpecialPathError(dirPath, "readdir");
|
|
122
|
+
if (specialError) {
|
|
123
|
+
throw specialError;
|
|
124
|
+
}
|
|
109
125
|
this.checkPermission(dirPath, "read");
|
|
110
126
|
const realPath = this.resolveSafePath(dirPath);
|
|
111
127
|
const entries = await this.underlyingFs.promises.readdir(realPath);
|
|
112
128
|
return entries.map(String);
|
|
113
129
|
}
|
|
114
130
|
async stat(filePath) {
|
|
131
|
+
const specialStat = statSpecialFile(filePath);
|
|
132
|
+
if (specialStat) {
|
|
133
|
+
return specialStat;
|
|
134
|
+
}
|
|
115
135
|
this.checkPermission(filePath, "read");
|
|
116
136
|
const realPath = this.resolveSafePath(filePath);
|
|
117
137
|
const stats = await this.underlyingFs.promises.stat(realPath);
|
|
@@ -123,6 +143,10 @@ class FileSystem {
|
|
|
123
143
|
};
|
|
124
144
|
}
|
|
125
145
|
async exists(filePath) {
|
|
146
|
+
const specialExists = existsSpecialFile(filePath);
|
|
147
|
+
if (specialExists !== undefined) {
|
|
148
|
+
return specialExists;
|
|
149
|
+
}
|
|
126
150
|
try {
|
|
127
151
|
this.checkPermission(filePath, "read");
|
|
128
152
|
const realPath = this.resolveSafePath(filePath);
|
|
@@ -133,21 +157,35 @@ class FileSystem {
|
|
|
133
157
|
}
|
|
134
158
|
}
|
|
135
159
|
async writeFile(filePath, data) {
|
|
160
|
+
if (discardsSpecialFileWrites(filePath)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
136
163
|
this.checkPermission(filePath, "write");
|
|
137
164
|
const realPath = this.resolveSafePath(filePath);
|
|
138
165
|
await this.underlyingFs.promises.writeFile(realPath, data);
|
|
139
166
|
}
|
|
140
167
|
async appendFile(filePath, data) {
|
|
168
|
+
if (discardsSpecialFileWrites(filePath)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
141
171
|
this.checkPermission(filePath, "write");
|
|
142
172
|
const realPath = this.resolveSafePath(filePath);
|
|
143
173
|
await this.underlyingFs.promises.appendFile(realPath, data);
|
|
144
174
|
}
|
|
145
175
|
async mkdir(dirPath, opts) {
|
|
176
|
+
const specialError = getSpecialPathError(dirPath, "mkdir");
|
|
177
|
+
if (specialError) {
|
|
178
|
+
throw specialError;
|
|
179
|
+
}
|
|
146
180
|
this.checkPermission(dirPath, "write");
|
|
147
181
|
const realPath = this.resolveSafePath(dirPath);
|
|
148
182
|
await this.underlyingFs.promises.mkdir(realPath, opts);
|
|
149
183
|
}
|
|
150
184
|
async rm(filePath, opts) {
|
|
185
|
+
const specialError = getSpecialPathError(filePath, "rm");
|
|
186
|
+
if (specialError) {
|
|
187
|
+
throw specialError;
|
|
188
|
+
}
|
|
151
189
|
this.checkPermission(filePath, "write");
|
|
152
190
|
const realPath = this.resolveSafePath(filePath);
|
|
153
191
|
await this.underlyingFs.promises.rm(realPath, opts);
|
|
@@ -164,121 +202,11 @@ class FileSystem {
|
|
|
164
202
|
async glob(pattern, opts) {
|
|
165
203
|
const cwd = opts?.cwd ?? "/";
|
|
166
204
|
this.checkPermission(cwd, "read");
|
|
167
|
-
|
|
168
|
-
return matches.filter((p) => this.getPermission(p) !== "excluded").sort();
|
|
169
|
-
}
|
|
170
|
-
async expandGlob(pattern, cwd) {
|
|
171
|
-
const patterns = this.expandBraces(pattern);
|
|
172
|
-
const allMatches = [];
|
|
173
|
-
for (const pat of patterns) {
|
|
174
|
-
const matches = await this.matchPattern(pat, cwd);
|
|
175
|
-
allMatches.push(...matches);
|
|
176
|
-
}
|
|
177
|
-
return [...new Set(allMatches)].sort();
|
|
178
|
-
}
|
|
179
|
-
expandBraces(pattern) {
|
|
180
|
-
const braceMatch = pattern.match(/\{([^{}]+)\}/);
|
|
181
|
-
if (!braceMatch)
|
|
182
|
-
return [pattern];
|
|
183
|
-
const before = pattern.slice(0, braceMatch.index);
|
|
184
|
-
const after = pattern.slice(braceMatch.index + braceMatch[0].length);
|
|
185
|
-
const options = braceMatch[1].split(",");
|
|
186
|
-
const results = [];
|
|
187
|
-
for (const opt of options) {
|
|
188
|
-
const expanded = this.expandBraces(before + opt + after);
|
|
189
|
-
results.push(...expanded);
|
|
190
|
-
}
|
|
191
|
-
return results;
|
|
192
|
-
}
|
|
193
|
-
async matchPattern(pattern, cwd) {
|
|
194
|
-
const parts = pattern.split("/").filter((p) => p !== "");
|
|
195
|
-
const isAbsolute2 = pattern.startsWith("/");
|
|
196
|
-
const startDir = isAbsolute2 ? "/" : cwd;
|
|
197
|
-
return this.matchParts(parts, startDir);
|
|
198
|
-
}
|
|
199
|
-
async matchParts(parts, currentPath) {
|
|
200
|
-
if (parts.length === 0) {
|
|
201
|
-
return [currentPath];
|
|
202
|
-
}
|
|
203
|
-
const [part, ...rest] = parts;
|
|
204
|
-
if (part === "**") {
|
|
205
|
-
const results = [];
|
|
206
|
-
const withoutStar = await this.matchParts(rest, currentPath);
|
|
207
|
-
results.push(...withoutStar);
|
|
208
|
-
try {
|
|
209
|
-
const realPath = this.resolveSafePath(currentPath);
|
|
210
|
-
const entries = await this.underlyingFs.promises.readdir(realPath);
|
|
211
|
-
for (const entry of entries) {
|
|
212
|
-
const entryPath = this.pathOps.join(currentPath, String(entry));
|
|
213
|
-
try {
|
|
214
|
-
const entryRealPath = this.resolveSafePath(entryPath);
|
|
215
|
-
const stat = await this.underlyingFs.promises.stat(entryRealPath);
|
|
216
|
-
if (stat.isDirectory()) {
|
|
217
|
-
const subMatches = await this.matchParts(parts, entryPath);
|
|
218
|
-
results.push(...subMatches);
|
|
219
|
-
}
|
|
220
|
-
} catch {}
|
|
221
|
-
}
|
|
222
|
-
} catch {}
|
|
223
|
-
return results;
|
|
224
|
-
}
|
|
225
|
-
const regex = this.globToRegex(part);
|
|
226
|
-
try {
|
|
227
|
-
const realPath = this.resolveSafePath(currentPath);
|
|
228
|
-
const entries = await this.underlyingFs.promises.readdir(realPath);
|
|
229
|
-
const results = [];
|
|
230
|
-
for (const entry of entries) {
|
|
231
|
-
const entryName = String(entry);
|
|
232
|
-
if (regex.test(entryName)) {
|
|
233
|
-
const entryPath = this.pathOps.join(currentPath, entryName);
|
|
234
|
-
if (rest.length === 0) {
|
|
235
|
-
results.push(entryPath);
|
|
236
|
-
} else {
|
|
237
|
-
try {
|
|
238
|
-
const entryRealPath = this.resolveSafePath(entryPath);
|
|
239
|
-
const stat = await this.underlyingFs.promises.stat(entryRealPath);
|
|
240
|
-
if (stat.isDirectory()) {
|
|
241
|
-
const subMatches = await this.matchParts(rest, entryPath);
|
|
242
|
-
results.push(...subMatches);
|
|
243
|
-
}
|
|
244
|
-
} catch {}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return results;
|
|
249
|
-
} catch {
|
|
250
|
-
return [];
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
globToRegex(pattern) {
|
|
254
|
-
let regex = "^";
|
|
255
|
-
for (let i = 0;i < pattern.length; i++) {
|
|
256
|
-
const char = pattern[i];
|
|
257
|
-
if (char === "*") {
|
|
258
|
-
regex += "[^/]*";
|
|
259
|
-
} else if (char === "?") {
|
|
260
|
-
regex += "[^/]";
|
|
261
|
-
} else if (char === "[") {
|
|
262
|
-
let j = i + 1;
|
|
263
|
-
let classContent = "";
|
|
264
|
-
while (j < pattern.length && pattern[j] !== "]") {
|
|
265
|
-
classContent += pattern[j];
|
|
266
|
-
j++;
|
|
267
|
-
}
|
|
268
|
-
regex += `[${classContent}]`;
|
|
269
|
-
i = j;
|
|
270
|
-
} else if (".+^${}()|\\".includes(char)) {
|
|
271
|
-
regex += "\\" + char;
|
|
272
|
-
} else {
|
|
273
|
-
regex += char;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
regex += "$";
|
|
277
|
-
return new RegExp(regex);
|
|
205
|
+
return globVirtualFS(this, pattern, { cwd });
|
|
278
206
|
}
|
|
279
207
|
}
|
|
280
208
|
export {
|
|
281
209
|
FileSystem
|
|
282
210
|
};
|
|
283
211
|
|
|
284
|
-
//# debugId=
|
|
212
|
+
//# debugId=9ACC9F274DA10B5E64756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/fs/real-fs.ts"],
|
|
4
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>;\nexport interface PathOps {\n readonly separator: string;\n resolve(...paths: string[]): string;\n normalize(path: string): string;\n join(...paths: string[]): string;\n relative(from: string, to: string): string;\n isAbsolute(path: string): boolean;\n dirname(path: string): string;\n basename(path: string): string;\n}\n\n// Minimal interface for the underlying fs (compatible with node:fs and memfs)\nexport interface UnderlyingFS {\n pathOps?: PathOps;\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 };\nconst nodePathOps: PathOps = {\n separator: path.sep,\n resolve: (...paths) => path.resolve(...paths),\n normalize: (filePath) => path.normalize(filePath),\n join: (...paths) => path.join(...paths),\n relative: (from, to) => path.relative(from, to),\n isAbsolute: (filePath) => path.isAbsolute(filePath),\n dirname: (filePath) => path.dirname(filePath),\n basename: (filePath) => path.basename(filePath),\n};\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 private readonly pathOps: PathOps;\n protected readonly underlyingFs: UnderlyingFS;\n\n constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS) {\n const underlyingFs = fs ?? defaultFS;\n this.pathOps = underlyingFs.pathOps ?? nodePathOps;\n this.mountBase = mountPath ? this.pathOps.resolve(mountPath) : null;\n this.rules = this.compileRules(permissions ?? {});\n this.underlyingFs = underlyingFs;\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 public 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 public 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 this.pathOps.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 = this.pathOps.normalize(virtualPath);\n const relativePath = normalized.startsWith(\"/\") ? normalized.slice(1) : normalized;\n const realPath = this.pathOps.join(this.mountBase, relativePath);\n const resolved = this.pathOps.resolve(realPath);\n\n // Double-check containment (defense in depth), including root mounts.\n const relativeFromMount = this.pathOps.relative(this.mountBase, resolved);\n const escapesMount =\n relativeFromMount === \"..\" ||\n relativeFromMount.startsWith(`..${this.pathOps.separator}`) ||\n this.pathOps.isAbsolute(relativeFromMount);\n if (escapesMount) {\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 async readFile(filePath: string, encoding: BufferEncoding): Promise<string>;\n async readFile(filePath: string, encoding?: BufferEncoding): Promise<Buffer | string> {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const content = await this.underlyingFs.promises.readFile(realPath);\n const buf = Buffer.from(content);\n return encoding ? buf.toString(encoding) : buf;\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 this.pathOps.resolve(\"/\", ...paths);\n }\n\n dirname(filePath: string): string {\n return this.pathOps.dirname(filePath);\n }\n\n basename(filePath: string): string {\n return this.pathOps.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 = this.pathOps.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 = this.pathOps.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"
|
|
5
|
+
"import * as path from \"path\";\nimport * as nodeFs from \"node:fs/promises\";\nimport type { VirtualFS, FileStat } from \"../types.mjs\";\nimport { globVirtualFS } from \"../utils/glob.mjs\";\nimport {\n discardsSpecialFileWrites,\n existsSpecialFile,\n getSpecialPathError,\n readSpecialFile,\n statSpecialFile,\n} from \"./special-files.mjs\";\n\nexport type Permission = \"read-write\" | \"read-only\" | \"excluded\";\nexport type PermissionRules = Record<string, Permission>;\nexport interface PathOps {\n readonly separator: string;\n resolve(...paths: string[]): string;\n normalize(path: string): string;\n join(...paths: string[]): string;\n relative(from: string, to: string): string;\n isAbsolute(path: string): boolean;\n dirname(path: string): string;\n basename(path: string): string;\n}\n\n// Minimal interface for the underlying fs (compatible with node:fs and memfs)\nexport interface UnderlyingFS {\n pathOps?: PathOps;\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 };\nconst nodePathOps: PathOps = {\n separator: path.sep,\n resolve: (...paths) => path.resolve(...paths),\n normalize: (filePath) => path.normalize(filePath),\n join: (...paths) => path.join(...paths),\n relative: (from, to) => path.relative(from, to),\n isAbsolute: (filePath) => path.isAbsolute(filePath),\n dirname: (filePath) => path.dirname(filePath),\n basename: (filePath) => path.basename(filePath),\n};\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 private readonly pathOps: PathOps;\n protected readonly underlyingFs: UnderlyingFS;\n\n constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS) {\n const underlyingFs = fs ?? defaultFS;\n this.pathOps = underlyingFs.pathOps ?? nodePathOps;\n this.mountBase = mountPath ? this.pathOps.resolve(mountPath) : null;\n this.rules = this.compileRules(permissions ?? {});\n this.underlyingFs = underlyingFs;\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 public 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 public 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 this.pathOps.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 = this.pathOps.normalize(virtualPath);\n const relativePath = normalized.startsWith(\"/\") ? normalized.slice(1) : normalized;\n const realPath = this.pathOps.join(this.mountBase, relativePath);\n const resolved = this.pathOps.resolve(realPath);\n\n // Double-check containment (defense in depth), including root mounts.\n const relativeFromMount = this.pathOps.relative(this.mountBase, resolved);\n const escapesMount =\n relativeFromMount === \"..\" ||\n relativeFromMount.startsWith(`..${this.pathOps.separator}`) ||\n this.pathOps.isAbsolute(relativeFromMount);\n if (escapesMount) {\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 async readFile(filePath: string, encoding: BufferEncoding): Promise<string>;\n async readFile(filePath: string, encoding?: BufferEncoding): Promise<Buffer | string> {\n const specialContent = readSpecialFile(filePath, encoding);\n if (specialContent !== undefined) {\n return specialContent;\n }\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const content = await this.underlyingFs.promises.readFile(realPath);\n const buf = Buffer.from(content);\n return encoding ? buf.toString(encoding) : buf;\n }\n\n async readdir(dirPath: string): Promise<string[]> {\n const specialError = getSpecialPathError(dirPath, \"readdir\");\n if (specialError) {\n throw specialError;\n }\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 const specialStat = statSpecialFile(filePath);\n if (specialStat) {\n return specialStat;\n }\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 const specialExists = existsSpecialFile(filePath);\n if (specialExists !== undefined) {\n return specialExists;\n }\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 if (discardsSpecialFileWrites(filePath)) {\n return;\n }\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 if (discardsSpecialFileWrites(filePath)) {\n return;\n }\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 const specialError = getSpecialPathError(dirPath, \"mkdir\");\n if (specialError) {\n throw specialError;\n }\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 const specialError = getSpecialPathError(filePath, \"rm\");\n if (specialError) {\n throw specialError;\n }\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 this.pathOps.resolve(\"/\", ...paths);\n }\n\n dirname(filePath: string): string {\n return this.pathOps.dirname(filePath);\n }\n\n basename(filePath: string): string {\n return this.pathOps.basename(filePath);\n }\n\n // Glob expansion\n async glob(pattern: string, opts?: { cwd?: string }): Promise<string[]> {\n const cwd = opts?.cwd ?? \"/\";\n this.checkPermission(cwd, \"read\");\n return globVirtualFS(this, pattern, { cwd });\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";AAAA;AACA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "9ACC9F274DA10B5E64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/fs/special-files.ts
|
|
2
|
+
var DEV_NULL_PATH = "/dev/null";
|
|
3
|
+
function normalizeSpecialPath(path) {
|
|
4
|
+
return path.replace(/\\/g, "/");
|
|
5
|
+
}
|
|
6
|
+
function isDevNullPath(path) {
|
|
7
|
+
return normalizeSpecialPath(path) === DEV_NULL_PATH;
|
|
8
|
+
}
|
|
9
|
+
function readSpecialFile(path, encoding) {
|
|
10
|
+
if (!isDevNullPath(path)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
return encoding ? "" : Buffer.alloc(0);
|
|
14
|
+
}
|
|
15
|
+
function statSpecialFile(path) {
|
|
16
|
+
if (!isDevNullPath(path)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
isFile: () => true,
|
|
21
|
+
isDirectory: () => false,
|
|
22
|
+
size: 0,
|
|
23
|
+
mtime: new Date(0)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function existsSpecialFile(path) {
|
|
27
|
+
if (!isDevNullPath(path)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
function discardsSpecialFileWrites(path) {
|
|
33
|
+
return isDevNullPath(path);
|
|
34
|
+
}
|
|
35
|
+
function getSpecialPathError(path, operation) {
|
|
36
|
+
if (!isDevNullPath(path)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
switch (operation) {
|
|
40
|
+
case "mkdir":
|
|
41
|
+
return new Error(`EEXIST: file already exists, mkdir '${DEV_NULL_PATH}'`);
|
|
42
|
+
case "readdir":
|
|
43
|
+
return new Error(`ENOTDIR: not a directory, scandir '${DEV_NULL_PATH}'`);
|
|
44
|
+
case "rm":
|
|
45
|
+
return new Error(`EPERM: operation not permitted, rm '${DEV_NULL_PATH}'`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
statSpecialFile,
|
|
50
|
+
readSpecialFile,
|
|
51
|
+
isDevNullPath,
|
|
52
|
+
getSpecialPathError,
|
|
53
|
+
existsSpecialFile,
|
|
54
|
+
discardsSpecialFileWrites,
|
|
55
|
+
DEV_NULL_PATH
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
//# debugId=5E984ABB0F12E5E964756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/fs/special-files.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { FileStat } from \"../types.mjs\";\n\nexport const DEV_NULL_PATH = \"/dev/null\";\n\nfunction normalizeSpecialPath(path: string): string {\n return path.replace(/\\\\/g, \"/\");\n}\n\nexport function isDevNullPath(path: string): boolean {\n return normalizeSpecialPath(path) === DEV_NULL_PATH;\n}\n\nexport function readSpecialFile(\n path: string,\n encoding?: BufferEncoding\n): Buffer | string | undefined {\n if (!isDevNullPath(path)) {\n return undefined;\n }\n\n return encoding ? \"\" : Buffer.alloc(0);\n}\n\nexport function statSpecialFile(path: string): FileStat | undefined {\n if (!isDevNullPath(path)) {\n return undefined;\n }\n\n return {\n isFile: () => true,\n isDirectory: () => false,\n size: 0,\n mtime: new Date(0),\n };\n}\n\nexport function existsSpecialFile(path: string): boolean | undefined {\n if (!isDevNullPath(path)) {\n return undefined;\n }\n\n return true;\n}\n\nexport function discardsSpecialFileWrites(path: string): boolean {\n return isDevNullPath(path);\n}\n\nexport function getSpecialPathError(\n path: string,\n operation: \"mkdir\" | \"readdir\" | \"rm\"\n): Error | undefined {\n if (!isDevNullPath(path)) {\n return undefined;\n }\n\n switch (operation) {\n case \"mkdir\":\n return new Error(`EEXIST: file already exists, mkdir '${DEV_NULL_PATH}'`);\n case \"readdir\":\n return new Error(`ENOTDIR: not a directory, scandir '${DEV_NULL_PATH}'`);\n case \"rm\":\n return new Error(`EPERM: operation not permitted, rm '${DEV_NULL_PATH}'`);\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAEO,IAAM,gBAAgB;AAE7B,SAAS,oBAAoB,CAAC,MAAsB;AAAA,EAClD,OAAO,KAAK,QAAQ,OAAO,GAAG;AAAA;AAGzB,SAAS,aAAa,CAAC,MAAuB;AAAA,EACnD,OAAO,qBAAqB,IAAI,MAAM;AAAA;AAGjC,SAAS,eAAe,CAC7B,MACA,UAC6B;AAAA,EAC7B,IAAI,CAAC,cAAc,IAAI,GAAG;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,OAAO,WAAW,KAAK,OAAO,MAAM,CAAC;AAAA;AAGhC,SAAS,eAAe,CAAC,MAAoC;AAAA,EAClE,IAAI,CAAC,cAAc,IAAI,GAAG;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,OAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,aAAa,MAAM;AAAA,IACnB,MAAM;AAAA,IACN,OAAO,IAAI,KAAK,CAAC;AAAA,EACnB;AAAA;AAGK,SAAS,iBAAiB,CAAC,MAAmC;AAAA,EACnE,IAAI,CAAC,cAAc,IAAI,GAAG;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,OAAO;AAAA;AAGF,SAAS,yBAAyB,CAAC,MAAuB;AAAA,EAC/D,OAAO,cAAc,IAAI;AAAA;AAGpB,SAAS,mBAAmB,CACjC,MACA,WACmB;AAAA,EACnB,IAAI,CAAC,cAAc,IAAI,GAAG;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,QAAQ;AAAA,SACD;AAAA,MACH,OAAO,IAAI,MAAM,uCAAuC,gBAAgB;AAAA,SACrE;AAAA,MACH,OAAO,IAAI,MAAM,sCAAsC,gBAAgB;AAAA,SACpE;AAAA,MACH,OAAO,IAAI,MAAM,uCAAuC,gBAAgB;AAAA;AAAA;",
|
|
8
|
+
"debugId": "5E984ABB0F12E5E964756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/dist/mjs/src/index.mjs
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "./fs/index.mjs";
|
|
34
34
|
import { createStdin, StdinImpl } from "./io/index.mjs";
|
|
35
35
|
import { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from "./io/index.mjs";
|
|
36
|
-
import { escape, escapeForInterpolation } from "./utils/index.mjs";
|
|
36
|
+
import { escape, escapeForInterpolation, globVirtualFS } from "./utils/index.mjs";
|
|
37
37
|
export {
|
|
38
38
|
tokenToString,
|
|
39
39
|
parse,
|
|
@@ -55,6 +55,7 @@ export {
|
|
|
55
55
|
isCaseNode,
|
|
56
56
|
isArithmeticNode,
|
|
57
57
|
isAndNode,
|
|
58
|
+
globVirtualFS,
|
|
58
59
|
escapeForInterpolation,
|
|
59
60
|
escape,
|
|
60
61
|
createWebUnderlyingFS,
|
|
@@ -82,4 +83,4 @@ export {
|
|
|
82
83
|
BreakException
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
//# debugId=
|
|
86
|
+
//# debugId=6B9F05D80609142564756E2164756E21
|
|
@@ -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 IfNode,\n ForNode,\n WhileNode,\n UntilNode,\n CaseNode,\n CaseClause,\n ArithmeticNode,\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 isIfNode,\n isForNode,\n isWhileNode,\n isUntilNode,\n isCaseNode,\n isArithmeticNode,\n} from \"./parser/index.mjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions, BreakException, ContinueException } from \"./interpreter/index.mjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.mjs\";\nexport {\n FileSystem,\n ReadOnlyFileSystem,\n WebFileSystem,\n createWebUnderlyingFS,\n type PathOps,\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"
|
|
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 IfNode,\n ForNode,\n WhileNode,\n UntilNode,\n CaseNode,\n CaseClause,\n ArithmeticNode,\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 isIfNode,\n isForNode,\n isWhileNode,\n isUntilNode,\n isCaseNode,\n isArithmeticNode,\n} from \"./parser/index.mjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions, BreakException, ContinueException } from \"./interpreter/index.mjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.mjs\";\nexport {\n FileSystem,\n ReadOnlyFileSystem,\n WebFileSystem,\n createWebUnderlyingFS,\n type PathOps,\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, globVirtualFS } from \"./utils/index.mjs\";\nexport type { GlobVirtualFS, GlobOptions } from \"./utils/index.mjs\";\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": ";AACA;AACA;AAgBA;AAGA;AAGA;AAIA;AAsBA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA;AACA;AAGA;",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "6B9F05D80609142564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createCommandContext } from "./context.mjs";
|
|
3
3
|
import { createStdin } from "../io/stdin.mjs";
|
|
4
4
|
import { createStdout, createStderr, createPipe, createBufferTargetCollector } from "../io/stdout.mjs";
|
|
5
|
+
import { isDevNullPath } from "../fs/special-files.mjs";
|
|
5
6
|
|
|
6
7
|
class BreakException extends Error {
|
|
7
8
|
levels;
|
|
@@ -239,7 +240,7 @@ class Interpreter {
|
|
|
239
240
|
stderr
|
|
240
241
|
};
|
|
241
242
|
}
|
|
242
|
-
if (target
|
|
243
|
+
if (isDevNullPath(target)) {
|
|
243
244
|
return {
|
|
244
245
|
stdin: async function* () {}(),
|
|
245
246
|
stdout,
|
|
@@ -258,7 +259,7 @@ class Interpreter {
|
|
|
258
259
|
}
|
|
259
260
|
case ">": {
|
|
260
261
|
const collector = createStdout();
|
|
261
|
-
if (target
|
|
262
|
+
if (isDevNullPath(target)) {
|
|
262
263
|
return { stdin, stdout: collector, stderr };
|
|
263
264
|
}
|
|
264
265
|
const path = this.fs.resolve(this.cwd, target);
|
|
@@ -270,7 +271,7 @@ class Interpreter {
|
|
|
270
271
|
}
|
|
271
272
|
case ">>": {
|
|
272
273
|
const collector = createStdout();
|
|
273
|
-
if (target
|
|
274
|
+
if (isDevNullPath(target)) {
|
|
274
275
|
return { stdin, stdout: collector, stderr };
|
|
275
276
|
}
|
|
276
277
|
const path = this.fs.resolve(this.cwd, target);
|
|
@@ -282,7 +283,7 @@ class Interpreter {
|
|
|
282
283
|
}
|
|
283
284
|
case "2>": {
|
|
284
285
|
const collector = createStderr();
|
|
285
|
-
if (target
|
|
286
|
+
if (isDevNullPath(target)) {
|
|
286
287
|
return { stdin, stdout, stderr: collector };
|
|
287
288
|
}
|
|
288
289
|
const path = this.fs.resolve(this.cwd, target);
|
|
@@ -294,7 +295,7 @@ class Interpreter {
|
|
|
294
295
|
}
|
|
295
296
|
case "2>>": {
|
|
296
297
|
const collector = createStderr();
|
|
297
|
-
if (target
|
|
298
|
+
if (isDevNullPath(target)) {
|
|
298
299
|
return { stdin, stdout, stderr: collector };
|
|
299
300
|
}
|
|
300
301
|
const path = this.fs.resolve(this.cwd, target);
|
|
@@ -306,7 +307,7 @@ class Interpreter {
|
|
|
306
307
|
}
|
|
307
308
|
case "&>": {
|
|
308
309
|
const collector = createStdout();
|
|
309
|
-
if (target
|
|
310
|
+
if (isDevNullPath(target)) {
|
|
310
311
|
return { stdin, stdout: collector, stderr: collector };
|
|
311
312
|
}
|
|
312
313
|
const path = this.fs.resolve(this.cwd, target);
|
|
@@ -318,7 +319,7 @@ class Interpreter {
|
|
|
318
319
|
}
|
|
319
320
|
case "&>>": {
|
|
320
321
|
const collector = createStdout();
|
|
321
|
-
if (target
|
|
322
|
+
if (isDevNullPath(target)) {
|
|
322
323
|
return { stdin, stdout: collector, stderr: collector };
|
|
323
324
|
}
|
|
324
325
|
const path = this.fs.resolve(this.cwd, target);
|
|
@@ -826,4 +827,4 @@ export {
|
|
|
826
827
|
BreakException
|
|
827
828
|
};
|
|
828
829
|
|
|
829
|
-
//# debugId=
|
|
830
|
+
//# debugId=23FFAE76E213214864756E2164756E21
|