@cjhyy/code-shell-server 0.9.9 → 0.9.11

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.
Files changed (35) hide show
  1. package/dist/index.panels.d.ts +8 -0
  2. package/dist/index.panels.js +8 -0
  3. package/dist/panels/bridge-contract.d.ts +72 -0
  4. package/dist/panels/bridge-contract.js +92 -0
  5. package/dist/panels/connections.d.ts +31 -0
  6. package/dist/panels/connections.js +118 -0
  7. package/dist/panels/process-entry.d.ts +2 -0
  8. package/dist/panels/process-entry.js +52 -0
  9. package/dist/panels/process-service.d.ts +61 -0
  10. package/dist/panels/process-service.js +367 -33
  11. package/dist/panels/process-state.d.ts +58 -0
  12. package/dist/panels/process-state.js +87 -0
  13. package/dist/panels/resources/directories.d.ts +12 -0
  14. package/dist/panels/resources/directories.js +89 -0
  15. package/dist/panels/resources/library.d.ts +63 -0
  16. package/dist/panels/resources/library.js +423 -0
  17. package/dist/panels/resources/service.d.ts +45 -0
  18. package/dist/panels/resources/service.js +317 -0
  19. package/dist/panels/resources/storage.d.ts +10 -0
  20. package/dist/panels/resources/storage.js +102 -0
  21. package/dist/panels/resources/types.d.ts +16 -0
  22. package/dist/panels/resources/types.js +1 -0
  23. package/dist/panels/resources/uploads.d.ts +64 -0
  24. package/dist/panels/resources/uploads.js +527 -0
  25. package/dist/panels/runtime.d.ts +1 -1
  26. package/dist/panels/runtime.js +137 -15
  27. package/dist/panels/tool-executor.d.ts +58 -0
  28. package/dist/panels/tool-executor.js +421 -0
  29. package/dist/panels/tool-jobs-storage.d.ts +16 -0
  30. package/dist/panels/tool-jobs-storage.js +169 -0
  31. package/dist/panels/tool-jobs.d.ts +103 -0
  32. package/dist/panels/tool-jobs.js +530 -0
  33. package/dist/sessions-service.d.ts +2 -0
  34. package/dist/sessions-service.js +8 -0
  35. package/package.json +5 -5
@@ -0,0 +1,421 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, lstat, realpath } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { processLimits } from "./process-state.js";
5
+ import { resourceRelativePath } from "./resources/directories.js";
6
+ import { materializePanelConnections, panelConnectionIds } from "./connections.js";
7
+ function toolInput(value) {
8
+ if (!value ||
9
+ typeof value !== "object" ||
10
+ Array.isArray(value) ||
11
+ Object.keys(value).some((key) => ![
12
+ "request",
13
+ "resources",
14
+ "directoryArguments",
15
+ "connectionIds",
16
+ "connectionArgument",
17
+ ].includes(key)))
18
+ throw new Error("Invalid tool job input");
19
+ const input = value;
20
+ if (!input.request || typeof input.request !== "object" || Array.isArray(input.request))
21
+ throw new Error("Tool request must be an object");
22
+ if (input.resources !== undefined &&
23
+ (!Array.isArray(input.resources) || input.resources.length > 128))
24
+ throw new Error("Too many tool resources");
25
+ const paths = new Set();
26
+ for (const resource of input.resources ?? []) {
27
+ if (!resource ||
28
+ Object.keys(resource).some((key) => !["assetId", "path"].includes(key)) ||
29
+ !/^asset-[a-f0-9]{64}$/.test(resource.assetId))
30
+ throw new Error("Invalid tool resource");
31
+ resourceRelativePath(resource.path);
32
+ if (paths.has(resource.path))
33
+ throw new Error("Duplicate tool resource path");
34
+ paths.add(resource.path);
35
+ }
36
+ if (input.directoryArguments !== undefined &&
37
+ (!Array.isArray(input.directoryArguments) || input.directoryArguments.length > 4))
38
+ throw new Error("Too many tool directories");
39
+ const argumentsSeen = new Set();
40
+ for (const item of input.directoryArguments ?? []) {
41
+ if (!item ||
42
+ !/^--[a-z][a-z0-9-]{0,63}$/.test(item.argumentName) ||
43
+ !["job", "app-data"].includes(item.directory) ||
44
+ Object.keys(item).some((key) => !["argumentName", "directory", "path"].includes(key)))
45
+ throw new Error("Invalid tool directory argument");
46
+ if (argumentsSeen.has(item.argumentName))
47
+ throw new Error("Duplicate tool argument");
48
+ argumentsSeen.add(item.argumentName);
49
+ if (item.path !== undefined)
50
+ resourceRelativePath(item.path);
51
+ }
52
+ if (input.connectionIds !== undefined) {
53
+ panelConnectionIds(input.connectionIds);
54
+ if (!input.connectionArgument ||
55
+ !/^--[a-z][a-z0-9-]{0,63}$/.test(input.connectionArgument) ||
56
+ argumentsSeen.has(input.connectionArgument))
57
+ throw new Error("Invalid connection argument");
58
+ }
59
+ else if (input.connectionArgument !== undefined)
60
+ throw new Error("Connection selection is required");
61
+ return input;
62
+ }
63
+ async function childDirectory(root, relative) {
64
+ let path = await realpath(root);
65
+ for (const part of relative ? resourceRelativePath(relative).split("/") : []) {
66
+ path = join(path, part);
67
+ await mkdir(path, { mode: 0o700 }).catch((error) => {
68
+ if (error.code !== "EEXIST")
69
+ throw error;
70
+ });
71
+ const info = await lstat(path);
72
+ if (!info.isDirectory() || info.isSymbolicLink() || (await realpath(path)) !== path)
73
+ throw new Error("Tool directory changed");
74
+ }
75
+ return path;
76
+ }
77
+ /** The only background processor: launch a reviewed tool and transport bounded JSON/files. */
78
+ export function createPanelToolExecutor(options) {
79
+ return {
80
+ async prepareInput(scope, raw, workDir, signal) {
81
+ const input = toolInput(raw);
82
+ await options.authorize(scope);
83
+ const handle = createHash("sha256").update(workDir).digest("hex");
84
+ try {
85
+ for (const resource of input.resources ?? []) {
86
+ await options.resources.dispatch(scope, "resources.materialize", { ...resource, directoryHandle: handle }, {
87
+ signal,
88
+ resolveDirectory: async () => {
89
+ await options.authorize(scope);
90
+ return workDir;
91
+ },
92
+ });
93
+ }
94
+ if (input.connectionIds)
95
+ await options.authorizeConnections(scope);
96
+ return input;
97
+ }
98
+ finally {
99
+ options.resources.releaseDirectory(scope, handle);
100
+ }
101
+ },
102
+ async execute(job, context) {
103
+ const input = toolInput(job.input);
104
+ await options.authorize(job.scope);
105
+ let stdout = "", outputBytes = 0, result, failed;
106
+ let processId = "", terminal = false, stopped;
107
+ let progress = Promise.resolve();
108
+ let pendingProgress;
109
+ let reportingProgress = false;
110
+ let sequence = 0;
111
+ const pendingEvents = new Map();
112
+ let reconciling;
113
+ let finish;
114
+ const exit = new Promise((resolve) => {
115
+ finish = resolve;
116
+ });
117
+ const stop = () => {
118
+ stopped ??= (async () => {
119
+ if (!processId || terminal)
120
+ return;
121
+ await options.processes.cancel(owner, { processId });
122
+ await exit;
123
+ })();
124
+ return stopped;
125
+ };
126
+ const parse = (line) => {
127
+ if (!line.trim())
128
+ return;
129
+ const message = JSON.parse(line);
130
+ if (!message || typeof message !== "object" || Array.isArray(message))
131
+ throw new Error("Invalid tool response");
132
+ if (message.type === "progress") {
133
+ if (!message.progress ||
134
+ typeof message.progress !== "object" ||
135
+ Array.isArray(message.progress))
136
+ throw new Error("Invalid tool progress");
137
+ pendingProgress = message.progress;
138
+ if (!reportingProgress) {
139
+ reportingProgress = true;
140
+ progress = (async () => {
141
+ while (pendingProgress !== undefined) {
142
+ const update = pendingProgress;
143
+ pendingProgress = undefined;
144
+ await context.reportProgress(update);
145
+ }
146
+ })()
147
+ .catch((error) => {
148
+ failed ??= error instanceof Error ? error : new Error(String(error));
149
+ void stop();
150
+ })
151
+ .finally(() => {
152
+ reportingProgress = false;
153
+ });
154
+ }
155
+ }
156
+ else if (message.type === "result") {
157
+ if (result !== undefined)
158
+ throw new Error("Tool returned multiple results");
159
+ result = message.result;
160
+ }
161
+ else if (message.type === "error") {
162
+ failed ??= Object.assign(new Error(typeof message.message === "string" ? message.message.slice(0, 2000) : "Tool failed"), {
163
+ ...(typeof message.code === "string" ? { code: message.code.slice(0, 100) } : {}),
164
+ ...(typeof message.retryable === "boolean" ? { retryable: message.retryable } : {}),
165
+ });
166
+ void stop();
167
+ }
168
+ else
169
+ throw new Error("Unsupported tool response");
170
+ };
171
+ const consume = (event, payload) => {
172
+ if (event === "process.exit") {
173
+ terminal = true;
174
+ finish({ code: payload.code, signal: payload.signal });
175
+ return;
176
+ }
177
+ if (payload.stream !== "stdout" || typeof payload.text !== "string")
178
+ return;
179
+ try {
180
+ outputBytes += Buffer.byteLength(payload.text);
181
+ if (outputBytes > processLimits.maxOutputBytes)
182
+ throw new Error("Tool output exceeds the limit");
183
+ stdout += payload.text;
184
+ while (stdout.includes("\n")) {
185
+ const end = stdout.indexOf("\n");
186
+ const line = stdout.slice(0, end);
187
+ stdout = stdout.slice(end + 1);
188
+ parse(line);
189
+ }
190
+ if (Buffer.byteLength(stdout) > 256 * 1024)
191
+ throw new Error("Tool response exceeds the limit");
192
+ }
193
+ catch (error) {
194
+ failed ??= error instanceof Error ? error : new Error("Invalid tool output");
195
+ void stop();
196
+ }
197
+ };
198
+ const receive = (event, payload) => {
199
+ if (processId && payload.processId !== processId)
200
+ return;
201
+ if (!Number.isSafeInteger(payload.sequence)) {
202
+ consume(event, payload);
203
+ return;
204
+ }
205
+ const cursor = Number(payload.sequence);
206
+ if (cursor <= sequence)
207
+ return;
208
+ pendingEvents.set(cursor, { event, payload });
209
+ if (pendingEvents.size > processLimits.maxEventsPerProcess) {
210
+ failed ??= new Error("Too many missed tool events");
211
+ void stop();
212
+ return;
213
+ }
214
+ while (pendingEvents.has(sequence + 1)) {
215
+ const item = pendingEvents.get(++sequence);
216
+ pendingEvents.delete(sequence);
217
+ consume(item.event, item.payload);
218
+ }
219
+ };
220
+ const owner = options.owner(job, receive);
221
+ const reconcile = () => {
222
+ if (!processId)
223
+ return Promise.resolve();
224
+ return (reconciling ??= (async () => {
225
+ const receipt = await options.processes.get(owner, {
226
+ processId,
227
+ afterSequence: sequence,
228
+ limit: 256,
229
+ });
230
+ if (!receipt.found)
231
+ throw new Error("Tool process receipt is unavailable");
232
+ if (receipt.truncated)
233
+ throw new Error("Tool output was lost before it could be read");
234
+ for (const item of receipt.events)
235
+ receive(item.event, item.payload);
236
+ })()
237
+ .catch((error) => {
238
+ failed ??= error instanceof Error ? error : new Error(String(error));
239
+ void stop();
240
+ })
241
+ .finally(() => {
242
+ reconciling = undefined;
243
+ }));
244
+ };
245
+ const receiptTimer = setInterval(() => {
246
+ void reconcile();
247
+ }, 200);
248
+ receiptTimer.unref();
249
+ const abort = () => {
250
+ void stop();
251
+ };
252
+ context.signal.addEventListener("abort", abort, { once: true });
253
+ const cleanups = [];
254
+ const resourceDirectoryHandles = [];
255
+ let checking = false;
256
+ const authorizationTimer = setInterval(() => {
257
+ if (checking || terminal)
258
+ return;
259
+ checking = true;
260
+ void options
261
+ .authorize(job.scope)
262
+ .catch(() => {
263
+ failed ??= Object.assign(new Error("Tool task authorization was revoked"), {
264
+ code: "APP_REVOKED",
265
+ retryable: false,
266
+ });
267
+ return stop();
268
+ })
269
+ .finally(() => {
270
+ checking = false;
271
+ });
272
+ }, 1000);
273
+ authorizationTimer.unref();
274
+ try {
275
+ const executable = await options.processes.findExecutable(owner, { name: "node" });
276
+ if (!executable.available || !executable.handle)
277
+ throw new Error("Node.js 20 or newer is required for installed tools");
278
+ const entry = await options.processes.resolveEntry(owner, {
279
+ name: job.entry.name,
280
+ executableHandle: executable.handle,
281
+ });
282
+ if (entry.sha256 !== job.entry.sha256)
283
+ throw new Error("Installed tool changed; start a new job");
284
+ const directory = await options.processes.grantDirectory(owner, context.workDir);
285
+ resourceDirectoryHandles.push(directory.handle);
286
+ const fileArgumentHandles = [];
287
+ for (const item of input.directoryArguments ?? []) {
288
+ const root = item.directory === "job" ? context.workDir : await options.appDataDirectory(job.scope);
289
+ const location = await childDirectory(root, item.path);
290
+ const grant = await options.processes.grantDirectory(owner, location);
291
+ const argument = await options.processes.grantDirectoryArgument(owner, {
292
+ executableHandle: executable.handle,
293
+ argumentName: item.argumentName,
294
+ directoryHandle: grant.handle,
295
+ });
296
+ fileArgumentHandles.push(argument.handle);
297
+ }
298
+ if (input.connectionIds) {
299
+ await options.authorizeConnections(job.scope);
300
+ const sealed = await materializePanelConnections(options.sealedRoot, job.scope.projectPath, input.connectionIds);
301
+ cleanups.push(sealed.cleanup);
302
+ const argument = await options.processes.grantFileArgument(owner, {
303
+ executableHandle: executable.handle,
304
+ argumentName: input.connectionArgument,
305
+ path: sealed.path,
306
+ cleanup: sealed.cleanup,
307
+ });
308
+ fileArgumentHandles.push(argument.handle);
309
+ }
310
+ context.signal.throwIfAborted();
311
+ const launched = await options.processes.start(owner, {
312
+ executableHandle: executable.handle,
313
+ entryHandle: entry.handle,
314
+ directoryHandle: directory.handle,
315
+ fileArgumentHandles,
316
+ args: [],
317
+ stdin: "pipe",
318
+ });
319
+ processId = launched.processId;
320
+ // Capture this Host-only waiter before revocation can retire the owner's grants.
321
+ void options.processes
322
+ .waitForExit(owner, { processId })
323
+ .then((value) => {
324
+ terminal = true;
325
+ finish(value);
326
+ })
327
+ .catch((error) => {
328
+ // This can only be absent after the process has already closed and its
329
+ // owner retired the receipt before the waiter attached.
330
+ failed ??= error instanceof Error ? error : new Error(String(error));
331
+ terminal = true;
332
+ finish({ code: null, signal: null });
333
+ });
334
+ if (failed || context.signal.aborted) {
335
+ stopped = undefined;
336
+ await stop();
337
+ context.signal.throwIfAborted();
338
+ throw failed;
339
+ }
340
+ const request = JSON.stringify({
341
+ ...input.request,
342
+ jobId: job.id,
343
+ scopeKey: createHash("sha256")
344
+ .update(JSON.stringify([job.scope.appId, job.scope.projectPath]))
345
+ .digest("hex"),
346
+ });
347
+ if (Buffer.byteLength(request) > processLimits.maxStdinBytes)
348
+ throw new Error("Tool input exceeds the stdin limit");
349
+ // Slice by characters conservatively, preserving surrogate pairs across writes.
350
+ const characters = Array.from(request);
351
+ for (let i = 0; i < characters.length; i += 3000) {
352
+ context.signal.throwIfAborted();
353
+ await options.processes.write(owner, {
354
+ processId,
355
+ text: characters.slice(i, i + 3000).join(""),
356
+ });
357
+ }
358
+ await options.processes.end(owner, { processId });
359
+ const receipt = await exit;
360
+ await reconciling;
361
+ await reconcile();
362
+ if (stdout.trim())
363
+ parse(stdout);
364
+ await progress;
365
+ context.signal.throwIfAborted();
366
+ if (failed)
367
+ throw failed;
368
+ if (receipt.code !== 0 || result === undefined)
369
+ throw new Error("Tool did not return a completed result");
370
+ const value = result;
371
+ if (!value || typeof value !== "object" || Array.isArray(value))
372
+ throw new Error("Tool result must be an object");
373
+ if (value.artifacts !== undefined) {
374
+ if (!Array.isArray(value.artifacts) || value.artifacts.length > 128)
375
+ throw new Error("Invalid tool output inventory");
376
+ const assets = [];
377
+ for (const artifact of value.artifacts) {
378
+ if (!artifact ||
379
+ typeof artifact !== "object" ||
380
+ !/^[a-f0-9]{64}$/.test(artifact.sha256) ||
381
+ artifact.assetId !== `asset-${artifact.sha256}`)
382
+ throw new Error("Invalid tool artifact identity");
383
+ resourceRelativePath(artifact.file);
384
+ if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0)
385
+ throw new Error("Invalid tool artifact size");
386
+ const captured = await options.resources.dispatch(job.scope, "resources.capture", {
387
+ directoryHandle: directory.handle,
388
+ path: artifact.file,
389
+ name: artifact.name ?? artifact.file.split("/").at(-1),
390
+ mimeType: artifact.mimeType,
391
+ expectedBytes: artifact.bytes,
392
+ expectedSha256: artifact.sha256,
393
+ }, {
394
+ resolveDirectory: (handle) => options.processes.directoryPath(owner, handle),
395
+ signal: context.signal,
396
+ });
397
+ assets.push({ ...artifact, asset: captured.asset });
398
+ }
399
+ value.artifacts = assets;
400
+ }
401
+ await options.authorize(job.scope);
402
+ return value;
403
+ }
404
+ finally {
405
+ clearInterval(receiptTimer);
406
+ clearInterval(authorizationTimer);
407
+ context.signal.removeEventListener("abort", abort);
408
+ if (processId && !terminal) {
409
+ stopped = undefined;
410
+ await stop().catch(() => { });
411
+ }
412
+ options.processes.revokeGuest(owner.guestId);
413
+ for (const handle of resourceDirectoryHandles)
414
+ options.resources.releaseDirectory(job.scope, handle);
415
+ options.releaseOwner(owner);
416
+ for (const cleanup of cleanups)
417
+ cleanup();
418
+ }
419
+ },
420
+ };
421
+ }
@@ -0,0 +1,16 @@
1
+ export declare function toolJobJson<T>(value: T, limit: number): T;
2
+ /** A private Host directory, with each descendant checked before file operations. */
3
+ export declare class ToolJobStorage {
4
+ private readonly configuredRoot;
5
+ private root;
6
+ private identity;
7
+ private nonce;
8
+ private readonly directories;
9
+ constructor(configuredRoot: string);
10
+ initialize(): Promise<void>;
11
+ directory(id?: string, work?: boolean, create?: boolean): Promise<string>;
12
+ read(path: string, limit: number): Promise<unknown>;
13
+ write(id: string, value: unknown, limit: number): Promise<void>;
14
+ close(): Promise<void>;
15
+ remove(id: string): Promise<void>;
16
+ }
@@ -0,0 +1,169 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join } from "node:path";
5
+ export function toolJobJson(value, limit) {
6
+ let text;
7
+ try {
8
+ text = JSON.stringify(value);
9
+ }
10
+ catch {
11
+ /* reject below */
12
+ }
13
+ if (text === undefined || Buffer.byteLength(text) > limit)
14
+ throw new Error("tool job JSON exceeds its limit or is not serializable");
15
+ return JSON.parse(text);
16
+ }
17
+ /** A private Host directory, with each descendant checked before file operations. */
18
+ export class ToolJobStorage {
19
+ configuredRoot;
20
+ root = "";
21
+ identity = "";
22
+ nonce = "";
23
+ directories = new Map();
24
+ constructor(configuredRoot) {
25
+ this.configuredRoot = configuredRoot;
26
+ }
27
+ async initialize() {
28
+ if (!isAbsolute(this.configuredRoot))
29
+ throw new Error("tool job root must be absolute");
30
+ await mkdir(this.configuredRoot, { recursive: true, mode: 0o700 });
31
+ const initial = await lstat(this.configuredRoot);
32
+ if (!initial.isDirectory() || initial.isSymbolicLink())
33
+ throw new Error("invalid tool job root");
34
+ this.root = await realpath(this.configuredRoot);
35
+ this.identity = `${initial.dev}:${initial.ino}`;
36
+ const lockPath = join(this.root, "owner.lock");
37
+ const create = () => open(lockPath, "wx", 0o600);
38
+ let lock;
39
+ try {
40
+ lock = await create();
41
+ }
42
+ catch (error) {
43
+ if (error.code !== "EEXIST")
44
+ throw error;
45
+ const current = (await this.read(lockPath, 1024));
46
+ let alive = false;
47
+ if (Number.isSafeInteger(current.pid) && Number(current.pid) > 1) {
48
+ try {
49
+ process.kill(Number(current.pid), 0);
50
+ alive = true;
51
+ }
52
+ catch (cause) {
53
+ if (cause.code !== "ESRCH")
54
+ alive = true;
55
+ }
56
+ }
57
+ else
58
+ throw new Error("invalid tool job ownership lock", { cause: error });
59
+ if (alive)
60
+ throw new Error("tool job storage is already owned by a running Host", { cause: error });
61
+ await rm(lockPath);
62
+ lock = await create();
63
+ }
64
+ this.nonce = randomUUID();
65
+ try {
66
+ await lock.writeFile(JSON.stringify({ pid: process.pid, nonce: this.nonce }));
67
+ await lock.sync();
68
+ }
69
+ finally {
70
+ await lock.close();
71
+ }
72
+ }
73
+ async directory(id, work = false, create = false) {
74
+ const info = await lstat(this.root);
75
+ if (!info.isDirectory() ||
76
+ info.isSymbolicLink() ||
77
+ `${info.dev}:${info.ino}` !== this.identity ||
78
+ (await realpath(this.root)) !== this.root)
79
+ throw new Error("tool job root was replaced");
80
+ let path = this.root;
81
+ for (const part of id === undefined ? [] : [id, ...(work ? ["work"] : [])]) {
82
+ if (part !== "work" && !/^[a-f0-9-]{36}$/.test(part))
83
+ throw new Error("invalid tool job directory");
84
+ path = join(path, part);
85
+ if (create)
86
+ await mkdir(path, { mode: 0o700 }).catch((error) => {
87
+ if (error.code !== "EEXIST")
88
+ throw error;
89
+ });
90
+ const child = await lstat(path);
91
+ if (!child.isDirectory() || child.isSymbolicLink())
92
+ throw new Error("invalid tool job directory");
93
+ const identity = `${child.dev}:${child.ino}`;
94
+ const expected = this.directories.get(path);
95
+ if (expected && expected !== identity)
96
+ throw new Error("tool job directory was replaced");
97
+ this.directories.set(path, identity);
98
+ }
99
+ return path;
100
+ }
101
+ async read(path, limit) {
102
+ const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
103
+ try {
104
+ const info = await file.stat();
105
+ if (!info.isFile() || info.nlink !== 1 || info.size > limit)
106
+ throw new Error("invalid tool job record");
107
+ const bytes = Buffer.alloc(info.size + 1);
108
+ let count = 0;
109
+ while (count < bytes.length) {
110
+ const { bytesRead } = await file.read(bytes, count, bytes.length - count, count);
111
+ if (!bytesRead)
112
+ break;
113
+ count += bytesRead;
114
+ }
115
+ const after = await file.stat();
116
+ if (count !== info.size ||
117
+ after.size !== info.size ||
118
+ after.mtimeMs !== info.mtimeMs ||
119
+ after.nlink !== 1)
120
+ throw new Error("tool job record changed while reading");
121
+ return JSON.parse(bytes.subarray(0, count).toString("utf8"));
122
+ }
123
+ finally {
124
+ await file.close();
125
+ }
126
+ }
127
+ async write(id, value, limit) {
128
+ const directory = await this.directory(id, false, true);
129
+ const path = join(directory, "job.json");
130
+ const temporary = join(directory, `${randomUUID()}.tmp`);
131
+ const file = await open(temporary, "wx", 0o600);
132
+ try {
133
+ await file.writeFile(JSON.stringify(toolJobJson(value, limit)));
134
+ await file.sync();
135
+ }
136
+ finally {
137
+ await file.close();
138
+ }
139
+ try {
140
+ await this.directory(id);
141
+ await rename(temporary, path);
142
+ const parent = await open(dirname(path), "r").catch(() => null);
143
+ if (parent) {
144
+ await parent.sync().catch(() => { });
145
+ await parent.close();
146
+ }
147
+ }
148
+ finally {
149
+ await rm(temporary, { force: true }).catch(() => { });
150
+ }
151
+ }
152
+ async close() {
153
+ if (!this.nonce)
154
+ return;
155
+ const path = join(await this.directory(), "owner.lock");
156
+ const lock = (await this.read(path, 1024));
157
+ if (lock.nonce === this.nonce)
158
+ await rm(path);
159
+ this.nonce = "";
160
+ this.directories.clear();
161
+ }
162
+ async remove(id) {
163
+ const path = await this.directory(id);
164
+ await rm(path, { recursive: true, force: true });
165
+ for (const key of this.directories.keys())
166
+ if (key === path || key.startsWith(`${path}/`))
167
+ this.directories.delete(key);
168
+ }
169
+ }