poe-code 4.0.12 → 4.0.14
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/dist/cli/commands/harness.js +41 -3
- package/dist/cli/commands/harness.js.map +1 -1
- package/dist/index.js +1053 -423
- package/dist/index.js.map +4 -4
- package/dist/metafile.json +1 -1
- package/package.json +2 -1
- package/packages/pipeline/dist/plan/discovery.js +3 -3
- package/packages/safejs/dist/cli.js +24 -1
- package/packages/safejs/dist/index.d.ts +2 -0
- package/packages/safejs/dist/index.js +1 -0
- package/packages/safejs/dist/interp/cancel.js +4 -0
- package/packages/safejs/dist/interp/host-bridge.js +32 -1
- package/packages/safejs/dist/interp/promise.d.ts +1 -0
- package/packages/safejs/dist/interp/promise.js +10 -2
- package/packages/safejs/dist/modules/canonical-path.d.ts +15 -0
- package/packages/safejs/dist/modules/canonical-path.js +161 -0
- package/packages/safejs/dist/modules/fs.conformance-cases.d.ts +99 -0
- package/packages/safejs/dist/modules/fs.conformance-cases.js +1293 -0
- package/packages/safejs/dist/modules/fs.d.ts +72 -0
- package/packages/safejs/dist/modules/fs.js +558 -0
- package/packages/safejs/dist/modules/git.js +13 -33
- package/packages/safejs/dist/run.js +2 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poe-code",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.14",
|
|
4
4
|
"description": "CLI tool to configure Poe API for developer workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"smoke:toolcraft-standalone": "node scripts/verify-toolcraft-standalone.mjs",
|
|
91
91
|
"smoke:toolcraft-sdk-types": "node scripts/verify-toolcraft-published-sdk-types.mjs",
|
|
92
92
|
"sync-skills": "tsx scripts/sync-skills.ts",
|
|
93
|
+
"record:fs-conformance": "tsx scripts/record-fs-conformance.ts",
|
|
93
94
|
"sync:json-schema-test-suite": "node scripts/sync-json-schema-test-suite.mjs",
|
|
94
95
|
"sync:uri-template-test": "node scripts/sync-uri-template-test.mjs",
|
|
95
96
|
"postinstall": "node scripts/postinstall-sync-skills.mjs"
|
|
@@ -34,8 +34,8 @@ function countCompletedTasks(planPath, content) {
|
|
|
34
34
|
total
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
async function ensurePlanExists(fs, cwd, planPath) {
|
|
38
|
-
const absolutePath =
|
|
37
|
+
async function ensurePlanExists(fs, cwd, homeDir, planPath) {
|
|
38
|
+
const absolutePath = resolveAbsolutePlanPath(planPath, cwd, homeDir);
|
|
39
39
|
const stat = await fs.stat(absolutePath).catch((error) => {
|
|
40
40
|
if (isNotFound(error)) {
|
|
41
41
|
return undefined;
|
|
@@ -97,7 +97,7 @@ export async function resolvePlanPaths(options) {
|
|
|
97
97
|
.filter((planPath) => planPath.length > 0);
|
|
98
98
|
if (explicitPlans.length > 0) {
|
|
99
99
|
for (const planPath of explicitPlans) {
|
|
100
|
-
await ensurePlanExists(fs, options.cwd, planPath);
|
|
100
|
+
await ensurePlanExists(fs, options.cwd, options.homeDir, planPath);
|
|
101
101
|
}
|
|
102
102
|
return explicitPlans;
|
|
103
103
|
}
|
|
@@ -12,6 +12,7 @@ import { lint } from "./lint.js";
|
|
|
12
12
|
import { createLintModulesFromRuntimeRegistry } from "./lint/runtime-modules.js";
|
|
13
13
|
import { makeAgentModule } from "./modules/agent.js";
|
|
14
14
|
import { makeFailModule } from "./modules/fail.js";
|
|
15
|
+
import { makeFsModule } from "./modules/fs.js";
|
|
15
16
|
import { makeHarnessModule } from "./modules/harness.js";
|
|
16
17
|
import { makeLogModule } from "./modules/log.js";
|
|
17
18
|
import { makeMetricModule } from "./modules/metric.js";
|
|
@@ -80,7 +81,8 @@ function readCurrentWorkingDirectory() {
|
|
|
80
81
|
}
|
|
81
82
|
function parseArgs(argv) {
|
|
82
83
|
const parsed = {
|
|
83
|
-
fix: false
|
|
84
|
+
fix: false,
|
|
85
|
+
fs: false
|
|
84
86
|
};
|
|
85
87
|
for (let index = 0; index < argv.length; index += 1) {
|
|
86
88
|
const arg = argv[index];
|
|
@@ -88,6 +90,15 @@ function parseArgs(argv) {
|
|
|
88
90
|
parsed.fix = true;
|
|
89
91
|
continue;
|
|
90
92
|
}
|
|
93
|
+
if (arg === "--fs") {
|
|
94
|
+
parsed.fs = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (arg === "--fs-root") {
|
|
98
|
+
parsed.fsRoot = readFlagValue(argv, index, arg);
|
|
99
|
+
index += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
91
102
|
if (arg === "--snapshot") {
|
|
92
103
|
parsed.snapshotPath = readFlagValue(argv, index, arg);
|
|
93
104
|
index += 1;
|
|
@@ -116,6 +127,9 @@ function parseArgs(argv) {
|
|
|
116
127
|
}
|
|
117
128
|
parsed.filepath = arg;
|
|
118
129
|
}
|
|
130
|
+
if (!parsed.fs && parsed.fsRoot !== undefined) {
|
|
131
|
+
throw new CliExitError("--fs-root requires --fs. Pass --fs to give the script a filesystem confined to that root, or drop --fs-root.", EXIT_RUNTIME);
|
|
132
|
+
}
|
|
119
133
|
return parsed;
|
|
120
134
|
}
|
|
121
135
|
function readFlagValue(argv, index, flag) {
|
|
@@ -155,6 +169,9 @@ async function runScriptFile(filepath, parsed, options) {
|
|
|
155
169
|
version: loaded.frontmatter.version
|
|
156
170
|
};
|
|
157
171
|
const runtime = createRuntime(loaded.frontmatter, meta, {
|
|
172
|
+
fs: parsed.fs
|
|
173
|
+
? { root: path.resolve(options.cwd, parsed.fsRoot ?? dirname(filepath)) }
|
|
174
|
+
: undefined,
|
|
158
175
|
modulesFor: options.modulesFor,
|
|
159
176
|
stderr: options.stderr,
|
|
160
177
|
stdout: options.stdout
|
|
@@ -348,6 +365,8 @@ function createRuntime(frontmatter, meta, options) {
|
|
|
348
365
|
registry: {
|
|
349
366
|
agent: toModuleExports(agent),
|
|
350
367
|
fail: toModuleExports(new Map([["default", makeFailModule().default]])),
|
|
368
|
+
// The one module here that is not a stub, so it is registered only when asked for.
|
|
369
|
+
...(options.fs === undefined ? {} : { fs: toModuleExports(makeFsModule(options.fs)) }),
|
|
351
370
|
git: toModuleExports(new Map([
|
|
352
371
|
["checkpoint", git.checkpoint],
|
|
353
372
|
["commit", git.commit],
|
|
@@ -413,6 +432,10 @@ function createUsage() {
|
|
|
413
432
|
"",
|
|
414
433
|
"Options:",
|
|
415
434
|
" --fix apply lint fixes before running",
|
|
435
|
+
" --fs register the fs module: a real filesystem, unlike the agent,",
|
|
436
|
+
" git, and metric stubs this runner bundles",
|
|
437
|
+
" --fs-root <path> directory --fs confines the script to (default: the script's",
|
|
438
|
+
" directory)",
|
|
416
439
|
" --snapshot <path> write the final snapshot, and best-effort snapshot on SIGINT",
|
|
417
440
|
" --restore <path> restore from a snapshot before running",
|
|
418
441
|
" --max-steps <n> cap interpreter step budget",
|
|
@@ -29,6 +29,8 @@ export { createSpawnUsageAccumulator, makeAgentModule, runWithSpawnUsageAccumula
|
|
|
29
29
|
export type { AgentModuleDefinition, AgentModuleOptions, AgentModuleParallelCall, AgentModuleRetryOptions, AgentModuleSpawnOptions, AgentSpawnEvent, SpawnAgent, SpawnAgentInput, SpawnAgentResult, SpawnUsageAccumulator, SpawnUsageTotal } from "./modules/agent.js";
|
|
30
30
|
export { makeEnvModule } from "./modules/env.js";
|
|
31
31
|
export { makeFailModule } from "./modules/fail.js";
|
|
32
|
+
export { makeFsModule } from "./modules/fs.js";
|
|
33
|
+
export type { FsImplementation, FsModule, FsModuleOptions, SandboxDirent, SandboxStats } from "./modules/fs.js";
|
|
32
34
|
export { makeGitModule } from "./modules/git.js";
|
|
33
35
|
export { makeHarnessModule } from "./modules/harness.js";
|
|
34
36
|
export { makeLogModule } from "./modules/log.js";
|
|
@@ -20,6 +20,7 @@ export { splitFrontmatter } from "./loader/frontmatter.js";
|
|
|
20
20
|
export { createSpawnUsageAccumulator, makeAgentModule, runWithSpawnUsageAccumulator } from "./modules/agent.js";
|
|
21
21
|
export { makeEnvModule } from "./modules/env.js";
|
|
22
22
|
export { makeFailModule } from "./modules/fail.js";
|
|
23
|
+
export { makeFsModule } from "./modules/fs.js";
|
|
23
24
|
export { makeGitModule } from "./modules/git.js";
|
|
24
25
|
export { makeHarnessModule } from "./modules/harness.js";
|
|
25
26
|
export { makeLogModule } from "./modules/log.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSandboxClosure, createSandboxPromise, isSandboxClosure, isSandboxPromise } from "./values.js";
|
|
2
|
+
import { observeSandboxPromise } from "./promise-tracker.js";
|
|
2
3
|
import { replaceErrorStack } from "../error/shape.js";
|
|
3
4
|
export function wrapCancelableBindings(bindings, signal) {
|
|
4
5
|
if (signal === undefined) {
|
|
@@ -32,6 +33,9 @@ function wrapCancelableValue(value, signal, seen) {
|
|
|
32
33
|
return wrapped;
|
|
33
34
|
}
|
|
34
35
|
if (isSandboxPromise(value)) {
|
|
36
|
+
// The wrapper delivers this promise's rejection to the sandbox in its place, so
|
|
37
|
+
// the sandbox never awaits the original and it would otherwise read as unhandled.
|
|
38
|
+
observeSandboxPromise(value);
|
|
35
39
|
const wrapped = createSandboxPromise(wrapCancelablePromise(value.promise, signal, seen), {
|
|
36
40
|
...(value.hostCall === undefined ? {} : { hostCall: value.hostCall }),
|
|
37
41
|
...(value.hostCallJournal === undefined ? {} : { hostCallJournal: value.hostCallJournal }),
|
|
@@ -7,6 +7,13 @@ import { digestHostCallArguments, HostCallResumabilityError } from "./host-call.
|
|
|
7
7
|
import { createSandboxClosure, createSandboxMap, createSandboxPromise, createSandboxSet, deepCopyFromSandbox, deepCopyToSandbox, isSandboxClosure, isSandboxPromise, measureSandboxData } from "./values.js";
|
|
8
8
|
import { enterRunningState } from "./running-state.js";
|
|
9
9
|
const AsyncFunction = (async () => undefined).constructor;
|
|
10
|
+
const hostErrorMetadata = {
|
|
11
|
+
code: "string",
|
|
12
|
+
dest: "string",
|
|
13
|
+
errno: "number",
|
|
14
|
+
path: "string",
|
|
15
|
+
syscall: "string"
|
|
16
|
+
};
|
|
10
17
|
const hostOperationPolicies = new WeakMap();
|
|
11
18
|
export function declareHostOperation(operation, policy) {
|
|
12
19
|
hostOperationPolicies.set(operation, policy);
|
|
@@ -190,9 +197,31 @@ function createHostErrorValue(reason, stackFrames, budget, span) {
|
|
|
190
197
|
: createSubsetErrorValue("Error", describeThrownReason(reason), stackFrames, budget, {
|
|
191
198
|
chargeBudget: false
|
|
192
199
|
});
|
|
200
|
+
if (reason instanceof Error) {
|
|
201
|
+
copyHostErrorMetadata(error, reason, budget);
|
|
202
|
+
}
|
|
193
203
|
attachErrorSpan(error, span);
|
|
194
204
|
return error;
|
|
195
205
|
}
|
|
206
|
+
function copyHostErrorMetadata(error, reason, budget) {
|
|
207
|
+
const resumeChecks = budget.suspendChecks();
|
|
208
|
+
try {
|
|
209
|
+
for (const [key, expectedType] of Object.entries(hostErrorMetadata)) {
|
|
210
|
+
const descriptor = Object.getOwnPropertyDescriptor(reason, key);
|
|
211
|
+
if (descriptor === undefined || "get" in descriptor || "set" in descriptor) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const value = descriptor.value;
|
|
215
|
+
if (typeof value !== expectedType) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
error[key] = typeof value === "string" ? budget.allocateString(value) : value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
resumeChecks();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
196
225
|
function wrapSandboxClosureForHost(closure, stackFrames, budget) {
|
|
197
226
|
return async (...args) => {
|
|
198
227
|
const leaveRunning = enterRunningState(closure);
|
|
@@ -410,7 +439,9 @@ function copyHostValueToSandbox(value, stackFrames, options, state, path) {
|
|
|
410
439
|
if ("get" in descriptor || "set" in descriptor) {
|
|
411
440
|
throw new TypeError(`Unsupported sandbox value at ${joinPath(path, key)}: accessor property`);
|
|
412
441
|
}
|
|
413
|
-
|
|
442
|
+
// Charged like any other string the copy carries in: a key is as readable to the sandbox
|
|
443
|
+
// as the value under it, so the same limit answers for both.
|
|
444
|
+
Object.defineProperty(copy, budget.allocateString(key), {
|
|
414
445
|
enumerable: true,
|
|
415
446
|
configurable: true,
|
|
416
447
|
writable: true,
|
|
@@ -10,3 +10,4 @@ export declare function getPromiseMember(target: SandboxPromise, property: strin
|
|
|
10
10
|
export declare function resolveSandboxValue(value: SandboxValue | Promise<SandboxValue> | PromiseLike<SandboxValue>, options?: {
|
|
11
11
|
budget?: Budget;
|
|
12
12
|
}): Promise<SandboxValue>;
|
|
13
|
+
export declare function consumeSettledHostCall(value: SandboxPromise): void;
|
|
@@ -148,6 +148,14 @@ function budgetSandboxValue(value, budget) {
|
|
|
148
148
|
export function resolveSandboxValue(value, options = {}) {
|
|
149
149
|
return Promise.resolve().then(() => resolveSandboxValueNow(value, options, new WeakSet()));
|
|
150
150
|
}
|
|
151
|
+
// A host call result is consumed once, when it settles. Observing the same promise
|
|
152
|
+
// again is ordinary JavaScript and must not be reported as a double consumption.
|
|
153
|
+
export function consumeSettledHostCall(value) {
|
|
154
|
+
if (value.hostCall?.lifecycle !== "settled") {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
value.hostCallJournal?.consume(value.hostCall);
|
|
158
|
+
}
|
|
151
159
|
function resolveSandboxValueNow(value, options, seenThenables) {
|
|
152
160
|
if (isPromiseLike(value)) {
|
|
153
161
|
return Promise.resolve(value).then((resolved) => resolveSandboxValueNow(resolved, options, seenThenables), (reason) => Promise.reject(budgetIfNeeded(reason, options.budget)));
|
|
@@ -155,10 +163,10 @@ function resolveSandboxValueNow(value, options, seenThenables) {
|
|
|
155
163
|
if (isSandboxPromise(value)) {
|
|
156
164
|
observeSandboxPromise(value);
|
|
157
165
|
return value.promise.then((resolved) => {
|
|
158
|
-
|
|
166
|
+
consumeSettledHostCall(value);
|
|
159
167
|
return resolveSandboxValueNow(resolved, options, seenThenables);
|
|
160
168
|
}, (reason) => {
|
|
161
|
-
|
|
169
|
+
consumeSettledHostCall(value);
|
|
162
170
|
return Promise.reject(budgetIfNeeded(reason, options.budget));
|
|
163
171
|
});
|
|
164
172
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Realpath = (path: string) => Promise<string>;
|
|
2
|
+
type Readlink = (path: string) => Promise<string>;
|
|
3
|
+
export type CanonicalPathFs = {
|
|
4
|
+
realpath: Realpath;
|
|
5
|
+
readlink: Readlink;
|
|
6
|
+
};
|
|
7
|
+
type PathIdentity = {
|
|
8
|
+
dev: number;
|
|
9
|
+
ino: number;
|
|
10
|
+
};
|
|
11
|
+
export type Stat = (path: string) => Promise<PathIdentity>;
|
|
12
|
+
export declare function resolveCanonicalPath({ realpath, readlink }: CanonicalPathFs, path: string): Promise<string>;
|
|
13
|
+
export declare function containsPath(stat: Stat, canonicalRoot: string, canonicalPath: string): Promise<boolean>;
|
|
14
|
+
export declare function isSamePath(stat: Stat, canonicalLeft: string, canonicalRight: string): Promise<boolean>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { getOwnErrorCode } from "../error-codes.js";
|
|
3
|
+
// realpath answers ENOENT for a missing segment and ENOTDIR when a parent is a
|
|
4
|
+
// file. Both mean the segment is not an existing directory, so both walk up.
|
|
5
|
+
const MISSING_PATH_CODES = ["ENOENT", "ENOTDIR"];
|
|
6
|
+
// What the filesystem answers for a path that has no link target to follow: EINVAL
|
|
7
|
+
// for a path that exists and is not a link, and the missing-path codes for one that
|
|
8
|
+
// is not there at all. Any other failure is the filesystem's own and is left to
|
|
9
|
+
// surface rather than read as "not a link", which would canonicalize a link the
|
|
10
|
+
// filesystem merely refused to read as though it named itself.
|
|
11
|
+
const NOT_A_SYMLINK_CODES = [...MISSING_PATH_CODES, "EINVAL"];
|
|
12
|
+
// A conforming realpath raises ELOOP for a cycle before the walk below can follow
|
|
13
|
+
// one, so this cap is only reached by a filesystem that reports a cycle as a
|
|
14
|
+
// missing path. Following one of those forever would hang the sandbox, so the walk
|
|
15
|
+
// gives up the way the platform does — at SYMLOOP_MAX, which is 32 on darwin and
|
|
16
|
+
// Linux alike.
|
|
17
|
+
const MAX_SYMLINK_FOLLOWS = 32;
|
|
18
|
+
// Canonicalizes as much of the path as exists and re-appends the segments that do
|
|
19
|
+
// not, so a path is checked after symlinks are followed rather than as written.
|
|
20
|
+
// The missing segments go back through resolve(), which collapses any `..` among
|
|
21
|
+
// them, so they cannot re-escape what realpath already pinned down.
|
|
22
|
+
//
|
|
23
|
+
// A dangling symlink is the case this cannot read off realpath alone: realpath
|
|
24
|
+
// refuses one with the same ENOENT it gives a path that was never there, yet the two
|
|
25
|
+
// are not the same place. node follows a dangling link and acts on its target — a
|
|
26
|
+
// write through one creates the target — so a dangling link canonicalizes to what it
|
|
27
|
+
// names, not to itself. Reading that ENOENT as "nothing here" would answer with the
|
|
28
|
+
// link's own path and call a link out of a caller's root contained.
|
|
29
|
+
export async function resolveCanonicalPath({ realpath, readlink }, path) {
|
|
30
|
+
const missingSegments = [];
|
|
31
|
+
let current = path;
|
|
32
|
+
let follows = 0;
|
|
33
|
+
while (true) {
|
|
34
|
+
try {
|
|
35
|
+
const canonicalCurrent = await realpath(current);
|
|
36
|
+
return resolve(canonicalCurrent, ...missingSegments.reverse());
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!MISSING_PATH_CODES.includes(getOwnErrorCode(error) ?? "")) {
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
const target = await readSymlinkTarget(readlink, current);
|
|
43
|
+
if (target !== undefined) {
|
|
44
|
+
if (++follows > MAX_SYMLINK_FOLLOWS) {
|
|
45
|
+
throw createLoopError(path);
|
|
46
|
+
}
|
|
47
|
+
// node resolves a relative target against the link's own directory. Any
|
|
48
|
+
// segments already collected still hang off whatever the target resolves to,
|
|
49
|
+
// so they stay on the list.
|
|
50
|
+
current = isAbsolute(target) ? resolve(target) : resolve(dirname(current), target);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const parent = dirname(current);
|
|
54
|
+
if (parent === current) {
|
|
55
|
+
return resolve(path);
|
|
56
|
+
}
|
|
57
|
+
missingSegments.push(basename(current));
|
|
58
|
+
current = parent;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// The target of a link realpath could not resolve, or undefined when the path is not
|
|
63
|
+
// a link at all and the walk should climb past it instead.
|
|
64
|
+
async function readSymlinkTarget(readlink, path) {
|
|
65
|
+
try {
|
|
66
|
+
return await readlink(path);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (NOT_A_SYMLINK_CODES.includes(getOwnErrorCode(error) ?? "")) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Shaped like the ELOOP a filesystem raises for a cycle it walked itself, so a
|
|
76
|
+
// caller that branches on the code reads the cap and the filesystem's own answer the
|
|
77
|
+
// same way.
|
|
78
|
+
function createLoopError(path) {
|
|
79
|
+
const error = new Error(`ELOOP: too many symbolic links encountered, realpath '${path}'`);
|
|
80
|
+
error.code = "ELOOP";
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
// Whether root contains path, root itself included. Both arguments must already be
|
|
84
|
+
// canonical: a `..` or a symlink left in either one would make the comparison lie.
|
|
85
|
+
//
|
|
86
|
+
// The rule: the filesystem has the deciding vote on containment, and comparing
|
|
87
|
+
// the canonical spellings is only a fast path that is trusted when it answers
|
|
88
|
+
// "contained". Two canonical paths can differ in spelling and still be the same
|
|
89
|
+
// place — on a case-insensitive filesystem (darwin's default) `/repo` and `/REPO`
|
|
90
|
+
// are one directory, and darwin's realpath echoes the spelling it was handed
|
|
91
|
+
// rather than the on-disk one, so canonicalization does not fold the case away.
|
|
92
|
+
// Comparing spellings can therefore only ever be wrong in one direction: it may
|
|
93
|
+
// answer "outside" for a path that resolves inside root, never the reverse. So
|
|
94
|
+
// "outside" is re-decided by walking the path's ancestors and asking the
|
|
95
|
+
// filesystem for each one's identity, which no spelling can lie about. Folding
|
|
96
|
+
// case here instead would be a hole: on a case-sensitive filesystem `/REPO/x` is
|
|
97
|
+
// genuinely a different file from `/repo/x`.
|
|
98
|
+
export async function containsPath(stat, canonicalRoot, canonicalPath) {
|
|
99
|
+
if (containsCanonicalSpelling(canonicalRoot, canonicalPath)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return descendsFromRoot(stat, canonicalRoot, canonicalPath);
|
|
103
|
+
}
|
|
104
|
+
// Whether two canonical paths name the same place. Two spellings can name one
|
|
105
|
+
// place on a case-insensitive filesystem, so identity decides here for the same
|
|
106
|
+
// reason it decides containment above.
|
|
107
|
+
export async function isSamePath(stat, canonicalLeft, canonicalRight) {
|
|
108
|
+
if (canonicalLeft === canonicalRight) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
const [left, right] = await Promise.all([
|
|
112
|
+
readIdentity(stat, canonicalLeft),
|
|
113
|
+
readIdentity(stat, canonicalRight)
|
|
114
|
+
]);
|
|
115
|
+
return (left !== undefined && right !== undefined && left.dev === right.dev && left.ino === right.ino);
|
|
116
|
+
}
|
|
117
|
+
function containsCanonicalSpelling(canonicalRoot, canonicalPath) {
|
|
118
|
+
const relativePath = relative(canonicalRoot, canonicalPath);
|
|
119
|
+
// An empty relative path is root itself, which the root contains.
|
|
120
|
+
return !(relativePath === ".." ||
|
|
121
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
122
|
+
isAbsolute(relativePath));
|
|
123
|
+
}
|
|
124
|
+
// Climbs the path's ancestors looking for root's own identity. A canonical path
|
|
125
|
+
// carries no symlinks and no `..`, so every ancestor it names is a real ancestor:
|
|
126
|
+
// finding root among them means the path descends from root whatever either one
|
|
127
|
+
// is spelled like.
|
|
128
|
+
async function descendsFromRoot(stat, canonicalRoot, canonicalPath) {
|
|
129
|
+
const root = await readIdentity(stat, canonicalRoot);
|
|
130
|
+
if (root === undefined) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
let current = canonicalPath;
|
|
134
|
+
while (true) {
|
|
135
|
+
const identity = await readIdentity(stat, current);
|
|
136
|
+
if (identity !== undefined && identity.dev === root.dev && identity.ino === root.ino) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
const parent = dirname(current);
|
|
140
|
+
if (parent === current) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
current = parent;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// A path that is not there has no identity to match, so the walk climbs past it
|
|
147
|
+
// rather than treating it as an answer. Any other failure is the filesystem's own
|
|
148
|
+
// and is left to surface: swallowing it would report a refusal for what node
|
|
149
|
+
// blames on something else.
|
|
150
|
+
async function readIdentity(stat, path) {
|
|
151
|
+
try {
|
|
152
|
+
const { dev, ino } = await stat(path);
|
|
153
|
+
return { dev, ino };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (MISSING_PATH_CODES.includes(getOwnErrorCode(error) ?? "")) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Volume } from "memfs";
|
|
2
|
+
import { makeFsModule, type FsImplementation } from "./fs.js";
|
|
3
|
+
export declare const STRING_ENCODINGS: readonly ["utf8", "utf-8", "utf16le", "utf-16le", "ucs2", "ucs-2", "latin1", "binary", "ascii", "base64", "base64url", "hex"];
|
|
4
|
+
type StringEncoding = (typeof STRING_ENCODINGS)[number];
|
|
5
|
+
export declare function encode(text: string, encoding: StringEncoding): string;
|
|
6
|
+
export type FsCaseDriver = Omit<ReturnType<typeof makeFsModule>, "constants" | "mkdir" | "rmdir"> & {
|
|
7
|
+
mkdir(path: string, options?: {
|
|
8
|
+
recursive?: boolean;
|
|
9
|
+
mode?: number;
|
|
10
|
+
}): Promise<string | undefined>;
|
|
11
|
+
rmdir(path: string, options?: {
|
|
12
|
+
recursive?: boolean;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
type ObservedFailure = {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly code: string;
|
|
19
|
+
readonly path?: string;
|
|
20
|
+
};
|
|
21
|
+
type Observed = {
|
|
22
|
+
readonly result: unknown;
|
|
23
|
+
} | ObservedFailure;
|
|
24
|
+
export type RecordedTruth = {
|
|
25
|
+
readonly result: unknown;
|
|
26
|
+
} | (ObservedFailure & {
|
|
27
|
+
readonly errno: number;
|
|
28
|
+
readonly syscall: string;
|
|
29
|
+
readonly dest?: string;
|
|
30
|
+
});
|
|
31
|
+
export type RecordedCase = {
|
|
32
|
+
readonly title: string;
|
|
33
|
+
readonly resolved: boolean;
|
|
34
|
+
readonly node: RecordedTruth;
|
|
35
|
+
readonly gap?: string;
|
|
36
|
+
};
|
|
37
|
+
export type RecordedPlatform = {
|
|
38
|
+
readonly nodeVersion: string;
|
|
39
|
+
readonly cases: readonly RecordedCase[];
|
|
40
|
+
};
|
|
41
|
+
export type NodeTruthFixture = {
|
|
42
|
+
readonly platforms: Readonly<Record<string, RecordedPlatform>>;
|
|
43
|
+
};
|
|
44
|
+
export declare const HANGS = "hangs";
|
|
45
|
+
export type FsConformanceCase = {
|
|
46
|
+
readonly title: string;
|
|
47
|
+
readonly setup?: (volume: Volume) => void;
|
|
48
|
+
readonly invoke: (fs: FsCaseDriver) => Promise<unknown>;
|
|
49
|
+
readonly node: RecordedTruth;
|
|
50
|
+
readonly gap?: {
|
|
51
|
+
readonly reason: string;
|
|
52
|
+
readonly memfs: Observed | typeof HANGS;
|
|
53
|
+
};
|
|
54
|
+
readonly readsAnswer?: true;
|
|
55
|
+
};
|
|
56
|
+
export declare function readSystemError(code: string): {
|
|
57
|
+
errno: number;
|
|
58
|
+
description: string;
|
|
59
|
+
};
|
|
60
|
+
export declare function systemErrorMessage(code: string, syscall: string, path?: string, dest?: string): string;
|
|
61
|
+
export declare function createNodeTruthFs(truth: RecordedTruth): FsImplementation;
|
|
62
|
+
export declare function readRecordedOutcome(operation: Promise<unknown>): Promise<RecordedTruth>;
|
|
63
|
+
export declare function readObserved(operation: Promise<unknown>): Promise<Observed>;
|
|
64
|
+
export declare function toObserved(truth: RecordedTruth): Observed;
|
|
65
|
+
export declare function toRecordedObserved(entry: RecordedCase): Observed;
|
|
66
|
+
export declare function driveCase(testCase: FsConformanceCase): {
|
|
67
|
+
fs: FsCaseDriver;
|
|
68
|
+
reference: FsCaseDriver;
|
|
69
|
+
volume: Volume;
|
|
70
|
+
};
|
|
71
|
+
export declare const CASE_ROOT = "/repo";
|
|
72
|
+
export declare const CASE_VOLUME: Readonly<Record<string, string>>;
|
|
73
|
+
type SystemErrorCase = {
|
|
74
|
+
readonly title: string;
|
|
75
|
+
readonly syscall: string;
|
|
76
|
+
readonly code: string;
|
|
77
|
+
readonly path?: string;
|
|
78
|
+
readonly dest?: string;
|
|
79
|
+
readonly setup?: (volume: Volume) => void;
|
|
80
|
+
readonly invoke: (fs: FsCaseDriver) => Promise<unknown>;
|
|
81
|
+
readonly gap?: FsConformanceCase["gap"];
|
|
82
|
+
};
|
|
83
|
+
export declare function systemErrorTruth(entry: {
|
|
84
|
+
readonly code: string;
|
|
85
|
+
readonly syscall: string;
|
|
86
|
+
readonly path?: string;
|
|
87
|
+
readonly dest?: string;
|
|
88
|
+
}): RecordedTruth;
|
|
89
|
+
export declare function toConformanceCase(entry: SystemErrorCase): FsConformanceCase;
|
|
90
|
+
export declare const SYSTEM_ERROR_CASES: readonly SystemErrorCase[];
|
|
91
|
+
export declare const MKDIR_RM_RMDIR_CASES: readonly FsConformanceCase[];
|
|
92
|
+
export declare const WRITE_CASES: readonly FsConformanceCase[];
|
|
93
|
+
export declare const stageLoop: (volume: Volume) => void;
|
|
94
|
+
export declare const LOOP_HANGS = "memfs recurses through the cycle instead of answering ELOOP";
|
|
95
|
+
export declare const SYMLINK_CASES: readonly FsConformanceCase[];
|
|
96
|
+
export declare const COPY_RENAME_CASES: readonly FsConformanceCase[];
|
|
97
|
+
export declare const READ_AND_SHAPE_CASES: readonly FsConformanceCase[];
|
|
98
|
+
export declare const FS_CONFORMANCE_CASES: readonly FsConformanceCase[];
|
|
99
|
+
export {};
|