@astrosheep/keiyaku 4.1.1 → 4.1.2

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 (58) hide show
  1. package/build/src/akuma/akuma-errors.d.ts +6 -0
  2. package/build/src/akuma/akuma-errors.js +9 -0
  3. package/build/src/akuma/akuma-handle.d.ts +39 -0
  4. package/build/src/akuma/akuma-handle.js +263 -0
  5. package/build/src/akuma/akuma-observe.d.ts +19 -0
  6. package/build/src/akuma/akuma-observe.js +74 -0
  7. package/build/src/akuma/akuma-product-symbols.d.ts +1 -0
  8. package/build/src/akuma/akuma-product-symbols.js +1 -0
  9. package/build/src/akuma/akuma-product.d.ts +18 -0
  10. package/build/src/akuma/akuma-product.js +154 -0
  11. package/build/src/akuma/akuma.d.ts +23 -70
  12. package/build/src/akuma/akuma.js +17 -433
  13. package/build/src/akuma/body-supervisor.d.ts +26 -0
  14. package/build/src/akuma/body-supervisor.js +89 -0
  15. package/build/src/akuma/body.d.ts +1 -1
  16. package/build/src/akuma/body.js +3 -2
  17. package/build/src/akuma/projection-read.d.ts +17 -0
  18. package/build/src/akuma/projection-read.js +135 -0
  19. package/build/src/akuma/projection.d.ts +1 -18
  20. package/build/src/akuma/projection.js +21 -155
  21. package/build/src/akuma/request-execution.d.ts +6 -0
  22. package/build/src/akuma/request-execution.js +86 -0
  23. package/build/src/akuma/request-serve.d.ts +12 -1
  24. package/build/src/akuma/request-serve.js +2 -102
  25. package/build/src/akuma/{body-turn.d.ts → turn-drive.d.ts} +2 -25
  26. package/build/src/akuma/{body-turn.js → turn-drive.js} +4 -99
  27. package/build/src/cli/commands/akuma.d.ts +0 -6
  28. package/build/src/cli/commands/akuma.js +1 -11
  29. package/build/src/cli/commands/contract-invoke.d.ts +25 -0
  30. package/build/src/cli/commands/contract-invoke.js +193 -0
  31. package/build/src/cli/commands/contract.d.ts +12 -0
  32. package/build/src/cli/commands/contract.js +229 -0
  33. package/build/src/cli/invoke.js +24 -184
  34. package/build/src/cli/parse.d.ts +1 -1
  35. package/build/src/cli/parse.js +8 -222
  36. package/build/src/cli/render/akuma-activity.d.ts +38 -0
  37. package/build/src/cli/render/akuma-activity.js +362 -0
  38. package/build/src/cli/render/akuma.d.ts +1 -1
  39. package/build/src/cli/render/akuma.js +14 -371
  40. package/build/src/cli/render/contract-history.d.ts +2 -0
  41. package/build/src/cli/render/contract-history.js +83 -0
  42. package/build/src/cli/render/contract.d.ts +1 -2
  43. package/build/src/cli/render/contract.js +1 -83
  44. package/build/src/git/reconcile.d.ts +8 -1
  45. package/build/src/git/reconcile.js +13 -301
  46. package/build/src/git/scratch.d.ts +8 -5
  47. package/build/src/git/scratch.js +29 -2
  48. package/build/src/git/terminal-reconcile.d.ts +21 -0
  49. package/build/src/git/terminal-reconcile.js +278 -0
  50. package/build/src/library/contract-bind.d.ts +26 -0
  51. package/build/src/library/contract-bind.js +106 -0
  52. package/build/src/library/contract-operations.d.ts +77 -0
  53. package/build/src/library/contract-operations.js +130 -0
  54. package/build/src/library/contract-types.d.ts +95 -0
  55. package/build/src/library/contract-types.js +1 -0
  56. package/build/src/library/contract.d.ts +11 -136
  57. package/build/src/library/contract.js +9 -222
  58. package/package.json +1 -1
@@ -0,0 +1,6 @@
1
+ import type { AkuId } from "./identity.js";
2
+ export declare class AkumaNotBornError extends Error {
3
+ readonly id: AkuId;
4
+ readonly kind = "akuma-not-born";
5
+ constructor(id: AkuId);
6
+ }
@@ -0,0 +1,9 @@
1
+ export class AkumaNotBornError extends Error {
2
+ id;
3
+ kind = "akuma-not-born";
4
+ constructor(id) {
5
+ super(`Akuma ${id} is not born`);
6
+ this.id = id;
7
+ this.name = "AkumaNotBornError";
8
+ }
9
+ }
@@ -0,0 +1,39 @@
1
+ import { type TellResult } from "./body.js";
2
+ import { type KillEvidence } from "./heart/index.js";
3
+ import { type AkuId } from "./identity.js";
4
+ import { type ActivityHistory } from "./projection.js";
5
+ import type { AkumaCallExecution, AkumaStatus, ForkReceipt, InterruptReceipt } from "./akuma.js";
6
+ import type { WorldRoot } from "../world.js";
7
+ declare const CALL_EXECUTION: unique symbol;
8
+ export declare class AkumaHandle {
9
+ readonly id: AkuId;
10
+ private readonly worldPath;
11
+ readonly [CALL_EXECUTION]?: AkumaCallExecution;
12
+ constructor(id: AkuId, worldPath: WorldRoot, execution?: AkumaCallExecution);
13
+ private get paths();
14
+ status(): Promise<AkumaStatus>;
15
+ history(input?: Readonly<{
16
+ before?: number;
17
+ since?: number;
18
+ limit?: number;
19
+ }>): Promise<ActivityHistory>;
20
+ wait(predicate?: (status: AkumaStatus) => boolean, options?: Readonly<{
21
+ timeoutMs?: number;
22
+ }>): Promise<AkumaStatus>;
23
+ tell(body: string): Promise<TellResult>;
24
+ interrupt(body: string): Promise<InterruptReceipt>;
25
+ fork(input: Readonly<{
26
+ at: string;
27
+ }>): Promise<ForkReceipt>;
28
+ kill(): Promise<KillEvidence>;
29
+ lastAnswer(): Promise<LastAnswer>;
30
+ }
31
+ /** Package-internal provenance retained only by the handle returned from call. */
32
+ export declare function akumaCallExecution(handle: AkumaHandle): AkumaCallExecution | undefined;
33
+ export type LastAnswer = Readonly<{
34
+ kind: "answer";
35
+ answer: string;
36
+ }> | Readonly<{
37
+ kind: "no-answer";
38
+ }>;
39
+ export {};
@@ -0,0 +1,263 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { CONTROL_RESPONSE_MS, wakeRecordedTell } from "./body.js";
3
+ import { HeldAkumaLeash, activitySlice, readForkPoint, readHeart, readKill, readLastAnsweredTurn, readSoul, recordTell, requestPause, requestStop, } from "./heart/index.js";
4
+ import { pathsForAkuId } from "./identity.js";
5
+ import { projectTurns, selectHistory } from "./projection.js";
6
+ import { resolveProviderExecution } from "./providers/index.js";
7
+ import { publishAkuma } from "./publication.js";
8
+ import { spawnAkumaBody } from "./body.js";
9
+ import { AkumaNotBornError } from "./akuma-errors.js";
10
+ import { bornStatus } from "./akuma-observe.js";
11
+ const CALL_EXECUTION = Symbol("akuma-call-execution");
12
+ const POLL_MS = 100;
13
+ const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
14
+ function diagnostic(error) {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+ function defaultWaitComplete(status) {
18
+ return (status.life !== "running" &&
19
+ !status.timeline.entries.some((entry) => entry.kind === "row" && entry.row.kind === "tell" && entry.row.state === "pending"));
20
+ }
21
+ async function takeLeashUntil(paths, deadline) {
22
+ for (;;) {
23
+ const leash = await HeldAkumaLeash.try(paths);
24
+ if (leash !== null)
25
+ return leash;
26
+ if (performance.now() >= deadline)
27
+ return null;
28
+ await wait(Math.min(POLL_MS, Math.max(0, deadline - performance.now())));
29
+ }
30
+ }
31
+ async function recordTellBody(paths, akuma, body, id = randomUUID(), recordedAt = new Date().toISOString()) {
32
+ const admitted = await recordTell(paths, { kind: "tell", id, body, recordedAt });
33
+ if (admitted.kind === "not-born")
34
+ throw new AkumaNotBornError(akuma);
35
+ return { kind: "recorded", tellId: admitted.tell.id };
36
+ }
37
+ async function killAkumaWithRecovery(paths, recover = async () => { }) {
38
+ try {
39
+ const request = await requestStop(paths, new Date().toISOString());
40
+ if (request.kind !== "requested")
41
+ return request.kind;
42
+ const target = request.body;
43
+ const leash = await takeLeashUntil(paths, performance.now() + CONTROL_RESPONSE_MS);
44
+ if ((await readKill(paths, target.sequence)) !== null) {
45
+ leash?.release();
46
+ return "killed";
47
+ }
48
+ if (leash === null) {
49
+ if ((await readKill(paths, target.sequence)) !== null)
50
+ return "killed";
51
+ const body = (await readHeart(paths)).latestBody;
52
+ return body?.sequence === target.sequence && body.hung !== undefined ? "hung" : "unavailable";
53
+ }
54
+ try {
55
+ const settledBody = (await readHeart(paths)).latestBody;
56
+ if (settledBody?.sequence !== target.sequence)
57
+ return (await readKill(paths, target.sequence)) === null ? "unavailable" : "killed";
58
+ if (settledBody.end !== "put-down") {
59
+ await leash.clearStop(paths);
60
+ return "untidy";
61
+ }
62
+ const settled = await leash.settleStop(paths, target.sequence);
63
+ return settled === null ? "unavailable" : "killed";
64
+ }
65
+ finally {
66
+ leash.release();
67
+ }
68
+ }
69
+ finally {
70
+ void recover(paths).catch(() => undefined);
71
+ }
72
+ }
73
+ export class AkumaHandle {
74
+ id;
75
+ worldPath;
76
+ [CALL_EXECUTION];
77
+ constructor(id, worldPath, execution) {
78
+ this.id = id;
79
+ this.worldPath = worldPath;
80
+ if (execution !== undefined)
81
+ this[CALL_EXECUTION] = execution;
82
+ }
83
+ get paths() {
84
+ return pathsForAkuId(this.worldPath, this.id);
85
+ }
86
+ async status() {
87
+ return (await bornStatus(this.paths, this.id, { aperture: "monitoring" })).status;
88
+ }
89
+ async history(input = {}) {
90
+ if (input.before !== undefined && input.since !== undefined) {
91
+ throw new TypeError("Akuma history before and since are mutually exclusive");
92
+ }
93
+ for (const [name, value] of [
94
+ ["before", input.before],
95
+ ["since", input.since],
96
+ ]) {
97
+ if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
98
+ throw new TypeError(`Akuma history ${name} must be a positive safe integer`);
99
+ }
100
+ }
101
+ const limit = input.limit ?? 50;
102
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 5_000) {
103
+ throw new TypeError("Akuma history limit must be a positive safe integer no greater than 5000");
104
+ }
105
+ const slice = await activitySlice(this.paths);
106
+ return selectHistory(projectTurns(slice.rows, {
107
+ lowestRetained: slice.lowestRetained,
108
+ highest: slice.highest,
109
+ }), {
110
+ ...(input.before === undefined ? {} : { before: input.before }),
111
+ ...(input.since === undefined ? {} : { since: input.since }),
112
+ limit,
113
+ });
114
+ }
115
+ async wait(predicate = defaultWaitComplete, options = {}) {
116
+ if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0)) {
117
+ throw new TypeError("Akuma wait timeoutMs must be a nonnegative finite millisecond duration");
118
+ }
119
+ const deadline = options.timeoutMs === undefined ? undefined : performance.now() + options.timeoutMs;
120
+ for (;;) {
121
+ const status = await this.status();
122
+ if (predicate(status) || (deadline !== undefined && performance.now() >= deadline))
123
+ return status;
124
+ await wait(deadline === undefined ? POLL_MS : Math.min(POLL_MS, Math.max(0, deadline - performance.now())));
125
+ }
126
+ }
127
+ async tell(body) {
128
+ const recorded = await recordTellBody(this.paths, this.id, body);
129
+ return await wakeRecordedTell(this.paths, recorded.tellId);
130
+ }
131
+ async interrupt(body) {
132
+ const request = await requestPause(this.paths, new Date().toISOString());
133
+ if (request.kind === "not-born") {
134
+ throw new AkumaNotBornError(this.id);
135
+ }
136
+ let putDown = "was-idle";
137
+ let leash = await HeldAkumaLeash.try(this.paths);
138
+ if (leash === null) {
139
+ leash = await takeLeashUntil(this.paths, performance.now() + CONTROL_RESPONSE_MS);
140
+ putDown = "self-aborted";
141
+ }
142
+ if (leash === null) {
143
+ const body = (await readHeart(this.paths)).latestBody;
144
+ return {
145
+ kind: "unavailable",
146
+ evidence: body?.sequence === request.body.sequence && body.hung !== undefined ? "hung" : "unavailable",
147
+ };
148
+ }
149
+ const settledBody = (await readHeart(this.paths)).latestBody;
150
+ if (settledBody?.sequence === request.body.sequence && settledBody.hung !== undefined) {
151
+ try {
152
+ await leash.clearPause(this.paths);
153
+ }
154
+ finally {
155
+ leash.release();
156
+ }
157
+ return { kind: "unavailable", evidence: "hung" };
158
+ }
159
+ if (settledBody?.sequence !== request.body.sequence || settledBody.end === undefined) {
160
+ try {
161
+ await leash.clearPause(this.paths);
162
+ }
163
+ finally {
164
+ leash.release();
165
+ }
166
+ return { kind: "unavailable", evidence: "untidy" };
167
+ }
168
+ if (request.body.end !== undefined || settledBody.end !== "put-down")
169
+ putDown = "was-idle";
170
+ let recorded;
171
+ try {
172
+ const id = randomUUID();
173
+ const admitted = await leash.recordInterruptTell(this.paths, {
174
+ kind: "tell",
175
+ id,
176
+ body,
177
+ recordedAt: new Date().toISOString(),
178
+ });
179
+ if (admitted.kind === "not-born")
180
+ throw new AkumaNotBornError(this.id);
181
+ recorded = { kind: "recorded", tellId: admitted.tell.id };
182
+ }
183
+ finally {
184
+ leash.release();
185
+ }
186
+ return { kind: "interrupted", putDown, tell: await wakeRecordedTell(this.paths, recorded.tellId) };
187
+ }
188
+ async fork(input) {
189
+ const source = await readSoul(this.paths);
190
+ if (source === null)
191
+ throw new AkumaNotBornError(this.id);
192
+ if (source.id !== this.id)
193
+ throw new Error("Akuma soul does not match its coordinate");
194
+ const adapter = (await resolveProviderExecution(source.provider)).adapter;
195
+ if (adapter.fork === undefined)
196
+ return { kind: "provider-cannot-fork", provider: source.provider.name };
197
+ const point = await readForkPoint(this.paths, input.at);
198
+ if (point === null)
199
+ return { kind: "unknown-history", at: input.at };
200
+ if (point.provider !== source.provider.name)
201
+ throw new Error(`Akuma fork point ${input.at} has a mismatched provider`);
202
+ let childSession;
203
+ try {
204
+ childSession = (await adapter.fork({ session: point.session, at: point.historyId, cwd: point.cwd })).session;
205
+ }
206
+ catch (error) {
207
+ return { kind: "fork-failed", diagnostic: diagnostic(error) };
208
+ }
209
+ const admittedAt = new Date().toISOString();
210
+ const birthSession = {
211
+ provider: point.provider,
212
+ coordinate: childSession,
213
+ cwd: point.cwd,
214
+ options: point.options,
215
+ admittedAt,
216
+ };
217
+ try {
218
+ const child = await publishAkuma({
219
+ worldPath: this.worldPath,
220
+ archetype: source.archetype,
221
+ awaitAsleep: true,
222
+ launch: async (allocated) => {
223
+ (await spawnAkumaBody({
224
+ paths: allocated.paths,
225
+ seed: {
226
+ id: allocated.id,
227
+ archetype: source.archetype,
228
+ ...(source.description === undefined ? {} : { description: source.description }),
229
+ provider: source.provider,
230
+ options: source.options,
231
+ ...(source.readonly === undefined ? {} : { readonly: source.readonly }),
232
+ allowed: source.allowed,
233
+ cwd: source.cwd,
234
+ origin: { kind: "fork", parent: this.id, at: input.at },
235
+ },
236
+ birthSession,
237
+ })).release();
238
+ },
239
+ });
240
+ return { kind: "forked", child: child.id };
241
+ }
242
+ catch (error) {
243
+ return {
244
+ kind: "upstream-forked",
245
+ childSession,
246
+ diagnostic: diagnostic(error),
247
+ };
248
+ }
249
+ }
250
+ async kill() {
251
+ return await killAkumaWithRecovery(this.paths);
252
+ }
253
+ async lastAnswer() {
254
+ const turn = await readLastAnsweredTurn(this.paths);
255
+ return turn?.end?.outcome.kind === "answered"
256
+ ? { kind: "answer", answer: turn.end.outcome.answer }
257
+ : { kind: "no-answer" };
258
+ }
259
+ }
260
+ /** Package-internal provenance retained only by the handle returned from call. */
261
+ export function akumaCallExecution(handle) {
262
+ return handle[CALL_EXECUTION];
263
+ }
@@ -0,0 +1,19 @@
1
+ import { type AkuId, type AkumaPaths } from "./identity.js";
2
+ import { selectHistory, type ActivityHistory, type ActivitySnapshot } from "./projection.js";
3
+ import type { WorldRoot } from "../world.js";
4
+ import type { AkumaListRow, AkumaStatus, UnbornAkumaListRow } from "./akuma.js";
5
+ export declare function fleetListRow(paths: AkumaPaths, expected: AkuId): Promise<AkumaListRow | UnbornAkumaListRow>;
6
+ export type BudgetedStatusObservation = Readonly<{
7
+ status: AkumaStatus;
8
+ ordinarySelected: number;
9
+ }>;
10
+ export declare function bornStatus(paths: AkumaPaths, expected: AkuId, input: Readonly<{
11
+ aperture: "monitoring" | "receipt";
12
+ ordinaryBudget?: number;
13
+ }>): Promise<BudgetedStatusObservation>;
14
+ export declare function readBudgetedStatus(worldPath: WorldRoot, id: AkuId, input: Readonly<{
15
+ aperture: "monitoring" | "receipt";
16
+ ordinaryBudget?: number;
17
+ }>): Promise<BudgetedStatusObservation>;
18
+ export declare function readAkumaBirthCwd(worldPath: WorldRoot, id: AkuId): Promise<string>;
19
+ export { selectHistory, type ActivityHistory, type ActivitySnapshot };
@@ -0,0 +1,74 @@
1
+ import { activitySlice, isHeartAbsent, life, lifeAt, probeLeash, readHeart, readSeal, readSoul, } from "./heart/index.js";
2
+ import { pathsForAkuId } from "./identity.js";
3
+ import { ordinarySnapshotBudget, projectTurns, selectHistory, selectSnapshot, } from "./projection.js";
4
+ import { resolveProviderExecution } from "./providers/index.js";
5
+ import { AkumaNotBornError } from "./akuma-errors.js";
6
+ export async function fleetListRow(paths, expected) {
7
+ const snapshot = await readHeart(paths);
8
+ if (snapshot.soul !== null)
9
+ return await bornListRow(paths, expected, snapshot);
10
+ try {
11
+ if ((await probeLeash(paths)) === "held")
12
+ return { id: expected, life: "unborn" };
13
+ const seal = await readSeal(paths);
14
+ return seal === null ? { id: expected, life: "unborn" } : { id: expected, life: "stillborn", seal };
15
+ }
16
+ catch (error) {
17
+ if (isHeartAbsent(error))
18
+ return { id: expected, life: "unborn" };
19
+ throw error;
20
+ }
21
+ }
22
+ async function bornListRow(paths, expected, snapshot) {
23
+ snapshot ??= await readHeart(paths);
24
+ if (snapshot.soul === null)
25
+ throw new AkumaNotBornError(expected);
26
+ if (snapshot.soul.id !== expected)
27
+ throw new Error("Akuma soul does not match its coordinate");
28
+ const currentLife = life({ leash: await probeLeash(paths), body: snapshot.latestBody, kill: snapshot.latestKill });
29
+ return {
30
+ id: snapshot.soul.id,
31
+ archetype: snapshot.soul.archetype,
32
+ ...(snapshot.soul.description === undefined ? {} : { description: snapshot.soul.description }),
33
+ life: currentLife,
34
+ lifeAt: lifeAt(currentLife, snapshot.latestBody, snapshot.latestKill, snapshot.soul.createdAt),
35
+ lastActivityAt: snapshot.lastActivityAt,
36
+ pending: snapshot.pending.map((tell) => tell.id),
37
+ };
38
+ }
39
+ export async function bornStatus(paths, expected, input) {
40
+ if (input.ordinaryBudget !== undefined && (!Number.isSafeInteger(input.ordinaryBudget) || input.ordinaryBudget < 0))
41
+ throw new TypeError("ordinary budget must be a nonnegative safe integer");
42
+ const snapshot = await readHeart(paths);
43
+ if (snapshot.soul === null)
44
+ throw new AkumaNotBornError(expected);
45
+ const current = await bornListRow(paths, expected, snapshot);
46
+ const resumeUnsupported = current.life === "stranded" &&
47
+ snapshot.latestSession?.provider === snapshot.soul.provider.name &&
48
+ (await resolveProviderExecution(snapshot.soul.provider)).adapter.resume === undefined;
49
+ const slice = await activitySlice(paths);
50
+ const selected = selectSnapshot(projectTurns(slice.rows), {
51
+ aperture: input.aperture,
52
+ budget: ordinarySnapshotBudget(input.ordinaryBudget),
53
+ });
54
+ return {
55
+ status: {
56
+ id: current.id,
57
+ life: current.life,
58
+ ...(snapshot.soul.readonly === undefined ? {} : { readonly: snapshot.soul.readonly }),
59
+ ...(resumeUnsupported ? { strandedReason: "resume-unsupported" } : {}),
60
+ timeline: selected.snapshot,
61
+ },
62
+ ordinarySelected: selected.ordinaryCount,
63
+ };
64
+ }
65
+ export async function readBudgetedStatus(worldPath, id, input) {
66
+ return await bornStatus(pathsForAkuId(worldPath, id), id, input);
67
+ }
68
+ export async function readAkumaBirthCwd(worldPath, id) {
69
+ const soul = await readSoul(pathsForAkuId(worldPath, id));
70
+ if (soul === null)
71
+ throw new AkumaNotBornError(id);
72
+ return soul.cwd;
73
+ }
74
+ export { selectHistory };
@@ -0,0 +1 @@
1
+ export declare const CALL_WITH_CONTEXT: unique symbol;
@@ -0,0 +1 @@
1
+ export const CALL_WITH_CONTEXT = Symbol("akuma-call-with-context");
@@ -0,0 +1,18 @@
1
+ import { AkumaHandle } from "./akuma-handle.js";
2
+ import type { AkumaCallContext, AkumaCallInput, AkumaConfiguration, AkumaList, AkumaListInput } from "./akuma.js";
3
+ import { CALL_WITH_CONTEXT } from "./akuma-product-symbols.js";
4
+ import type { WorldRoot } from "../world.js";
5
+ export declare class Akuma {
6
+ private readonly path;
7
+ private readonly configuration;
8
+ private constructor();
9
+ static of(root: WorldRoot, input?: AkumaConfiguration): Akuma;
10
+ of(input: Readonly<{
11
+ id: string;
12
+ }>): AkumaHandle;
13
+ listArchetypes(): Promise<readonly string[]>;
14
+ call(input: AkumaCallInput): Promise<AkumaHandle>;
15
+ [CALL_WITH_CONTEXT](input: AkumaCallInput, context: AkumaCallContext): Promise<AkumaHandle>;
16
+ list(input?: AkumaListInput): Promise<AkumaList>;
17
+ }
18
+ export declare function callAkumaWithContext(akuma: Akuma, input: AkumaCallInput, context: AkumaCallContext): Promise<AkumaHandle>;
@@ -0,0 +1,154 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readdir, realpath, stat } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { AkumaHandle } from "./akuma-handle.js";
5
+ import { CALL_WITH_CONTEXT } from "./akuma-product-symbols.js";
6
+ import { fleetListRow, readAkumaBirthCwd } from "./akuma-observe.js";
7
+ import { akuIdFromDirectoryName, akumaPaths, akumaRunRoot, archetypeName, parseAkuId } from "./identity.js";
8
+ import { loadArchetype, listArchetypes as readArchetypes } from "./archetype.js";
9
+ import { publishAkuma } from "./publication.js";
10
+ import { spawnAkumaBody } from "./body.js";
11
+ import { injectedBodyRequests, requestBodyCall } from "./requests.js";
12
+ import { decodeAllowedActions, unionAllowedActions } from "./allowed.js";
13
+ import { settings as readSettings } from "../settings.js";
14
+ function callReadonly(value) {
15
+ if (value === undefined)
16
+ return {};
17
+ if (value !== true)
18
+ throw new TypeError("Akuma call readonly must be true");
19
+ return { readonly: true };
20
+ }
21
+ async function canonicalBirthCwd(input) {
22
+ const selected = resolve(input);
23
+ try {
24
+ const canonical = await realpath(selected);
25
+ if (!(await stat(canonical)).isDirectory())
26
+ throw new Error("not a directory");
27
+ return canonical;
28
+ }
29
+ catch {
30
+ throw new Error(`cwd is not an existing directory: ${input}`);
31
+ }
32
+ }
33
+ export class Akuma {
34
+ path;
35
+ configuration;
36
+ constructor(path, configuration) {
37
+ this.path = path;
38
+ this.configuration = configuration;
39
+ }
40
+ static of(root, input = {}) {
41
+ if (typeof root !== "string")
42
+ throw new TypeError("Akuma.of root must be a WorldRoot");
43
+ return new Akuma(root, input);
44
+ }
45
+ of(input) {
46
+ return new AkumaHandle(parseAkuId(input.id).id, this.path);
47
+ }
48
+ async listArchetypes() {
49
+ return readArchetypes(this.configuration.home === undefined ? {} : { home: this.configuration.home });
50
+ }
51
+ async call(input) {
52
+ return await this[CALL_WITH_CONTEXT](input, { initiatorCwd: process.cwd() });
53
+ }
54
+ async [CALL_WITH_CONTEXT](input, context) {
55
+ const readonly = callReadonly(input.readonly);
56
+ const name = archetypeName(input.archetype);
57
+ const home = this.configuration.home === undefined ? {} : { home: this.configuration.home };
58
+ const settings = this.configuration.settings ?? (await readSettings({ root: this.path, ...home }));
59
+ const archetype = await loadArchetype({ name, ...home, settings, ...readonly });
60
+ const allowed = input.allowed === undefined
61
+ ? archetype.allowed
62
+ : unionAllowedActions(archetype.allowed, decodeAllowedActions(input.allowed, "Akuma call allowed"));
63
+ const requests = injectedBodyRequests();
64
+ const requestRecipe = Object.freeze({
65
+ ...(archetype.description === undefined ? {} : { description: archetype.description }),
66
+ provider: archetype.provider,
67
+ options: archetype.options,
68
+ ...(archetype.readonly === undefined ? {} : { readonly: archetype.readonly }),
69
+ allowed,
70
+ });
71
+ if (requests !== null) {
72
+ const cwd = input.cwd === undefined
73
+ ? undefined
74
+ : context?.cwdCanonical === true
75
+ ? input.cwd
76
+ : await canonicalBirthCwd(input.cwd);
77
+ const child = await requestBodyCall({
78
+ directory: requests,
79
+ id: randomUUID(),
80
+ world: this.path,
81
+ archetype: name,
82
+ body: input.body,
83
+ ...(cwd === undefined ? {} : { cwd }),
84
+ recipe: requestRecipe,
85
+ });
86
+ const bornCwd = await readAkumaBirthCwd(this.path, child);
87
+ return new AkumaHandle(child, this.path, { cwd: bornCwd, source: cwd === undefined ? "caller" : "input" });
88
+ }
89
+ const initiatorCwd = context.initiatorCwd;
90
+ const selectedCwd = input.cwd ?? initiatorCwd ?? this.path;
91
+ const cwd = input.cwd !== undefined && context?.cwdCanonical === true ? input.cwd : await canonicalBirthCwd(selectedCwd);
92
+ const published = await publishAkuma({
93
+ worldPath: this.path,
94
+ archetype: archetype.name,
95
+ launch: async (allocated) => (await spawnAkumaBody({
96
+ paths: allocated.paths,
97
+ seed: {
98
+ id: allocated.id,
99
+ archetype: allocated.archetype,
100
+ ...requestRecipe,
101
+ cwd,
102
+ origin: { kind: "direct" },
103
+ },
104
+ initialBody: input.body,
105
+ })).release(),
106
+ });
107
+ return new AkumaHandle(published.id, this.path, {
108
+ cwd,
109
+ source: input.cwd !== undefined ? "input" : initiatorCwd === undefined ? "world" : "process",
110
+ });
111
+ }
112
+ async list(input = {}) {
113
+ if (typeof input !== "object" || input === null || Array.isArray(input))
114
+ throw new TypeError("Akuma list input must be an object");
115
+ const unknown = Object.keys(input).find((key) => key !== "archetype");
116
+ if (unknown !== undefined)
117
+ throw new TypeError(`Akuma list input has unknown field: ${unknown}`);
118
+ const selected = input.archetype === undefined ? undefined : archetypeName(input.archetype);
119
+ const runRoot = akumaRunRoot(this.path);
120
+ let names;
121
+ try {
122
+ names = (await readdir(runRoot, { withFileTypes: true }))
123
+ .filter((entry) => entry.isDirectory())
124
+ .map((entry) => entry.name)
125
+ .sort();
126
+ }
127
+ catch (error) {
128
+ if (error.code === "ENOENT")
129
+ return { rows: [], searched: [runRoot] };
130
+ throw error;
131
+ }
132
+ const rows = [];
133
+ for (const name of names) {
134
+ let physical;
135
+ try {
136
+ physical = akuIdFromDirectoryName(name);
137
+ }
138
+ catch {
139
+ continue;
140
+ }
141
+ if (selected !== undefined && physical.archetype !== selected)
142
+ continue;
143
+ const paths = akumaPaths({ runRoot, archetype: physical.archetype, suffix: physical.suffix });
144
+ try {
145
+ rows.push(await fleetListRow(paths, physical.id));
146
+ }
147
+ catch { }
148
+ }
149
+ return { rows, searched: [runRoot] };
150
+ }
151
+ }
152
+ export async function callAkumaWithContext(akuma, input, context) {
153
+ return await akuma[CALL_WITH_CONTEXT](input, context);
154
+ }