@outfitter/file-ops 0.2.3 → 0.2.4
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 +48 -48
- package/dist/index.js +4 -3
- package/package.json +26 -26
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
securePath,
|
|
17
17
|
glob,
|
|
18
18
|
withLock,
|
|
19
|
-
atomicWrite
|
|
19
|
+
atomicWrite,
|
|
20
20
|
} from "@outfitter/file-ops";
|
|
21
21
|
|
|
22
22
|
// Find workspace root by marker files (.git, package.json)
|
|
@@ -34,7 +34,7 @@ if (rootResult.isOk()) {
|
|
|
34
34
|
// Find files with glob patterns
|
|
35
35
|
const files = await glob("**/*.ts", {
|
|
36
36
|
cwd: "/project",
|
|
37
|
-
ignore: ["node_modules/**", "**/*.test.ts"]
|
|
37
|
+
ignore: ["node_modules/**", "**/*.test.ts"],
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// Atomic write with file locking
|
|
@@ -60,16 +60,16 @@ if (result.isOk()) {
|
|
|
60
60
|
|
|
61
61
|
**Options:**
|
|
62
62
|
|
|
63
|
-
| Option
|
|
64
|
-
|
|
63
|
+
| Option | Type | Default | Description |
|
|
64
|
+
| --------- | ---------- | -------------------------- | -------------------------------------- |
|
|
65
65
|
| `markers` | `string[]` | `[".git", "package.json"]` | Marker files/directories to search for |
|
|
66
|
-
| `stopAt`
|
|
66
|
+
| `stopAt` | `string` | filesystem root | Stop searching at this directory |
|
|
67
67
|
|
|
68
68
|
```typescript
|
|
69
69
|
// Custom markers for Rust or Python projects
|
|
70
70
|
const result = await findWorkspaceRoot(startPath, {
|
|
71
71
|
markers: ["Cargo.toml", "pyproject.toml"],
|
|
72
|
-
stopAt: "/home/user"
|
|
72
|
+
stopAt: "/home/user",
|
|
73
73
|
});
|
|
74
74
|
```
|
|
75
75
|
|
|
@@ -102,12 +102,12 @@ console.log(outside); // false
|
|
|
102
102
|
|
|
103
103
|
#### Security Model
|
|
104
104
|
|
|
105
|
-
| Attack Vector
|
|
106
|
-
|
|
107
|
-
| Path traversal (`../`)
|
|
108
|
-
| Null bytes (`\x00`)
|
|
109
|
-
| Absolute paths
|
|
110
|
-
| Escape from base directory | Defense-in-depth verification
|
|
105
|
+
| Attack Vector | Protection |
|
|
106
|
+
| -------------------------- | --------------------------------- |
|
|
107
|
+
| Path traversal (`../`) | Blocked by all security functions |
|
|
108
|
+
| Null bytes (`\x00`) | Rejected immediately |
|
|
109
|
+
| Absolute paths | Blocked when relative expected |
|
|
110
|
+
| Escape from base directory | Defense-in-depth verification |
|
|
111
111
|
|
|
112
112
|
#### `securePath(path, basePath)`
|
|
113
113
|
|
|
@@ -122,9 +122,9 @@ if (result.isOk()) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// These all return ValidationError:
|
|
125
|
-
securePath("../etc/passwd", base);
|
|
126
|
-
securePath("/etc/passwd", base);
|
|
127
|
-
securePath("file\x00.txt", base);
|
|
125
|
+
securePath("../etc/passwd", base); // Traversal sequence
|
|
126
|
+
securePath("/etc/passwd", base); // Absolute path
|
|
127
|
+
securePath("file\x00.txt", base); // Null byte
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
**UNSAFE pattern - never do this:**
|
|
@@ -161,8 +161,8 @@ if (result.isOk()) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
// Rejects dangerous segments
|
|
164
|
-
resolveSafePath("/app", "..", "etc");
|
|
165
|
-
resolveSafePath("/app", "/etc/passwd");
|
|
164
|
+
resolveSafePath("/app", "..", "etc"); // Error: traversal
|
|
165
|
+
resolveSafePath("/app", "/etc/passwd"); // Error: absolute segment
|
|
166
166
|
```
|
|
167
167
|
|
|
168
168
|
### Glob Patterns
|
|
@@ -178,7 +178,7 @@ const result = await glob("**/*.ts", { cwd: "/project" });
|
|
|
178
178
|
// Exclude test files and node_modules
|
|
179
179
|
const result = await glob("**/*.ts", {
|
|
180
180
|
cwd: "/project",
|
|
181
|
-
ignore: ["**/*.test.ts", "**/node_modules/**"]
|
|
181
|
+
ignore: ["**/*.test.ts", "**/node_modules/**"],
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
// Include dot files
|
|
@@ -187,28 +187,28 @@ const result = await glob("**/.*", { cwd: "/project", dot: true });
|
|
|
187
187
|
|
|
188
188
|
**Options:**
|
|
189
189
|
|
|
190
|
-
| Option
|
|
191
|
-
|
|
192
|
-
| `cwd`
|
|
193
|
-
| `ignore`
|
|
194
|
-
| `followSymlinks` | `boolean`
|
|
195
|
-
| `dot`
|
|
190
|
+
| Option | Type | Default | Description |
|
|
191
|
+
| ---------------- | ---------- | --------------- | --------------------------- |
|
|
192
|
+
| `cwd` | `string` | `process.cwd()` | Base directory for matching |
|
|
193
|
+
| `ignore` | `string[]` | `[]` | Patterns to exclude |
|
|
194
|
+
| `followSymlinks` | `boolean` | `false` | Follow symbolic links |
|
|
195
|
+
| `dot` | `boolean` | `false` | Include dot files |
|
|
196
196
|
|
|
197
197
|
**Pattern Syntax:**
|
|
198
198
|
|
|
199
|
-
| Pattern
|
|
200
|
-
|
|
201
|
-
| `*`
|
|
202
|
-
| `**`
|
|
203
|
-
| `{a,b}`
|
|
204
|
-
| `[abc]`
|
|
205
|
-
| `!pattern` | Negation (in ignore array)
|
|
199
|
+
| Pattern | Matches |
|
|
200
|
+
| ---------- | ------------------------------------------ |
|
|
201
|
+
| `*` | Any characters except `/` |
|
|
202
|
+
| `**` | Any characters including `/` (recursive) |
|
|
203
|
+
| `{a,b}` | Alternation (matches `a` or `b`) |
|
|
204
|
+
| `[abc]` | Character class (matches `a`, `b`, or `c`) |
|
|
205
|
+
| `!pattern` | Negation (in ignore array) |
|
|
206
206
|
|
|
207
207
|
```typescript
|
|
208
208
|
// Negation patterns in ignore array
|
|
209
209
|
const result = await glob("src/**/*.ts", {
|
|
210
210
|
cwd: "/project",
|
|
211
|
-
ignore: ["**/*.ts", "!**/index.ts"]
|
|
211
|
+
ignore: ["**/*.ts", "!**/index.ts"], // Ignore all except index.ts
|
|
212
212
|
});
|
|
213
213
|
```
|
|
214
214
|
|
|
@@ -275,9 +275,9 @@ if (await isLocked("/data/file.db")) {
|
|
|
275
275
|
|
|
276
276
|
```typescript
|
|
277
277
|
interface FileLock {
|
|
278
|
-
path: string;
|
|
279
|
-
lockPath: string;
|
|
280
|
-
pid: number;
|
|
278
|
+
path: string; // Path to the locked file
|
|
279
|
+
lockPath: string; // Path to the .lock file
|
|
280
|
+
pid: number; // Process ID holding the lock
|
|
281
281
|
timestamp: number; // When lock was acquired
|
|
282
282
|
}
|
|
283
283
|
```
|
|
@@ -299,21 +299,21 @@ if (result.isErr()) {
|
|
|
299
299
|
|
|
300
300
|
**Options:**
|
|
301
301
|
|
|
302
|
-
| Option
|
|
303
|
-
|
|
304
|
-
| `createParentDirs`
|
|
302
|
+
| Option | Type | Default | Description |
|
|
303
|
+
| --------------------- | --------- | ------- | ----------------------------------- |
|
|
304
|
+
| `createParentDirs` | `boolean` | `true` | Create parent directories if needed |
|
|
305
305
|
| `preservePermissions` | `boolean` | `false` | Keep permissions from existing file |
|
|
306
|
-
| `mode`
|
|
306
|
+
| `mode` | `number` | `0o644` | File mode for new files |
|
|
307
307
|
|
|
308
308
|
```typescript
|
|
309
309
|
// Preserve executable permissions
|
|
310
310
|
await atomicWrite("/scripts/run.sh", newContent, {
|
|
311
|
-
preservePermissions: true
|
|
311
|
+
preservePermissions: true,
|
|
312
312
|
});
|
|
313
313
|
|
|
314
314
|
// Create nested directories automatically
|
|
315
315
|
await atomicWrite("/data/deep/nested/file.json", content, {
|
|
316
|
-
createParentDirs: true
|
|
316
|
+
createParentDirs: true,
|
|
317
317
|
});
|
|
318
318
|
```
|
|
319
319
|
|
|
@@ -325,7 +325,7 @@ Serializes and writes JSON data atomically.
|
|
|
325
325
|
const result = await atomicWriteJson("/data/config.json", {
|
|
326
326
|
name: "app",
|
|
327
327
|
version: "1.0.0",
|
|
328
|
-
settings: { debug: false }
|
|
328
|
+
settings: { debug: false },
|
|
329
329
|
});
|
|
330
330
|
```
|
|
331
331
|
|
|
@@ -348,12 +348,12 @@ if (result.isOk()) {
|
|
|
348
348
|
|
|
349
349
|
**Error Types:**
|
|
350
350
|
|
|
351
|
-
| Error
|
|
352
|
-
|
|
353
|
-
| `NotFoundError`
|
|
354
|
-
| `ValidationError` | `securePath`, `isPathSafe`, `resolveSafePath`, `atomicWriteJson` | Invalid path or data
|
|
355
|
-
| `ConflictError`
|
|
356
|
-
| `InternalError`
|
|
351
|
+
| Error | Functions | When |
|
|
352
|
+
| ----------------- | ---------------------------------------------------------------- | -------------------------- |
|
|
353
|
+
| `NotFoundError` | `findWorkspaceRoot`, `getRelativePath` | No workspace marker found |
|
|
354
|
+
| `ValidationError` | `securePath`, `isPathSafe`, `resolveSafePath`, `atomicWriteJson` | Invalid path or data |
|
|
355
|
+
| `ConflictError` | `acquireLock`, `withLock` | File already locked |
|
|
356
|
+
| `InternalError` | `glob`, `releaseLock`, `withLock`, `atomicWrite` | Filesystem or system error |
|
|
357
357
|
|
|
358
358
|
## Dependencies
|
|
359
359
|
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
//
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/file-ops/src/index.ts
|
|
2
3
|
import {
|
|
3
4
|
writeFile as fsWriteFile,
|
|
4
5
|
mkdir,
|
|
5
6
|
rename,
|
|
6
7
|
stat,
|
|
7
8
|
unlink
|
|
8
|
-
} from "
|
|
9
|
+
} from "fs/promises";
|
|
9
10
|
import {
|
|
10
11
|
dirname,
|
|
11
12
|
isAbsolute,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
relative,
|
|
15
16
|
resolve,
|
|
16
17
|
sep
|
|
17
|
-
} from "
|
|
18
|
+
} from "path";
|
|
18
19
|
import {
|
|
19
20
|
ConflictError,
|
|
20
21
|
InternalError,
|
package/package.json
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outfitter/file-ops",
|
|
3
|
+
"version": "0.2.4",
|
|
3
4
|
"description": "Workspace detection, secure path handling, and file locking for Outfitter",
|
|
4
|
-
"
|
|
5
|
-
|
|
5
|
+
"keywords": [
|
|
6
|
+
"file-ops",
|
|
7
|
+
"outfitter",
|
|
8
|
+
"security",
|
|
9
|
+
"typescript",
|
|
10
|
+
"workspace"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/outfitter-dev/outfitter.git",
|
|
16
|
+
"directory": "packages/file-ops"
|
|
17
|
+
},
|
|
6
18
|
"files": [
|
|
7
19
|
"dist"
|
|
8
20
|
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
9
23
|
"module": "./dist/index.js",
|
|
10
24
|
"types": "./dist/index.d.ts",
|
|
11
25
|
"exports": {
|
|
@@ -17,38 +31,24 @@
|
|
|
17
31
|
},
|
|
18
32
|
"./package.json": "./package.json"
|
|
19
33
|
},
|
|
20
|
-
"
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
21
37
|
"scripts": {
|
|
22
|
-
"build": "bunup --filter @outfitter/file-ops",
|
|
23
|
-
"lint": "
|
|
24
|
-
"lint:fix": "
|
|
38
|
+
"build": "cd ../.. && bunup --filter @outfitter/file-ops",
|
|
39
|
+
"lint": "oxlint ./src",
|
|
40
|
+
"lint:fix": "oxlint --fix ./src",
|
|
25
41
|
"test": "bun test",
|
|
26
42
|
"typecheck": "tsc --noEmit",
|
|
27
43
|
"clean": "rm -rf dist",
|
|
28
44
|
"prepublishOnly": "bun ../../scripts/check-publish-manifest.ts"
|
|
29
45
|
},
|
|
30
46
|
"dependencies": {
|
|
31
|
-
"@outfitter/contracts": "0.4.
|
|
32
|
-
"@outfitter/types": "0.2.
|
|
47
|
+
"@outfitter/contracts": "0.4.2",
|
|
48
|
+
"@outfitter/types": "0.2.4"
|
|
33
49
|
},
|
|
34
50
|
"devDependencies": {
|
|
35
|
-
"@types/bun": "
|
|
36
|
-
"typescript": "^5.
|
|
37
|
-
},
|
|
38
|
-
"keywords": [
|
|
39
|
-
"outfitter",
|
|
40
|
-
"file-ops",
|
|
41
|
-
"workspace",
|
|
42
|
-
"security",
|
|
43
|
-
"typescript"
|
|
44
|
-
],
|
|
45
|
-
"license": "MIT",
|
|
46
|
-
"repository": {
|
|
47
|
-
"type": "git",
|
|
48
|
-
"url": "https://github.com/outfitter-dev/outfitter.git",
|
|
49
|
-
"directory": "packages/file-ops"
|
|
50
|
-
},
|
|
51
|
-
"publishConfig": {
|
|
52
|
-
"access": "public"
|
|
51
|
+
"@types/bun": "^1.3.9",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
53
53
|
}
|
|
54
54
|
}
|