@springbrand/agent-runtime 0.1.3-alpha.7 → 0.2.0-alpha.13
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/package.json +4 -3
- package/src/adapter/cloudflare/index.ts +1 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +4 -0
- package/src/adapter/cloudflare/subagent/definition.ts +60 -5
- package/src/adapter/cloudflare/universal-agent/hooks.ts +23 -1
- package/src/adapter/cloudflare/universal-agent/preparation.ts +34 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +7 -7
- package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
- package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
- package/src/db/index.ts +4 -0
- package/src/db/runtime-event-outbox.repo.ts +34 -1
- package/src/db/schema.ts +42 -0
- package/src/db/submission-admission.repo.ts +127 -0
- package/src/db/submission.repo.ts +54 -3
- package/src/index.ts +3 -0
- package/src/kernel/bindings.ts +52 -0
- package/src/kernel/durable-lifecycle.ts +100 -0
- package/src/kernel/public-contracts.ts +11 -0
- package/src/kernel/receipts.ts +1 -0
- package/src/kernel/subagent-runtime.ts +137 -0
- package/src/kernel/submission-authority.ts +114 -0
- package/src/kernel/submission-lifecycle.ts +17 -5
- package/src/lib/prompt.ts +3 -0
- package/src/pi/runtime-adapter/assembly.ts +2 -1
- package/src/pi/runtime-adapter/execution.ts +8 -1
- package/src/pi/runtime-adapter/index.ts +33 -0
- package/src/pi/runtime-adapter/models.ts +9 -0
- package/src/pi/tool/core-host.ts +5 -1
- package/src/pi/tool/core.ts +1 -10
- package/src/pi/tool/index.ts +1 -0
- package/src/pi/tool/mcp.ts +2 -2
- package/src/pi/tool/schedule.ts +30 -18
- package/src/pi/tool/subagent.ts +142 -16
- package/src/pi/tool/workspace-revision.ts +64 -0
- package/src/runtime-agent-context.ts +1 -0
- package/src/runtime-assembler.ts +16 -1
- package/src/runtime-definition.ts +12 -0
- package/src/runtime.ts +427 -46
- package/src/tool-registry.ts +2 -0
- package/src/workspace-versioning.ts +46 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-alpha.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -22,15 +22,16 @@
|
|
|
22
22
|
"@cloudflare/think": "0.15.0",
|
|
23
23
|
"@earendil-works/pi-agent-core": "0.83.0",
|
|
24
24
|
"@earendil-works/pi-ai": "0.83.0",
|
|
25
|
+
"isomorphic-git": "1.38.6",
|
|
25
26
|
"lodash-es": "^4.18.1",
|
|
26
27
|
"zod": "^4.4.3"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
|
-
"agents": "^0.
|
|
30
|
+
"agents": "^0.20.1",
|
|
30
31
|
"ai": "^7.0.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"agents": "0.
|
|
34
|
+
"agents": "0.20.1",
|
|
34
35
|
"ai": "7.0.37"
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -155,6 +155,7 @@ export interface CloudflareSandboxAdapterOptions {
|
|
|
155
155
|
sandboxId: string;
|
|
156
156
|
workspace: WorkspacePort;
|
|
157
157
|
publisher: WorkspacePublishPort;
|
|
158
|
+
onPublishComplete?: () => Promise<void>;
|
|
158
159
|
admission?: SandboxAdmission;
|
|
159
160
|
observer?: SandboxObserver;
|
|
160
161
|
clientFactory?: SandboxClientFactory;
|
|
@@ -829,6 +830,9 @@ export class CloudflareSandboxAdapter implements RuntimeSandboxPort {
|
|
|
829
830
|
await this.writeHydrationManifest(manifest);
|
|
830
831
|
}
|
|
831
832
|
const frozen = this.freezeSyncResult(result);
|
|
833
|
+
if (frozen.files.length > 0 && frozen.failed.length === 0) {
|
|
834
|
+
await this.options.onPublishComplete?.();
|
|
835
|
+
}
|
|
832
836
|
this.emit("sandbox.workspace.published", {
|
|
833
837
|
success: frozen.failed.length === 0,
|
|
834
838
|
files: frozen.files.length,
|
|
@@ -24,6 +24,13 @@ import {
|
|
|
24
24
|
withProviderRetry,
|
|
25
25
|
} from "../../../pi/runtime-adapter/models";
|
|
26
26
|
import { runCloudflareSubAgent } from "./runner";
|
|
27
|
+
import {
|
|
28
|
+
addSubagentUsage,
|
|
29
|
+
decodeSubagentCompletion,
|
|
30
|
+
emptySubagentUsage,
|
|
31
|
+
encodeSubagentCompletion,
|
|
32
|
+
} from "../../../kernel/subagent-runtime";
|
|
33
|
+
import type { RuntimeSubagentUsageMetrics } from "../../../kernel/submission-authority";
|
|
27
34
|
import { createCloudflareSubAgentTools } from "./tools";
|
|
28
35
|
|
|
29
36
|
type RunStatus =
|
|
@@ -44,6 +51,7 @@ interface RunRow {
|
|
|
44
51
|
started_at: number;
|
|
45
52
|
completed_at: number | null;
|
|
46
53
|
progress_json: string | null;
|
|
54
|
+
usage_json: string | null;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
interface ChunkRow {
|
|
@@ -148,9 +156,17 @@ export abstract class CloudflareSubAgent<
|
|
|
148
156
|
error_message TEXT,
|
|
149
157
|
started_at INTEGER NOT NULL,
|
|
150
158
|
completed_at INTEGER,
|
|
151
|
-
progress_json TEXT
|
|
159
|
+
progress_json TEXT,
|
|
160
|
+
usage_json TEXT
|
|
152
161
|
)
|
|
153
162
|
`;
|
|
163
|
+
const runColumns = new Set(
|
|
164
|
+
this.sql<{ name: string }>`PRAGMA table_info(pi_subagent_runs)`
|
|
165
|
+
.map((column) => column.name),
|
|
166
|
+
);
|
|
167
|
+
if (!runColumns.has("usage_json")) {
|
|
168
|
+
this.sql`ALTER TABLE pi_subagent_runs ADD COLUMN usage_json TEXT`;
|
|
169
|
+
}
|
|
154
170
|
this.sql`
|
|
155
171
|
CREATE TABLE IF NOT EXISTS pi_subagent_chunks (
|
|
156
172
|
run_id TEXT NOT NULL,
|
|
@@ -182,7 +198,8 @@ export abstract class CloudflareSubAgent<
|
|
|
182
198
|
error_message,
|
|
183
199
|
started_at,
|
|
184
200
|
completed_at,
|
|
185
|
-
progress_json
|
|
201
|
+
progress_json,
|
|
202
|
+
usage_json
|
|
186
203
|
FROM pi_subagent_runs
|
|
187
204
|
WHERE run_id = ${runId}
|
|
188
205
|
`[0] ?? null
|
|
@@ -240,6 +257,26 @@ export abstract class CloudflareSubAgent<
|
|
|
240
257
|
}
|
|
241
258
|
}
|
|
242
259
|
|
|
260
|
+
private readUsage(runId: string): RuntimeSubagentUsageMetrics {
|
|
261
|
+
const value = this.readRun(runId)?.usage_json;
|
|
262
|
+
if (!value) return emptySubagentUsage();
|
|
263
|
+
const decoded = decodeSubagentCompletion(
|
|
264
|
+
stringifyJson({ usage: parseJson(value) }),
|
|
265
|
+
);
|
|
266
|
+
return decoded?.usage ?? emptySubagentUsage();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private persistUsage(
|
|
270
|
+
runId: string,
|
|
271
|
+
usage: RuntimeSubagentUsageMetrics,
|
|
272
|
+
): void {
|
|
273
|
+
this.sql`
|
|
274
|
+
UPDATE pi_subagent_runs
|
|
275
|
+
SET usage_json = ${stringifyJson(usage)}
|
|
276
|
+
WHERE run_id = ${runId}
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
243
280
|
private persistProgress(
|
|
244
281
|
runId: string,
|
|
245
282
|
progress: Omit<AgentToolProgressSnapshot, "at">,
|
|
@@ -269,14 +306,20 @@ export abstract class CloudflareSubAgent<
|
|
|
269
306
|
for (const subscriber of subscribers) subscriber.close();
|
|
270
307
|
}
|
|
271
308
|
|
|
272
|
-
private completeRun(
|
|
309
|
+
private completeRun(
|
|
310
|
+
runId: string,
|
|
311
|
+
output: unknown,
|
|
312
|
+
usage: RuntimeSubagentUsageMetrics,
|
|
313
|
+
): void {
|
|
273
314
|
const outputJson = stringifyJson(output);
|
|
315
|
+
const summary = encodeSubagentCompletion(output, usage);
|
|
274
316
|
this.sql`
|
|
275
317
|
UPDATE pi_subagent_runs
|
|
276
318
|
SET
|
|
277
319
|
status = 'completed',
|
|
278
320
|
output_json = ${outputJson},
|
|
279
|
-
summary = ${
|
|
321
|
+
summary = ${summary},
|
|
322
|
+
usage_json = ${stringifyJson(usage)},
|
|
280
323
|
error_message = NULL,
|
|
281
324
|
completed_at = ${Date.now()}
|
|
282
325
|
WHERE run_id = ${runId}
|
|
@@ -289,11 +332,15 @@ export abstract class CloudflareSubAgent<
|
|
|
289
332
|
runId: string,
|
|
290
333
|
status: "error" | "aborted",
|
|
291
334
|
error: string,
|
|
335
|
+
usage: RuntimeSubagentUsageMetrics = this.readUsage(runId),
|
|
292
336
|
): void {
|
|
337
|
+
const summary = encodeSubagentCompletion(undefined, usage);
|
|
293
338
|
this.sql`
|
|
294
339
|
UPDATE pi_subagent_runs
|
|
295
340
|
SET
|
|
296
341
|
status = ${status},
|
|
342
|
+
summary = ${summary},
|
|
343
|
+
usage_json = ${stringifyJson(usage)},
|
|
297
344
|
error_message = ${error},
|
|
298
345
|
completed_at = ${Date.now()}
|
|
299
346
|
WHERE run_id = ${runId}
|
|
@@ -315,6 +362,7 @@ export abstract class CloudflareSubAgent<
|
|
|
315
362
|
WHERE run_id = ${runId} AND status = 'starting'
|
|
316
363
|
`;
|
|
317
364
|
|
|
365
|
+
const usage = this.readUsage(runId);
|
|
318
366
|
try {
|
|
319
367
|
const { provider, workspace, loader, outbound } =
|
|
320
368
|
await this.prepareExecution();
|
|
@@ -355,6 +403,10 @@ export abstract class CloudflareSubAgent<
|
|
|
355
403
|
tools,
|
|
356
404
|
signal,
|
|
357
405
|
onEvent: (event) => {
|
|
406
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
407
|
+
addSubagentUsage(usage, event.message.usage);
|
|
408
|
+
this.persistUsage(runId, usage);
|
|
409
|
+
}
|
|
358
410
|
for (const chunk of encoder.encode(event)) {
|
|
359
411
|
this.appendChunk(runId, stringifyJson({
|
|
360
412
|
v: 1,
|
|
@@ -367,12 +419,13 @@ export abstract class CloudflareSubAgent<
|
|
|
367
419
|
if (progress) this.persistProgress(runId, progress);
|
|
368
420
|
},
|
|
369
421
|
});
|
|
370
|
-
this.completeRun(runId, output);
|
|
422
|
+
this.completeRun(runId, output, usage);
|
|
371
423
|
} catch (error) {
|
|
372
424
|
this.failRun(
|
|
373
425
|
runId,
|
|
374
426
|
signal.aborted ? "aborted" : "error",
|
|
375
427
|
signal.aborted ? "SubAgent run aborted" : errorMessage(error),
|
|
428
|
+
usage,
|
|
376
429
|
);
|
|
377
430
|
}
|
|
378
431
|
}
|
|
@@ -413,6 +466,7 @@ export abstract class CloudflareSubAgent<
|
|
|
413
466
|
agent_type,
|
|
414
467
|
status,
|
|
415
468
|
input_json,
|
|
469
|
+
usage_json,
|
|
416
470
|
started_at
|
|
417
471
|
)
|
|
418
472
|
VALUES (
|
|
@@ -420,6 +474,7 @@ export abstract class CloudflareSubAgent<
|
|
|
420
474
|
${type.name},
|
|
421
475
|
'starting',
|
|
422
476
|
${inputJson},
|
|
477
|
+
${stringifyJson(emptySubagentUsage())},
|
|
423
478
|
${startedAt}
|
|
424
479
|
)
|
|
425
480
|
`;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
RuntimeSubmissionAdmissionHook,
|
|
3
|
+
RuntimeTurnEventsPort,
|
|
4
|
+
} from "../../../kernel/bindings";
|
|
2
5
|
import type { RuntimeAgentHooks } from "../../../runtime-definition";
|
|
3
6
|
|
|
4
7
|
export function createUniversalAgentHooks<
|
|
@@ -9,6 +12,7 @@ export function createUniversalAgentHooks<
|
|
|
9
12
|
>(adapter: {
|
|
10
13
|
guard(config: Config): Promise<void>;
|
|
11
14
|
turnEvents(): RuntimeTurnEventsPort | Promise<RuntimeTurnEventsPort>;
|
|
15
|
+
onSubmissionAdmission?: RuntimeSubmissionAdmissionHook;
|
|
12
16
|
}): RuntimeAgentHooks<Env, Config, Command, Change> {
|
|
13
17
|
const turnEvents = () => Promise.resolve(adapter.turnEvents());
|
|
14
18
|
return {
|
|
@@ -19,6 +23,24 @@ export function createUniversalAgentHooks<
|
|
|
19
23
|
onModelUsage: async (event) => {
|
|
20
24
|
await (await turnEvents()).onModelUsage?.(event);
|
|
21
25
|
},
|
|
26
|
+
...(adapter.onSubmissionAdmission
|
|
27
|
+
? { onSubmissionAdmission: adapter.onSubmissionAdmission }
|
|
28
|
+
: {}),
|
|
29
|
+
// 一个不订阅这两类事实的 Host 不能把 outbox 堵住:Runtime 只有拿到确认才
|
|
30
|
+
// 会标记投递,而 `undefined` 表示"没投出去"。因此没有 Host 实现时这里就地确认,
|
|
31
|
+
// 语义等同于旧版本"Host 不关心就丢弃",而不是让整条队列在队头无限重试。
|
|
32
|
+
onSubagentUsage: async (event) => {
|
|
33
|
+
const port = await turnEvents();
|
|
34
|
+
return port.onSubagentUsage
|
|
35
|
+
? port.onSubagentUsage(event)
|
|
36
|
+
: { confirmed: true };
|
|
37
|
+
},
|
|
38
|
+
onLifecycleFact: async (event) => {
|
|
39
|
+
const port = await turnEvents();
|
|
40
|
+
return port.onLifecycleFact
|
|
41
|
+
? port.onLifecycleFact(event)
|
|
42
|
+
: { confirmed: true };
|
|
43
|
+
},
|
|
22
44
|
onToolSettled: async (event) => {
|
|
23
45
|
await (await turnEvents()).onToolSettled?.(event);
|
|
24
46
|
},
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
RuntimeSubagentPort,
|
|
11
11
|
WorkspacePort,
|
|
12
12
|
} from "../../../kernel/bindings";
|
|
13
|
+
import { decodeSubagentCompletion } from "../../../kernel/subagent-runtime";
|
|
13
14
|
import type { RuntimeDegradation } from "../../../kernel/degradation";
|
|
14
15
|
import type { RuntimeMemoryProfile } from "../../../kernel/profile";
|
|
15
16
|
import { AGENT_TYPES } from "../../../layers/orchestration/subagents/agent-types/registry";
|
|
@@ -193,9 +194,33 @@ export async function prepareSubagents<Env extends Cloudflare.Env>(
|
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
const subagents: RuntimeSubagentPort = {
|
|
197
|
+
reserve: async (_agentType, _input, options) => {
|
|
198
|
+
if (!options?.submissionId || !options.toolCallId) {
|
|
199
|
+
throw new Error("Durable SubAgent reservation identity is required");
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
runId: `${options.submissionId}:subagent:${options.toolCallId}`,
|
|
203
|
+
};
|
|
204
|
+
},
|
|
196
205
|
run: async (agentType, input, options) => {
|
|
197
206
|
const runOptions: RunAgentToolOptions<unknown> = {
|
|
198
207
|
input: { agentType, ...input },
|
|
208
|
+
...(options?.subagentRunId ? { runId: options.subagentRunId } : {}),
|
|
209
|
+
...(options?.submissionId && options.parentRunId
|
|
210
|
+
? {
|
|
211
|
+
inputPreview: {
|
|
212
|
+
submissionId: options.submissionId,
|
|
213
|
+
parentRunId: options.parentRunId,
|
|
214
|
+
...(options.accountId ? { accountId: options.accountId } : {}),
|
|
215
|
+
...(options.rateVersion !== undefined
|
|
216
|
+
? { rateVersion: options.rateVersion }
|
|
217
|
+
: {}),
|
|
218
|
+
...(options.slotIdentity
|
|
219
|
+
? { slotIdentity: options.slotIdentity }
|
|
220
|
+
: {}),
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
: {}),
|
|
199
224
|
};
|
|
200
225
|
if (options?.detached) {
|
|
201
226
|
runOptions.detached = { maxBudgetMs: options.maxBudgetMs };
|
|
@@ -204,11 +229,16 @@ export async function prepareSubagents<Env extends Cloudflare.Env>(
|
|
|
204
229
|
}
|
|
205
230
|
}
|
|
206
231
|
const result = await context.runAgentTool(agentClass, runOptions);
|
|
232
|
+
const completion = decodeSubagentCompletion(result.summary);
|
|
207
233
|
return {
|
|
208
234
|
status: result.status,
|
|
209
235
|
runId: result.runId,
|
|
210
236
|
output: result.output,
|
|
211
237
|
error: result.error,
|
|
238
|
+
...(result.childStillRunning !== undefined
|
|
239
|
+
? { childStillRunning: result.childStillRunning }
|
|
240
|
+
: {}),
|
|
241
|
+
...(completion ? { usage: completion.usage } : {}),
|
|
212
242
|
};
|
|
213
243
|
},
|
|
214
244
|
};
|
|
@@ -228,6 +258,7 @@ export async function prepareSandbox(options: {
|
|
|
228
258
|
sessionId: string;
|
|
229
259
|
workspaceLoader: WorkspaceLoader;
|
|
230
260
|
publisher: WorkspacePublishPort;
|
|
261
|
+
onPublishComplete?: () => Promise<void>;
|
|
231
262
|
admission?: SandboxAdmission;
|
|
232
263
|
observer?: SandboxObserver;
|
|
233
264
|
}) {
|
|
@@ -265,6 +296,9 @@ export async function prepareSandbox(options: {
|
|
|
265
296
|
sandboxId: await sandboxIdForSession(options.userId, options.sessionId),
|
|
266
297
|
workspace,
|
|
267
298
|
publisher: options.publisher,
|
|
299
|
+
...(options.onPublishComplete
|
|
300
|
+
? { onPublishComplete: options.onPublishComplete }
|
|
301
|
+
: {}),
|
|
268
302
|
...(options.admission ? { admission: options.admission } : {}),
|
|
269
303
|
...(options.observer ? { observer: options.observer } : {}),
|
|
270
304
|
}),
|
|
@@ -8,9 +8,10 @@ import type {
|
|
|
8
8
|
} from "../../../kernel/bindings";
|
|
9
9
|
import type { RuntimeDegradation } from "../../../kernel/degradation";
|
|
10
10
|
import type { RuntimeMemoryProfile } from "../../../kernel/profile";
|
|
11
|
+
import type { WorkspaceRevisionRestorePort } from "../../../workspace-versioning";
|
|
11
12
|
import { createCodeExecutionTool } from "../../../pi/tool/core";
|
|
12
13
|
import { createScheduleTools } from "../../../pi/tool/schedule";
|
|
13
|
-
import {
|
|
14
|
+
import { createWorkspaceRevisionTools } from "../../../pi/tool/workspace-revision";
|
|
14
15
|
import {
|
|
15
16
|
createSandboxTools,
|
|
16
17
|
createWorkspaceTools,
|
|
@@ -34,6 +35,7 @@ export function selectTools(
|
|
|
34
35
|
export function assembleUniversalAgentTools(options: {
|
|
35
36
|
hostTools?: ToolRegistry;
|
|
36
37
|
workspace?: WorkspacePort;
|
|
38
|
+
workspaceRevisions?: WorkspaceRevisionRestorePort;
|
|
37
39
|
codeExecution?: RuntimeCodeExecutionPort;
|
|
38
40
|
sandbox?: RuntimeSandboxPort;
|
|
39
41
|
schedule?: RuntimeSchedulePort;
|
|
@@ -47,17 +49,14 @@ export function assembleUniversalAgentTools(options: {
|
|
|
47
49
|
const tools = mergeToolRegistries(
|
|
48
50
|
options.hostTools ?? {},
|
|
49
51
|
options.workspace ? createWorkspaceTools(options.workspace) : {},
|
|
52
|
+
options.workspaceRevisions
|
|
53
|
+
? createWorkspaceRevisionTools(options.workspaceRevisions)
|
|
54
|
+
: {},
|
|
50
55
|
options.codeExecution
|
|
51
56
|
? createCodeExecutionTool(options.codeExecution)
|
|
52
57
|
: {},
|
|
53
58
|
options.sandbox ? createSandboxTools(options.sandbox) : {},
|
|
54
59
|
options.schedule ? createScheduleTools(options.schedule) : {},
|
|
55
|
-
options.subagents
|
|
56
|
-
? createSubagentTools(
|
|
57
|
-
options.subagents,
|
|
58
|
-
options.enabledSubagents ?? [],
|
|
59
|
-
)
|
|
60
|
-
: {},
|
|
61
60
|
);
|
|
62
61
|
|
|
63
62
|
return {
|
|
@@ -65,6 +64,7 @@ export function assembleUniversalAgentTools(options: {
|
|
|
65
64
|
bindings: {
|
|
66
65
|
...(options.workspace ? { workspace: options.workspace } : {}),
|
|
67
66
|
...(options.memory ? { memory: options.memory } : {}),
|
|
67
|
+
...(options.subagents ? { subagents: options.subagents } : {}),
|
|
68
68
|
},
|
|
69
69
|
enabledSubagents: options.enabledSubagents ?? [],
|
|
70
70
|
memoryProfile: options.memoryProfile,
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from @cloudflare/shell.
|
|
3
|
+
* MIT License Copyright (c) 2025 Cloudflare, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
* in the Software without restriction, including without limitation the rights
|
|
8
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
* furnished to do so, subject to the following conditions:
|
|
11
|
+
*
|
|
12
|
+
* The above copyright notice and this permission notice shall be included in
|
|
13
|
+
* all copies or substantial portions of the Software.
|
|
14
|
+
*
|
|
15
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
* SOFTWARE.
|
|
22
|
+
*/
|
|
23
|
+
import type {
|
|
24
|
+
FsClient,
|
|
25
|
+
PromiseFsClient,
|
|
26
|
+
} from "isomorphic-git";
|
|
27
|
+
import type {
|
|
28
|
+
WorkspaceFileInfo,
|
|
29
|
+
WorkspacePort,
|
|
30
|
+
} from "../../../kernel/bindings";
|
|
31
|
+
|
|
32
|
+
const PAGE_SIZE = 256;
|
|
33
|
+
|
|
34
|
+
class GitStat {
|
|
35
|
+
readonly size: number;
|
|
36
|
+
readonly mtime: Date;
|
|
37
|
+
readonly mtimeMs: number;
|
|
38
|
+
readonly ctimeMs: number;
|
|
39
|
+
readonly mode: number;
|
|
40
|
+
readonly ino = 0;
|
|
41
|
+
readonly uid = 0;
|
|
42
|
+
readonly gid = 0;
|
|
43
|
+
readonly dev = 0;
|
|
44
|
+
|
|
45
|
+
constructor(private readonly info: WorkspaceFileInfo) {
|
|
46
|
+
this.size = info.size;
|
|
47
|
+
this.mtime = new Date(info.updatedAt);
|
|
48
|
+
this.mtimeMs = info.updatedAt;
|
|
49
|
+
this.ctimeMs = info.createdAt;
|
|
50
|
+
this.mode = info.type === "directory"
|
|
51
|
+
? 0o40755
|
|
52
|
+
: info.type === "symlink"
|
|
53
|
+
? 0o120000
|
|
54
|
+
: 0o100644;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
isFile() {
|
|
58
|
+
return this.info.type === "file";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
isDirectory() {
|
|
62
|
+
return this.info.type === "directory";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
isSymbolicLink() {
|
|
66
|
+
return this.info.type === "symlink";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fsError(path: string, cause?: unknown): Error & { code: string } {
|
|
71
|
+
if (
|
|
72
|
+
cause instanceof Error &&
|
|
73
|
+
"code" in cause &&
|
|
74
|
+
typeof (cause as { code: unknown }).code === "string"
|
|
75
|
+
) return cause as Error & { code: string };
|
|
76
|
+
|
|
77
|
+
const error = new Error(
|
|
78
|
+
cause instanceof Error ? cause.message : `ENOENT: ${path}`,
|
|
79
|
+
) as Error & { code: string };
|
|
80
|
+
error.code = cause === undefined ? "ENOENT" : "EIO";
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readAll(workspace: WorkspacePort, path: string) {
|
|
85
|
+
const entries: WorkspaceFileInfo[] = [];
|
|
86
|
+
let offset = 0;
|
|
87
|
+
while (true) {
|
|
88
|
+
const page = await workspace.readDir(path, { limit: PAGE_SIZE, offset });
|
|
89
|
+
if (page.length === 0) return entries;
|
|
90
|
+
entries.push(...page);
|
|
91
|
+
offset += page.length;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createWorkspaceGitFs(workspace: WorkspacePort): FsClient {
|
|
96
|
+
const promises: PromiseFsClient["promises"] = {
|
|
97
|
+
async readFile(
|
|
98
|
+
path: string,
|
|
99
|
+
options?: { encoding?: string } | string,
|
|
100
|
+
): Promise<Uint8Array | string> {
|
|
101
|
+
const encoding = typeof options === "string" ? options : options?.encoding;
|
|
102
|
+
try {
|
|
103
|
+
const value = encoding === "utf8" || encoding === "utf-8"
|
|
104
|
+
? await workspace.readFile(path)
|
|
105
|
+
: await workspace.readFileBytes(path);
|
|
106
|
+
if (value === null) throw fsError(path);
|
|
107
|
+
return value;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw fsError(path, error);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
async writeFile(path: string, data: string | Uint8Array): Promise<void> {
|
|
113
|
+
const parent = path.replace(/\/[^/]+$/u, "");
|
|
114
|
+
if (parent && parent !== "/" && parent !== path) {
|
|
115
|
+
await workspace.mkdir(parent, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
if (typeof data === "string") await workspace.writeFile(path, data);
|
|
118
|
+
else await workspace.writeFileBytes(path, data);
|
|
119
|
+
},
|
|
120
|
+
async unlink(path: string): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await workspace.rm(path);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw fsError(path, error);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async readdir(path: string): Promise<string[]> {
|
|
128
|
+
try {
|
|
129
|
+
const info = await workspace.stat(path);
|
|
130
|
+
if (!info || info.type !== "directory") throw fsError(path);
|
|
131
|
+
return (await readAll(workspace, path)).map(({ name }) => name);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw fsError(path, error);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
async mkdir(
|
|
137
|
+
path: string,
|
|
138
|
+
mode?: number | { recursive?: boolean },
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
await workspace.mkdir(path, {
|
|
141
|
+
recursive: typeof mode === "object" ? mode.recursive : false,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
async rmdir(path: string): Promise<void> {
|
|
145
|
+
await workspace.rm(path);
|
|
146
|
+
},
|
|
147
|
+
async stat(path: string): Promise<GitStat> {
|
|
148
|
+
try {
|
|
149
|
+
const info = await workspace.stat(path);
|
|
150
|
+
if (!info) throw fsError(path);
|
|
151
|
+
return new GitStat(info);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw fsError(path, error);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
async lstat(path: string): Promise<GitStat> {
|
|
157
|
+
try {
|
|
158
|
+
const info = await workspace.lstat(path);
|
|
159
|
+
if (!info) throw fsError(path);
|
|
160
|
+
return new GitStat(info);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw fsError(path, error);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
async readlink(path: string): Promise<string> {
|
|
166
|
+
try {
|
|
167
|
+
return await workspace.readlink(path);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw fsError(path, error);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
async symlink(target: string, path: string): Promise<void> {
|
|
173
|
+
await workspace.symlink(target, path);
|
|
174
|
+
},
|
|
175
|
+
async chmod(): Promise<void> {},
|
|
176
|
+
};
|
|
177
|
+
return { promises };
|
|
178
|
+
}
|