@outfitter/file-ops 0.2.2 → 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.d.ts +18 -18
- package/dist/index.js +4 -3
- package/package.json +28 -27
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.d.ts
CHANGED
|
@@ -31,20 +31,20 @@ interface GlobOptions {
|
|
|
31
31
|
*/
|
|
32
32
|
cwd?: string;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Include files and directories starting with a dot in results.
|
|
35
|
+
* @defaultValue false
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
dot?: boolean;
|
|
38
38
|
/**
|
|
39
39
|
* Follow symbolic links when scanning directories.
|
|
40
40
|
* @defaultValue false
|
|
41
41
|
*/
|
|
42
42
|
followSymlinks?: boolean;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* Patterns to exclude from results.
|
|
45
|
+
* Supports negation with "!" prefix to re-include previously excluded files.
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
ignore?: string[];
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Options for atomic write operations.
|
|
@@ -59,16 +59,16 @@ interface AtomicWriteOptions {
|
|
|
59
59
|
*/
|
|
60
60
|
createParentDirs?: boolean;
|
|
61
61
|
/**
|
|
62
|
+
* Unix file mode for newly created files.
|
|
63
|
+
* @defaultValue 0o644
|
|
64
|
+
*/
|
|
65
|
+
mode?: number;
|
|
66
|
+
/**
|
|
62
67
|
* Preserve file permissions from existing file.
|
|
63
68
|
* If the target file does not exist, falls back to the mode option.
|
|
64
69
|
* @defaultValue false
|
|
65
70
|
*/
|
|
66
71
|
preservePermissions?: boolean;
|
|
67
|
-
/**
|
|
68
|
-
* Unix file mode for newly created files.
|
|
69
|
-
* @defaultValue 0o644
|
|
70
|
-
*/
|
|
71
|
-
mode?: number;
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Represents an acquired file lock.
|
|
@@ -77,10 +77,10 @@ interface AtomicWriteOptions {
|
|
|
77
77
|
* acquisition timestamp. Used with acquireLock and releaseLock functions.
|
|
78
78
|
*/
|
|
79
79
|
interface FileLock {
|
|
80
|
-
/** Absolute path to the locked file */
|
|
81
|
-
path: string;
|
|
82
80
|
/** Path to the .lock file that indicates the lock */
|
|
83
81
|
lockPath: string;
|
|
82
|
+
/** Absolute path to the locked file */
|
|
83
|
+
path: string;
|
|
84
84
|
/** Process ID of the lock holder */
|
|
85
85
|
pid: number;
|
|
86
86
|
/** Unix timestamp (milliseconds) when the lock was acquired */
|
|
@@ -102,16 +102,16 @@ interface SharedFileLock extends FileLock {
|
|
|
102
102
|
* Options for lock acquisition.
|
|
103
103
|
*/
|
|
104
104
|
interface LockOptions {
|
|
105
|
-
/**
|
|
106
|
-
* Maximum time in milliseconds to wait for lock acquisition.
|
|
107
|
-
* If not specified, fails immediately if lock cannot be acquired.
|
|
108
|
-
*/
|
|
109
|
-
timeout?: number;
|
|
110
105
|
/**
|
|
111
106
|
* Interval in milliseconds between retry attempts when waiting.
|
|
112
107
|
* @defaultValue 50
|
|
113
108
|
*/
|
|
114
109
|
retryInterval?: number;
|
|
110
|
+
/**
|
|
111
|
+
* Maximum time in milliseconds to wait for lock acquisition.
|
|
112
|
+
* If not specified, fails immediately if lock cannot be acquired.
|
|
113
|
+
*/
|
|
114
|
+
timeout?: number;
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* Finds the workspace root by searching upward for marker files/directories.
|
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,37 +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
|
-
"clean": "rm -rf dist"
|
|
43
|
+
"clean": "rm -rf dist",
|
|
44
|
+
"prepublishOnly": "bun ../../scripts/check-publish-manifest.ts"
|
|
28
45
|
},
|
|
29
46
|
"dependencies": {
|
|
30
|
-
"@outfitter/contracts": "0.4.
|
|
31
|
-
"@outfitter/types": "0.2.
|
|
47
|
+
"@outfitter/contracts": "0.4.2",
|
|
48
|
+
"@outfitter/types": "0.2.4"
|
|
32
49
|
},
|
|
33
50
|
"devDependencies": {
|
|
34
|
-
"@types/bun": "
|
|
35
|
-
"typescript": "^5.
|
|
36
|
-
},
|
|
37
|
-
"keywords": [
|
|
38
|
-
"outfitter",
|
|
39
|
-
"file-ops",
|
|
40
|
-
"workspace",
|
|
41
|
-
"security",
|
|
42
|
-
"typescript"
|
|
43
|
-
],
|
|
44
|
-
"license": "MIT",
|
|
45
|
-
"repository": {
|
|
46
|
-
"type": "git",
|
|
47
|
-
"url": "https://github.com/outfitter-dev/outfitter.git",
|
|
48
|
-
"directory": "packages/file-ops"
|
|
49
|
-
},
|
|
50
|
-
"publishConfig": {
|
|
51
|
-
"access": "public"
|
|
51
|
+
"@types/bun": "^1.3.9",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
52
53
|
}
|
|
53
54
|
}
|