@ai-ecoverse/slicc-agent 6.11.2 → 6.12.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 CHANGED
@@ -152,6 +152,8 @@ The first provider is AWS Bedrock with a Bedrock API key as the bearer token; th
152
152
 
153
153
  **Settings.** The `slicc.agent.settings` service shows the models and accounts and takes `connect(providerId, secret, fields)`, `disconnect(providerId)` and `remove(providerId)`. An account's `id` is its lookup key: the pi-ai provider id for model accounts (the vault record's `provider`), and `mcp:<server>` for MCP servers (the record's `server` is the `mcp.json` key); `provider` is only the display name. Accounts whose key the agent reads from the vault carry `store: 'vault'`: spectrum (SPEC-S4) `put`s the key into the vault itself, then calls `connect(id)` with no secret, and the agent rereads `records()`. `fields` holds an account's non-secret settings (`Account.fields`, unknown names ignored), kept in `/.slicc/accounts.json`. Removing the key is spectrum's `vault.remove(name, kind)`, since the agent port can't remove records.
154
154
 
155
+ **Blocked providers.** `needs: 'cors-free-transport'` is the static fact; `blocked: 'cors-free-transport'`, on both `ProviderOption` and the account, says the current transport can't reach that provider right now: the worker's kernel transport is unavailable, or it is the page's own fetch without a linked exit (`transportReach(client)` in `src/net.ts`, from the transport's traits). The worker follows `client.onTraits`, so a provider unblocks as soon as an extension or a linked slicc gives a CORS-free route, and blocks again when it goes, with no reload. A blocked provider is still offered and can be added; a connected one keeps its key and its row, but its models are left out of `models` and `labels`, so the picker offers none until the route is back.
156
+
155
157
  **Adding providers.** The settings state's `providers` lists every provider seven can add (`ProviderOption`: `id`, `name`, `description`, `auth`, `keyUrl`, `needs`, `fields` and `vaultName`, sorted by name), and the spectrum adapter serves it as `providers()` with a `providers` event when it changes. `accounts` then lists only the added ones: Bedrock and Adobe start as added, `connect` adds a provider, and one with a vault record or stored fields counts as added too. The added list is `providers` in `/.slicc/accounts.json`. `vaultName` is the record name spectrum `put`s the key under: pi-ai's env name, or `<ID>_API_KEY` for providers that share one (Moonshot AI, OpenCode and the Qwen token plans), since a `put` under another provider's name would rebind that record; Bedrock's is `AWS_BEARER_TOKEN_BEDROCK` and Adobe's `ADOBE_IMS_TOKEN`. The agent finds a provider's record by its `provider`, never by name. Each provider has at most one secret, its key; `fields` are the non-secret inputs (`name`, `label`, `required`, `placeholder`, `help`, `customBase`, `options` and `value`). On an added account each field's `value` is its stored value, trimmed, so Settings can show it and knows a custom base URL to unregister after a reload; a field with a default carries that instead (Bedrock's `us-west-2`). Bedrock's `region` has `options`, the 21 regions where AWS supports Bedrock API keys (the bearer token seven uses; AWS's “Supported Regions … for Amazon Bedrock API keys”), labelled with AWS's names (`Europe (Frankfurt), eu-central-1`); a stored region outside the list comes first, under its own name, so a select never shows a region the account doesn't use. `disconnect(id)` keeps the row and its fields, so connecting again needs only the key, and `remove(id)` drops the row and its fields; for both, spectrum removes the vault record first. On an MCP row, `remove` disconnects it. `connect` with a secret is refused for every model account ("API keys are kept in seven's vault: update seven"); the only secret it still takes is an MCP sign-in's callback answer.
156
158
 
157
159
  ### In slicc-spectrum
@@ -42,6 +42,7 @@ class SliccKernelEnv {
42
42
  #poll;
43
43
  #activity;
44
44
  #groups;
45
+ #jobs;
45
46
  #owner;
46
47
  #exports;
47
48
  #hooks;
@@ -53,6 +54,7 @@ class SliccKernelEnv {
53
54
  this.#poll = options.watch ?? {};
54
55
  this.#activity = options.activity;
55
56
  this.#groups = options.groups;
57
+ this.#jobs = options.jobs;
56
58
  this.#owner = options.owner;
57
59
  this.#exports = options.exports;
58
60
  this.#scrub = options.scrub;
@@ -255,27 +257,27 @@ class SliccKernelEnv {
255
257
  exec(command, options, context) {
256
258
  const ended = this.#activity?.began();
257
259
  const line = typeof command === "string" ? prefixed(command, this.#exports) : command;
260
+ const jobs = this.#jobs;
261
+ const client = this.#client;
262
+ let recorded;
263
+ const tracked = jobs && typeof command === "string" ? (process) => {
264
+ recorded = jobs.started(client, command, process);
265
+ } : void 0;
258
266
  const groups = this.#groups;
259
267
  const owner = this.#owner;
260
- if (!groups || owner === void 0)
261
- return execute(
262
- this.#client,
263
- this.cwd,
264
- line,
265
- options,
266
- context,
267
- void 0,
268
- void 0,
269
- this.#hooks
270
- ).finally(ended);
271
- return this.#grouped(groups, owner, line, options, context).finally(ended);
268
+ const run = !groups || owner === void 0 ? execute(client, this.cwd, line, options, context, tracked, void 0, this.#hooks) : this.#grouped(groups, owner, line, options, context, tracked);
269
+ return run.then(async (result) => {
270
+ await recorded;
271
+ return result;
272
+ }).finally(ended);
272
273
  }
273
- async #grouped(groups, owner, line, options, context) {
274
+ async #grouped(groups, owner, line, options, context, tracked) {
274
275
  for (let wait = groups.pending(owner); wait; wait = groups.pending(owner)) await wait;
275
276
  const pgid = groups.shared(owner);
276
277
  const release = pgid === void 0 && groups.joins() ? groups.reserve(owner) : () => void 0;
277
278
  const spawned = (process) => {
278
279
  groups.track(owner, process);
280
+ tracked?.(process);
279
281
  release();
280
282
  };
281
283
  const join = groups.joins() ? {
@@ -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 { mimeOf } from '../images/index.ts';\nimport type { Scrub } from '../secrets/scrub.ts';\nimport type { Activity } from './activity.ts';\nimport type { KernelClient, KernelProcess } from './client.ts';\nimport { type ExecHooks, execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport type { ProcessGroups } from './groups.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 groups?: ProcessGroups;\n owner?: number;\n exports?: Readonly<Record<string, string>>;\n scrub?: Scrub;\n spawn?: (argv: readonly string[]) => readonly string[];\n}\n\nexport const STREAMED_READ = 8 * 1024 * 1024;\nexport const READ_CHUNK = 1024 * 1024;\n\nexport async function scrubbedFile(scrub: Scrub, bytes: Uint8Array): Promise<Uint8Array> {\n if (mimeOf(bytes)) return bytes;\n if (bytes.length <= STREAMED_READ) return (await scrub.texts([bytes]))[0] as Uint8Array;\n const stream = scrub.stream();\n const parts: Uint8Array[] = [];\n for (let at = 0; at < bytes.length; at += READ_CHUNK)\n parts.push(await stream.push(bytes.subarray(at, at + READ_CHUNK)));\n parts.push(await stream.end());\n const all = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));\n let offset = 0;\n for (const part of parts) {\n all.set(part, offset);\n offset += part.length;\n }\n return all;\n}\n\nexport function quote(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`;\n}\n\nexport function prefixed(\n command: string,\n exports: Readonly<Record<string, string>> | undefined\n): string {\n const names = Object.keys(exports ?? {}).filter((name) => /^[A-Z_][A-Z0-9_]*$/.test(name));\n if (!names.length) return command;\n return `export ${names.map((name) => `${name}=${quote((exports as Record<string, string>)[name] as string)}`).join(' ')}; ${command}`;\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 readonly #groups: ProcessGroups | undefined;\n readonly #owner: number | undefined;\n readonly #exports: Readonly<Record<string, string>> | undefined;\n readonly #hooks: ExecHooks;\n readonly #scrub: Scrub | 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 this.#groups = options.groups;\n this.#owner = options.owner;\n this.#exports = options.exports;\n this.#scrub = options.scrub;\n const scrub = options.scrub;\n this.#hooks = {\n ...(scrub ? { scrub: () => scrub.stream() } : {}),\n ...(options.spawn ? { spawn: options.spawn } : {}),\n };\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 const bytes = await this.#fs.readFile(resolved);\n if (!this.#scrub) return new SnapshotReader(bytes, info(resolved, stat));\n const shown = await scrubbedFile(this.#scrub, bytes);\n return new SnapshotReader(shown, { ...info(resolved, stat), size: shown.length });\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 const line = typeof command === 'string' ? prefixed(command, this.#exports) : command;\n const groups = this.#groups;\n const owner = this.#owner;\n if (!groups || owner === undefined)\n return execute(\n this.#client,\n this.cwd,\n line,\n options,\n context,\n undefined,\n undefined,\n this.#hooks\n ).finally(ended);\n return this.#grouped(groups, owner, line, options, context).finally(ended);\n }\n\n async #grouped(\n groups: ProcessGroups,\n owner: number,\n line: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n for (let wait = groups.pending(owner); wait; wait = groups.pending(owner)) await wait;\n const pgid = groups.shared(owner);\n const release = pgid === undefined && groups.joins() ? groups.reserve(owner) : () => undefined;\n const spawned = (process: KernelProcess) => {\n groups.track(owner, process);\n release();\n };\n const join = groups.joins()\n ? {\n ...(pgid === undefined ? {} : { pgid }),\n refused: (code: string) => groups.refused(owner, code),\n }\n : undefined;\n return execute(\n this.#client,\n this.cwd,\n line,\n options,\n context,\n spawned,\n join,\n this.#hooks\n ).finally(release);\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 type EnvTargetLike = { cwd?: string; conversationId?: number; read?: unknown };\n\nexport type EnvRoute = { client: KernelClient; home: string; cwd?: string };\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> & {\n identify?: (target: EnvTargetLike, context: Context) => Promise<Record<string, string>>;\n route?: (target: EnvTargetLike) => Promise<EnvRoute | undefined>;\n } = {}\n): (target: EnvTargetLike, context?: Context) => SliccKernelEnv | Promise<SliccKernelEnv> {\n const { identify, route, ...rest } = options;\n const make = (target: EnvTargetLike, exports?: Record<string, string>, routed?: EnvRoute) =>\n new SliccKernelEnv(routed?.client ?? client, {\n ...rest,\n cwd: routed?.cwd ?? target.cwd ?? routed?.home ?? HOME,\n ...(target.conversationId === undefined ? {} : { owner: target.conversationId }),\n ...(exports ? { exports } : {}),\n });\n if (route)\n return async (target, context) => {\n const routed = await route(target);\n const exports = identify && context ? await identify(target, context) : undefined;\n return make(target, exports, routed);\n };\n return (target, context) =>\n identify && context\n ? identify(target, context).then((exports) => make(target, exports))\n : make(target);\n}\n"],
5
- "mappings": "AACA;AAAA,EAKE;AAAA,EACA;AAAA,EAGA;AAAA,OAOK;AACP,SAAS,cAAc;AAIvB,SAAyB,eAAe;AACxC,SAAS,SAAS,MAAM,iBAAiB,eAAe,sBAAsB;AAE9E,SAAS,SAAS,WAAW,eAAe;AAC5C,SAAS,aAA+B,WAAW,iBAAiB;AAc7D,MAAM,gBAAgB,IAAI,OAAO;AACjC,MAAM,aAAa,OAAO;AAEjC,eAAsB,aAAa,OAAc,OAAwC;AACvF,MAAI,OAAO,KAAK,EAAG,QAAO;AAC1B,MAAI,MAAM,UAAU,cAAe,SAAQ,MAAM,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;AACxE,QAAM,SAAS,MAAM,OAAO;AAC5B,QAAM,QAAsB,CAAC;AAC7B,WAAS,KAAK,GAAG,KAAK,MAAM,QAAQ,MAAM;AACxC,UAAM,KAAK,MAAM,OAAO,KAAK,MAAM,SAAS,IAAI,KAAK,UAAU,CAAC,CAAC;AACnE,QAAM,KAAK,MAAM,OAAO,IAAI,CAAC;AAC7B,QAAM,MAAM,IAAI,WAAW,MAAM,OAAO,CAAC,KAAK,SAAS,MAAM,KAAK,QAAQ,CAAC,CAAC;AAC5E,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACxB,QAAI,IAAI,MAAM,MAAM;AACpB,cAAU,KAAK;AAAA,EACjB;AACA,SAAO;AACT;AAEO,SAAS,MAAM,OAAuB;AAC3C,SAAO,IAAI,MAAM,QAAQ,MAAM,OAAO,CAAC;AACzC;AAEO,SAAS,SACd,SACA,SACQ;AACR,QAAM,QAAQ,OAAO,KAAK,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC,SAAS,qBAAqB,KAAK,IAAI,CAAC;AACzF,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,SAAO,UAAU,MAAM,IAAI,CAAC,SAAS,GAAG,IAAI,IAAI,MAAO,QAAmC,IAAI,CAAW,CAAC,EAAE,EAAE,KAAK,GAAG,CAAC,KAAK,OAAO;AACrI;AAIO,MAAM,eAAuC;AAAA,EACzC;AAAA,EACT;AAAA,EACS;AAAA,EACA,YAAY,oBAAI,IAAiB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAsB,SAA2B;AAC3D,SAAK,UAAU;AACf,SAAK,MAAM,QAAQ;AACnB,SAAK,KAAK,QAAQ,MAAM;AACxB,SAAK,QAAQ,QAAQ,SAAS,CAAC;AAC/B,SAAK,YAAY,QAAQ;AACzB,SAAK,UAAU,QAAQ;AACvB,SAAK,SAAS,QAAQ;AACtB,SAAK,WAAW,QAAQ;AACxB,SAAK,SAAS,QAAQ;AACtB,UAAM,QAAQ,QAAQ;AACtB,SAAK,SAAS;AAAA,MACZ,GAAI,QAAQ,EAAE,OAAO,MAAM,MAAM,OAAO,EAAE,IAAI,CAAC;AAAA,MAC/C,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,MAAM,MAAsB;AAC1B,WAAO,QAAQ,KAAK,KAAK,IAAI;AAAA,EAC/B;AAAA,EAEA,SAAS,MAAsB;AAC7B,UAAM,WAAW,KAAK,MAAM,IAAI;AAChC,SAAK,WAAW,MAAM,QAAQ;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,MAAM;AACR,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,MAAM,aAAa,MAAkC;AACnD,WAAO,GAAG,KAAK,MAAM,IAAI,CAAC;AAAA,EAC5B;AAAA,EAEA,MAAM,SAAS,OAAqC;AAClD,WAAO,GAAG,UAAU,MAAM,KAAK,GAAG,CAAC,CAAC;AAAA,EACtC;AAAA,EAEA,aAAa,MAAc,SAAsC;AAC/D,UAAM,WAAW,KAAK,MAAM,IAAI;AAChC,WAAO,QAAQ,SAAS,UAAU,MAAM,KAAK,IAAI,SAAS,QAAQ,CAAC;AAAA,EACrE;AAAA,EAEA,eAAe,MAAc,SAA0C;AACrE,UAAM,WAAW,KAAK,MAAM,IAAI;AAChC,WAAO,QAAQ,SAAS,UAAU,MAAM,KAAK,IAAI,SAAS,QAAQ,CAAC;AAAA,EACrE;AAAA,EAEA,MAAM,mBAAmB,MAAc,SAA8C;AACnF,UAAM,OAAO,MAAM,KAAK,aAAa,MAAM,OAAO;AAClD,WAAO,KAAK,KAAK,GAAG,IAAI,cAAc,KAAK,KAAK,CAAC,IAAI;AAAA,EACvD;AAAA,EAEA,MAAM,cACJ,MACA,SACA,SACsB;AACtB,QAAI,SAAS,aAAa,UAAa,QAAQ,YAAY,EAAG,QAAO,GAAG,CAAC,CAAC;AAC1E,UAAM,OAAO,MAAM,KAAK,aAAa,MAAM,OAAO;AAClD,QAAI,CAAC,KAAK,GAAI,QAAO;AACrB,UAAM,SAAS,IAAI,cAAc,KAAK,KAAK;AAC3C,UAAM,QAAkB,CAAC;AACzB,aACM,OAAO,MAAM,OAAO,SAAS,GACjC,KAAK,MAAM,KAAK,OAChB,OAAO,MAAM,OAAO,SAAS,GAC7B;AACA,UAAI,SAAS,aAAa,UAAa,MAAM,UAAU,QAAQ,SAAU;AACzE,YAAM,KAAK,KAAK,MAAM,IAAI;AAAA,IAC5B;AACA,WAAO,GAAG,KAAK;AAAA,EACjB;AAAA,EAEA,iBACE,MACA,SACA,SAC0B;AAC1B,UAAM,WAAW,KAAK,MAAM,IAAI;AAChC,WAAO,QAAQ,SAAS,UAAU,YAAY;AAC5C,UAAI,SAAS,aAAa,MAAM,KAAK,IAAI,MAAM,QAAQ,GAAG,gBAAgB;AACxE,cAAM,IAAI,UAAU,WAAW,sCAAsC,QAAQ;AAAA,MAC/E;AACA,YAAM,OAAO,MAAM,KAAK,IAAI,KAAK,QAAQ;AACzC,UAAI,KAAK,YAAa,OAAM,IAAI,UAAU,gBAAgB,kBAAkB,QAAQ;AACpF,UAAI,CAAC,KAAK,OAAQ,OAAM,IAAI,UAAU,WAAW,sBAAsB,QAAQ;AAC/E,YAAM,QAAQ,MAAM,KAAK,IAAI,SAAS,QAAQ;AAC9C,UAAI,CAAC,KAAK,OAAQ,QAAO,IAAI,eAAe,OAAO,KAAK,UAAU,IAAI,CAAC;AACvE,YAAM,QAAQ,MAAM,aAAa,KAAK,QAAQ,KAAK;AACnD,aAAO,IAAI,eAAe,OAAO,EAAE,GAAG,KAAK,UAAU,IAAI,GAAG,MAAM,MAAM,OAAO,CAAC;AAAA,IAClF,CAAC;AAAA,EACH;AAAA,EAEA,UAAU,MAAc,SAA8B,SAAoC;AACxF,UAAM,WAAW,KAAK,SAAS,IAAI;AACnC,WAAO,QAAQ,SAAS,UAAU,YAAY;AAC5C,YAAM,KAAK,IAAI,MAAM,QAAQ,QAAQ,CAAC;AACtC,YAAM,KAAK,IAAI,UAAU,UAAU,OAAO;AAAA,IAC5C,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,MAAc,SAA8B,SAAoC;AACzF,UAAM,WAAW,KAAK,SAAS,IAAI;AACnC,WAAO,QAAQ,SAAS,UAAU,YAAY;AAC5C,YAAM,SAAU,MAAM,KAAK,IAAI,OAAO,QAAQ,IAC1C,MAAM,KAAK,IAAI,SAAS,QAAQ,IAChC,IAAI,WAAW;AACnB,YAAM,QAAQ,OAAO,YAAY,WAAW,IAAI,YAAY,EAAE,OAAO,OAAO,IAAI;AAChF,YAAM,MAAM,IAAI,WAAW,OAAO,SAAS,MAAM,MAAM;AACvD,UAAI,IAAI,MAAM;AACd,UAAI,IAAI,OAAO,OAAO,MAAM;AAC5B,YAAM,KAAK,IAAI,MAAM,QAAQ,QAAQ,CAAC;AACtC,YAAM,KAAK,IAAI,UAAU,UAAU,GAAG;AAAA,IACxC,CAAC;AAAA,EACH;AAAA,EAEA,aAAa,MAAc,MAAc,SAAoC;AAC3E,UAAM,WAAW,KAAK,SAAS,IAAI;AACnC,QAAI,CAAC,OAAO,cAAc,IAAI,KAAK,OAAO,GAAG;AAC3C,aAAO,QAAQ;AAAA,QACb,IAAI,IAAI,UAAU,WAAW,4CAA4C,QAAQ,CAAC;AAAA,MACpF;AAAA,IACF;AACA,WAAO,QAAQ,SAAS,UAAU,YAAY;AAC5C,YAAM,SAAS,MAAM,KAAK,IAAI,SAAS,QAAQ;AAC/C,YAAM,QAAQ,IAAI,WAAW,IAAI;AACjC,YAAM,IAAI,OAAO,SAAS,GAAG,IAAI,CAAC;AAClC,YAAM,KAAK,IAAI,UAAU,UAAU,KAAK;AAAA,IAC1C,CAAC;AAAA,EACH;AAAA,EAEA,UAAU,MAAc,SAAoC;AAC1D,UAAM,WAAW,KAAK,MAAM,IAAI;AAChC,WAAO,QAAQ,SAAS,UAAU,YAAY;AAC5C,WAAK,MAAM,KAAK,IAAI,KAAK,QAAQ,GAAG;AAClC,cAAM,OAAO,OAAO,IAAI,MAAM,gBAAgB,GAAG,EAAE,MAAM,SAAS,CAAC;AAAA,IACvE,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,YAAoB,iBAAyB,SAAoC;AAC1F,UAAM,SAAS,KAAK,SAAS,UAAU;AACvC,WAAO,QAAQ,SAAS,QAAQ,MAAM,KAAK,IAAI,OAAO,QAAQ,KAAK,SAAS,eAAe,CAAC,CAAC;AAAA,EAC/F;AAAA,EAEA,SAAS,MAAc,SAAwC;AAC7D,UAAM,WAAW,KAAK,MAAM,IAAI;AAChC,WAAO,QAAQ,SAAS,UAAU,YAAY,KAAK,UAAU,MAAM,KAAK,IAAI,MAAM,QAAQ,CAAC,CAAC;AAAA,EAC9F;AAAA,EAEA,MAAM,QAAQ,MAAc,SAA0C;AACpE,UAAM,SAAS,MAAM,KAAK,cAAc,MAAM,OAAO;AACrD,QAAI,CAAC,OAAO,GAAI,QAAO;AACvB,WAAO,GAAG,MAAO,OAAO,MAA0B,KAAK,CAAC;AAAA,EAC1D;AAAA,EAEA,cAAc,MAAc,SAAyC;AACnE,UAAM,WAAW,KAAK,MAAM,IAAI;AAChC,WAAO,QAAQ,SAAS,UAAU,YAAY;AAC5C,UAAI,EAAE,MAAM,KAAK,IAAI,KAAK,QAAQ,GAAG,aAAa;AAChD,cAAM,OAAO,OAAO,IAAI,MAAM,iBAAiB,GAAG,EAAE,MAAM,UAAU,CAAC;AAAA,MACvE;AACA,aAAO,IAAI,gBAAgB,KAAK,KAAK,UAAU,MAAM,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAAA,IACjF,CAAC;AAAA,EACH;AAAA,EAEA,MACE,SACA,UACA,SACyB;AACzB,WAAO,QAAQ,SAAS,KAAK,KAAK,YAAY;AAC5C,UAAI,SAA6C,MAAM;AACvD,YAAM,WAAW,CAAC,WAAwB;AACxC,YAAI,WAAW,OAAQ,QAAO,OAAO,KAAK;AAC1C,iBAAS,MAAM;AAAA,MACjB;AACA,YAAM,SACJ,KAAK,MAAM,SAAS,YAChB,SACA,MAAM,YAAY,KAAK,KAAK,KAAK,KAAK,SAAS,QAAQ;AAC7D,YAAM,aAAa,SAAU,KAAK,MAAM,YAAY,YAAa,KAAK,MAAM;AAC5E,YAAM,SAAS,MAAM,UAAU,KAAK,KAAK,KAAK,KAAK,SAAS,UAAU;AAAA,QACpE,GAAI,eAAe,SAAY,CAAC,IAAI,EAAE,WAAW;AAAA,MACnD,CAAC;AACD,eAAS,CAAC,UAAU,OAAO,OAAO,KAAK;AACvC,YAAM,UAAuB,SACzB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,MAAM,SAAkB;AAC5B,gBAAM,OAAO,MAAM,OAAO;AAC1B,gBAAM,OAAO,MAAM,OAAO;AAAA,QAC5B;AAAA,MACF,IACA;AACJ,WAAK,UAAU,IAAI,OAAO;AAC1B,aAAO;AAAA,QACL,MAAM,QAAQ;AAAA,QACd,OAAO,OAAO,YAAqB;AACjC,eAAK,UAAU,OAAO,OAAO;AAC7B,gBAAM,QAAQ,MAAM,OAAO;AAAA,QAC7B;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,cAAc,MAAc,SAAsC;AAChE,UAAM,WAAW,KAAK,MAAM,IAAI;AAChC,WAAO,QAAQ,SAAS,UAAU,MAAM,KAAK,IAAI,SAAS,QAAQ,CAAC;AAAA,EACrE;AAAA,EAEA,MAAM,OAAO,MAAc,SAAuC;AAChE,UAAM,QAAQ,MAAM,KAAK,SAAS,MAAM,OAAO;AAC/C,QAAI,MAAM,GAAI,QAAO,GAAG,IAAI;AAC5B,WAAO,MAAM,MAAM,SAAS,cAAc,GAAG,KAAK,IAAI;AAAA,EACxD;AAAA,EAEA,UACE,MACA,UACA,SACkB;AAClB,UAAM,WAAW,KAAK,SAAS,IAAI;AACnC,WAAO,QAAQ,SAAS,UAAU,MAAM,KAAK,IAAI,MAAM,QAAQ,CAAC;AAAA,EAClE;AAAA,EAEA,OACE,MACA,SACA,SACkB;AAClB,UAAM,WAAW,KAAK,SAAS,IAAI;AACnC,WAAO,QAAQ,SAAS,UAAU,YAAY;AAC5C,UAAI,CAAC,SAAS,WAAW;AACvB,cAAM,OAAO,MAAM,KAAK,IAAI,MAAM,QAAQ,EAAE,MAAM,CAAC,UAAmB;AACpE,cAAI,SAAS,MAAO,QAAO;AAC3B,gBAAM;AAAA,QACR,CAAC;AACD,YAAI,MAAM,YAAa,OAAM,OAAO,OAAO,IAAI,MAAM,gBAAgB,GAAG,EAAE,MAAM,SAAS,CAAC;AAAA,MAC5F;AACA,YAAM,KAAK,IAAI,GAAG,UAAU,EAAE,OAAO,SAAS,SAAS,MAAM,CAAC;AAAA,IAChE,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,cAAc,QAA4B,SAAsC;AACpF,UAAM,OAAO,QAAQ,UAAU,MAAM,GAAG,OAAO,WAAW,CAAC;AAC3D,WAAO,QAAQ,SAAS,MAAM,YAAY;AACxC,YAAM,KAAK,IAAI,MAAM,IAAI;AACzB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,eACJ,SACA,SACoB;AACpB,UAAM,MAAM,MAAM,KAAK,cAAc,QAAQ,OAAO;AACpD,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,GAAG,IAAI,KAAK,IAAI,SAAS,UAAU,EAAE,GAAG,OAAO,WAAW,CAAC,GAAG,SAAS,UAAU,EAAE;AAChG,WAAO,QAAQ,SAAS,MAAM,YAAY;AACxC,YAAM,KAAK,IAAI,UAAU,MAAM,EAAE;AACjC,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,KACE,SACA,SACA,SACkD;AAClD,UAAM,QAAQ,KAAK,WAAW,MAAM;AACpC,UAAM,OAAO,OAAO,YAAY,WAAW,SAAS,SAAS,KAAK,QAAQ,IAAI;AAC9E,UAAM,SAAS,KAAK;AACpB,UAAM,QAAQ,KAAK;AACnB,QAAI,CAAC,UAAU,UAAU;AACvB,aAAO;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,KAAK;AAAA,MACP,EAAE,QAAQ,KAAK;AACjB,WAAO,KAAK,SAAS,QAAQ,OAAO,MAAM,SAAS,OAAO,EAAE,QAAQ,KAAK;AAAA,EAC3E;AAAA,EAEA,MAAM,SACJ,QACA,OACA,MACA,SACA,SACkD;AAClD,aAAS,OAAO,OAAO,QAAQ,KAAK,GAAG,MAAM,OAAO,OAAO,QAAQ,KAAK,EAAG,OAAM;AACjF,UAAM,OAAO,OAAO,OAAO,KAAK;AAChC,UAAM,UAAU,SAAS,UAAa,OAAO,MAAM,IAAI,OAAO,QAAQ,KAAK,IAAI,MAAM;AACrF,UAAM,UAAU,CAAC,YAA2B;AAC1C,aAAO,MAAM,OAAO,OAAO;AAC3B,cAAQ;AAAA,IACV;AACA,UAAM,OAAO,OAAO,MAAM,IACtB;AAAA,MACE,GAAI,SAAS,SAAY,CAAC,IAAI,EAAE,KAAK;AAAA,MACrC,SAAS,CAAC,SAAiB,OAAO,QAAQ,OAAO,IAAI;AAAA,IACvD,IACA;AACJ,WAAO;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,KAAK;AAAA,IACP,EAAE,QAAQ,OAAO;AAAA,EACnB;AAAA,EAEA,MAAM,QAAQ,SAAiC;AAC7C,eAAW,WAAW,CAAC,GAAG,KAAK,SAAS,EAAG,OAAM,QAAQ,MAAM,OAAO;AACtE,SAAK,UAAU,MAAM;AAAA,EACvB;AACF;AAEO,MAAM,OAAO;AAMb,SAAS,kBACd,QACA,UAGI,CAAC,GACmF;AACxF,QAAM,EAAE,UAAU,OAAO,GAAG,KAAK,IAAI;AACrC,QAAM,OAAO,CAAC,QAAuB,SAAkC,WACrE,IAAI,eAAe,QAAQ,UAAU,QAAQ;AAAA,IAC3C,GAAG;AAAA,IACH,KAAK,QAAQ,OAAO,OAAO,OAAO,QAAQ,QAAQ;AAAA,IAClD,GAAI,OAAO,mBAAmB,SAAY,CAAC,IAAI,EAAE,OAAO,OAAO,eAAe;AAAA,IAC9E,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/B,CAAC;AACH,MAAI;AACF,WAAO,OAAO,QAAQ,YAAY;AAChC,YAAM,SAAS,MAAM,MAAM,MAAM;AACjC,YAAM,UAAU,YAAY,UAAU,MAAM,SAAS,QAAQ,OAAO,IAAI;AACxE,aAAO,KAAK,QAAQ,SAAS,MAAM;AAAA,IACrC;AACF,SAAO,CAAC,QAAQ,YACd,YAAY,UACR,SAAS,QAAQ,OAAO,EAAE,KAAK,CAAC,YAAY,KAAK,QAAQ,OAAO,CAAC,IACjE,KAAK,MAAM;AACnB;",
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 { mimeOf } from '../images/index.ts';\nimport type { Scrub } from '../secrets/scrub.ts';\nimport type { Activity } from './activity.ts';\nimport type { KernelClient, KernelProcess } from './client.ts';\nimport { type ExecHooks, execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport type { ProcessGroups } from './groups.ts';\nimport type { Jobs } from './jobs.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 groups?: ProcessGroups;\n jobs?: Jobs;\n owner?: number;\n exports?: Readonly<Record<string, string>>;\n scrub?: Scrub;\n spawn?: (argv: readonly string[]) => readonly string[];\n}\n\nexport const STREAMED_READ = 8 * 1024 * 1024;\nexport const READ_CHUNK = 1024 * 1024;\n\nexport async function scrubbedFile(scrub: Scrub, bytes: Uint8Array): Promise<Uint8Array> {\n if (mimeOf(bytes)) return bytes;\n if (bytes.length <= STREAMED_READ) return (await scrub.texts([bytes]))[0] as Uint8Array;\n const stream = scrub.stream();\n const parts: Uint8Array[] = [];\n for (let at = 0; at < bytes.length; at += READ_CHUNK)\n parts.push(await stream.push(bytes.subarray(at, at + READ_CHUNK)));\n parts.push(await stream.end());\n const all = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));\n let offset = 0;\n for (const part of parts) {\n all.set(part, offset);\n offset += part.length;\n }\n return all;\n}\n\nexport function quote(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`;\n}\n\nexport function prefixed(\n command: string,\n exports: Readonly<Record<string, string>> | undefined\n): string {\n const names = Object.keys(exports ?? {}).filter((name) => /^[A-Z_][A-Z0-9_]*$/.test(name));\n if (!names.length) return command;\n return `export ${names.map((name) => `${name}=${quote((exports as Record<string, string>)[name] as string)}`).join(' ')}; ${command}`;\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 readonly #groups: ProcessGroups | undefined;\n readonly #jobs: Jobs | undefined;\n readonly #owner: number | undefined;\n readonly #exports: Readonly<Record<string, string>> | undefined;\n readonly #hooks: ExecHooks;\n readonly #scrub: Scrub | 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 this.#groups = options.groups;\n this.#jobs = options.jobs;\n this.#owner = options.owner;\n this.#exports = options.exports;\n this.#scrub = options.scrub;\n const scrub = options.scrub;\n this.#hooks = {\n ...(scrub ? { scrub: () => scrub.stream() } : {}),\n ...(options.spawn ? { spawn: options.spawn } : {}),\n };\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 const bytes = await this.#fs.readFile(resolved);\n if (!this.#scrub) return new SnapshotReader(bytes, info(resolved, stat));\n const shown = await scrubbedFile(this.#scrub, bytes);\n return new SnapshotReader(shown, { ...info(resolved, stat), size: shown.length });\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 const line = typeof command === 'string' ? prefixed(command, this.#exports) : command;\n const jobs = this.#jobs;\n const client = this.#client;\n let recorded: Promise<void> | undefined;\n const tracked =\n jobs && typeof command === 'string'\n ? (process: KernelProcess) => {\n recorded = jobs.started(client, command, process);\n }\n : undefined;\n const groups = this.#groups;\n const owner = this.#owner;\n const run =\n !groups || owner === undefined\n ? execute(client, this.cwd, line, options, context, tracked, undefined, this.#hooks)\n : this.#grouped(groups, owner, line, options, context, tracked);\n return run\n .then(async (result) => {\n await recorded;\n return result;\n })\n .finally(ended);\n }\n\n async #grouped(\n groups: ProcessGroups,\n owner: number,\n line: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context,\n tracked: ((process: KernelProcess) => void) | undefined\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n for (let wait = groups.pending(owner); wait; wait = groups.pending(owner)) await wait;\n const pgid = groups.shared(owner);\n const release = pgid === undefined && groups.joins() ? groups.reserve(owner) : () => undefined;\n const spawned = (process: KernelProcess) => {\n groups.track(owner, process);\n tracked?.(process);\n release();\n };\n const join = groups.joins()\n ? {\n ...(pgid === undefined ? {} : { pgid }),\n refused: (code: string) => groups.refused(owner, code),\n }\n : undefined;\n return execute(\n this.#client,\n this.cwd,\n line,\n options,\n context,\n spawned,\n join,\n this.#hooks\n ).finally(release);\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 type EnvTargetLike = { cwd?: string; conversationId?: number; read?: unknown };\n\nexport type EnvRoute = { client: KernelClient; home: string; cwd?: string };\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> & {\n identify?: (target: EnvTargetLike, context: Context) => Promise<Record<string, string>>;\n route?: (target: EnvTargetLike) => Promise<EnvRoute | undefined>;\n } = {}\n): (target: EnvTargetLike, context?: Context) => SliccKernelEnv | Promise<SliccKernelEnv> {\n const { identify, route, ...rest } = options;\n const make = (target: EnvTargetLike, exports?: Record<string, string>, routed?: EnvRoute) =>\n new SliccKernelEnv(routed?.client ?? client, {\n ...rest,\n cwd: routed?.cwd ?? target.cwd ?? routed?.home ?? HOME,\n ...(target.conversationId === undefined ? {} : { owner: target.conversationId }),\n ...(exports ? { exports } : {}),\n });\n if (route)\n return async (target, context) => {\n const routed = await route(target);\n const exports = identify && context ? await identify(target, context) : undefined;\n return make(target, exports, routed);\n };\n return (target, context) =>\n identify && context\n ? identify(target, context).then((exports) => make(target, exports))\n : make(target);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,87 @@
1
+ import { redacted } from "../monitor/redact.js";
2
+ const JOBS_MS = 2e3;
3
+ function shortCommand(command) {
4
+ const lines = command.trim().split("\n");
5
+ const first = redacted(lines[0]);
6
+ return lines.length > 1 && !first.endsWith("\u2026") ? `${first} \u2026` : first;
7
+ }
8
+ function backgroundJobs(client, ms = JOBS_MS) {
9
+ const jobs = /* @__PURE__ */ new Map();
10
+ const running = /* @__PURE__ */ new Set();
11
+ const listeners = /* @__PURE__ */ new Set();
12
+ let timer;
13
+ const list = () => [...jobs.values()].map(({ pid, pgid, command }) => ({ pid, pgid, command }));
14
+ const orphans = (table, pgid, after) => table.filter(
15
+ (row) => row.ppid === 1 && row.pgid === pgid && row.pid > after && row.state !== "Z" && !running.has(row.pid) && !jobs.has(row.pid)
16
+ );
17
+ const keep = (rows, pgid, command, owner) => {
18
+ for (const row of rows) jobs.set(row.pid, { pid: row.pid, pgid, command, client: owner });
19
+ };
20
+ const schedule = () => {
21
+ const wanted = jobs.size > 0 && listeners.size > 0;
22
+ if (wanted && !timer) timer = setInterval(() => void prune(), ms);
23
+ if (!wanted && timer) {
24
+ clearInterval(timer);
25
+ timer = void 0;
26
+ }
27
+ };
28
+ const changed = async () => {
29
+ schedule();
30
+ await Promise.all([...listeners].map((listener) => listener()));
31
+ };
32
+ const sweep = (owner, table) => {
33
+ const alive = new Set(table.filter((row) => row.state !== "Z").map((row) => row.pid));
34
+ let gone = false;
35
+ for (const [pid, job] of [...jobs])
36
+ if (job.client === owner && !alive.has(pid)) {
37
+ gone = jobs.delete(pid);
38
+ keep(orphans(table, job.pgid, pid), job.pgid, job.command, owner);
39
+ }
40
+ return gone;
41
+ };
42
+ async function prune() {
43
+ let gone = false;
44
+ for (const each of new Set([...jobs.values()].map((job) => job.client))) {
45
+ const table = await each.ps?.().catch(() => void 0);
46
+ if (table && sweep(each, table)) gone = true;
47
+ }
48
+ if (gone) await changed();
49
+ }
50
+ async function survivors(owner, command, process) {
51
+ const table = await owner.ps?.().catch(() => void 0);
52
+ if (!table) return;
53
+ const found = orphans(table, process.pgid, process.pid);
54
+ keep(found, process.pgid, shortCommand(command), owner);
55
+ if (sweep(owner, table) || found.length) await changed();
56
+ }
57
+ return {
58
+ async started(owner, command, process) {
59
+ if (!owner.ps) return;
60
+ running.add(process.pid);
61
+ const ended = () => {
62
+ running.delete(process.pid);
63
+ return survivors(owner, command, process);
64
+ };
65
+ await process.exited.then(ended, ended).catch(() => void 0);
66
+ },
67
+ list,
68
+ adopt(adopted) {
69
+ for (const job of adopted) if (!jobs.has(job.pid)) jobs.set(job.pid, { ...job, client });
70
+ schedule();
71
+ },
72
+ watch(listener) {
73
+ listeners.add(listener);
74
+ schedule();
75
+ return () => {
76
+ listeners.delete(listener);
77
+ schedule();
78
+ };
79
+ }
80
+ };
81
+ }
82
+ export {
83
+ JOBS_MS,
84
+ backgroundJobs,
85
+ shortCommand
86
+ };
87
+ //# sourceMappingURL=jobs.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/kernel/jobs.ts"],
4
+ "sourcesContent": ["import { redacted } from '../monitor/redact.ts';\nimport type { KernelClient, KernelProcess } from './client.ts';\n\nexport type Job = { pid: number; pgid: number; command: string };\n\nexport interface Jobs {\n started(client: KernelClient, command: string, process: KernelProcess): Promise<void>;\n list(): Job[];\n adopt(jobs: readonly Job[]): void;\n watch(listener: () => Promise<void>): () => void;\n}\n\nexport const JOBS_MS = 2000;\n\ntype Ps = Pick<KernelClient, 'ps'>;\n\ntype Row = Awaited<ReturnType<NonNullable<KernelClient['ps']>>>[number];\n\nexport function shortCommand(command: string): string {\n const lines = command.trim().split('\\n');\n const first = redacted(lines[0] as string);\n return lines.length > 1 && !first.endsWith('\u2026') ? `${first} \u2026` : first;\n}\n\nexport function backgroundJobs(client: Ps, ms = JOBS_MS): Jobs {\n const jobs = new Map<number, Job & { client: Ps }>();\n const running = new Set<number>();\n const listeners = new Set<() => Promise<void>>();\n let timer: ReturnType<typeof setInterval> | undefined;\n const list = () => [...jobs.values()].map(({ pid, pgid, command }) => ({ pid, pgid, command }));\n const orphans = (table: readonly Row[], pgid: number, after: number) =>\n table.filter(\n (row) =>\n row.ppid === 1 &&\n row.pgid === pgid &&\n row.pid > after &&\n row.state !== 'Z' &&\n !running.has(row.pid) &&\n !jobs.has(row.pid)\n );\n const keep = (rows: readonly Row[], pgid: number, command: string, owner: Ps) => {\n for (const row of rows) jobs.set(row.pid, { pid: row.pid, pgid, command, client: owner });\n };\n const schedule = () => {\n const wanted = jobs.size > 0 && listeners.size > 0;\n if (wanted && !timer) timer = setInterval(() => void prune(), ms);\n if (!wanted && timer) {\n clearInterval(timer);\n timer = undefined;\n }\n };\n const changed = async () => {\n schedule();\n await Promise.all([...listeners].map((listener) => listener()));\n };\n const sweep = (owner: Ps, table: readonly Row[]) => {\n const alive = new Set(table.filter((row) => row.state !== 'Z').map((row) => row.pid));\n let gone = false;\n for (const [pid, job] of [...jobs])\n if (job.client === owner && !alive.has(pid)) {\n gone = jobs.delete(pid);\n keep(orphans(table, job.pgid, pid), job.pgid, job.command, owner);\n }\n return gone;\n };\n async function prune(): Promise<void> {\n let gone = false;\n for (const each of new Set([...jobs.values()].map((job) => job.client))) {\n const table = await each.ps?.().catch(() => undefined);\n if (table && sweep(each, table)) gone = true;\n }\n if (gone) await changed();\n }\n async function survivors(owner: Ps, command: string, process: KernelProcess): Promise<void> {\n const table = await owner.ps?.().catch(() => undefined);\n if (!table) return;\n const found = orphans(table, process.pgid, process.pid);\n keep(found, process.pgid, shortCommand(command), owner);\n if (sweep(owner, table) || found.length) await changed();\n }\n return {\n async started(owner, command, process) {\n if (!owner.ps) return;\n running.add(process.pid);\n const ended = () => {\n running.delete(process.pid);\n return survivors(owner, command, process);\n };\n await process.exited.then(ended, ended).catch(() => undefined);\n },\n list,\n adopt(adopted) {\n for (const job of adopted) if (!jobs.has(job.pid)) jobs.set(job.pid, { ...job, client });\n schedule();\n },\n watch(listener) {\n listeners.add(listener);\n schedule();\n return () => {\n listeners.delete(listener);\n schedule();\n };\n },\n };\n}\n"],
5
+ "mappings": "AAAA,SAAS,gBAAgB;AAYlB,MAAM,UAAU;AAMhB,SAAS,aAAa,SAAyB;AACpD,QAAM,QAAQ,QAAQ,KAAK,EAAE,MAAM,IAAI;AACvC,QAAM,QAAQ,SAAS,MAAM,CAAC,CAAW;AACzC,SAAO,MAAM,SAAS,KAAK,CAAC,MAAM,SAAS,QAAG,IAAI,GAAG,KAAK,YAAO;AACnE;AAEO,SAAS,eAAe,QAAY,KAAK,SAAe;AAC7D,QAAM,OAAO,oBAAI,IAAkC;AACnD,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,YAAY,oBAAI,IAAyB;AAC/C,MAAI;AACJ,QAAM,OAAO,MAAM,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,KAAK,MAAM,QAAQ,OAAO,EAAE,KAAK,MAAM,QAAQ,EAAE;AAC9F,QAAM,UAAU,CAAC,OAAuB,MAAc,UACpD,MAAM;AAAA,IACJ,CAAC,QACC,IAAI,SAAS,KACb,IAAI,SAAS,QACb,IAAI,MAAM,SACV,IAAI,UAAU,OACd,CAAC,QAAQ,IAAI,IAAI,GAAG,KACpB,CAAC,KAAK,IAAI,IAAI,GAAG;AAAA,EACrB;AACF,QAAM,OAAO,CAAC,MAAsB,MAAc,SAAiB,UAAc;AAC/E,eAAW,OAAO,KAAM,MAAK,IAAI,IAAI,KAAK,EAAE,KAAK,IAAI,KAAK,MAAM,SAAS,QAAQ,MAAM,CAAC;AAAA,EAC1F;AACA,QAAM,WAAW,MAAM;AACrB,UAAM,SAAS,KAAK,OAAO,KAAK,UAAU,OAAO;AACjD,QAAI,UAAU,CAAC,MAAO,SAAQ,YAAY,MAAM,KAAK,MAAM,GAAG,EAAE;AAChE,QAAI,CAAC,UAAU,OAAO;AACpB,oBAAc,KAAK;AACnB,cAAQ;AAAA,IACV;AAAA,EACF;AACA,QAAM,UAAU,YAAY;AAC1B,aAAS;AACT,UAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,EAAE,IAAI,CAAC,aAAa,SAAS,CAAC,CAAC;AAAA,EAChE;AACA,QAAM,QAAQ,CAAC,OAAW,UAA0B;AAClD,UAAM,QAAQ,IAAI,IAAI,MAAM,OAAO,CAAC,QAAQ,IAAI,UAAU,GAAG,EAAE,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC;AACpF,QAAI,OAAO;AACX,eAAW,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,IAAI;AAC/B,UAAI,IAAI,WAAW,SAAS,CAAC,MAAM,IAAI,GAAG,GAAG;AAC3C,eAAO,KAAK,OAAO,GAAG;AACtB,aAAK,QAAQ,OAAO,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,IAAI,SAAS,KAAK;AAAA,MAClE;AACF,WAAO;AAAA,EACT;AACA,iBAAe,QAAuB;AACpC,QAAI,OAAO;AACX,eAAW,QAAQ,IAAI,IAAI,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,GAAG;AACvE,YAAM,QAAQ,MAAM,KAAK,KAAK,EAAE,MAAM,MAAM,MAAS;AACrD,UAAI,SAAS,MAAM,MAAM,KAAK,EAAG,QAAO;AAAA,IAC1C;AACA,QAAI,KAAM,OAAM,QAAQ;AAAA,EAC1B;AACA,iBAAe,UAAU,OAAW,SAAiB,SAAuC;AAC1F,UAAM,QAAQ,MAAM,MAAM,KAAK,EAAE,MAAM,MAAM,MAAS;AACtD,QAAI,CAAC,MAAO;AACZ,UAAM,QAAQ,QAAQ,OAAO,QAAQ,MAAM,QAAQ,GAAG;AACtD,SAAK,OAAO,QAAQ,MAAM,aAAa,OAAO,GAAG,KAAK;AACtD,QAAI,MAAM,OAAO,KAAK,KAAK,MAAM,OAAQ,OAAM,QAAQ;AAAA,EACzD;AACA,SAAO;AAAA,IACL,MAAM,QAAQ,OAAO,SAAS,SAAS;AACrC,UAAI,CAAC,MAAM,GAAI;AACf,cAAQ,IAAI,QAAQ,GAAG;AACvB,YAAM,QAAQ,MAAM;AAClB,gBAAQ,OAAO,QAAQ,GAAG;AAC1B,eAAO,UAAU,OAAO,SAAS,OAAO;AAAA,MAC1C;AACA,YAAM,QAAQ,OAAO,KAAK,OAAO,KAAK,EAAE,MAAM,MAAM,MAAS;AAAA,IAC/D;AAAA,IACA;AAAA,IACA,MAAM,SAAS;AACb,iBAAW,OAAO,QAAS,KAAI,CAAC,KAAK,IAAI,IAAI,GAAG,EAAG,MAAK,IAAI,IAAI,KAAK,EAAE,GAAG,KAAK,OAAO,CAAC;AACvF,eAAS;AAAA,IACX;AAAA,IACA,MAAM,UAAU;AACd,gBAAU,IAAI,QAAQ;AACtB,eAAS;AACT,aAAO,MAAM;AACX,kBAAU,OAAO,QAAQ;AACzB,iBAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -7,7 +7,7 @@ const LicksHostDoc = defineDoc({
7
7
  scope: "session",
8
8
  initial: () => ({ cron: {}, agent: null, boot: null, mark: 0, upgrades: 0 })
9
9
  });
10
- const BACKGROUND = /(?:&\s*$|&\s*disown\b|\bnohup\b|\bsetsid\b)/m;
10
+ const LISTED = 5;
11
11
  function bashCommands(entries, after) {
12
12
  const commands = [];
13
13
  for (const entry of entries) {
@@ -32,8 +32,13 @@ function upgraded(from, to, upgrades) {
32
32
  eventId: `${from}->${to}#${upgrades}`
33
33
  };
34
34
  }
35
- function restarted(boot, commands) {
36
- const background = commands.filter((command) => BACKGROUND.test(command)).slice(-5);
35
+ function backgroundList(jobs) {
36
+ const commands = [...new Set(jobs.map((job) => job.command))];
37
+ const more = commands.length - LISTED;
38
+ return more > 0 ? [...commands.slice(-LISTED), `\u2026and ${more} more`] : commands;
39
+ }
40
+ function restarted(boot, jobs) {
41
+ const background = backgroundList(jobs);
37
42
  return {
38
43
  channel: "session-reload",
39
44
  source: "kernel",
@@ -45,16 +50,18 @@ ${background.join("\n")}` : "Files are unchanged. Start again whatever you still
45
50
  eventId: `boot:${boot}`
46
51
  };
47
52
  }
48
- async function bootLicks(harness, cone, deliver, facts, context) {
53
+ async function bootLicks(harness, cone, deliver, facts, context, jobs, listen) {
49
54
  const state = await harness.snapshot(LicksHostDoc, context);
55
+ listen?.();
50
56
  const previous = state?.agent ?? null;
51
57
  if (previous !== null && previous !== facts.version)
52
58
  await deliver(upgraded(previous, facts.version, state?.upgrades ?? 0), context);
53
59
  const { entries } = await cone.context(context);
60
+ const before = state?.jobs ?? [];
54
61
  if (facts.boot !== null && state?.boot && facts.boot !== state.boot) {
55
62
  const commands = bashCommands(entries, state.mark);
56
- if (commands.length) await deliver(restarted(facts.boot, commands), context);
57
- }
63
+ if (commands.length || before.length) await deliver(restarted(facts.boot, before), context);
64
+ } else if (facts.boot !== null && facts.boot === state?.boot) jobs?.adopt(before);
58
65
  const { entries: after } = await cone.context(context);
59
66
  const mark = Number(after.at(-1)?.id ?? 0);
60
67
  await harness.commit(async (tx) => {
@@ -65,11 +72,42 @@ async function bootLicks(harness, cone, deliver, facts, context) {
65
72
  doc.boot = facts.boot;
66
73
  doc.mark = mark;
67
74
  }
75
+ doc.jobs = jobs?.list() ?? [];
68
76
  }, context);
69
77
  }
78
+ function recordJobs(harness, jobs, context) {
79
+ let chain = Promise.resolve();
80
+ return jobs.watch(() => {
81
+ chain = chain.then(
82
+ () => harness.commit(async (tx) => {
83
+ (await tx.doc(LicksHostDoc)).jobs = jobs.list();
84
+ }, context)
85
+ ).catch(() => void 0);
86
+ return chain;
87
+ });
88
+ }
89
+ function booting(harness, cone, deliver, jobs) {
90
+ let stopped = false;
91
+ let off = () => void 0;
92
+ return {
93
+ async boot(facts, context) {
94
+ await bootLicks(harness, cone(), deliver, facts, context, jobs, () => {
95
+ off();
96
+ if (jobs && !stopped) off = recordJobs(harness, jobs, context);
97
+ });
98
+ },
99
+ stop() {
100
+ stopped = true;
101
+ off();
102
+ }
103
+ };
104
+ }
70
105
  export {
106
+ LISTED,
71
107
  LicksHostDoc,
108
+ backgroundList,
72
109
  bashCommands,
73
- bootLicks
110
+ bootLicks,
111
+ booting
74
112
  };
75
113
  //# sourceMappingURL=boot.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/licks/boot.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type EntryRecord,\n type Harness,\n} from '@earendil-works/pi-durable';\nimport type { Deliver } from './fswatch.ts';\n\ntype HostState = {\n cron: Record<string, { task: number }>;\n agent: string | null;\n boot: string | null;\n mark: number;\n upgrades: number;\n};\n\nexport const LicksHostDoc = defineDoc<HostState>({\n kind: 'slicc.licks.host',\n version: 1,\n scope: 'session',\n initial: () => ({ cron: {}, agent: null, boot: null, mark: 0, upgrades: 0 }),\n});\n\nexport interface BootFacts {\n version: string;\n boot: string | null;\n}\n\nconst BACKGROUND = /(?:&\\s*$|&\\s*disown\\b|\\bnohup\\b|\\bsetsid\\b)/m;\n\ninterface Block {\n type?: string;\n name?: string;\n arguments?: { command?: unknown };\n}\n\nexport function bashCommands(entries: readonly EntryRecord[], after: number): string[] {\n const commands: string[] = [];\n for (const entry of entries) {\n if (entry.kind !== 'pi.assistant' || Number(entry.id) <= after) continue;\n const content = (entry.model?.[0] as { content?: unknown } | undefined)?.content;\n for (const block of Array.isArray(content) ? (content as Block[]) : []) {\n const command = block.arguments?.command;\n if (block.type === 'toolCall' && block.name === 'bash' && typeof command === 'string')\n commands.push(command);\n }\n }\n return commands;\n}\n\nfunction upgraded(from: string, to: string, upgrades: number) {\n return {\n channel: 'upgrade',\n source: 'slicc-agent',\n title: `slicc-agent ${from} \u2192 ${to}`,\n text: `The agent was updated from ${from} to ${to}.`,\n body: `Release notes: https://github.com/ai-ecoverse/slicc-agent/releases/tag/v${to}`,\n target: 'cone',\n eventId: `${from}->${to}#${upgrades}`,\n } as const;\n}\n\nfunction restarted(boot: string, commands: readonly string[]) {\n const background = commands.filter((command) => BACKGROUND.test(command)).slice(-5);\n return {\n channel: 'session-reload',\n source: 'kernel',\n title: 'The kernel restarted',\n text: 'The page reloaded, so the kernel started fresh: processes from before the reload are gone.',\n body: background.length\n ? `Background commands that are no longer running:\\n${background.join('\\n')}`\n : 'Files are unchanged. Start again whatever you still need running.',\n target: 'cone',\n eventId: `boot:${boot}`,\n } as const;\n}\n\nexport async function bootLicks(\n harness: Harness,\n cone: Conversation,\n deliver: Deliver,\n facts: BootFacts,\n context: Context\n): Promise<void> {\n const state = await harness.snapshot(LicksHostDoc, context);\n const previous = state?.agent ?? null;\n if (previous !== null && previous !== facts.version)\n await deliver(upgraded(previous, facts.version, state?.upgrades ?? 0), context);\n const { entries } = await cone.context(context);\n if (facts.boot !== null && state?.boot && facts.boot !== state.boot) {\n const commands = bashCommands(entries, state.mark);\n if (commands.length) await deliver(restarted(facts.boot, commands), context);\n }\n const { entries: after } = await cone.context(context);\n const mark = Number(after.at(-1)?.id ?? 0);\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksHostDoc);\n if (doc.agent !== null && doc.agent !== facts.version) doc.upgrades += 1;\n doc.agent = facts.version;\n if (facts.boot !== null && doc.boot !== facts.boot) {\n doc.boot = facts.boot;\n doc.mark = mark;\n }\n }, context);\n}\n"],
5
- "mappings": "AACA;AAAA,EAEE;AAAA,OAGK;AAWA,MAAM,eAAe,UAAqB;AAAA,EAC/C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,OAAO,EAAE,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM,MAAM,MAAM,GAAG,UAAU,EAAE;AAC5E,CAAC;AAOD,MAAM,aAAa;AAQZ,SAAS,aAAa,SAAiC,OAAyB;AACrF,QAAM,WAAqB,CAAC;AAC5B,aAAW,SAAS,SAAS;AAC3B,QAAI,MAAM,SAAS,kBAAkB,OAAO,MAAM,EAAE,KAAK,MAAO;AAChE,UAAM,UAAW,MAAM,QAAQ,CAAC,GAAyC;AACzE,eAAW,SAAS,MAAM,QAAQ,OAAO,IAAK,UAAsB,CAAC,GAAG;AACtE,YAAM,UAAU,MAAM,WAAW;AACjC,UAAI,MAAM,SAAS,cAAc,MAAM,SAAS,UAAU,OAAO,YAAY;AAC3E,iBAAS,KAAK,OAAO;AAAA,IACzB;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,SAAS,MAAc,IAAY,UAAkB;AAC5D,SAAO;AAAA,IACL,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,OAAO,eAAe,IAAI,WAAM,EAAE;AAAA,IAClC,MAAM,8BAA8B,IAAI,OAAO,EAAE;AAAA,IACjD,MAAM,2EAA2E,EAAE;AAAA,IACnF,QAAQ;AAAA,IACR,SAAS,GAAG,IAAI,KAAK,EAAE,IAAI,QAAQ;AAAA,EACrC;AACF;AAEA,SAAS,UAAU,MAAc,UAA6B;AAC5D,QAAM,aAAa,SAAS,OAAO,CAAC,YAAY,WAAW,KAAK,OAAO,CAAC,EAAE,MAAM,EAAE;AAClF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM,WAAW,SACb;AAAA,EAAoD,WAAW,KAAK,IAAI,CAAC,KACzE;AAAA,IACJ,QAAQ;AAAA,IACR,SAAS,QAAQ,IAAI;AAAA,EACvB;AACF;AAEA,eAAsB,UACpB,SACA,MACA,SACA,OACA,SACe;AACf,QAAM,QAAQ,MAAM,QAAQ,SAAS,cAAc,OAAO;AAC1D,QAAM,WAAW,OAAO,SAAS;AACjC,MAAI,aAAa,QAAQ,aAAa,MAAM;AAC1C,UAAM,QAAQ,SAAS,UAAU,MAAM,SAAS,OAAO,YAAY,CAAC,GAAG,OAAO;AAChF,QAAM,EAAE,QAAQ,IAAI,MAAM,KAAK,QAAQ,OAAO;AAC9C,MAAI,MAAM,SAAS,QAAQ,OAAO,QAAQ,MAAM,SAAS,MAAM,MAAM;AACnE,UAAM,WAAW,aAAa,SAAS,MAAM,IAAI;AACjD,QAAI,SAAS,OAAQ,OAAM,QAAQ,UAAU,MAAM,MAAM,QAAQ,GAAG,OAAO;AAAA,EAC7E;AACA,QAAM,EAAE,SAAS,MAAM,IAAI,MAAM,KAAK,QAAQ,OAAO;AACrD,QAAM,OAAO,OAAO,MAAM,GAAG,EAAE,GAAG,MAAM,CAAC;AACzC,QAAM,QAAQ,OAAO,OAAO,OAAO;AACjC,UAAM,MAAM,MAAM,GAAG,IAAI,YAAY;AACrC,QAAI,IAAI,UAAU,QAAQ,IAAI,UAAU,MAAM,QAAS,KAAI,YAAY;AACvE,QAAI,QAAQ,MAAM;AAClB,QAAI,MAAM,SAAS,QAAQ,IAAI,SAAS,MAAM,MAAM;AAClD,UAAI,OAAO,MAAM;AACjB,UAAI,OAAO;AAAA,IACb;AAAA,EACF,GAAG,OAAO;AACZ;",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type EntryRecord,\n type Harness,\n} from '@earendil-works/pi-durable';\nimport type { Job, Jobs } from '../kernel/jobs.ts';\nimport type { Deliver } from './fswatch.ts';\n\ntype HostState = {\n cron: Record<string, { task: number }>;\n agent: string | null;\n boot: string | null;\n mark: number;\n upgrades: number;\n jobs?: Job[];\n};\n\nexport const LicksHostDoc = defineDoc<HostState>({\n kind: 'slicc.licks.host',\n version: 1,\n scope: 'session',\n initial: () => ({ cron: {}, agent: null, boot: null, mark: 0, upgrades: 0 }),\n});\n\nexport interface BootFacts {\n version: string;\n boot: string | null;\n}\n\nexport const LISTED = 5;\n\ninterface Block {\n type?: string;\n name?: string;\n arguments?: { command?: unknown };\n}\n\nexport function bashCommands(entries: readonly EntryRecord[], after: number): string[] {\n const commands: string[] = [];\n for (const entry of entries) {\n if (entry.kind !== 'pi.assistant' || Number(entry.id) <= after) continue;\n const content = (entry.model?.[0] as { content?: unknown } | undefined)?.content;\n for (const block of Array.isArray(content) ? (content as Block[]) : []) {\n const command = block.arguments?.command;\n if (block.type === 'toolCall' && block.name === 'bash' && typeof command === 'string')\n commands.push(command);\n }\n }\n return commands;\n}\n\nfunction upgraded(from: string, to: string, upgrades: number) {\n return {\n channel: 'upgrade',\n source: 'slicc-agent',\n title: `slicc-agent ${from} \u2192 ${to}`,\n text: `The agent was updated from ${from} to ${to}.`,\n body: `Release notes: https://github.com/ai-ecoverse/slicc-agent/releases/tag/v${to}`,\n target: 'cone',\n eventId: `${from}->${to}#${upgrades}`,\n } as const;\n}\n\nexport function backgroundList(jobs: readonly Job[]): string[] {\n const commands = [...new Set(jobs.map((job) => job.command))];\n const more = commands.length - LISTED;\n return more > 0 ? [...commands.slice(-LISTED), `\u2026and ${more} more`] : commands;\n}\n\nfunction restarted(boot: string, jobs: readonly Job[]) {\n const background = backgroundList(jobs);\n return {\n channel: 'session-reload',\n source: 'kernel',\n title: 'The kernel restarted',\n text: 'The page reloaded, so the kernel started fresh: processes from before the reload are gone.',\n body: background.length\n ? `Background commands that are no longer running:\\n${background.join('\\n')}`\n : 'Files are unchanged. Start again whatever you still need running.',\n target: 'cone',\n eventId: `boot:${boot}`,\n } as const;\n}\n\nexport async function bootLicks(\n harness: Harness,\n cone: Conversation,\n deliver: Deliver,\n facts: BootFacts,\n context: Context,\n jobs?: Jobs,\n listen?: () => void\n): Promise<void> {\n const state = await harness.snapshot(LicksHostDoc, context);\n listen?.();\n const previous = state?.agent ?? null;\n if (previous !== null && previous !== facts.version)\n await deliver(upgraded(previous, facts.version, state?.upgrades ?? 0), context);\n const { entries } = await cone.context(context);\n const before = state?.jobs ?? [];\n if (facts.boot !== null && state?.boot && facts.boot !== state.boot) {\n const commands = bashCommands(entries, state.mark);\n if (commands.length || before.length) await deliver(restarted(facts.boot, before), context);\n } else if (facts.boot !== null && facts.boot === state?.boot) jobs?.adopt(before);\n const { entries: after } = await cone.context(context);\n const mark = Number(after.at(-1)?.id ?? 0);\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksHostDoc);\n if (doc.agent !== null && doc.agent !== facts.version) doc.upgrades += 1;\n doc.agent = facts.version;\n if (facts.boot !== null && doc.boot !== facts.boot) {\n doc.boot = facts.boot;\n doc.mark = mark;\n }\n doc.jobs = jobs?.list() ?? [];\n }, context);\n}\n\nfunction recordJobs(harness: Harness, jobs: Jobs, context: Context): () => void {\n let chain: Promise<void> = Promise.resolve();\n return jobs.watch(() => {\n chain = chain\n .then(() =>\n harness.commit(async (tx) => {\n (await tx.doc(LicksHostDoc)).jobs = jobs.list();\n }, context)\n )\n .catch(() => undefined);\n return chain;\n });\n}\n\nexport function booting(\n harness: Harness,\n cone: () => Conversation,\n deliver: Deliver,\n jobs: Jobs | undefined\n) {\n let stopped = false;\n let off = () => undefined as void;\n return {\n async boot(facts: BootFacts, context: Context): Promise<void> {\n await bootLicks(harness, cone(), deliver, facts, context, jobs, () => {\n off();\n if (jobs && !stopped) off = recordJobs(harness, jobs, context);\n });\n },\n stop() {\n stopped = true;\n off();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/licks/index.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, ConversationId, Harness, Registry } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { type Agents, live } from '../agents.ts';\nimport type { Activity } from '../kernel/activity.ts';\nimport { licksExtension } from './extension.ts';\nimport type { LickChannel, LickTarget } from './lick.ts';\nimport { createLicks, type Licks, type LicksHost } from './licks.ts';\nimport { createLickSources, type LickDir, type LickSources } from './sources.ts';\n\nexport interface LicksAgent {\n harness: Harness;\n root: Conversation;\n agents: Agents;\n cone(): Conversation;\n}\n\nconst UNTRUSTED = new Set<LickChannel>(['webhook']);\n\nexport function lickResolver(agent: LicksAgent) {\n return async (target: LickTarget, channel: LickChannel, context: Context) => {\n const cone = agent.cone();\n if (target === 'cone') return cone;\n if (target.startsWith('cone:'))\n return (await agent.agents.conversation(target.slice('cone:'.length), context)) ?? cone;\n if (UNTRUSTED.has(channel)) return cone;\n const scoop = agent.agents.state().scoops[target];\n if (!scoop || !live(scoop)) return cone;\n return (await agent.agents.conversation(target, context)) ?? cone;\n };\n}\n\nexport interface LicksSetup {\n licks: Licks;\n attach(\n agent: LicksAgent,\n options: {\n env: ExecutionEnv;\n home: string;\n flushEvery?: number;\n activity?: Activity;\n dirs?: () => Promise<readonly LickDir[]>;\n cronUser?: (target: LickTarget, place: LickDir) => string | undefined;\n }\n ): LickSources;\n}\n\nexport function setupLicks(registry: Registry, fetcher?: typeof fetch): LicksSetup {\n let bind: (host: LicksHost) => void = () => undefined;\n const host = new Promise<LicksHost>((resolve) => {\n bind = resolve;\n });\n const licks = createLicks(host);\n const lookup = async (id: ConversationId, context: Context) => {\n const found = await (await host).harness.conversation(id, context);\n if (!found) throw new Error(`conversation ${id} is gone`);\n return found;\n };\n registry.install(licksExtension(licks, lookup));\n return {\n licks,\n attach(agent, options) {\n const { harness, root } = agent;\n bind({ harness, resolve: lickResolver(agent) });\n return createLickSources({\n ...options,\n harness,\n root,\n cone: () => agent.cone(),\n licks,\n ...(fetcher ? { fetch: fetcher } : {}),\n });\n },\n };\n}\n\nexport type { Lick, LickChannel } from './lick.ts';\nexport type { LickEvent, Licks } from './licks.ts';\nexport type { LickSources, WebhookDelivery } from './sources.ts';\n"],
5
- "mappings": "AAGA,SAAsB,YAAY;AAElC,SAAS,sBAAsB;AAE/B,SAAS,mBAA+C;AACxD,SAAS,yBAAyD;AASlE,MAAM,YAAY,oBAAI,IAAiB,CAAC,SAAS,CAAC;AAE3C,SAAS,aAAa,OAAmB;AAC9C,SAAO,OAAO,QAAoB,SAAsB,YAAqB;AAC3E,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,WAAW,OAAQ,QAAO;AAC9B,QAAI,OAAO,WAAW,OAAO;AAC3B,aAAQ,MAAM,MAAM,OAAO,aAAa,OAAO,MAAM,QAAQ,MAAM,GAAG,OAAO,KAAM;AACrF,QAAI,UAAU,IAAI,OAAO,EAAG,QAAO;AACnC,UAAM,QAAQ,MAAM,OAAO,MAAM,EAAE,OAAO,MAAM;AAChD,QAAI,CAAC,SAAS,CAAC,KAAK,KAAK,EAAG,QAAO;AACnC,WAAQ,MAAM,MAAM,OAAO,aAAa,QAAQ,OAAO,KAAM;AAAA,EAC/D;AACF;AAiBO,SAAS,WAAW,UAAoB,SAAoC;AACjF,MAAI,OAAkC,MAAM;AAC5C,QAAM,OAAO,IAAI,QAAmB,CAAC,YAAY;AAC/C,WAAO;AAAA,EACT,CAAC;AACD,QAAM,QAAQ,YAAY,IAAI;AAC9B,QAAM,SAAS,OAAO,IAAoB,YAAqB;AAC7D,UAAM,QAAQ,OAAO,MAAM,MAAM,QAAQ,aAAa,IAAI,OAAO;AACjE,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,gBAAgB,EAAE,UAAU;AACxD,WAAO;AAAA,EACT;AACA,WAAS,QAAQ,eAAe,OAAO,MAAM,CAAC;AAC9C,SAAO;AAAA,IACL;AAAA,IACA,OAAO,OAAO,SAAS;AACrB,YAAM,EAAE,SAAS,KAAK,IAAI;AAC1B,WAAK,EAAE,SAAS,SAAS,aAAa,KAAK,EAAE,CAAC;AAC9C,aAAO,kBAAkB;AAAA,QACvB,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA,MAAM,MAAM,MAAM,KAAK;AAAA,QACvB;AAAA,QACA,GAAI,UAAU,EAAE,OAAO,QAAQ,IAAI,CAAC;AAAA,MACtC,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, ConversationId, Harness, Registry } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { type Agents, live } from '../agents.ts';\nimport type { Activity } from '../kernel/activity.ts';\nimport type { Jobs } from '../kernel/jobs.ts';\nimport { licksExtension } from './extension.ts';\nimport type { LickChannel, LickTarget } from './lick.ts';\nimport { createLicks, type Licks, type LicksHost } from './licks.ts';\nimport { createLickSources, type LickDir, type LickSources } from './sources.ts';\n\nexport interface LicksAgent {\n harness: Harness;\n root: Conversation;\n agents: Agents;\n cone(): Conversation;\n}\n\nconst UNTRUSTED = new Set<LickChannel>(['webhook']);\n\nexport function lickResolver(agent: LicksAgent) {\n return async (target: LickTarget, channel: LickChannel, context: Context) => {\n const cone = agent.cone();\n if (target === 'cone') return cone;\n if (target.startsWith('cone:'))\n return (await agent.agents.conversation(target.slice('cone:'.length), context)) ?? cone;\n if (UNTRUSTED.has(channel)) return cone;\n const scoop = agent.agents.state().scoops[target];\n if (!scoop || !live(scoop)) return cone;\n return (await agent.agents.conversation(target, context)) ?? cone;\n };\n}\n\nexport interface LicksSetup {\n licks: Licks;\n attach(\n agent: LicksAgent,\n options: {\n env: ExecutionEnv;\n home: string;\n flushEvery?: number;\n activity?: Activity;\n jobs?: Jobs;\n dirs?: () => Promise<readonly LickDir[]>;\n cronUser?: (target: LickTarget, place: LickDir) => string | undefined;\n }\n ): LickSources;\n}\n\nexport function setupLicks(registry: Registry, fetcher?: typeof fetch): LicksSetup {\n let bind: (host: LicksHost) => void = () => undefined;\n const host = new Promise<LicksHost>((resolve) => {\n bind = resolve;\n });\n const licks = createLicks(host);\n const lookup = async (id: ConversationId, context: Context) => {\n const found = await (await host).harness.conversation(id, context);\n if (!found) throw new Error(`conversation ${id} is gone`);\n return found;\n };\n registry.install(licksExtension(licks, lookup));\n return {\n licks,\n attach(agent, options) {\n const { harness, root } = agent;\n bind({ harness, resolve: lickResolver(agent) });\n return createLickSources({\n ...options,\n harness,\n root,\n cone: () => agent.cone(),\n licks,\n ...(fetcher ? { fetch: fetcher } : {}),\n });\n },\n };\n}\n\nexport type { Lick, LickChannel } from './lick.ts';\nexport type { LickEvent, Licks } from './licks.ts';\nexport type { LickSources, WebhookDelivery } from './sources.ts';\n"],
5
+ "mappings": "AAGA,SAAsB,YAAY;AAGlC,SAAS,sBAAsB;AAE/B,SAAS,mBAA+C;AACxD,SAAS,yBAAyD;AASlE,MAAM,YAAY,oBAAI,IAAiB,CAAC,SAAS,CAAC;AAE3C,SAAS,aAAa,OAAmB;AAC9C,SAAO,OAAO,QAAoB,SAAsB,YAAqB;AAC3E,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,WAAW,OAAQ,QAAO;AAC9B,QAAI,OAAO,WAAW,OAAO;AAC3B,aAAQ,MAAM,MAAM,OAAO,aAAa,OAAO,MAAM,QAAQ,MAAM,GAAG,OAAO,KAAM;AACrF,QAAI,UAAU,IAAI,OAAO,EAAG,QAAO;AACnC,UAAM,QAAQ,MAAM,OAAO,MAAM,EAAE,OAAO,MAAM;AAChD,QAAI,CAAC,SAAS,CAAC,KAAK,KAAK,EAAG,QAAO;AACnC,WAAQ,MAAM,MAAM,OAAO,aAAa,QAAQ,OAAO,KAAM;AAAA,EAC/D;AACF;AAkBO,SAAS,WAAW,UAAoB,SAAoC;AACjF,MAAI,OAAkC,MAAM;AAC5C,QAAM,OAAO,IAAI,QAAmB,CAAC,YAAY;AAC/C,WAAO;AAAA,EACT,CAAC;AACD,QAAM,QAAQ,YAAY,IAAI;AAC9B,QAAM,SAAS,OAAO,IAAoB,YAAqB;AAC7D,UAAM,QAAQ,OAAO,MAAM,MAAM,QAAQ,aAAa,IAAI,OAAO;AACjE,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,gBAAgB,EAAE,UAAU;AACxD,WAAO;AAAA,EACT;AACA,WAAS,QAAQ,eAAe,OAAO,MAAM,CAAC;AAC9C,SAAO;AAAA,IACL;AAAA,IACA,OAAO,OAAO,SAAS;AACrB,YAAM,EAAE,SAAS,KAAK,IAAI;AAC1B,WAAK,EAAE,SAAS,SAAS,aAAa,KAAK,EAAE,CAAC;AAC9C,aAAO,kBAAkB;AAAA,QACvB,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA,MAAM,MAAM,MAAM,KAAK;AAAA,QACvB;AAAA,QACA,GAAI,UAAU,EAAE,OAAO,QAAQ,IAAI,CAAC;AAAA,MACtC,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { ROOT_USER } from "../layout.js";
2
2
  import { attachmentPart } from "./attachment.js";
3
- import { bootLicks, LicksHostDoc } from "./boot.js";
3
+ import { booting, LicksHostDoc } from "./boot.js";
4
4
  import { parseWebhook } from "./config.js";
5
5
  import {
6
6
  installJobs,
@@ -86,6 +86,7 @@ function createLickSources(options) {
86
86
  const { harness, env, licks, home } = options;
87
87
  const files = configFiles(env);
88
88
  const deliver = (event, context) => licks.deliver(event, context);
89
+ const boots = booting(harness, options.cone, deliver, options.jobs);
89
90
  const dirsOf = options.dirs ?? (() => [{ dir: `${home}/${SLICC_DIR}`, home, cone: null }]);
90
91
  const watching = /* @__PURE__ */ new Map();
91
92
  const watchesOf = (place) => {
@@ -224,7 +225,7 @@ function createLickSources(options) {
224
225
  }, options.flushEvery ?? 2e3);
225
226
  },
226
227
  reconcile,
227
- boot: (facts, context) => bootLicks(harness, options.cone(), deliver, facts, context),
228
+ boot: boots.boot,
228
229
  async webhook(name, delivery, context) {
229
230
  const entry = webhooks.get(name);
230
231
  if (!entry) return false;
@@ -236,6 +237,7 @@ function createLickSources(options) {
236
237
  },
237
238
  async close(context) {
238
239
  closed = true;
240
+ boots.stop();
239
241
  clearTimeout(reconcileTimer);
240
242
  clearInterval(flushTimer);
241
243
  await control?.close(context);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/licks/sources.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, Harness, TaskId } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { ROOT_USER } from '../layout.ts';\nimport { type Attachment, attachmentPart } from './attachment.ts';\nimport { type BootFacts, bootLicks, LicksHostDoc } from './boot.ts';\nimport { type ConfigError, parseWebhook, type WebhookEntry } from './config.ts';\nimport {\n type CronJob,\n installJobs,\n type LegacyJob,\n legacyJobs,\n relickJobs,\n sendCommand,\n} from './crontab.ts';\nimport { type ConfigFiles, configFiles, Watches } from './fswatch.ts';\nimport type { LickChannel, LickTarget } from './lick.ts';\nimport type { LickEvent, Licks } from './licks.ts';\n\nexport const SLICC_DIR = '.slicc';\n\nexport interface LickSourcesOptions {\n harness: Harness;\n root: Conversation;\n cone: () => Conversation;\n env: ExecutionEnv;\n home: string;\n licks: Licks;\n flushEvery?: number;\n activity?: Activity;\n dirs?: () => readonly LickDir[] | Promise<readonly LickDir[]>;\n cronUser?: (target: LickTarget, place: LickDir) => string | undefined;\n fetch?: typeof fetch;\n}\n\nexport interface LickDir {\n dir: string;\n home: string;\n cone: string | null;\n env?: ExecutionEnv;\n}\n\nexport function scopedTarget(target: LickTarget, cone: string | null): LickTarget {\n return cone && target === 'cone' ? (`cone:${cone}` as LickTarget) : target;\n}\n\nexport function scopedName(name: string, cone: string | null): string {\n return cone ? `${cone}:${name}` : name;\n}\n\nfunction scoped<T extends { name: string; target: LickTarget }>(entry: T, cone: string | null): T {\n return { ...entry, name: scopedName(entry.name, cone), target: scopedTarget(entry.target, cone) };\n}\n\nexport interface WebhookDelivery {\n id?: string;\n headers?: Record<string, string>;\n body: unknown;\n verified?: boolean;\n attachment?: Attachment;\n}\n\nexport async function withAttachment(\n delivery: WebhookDelivery,\n fetcher: typeof fetch | undefined\n): Promise<WebhookDelivery> {\n const { attachment, ...rest } = delivery;\n if (!attachment) return delivery;\n const part = await attachmentPart(attachment, fetcher);\n return { ...rest, body: [rest.body, part].filter(Boolean).join('\\n\\n') };\n}\n\nexport interface LickSources {\n start(context: Context): Promise<void>;\n reconcile(context: Context): Promise<void>;\n boot(facts: BootFacts, context: Context): Promise<void>;\n webhook(name: string, delivery: WebhookDelivery, context: Context): Promise<boolean>;\n close(context: Context): Promise<void>;\n}\n\nexport function hash(text: string): string {\n let value = 2166136261;\n for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);\n return (value >>> 0).toString(36);\n}\n\nexport function shortPath(path: string, home: string): string {\n return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;\n}\n\nexport function configLick(\n error: ConfigError,\n home: string,\n cone: string | null = null\n): LickEvent {\n const file = shortPath(error.file, home);\n const channel: LickChannel = error.file.includes('/watches/')\n ? 'fswatch'\n : error.file.includes('/webhooks/')\n ? 'webhook'\n : 'cron';\n const text = `${error.line ? `line ${error.line}: ` : ''}${error.error}`;\n return {\n channel,\n source: `config:${file}`,\n title: `Invalid ${file}`,\n text,\n body: error.file.endsWith('/crontab.migrated')\n ? \"Its jobs in the kernel crontab still lack --lick=cron, so they don't fire as cron licks; add it with crontab -.\"\n : channel === 'cron'\n ? 'Not moved into the kernel crontab until the file is fixed; the installed crontab is unchanged.\\nThe format is in the slicc-agent README.'\n : 'Ignored until the file is fixed.\\nThe formats are in the slicc-agent README.',\n target: scopedTarget('cone', cone),\n severity: 'error',\n eventId: hash(`${file}\\n${text}`),\n };\n}\n\nexport const UNVERIFIED = \"verified: false (the gateway couldn't check this request's signature)\";\n\nexport function webhookLick(\n name: string,\n entry: WebhookEntry,\n delivery: WebhookDelivery\n): LickEvent {\n const payload =\n typeof delivery.body === 'string' ? delivery.body : JSON.stringify(delivery.body, null, 2);\n const headers = Object.entries(delivery.headers ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join('\\n');\n return {\n channel: 'webhook',\n source: name,\n title: name,\n text: entry.message || `Webhook ${name} received a request`,\n body: [delivery.verified === false ? UNVERIFIED : '', headers, payload]\n .filter(Boolean)\n .join('\\n\\n'),\n target: entry.target,\n coalesce: false,\n ...(delivery.id ? { eventId: delivery.id } : {}),\n ...(entry.triage ? { triage: entry.triage } : {}),\n };\n}\n\nasync function webhooksIn(\n files: ConfigFiles,\n dir: string,\n context: Context\n): Promise<{ entries: Map<string, WebhookEntry>; errors: ConfigError[] }> {\n const errors: ConfigError[] = [];\n const into = new Map<string, WebhookEntry>();\n for (const name of await files.list(dir, context)) {\n if (name.endsWith('.v6')) continue;\n const file = `${dir}/${name}`;\n const json = await files.read(file, context);\n if (json === undefined) continue;\n try {\n const entry = parseWebhook(file, json);\n into.set(entry.name, entry);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n }\n }\n return { entries: into, errors };\n}\n\nexport function createLickSources(options: LickSourcesOptions): LickSources {\n const { harness, env, licks, home } = options;\n const files = configFiles(env);\n const deliver = (event: LickEvent, context: Context) => licks.deliver(event, context);\n const dirsOf = options.dirs ?? (() => [{ dir: `${home}/${SLICC_DIR}`, home, cone: null }]);\n const watching = new Map<string, Watches>();\n const watchesOf = (place: LickDir) => {\n let found = watching.get(place.dir);\n if (!found) {\n const retarget = (event: LickEvent, context: Context) =>\n deliver({ ...event, target: scopedTarget(event.target, place.cone) }, context);\n found = new Watches(place.env ?? env, retarget, place.home, options.activity);\n watching.set(place.dir, found);\n }\n return found;\n };\n let webhooks = new Map<string, WebhookEntry>();\n const relicked = new Set<string>();\n let control: FileWatcher | undefined;\n let controlled = '';\n let reconcileTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n async function reconcileNow(context: Context): Promise<void> {\n if (closed) return;\n const places = await dirsOf();\n const errors: { error: ConfigError; place: LickDir }[] = [];\n const hooks = new Map<string, WebhookEntry>();\n for (const place of places) {\n const crontab = [...(await relick(place, context)), ...(await migrate(place, context))];\n const watchErrors = await watchesOf(place).reconcile(`${place.dir}/watches`, context);\n const found = await webhooksIn(files, `${place.dir}/webhooks`, context);\n for (const entry of found.entries.values()) {\n const named = scoped(entry, place.cone);\n hooks.set(named.name, named);\n }\n for (const error of [...crontab, ...watchErrors, ...found.errors])\n errors.push({ error, place });\n }\n for (const [dir, watches] of [...watching])\n if (!places.some((place) => place.dir === dir)) {\n watching.delete(dir);\n await watches.close(context);\n }\n webhooks = hooks;\n for (const { error, place } of errors)\n await deliver(configLick(error, place.home, place.cone), context);\n await watchControl(places, context);\n }\n\n function byUser(place: LickDir, jobs: readonly LegacyJob[]): Map<string, LegacyJob[]> {\n const users = new Map<string, LegacyJob[]>();\n for (const job of jobs) {\n const user = options.cronUser?.(job.target, place) ?? place.cone ?? ROOT_USER;\n users.set(user, [...(users.get(user) ?? []), job]);\n }\n return users;\n }\n\n async function relick(place: LickDir, context: Context): Promise<ConfigError[]> {\n if (relicked.has(place.dir)) return [];\n relicked.add(place.dir);\n const file = `${place.dir}/crontab.migrated`;\n const text = await files.read(file, context);\n if (text === undefined) return [];\n const errors: ConfigError[] = [];\n for (const [user, list] of byUser(place, legacyJobs(text).jobs))\n for (const problem of await relickJobs(env, user, list, context))\n errors.push({ file, ...problem });\n return errors;\n }\n\n async function migrate(place: LickDir, context: Context): Promise<ConfigError[]> {\n const file = `${place.dir}/crontab`;\n const text = await files.read(file, context);\n if (!text?.trim()) return [];\n const { jobs, problems } = legacyJobs(text);\n if (problems.length) return problems.map((problem) => ({ file, ...problem }));\n const users = new Map<string, CronJob[]>();\n for (const [user, list] of byUser(place, jobs))\n users.set(\n user,\n list.map((job) => ({\n name: job.name,\n schedule: job.schedule,\n command: sendCommand(job.target, job.message),\n line: job.line,\n }))\n );\n const errors: ConfigError[] = [];\n for (const [user, list] of users)\n for (const problem of await installJobs(env, user, list, context))\n errors.push({ file, ...problem });\n if (errors.length) return errors;\n await env.writeFile(`${file}.migrated`, text, context);\n await env.remove(file, { force: true }, context);\n return [];\n }\n\n async function retire(context: Context): Promise<void> {\n const stored = (await harness.snapshot(LicksHostDoc, context))?.cron ?? {};\n const records = Object.values(stored);\n if (!records.length) return;\n for (const record of records)\n await harness.abortTask(record.task as TaskId, context).catch(() => undefined);\n await harness.commit(async (tx) => {\n (await tx.doc(LicksHostDoc)).cron = {};\n }, context);\n }\n\n async function watchControl(places: readonly LickDir[], context: Context): Promise<void> {\n const key = places.map((place) => place.dir).join('\\n');\n if (key === controlled || closed) return;\n controlled = key;\n await control?.close(context);\n const result = await env.watch(\n places.map((place) => ({ path: place.dir, recursive: true })),\n () => scheduleReconcile(context),\n context\n );\n control = result.ok ? result.value : undefined;\n }\n\n const reconcile = (context: Context) => serial(() => reconcileNow(context));\n\n function scheduleReconcile(context: Context): void {\n clearTimeout(reconcileTimer);\n reconcileTimer = setTimeout(() => {\n void reconcile(context).catch(() => undefined);\n }, 300);\n }\n\n async function flushIfWaiting(context: Context): Promise<void> {\n if (await licks.pending(context)) await licks.flush(context);\n }\n\n return {\n async start(context) {\n await licks.flush(context);\n await retire(context);\n await reconcile(context);\n flushTimer = setInterval(() => {\n void flushIfWaiting(context).catch(() => undefined);\n }, options.flushEvery ?? 2000);\n },\n reconcile,\n boot: (facts, context) => bootLicks(harness, options.cone(), deliver, facts, context),\n async webhook(name, delivery, context) {\n const entry = webhooks.get(name);\n if (!entry) return false;\n await deliver(\n webhookLick(name, entry, await withAttachment(delivery, options.fetch)),\n context\n );\n return true;\n },\n async close(context) {\n closed = true;\n clearTimeout(reconcileTimer);\n clearInterval(flushTimer);\n await control?.close(context);\n for (const watches of watching.values()) await watches.close(context);\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, Harness, TaskId } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport type { Jobs } from '../kernel/jobs.ts';\nimport { ROOT_USER } from '../layout.ts';\nimport { type Attachment, attachmentPart } from './attachment.ts';\nimport { type BootFacts, booting, LicksHostDoc } from './boot.ts';\nimport { type ConfigError, parseWebhook, type WebhookEntry } from './config.ts';\nimport {\n type CronJob,\n installJobs,\n type LegacyJob,\n legacyJobs,\n relickJobs,\n sendCommand,\n} from './crontab.ts';\nimport { type ConfigFiles, configFiles, Watches } from './fswatch.ts';\nimport type { LickChannel, LickTarget } from './lick.ts';\nimport type { LickEvent, Licks } from './licks.ts';\n\nexport const SLICC_DIR = '.slicc';\n\nexport interface LickSourcesOptions {\n harness: Harness;\n root: Conversation;\n cone: () => Conversation;\n env: ExecutionEnv;\n home: string;\n licks: Licks;\n flushEvery?: number;\n activity?: Activity;\n jobs?: Jobs;\n dirs?: () => readonly LickDir[] | Promise<readonly LickDir[]>;\n cronUser?: (target: LickTarget, place: LickDir) => string | undefined;\n fetch?: typeof fetch;\n}\n\nexport interface LickDir {\n dir: string;\n home: string;\n cone: string | null;\n env?: ExecutionEnv;\n}\n\nexport function scopedTarget(target: LickTarget, cone: string | null): LickTarget {\n return cone && target === 'cone' ? (`cone:${cone}` as LickTarget) : target;\n}\n\nexport function scopedName(name: string, cone: string | null): string {\n return cone ? `${cone}:${name}` : name;\n}\n\nfunction scoped<T extends { name: string; target: LickTarget }>(entry: T, cone: string | null): T {\n return { ...entry, name: scopedName(entry.name, cone), target: scopedTarget(entry.target, cone) };\n}\n\nexport interface WebhookDelivery {\n id?: string;\n headers?: Record<string, string>;\n body: unknown;\n verified?: boolean;\n attachment?: Attachment;\n}\n\nexport async function withAttachment(\n delivery: WebhookDelivery,\n fetcher: typeof fetch | undefined\n): Promise<WebhookDelivery> {\n const { attachment, ...rest } = delivery;\n if (!attachment) return delivery;\n const part = await attachmentPart(attachment, fetcher);\n return { ...rest, body: [rest.body, part].filter(Boolean).join('\\n\\n') };\n}\n\nexport interface LickSources {\n start(context: Context): Promise<void>;\n reconcile(context: Context): Promise<void>;\n boot(facts: BootFacts, context: Context): Promise<void>;\n webhook(name: string, delivery: WebhookDelivery, context: Context): Promise<boolean>;\n close(context: Context): Promise<void>;\n}\n\nexport function hash(text: string): string {\n let value = 2166136261;\n for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);\n return (value >>> 0).toString(36);\n}\n\nexport function shortPath(path: string, home: string): string {\n return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;\n}\n\nexport function configLick(\n error: ConfigError,\n home: string,\n cone: string | null = null\n): LickEvent {\n const file = shortPath(error.file, home);\n const channel: LickChannel = error.file.includes('/watches/')\n ? 'fswatch'\n : error.file.includes('/webhooks/')\n ? 'webhook'\n : 'cron';\n const text = `${error.line ? `line ${error.line}: ` : ''}${error.error}`;\n return {\n channel,\n source: `config:${file}`,\n title: `Invalid ${file}`,\n text,\n body: error.file.endsWith('/crontab.migrated')\n ? \"Its jobs in the kernel crontab still lack --lick=cron, so they don't fire as cron licks; add it with crontab -.\"\n : channel === 'cron'\n ? 'Not moved into the kernel crontab until the file is fixed; the installed crontab is unchanged.\\nThe format is in the slicc-agent README.'\n : 'Ignored until the file is fixed.\\nThe formats are in the slicc-agent README.',\n target: scopedTarget('cone', cone),\n severity: 'error',\n eventId: hash(`${file}\\n${text}`),\n };\n}\n\nexport const UNVERIFIED = \"verified: false (the gateway couldn't check this request's signature)\";\n\nexport function webhookLick(\n name: string,\n entry: WebhookEntry,\n delivery: WebhookDelivery\n): LickEvent {\n const payload =\n typeof delivery.body === 'string' ? delivery.body : JSON.stringify(delivery.body, null, 2);\n const headers = Object.entries(delivery.headers ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join('\\n');\n return {\n channel: 'webhook',\n source: name,\n title: name,\n text: entry.message || `Webhook ${name} received a request`,\n body: [delivery.verified === false ? UNVERIFIED : '', headers, payload]\n .filter(Boolean)\n .join('\\n\\n'),\n target: entry.target,\n coalesce: false,\n ...(delivery.id ? { eventId: delivery.id } : {}),\n ...(entry.triage ? { triage: entry.triage } : {}),\n };\n}\n\nasync function webhooksIn(\n files: ConfigFiles,\n dir: string,\n context: Context\n): Promise<{ entries: Map<string, WebhookEntry>; errors: ConfigError[] }> {\n const errors: ConfigError[] = [];\n const into = new Map<string, WebhookEntry>();\n for (const name of await files.list(dir, context)) {\n if (name.endsWith('.v6')) continue;\n const file = `${dir}/${name}`;\n const json = await files.read(file, context);\n if (json === undefined) continue;\n try {\n const entry = parseWebhook(file, json);\n into.set(entry.name, entry);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n }\n }\n return { entries: into, errors };\n}\n\nexport function createLickSources(options: LickSourcesOptions): LickSources {\n const { harness, env, licks, home } = options;\n const files = configFiles(env);\n const deliver = (event: LickEvent, context: Context) => licks.deliver(event, context);\n const boots = booting(harness, options.cone, deliver, options.jobs);\n const dirsOf = options.dirs ?? (() => [{ dir: `${home}/${SLICC_DIR}`, home, cone: null }]);\n const watching = new Map<string, Watches>();\n const watchesOf = (place: LickDir) => {\n let found = watching.get(place.dir);\n if (!found) {\n const retarget = (event: LickEvent, context: Context) =>\n deliver({ ...event, target: scopedTarget(event.target, place.cone) }, context);\n found = new Watches(place.env ?? env, retarget, place.home, options.activity);\n watching.set(place.dir, found);\n }\n return found;\n };\n let webhooks = new Map<string, WebhookEntry>();\n const relicked = new Set<string>();\n let control: FileWatcher | undefined;\n let controlled = '';\n let reconcileTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n async function reconcileNow(context: Context): Promise<void> {\n if (closed) return;\n const places = await dirsOf();\n const errors: { error: ConfigError; place: LickDir }[] = [];\n const hooks = new Map<string, WebhookEntry>();\n for (const place of places) {\n const crontab = [...(await relick(place, context)), ...(await migrate(place, context))];\n const watchErrors = await watchesOf(place).reconcile(`${place.dir}/watches`, context);\n const found = await webhooksIn(files, `${place.dir}/webhooks`, context);\n for (const entry of found.entries.values()) {\n const named = scoped(entry, place.cone);\n hooks.set(named.name, named);\n }\n for (const error of [...crontab, ...watchErrors, ...found.errors])\n errors.push({ error, place });\n }\n for (const [dir, watches] of [...watching])\n if (!places.some((place) => place.dir === dir)) {\n watching.delete(dir);\n await watches.close(context);\n }\n webhooks = hooks;\n for (const { error, place } of errors)\n await deliver(configLick(error, place.home, place.cone), context);\n await watchControl(places, context);\n }\n\n function byUser(place: LickDir, jobs: readonly LegacyJob[]): Map<string, LegacyJob[]> {\n const users = new Map<string, LegacyJob[]>();\n for (const job of jobs) {\n const user = options.cronUser?.(job.target, place) ?? place.cone ?? ROOT_USER;\n users.set(user, [...(users.get(user) ?? []), job]);\n }\n return users;\n }\n\n async function relick(place: LickDir, context: Context): Promise<ConfigError[]> {\n if (relicked.has(place.dir)) return [];\n relicked.add(place.dir);\n const file = `${place.dir}/crontab.migrated`;\n const text = await files.read(file, context);\n if (text === undefined) return [];\n const errors: ConfigError[] = [];\n for (const [user, list] of byUser(place, legacyJobs(text).jobs))\n for (const problem of await relickJobs(env, user, list, context))\n errors.push({ file, ...problem });\n return errors;\n }\n\n async function migrate(place: LickDir, context: Context): Promise<ConfigError[]> {\n const file = `${place.dir}/crontab`;\n const text = await files.read(file, context);\n if (!text?.trim()) return [];\n const { jobs, problems } = legacyJobs(text);\n if (problems.length) return problems.map((problem) => ({ file, ...problem }));\n const users = new Map<string, CronJob[]>();\n for (const [user, list] of byUser(place, jobs))\n users.set(\n user,\n list.map((job) => ({\n name: job.name,\n schedule: job.schedule,\n command: sendCommand(job.target, job.message),\n line: job.line,\n }))\n );\n const errors: ConfigError[] = [];\n for (const [user, list] of users)\n for (const problem of await installJobs(env, user, list, context))\n errors.push({ file, ...problem });\n if (errors.length) return errors;\n await env.writeFile(`${file}.migrated`, text, context);\n await env.remove(file, { force: true }, context);\n return [];\n }\n\n async function retire(context: Context): Promise<void> {\n const stored = (await harness.snapshot(LicksHostDoc, context))?.cron ?? {};\n const records = Object.values(stored);\n if (!records.length) return;\n for (const record of records)\n await harness.abortTask(record.task as TaskId, context).catch(() => undefined);\n await harness.commit(async (tx) => {\n (await tx.doc(LicksHostDoc)).cron = {};\n }, context);\n }\n\n async function watchControl(places: readonly LickDir[], context: Context): Promise<void> {\n const key = places.map((place) => place.dir).join('\\n');\n if (key === controlled || closed) return;\n controlled = key;\n await control?.close(context);\n const result = await env.watch(\n places.map((place) => ({ path: place.dir, recursive: true })),\n () => scheduleReconcile(context),\n context\n );\n control = result.ok ? result.value : undefined;\n }\n\n const reconcile = (context: Context) => serial(() => reconcileNow(context));\n\n function scheduleReconcile(context: Context): void {\n clearTimeout(reconcileTimer);\n reconcileTimer = setTimeout(() => {\n void reconcile(context).catch(() => undefined);\n }, 300);\n }\n\n async function flushIfWaiting(context: Context): Promise<void> {\n if (await licks.pending(context)) await licks.flush(context);\n }\n\n return {\n async start(context) {\n await licks.flush(context);\n await retire(context);\n await reconcile(context);\n flushTimer = setInterval(() => {\n void flushIfWaiting(context).catch(() => undefined);\n }, options.flushEvery ?? 2000);\n },\n reconcile,\n boot: boots.boot,\n async webhook(name, delivery, context) {\n const entry = webhooks.get(name);\n if (!entry) return false;\n await deliver(\n webhookLick(name, entry, await withAttachment(delivery, options.fetch)),\n context\n );\n return true;\n },\n async close(context) {\n closed = true;\n boots.stop();\n clearTimeout(reconcileTimer);\n clearInterval(flushTimer);\n await control?.close(context);\n for (const watches of watching.values()) await watches.close(context);\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }