@ai-ecoverse/slicc-agent 3.5.0 → 3.6.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/kernel/env.js +19 -2
- package/dist/kernel/env.js.map +2 -2
- package/dist/kernel/watch.js +86 -0
- package/dist/kernel/watch.js.map +2 -2
- package/dist/types/kernel/client.d.ts +9 -0
- package/dist/types/kernel/watch.d.ts +12 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ const agent = await openAgent({ models, model, storage, registry, env: kernelEnv
|
|
|
54
54
|
- string commands run as `bash -c`, each in its own process group; timeouts and aborts kill the group;
|
|
55
55
|
- output past the spill limits goes to `/tmp/slicc-agent-output-*.log`;
|
|
56
56
|
- files and directories go through the kernel's OPFS view and metadata sidecar;
|
|
57
|
-
- `watch()`
|
|
57
|
+
- `watch()` uses the kernel client's own `fs.watch` (slicc-kernel protocol 1.1), so a change made through the kernel by any process or client is reported in the same task. A watched path whose parent is renamed or replaced counts as changed, and a watched symbolic link also reports changes to its target. Writers that bypass the kernel, such as spectrum's Files panel writing OPFS directly, are caught by a rescan every 2 s (`watch: { rescanMs }`). On a kernel without `fs.watch` (`ENOSYS`), or for a target that doesn't exist yet, it polls every 100 ms instead; `watch: { mode: 'polling' }` forces that.
|
|
58
58
|
|
|
59
59
|
A conversation without a `cwd` works in `/home`.
|
|
60
60
|
|
package/dist/kernel/env.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { execute } from "./exec.js";
|
|
7
7
|
import { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from "./files.js";
|
|
8
8
|
import { dirname, normalize, resolve } from "./paths.js";
|
|
9
|
-
import { pollWatch } from "./watch.js";
|
|
9
|
+
import { nativeWatch, pollWatch, RESCAN_MS } from "./watch.js";
|
|
10
10
|
class SliccKernelEnv {
|
|
11
11
|
id;
|
|
12
12
|
cwd;
|
|
@@ -138,7 +138,24 @@ class SliccKernelEnv {
|
|
|
138
138
|
}
|
|
139
139
|
watch(targets, onChange, context) {
|
|
140
140
|
return attempt(context, this.cwd, async () => {
|
|
141
|
-
|
|
141
|
+
let absorb = () => void 0;
|
|
142
|
+
const reported = (change) => {
|
|
143
|
+
if ("paths" in change) absorb(change.paths);
|
|
144
|
+
onChange(change);
|
|
145
|
+
};
|
|
146
|
+
const native = this.#poll.mode === "polling" ? void 0 : await nativeWatch(this.#fs, this.cwd, targets, reported);
|
|
147
|
+
const intervalMs = native ? this.#poll.rescanMs ?? RESCAN_MS : this.#poll.intervalMs;
|
|
148
|
+
const polled = await pollWatch(this.#fs, this.cwd, targets, onChange, {
|
|
149
|
+
...intervalMs === void 0 ? {} : { intervalMs }
|
|
150
|
+
});
|
|
151
|
+
absorb = (paths) => polled.absorb(paths);
|
|
152
|
+
const watcher = native ? {
|
|
153
|
+
mode: "native",
|
|
154
|
+
async close(closing) {
|
|
155
|
+
await native.close(closing);
|
|
156
|
+
await polled.close(closing);
|
|
157
|
+
}
|
|
158
|
+
} : polled;
|
|
142
159
|
this.#watchers.add(watcher);
|
|
143
160
|
return {
|
|
144
161
|
mode: watcher.mode,
|
package/dist/kernel/env.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/kernel/env.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type BinaryReader,\n type DirReader,\n type ExecutionEnv,\n type ExecutionError,\n err,\n FileError,\n type FileInfo,\n type FileWatcher,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n type TextLineReader,\n type WatchChange,\n type WatchTarget,\n} from '@earendil-works/pi-durable/env';\nimport type { Activity } from './activity.ts';\nimport type { KernelClient } from './client.ts';\nimport { execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport { dirname, normalize, resolve } from './paths.ts';\nimport { type PollOptions, pollWatch } from './watch.ts';\n\nexport interface KernelEnvOptions {\n cwd: string;\n id?: string;\n watch?: PollOptions;\n activity?: Activity;\n}\n\ntype FileResult<T> = Promise<Result<T, FileError>>;\n\nexport class SliccKernelEnv implements ExecutionEnv {\n readonly id: string;\n cwd: string;\n readonly #client: KernelClient;\n readonly #watchers = new Set<FileWatcher>();\n readonly #poll: PollOptions;\n readonly #activity: Activity | undefined;\n\n constructor(client: KernelClient, options: KernelEnvOptions) {\n this.#client = client;\n this.cwd = options.cwd;\n this.id = options.id ?? 'slicc-kernel';\n this.#poll = options.watch ?? {};\n this.#activity = options.activity;\n }\n\n #path(path: string): string {\n return resolve(this.cwd, path);\n }\n\n #written(path: string): string {\n const resolved = this.#path(path);\n this.#activity?.wrote(resolved);\n return resolved;\n }\n\n get #fs() {\n return this.#client.fs;\n }\n\n async absolutePath(path: string): FileResult<string> {\n return ok(this.#path(path));\n }\n\n async joinPath(parts: string[]): FileResult<string> {\n return ok(normalize(parts.join('/')));\n }\n\n readTextFile(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readText(resolved));\n }\n\n readBinaryFile(path: string, context: Context): FileResult<Uint8Array> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readFile(resolved));\n }\n\n async openTextLineReader(path: string, context: Context): FileResult<TextLineReader> {\n const text = await this.readTextFile(path, context);\n return text.ok ? ok(new SnapshotLines(text.value)) : text;\n }\n\n async readTextLines(\n path: string,\n options: { maxLines?: number } | undefined,\n context: Context\n ): FileResult<string[]> {\n if (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]);\n const text = await this.readTextFile(path, context);\n if (!text.ok) return text;\n const reader = new SnapshotLines(text.value);\n const lines: string[] = [];\n for (\n let line = await reader.readLine();\n line.ok && line.value;\n line = await reader.readLine()\n ) {\n if (options?.maxLines !== undefined && lines.length >= options.maxLines) break;\n lines.push(line.value.text);\n }\n return ok(lines);\n }\n\n openBinaryReader(\n path: string,\n options: { noFollow?: boolean } | undefined,\n context: Context\n ): FileResult<BinaryReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {\n throw new FileError('invalid', 'Refusing to follow a symbolic link', resolved);\n }\n const stat = await this.#fs.stat(resolved);\n if (stat.isDirectory) throw new FileError('is_directory', 'Is a directory', resolved);\n if (!stat.isFile) throw new FileError('invalid', 'Not a regular file', resolved);\n return new SnapshotReader(await this.#fs.readFile(resolved), info(resolved, stat));\n });\n }\n\n writeFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, content);\n });\n }\n\n appendFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n const before = (await this.#fs.exists(resolved))\n ? await this.#fs.readFile(resolved)\n : new Uint8Array();\n const added = typeof content === 'string' ? new TextEncoder().encode(content) : content;\n const all = new Uint8Array(before.length + added.length);\n all.set(before);\n all.set(added, before.length);\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, all);\n });\n }\n\n truncateFile(path: string, size: number, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n if (!Number.isSafeInteger(size) || size < 0) {\n return Promise.resolve(\n err(new FileError('invalid', 'File size must be a non-negative integer', resolved))\n );\n }\n return attempt(context, resolved, async () => {\n const before = await this.#fs.readFile(resolved);\n const after = new Uint8Array(size);\n after.set(before.subarray(0, size));\n await this.#fs.writeFile(resolved, after);\n });\n }\n\n flushFile(path: string, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if ((await this.#fs.stat(resolved)).isDirectory)\n throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n });\n }\n\n renameFile(sourcePath: string, destinationPath: string, context: Context): FileResult<void> {\n const source = this.#written(sourcePath);\n return attempt(context, source, () => this.#fs.rename(source, this.#written(destinationPath)));\n }\n\n fileInfo(path: string, context: Context): FileResult<FileInfo> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));\n }\n\n async listDir(path: string, context: Context): FileResult<FileInfo[]> {\n const reader = await this.openDirReader(path, context);\n if (!reader.ok) return reader;\n return ok(await (reader.value as KernelDirReader).rest());\n }\n\n openDirReader(path: string, context: Context): FileResult<DirReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!(await this.#fs.stat(resolved)).isDirectory) {\n throw Object.assign(new Error('Not a directory'), { code: 'ENOTDIR' });\n }\n return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));\n });\n }\n\n watch(\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n context: Context\n ): FileResult<FileWatcher> {\n return attempt(context, this.cwd, async () => {\n const watcher = await pollWatch(this.#fs, this.cwd, targets, onChange, this.#poll);\n this.#watchers.add(watcher);\n return {\n mode: watcher.mode,\n close: async (closing: Context) => {\n this.#watchers.delete(watcher);\n await watcher.close(closing);\n },\n };\n });\n }\n\n canonicalPath(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.realpath(resolved));\n }\n\n async exists(path: string, context: Context): FileResult<boolean> {\n const found = await this.fileInfo(path, context);\n if (found.ok) return ok(true);\n return found.error.code === 'not_found' ? ok(false) : found;\n }\n\n createDir(\n path: string,\n _options: { recursive?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, () => this.#fs.mkdir(resolved));\n }\n\n remove(\n path: string,\n options: { recursive?: boolean; force?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n if (!options?.recursive) {\n const stat = await this.#fs.lstat(resolved).catch((error: unknown) => {\n if (options?.force) return undefined;\n throw error;\n });\n if (stat?.isDirectory) throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n }\n await this.#fs.rm(resolved, { force: options?.force ?? false });\n });\n }\n\n async createTempDir(prefix: string | undefined, context: Context): FileResult<string> {\n const path = `/tmp/${prefix ?? 'tmp-'}${crypto.randomUUID()}`;\n return attempt(context, path, async () => {\n await this.#fs.mkdir(path);\n return path;\n });\n }\n\n async createTempFile(\n options: { prefix?: string; suffix?: string } | undefined,\n context: Context\n ): FileResult<string> {\n const dir = await this.createTempDir('tmp-', context);\n if (!dir.ok) return dir;\n const path = `${dir.value}/${options?.prefix ?? ''}${crypto.randomUUID()}${options?.suffix ?? ''}`;\n return attempt(context, path, async () => {\n await this.#fs.writeFile(path, '');\n return path;\n });\n }\n\n exec(\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n const ended = this.#activity?.began();\n return execute(this.#client, this.cwd, command, options, context).finally(ended);\n }\n\n async cleanup(context: Context): Promise<void> {\n for (const watcher of [...this.#watchers]) await watcher.close(context);\n this.#watchers.clear();\n }\n}\n\nexport const HOME = '/home';\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> = {}\n): (target: { cwd?: string }) => SliccKernelEnv {\n return (target) => new SliccKernelEnv(client, { ...options, cwd: target.cwd ?? HOME });\n}\n"],
|
|
5
|
-
"mappings": "AACA;AAAA,EAKE;AAAA,EACA;AAAA,EAGA;AAAA,OAOK;AAGP,SAAS,eAAe;AACxB,SAAS,SAAS,MAAM,iBAAiB,eAAe,sBAAsB;AAC9E,SAAS,SAAS,WAAW,eAAe;AAC5C,
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type BinaryReader,\n type DirReader,\n type ExecutionEnv,\n type ExecutionError,\n err,\n FileError,\n type FileInfo,\n type FileWatcher,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n type TextLineReader,\n type WatchChange,\n type WatchTarget,\n} from '@earendil-works/pi-durable/env';\nimport type { Activity } from './activity.ts';\nimport type { KernelClient } from './client.ts';\nimport { execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport { dirname, normalize, resolve } from './paths.ts';\nimport { nativeWatch, type PollOptions, pollWatch, RESCAN_MS } from './watch.ts';\n\nexport interface KernelEnvOptions {\n cwd: string;\n id?: string;\n watch?: PollOptions;\n activity?: Activity;\n}\n\ntype FileResult<T> = Promise<Result<T, FileError>>;\n\nexport class SliccKernelEnv implements ExecutionEnv {\n readonly id: string;\n cwd: string;\n readonly #client: KernelClient;\n readonly #watchers = new Set<FileWatcher>();\n readonly #poll: PollOptions;\n readonly #activity: Activity | undefined;\n\n constructor(client: KernelClient, options: KernelEnvOptions) {\n this.#client = client;\n this.cwd = options.cwd;\n this.id = options.id ?? 'slicc-kernel';\n this.#poll = options.watch ?? {};\n this.#activity = options.activity;\n }\n\n #path(path: string): string {\n return resolve(this.cwd, path);\n }\n\n #written(path: string): string {\n const resolved = this.#path(path);\n this.#activity?.wrote(resolved);\n return resolved;\n }\n\n get #fs() {\n return this.#client.fs;\n }\n\n async absolutePath(path: string): FileResult<string> {\n return ok(this.#path(path));\n }\n\n async joinPath(parts: string[]): FileResult<string> {\n return ok(normalize(parts.join('/')));\n }\n\n readTextFile(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readText(resolved));\n }\n\n readBinaryFile(path: string, context: Context): FileResult<Uint8Array> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readFile(resolved));\n }\n\n async openTextLineReader(path: string, context: Context): FileResult<TextLineReader> {\n const text = await this.readTextFile(path, context);\n return text.ok ? ok(new SnapshotLines(text.value)) : text;\n }\n\n async readTextLines(\n path: string,\n options: { maxLines?: number } | undefined,\n context: Context\n ): FileResult<string[]> {\n if (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]);\n const text = await this.readTextFile(path, context);\n if (!text.ok) return text;\n const reader = new SnapshotLines(text.value);\n const lines: string[] = [];\n for (\n let line = await reader.readLine();\n line.ok && line.value;\n line = await reader.readLine()\n ) {\n if (options?.maxLines !== undefined && lines.length >= options.maxLines) break;\n lines.push(line.value.text);\n }\n return ok(lines);\n }\n\n openBinaryReader(\n path: string,\n options: { noFollow?: boolean } | undefined,\n context: Context\n ): FileResult<BinaryReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {\n throw new FileError('invalid', 'Refusing to follow a symbolic link', resolved);\n }\n const stat = await this.#fs.stat(resolved);\n if (stat.isDirectory) throw new FileError('is_directory', 'Is a directory', resolved);\n if (!stat.isFile) throw new FileError('invalid', 'Not a regular file', resolved);\n return new SnapshotReader(await this.#fs.readFile(resolved), info(resolved, stat));\n });\n }\n\n writeFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, content);\n });\n }\n\n appendFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n const before = (await this.#fs.exists(resolved))\n ? await this.#fs.readFile(resolved)\n : new Uint8Array();\n const added = typeof content === 'string' ? new TextEncoder().encode(content) : content;\n const all = new Uint8Array(before.length + added.length);\n all.set(before);\n all.set(added, before.length);\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, all);\n });\n }\n\n truncateFile(path: string, size: number, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n if (!Number.isSafeInteger(size) || size < 0) {\n return Promise.resolve(\n err(new FileError('invalid', 'File size must be a non-negative integer', resolved))\n );\n }\n return attempt(context, resolved, async () => {\n const before = await this.#fs.readFile(resolved);\n const after = new Uint8Array(size);\n after.set(before.subarray(0, size));\n await this.#fs.writeFile(resolved, after);\n });\n }\n\n flushFile(path: string, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if ((await this.#fs.stat(resolved)).isDirectory)\n throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n });\n }\n\n renameFile(sourcePath: string, destinationPath: string, context: Context): FileResult<void> {\n const source = this.#written(sourcePath);\n return attempt(context, source, () => this.#fs.rename(source, this.#written(destinationPath)));\n }\n\n fileInfo(path: string, context: Context): FileResult<FileInfo> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));\n }\n\n async listDir(path: string, context: Context): FileResult<FileInfo[]> {\n const reader = await this.openDirReader(path, context);\n if (!reader.ok) return reader;\n return ok(await (reader.value as KernelDirReader).rest());\n }\n\n openDirReader(path: string, context: Context): FileResult<DirReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!(await this.#fs.stat(resolved)).isDirectory) {\n throw Object.assign(new Error('Not a directory'), { code: 'ENOTDIR' });\n }\n return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));\n });\n }\n\n watch(\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n context: Context\n ): FileResult<FileWatcher> {\n return attempt(context, this.cwd, async () => {\n let absorb: (paths: readonly string[]) => void = () => undefined;\n const reported = (change: WatchChange) => {\n if ('paths' in change) absorb(change.paths);\n onChange(change);\n };\n const native =\n this.#poll.mode === 'polling'\n ? undefined\n : await nativeWatch(this.#fs, this.cwd, targets, reported);\n const intervalMs = native ? (this.#poll.rescanMs ?? RESCAN_MS) : this.#poll.intervalMs;\n const polled = await pollWatch(this.#fs, this.cwd, targets, onChange, {\n ...(intervalMs === undefined ? {} : { intervalMs }),\n });\n absorb = (paths) => polled.absorb(paths);\n const watcher: FileWatcher = native\n ? {\n mode: 'native',\n async close(closing: Context) {\n await native.close(closing);\n await polled.close(closing);\n },\n }\n : polled;\n this.#watchers.add(watcher);\n return {\n mode: watcher.mode,\n close: async (closing: Context) => {\n this.#watchers.delete(watcher);\n await watcher.close(closing);\n },\n };\n });\n }\n\n canonicalPath(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.realpath(resolved));\n }\n\n async exists(path: string, context: Context): FileResult<boolean> {\n const found = await this.fileInfo(path, context);\n if (found.ok) return ok(true);\n return found.error.code === 'not_found' ? ok(false) : found;\n }\n\n createDir(\n path: string,\n _options: { recursive?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, () => this.#fs.mkdir(resolved));\n }\n\n remove(\n path: string,\n options: { recursive?: boolean; force?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n if (!options?.recursive) {\n const stat = await this.#fs.lstat(resolved).catch((error: unknown) => {\n if (options?.force) return undefined;\n throw error;\n });\n if (stat?.isDirectory) throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n }\n await this.#fs.rm(resolved, { force: options?.force ?? false });\n });\n }\n\n async createTempDir(prefix: string | undefined, context: Context): FileResult<string> {\n const path = `/tmp/${prefix ?? 'tmp-'}${crypto.randomUUID()}`;\n return attempt(context, path, async () => {\n await this.#fs.mkdir(path);\n return path;\n });\n }\n\n async createTempFile(\n options: { prefix?: string; suffix?: string } | undefined,\n context: Context\n ): FileResult<string> {\n const dir = await this.createTempDir('tmp-', context);\n if (!dir.ok) return dir;\n const path = `${dir.value}/${options?.prefix ?? ''}${crypto.randomUUID()}${options?.suffix ?? ''}`;\n return attempt(context, path, async () => {\n await this.#fs.writeFile(path, '');\n return path;\n });\n }\n\n exec(\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n const ended = this.#activity?.began();\n return execute(this.#client, this.cwd, command, options, context).finally(ended);\n }\n\n async cleanup(context: Context): Promise<void> {\n for (const watcher of [...this.#watchers]) await watcher.close(context);\n this.#watchers.clear();\n }\n}\n\nexport const HOME = '/home';\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> = {}\n): (target: { cwd?: string }) => SliccKernelEnv {\n return (target) => new SliccKernelEnv(client, { ...options, cwd: target.cwd ?? HOME });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/kernel/watch.js
CHANGED
|
@@ -41,8 +41,24 @@ function differences(before, after) {
|
|
|
41
41
|
for (const path of before.keys()) if (!after.has(path)) moved(path);
|
|
42
42
|
return [...changed];
|
|
43
43
|
}
|
|
44
|
+
const RESCAN_MS = 2e3;
|
|
45
|
+
function baseOf(path, target) {
|
|
46
|
+
if (inside(path, target.path)) return target.path;
|
|
47
|
+
return inside(target.path, path) ? path : void 0;
|
|
48
|
+
}
|
|
49
|
+
async function absorbPaths(fs, current, targets, paths) {
|
|
50
|
+
for (const path of paths)
|
|
51
|
+
for (const target of targets) {
|
|
52
|
+
const base = baseOf(path, target);
|
|
53
|
+
if (base === void 0) continue;
|
|
54
|
+
for (const key of [...current.keys()]) if (inside(base, key)) current.delete(key);
|
|
55
|
+
await scan(fs, base, target, base === target.path || target.recursive === true, current);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
44
58
|
async function pollWatch(fs, cwd, targets, onChange, options = {}) {
|
|
45
59
|
let current = await snapshot(fs, cwd, targets);
|
|
60
|
+
const resolved = targets.map((target) => ({ ...target, path: resolve(cwd, target.path) }));
|
|
61
|
+
const refresh = (paths) => absorbPaths(fs, current, resolved, paths);
|
|
46
62
|
let open = true;
|
|
47
63
|
let running = Promise.resolve();
|
|
48
64
|
const tick = async () => {
|
|
@@ -56,6 +72,9 @@ async function pollWatch(fs, cwd, targets, onChange, options = {}) {
|
|
|
56
72
|
}, options.intervalMs ?? 100);
|
|
57
73
|
return {
|
|
58
74
|
mode: "polling",
|
|
75
|
+
absorb(paths) {
|
|
76
|
+
running = running.then(() => refresh(paths));
|
|
77
|
+
},
|
|
59
78
|
async close(_context) {
|
|
60
79
|
open = false;
|
|
61
80
|
clearInterval(timer);
|
|
@@ -63,8 +82,75 @@ async function pollWatch(fs, cwd, targets, onChange, options = {}) {
|
|
|
63
82
|
}
|
|
64
83
|
};
|
|
65
84
|
}
|
|
85
|
+
function hidden(base, path, target) {
|
|
86
|
+
if (!target.exclude || path === base) return false;
|
|
87
|
+
const below = path.slice(base === "/" ? 1 : base.length + 1);
|
|
88
|
+
return below.split("/").some((name) => excluded(name, target));
|
|
89
|
+
}
|
|
90
|
+
function inside(base, path) {
|
|
91
|
+
return path === base || path.startsWith(base === "/" ? "/" : `${base}/`);
|
|
92
|
+
}
|
|
93
|
+
const FALLBACK = /* @__PURE__ */ new Set(["ENOSYS", "ENOENT", "ENOTDIR"]);
|
|
94
|
+
function mapChange(targets, path) {
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const target of targets) {
|
|
97
|
+
if (inside(target.path, path)) {
|
|
98
|
+
if (!hidden(target.path, path, target)) out.push(path);
|
|
99
|
+
} else if (inside(path, target.path)) out.push(target.path);
|
|
100
|
+
else if (target.real !== target.path && inside(target.real, path)) {
|
|
101
|
+
const mapped = `${target.path}${path.slice(target.real.length)}`;
|
|
102
|
+
if (!hidden(target.path, mapped, target)) out.push(mapped);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
async function nativeWatch(fs, cwd, targets, onChange) {
|
|
108
|
+
const watch = fs.watch?.bind(fs);
|
|
109
|
+
if (!watch) return void 0;
|
|
110
|
+
const resolved = [];
|
|
111
|
+
for (const target of targets) {
|
|
112
|
+
const path = resolve(cwd, target.path);
|
|
113
|
+
if (!await fs.exists(path).catch(() => false)) return void 0;
|
|
114
|
+
resolved.push({ ...target, path, real: await fs.realpath(path).catch(() => path) });
|
|
115
|
+
}
|
|
116
|
+
let open = true;
|
|
117
|
+
const handles = [];
|
|
118
|
+
const closeAll = async () => {
|
|
119
|
+
for (const handle of handles.splice(0)) await handle.close();
|
|
120
|
+
};
|
|
121
|
+
const report = (change) => {
|
|
122
|
+
if (!open) return;
|
|
123
|
+
if (!("paths" in change)) {
|
|
124
|
+
onChange({ overflow: true });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const paths = [...new Set(change.paths.flatMap((path) => mapChange(resolved, path)))];
|
|
128
|
+
if (paths.length) onChange({ paths });
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
for (const target of resolved) {
|
|
132
|
+
const recursive = { recursive: target.recursive === true };
|
|
133
|
+
handles.push(await watch([target.path], recursive, report));
|
|
134
|
+
if (target.real !== target.path) handles.push(await watch([target.real], recursive, report));
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await closeAll();
|
|
138
|
+
if (FALLBACK.has(error.code ?? "")) return void 0;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
mode: "native",
|
|
143
|
+
async close(_context) {
|
|
144
|
+
open = false;
|
|
145
|
+
await closeAll();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
66
149
|
export {
|
|
150
|
+
RESCAN_MS,
|
|
67
151
|
differences,
|
|
152
|
+
mapChange,
|
|
153
|
+
nativeWatch,
|
|
68
154
|
pollWatch,
|
|
69
155
|
snapshot
|
|
70
156
|
};
|
package/dist/kernel/watch.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/kernel/watch.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { FileWatcher, WatchChange, WatchTarget } from '@earendil-works/pi-durable/env';\nimport type { KernelFs } from './client.ts';\nimport { dirname, resolve } from './paths.ts';\n\ntype Snapshot = Map<string, string>;\n\nfunction excluded(name: string, target: WatchTarget): boolean {\n const rule = target.exclude;\n if (!rule) return false;\n return (rule.hidden === true && name.startsWith('.')) || (rule.names?.includes(name) ?? false);\n}\n\nasync function record(fs: KernelFs, path: string, into: Snapshot): Promise<boolean> {\n const stat = await fs.stat(path).catch(() => undefined);\n if (!stat) return false;\n into.set(\n path,\n `${stat.isDirectory ? 'd' : 'f'}:${stat.ino}:${stat.size}:${new Date(stat.mtime).getTime()}`\n );\n return stat.isDirectory;\n}\n\nasync function scan(\n fs: KernelFs,\n path: string,\n target: WatchTarget,\n deep: boolean,\n into: Snapshot\n) {\n const directory = await record(fs, path, into);\n if (!directory || !deep) return;\n const names = await fs.readdir(path).catch(() => [] as string[]);\n for (const name of names) {\n if (excluded(name, target)) continue;\n const child = `${path === '/' ? '' : path}/${name}`;\n await scan(fs, child, target, target.recursive === true, into);\n }\n}\n\nexport async function snapshot(\n fs: KernelFs,\n cwd: string,\n targets: readonly WatchTarget[]\n): Promise<Snapshot> {\n const all: Snapshot = new Map();\n for (const target of targets) await scan(fs, resolve(cwd, target.path), target, true, all);\n return all;\n}\n\nexport function differences(before: Snapshot, after: Snapshot): string[] {\n const changed = new Set<string>();\n const moved = (path: string) => {\n changed.add(path);\n changed.add(dirname(path));\n };\n for (const [path, value] of after) {\n if (!before.has(path)) moved(path);\n else if (before.get(path) !== value) changed.add(path);\n }\n for (const path of before.keys()) if (!after.has(path)) moved(path);\n return [...changed];\n}\n\nexport interface PollOptions {\n intervalMs?: number;\n}\n\nexport async function pollWatch(\n fs: KernelFs,\n cwd: string,\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n options: PollOptions = {}\n): Promise<
|
|
5
|
-
"mappings": "AAGA,SAAS,SAAS,eAAe;AAIjC,SAAS,SAAS,MAAc,QAA8B;AAC5D,QAAM,OAAO,OAAO;AACpB,MAAI,CAAC,KAAM,QAAO;AAClB,SAAQ,KAAK,WAAW,QAAQ,KAAK,WAAW,GAAG,MAAO,KAAK,OAAO,SAAS,IAAI,KAAK;AAC1F;AAEA,eAAe,OAAO,IAAc,MAAc,MAAkC;AAClF,QAAM,OAAO,MAAM,GAAG,KAAK,IAAI,EAAE,MAAM,MAAM,MAAS;AACtD,MAAI,CAAC,KAAM,QAAO;AAClB,OAAK;AAAA,IACH;AAAA,IACA,GAAG,KAAK,cAAc,MAAM,GAAG,IAAI,KAAK,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,EAAE,QAAQ,CAAC;AAAA,EAC5F;AACA,SAAO,KAAK;AACd;AAEA,eAAe,KACb,IACA,MACA,QACA,MACA,MACA;AACA,QAAM,YAAY,MAAM,OAAO,IAAI,MAAM,IAAI;AAC7C,MAAI,CAAC,aAAa,CAAC,KAAM;AACzB,QAAM,QAAQ,MAAM,GAAG,QAAQ,IAAI,EAAE,MAAM,MAAM,CAAC,CAAa;AAC/D,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,MAAM,MAAM,EAAG;AAC5B,UAAM,QAAQ,GAAG,SAAS,MAAM,KAAK,IAAI,IAAI,IAAI;AACjD,UAAM,KAAK,IAAI,OAAO,QAAQ,OAAO,cAAc,MAAM,IAAI;AAAA,EAC/D;AACF;AAEA,eAAsB,SACpB,IACA,KACA,SACmB;AACnB,QAAM,MAAgB,oBAAI,IAAI;AAC9B,aAAW,UAAU,QAAS,OAAM,KAAK,IAAI,QAAQ,KAAK,OAAO,IAAI,GAAG,QAAQ,MAAM,GAAG;AACzF,SAAO;AACT;AAEO,SAAS,YAAY,QAAkB,OAA2B;AACvE,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,CAAC,SAAiB;AAC9B,YAAQ,IAAI,IAAI;AAChB,YAAQ,IAAI,QAAQ,IAAI,CAAC;AAAA,EAC3B;AACA,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO;AACjC,QAAI,CAAC,OAAO,IAAI,IAAI,EAAG,OAAM,IAAI;AAAA,aACxB,OAAO,IAAI,IAAI,MAAM,MAAO,SAAQ,IAAI,IAAI;AAAA,EACvD;AACA,aAAW,QAAQ,OAAO,KAAK,EAAG,KAAI,CAAC,MAAM,IAAI,IAAI,EAAG,OAAM,IAAI;AAClE,SAAO,CAAC,GAAG,OAAO;AACpB;
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { FileWatcher, WatchChange, WatchTarget } from '@earendil-works/pi-durable/env';\nimport type { KernelFs } from './client.ts';\nimport { dirname, resolve } from './paths.ts';\n\ntype Snapshot = Map<string, string>;\n\nfunction excluded(name: string, target: WatchTarget): boolean {\n const rule = target.exclude;\n if (!rule) return false;\n return (rule.hidden === true && name.startsWith('.')) || (rule.names?.includes(name) ?? false);\n}\n\nasync function record(fs: KernelFs, path: string, into: Snapshot): Promise<boolean> {\n const stat = await fs.stat(path).catch(() => undefined);\n if (!stat) return false;\n into.set(\n path,\n `${stat.isDirectory ? 'd' : 'f'}:${stat.ino}:${stat.size}:${new Date(stat.mtime).getTime()}`\n );\n return stat.isDirectory;\n}\n\nasync function scan(\n fs: KernelFs,\n path: string,\n target: WatchTarget,\n deep: boolean,\n into: Snapshot\n) {\n const directory = await record(fs, path, into);\n if (!directory || !deep) return;\n const names = await fs.readdir(path).catch(() => [] as string[]);\n for (const name of names) {\n if (excluded(name, target)) continue;\n const child = `${path === '/' ? '' : path}/${name}`;\n await scan(fs, child, target, target.recursive === true, into);\n }\n}\n\nexport async function snapshot(\n fs: KernelFs,\n cwd: string,\n targets: readonly WatchTarget[]\n): Promise<Snapshot> {\n const all: Snapshot = new Map();\n for (const target of targets) await scan(fs, resolve(cwd, target.path), target, true, all);\n return all;\n}\n\nexport function differences(before: Snapshot, after: Snapshot): string[] {\n const changed = new Set<string>();\n const moved = (path: string) => {\n changed.add(path);\n changed.add(dirname(path));\n };\n for (const [path, value] of after) {\n if (!before.has(path)) moved(path);\n else if (before.get(path) !== value) changed.add(path);\n }\n for (const path of before.keys()) if (!after.has(path)) moved(path);\n return [...changed];\n}\n\nexport interface PollOptions {\n intervalMs?: number;\n rescanMs?: number;\n mode?: 'native' | 'polling';\n}\n\nexport const RESCAN_MS = 2000;\n\nfunction baseOf(path: string, target: WatchTarget): string | undefined {\n if (inside(path, target.path)) return target.path;\n return inside(target.path, path) ? path : undefined;\n}\n\nasync function absorbPaths(\n fs: KernelFs,\n current: Snapshot,\n targets: readonly WatchTarget[],\n paths: readonly string[]\n): Promise<void> {\n for (const path of paths)\n for (const target of targets) {\n const base = baseOf(path, target);\n if (base === undefined) continue;\n for (const key of [...current.keys()]) if (inside(base, key)) current.delete(key);\n await scan(fs, base, target, base === target.path || target.recursive === true, current);\n }\n}\n\nexport type PolledWatcher = FileWatcher & { absorb(paths: readonly string[]): void };\n\nexport async function pollWatch(\n fs: KernelFs,\n cwd: string,\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n options: PollOptions = {}\n): Promise<PolledWatcher> {\n let current = await snapshot(fs, cwd, targets);\n const resolved = targets.map((target) => ({ ...target, path: resolve(cwd, target.path) }));\n const refresh = (paths: readonly string[]) => absorbPaths(fs, current, resolved, paths);\n let open = true;\n let running = Promise.resolve();\n const tick = async () => {\n const next = await snapshot(fs, cwd, targets);\n const paths = differences(current, next);\n current = next;\n if (open && paths.length > 0) onChange({ paths });\n };\n const timer = setInterval(() => {\n running = running.then(tick);\n }, options.intervalMs ?? 100);\n return {\n mode: 'polling',\n absorb(paths) {\n running = running.then(() => refresh(paths));\n },\n async close(_context: Context) {\n open = false;\n clearInterval(timer);\n await running;\n },\n };\n}\n\nfunction hidden(base: string, path: string, target: WatchTarget): boolean {\n if (!target.exclude || path === base) return false;\n const below = path.slice(base === '/' ? 1 : base.length + 1);\n return below.split('/').some((name) => excluded(name, target));\n}\n\nfunction inside(base: string, path: string): boolean {\n return path === base || path.startsWith(base === '/' ? '/' : `${base}/`);\n}\n\nconst FALLBACK = new Set(['ENOSYS', 'ENOENT', 'ENOTDIR']);\n\ntype Watched = WatchTarget & { real: string };\n\nexport function mapChange(targets: readonly Watched[], path: string): string[] {\n const out: string[] = [];\n for (const target of targets) {\n if (inside(target.path, path)) {\n if (!hidden(target.path, path, target)) out.push(path);\n } else if (inside(path, target.path)) out.push(target.path);\n else if (target.real !== target.path && inside(target.real, path)) {\n const mapped = `${target.path}${path.slice(target.real.length)}`;\n if (!hidden(target.path, mapped, target)) out.push(mapped);\n }\n }\n return out;\n}\n\nexport async function nativeWatch(\n fs: KernelFs,\n cwd: string,\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void\n): Promise<FileWatcher | undefined> {\n const watch = fs.watch?.bind(fs);\n if (!watch) return undefined;\n const resolved: Watched[] = [];\n for (const target of targets) {\n const path = resolve(cwd, target.path);\n if (!(await fs.exists(path).catch(() => false))) return undefined;\n resolved.push({ ...target, path, real: await fs.realpath(path).catch(() => path) });\n }\n let open = true;\n const handles: { close(): unknown }[] = [];\n const closeAll = async () => {\n for (const handle of handles.splice(0)) await handle.close();\n };\n const report = (change: { paths: string[] } | { overflow: true }) => {\n if (!open) return;\n if (!('paths' in change)) {\n onChange({ overflow: true });\n return;\n }\n const paths = [...new Set(change.paths.flatMap((path) => mapChange(resolved, path)))];\n if (paths.length) onChange({ paths });\n };\n try {\n for (const target of resolved) {\n const recursive = { recursive: target.recursive === true };\n handles.push(await watch([target.path], recursive, report));\n if (target.real !== target.path) handles.push(await watch([target.real], recursive, report));\n }\n } catch (error) {\n await closeAll();\n if (FALLBACK.has((error as { code?: string }).code ?? '')) return undefined;\n throw error;\n }\n return {\n mode: 'native',\n async close(_context: Context) {\n open = false;\n await closeAll();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -20,6 +20,15 @@ export interface KernelFs {
|
|
|
20
20
|
rename(from: string, to: string): Promise<void>;
|
|
21
21
|
realpath(path: string): Promise<string>;
|
|
22
22
|
exists(path: string): Promise<boolean>;
|
|
23
|
+
watch?(paths: readonly string[], options: {
|
|
24
|
+
recursive: boolean;
|
|
25
|
+
}, onChange: (change: {
|
|
26
|
+
paths: string[];
|
|
27
|
+
} | {
|
|
28
|
+
overflow: true;
|
|
29
|
+
}) => void): Promise<{
|
|
30
|
+
close(): unknown;
|
|
31
|
+
}>;
|
|
23
32
|
}
|
|
24
33
|
export interface KernelProcess {
|
|
25
34
|
pid: number;
|
|
@@ -5,6 +5,17 @@ export declare function snapshot(fs: KernelFs, cwd: string, targets: readonly Wa
|
|
|
5
5
|
export declare function differences(before: Snapshot, after: Snapshot): string[];
|
|
6
6
|
export interface PollOptions {
|
|
7
7
|
intervalMs?: number;
|
|
8
|
+
rescanMs?: number;
|
|
9
|
+
mode?: 'native' | 'polling';
|
|
8
10
|
}
|
|
9
|
-
export declare
|
|
11
|
+
export declare const RESCAN_MS = 2000;
|
|
12
|
+
export type PolledWatcher = FileWatcher & {
|
|
13
|
+
absorb(paths: readonly string[]): void;
|
|
14
|
+
};
|
|
15
|
+
export declare function pollWatch(fs: KernelFs, cwd: string, targets: readonly WatchTarget[], onChange: (change: WatchChange) => void, options?: PollOptions): Promise<PolledWatcher>;
|
|
16
|
+
type Watched = WatchTarget & {
|
|
17
|
+
real: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function mapChange(targets: readonly Watched[], path: string): string[];
|
|
20
|
+
export declare function nativeWatch(fs: KernelFs, cwd: string, targets: readonly WatchTarget[], onChange: (change: WatchChange) => void): Promise<FileWatcher | undefined>;
|
|
10
21
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "SLICC's agent core on pi durable: an agent worker, session storage, remoting and pi extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"release": "semantic-release"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@ai-ecoverse/slicc-shared-web": "1.
|
|
62
|
+
"@ai-ecoverse/slicc-shared-web": "1.8.0",
|
|
63
63
|
"@ai-ecoverse/slicc-spectrum": "1.13.0",
|
|
64
64
|
"@ai-ecoverse/wasm-bash": "5.3.0-7",
|
|
65
65
|
"@ai-ecoverse/wasm-coreutils": "9.7.0-3",
|