@xandreed/core 0.12.0 → 0.14.0
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 +1 -1
- package/dist/conformance.functions.d.ts +34 -0
- package/dist/conformance.functions.js +25 -0
- package/dist/domain/bash.entity.d.ts +82 -0
- package/dist/domain/bash.entity.functions.d.ts +17 -0
- package/dist/domain/bash.entity.functions.js +26 -0
- package/dist/domain/bash.entity.functions.test.d.ts +1 -0
- package/dist/domain/bash.entity.js +30 -0
- package/dist/domain/file-system.entity.d.ts +91 -0
- package/dist/domain/file-system.entity.functions.d.ts +18 -0
- package/dist/domain/file-system.entity.functions.js +30 -0
- package/dist/domain/file-system.entity.js +37 -0
- package/dist/domain/loop-event.entity.d.ts +9 -0
- package/dist/domain/media.entity.d.ts +79 -0
- package/dist/domain/media.entity.functions.d.ts +76 -0
- package/dist/domain/media.entity.functions.js +110 -0
- package/dist/domain/media.entity.js +38 -0
- package/dist/domain/media.entity.test.d.ts +1 -0
- package/dist/domain/message.entity.d.ts +157 -5
- package/dist/domain/message.entity.js +8 -4
- package/dist/domain/model-catalog.entity.d.ts +2 -0
- package/dist/domain/model-catalog.entity.js +2 -1
- package/dist/domain/token-usage.entity.d.ts +45 -1
- package/dist/domain/token-usage.entity.functions.d.ts +30 -1
- package/dist/domain/token-usage.entity.functions.js +24 -4
- package/dist/domain/token-usage.entity.js +19 -2
- package/dist/domain/token-usage.entity.test.d.ts +1 -0
- package/dist/harness/capability.entity.functions.js +2 -1
- package/dist/harness/plugin.adapter.d.ts +3 -0
- package/dist/harness/plugin.adapter.js +7 -1
- package/dist/harness/skill-files.entity.d.ts +101 -0
- package/dist/harness/skill-files.entity.functions.d.ts +58 -0
- package/dist/harness/skill-files.entity.functions.js +150 -0
- package/dist/harness/skill-files.entity.functions.test.d.ts +1 -0
- package/dist/harness/skill-files.entity.js +25 -0
- package/dist/index.d.ts +48 -10
- package/dist/index.js +67 -14
- package/dist/loop/cacheKey.d.ts +1 -1
- package/dist/loop/mapping.d.ts +13 -7
- package/dist/loop/mapping.js +30 -6
- package/dist/loop/stepContext.d.ts +8 -0
- package/dist/loop/stepContext.js +1 -1
- package/dist/loop/traceContent.d.ts +29 -0
- package/dist/loop/traceContent.js +4 -0
- package/dist/loop/traceContent.test.d.ts +1 -0
- package/dist/memory/memory-log.entity.d.ts +152 -14
- package/dist/memory/memory-log.entity.functions.d.ts +35 -5
- package/dist/memory/memory-log.entity.functions.js +59 -20
- package/dist/memory/memory-log.entity.js +19 -6
- package/dist/memory/memory-session.js +10 -6
- package/dist/memory/memory.conformance.d.ts +8 -1
- package/dist/memory/memory.conformance.js +32 -0
- package/dist/models/auth-store.conformance.d.ts +13 -0
- package/dist/models/auth-store.conformance.js +73 -0
- package/dist/models/auth-store.conformance.test.d.ts +1 -0
- package/dist/models/model-catalog.conformance.d.ts +15 -0
- package/dist/models/model-catalog.conformance.js +27 -0
- package/dist/models/model-catalog.conformance.test.d.ts +1 -0
- package/dist/models/settings-store.conformance.d.ts +26 -0
- package/dist/models/settings-store.conformance.js +80 -0
- package/dist/models/settings-store.conformance.test.d.ts +1 -0
- package/dist/ports/activity.port.d.ts +35 -0
- package/dist/ports/activity.port.js +7 -0
- package/dist/ports/auth-store.port.d.ts +7 -5
- package/dist/ports/bash.port.d.ts +22 -0
- package/dist/ports/bash.port.js +4 -0
- package/dist/ports/capability.port.d.ts +11 -1
- package/dist/ports/file-system.port.d.ts +44 -15
- package/dist/ports/file-system.port.js +1 -7
- package/dist/ports/harness.port.d.ts +1 -99
- package/dist/ports/harness.port.js +0 -24
- package/dist/ports/memory.port.d.ts +13 -1
- package/dist/ports/model-catalog.port.d.ts +4 -1
- package/dist/ports/run-context.port.d.ts +6 -1
- package/dist/ports/session-log.port.d.ts +4 -1
- package/dist/ports/sessions.port.d.ts +35 -6
- package/dist/ports/settings-store.port.d.ts +11 -7
- package/dist/ports/step-loop.port.d.ts +30 -2
- package/dist/ports/tasks.port.d.ts +5 -1
- package/dist/ports/turn-admission.port.d.ts +8 -2
- package/dist/ports/turn-events.port.d.ts +8 -1
- package/dist/ports/turn-scope.port.d.ts +19 -1
- package/dist/ports/turn.port.d.ts +70 -5
- package/dist/ports/utility-llm.port.d.ts +2 -0
- package/dist/session/activity.entity.d.ts +42 -0
- package/dist/session/activity.entity.functions.d.ts +21 -0
- package/dist/session/activity.entity.functions.js +23 -0
- package/dist/session/activity.entity.functions.test.d.ts +1 -0
- package/dist/session/activity.entity.js +15 -0
- package/dist/session/chassis.js +2 -1
- package/dist/session/session-event.entity.d.ts +68 -6
- package/dist/session/session-event.entity.functions.d.ts +34 -2
- package/dist/session/session-event.entity.functions.js +54 -16
- package/dist/session/session-event.entity.js +19 -2
- package/dist/session/session-log.conformance.d.ts +4 -3
- package/dist/session/session-log.conformance.js +16 -14
- package/dist/session/session-log.recovery.conformance.d.ts +40 -0
- package/dist/session/session-log.recovery.conformance.js +143 -0
- package/dist/session/session-log.recovery.conformance.test.d.ts +1 -0
- package/dist/session/sessions.conformance.d.ts +79 -0
- package/dist/session/sessions.conformance.js +355 -0
- package/dist/session/sessions.entity.d.ts +36 -3
- package/dist/session/sessions.entity.functions.d.ts +42 -0
- package/dist/session/sessions.entity.functions.js +26 -0
- package/dist/session/sessions.entity.js +17 -4
- package/dist/session/sessions.entity.test.d.ts +1 -0
- package/dist/session/tasks.conformance.d.ts +70 -0
- package/dist/session/tasks.conformance.js +213 -0
- package/dist/session/tasks.conformance.test.d.ts +1 -0
- package/dist/turn/model-request.entity.d.ts +4 -0
- package/dist/turn/model-request.entity.js +7 -2
- package/dist/turn/step-loop.conformance.d.ts +4 -1
- package/dist/turn/step-loop.conformance.js +73 -7
- package/dist/turn/tool-registry.conformance.d.ts +38 -0
- package/dist/turn/tool-registry.conformance.js +206 -0
- package/dist/turn/turn-bus.js +4 -3
- package/dist/turn/turn-event.entity.d.ts +134 -34
- package/dist/turn/turn-event.entity.functions.d.ts +11 -0
- package/dist/turn/turn-event.entity.functions.js +9 -1
- package/dist/turn/turn-event.entity.js +76 -3
- package/dist/turn/turn-event.entity.test.d.ts +1 -0
- package/dist/turn/turn-lifecycle.d.ts +46 -8
- package/dist/turn/turn-lifecycle.js +33 -3
- package/dist/turn/turn-lifecycle.test.d.ts +1 -0
- package/dist/turn/turn-run.d.ts +15 -3
- package/dist/turn/turn-run.js +37 -9
- package/dist/turn/turn.adapter.d.ts +6 -5
- package/dist/turn/turn.adapter.js +15 -3
- package/dist/turn/turn.conformance.d.ts +11 -2
- package/dist/turn/turn.conformance.js +63 -2
- package/dist/turn/user-message.entity.d.ts +14 -4
- package/dist/turn/user-message.entity.functions.d.ts +21 -0
- package/dist/turn/user-message.entity.functions.js +24 -0
- package/dist/turn/user-message.entity.js +6 -8
- package/dist/turn/user-message.entity.test.d.ts +1 -0
- package/dist/util/glob.d.ts +30 -0
- package/dist/util/glob.js +91 -0
- package/dist/util/glob.test.d.ts +1 -0
- package/dist/workspace/bash.conformance.d.ts +37 -0
- package/dist/workspace/bash.conformance.js +120 -0
- package/dist/workspace/bash.conformance.test.d.ts +1 -0
- package/dist/workspace/file-system.conformance.d.ts +38 -0
- package/dist/workspace/file-system.conformance.js +254 -0
- package/dist/workspace/file-system.conformance.test.d.ts +1 -0
- package/dist/workspace/file-system.memory.adapter.d.ts +23 -0
- package/dist/workspace/file-system.memory.adapter.js +132 -0
- package/package.json +6 -6
- package/dist/harness/session.entity.d.ts +0 -33
- package/dist/harness/session.entity.js +0 -25
- package/dist/harness/skill-files.d.ts +0 -17
- package/dist/harness/skill-files.js +0 -25
- package/dist/ports/conversation-store.port.d.ts +0 -127
- package/dist/ports/conversation-store.port.js +0 -36
- package/dist/ports/shell.port.d.ts +0 -32
- package/dist/ports/shell.port.js +0 -16
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { check, expect, fail } from "../conformance.functions.js";
|
|
3
|
+
const show = (value) => JSON.stringify(value), decoded = (bytes) => new TextDecoder().decode(bytes), entries = (listed) => listed.map((entry) => `${entry.name}:${entry.kind}`).join("|"), under = (root, relative) => root.endsWith("/") ? `${root}${relative}` : `${root}/${relative}`, refuses = (id, what, effect, reason, path) => Effect.result(effect).pipe(Effect.flatMap((result) => {
|
|
4
|
+
if (result._tag === "Success")
|
|
5
|
+
return fail(id, `${what} succeeded; it must fail with ${reason}`);
|
|
6
|
+
const error = result.failure;
|
|
7
|
+
return expect(id, error._tag === "FsError" && error.reason === reason && (path === void 0 || error.path === path), `${what} failed with ${show({ tag: error._tag, reason: error.reason, path: error.path })}, not ${reason}${path === void 0 ? "" : ` for ${path}`}`);
|
|
8
|
+
})), globTree = ({ fs, root, symlink }) => Effect.gen(function* () {
|
|
9
|
+
const files = [
|
|
10
|
+
".hidden/h.ts",
|
|
11
|
+
"docs/x.md",
|
|
12
|
+
"file1.txt",
|
|
13
|
+
"file2.txt",
|
|
14
|
+
"file10.txt",
|
|
15
|
+
"node_modules/m/i.ts",
|
|
16
|
+
"top.ts",
|
|
17
|
+
"src/.dot.ts",
|
|
18
|
+
"src/a.ts",
|
|
19
|
+
"src/b.ts",
|
|
20
|
+
"src/c.js",
|
|
21
|
+
"src/deep/d.ts",
|
|
22
|
+
"src/deep/deeper/e.ts"
|
|
23
|
+
];
|
|
24
|
+
yield* Effect.forEach(files, (file) => fs.write(under(root, file), file, { createParents: !0 }), { discard: !0 });
|
|
25
|
+
yield* symlink(under(root, "top.ts"), under(root, "link.ts"));
|
|
26
|
+
yield* symlink(under(root, "src"), under(root, "linked"));
|
|
27
|
+
});
|
|
28
|
+
export const fileSystemConformance = (subject) => {
|
|
29
|
+
const fsCheck = (name, id, body) => check(name, id, () => Effect.timeoutOrElse(Effect.scoped(subject.open.pipe(Effect.flatMap((fixture) => body(fixture, (relative) => under(fixture.root, relative))))), {
|
|
30
|
+
duration: "30 seconds",
|
|
31
|
+
orElse: () => fail(id, "the check did not finish in 30 seconds")
|
|
32
|
+
})), globs = (id, fixture) => (pattern, options, expected, truncated = !1) => fixture.fs.glob(pattern, { ...options, cwd: fixture.root }).pipe(Effect.flatMap((result) => expect(id, result.paths.join("|") === expected.join("|") && result.truncated === truncated, `glob ${show(pattern)} ${show(options)} gave ${show(result)}, not ${show({ paths: expected, truncated })}`)));
|
|
33
|
+
return [
|
|
34
|
+
fsCheck("text and bytes read back exactly as written, UTF-8 included, and a write replaces the whole file", "read-write", ({ fs }, at) => Effect.gen(function* () {
|
|
35
|
+
const sample = `h\xE9llo \u2713 \u65E5\u672C\u8A9E \uD83D\uDE80
|
|
36
|
+
second line
|
|
37
|
+
`;
|
|
38
|
+
yield* fs.write(at("text.txt"), sample);
|
|
39
|
+
const back = yield* fs.read(at("text.txt"));
|
|
40
|
+
yield* expect("read-write", back === sample, `read ${show(back)} after writing ${show(sample)}`);
|
|
41
|
+
yield* fs.write(at("text.txt"), "short");
|
|
42
|
+
const replaced = yield* fs.read(at("text.txt"));
|
|
43
|
+
yield* expect("read-write", replaced === "short", `a shorter second write left ${show(replaced)}`);
|
|
44
|
+
const binary = Uint8Array.from([0, 255, 128, 10, 13, 0, 7]);
|
|
45
|
+
yield* fs.write(at("data.bin"), binary);
|
|
46
|
+
const raw = yield* fs.readBytes(at("data.bin"));
|
|
47
|
+
yield* expect("read-write", raw.join() === binary.join(), `readBytes returned [${raw.join()}] after writing [${binary.join()}]`);
|
|
48
|
+
const textBytes = yield* fs.readBytes(at("text.txt"));
|
|
49
|
+
yield* expect("read-write", decoded(textBytes) === "short", `readBytes of a text file returned ${show(decoded(textBytes))}`);
|
|
50
|
+
yield* fs.mkdir(at("dir"));
|
|
51
|
+
yield* refuses("read-write", "reading a missing file", fs.read(at("missing.txt")), "NotFound", at("missing.txt"));
|
|
52
|
+
yield* refuses("read-write", "reading a directory", fs.read(at("dir")), "IsDirectory", at("dir"));
|
|
53
|
+
yield* refuses("read-write", "writing over a directory", fs.write(at("dir"), "x"), "IsDirectory", at("dir"));
|
|
54
|
+
yield* fs.write(at("plain"), "a file");
|
|
55
|
+
yield* refuses("read-write", "writing below a file", fs.write(at("plain/inner.txt"), "x"), "NotDirectory", at("plain/inner.txt"));
|
|
56
|
+
})),
|
|
57
|
+
fsCheck("without createParents a missing directory fails with NotFound; with it, write and append create every directory on the way", "create-parents", ({ fs }, at) => Effect.gen(function* () {
|
|
58
|
+
yield* refuses("create-parents", "writing into a missing directory", fs.write(at("a/b/c.txt"), "x"), "NotFound", at("a/b/c.txt"));
|
|
59
|
+
yield* refuses("create-parents", "appending into a missing directory", fs.append(at("a/b/c.txt"), "x"), "NotFound", at("a/b/c.txt"));
|
|
60
|
+
yield* fs.write(at("a/b/c.txt"), "deep", { createParents: !0 });
|
|
61
|
+
const deep = yield* fs.read(at("a/b/c.txt")), listed = yield* fs.list(at("a"));
|
|
62
|
+
yield* expect("create-parents", deep === "deep" && entries(listed) === "b:directory", `after a createParents write: ${show(deep)}, a/ lists ${entries(listed)}`);
|
|
63
|
+
yield* fs.write(at("a/b/c.txt"), "again", { createParents: !0 });
|
|
64
|
+
yield* fs.append(at("x/y/log.txt"), "1", { createParents: !0 });
|
|
65
|
+
const both = [yield* fs.read(at("a/b/c.txt")), yield* fs.read(at("x/y/log.txt"))];
|
|
66
|
+
yield* expect("create-parents", both.join() === "again,1", `createParents over existing directories, and on append, gave ${show(both)}`);
|
|
67
|
+
})),
|
|
68
|
+
fsCheck("readBytes returns the range asked for, what there is of a range past the end, and refuses a negative offset", "read-bytes", ({ fs }, at) => Effect.gen(function* () {
|
|
69
|
+
yield* fs.write(at("digits.txt"), "0123456789");
|
|
70
|
+
const ranges = [{ offset: 2, length: 3 }, { offset: 8 }, { offset: 5, length: 100 }, { offset: 20 }, { offset: 3, length: 0 }, { length: 4 }, {}], read = yield* Effect.forEach(ranges, (range) => fs.readBytes(at("digits.txt"), range).pipe(Effect.map(decoded))), expected = ["234", "89", "56789", "", "", "0123", "0123456789"];
|
|
71
|
+
yield* expect("read-bytes", read.join("|") === expected.join("|"), `ranges ${show(ranges)} read ${show(read)}, not ${show(expected)}`);
|
|
72
|
+
yield* refuses("read-bytes", "a negative offset", fs.readBytes(at("digits.txt"), { offset: -1 }), "Invalid", at("digits.txt"));
|
|
73
|
+
yield* refuses("read-bytes", "readBytes of a missing file", fs.readBytes(at("missing.bin")), "NotFound", at("missing.bin"));
|
|
74
|
+
yield* fs.mkdir(at("dir"));
|
|
75
|
+
yield* refuses("read-bytes", "readBytes of a directory", fs.readBytes(at("dir")), "IsDirectory", at("dir"));
|
|
76
|
+
})),
|
|
77
|
+
fsCheck("append creates a missing file and adds text and bytes at the end, in order", "append", ({ fs }, at) => Effect.gen(function* () {
|
|
78
|
+
yield* fs.append(at("log.txt"), `one
|
|
79
|
+
`);
|
|
80
|
+
yield* fs.append(at("log.txt"), `tw\xF6
|
|
81
|
+
`);
|
|
82
|
+
yield* fs.append(at("log.txt"), new TextEncoder().encode(`three
|
|
83
|
+
`));
|
|
84
|
+
const log = yield* fs.read(at("log.txt"));
|
|
85
|
+
yield* expect("append", log === `one
|
|
86
|
+
tw\xF6
|
|
87
|
+
three
|
|
88
|
+
`, `three appends gave ${show(log)}`);
|
|
89
|
+
yield* fs.write(at("written.txt"), "start");
|
|
90
|
+
yield* fs.append(at("written.txt"), "+end");
|
|
91
|
+
const written = yield* fs.read(at("written.txt"));
|
|
92
|
+
yield* expect("append", written === "start+end", `an append after a write gave ${show(written)}`);
|
|
93
|
+
yield* fs.mkdir(at("dir"));
|
|
94
|
+
yield* refuses("append", "appending to a directory", fs.append(at("dir"), "x"), "IsDirectory", at("dir"));
|
|
95
|
+
})),
|
|
96
|
+
fsCheck("rename moves a file, replaces an existing file at the target, moves a directory with what it holds, moves a link (not its target), and names the side a failure came from", "rename", ({ fs, symlink }, at) => Effect.gen(function* () {
|
|
97
|
+
yield* fs.write(at("from.txt"), "moved");
|
|
98
|
+
yield* fs.rename(at("from.txt"), at("to.txt"));
|
|
99
|
+
const moved = [yield* fs.exists(at("from.txt")), yield* fs.read(at("to.txt"))];
|
|
100
|
+
yield* expect("rename", show(moved) === show([!1, "moved"]), `after rename: source exists ${show(moved[0])}, target reads ${show(moved[1])}`);
|
|
101
|
+
yield* fs.write(at("new.txt"), "newer");
|
|
102
|
+
yield* fs.rename(at("new.txt"), at("to.txt"));
|
|
103
|
+
const replaced = [yield* fs.exists(at("new.txt")), yield* fs.read(at("to.txt"))];
|
|
104
|
+
yield* expect("rename", show(replaced) === show([!1, "newer"]), `renaming over an existing file: source exists ${show(replaced[0])}, target reads ${show(replaced[1])}`);
|
|
105
|
+
yield* fs.write(at("tree/inner/leaf.txt"), "leaf", { createParents: !0 });
|
|
106
|
+
yield* fs.rename(at("tree"), at("moved-tree"));
|
|
107
|
+
const tree = [yield* fs.exists(at("tree")), yield* fs.read(at("moved-tree/inner/leaf.txt"))];
|
|
108
|
+
yield* expect("rename", show(tree) === show([!1, "leaf"]), `renaming a directory: old exists ${show(tree[0])}, moved leaf reads ${show(tree[1])}`);
|
|
109
|
+
yield* fs.write(at("target.txt"), "target");
|
|
110
|
+
yield* symlink(at("target.txt"), at("link"));
|
|
111
|
+
yield* fs.rename(at("link"), at("moved-link"));
|
|
112
|
+
const link = [yield* fs.exists(at("link")), (yield* fs.stat(at("moved-link"), { followSymlinks: !1 })).kind, yield* fs.read(at("moved-link")), yield* fs.read(at("target.txt"))];
|
|
113
|
+
yield* expect("rename", show(link) === show([!1, "symlink", "target", "target"]), `renaming a link: old exists ${show(link[0])}, the new path is a ${show(link[1])} reading ${show(link[2])}, its target reads ${show(link[3])}`);
|
|
114
|
+
yield* refuses("rename", "renaming a missing file", fs.rename(at("missing.txt"), at("elsewhere.txt")), "NotFound", at("missing.txt"));
|
|
115
|
+
yield* refuses("rename", "renaming into a missing directory", fs.rename(at("to.txt"), at("missing/to.txt")), "NotFound", at("missing/to.txt"));
|
|
116
|
+
yield* refuses("rename", "renaming a file over a directory", fs.rename(at("to.txt"), at("moved-tree")), "IsDirectory", at("moved-tree"));
|
|
117
|
+
yield* refuses("rename", "renaming a directory over a file", fs.rename(at("moved-tree/inner"), at("to.txt")), "NotDirectory", at("to.txt"));
|
|
118
|
+
yield* fs.mkdir(at("other"));
|
|
119
|
+
yield* refuses("rename", "renaming a directory over one with entries", fs.rename(at("other"), at("moved-tree")), "NotEmpty", at("moved-tree"));
|
|
120
|
+
})),
|
|
121
|
+
fsCheck("stat reports kind, size and time, follows links unless told not to, and a missing path fails with NotFound", "stat", ({ fs, symlink }, at) => Effect.gen(function* () {
|
|
122
|
+
yield* fs.write(at("file.txt"), "h\xE9llo");
|
|
123
|
+
yield* fs.mkdir(at("dir"));
|
|
124
|
+
yield* symlink(at("file.txt"), at("link"));
|
|
125
|
+
const file = yield* fs.stat(at("file.txt"));
|
|
126
|
+
yield* expect("stat", file.kind === "file" && file.size === 6 && file.path === at("file.txt") && Number.isFinite(file.mtimeMs), `stat of a 6-byte file gave ${show({ ...file, mode: void 0 })}`);
|
|
127
|
+
const dir = yield* fs.stat(at("dir"));
|
|
128
|
+
yield* expect("stat", dir.kind === "directory", `stat of a directory gave kind ${dir.kind}`);
|
|
129
|
+
const followed = yield* fs.stat(at("link")), link = yield* fs.stat(at("link"), { followSymlinks: !1 });
|
|
130
|
+
yield* expect("stat", followed.kind === "file" && followed.size === 6 && link.kind === "symlink", `stat of a link to a file gave ${followed.kind} (${followed.size} bytes), and without following ${link.kind}`);
|
|
131
|
+
yield* refuses("stat", "stat of a missing path", fs.stat(at("missing")), "NotFound", at("missing"));
|
|
132
|
+
})),
|
|
133
|
+
fsCheck("exists is true for files and directories and false, never an error, for missing paths and dangling links", "exists", ({ fs, symlink }, at) => Effect.gen(function* () {
|
|
134
|
+
yield* fs.write(at("file.txt"), "x");
|
|
135
|
+
yield* fs.mkdir(at("dir"));
|
|
136
|
+
yield* symlink(at("nowhere"), at("dangling"));
|
|
137
|
+
const found = yield* Effect.forEach(["file.txt", "dir", "missing", "missing/below", "dangling"], (path) => fs.exists(at(path)));
|
|
138
|
+
yield* expect("exists", show(found) === show([!0, !0, !1, !1, !1]), `exists for file, dir, missing, missing/below, dangling link: ${show(found)}`);
|
|
139
|
+
})),
|
|
140
|
+
fsCheck("list returns entries sorted by name with their kinds, links unfollowed; a file fails with NotDirectory and a missing path with NotFound", "list", ({ fs, symlink }, at) => Effect.gen(function* () {
|
|
141
|
+
yield* Effect.forEach(["b.txt", "C.txt", "a.txt", ".hidden"], (name) => fs.write(at(`dir/${name}`), name, { createParents: !0 }), { discard: !0 });
|
|
142
|
+
yield* fs.mkdir(at("dir/a"));
|
|
143
|
+
yield* symlink(at("dir/a"), at("dir/link"));
|
|
144
|
+
const listed = yield* fs.list(at("dir")), expected = ".hidden:file|C.txt:file|a:directory|a.txt:file|b.txt:file|link:symlink";
|
|
145
|
+
yield* expect("list", entries(listed) === expected, `list gave ${entries(listed)}, not ${expected}`);
|
|
146
|
+
const empty = yield* fs.list(at("dir/a")), throughLink = yield* fs.list(at("dir/link"));
|
|
147
|
+
yield* expect("list", empty.length === 0 && throughLink.length === 0, `an empty directory listed ${show(empty)}, and through a link ${show(throughLink)}`);
|
|
148
|
+
yield* refuses("list", "listing a file", fs.list(at("dir/b.txt")), "NotDirectory", at("dir/b.txt"));
|
|
149
|
+
yield* refuses("list", "listing a missing directory", fs.list(at("missing")), "NotFound", at("missing"));
|
|
150
|
+
})),
|
|
151
|
+
fsCheck("glob matches *, ?, classes and braces within a segment, in depth-first order by name", "glob", (fixture) => Effect.gen(function* () {
|
|
152
|
+
yield* globTree(fixture);
|
|
153
|
+
const globbed = globs("glob", fixture);
|
|
154
|
+
yield* globbed("*.ts", { onlyFiles: !0 }, ["top.ts"]);
|
|
155
|
+
yield* globbed("file?.txt", {}, ["file1.txt", "file2.txt"]);
|
|
156
|
+
yield* globbed("file[0-1]*.txt", {}, ["file1.txt", "file10.txt"]);
|
|
157
|
+
yield* globbed("src/[!a]*.ts", {}, ["src/b.ts"]);
|
|
158
|
+
yield* globbed("src/{a,c}.*", {}, ["src/a.ts", "src/c.js"]);
|
|
159
|
+
yield* globbed("{top,src/{b,a}}.ts", {}, ["src/a.ts", "src/b.ts", "top.ts"]);
|
|
160
|
+
yield* globbed("./docs/*.md", {}, ["docs/x.md"]);
|
|
161
|
+
})),
|
|
162
|
+
fsCheck("glob ** crosses directories but never enters a linked directory, and exclude prunes names and paths", "glob-recursive", (fixture) => Effect.gen(function* () {
|
|
163
|
+
yield* globTree(fixture);
|
|
164
|
+
const globbed = globs("glob-recursive", fixture);
|
|
165
|
+
yield* globbed("**/*.ts", { onlyFiles: !0 }, ["node_modules/m/i.ts", "src/a.ts", "src/b.ts", "src/deep/d.ts", "src/deep/deeper/e.ts", "top.ts"]);
|
|
166
|
+
yield* globbed("**/*.ts", { onlyFiles: !0, exclude: ["node_modules", "deeper"] }, ["src/a.ts", "src/b.ts", "src/deep/d.ts", "top.ts"]);
|
|
167
|
+
yield* globbed("**", { onlyFiles: !0, exclude: ["node_modules", "*.txt", "src/deep"] }, ["docs/x.md", "src/a.ts", "src/b.ts", "src/c.js", "top.ts"]);
|
|
168
|
+
yield* globbed("linked/**", { onlyFiles: !0 }, []);
|
|
169
|
+
yield* globbed("src/deep/**", {}, ["src/deep", "src/deep/d.ts", "src/deep/deeper", "src/deep/deeper/e.ts"]);
|
|
170
|
+
})),
|
|
171
|
+
fsCheck("glob skips dot entries unless dot is set or the pattern names them with a literal dot", "glob-dot", (fixture) => Effect.gen(function* () {
|
|
172
|
+
yield* globTree(fixture);
|
|
173
|
+
const globbed = globs("glob-dot", fixture);
|
|
174
|
+
yield* globbed("**/*.ts", { onlyFiles: !0, exclude: ["node_modules"], dot: !0 }, [".hidden/h.ts", "src/.dot.ts", "src/a.ts", "src/b.ts", "src/deep/d.ts", "src/deep/deeper/e.ts", "top.ts"]);
|
|
175
|
+
yield* globbed("src/*", { onlyFiles: !0, dot: !1 }, ["src/a.ts", "src/b.ts", "src/c.js"]);
|
|
176
|
+
yield* globbed(".hidden/*.ts", {}, [".hidden/h.ts"]);
|
|
177
|
+
yield* globbed("src/.*", {}, ["src/.dot.ts"]);
|
|
178
|
+
})),
|
|
179
|
+
fsCheck("glob onlyFiles keeps regular files only; without it directories and links that match are listed", "glob-only-files", (fixture) => Effect.gen(function* () {
|
|
180
|
+
yield* globTree(fixture);
|
|
181
|
+
const globbed = globs("glob-only-files", fixture);
|
|
182
|
+
yield* globbed("src/*", {}, ["src/a.ts", "src/b.ts", "src/c.js", "src/deep"]);
|
|
183
|
+
yield* globbed("src/*", { onlyFiles: !0 }, ["src/a.ts", "src/b.ts", "src/c.js"]);
|
|
184
|
+
yield* globbed("*.ts", {}, ["link.ts", "top.ts"]);
|
|
185
|
+
yield* globbed("l*", {}, ["link.ts", "linked"]);
|
|
186
|
+
})),
|
|
187
|
+
fsCheck("glob stops at limit, keeping the first paths in walk order, and reports truncated only when more matched", "glob-limit", (fixture) => Effect.gen(function* () {
|
|
188
|
+
yield* globTree(fixture);
|
|
189
|
+
const globbed = globs("glob-limit", fixture), options = { onlyFiles: !0, exclude: ["node_modules"] };
|
|
190
|
+
yield* globbed("**/*.ts", { ...options, limit: 2 }, ["src/a.ts", "src/b.ts"], !0);
|
|
191
|
+
yield* globbed("**/*.ts", { ...options, limit: 5 }, ["src/a.ts", "src/b.ts", "src/deep/d.ts", "src/deep/deeper/e.ts", "top.ts"]);
|
|
192
|
+
yield* globbed("**/*.ts", { ...options, limit: 0 }, [], !0);
|
|
193
|
+
yield* globbed("*.none", { limit: 0 }, []);
|
|
194
|
+
})),
|
|
195
|
+
fsCheck("glob of a missing cwd fails with NotFound, and an absolute or escaping pattern with Invalid", "glob-errors", ({ fs }, at) => Effect.gen(function* () {
|
|
196
|
+
yield* refuses("glob-errors", "glob in a missing cwd", fs.glob("*", { cwd: at("missing") }), "NotFound", at("missing"));
|
|
197
|
+
yield* fs.mkdir(at("dir"));
|
|
198
|
+
yield* refuses("glob-errors", "an absolute pattern", fs.glob("/etc/*", { cwd: at("dir") }), "Invalid");
|
|
199
|
+
yield* refuses("glob-errors", "a pattern with ..", fs.glob("../*", { cwd: at("dir") }), "Invalid");
|
|
200
|
+
})),
|
|
201
|
+
fsCheck("mkdir creates one directory, or with recursive every missing one (idempotently); otherwise an existing path fails with Exists and a missing parent with NotFound", "mkdir", ({ fs }, at) => Effect.gen(function* () {
|
|
202
|
+
yield* fs.mkdir(at("one"));
|
|
203
|
+
yield* refuses("mkdir", "mkdir of an existing directory", fs.mkdir(at("one")), "Exists", at("one"));
|
|
204
|
+
yield* refuses("mkdir", "mkdir with a missing parent", fs.mkdir(at("two/three")), "NotFound", at("two/three"));
|
|
205
|
+
yield* fs.mkdir(at("two/three/four"), { recursive: !0 });
|
|
206
|
+
yield* fs.mkdir(at("two/three/four"), { recursive: !0 });
|
|
207
|
+
yield* fs.mkdir(at("one"), { recursive: !0 });
|
|
208
|
+
const kinds = yield* Effect.forEach(["one", "two", "two/three", "two/three/four"], (path) => fs.stat(at(path)).pipe(Effect.map((info) => info.kind)));
|
|
209
|
+
yield* expect("mkdir", kinds.every((kind) => kind === "directory"), `after mkdir the paths are ${show(kinds)}`);
|
|
210
|
+
yield* fs.write(at("file"), "x");
|
|
211
|
+
yield* refuses("mkdir", "a recursive mkdir over a file", fs.mkdir(at("file"), { recursive: !0 }), "Exists", at("file"));
|
|
212
|
+
})),
|
|
213
|
+
fsCheck("realPath resolves every link on the way to the canonical path, and a missing path fails with NotFound", "real-path", ({ fs, symlink }, at) => Effect.gen(function* () {
|
|
214
|
+
yield* fs.write(at("target/inner/file.txt"), "x", { createParents: !0 });
|
|
215
|
+
yield* symlink(at("target"), at("abs-link"));
|
|
216
|
+
yield* symlink("target/inner", at("rel-link"));
|
|
217
|
+
const root = yield* fs.realPath(at("")), resolved = yield* Effect.forEach(["abs-link/inner/file.txt", "rel-link/file.txt", "target"], (path) => fs.realPath(at(path))), expected = [under(root, "target/inner/file.txt"), under(root, "target/inner/file.txt"), under(root, "target")];
|
|
218
|
+
yield* expect("real-path", show(resolved) === show(expected), `realPath gave ${show(resolved)}, not ${show(expected)}`);
|
|
219
|
+
yield* refuses("real-path", "realPath of a missing path", fs.realPath(at("missing")), "NotFound", at("missing"));
|
|
220
|
+
})),
|
|
221
|
+
fsCheck("remove: a missing path is a no-op, an empty directory goes without recursive, a full one needs it, and a link goes without its target", "remove", ({ fs, symlink }, at) => Effect.gen(function* () {
|
|
222
|
+
yield* fs.remove(at("missing"));
|
|
223
|
+
yield* fs.remove(at("missing/below"), { recursive: !0 });
|
|
224
|
+
yield* fs.write(at("file.txt"), "x");
|
|
225
|
+
yield* fs.remove(at("file.txt"));
|
|
226
|
+
yield* fs.mkdir(at("empty"));
|
|
227
|
+
yield* fs.remove(at("empty"));
|
|
228
|
+
yield* fs.write(at("full/inner/leaf.txt"), "leaf", { createParents: !0 });
|
|
229
|
+
yield* refuses("remove", "removing a directory with entries without recursive", fs.remove(at("full")), "NotEmpty", at("full"));
|
|
230
|
+
const kept = yield* fs.read(at("full/inner/leaf.txt"));
|
|
231
|
+
yield* symlink(at("full"), at("link"));
|
|
232
|
+
yield* fs.remove(at("link"));
|
|
233
|
+
const afterLink = yield* fs.exists(at("full/inner/leaf.txt"));
|
|
234
|
+
yield* fs.remove(at("full"), { recursive: !0 });
|
|
235
|
+
const gone = yield* Effect.forEach(["file.txt", "empty", "full", "link"], (path) => fs.exists(at(path)));
|
|
236
|
+
yield* expect("remove", kept === "leaf" && afterLink && gone.every((exists) => !exists), `refused removal kept ${show(kept)}, removing the link kept its target ${show(afterLink)}, removed paths exist ${show(gone)}`);
|
|
237
|
+
})),
|
|
238
|
+
fsCheck("remove acts on the last name as written: a link named with a trailing slash goes without its target, links in the parent are followed, and a path ending in . or .. fails with Invalid", "remove-paths", ({ fs, symlink }, at) => Effect.gen(function* () {
|
|
239
|
+
yield* fs.write(at("dir/sub/leaf.txt"), "leaf", { createParents: !0 });
|
|
240
|
+
yield* fs.write(at("dir/other/kept.txt"), "kept", { createParents: !0 });
|
|
241
|
+
yield* symlink(at("dir"), at("link"));
|
|
242
|
+
yield* symlink(at("dir"), at("recursive-link"));
|
|
243
|
+
yield* symlink(at("dir"), at("via"));
|
|
244
|
+
yield* refuses("remove-paths", "removing a path ending in ..", fs.remove(at("dir/sub/.."), { recursive: !0 }), "Invalid", at("dir/sub/.."));
|
|
245
|
+
yield* refuses("remove-paths", "removing a path ending in .", fs.remove(at("dir/sub/."), { recursive: !0 }), "Invalid", at("dir/sub/."));
|
|
246
|
+
yield* refuses("remove-paths", "removing a link's parent through ..", fs.remove(at("link/.."), { recursive: !0 }), "Invalid", at("link/.."));
|
|
247
|
+
yield* fs.remove(at("link/"));
|
|
248
|
+
yield* fs.remove(at("recursive-link/"), { recursive: !0 });
|
|
249
|
+
yield* fs.remove(at("via/sub"), { recursive: !0 });
|
|
250
|
+
const links = yield* Effect.forEach(["link", "recursive-link"], (name) => Effect.result(fs.stat(at(name), { followSymlinks: !1 })).pipe(Effect.map((result) => result._tag === "Success" ? result.success.kind : result.failure.reason))), left = [yield* fs.exists(at("dir/sub")), yield* fs.read(at("dir/other/kept.txt")), (yield* fs.stat(at("via"), { followSymlinks: !1 })).kind];
|
|
251
|
+
yield* expect("remove-paths", show(links) === show(["NotFound", "NotFound"]) && show(left) === show([!1, "kept", "symlink"]), `after removing link/ and recursive-link/ they are ${show(links)}; after removing via/sub, dir/sub exists ${show(left[0])}, dir/other/kept.txt reads ${show(left[1])} and via is a ${show(left[2])}`);
|
|
252
|
+
}))
|
|
253
|
+
];
|
|
254
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import type { Context } from "effect";
|
|
3
|
+
import { FsError } from "../domain/file-system.entity.js";
|
|
4
|
+
import { FileSystem } from "../ports/file-system.port.js";
|
|
5
|
+
/** What a seeded path holds: text or bytes (a file), or a symbolic link to `symlink`. */
|
|
6
|
+
export type MemorySeed = string | Uint8Array | {
|
|
7
|
+
readonly symlink: string;
|
|
8
|
+
};
|
|
9
|
+
/** The in-memory backend: the port, and a way to make the symbolic links a real disk would have. */
|
|
10
|
+
export type MemoryFileSystem = Context.Service.Shape<typeof FileSystem> & {
|
|
11
|
+
/** Create a symbolic link at `path` to `target` (absolute, or relative to the link's directory), as `ln -s target path` does. */
|
|
12
|
+
readonly symlink: (target: string, path: string) => Effect.Effect<void, FsError>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A file system held in memory, with the semantics `fileSystemConformance`
|
|
16
|
+
* checks: for tests, and for hosts that give an agent a scratch workspace
|
|
17
|
+
* that never touches disk. Paths are absolute and POSIX; the tree starts
|
|
18
|
+
* with `/` and the `initial` entries (parents created). Times come from the
|
|
19
|
+
* Effect `Clock`; files report no `mode`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const makeMemoryFileSystem: (initial?: Readonly<Record<string, MemorySeed>>) => Effect.Effect<MemoryFileSystem, FsError>;
|
|
22
|
+
/** An empty in-memory `FileSystem` (see `makeMemoryFileSystem`). */
|
|
23
|
+
export declare const FileSystemMemoryLive: Layer.Layer<FileSystem>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Clock, Effect, Layer, Option, Ref, Result } from "effect";
|
|
2
|
+
import { FsError } from "../domain/file-system.entity.js";
|
|
3
|
+
import { FileSystem } from "../ports/file-system.port.js";
|
|
4
|
+
import { walkGlob } from "../domain/file-system.entity.functions.js";
|
|
5
|
+
const kinds = { File: "file", Directory: "directory", Symlink: "symlink" }, encoder = new TextEncoder, decoder = new TextDecoder, maxHops = 40, failure = (path, reason, message) => new FsError({ path, reason, message }), segmentsOf = (path) => path.split("/").filter((segment) => segment.length > 0 && segment !== "."), parentOf = (path) => {
|
|
6
|
+
const at = path.lastIndexOf("/");
|
|
7
|
+
return at <= 0 ? "/" : path.slice(0, at);
|
|
8
|
+
}, join = (dir, name) => dir === "/" ? `/${name}` : `${dir}/${name}`, endsInDots = (path) => {
|
|
9
|
+
const last = path.split("/").filter((segment) => segment.length > 0).at(-1);
|
|
10
|
+
return last === "." || last === "..";
|
|
11
|
+
}, writtenParent = (path) => parentOf(`/${segmentsOf(path).join("/")}`), bytesOf = (content) => typeof content === "string" ? encoder.encode(content) : Uint8Array.from(content), concat = (left, right) => {
|
|
12
|
+
const joined = new Uint8Array(left.length + right.length);
|
|
13
|
+
joined.set(left);
|
|
14
|
+
joined.set(right, left.length);
|
|
15
|
+
return joined;
|
|
16
|
+
}, sizeOf = (node) => node._tag === "File" ? node.bytes.length : node._tag === "Symlink" ? encoder.encode(node.target).length : 0, withNode = (tree, path, node) => new Map([...tree, [path, node]]), inside = (tree, dir) => [...tree.keys()].filter((key) => key.startsWith(dir === "/" ? "/" : `${dir}/`) && key !== dir), resolveIn = (tree, asked, followLast) => {
|
|
17
|
+
if (!asked.startsWith("/"))
|
|
18
|
+
return Result.fail(failure(asked, "Invalid", "paths must be absolute"));
|
|
19
|
+
const step = (current, rest, hops) => {
|
|
20
|
+
const [name, ...tail] = rest;
|
|
21
|
+
if (name === void 0)
|
|
22
|
+
return Result.succeed(current);
|
|
23
|
+
if (name === "..")
|
|
24
|
+
return step(parentOf(current), tail, hops);
|
|
25
|
+
const next = join(current, name), node = tree.get(next);
|
|
26
|
+
if (node === void 0)
|
|
27
|
+
return tail.length === 0 ? Result.succeed(next) : Result.fail(failure(asked, "NotFound", `no such file or directory: ${next}`));
|
|
28
|
+
if (node._tag === "Symlink" && (tail.length > 0 || followLast)) {
|
|
29
|
+
if (hops >= maxHops)
|
|
30
|
+
return Result.fail(failure(asked, "Invalid", "too many levels of symbolic links"));
|
|
31
|
+
return step("/", [...segmentsOf(node.target.startsWith("/") ? node.target : join(current, node.target)), ...tail], hops + 1);
|
|
32
|
+
}
|
|
33
|
+
if (tail.length > 0 && node._tag === "File")
|
|
34
|
+
return Result.fail(failure(asked, "NotDirectory", `not a directory: ${next}`));
|
|
35
|
+
return step(next, tail, hops);
|
|
36
|
+
};
|
|
37
|
+
return step("/", segmentsOf(asked), 0);
|
|
38
|
+
}, refused = (tree, error) => [Result.fail(error), tree];
|
|
39
|
+
export const makeMemoryFileSystem = (initial = {}) => Effect.gen(function* () {
|
|
40
|
+
const tree = yield* Ref.make(new Map([["/", { _tag: "Directory", mtimeMs: yield* Clock.currentTimeMillis }]])), change = (apply) => Clock.currentTimeMillis.pipe(Effect.flatMap((now) => Ref.modify(tree, (all) => apply(all, now))), Effect.flatMap(Effect.fromResult)), look = (read) => Ref.get(tree).pipe(Effect.flatMap((all) => Effect.fromResult(read(all)))), nodeAt = (all, asked, followLast = !0) => Result.flatMap(resolveIn(all, asked, followLast), (real) => Option.match(Option.fromNullishOr(all.get(real)), {
|
|
41
|
+
onNone: () => Result.fail(failure(asked, "NotFound", `no such file or directory: ${asked}`)),
|
|
42
|
+
onSome: (node) => Result.succeed({ real, node })
|
|
43
|
+
})), fileAt = (all, asked) => Result.flatMap(nodeAt(all, asked), ({ node }) => node._tag === "File" ? Result.succeed(node.bytes) : Result.fail(failure(asked, "IsDirectory", `is a directory: ${asked}`))), makeDirectories = (asked, retried = !1) => change((all, now) => Result.match(resolveIn(all, asked, !1), {
|
|
44
|
+
onFailure: (error) => error.reason === "NotFound" && !retried ? [Result.succeed("parent-missing"), all] : refused(all, error),
|
|
45
|
+
onSuccess: (place) => Option.match(Option.fromNullishOr(all.get(place)), {
|
|
46
|
+
onNone: () => [Result.succeed("done"), withNode(all, place, { _tag: "Directory", mtimeMs: now })],
|
|
47
|
+
onSome: (node) => node._tag === "Directory" ? [Result.succeed("done"), all] : node._tag === "File" ? refused(all, failure(asked, "Exists", `exists and is not a directory: ${asked}`)) : Result.match(nodeAt(all, place), {
|
|
48
|
+
onFailure: (error) => refused(all, failure(asked, error.reason, error.reason === "NotFound" ? `a symbolic link to a missing directory: ${asked}` : error.message)),
|
|
49
|
+
onSuccess: (target) => target.node._tag === "Directory" ? [Result.succeed("done"), all] : refused(all, failure(asked, "Exists", `exists and is not a directory: ${asked}`))
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
})).pipe(Effect.flatMap((outcome) => outcome === "done" ? Effect.void : makeDirectories(writtenParent(asked)).pipe(Effect.andThen(makeDirectories(asked, !0))))), put = (asked, content, options, append) => (options?.createParents === !0 ? makeDirectories(writtenParent(asked)) : Effect.void).pipe(Effect.andThen(change((all, now) => Result.match(resolveIn(all, asked, !0), {
|
|
53
|
+
onFailure: (error) => refused(all, error),
|
|
54
|
+
onSuccess: (real) => {
|
|
55
|
+
const existing = all.get(real);
|
|
56
|
+
if (existing?._tag === "Directory")
|
|
57
|
+
return refused(all, failure(asked, "IsDirectory", `is a directory: ${asked}`));
|
|
58
|
+
const before = append && existing?._tag === "File" ? existing.bytes : new Uint8Array;
|
|
59
|
+
return [Result.succeed(void 0), withNode(all, real, { _tag: "File", bytes: concat(before, bytesOf(content)), mtimeMs: now })];
|
|
60
|
+
}
|
|
61
|
+
})))), list = (asked) => look((all) => Result.flatMap(nodeAt(all, asked), ({ real, node }) => node._tag !== "Directory" ? Result.fail(failure(asked, "NotDirectory", `not a directory: ${asked}`)) : Result.succeed(inside(all, real).filter((key) => parentOf(key) === real).map((key) => ({ name: key.slice(key.lastIndexOf("/") + 1), kind: kinds[all.get(key)?._tag ?? "File"] })).toSorted((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)))), readBytes = (asked, range) => {
|
|
62
|
+
const offset = range?.offset ?? 0;
|
|
63
|
+
return Number.isInteger(offset) && offset >= 0 && (range?.length === void 0 || Number.isInteger(range.length) && range.length >= 0) ? look((all) => Result.map(fileAt(all, asked), (bytes) => bytes.slice(offset, range?.length === void 0 ? void 0 : offset + range.length))) : Effect.fail(failure(asked, "Invalid", `offset and length must be whole numbers of at least 0, not ${JSON.stringify(range)}`));
|
|
64
|
+
}, rename = (from, to) => change((all, now) => {
|
|
65
|
+
const source = nodeAt(all, from, !1), target = resolveIn(all, to, !1);
|
|
66
|
+
if (Result.isFailure(source))
|
|
67
|
+
return refused(all, source.failure);
|
|
68
|
+
if (Result.isFailure(target))
|
|
69
|
+
return refused(all, target.failure);
|
|
70
|
+
const { real, node } = source.success, destination = target.success;
|
|
71
|
+
if (real === "/" || destination === "/")
|
|
72
|
+
return refused(all, failure(real === "/" ? from : to, "Invalid", "the root cannot be moved or replaced"));
|
|
73
|
+
if (destination === real)
|
|
74
|
+
return [Result.succeed(void 0), all];
|
|
75
|
+
if (destination.startsWith(`${real}/`))
|
|
76
|
+
return refused(all, failure(to, "Invalid", `cannot move ${from} into itself`));
|
|
77
|
+
const existing = all.get(destination);
|
|
78
|
+
if (existing !== void 0 && node._tag !== "Directory" && existing._tag === "Directory")
|
|
79
|
+
return refused(all, failure(to, "IsDirectory", `is a directory: ${to}`));
|
|
80
|
+
if (existing !== void 0 && node._tag === "Directory" && existing._tag !== "Directory")
|
|
81
|
+
return refused(all, failure(to, "NotDirectory", `not a directory: ${to}`));
|
|
82
|
+
if (existing?._tag === "Directory" && inside(all, destination).length > 0)
|
|
83
|
+
return refused(all, failure(to, "NotEmpty", `directory not empty: ${to}`));
|
|
84
|
+
const moved = [real, ...node._tag === "Directory" ? inside(all, real) : []], kept = [...all].filter(([key]) => !moved.includes(key) && key !== destination), placed = moved.map((key) => [`${destination}${key.slice(real.length)}`, key === real ? { ...node, mtimeMs: now } : all.get(key) ?? node]);
|
|
85
|
+
return [Result.succeed(void 0), new Map([...kept, ...placed])];
|
|
86
|
+
}), stat = (asked, options) => look((all) => Result.map(nodeAt(all, asked, options?.followSymlinks !== !1), ({ node }) => ({
|
|
87
|
+
path: asked,
|
|
88
|
+
kind: kinds[node._tag],
|
|
89
|
+
size: sizeOf(node),
|
|
90
|
+
mtimeMs: node.mtimeMs,
|
|
91
|
+
mode: Option.none()
|
|
92
|
+
}))), remove = (asked, options) => endsInDots(asked) ? Effect.fail(failure(asked, "Invalid", `a path whose last segment is . or .. is never removed: ${asked}`)) : change((all) => Result.match(resolveIn(all, asked, !1), {
|
|
93
|
+
onFailure: (error) => error.reason === "NotFound" ? [Result.succeed(void 0), all] : refused(all, error),
|
|
94
|
+
onSuccess: (real) => {
|
|
95
|
+
const node = all.get(real);
|
|
96
|
+
if (node === void 0)
|
|
97
|
+
return [Result.succeed(void 0), all];
|
|
98
|
+
if (real === "/")
|
|
99
|
+
return refused(all, failure(asked, "Invalid", "the root cannot be removed"));
|
|
100
|
+
const below = node._tag === "Directory" ? inside(all, real) : [];
|
|
101
|
+
if (below.length > 0 && options?.recursive !== !0)
|
|
102
|
+
return refused(all, failure(asked, "NotEmpty", `directory not empty: ${asked}`));
|
|
103
|
+
const gone = new Set([real, ...below]);
|
|
104
|
+
return [Result.succeed(void 0), new Map([...all].filter(([key]) => !gone.has(key)))];
|
|
105
|
+
}
|
|
106
|
+
})), symlink = (target, asked) => change((all, now) => Result.match(resolveIn(all, asked, !1), {
|
|
107
|
+
onFailure: (error) => refused(all, error),
|
|
108
|
+
onSuccess: (real) => all.has(real) ? refused(all, failure(asked, "Exists", `already exists: ${asked}`)) : [Result.succeed(void 0), withNode(all, real, { _tag: "Symlink", target, mtimeMs: now })]
|
|
109
|
+
})), fs = {
|
|
110
|
+
read: (asked) => look((all) => Result.map(fileAt(all, asked), (bytes) => decoder.decode(bytes))),
|
|
111
|
+
readBytes,
|
|
112
|
+
write: (asked, content, options) => put(asked, content, options, !1),
|
|
113
|
+
append: (asked, content, options) => put(asked, content, options, !0),
|
|
114
|
+
rename,
|
|
115
|
+
stat,
|
|
116
|
+
exists: (asked) => look((all) => Result.match(nodeAt(all, asked), {
|
|
117
|
+
onFailure: (error) => error.reason === "NotFound" || error.reason === "NotDirectory" ? Result.succeed(!1) : Result.fail(error),
|
|
118
|
+
onSuccess: () => Result.succeed(!0)
|
|
119
|
+
})),
|
|
120
|
+
list,
|
|
121
|
+
glob: (pattern, options) => walkGlob(list, pattern, options),
|
|
122
|
+
mkdir: (asked, options) => options?.recursive === !0 ? makeDirectories(asked) : change((all, now) => Result.match(resolveIn(all, asked, !1), {
|
|
123
|
+
onFailure: (error) => refused(all, error),
|
|
124
|
+
onSuccess: (real) => all.has(real) ? refused(all, failure(asked, "Exists", `already exists: ${asked}`)) : [Result.succeed(void 0), withNode(all, real, { _tag: "Directory", mtimeMs: now })]
|
|
125
|
+
})),
|
|
126
|
+
realPath: (asked) => look((all) => Result.map(nodeAt(all, asked), ({ real }) => real)),
|
|
127
|
+
remove,
|
|
128
|
+
symlink
|
|
129
|
+
};
|
|
130
|
+
yield* Effect.forEach(Object.entries(initial).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0), ([path, seed]) => typeof seed === "string" || seed instanceof Uint8Array ? fs.write(path, seed, { createParents: !0 }) : makeDirectories(writtenParent(path)).pipe(Effect.andThen(symlink(seed.symlink, path))), { discard: !0 });
|
|
131
|
+
return fs;
|
|
132
|
+
}), FileSystemMemoryLive = Layer.effect(FileSystem, Effect.orDie(makeMemoryFileSystem()));
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xandreed/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=24",
|
|
6
|
+
"bun": ">=1.3.0"
|
|
7
|
+
},
|
|
4
8
|
"private": false,
|
|
5
9
|
"type": "module",
|
|
6
10
|
"description": "The agent kernel of the new line: entities, ports, the provider-agnostic agent loop, and the session chassis — pure Effect, no IO, no provider SDKs.",
|
|
@@ -12,7 +16,7 @@
|
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"dependencies": {
|
|
15
|
-
"effect": "4.0.
|
|
19
|
+
"effect": "4.0.7"
|
|
16
20
|
},
|
|
17
21
|
"types": "./dist/index.d.ts",
|
|
18
22
|
"files": [
|
|
@@ -22,9 +26,5 @@
|
|
|
22
26
|
"README.md",
|
|
23
27
|
"LICENSE"
|
|
24
28
|
],
|
|
25
|
-
"engines": {
|
|
26
|
-
"node": ">=24",
|
|
27
|
-
"bun": ">=1.3.0"
|
|
28
|
-
},
|
|
29
29
|
"license": "MIT"
|
|
30
30
|
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
export declare const SessionRecord: Schema.Struct<{
|
|
3
|
-
readonly id: Schema.brand<Schema.String, "ConversationId">;
|
|
4
|
-
readonly workspace: Schema.String;
|
|
5
|
-
readonly profile: Schema.String;
|
|
6
|
-
readonly createdAt: Schema.Number;
|
|
7
|
-
readonly parent: Schema.optional<Schema.brand<Schema.String, "ConversationId">>;
|
|
8
|
-
}>;
|
|
9
|
-
export type SessionRecord = typeof SessionRecord.Type;
|
|
10
|
-
export declare const EventBody: Schema.Struct<{
|
|
11
|
-
readonly name: Schema.NonEmptyString;
|
|
12
|
-
readonly runId: Schema.optional<Schema.String>;
|
|
13
|
-
readonly data: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
14
|
-
}>;
|
|
15
|
-
export type EventBody = typeof EventBody.Type;
|
|
16
|
-
export declare const SessionEvent: Schema.Struct<{
|
|
17
|
-
readonly version: Schema.Literal<1>;
|
|
18
|
-
readonly id: Schema.String;
|
|
19
|
-
readonly sessionId: Schema.brand<Schema.String, "ConversationId">;
|
|
20
|
-
readonly seq: Schema.Int;
|
|
21
|
-
readonly at: Schema.Number;
|
|
22
|
-
readonly name: Schema.NonEmptyString;
|
|
23
|
-
readonly runId: Schema.optional<Schema.String>;
|
|
24
|
-
readonly data: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
25
|
-
}>;
|
|
26
|
-
export type SessionEvent = typeof SessionEvent.Type;
|
|
27
|
-
export declare const MemoryEntry: Schema.Struct<{
|
|
28
|
-
readonly id: Schema.String;
|
|
29
|
-
readonly workspace: Schema.String;
|
|
30
|
-
readonly text: Schema.String;
|
|
31
|
-
readonly createdAt: Schema.Number;
|
|
32
|
-
}>;
|
|
33
|
-
export type MemoryEntry = typeof MemoryEntry.Type;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
import { ConversationId } from "../domain/message.entity.js";
|
|
3
|
-
export const SessionRecord = Schema.Struct({
|
|
4
|
-
id: ConversationId,
|
|
5
|
-
workspace: Schema.String,
|
|
6
|
-
profile: Schema.String,
|
|
7
|
-
createdAt: Schema.Number,
|
|
8
|
-
parent: Schema.optional(ConversationId)
|
|
9
|
-
}), EventBody = Schema.Struct({
|
|
10
|
-
name: Schema.NonEmptyString,
|
|
11
|
-
runId: Schema.optional(Schema.String),
|
|
12
|
-
data: Schema.Record(Schema.String, Schema.Unknown)
|
|
13
|
-
}), SessionEvent = Schema.Struct({
|
|
14
|
-
...EventBody.fields,
|
|
15
|
-
version: Schema.Literal(1),
|
|
16
|
-
id: Schema.String,
|
|
17
|
-
sessionId: ConversationId,
|
|
18
|
-
seq: Schema.Int,
|
|
19
|
-
at: Schema.Number
|
|
20
|
-
}), MemoryEntry = Schema.Struct({
|
|
21
|
-
id: Schema.String,
|
|
22
|
-
workspace: Schema.String,
|
|
23
|
-
text: Schema.String,
|
|
24
|
-
createdAt: Schema.Number
|
|
25
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { SkillDefinition } from "./capability.entity.js";
|
|
3
|
-
import { HarnessError } from "./plugin.entity.js";
|
|
4
|
-
/** One file of a skills tree, as the host read it. */
|
|
5
|
-
export interface SkillFile {
|
|
6
|
-
/** Relative path, `/`-separated: `<skill>/SKILL.md` or `<skill>/references/<id>.md`. */
|
|
7
|
-
readonly path: string;
|
|
8
|
-
readonly text: string;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Skills from markdown files. `<skill>/SKILL.md` carries flat frontmatter —
|
|
12
|
-
* `summary` (required), `id` (defaults to the directory), `version`,
|
|
13
|
-
* `tools` and `permissions` (comma-separated), `always` (`true`) — and its
|
|
14
|
-
* body is the load_skill instructions. Each `<skill>/references/<id>.md` is
|
|
15
|
-
* one reference, titled by its `title` field or first heading.
|
|
16
|
-
*/
|
|
17
|
-
export declare const skillsFromFiles: (files: ReadonlyArray<SkillFile>) => Effect.Effect<ReadonlyArray<SkillDefinition>, HarnessError>;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Effect, Option, Schema } from "effect";
|
|
2
|
-
import { parseFrontmatter } from "../spec/frontmatter.js";
|
|
3
|
-
import { SkillDefinition } from "./capability.entity.js";
|
|
4
|
-
import { HarnessError } from "./plugin.entity.js";
|
|
5
|
-
const invalid = (path, message) => new HarnessError({ code: "skills.files", message: `${path}: ${message}` }), list = (value) => (value ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0), headingOf = (body) => Option.fromNullishOr(body.split(`
|
|
6
|
-
`).find((line) => line.startsWith("# "))).pipe(Option.map((line) => line.slice(2).trim()));
|
|
7
|
-
export const skillsFromFiles = (files) => Effect.forEach(files.filter((file) => /^[^/]+\/SKILL\.md$/.test(file.path)), (file) => Effect.gen(function* () {
|
|
8
|
-
const directory = file.path.split("/")[0] ?? "", parsed = yield* Option.match(parseFrontmatter(file.text), {
|
|
9
|
-
onNone: () => Effect.fail(invalid(file.path, "missing frontmatter")),
|
|
10
|
-
onSome: Effect.succeed
|
|
11
|
-
}), references = yield* Effect.forEach(files.filter((candidate) => candidate.path.startsWith(`${directory}/references/`) && candidate.path.endsWith(".md")), (reference) => {
|
|
12
|
-
const id = reference.path.slice(`${directory}/references/`.length, -3), document = Option.getOrElse(parseFrontmatter(reference.text), () => ({ fields: {}, body: reference.text })), title = Option.getOrElse(Option.orElse(Option.fromNullishOr(document.fields.title), () => headingOf(document.body)), () => id);
|
|
13
|
-
return Effect.succeed({ id, title, text: document.body.trim() });
|
|
14
|
-
}), fields = parsed.fields;
|
|
15
|
-
return yield* Schema.decodeUnknownEffect(SkillDefinition)({
|
|
16
|
-
id: fields.id ?? directory,
|
|
17
|
-
version: fields.version ?? "1",
|
|
18
|
-
summary: fields.summary ?? "",
|
|
19
|
-
instructions: parsed.body.trim(),
|
|
20
|
-
tools: list(fields.tools),
|
|
21
|
-
always: fields.always === "true",
|
|
22
|
-
permissions: list(fields.permissions),
|
|
23
|
-
references
|
|
24
|
-
}).pipe(Effect.mapError((error) => invalid(file.path, error.message)));
|
|
25
|
-
}));
|