@fungi.computer/agar 0.0.0-stage → 0.0.1

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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +766 -2
  3. package/dist/agent-message-meta.d.ts +74 -0
  4. package/dist/agent-message-meta.js +63 -0
  5. package/dist/agents.d.ts +170 -0
  6. package/dist/agents.js +218 -0
  7. package/dist/app-build-contract.d.ts +43 -0
  8. package/dist/app-build-contract.js +24 -0
  9. package/dist/apps.d.ts +66 -0
  10. package/dist/apps.js +47 -0
  11. package/dist/backend-declaration.d.ts +6 -0
  12. package/dist/backend-declaration.js +65 -0
  13. package/dist/backend-definition.d.ts +88 -0
  14. package/dist/backend-definition.js +158 -0
  15. package/dist/backend-refusal.d.ts +10 -0
  16. package/dist/backend-refusal.js +10 -0
  17. package/dist/backend.d.ts +30 -0
  18. package/dist/backend.js +28 -0
  19. package/dist/capabilities.d.ts +30 -0
  20. package/dist/capabilities.js +50 -0
  21. package/dist/channels.d.ts +142 -0
  22. package/dist/channels.js +97 -0
  23. package/dist/commands.d.ts +33 -0
  24. package/dist/commands.js +46 -0
  25. package/dist/computer-contracts.d.ts +73 -0
  26. package/dist/computer-contracts.js +57 -0
  27. package/dist/computer.d.ts +294 -0
  28. package/dist/computer.js +319 -0
  29. package/dist/connect.d.ts +11 -0
  30. package/dist/connect.js +211 -0
  31. package/dist/errors.d.ts +34 -0
  32. package/dist/errors.js +106 -0
  33. package/dist/failure.d.ts +2 -0
  34. package/dist/failure.js +9 -0
  35. package/dist/files.d.ts +25 -0
  36. package/dist/files.js +84 -0
  37. package/dist/game-servers.d.ts +530 -0
  38. package/dist/game-servers.js +244 -0
  39. package/dist/gills.d.ts +43 -0
  40. package/dist/gills.js +174 -0
  41. package/dist/git-commit.d.ts +143 -0
  42. package/dist/git-commit.js +251 -0
  43. package/dist/git-rpc.d.ts +310 -0
  44. package/dist/git-rpc.js +166 -0
  45. package/dist/git-steps.d.ts +579 -0
  46. package/dist/git-steps.js +804 -0
  47. package/dist/git-wire.d.ts +33 -0
  48. package/dist/git-wire.js +240 -0
  49. package/dist/git.d.ts +322 -0
  50. package/dist/git.js +98 -0
  51. package/dist/host.d.ts +97 -0
  52. package/dist/host.js +732 -0
  53. package/dist/inbox.d.ts +99 -0
  54. package/dist/inbox.js +184 -0
  55. package/dist/index.d.ts +31 -0
  56. package/dist/index.js +15 -0
  57. package/dist/inference.d.ts +17 -0
  58. package/dist/inference.js +115 -0
  59. package/dist/internal/app-error.d.ts +8 -0
  60. package/dist/internal/app-error.js +36 -0
  61. package/dist/internal/app-session.d.ts +8 -0
  62. package/dist/internal/app-session.js +551 -0
  63. package/dist/internal/back-inputs.d.ts +22 -0
  64. package/dist/internal/back-inputs.js +93 -0
  65. package/dist/internal/browser-bridge.d.ts +30 -0
  66. package/dist/internal/browser-bridge.js +203 -0
  67. package/dist/internal/capabilities.d.ts +13 -0
  68. package/dist/internal/capabilities.js +138 -0
  69. package/dist/internal/commands.d.ts +9 -0
  70. package/dist/internal/commands.js +257 -0
  71. package/dist/internal/disconnect-host.d.ts +7 -0
  72. package/dist/internal/disconnect-host.js +19 -0
  73. package/dist/internal/git-edit-tree.d.ts +6 -0
  74. package/dist/internal/git-edit-tree.js +46 -0
  75. package/dist/internal/memory-fs.d.ts +39 -0
  76. package/dist/internal/memory-fs.js +154 -0
  77. package/dist/internal/protocol.d.ts +116 -0
  78. package/dist/internal/protocol.js +6 -0
  79. package/dist/internal/query-feed.d.ts +23 -0
  80. package/dist/internal/query-feed.js +111 -0
  81. package/dist/internal/result.d.ts +22 -0
  82. package/dist/internal/result.js +61 -0
  83. package/dist/internal/rpc-contract.d.ts +24 -0
  84. package/dist/internal/rpc-contract.js +78 -0
  85. package/dist/internal/schemas.d.ts +307 -0
  86. package/dist/internal/schemas.js +524 -0
  87. package/dist/internal/store.d.ts +8 -0
  88. package/dist/internal/store.js +30 -0
  89. package/dist/internal/terminal-gate.d.ts +17 -0
  90. package/dist/internal/terminal-gate.js +94 -0
  91. package/dist/lifecycle.d.ts +44 -0
  92. package/dist/lifecycle.js +1 -0
  93. package/dist/marketplace.d.ts +159 -0
  94. package/dist/marketplace.js +102 -0
  95. package/dist/media-json.d.ts +181 -0
  96. package/dist/media-json.js +204 -0
  97. package/dist/media-library.d.ts +11 -0
  98. package/dist/media-library.js +15 -0
  99. package/dist/media-request-id.d.ts +7 -0
  100. package/dist/media-request-id.js +16 -0
  101. package/dist/media.d.ts +354 -0
  102. package/dist/media.js +654 -0
  103. package/dist/method-refusal.d.ts +64 -0
  104. package/dist/method-refusal.js +48 -0
  105. package/dist/pending.d.ts +10 -0
  106. package/dist/pending.js +10 -0
  107. package/dist/query.d.ts +76 -0
  108. package/dist/query.js +195 -0
  109. package/dist/react-mount.d.ts +23 -0
  110. package/dist/react-mount.js +108 -0
  111. package/dist/react.d.ts +41 -0
  112. package/dist/react.js +111 -0
  113. package/dist/read-copy.d.ts +103 -0
  114. package/dist/read-copy.js +195 -0
  115. package/dist/roles/files.d.ts +295 -0
  116. package/dist/roles/files.js +184 -0
  117. package/dist/root.d.ts +21 -0
  118. package/dist/root.js +1 -0
  119. package/dist/rpc.d.ts +1 -0
  120. package/dist/rpc.js +1 -0
  121. package/dist/status.d.ts +17 -0
  122. package/dist/status.js +50 -0
  123. package/dist/storage-wire.d.ts +167 -0
  124. package/dist/storage-wire.js +115 -0
  125. package/dist/storage.d.ts +72 -0
  126. package/dist/storage.js +143 -0
  127. package/dist/store.d.ts +11 -0
  128. package/dist/store.js +1 -0
  129. package/dist/test-kit/git.d.ts +31 -0
  130. package/dist/test-kit/git.js +174 -0
  131. package/dist/ui.d.ts +169 -0
  132. package/dist/ui.js +10 -0
  133. package/dist/window.d.ts +41 -0
  134. package/dist/window.js +15 -0
  135. package/dist/world.d.ts +16 -0
  136. package/dist/world.js +17 -0
  137. package/docs/api/README.md +28 -0
  138. package/docs/api/app-build-contract.md +113 -0
  139. package/docs/api/backend-declaration.md +62 -0
  140. package/docs/api/backend-definition.md +215 -0
  141. package/docs/api/backend.md +269 -0
  142. package/docs/api/errors.md +169 -0
  143. package/docs/api/files.md +91 -0
  144. package/docs/api/gills.md +164 -0
  145. package/docs/api/git-commit.md +279 -0
  146. package/docs/api/git-rpc.md +243 -0
  147. package/docs/api/git-steps.md +1010 -0
  148. package/docs/api/git-wire.md +197 -0
  149. package/docs/api/git.md +579 -0
  150. package/docs/api/index.md +2880 -0
  151. package/docs/api/media-json.md +507 -0
  152. package/docs/api/media-request-id.md +32 -0
  153. package/docs/api/modules.md +32 -0
  154. package/docs/api/query.md +253 -0
  155. package/docs/api/react.md +216 -0
  156. package/docs/api/read-copy.md +204 -0
  157. package/docs/api/rpc.md +128 -0
  158. package/docs/api/storage.md +620 -0
  159. package/package.json +197 -3
@@ -0,0 +1,244 @@
1
+ import { defineCapability } from "./capabilities.js";
2
+ import { rpcDataSchema, rpcHandleSchema, rpcMethod, rpcVoid, } from "./internal/rpc-contract.js";
3
+ import * as z from "zod";
4
+ const identity = z.string().min(1).max(256);
5
+ const count = z.number().int().nonnegative();
6
+ const dollars = z.number().nonnegative();
7
+ /** The games supported by the on-box controller's current catalog. */
8
+ export const gameIdSchema = z.enum([
9
+ "minecraft",
10
+ "terraria",
11
+ "factorio",
12
+ "valheim",
13
+ "project-zomboid",
14
+ ]);
15
+ export const gameServerSizeTierSchema = z.enum(["squad", "raid"]);
16
+ export const gameServerSizes = {
17
+ minecraft: {
18
+ squad: { memoryMiB: 2048, vcpuCount: 1 },
19
+ raid: { memoryMiB: 4096, vcpuCount: 2 },
20
+ },
21
+ terraria: {
22
+ squad: { memoryMiB: 2048, vcpuCount: 1 },
23
+ raid: { memoryMiB: 4096, vcpuCount: 2 },
24
+ },
25
+ factorio: {
26
+ squad: { memoryMiB: 3072, vcpuCount: 1 },
27
+ raid: { memoryMiB: 6144, vcpuCount: 2 },
28
+ },
29
+ valheim: {
30
+ squad: { memoryMiB: 4096, vcpuCount: 2 },
31
+ raid: { memoryMiB: 6144, vcpuCount: 3 },
32
+ },
33
+ "project-zomboid": {
34
+ squad: { memoryMiB: 8192, vcpuCount: 2 },
35
+ raid: { memoryMiB: 12288, vcpuCount: 3 },
36
+ },
37
+ };
38
+ export const gameServerBillingModeSchema = z.enum(["insert-coin", "hardcore"]);
39
+ export const gameServerPassIdSchema = z.enum(["booster-pack", "battle-pass"]);
40
+ export const gameServerSizeSchema = z.strictObject({
41
+ tier: gameServerSizeTierSchema,
42
+ memoryMiB: z.number().int().positive(),
43
+ vcpuCount: z.number().int().positive(),
44
+ });
45
+ export const gameServerBackupSchema = z.strictObject({
46
+ backupId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/),
47
+ createdAt: count,
48
+ sizeBytes: count,
49
+ });
50
+ export const gameServerCatalogSchema = z.strictObject({
51
+ games: z
52
+ .array(z.strictObject({
53
+ game: gameIdSchema,
54
+ variants: z.array(identity),
55
+ sizes: z.strictObject({
56
+ squad: z.strictObject({
57
+ size: gameServerSizeSchema,
58
+ hourlyPriceUsd: dollars.positive().nullable(),
59
+ monthlyCapUsd: dollars.positive().nullable(),
60
+ hardcoreMonthlyPriceUsd: dollars.positive().nullable(),
61
+ }),
62
+ raid: z.strictObject({
63
+ size: gameServerSizeSchema,
64
+ hourlyPriceUsd: dollars.positive().nullable(),
65
+ monthlyCapUsd: dollars.positive().nullable(),
66
+ hardcoreMonthlyPriceUsd: dollars.positive().nullable(),
67
+ }),
68
+ }),
69
+ }))
70
+ .length(5),
71
+ pricing: z
72
+ .strictObject({
73
+ labels: z.strictObject({
74
+ modes: z.strictObject({ insertCoin: identity, hardcore: identity }),
75
+ sizes: z.strictObject({ squad: identity, raid: identity }),
76
+ passes: z.strictObject({
77
+ "booster-pack": identity,
78
+ "battle-pass": identity,
79
+ }),
80
+ }),
81
+ passes: z
82
+ .array(z.strictObject({
83
+ id: gameServerPassIdSchema,
84
+ displayName: identity,
85
+ monthlyPriceUsd: dollars,
86
+ playerHours: count,
87
+ serverLimit: count,
88
+ eligibleGames: z.array(gameIdSchema),
89
+ includesRaid: z.boolean(),
90
+ includedWithPro: z.boolean(),
91
+ }))
92
+ .length(2),
93
+ alwaysOnMemoryCapMiB: count,
94
+ })
95
+ .nullable(),
96
+ creditRemainingUsd: dollars,
97
+ });
98
+ export const gameServerStateSchema = z.strictObject({
99
+ serverId: identity,
100
+ teamId: identity,
101
+ name: identity,
102
+ game: gameIdSchema,
103
+ variant: identity,
104
+ size: gameServerSizeSchema,
105
+ billingMode: gameServerBillingModeSchema,
106
+ passId: gameServerPassIdSchema.nullable(),
107
+ tunnelStatus: z.enum(["configured", "not_configured"]),
108
+ desiredState: z.enum(["running", "stopped"]),
109
+ phase: z.enum(["stopped", "starting", "awake", "sleeping", "failed"]),
110
+ players: count,
111
+ maxPlayers: count.nullable(),
112
+ connections: z
113
+ .array(z.strictObject({
114
+ purpose: identity,
115
+ protocol: z.enum(["tcp", "udp"]),
116
+ host: identity,
117
+ port: z.number().int().min(1).max(65_535),
118
+ }))
119
+ .max(16),
120
+ version: z.number().int().positive(),
121
+ createdAt: count,
122
+ updatedAt: count,
123
+ });
124
+ export const gameServerSchema = gameServerStateSchema.extend({
125
+ monthToDateCostUsd: dollars,
126
+ teamMonthToDateCostUsd: dollars,
127
+ });
128
+ export const gameServerPageSchema = z.strictObject({
129
+ servers: z.array(gameServerSchema).max(100),
130
+ nextOffset: count.nullable(),
131
+ teamMonthToDateCostUsd: dollars,
132
+ });
133
+ export const gameServerErrorCodeSchema = z.enum([
134
+ "denied",
135
+ "not_found",
136
+ "conflict",
137
+ "credit_exhausted",
138
+ "pricing_not_configured",
139
+ "pass_not_included",
140
+ "pass_not_active",
141
+ "pass_game_not_eligible",
142
+ "pass_size_not_eligible",
143
+ "pass_server_limit",
144
+ "pass_mode_incompatible",
145
+ "always_on_capacity",
146
+ "capacity",
147
+ "tunnel_unavailable",
148
+ "unsupported_variant",
149
+ "unavailable",
150
+ ]);
151
+ /** Versioned command delivered from the Team projection outbox to the box. */
152
+ export const gameServerHostCommandSchema = z.discriminatedUnion("kind", [
153
+ z.strictObject({
154
+ kind: z.literal("create"),
155
+ schemaVersion: z.literal(1),
156
+ teamId: identity,
157
+ serverId: identity,
158
+ version: z.number().int().positive(),
159
+ game: gameIdSchema,
160
+ variant: identity,
161
+ sizeTier: gameServerSizeTierSchema,
162
+ billingMode: gameServerBillingModeSchema,
163
+ passId: gameServerPassIdSchema.nullable(),
164
+ settings: z.union([
165
+ z.strictObject({
166
+ eulaAcceptedByOperator: z.literal(true),
167
+ onlineMode: z.boolean().optional(),
168
+ }),
169
+ z.strictObject({ password: z.string().min(5).max(128) }),
170
+ z.strictObject({ adminPassword: z.string().min(8).max(128) }),
171
+ z.strictObject({}),
172
+ ]),
173
+ }),
174
+ ...["start", "stop", "delete", "backup-now"].map((kind) => z.strictObject({
175
+ kind: z.literal(kind),
176
+ schemaVersion: z.literal(1),
177
+ teamId: identity,
178
+ serverId: identity,
179
+ version: z.number().int().positive(),
180
+ })),
181
+ z.strictObject({
182
+ kind: z.literal("restore"),
183
+ schemaVersion: z.literal(1),
184
+ teamId: identity,
185
+ serverId: identity,
186
+ version: z.number().int().positive(),
187
+ backupId: gameServerBackupSchema.shape.backupId,
188
+ }),
189
+ ]);
190
+ /** Signed host observation; sequence and version reject stale or replayed reports. */
191
+ export const gameServerHostReportSchema = z.strictObject({
192
+ schemaVersion: z.literal(1),
193
+ hostId: identity,
194
+ teamId: identity,
195
+ serverId: identity,
196
+ version: z.number().int().positive(),
197
+ sequence: count,
198
+ observedAt: count,
199
+ phase: gameServerStateSchema.shape.phase,
200
+ players: count,
201
+ maxPlayers: count.nullable(),
202
+ connections: gameServerStateSchema.shape.connections,
203
+ tunnelStatus: gameServerStateSchema.shape.tunnelStatus,
204
+ backups: z.array(gameServerBackupSchema).max(100),
205
+ runtime: z
206
+ .strictObject({
207
+ runtimeId: z.string().min(1).max(128),
208
+ startedAt: count,
209
+ ended: z.boolean(),
210
+ })
211
+ .nullable(),
212
+ });
213
+ /** A public or App game-server request refused by the Team decision. */
214
+ export class GameServerRequestError extends Error {
215
+ code;
216
+ status;
217
+ constructor(code, status) {
218
+ super(`Game-server request refused: ${code}`);
219
+ this.code = code;
220
+ this.status = status;
221
+ this.name = "GameServerRequestError";
222
+ }
223
+ }
224
+ export const gamesServers = defineCapability("games:servers", 1, rpcHandleSchema({
225
+ list: rpcMethod(rpcDataSchema(z.unknown().transform((value) => gameServerPageSchema.parse(value)))),
226
+ catalog: rpcMethod(rpcDataSchema(z
227
+ .unknown()
228
+ .transform((value) => gameServerCatalogSchema.parse(value)))),
229
+ create: rpcMethod(rpcDataSchema(z.unknown().transform((value) => gameServerSchema.parse(value)))),
230
+ status: rpcMethod(rpcDataSchema(z.unknown().transform((value) => gameServerSchema.parse(value)))),
231
+ start: rpcMethod(rpcDataSchema(z.unknown().transform((value) => gameServerSchema.parse(value)))),
232
+ stop: rpcMethod(rpcDataSchema(z.unknown().transform((value) => gameServerSchema.parse(value)))),
233
+ restore: rpcMethod(rpcDataSchema(z.unknown().transform((value) => gameServerSchema.parse(value)))),
234
+ backups: rpcMethod(rpcDataSchema(z
235
+ .unknown()
236
+ .transform((value) => z.array(gameServerBackupSchema).parse(value)))),
237
+ delete: rpcVoid,
238
+ backupNow: rpcVoid,
239
+ }));
240
+ /** Decode the host's listing at the App boundary. */
241
+ export async function listGameServers(capabilities, input) {
242
+ const page = await capabilities.require(gamesServers).list(input);
243
+ return gameServerPageSchema.parse(page);
244
+ }
@@ -0,0 +1,43 @@
1
+ import { type AppDeclaredGill, type AppGillDesktopSize, type AppGillParam, type AppGillWatch, type GillCall, type GillReply } from "@fungi.computer/gills";
2
+ import type { WhistleJsonValue } from "@fungi.computer/whistle";
3
+ import { type GillResponse } from "@fungi.computer/gills";
4
+ /** Decode the App host's strict isolation refusal into its permanent Gill state. */
5
+ export declare function decodeGillRefusal(raw: unknown, declaration: Readonly<{
6
+ id: string;
7
+ title: string;
8
+ }>): GillResponse | undefined;
9
+ /** App-owned projection; methods keep their ordinary domain-shaped output. */
10
+ export type AppBackendGill = Readonly<{
11
+ declaration: AppDeclaredGill;
12
+ /** Page size passed to the source; projection never discards rows. */
13
+ limit: number;
14
+ /** Map placement and continuation to the source method, forwarding its limit. */
15
+ input: (selection: Readonly<{
16
+ params: Readonly<Partial<Record<string, string>>>;
17
+ limit: number;
18
+ after?: string | undefined;
19
+ }>) => Readonly<{
20
+ limit: number;
21
+ [key: string]: WhistleJsonValue;
22
+ }>;
23
+ /** Decode the maintained call wire only for a backend declaring Gills. */
24
+ parseCall: (raw: GillCall) => GillCall;
25
+ /** Project through the backend's admitted read method callback. */
26
+ call: (selection: GillCall, read: (name: string, input: WhistleJsonValue, kind: "rows" | "options") => Promise<WhistleJsonValue>) => Promise<GillReply>;
27
+ /** Project parsed source output into text rows without dropping records. */
28
+ rows: (output: WhistleJsonValue, context: Readonly<{
29
+ params: Readonly<Record<string, string>>;
30
+ }>) => GillResponse;
31
+ }>;
32
+ /** Define a compact view of one of this App's declared read methods. */
33
+ export declare function defineAppGill(options: Readonly<{
34
+ id: string;
35
+ title: string;
36
+ source: string;
37
+ limit: number;
38
+ sizes?: readonly AppGillDesktopSize[];
39
+ params?: readonly AppGillParam[];
40
+ watch?: AppGillWatch;
41
+ input: AppBackendGill["input"];
42
+ rows: AppBackendGill["rows"];
43
+ }>): AppBackendGill;
package/dist/gills.js ADDED
@@ -0,0 +1,174 @@
1
+ import * as z from "zod";
2
+ import * as Schema from "effect/Schema";
3
+ import { appDeclaredGillSchema, decodeDeclaredGillResponse, decodeGillOptionsResponse, gillCallSchema, } from "@fungi.computer/gills";
4
+ import { appBackendRefusalSchema } from "./backend-refusal.js";
5
+ import { decodeGillResponse } from "@fungi.computer/gills";
6
+ /** Decode the App host's strict isolation refusal into its permanent Gill state. */
7
+ export function decodeGillRefusal(raw, declaration) {
8
+ const refusal = appBackendRefusalSchema.safeParse(raw);
9
+ if (!refusal.success || refusal.data.reason !== "exec_isolation_required")
10
+ return undefined;
11
+ return decodeGillResponse({
12
+ viewId: declaration.id,
13
+ title: declaration.title,
14
+ state: "error",
15
+ rows: [],
16
+ reason: refusal.data.reason,
17
+ error: `${declaration.title} isn't available on this preview.`,
18
+ });
19
+ }
20
+ /** Define a compact view of one of this App's declared read methods. */
21
+ export function defineAppGill(options) {
22
+ let declaration = {
23
+ id: options.id,
24
+ title: options.title,
25
+ source: options.source,
26
+ };
27
+ if (options.sizes !== undefined)
28
+ declaration = { ...declaration, desktop: { sizes: options.sizes } };
29
+ if (options.params !== undefined)
30
+ declaration = { ...declaration, params: options.params };
31
+ if (options.watch !== undefined)
32
+ declaration = { ...declaration, watch: options.watch };
33
+ declaration = Schema.decodeUnknownSync(appDeclaredGillSchema, {
34
+ onExcessProperty: "error",
35
+ })(declaration);
36
+ return Object.freeze({
37
+ declaration,
38
+ limit: z.number().int().min(1).max(200).parse(options.limit),
39
+ input: options.input,
40
+ rows: options.rows,
41
+ parseCall: Schema.decodeUnknownSync(gillCallSchema, {
42
+ onExcessProperty: "error",
43
+ }),
44
+ call: async (selection, read) => {
45
+ const params = selection.params ?? {};
46
+ const declared = declaration.params ?? [];
47
+ if (Object.keys(params).some((key) => !declared.some((param) => param.key === key)))
48
+ throw new Error("Undeclared Gill parameter");
49
+ if (selection.options !== undefined) {
50
+ const param = declared.find((candidate) => candidate.key === selection.options);
51
+ const source = param?.options;
52
+ if (source === undefined)
53
+ throw new Error("Gill options source must be a declared read method");
54
+ let input = { limit: options.limit, params };
55
+ if (selection.after !== undefined)
56
+ input = { ...input, after: selection.after };
57
+ return fitGillOptionsText(await read(source, input, "options"), options.limit);
58
+ }
59
+ const missing = declared.find((param) => param.required &&
60
+ (!Object.hasOwn(params, param.key) || !params[param.key]));
61
+ if (missing)
62
+ return {
63
+ viewId: selection.id,
64
+ title: declaration.title,
65
+ state: "error",
66
+ rows: [],
67
+ reason: "params_required",
68
+ error: `Choose ${missing.label.toLowerCase()}`,
69
+ recovery: { kind: "configure", param: missing.key },
70
+ };
71
+ const input = options.input({
72
+ params,
73
+ after: selection.after,
74
+ limit: options.limit,
75
+ });
76
+ if (input.limit !== options.limit)
77
+ throw new Error("Gill input must preserve its page size");
78
+ const result = await read(declaration.source, input, "rows");
79
+ return decodeDeclaredGillResponse(fitGillText(options.rows(result, { params }), options.limit), declaration);
80
+ },
81
+ });
82
+ }
83
+ // The App-shard reply bound is 240 KiB. Navigation and cursors are never shortened.
84
+ const gillReplyByteLimit = 240 * 1024;
85
+ const utf8 = new TextEncoder();
86
+ function shortenText(text, limit) {
87
+ if (text.length <= limit)
88
+ return text;
89
+ if (limit === 0)
90
+ return "";
91
+ let prefix = text.slice(0, limit - 1);
92
+ // Keep a surrogate pair intact when shortening Unicode display text.
93
+ if (/[\uD800-\uDBFF]$/.test(prefix))
94
+ prefix = prefix.slice(0, -1);
95
+ return `${prefix}…`;
96
+ }
97
+ function fitGillText(raw, pageSize) {
98
+ if (raw.rows.length > pageSize)
99
+ throw new Error("Gill source exceeded its page size");
100
+ let titleLimit = 4096;
101
+ let detailLimit = 1024;
102
+ for (;;) {
103
+ let page;
104
+ if (raw.state === "full")
105
+ page = {
106
+ ...raw,
107
+ title: shortenText(raw.title, titleLimit),
108
+ rows: raw.rows.map((row) => {
109
+ let text = { ...row, title: shortenText(row.title, titleLimit) };
110
+ if (row.detail !== undefined)
111
+ text = { ...text, detail: shortenText(row.detail, detailLimit) };
112
+ if (row.status !== undefined)
113
+ text = {
114
+ ...text,
115
+ status: {
116
+ ...row.status,
117
+ label: shortenText(row.status.label, titleLimit),
118
+ },
119
+ };
120
+ return text;
121
+ }),
122
+ };
123
+ else if (raw.state === "error")
124
+ page = {
125
+ ...raw,
126
+ title: shortenText(raw.title, titleLimit),
127
+ error: shortenText(raw.error, 512),
128
+ };
129
+ else
130
+ page = { ...raw, title: shortenText(raw.title, titleLimit) };
131
+ const decoded = decodeGillResponse(page);
132
+ if (utf8.encode(JSON.stringify(decoded)).byteLength <= gillReplyByteLimit)
133
+ return decoded;
134
+ if (titleLimit === 1 && detailLimit === 0)
135
+ throw new Error("Gill navigation exceeds the reply byte budget");
136
+ titleLimit = Math.max(1, Math.floor(titleLimit / 2));
137
+ detailLimit = Math.floor(detailLimit / 2);
138
+ }
139
+ }
140
+ function fitGillOptionsText(raw, pageSize) {
141
+ // The method's own output parse has already run. Bound display text before
142
+ // decoding the shared choice wire; values and continuation are preserved.
143
+ const projected = z
144
+ .strictObject({
145
+ options: z
146
+ .array(z.strictObject({
147
+ value: z.string().max(256),
148
+ label: z.string().min(1),
149
+ detail: z.string().optional(),
150
+ }))
151
+ .max(pageSize),
152
+ next: z.string().max(1024).optional(),
153
+ })
154
+ .parse(raw);
155
+ let labelLimit = 4096;
156
+ let detailLimit = 1024;
157
+ for (;;) {
158
+ const choices = decodeGillOptionsResponse({
159
+ ...projected,
160
+ options: projected.options.map((option) => {
161
+ let text = { ...option, label: shortenText(option.label, labelLimit) };
162
+ if (option.detail !== undefined)
163
+ text = { ...text, detail: shortenText(option.detail, detailLimit) };
164
+ return text;
165
+ }),
166
+ });
167
+ if (utf8.encode(JSON.stringify(choices)).byteLength <= gillReplyByteLimit)
168
+ return choices;
169
+ if (labelLimit === 1 && detailLimit === 0)
170
+ throw new Error("Gill option values exceed the reply byte budget");
171
+ labelLimit = Math.max(1, Math.floor(labelLimit / 2));
172
+ detailLimit = Math.floor(detailLimit / 2);
173
+ }
174
+ }
@@ -0,0 +1,143 @@
1
+ import type { GitBinding } from "./git.js";
2
+ /** The binding operations these writes use. */
3
+ export type GitCommitStore = Pick<GitBinding, "exchange">;
4
+ /** One file to write (`content`) or delete (`content: null`), repository-relative. */
5
+ export type GitFileChange = Readonly<{
6
+ /** Slash-separated path inside the repository, without `.` or `..` parts. */
7
+ path: string;
8
+ /** New file bytes, or null to delete the file. */
9
+ content: Uint8Array | null;
10
+ /**
11
+ * File mode for a written file. Defaults to the existing entry's mode, or
12
+ * `100644` for a new file.
13
+ */
14
+ mode?: "100644" | "100755";
15
+ }>;
16
+ /** Name and email recorded as a commit's author and committer. */
17
+ export type GitIdentity = Readonly<{
18
+ name: string;
19
+ email: string;
20
+ }>;
21
+ /** A commit of file changes to one branch. */
22
+ export type CommitFilesRequest = Readonly<{
23
+ /** Installation-local repository name. */
24
+ repository: string;
25
+ /** Branch to commit to, without `refs/heads/`. */
26
+ branch: string;
27
+ /** The branch head the changes were made against, or null to create the branch. */
28
+ expectedHead: string | null;
29
+ /** When creating the branch, start from this branch's head instead of an empty tree. */
30
+ startFrom?: string;
31
+ /** Files to write or delete. */
32
+ changes: readonly GitFileChange[];
33
+ /** Commit message. */
34
+ message: string;
35
+ /** Author and committer. */
36
+ author: GitIdentity;
37
+ }>;
38
+ /** What a compare-and-swap write did. */
39
+ export type GitWriteOutcome = Readonly<{
40
+ kind: "committed";
41
+ commit: string;
42
+ }> | Readonly<{
43
+ kind: "unchanged";
44
+ commit: string;
45
+ }> | Readonly<{
46
+ kind: "updated";
47
+ }>
48
+ /** The ref was not at the expected head; nothing was written. */
49
+ | Readonly<{
50
+ kind: "stale";
51
+ head: string | null;
52
+ }>;
53
+ /** Merge a branch of `source` (such as a fork) into a branch of `target`. */
54
+ export type MergeBranchRequest = Readonly<{
55
+ /** Repository receiving the merge. */
56
+ target: string;
57
+ /** Repository the work comes from; may equal `target`. */
58
+ source: string;
59
+ /** Branch in `target` to merge into, without `refs/heads/`. */
60
+ branch: string;
61
+ /** Branch in `source` to merge, without `refs/heads/`. */
62
+ sourceBranch: string;
63
+ /** Reviewed source commit; must match the fetched source branch head. */
64
+ expectedSource: string;
65
+ /** Copied target head; used only when full source history proves it is an ancestor. */
66
+ expectedTarget?: string;
67
+ /** Copied distance behind the target. Zero permits an ancestry-checked source-only fetch. */
68
+ behind?: number;
69
+ /** Merge commit message. */
70
+ message: string;
71
+ /** Author and committer of the merge commit. */
72
+ author: GitIdentity;
73
+ }>;
74
+ /** What a merge did. Merges always write a merge commit when there is new work. */
75
+ export type GitMergeOutcome = Readonly<{
76
+ kind: "merged";
77
+ before: string;
78
+ after: string;
79
+ }>
80
+ /** The target already contains the source's work. */
81
+ | Readonly<{
82
+ kind: "up-to-date";
83
+ head: string;
84
+ }>
85
+ /** Both sides changed the same lines; nothing was written. */
86
+ | Readonly<{
87
+ kind: "conflict";
88
+ head: string;
89
+ paths: readonly string[];
90
+ }>
91
+ /** The target moved while merging; nothing was written. */
92
+ | Readonly<{
93
+ kind: "stale";
94
+ head: string | null;
95
+ }>
96
+ /** The fetched source differs from the reviewed commit; nothing was written. */
97
+ | Readonly<{
98
+ kind: "source-moved";
99
+ head: string;
100
+ }>;
101
+ /** Point a ref at a commit that is the head of `source`. */
102
+ export type MoveRefRequest = Readonly<{
103
+ /** Installation-local repository name. */
104
+ repository: string;
105
+ /** Full ref name, such as `refs/heads/main` or `refs/tags/v1`. */
106
+ ref: string;
107
+ /** The ref's current commit, or null to create it. */
108
+ expected: string | null;
109
+ /** Commit to point the ref at. */
110
+ commit: string;
111
+ /** Branch (without `refs/heads/`) whose head is `commit`. */
112
+ source: string;
113
+ }>;
114
+ /** Ordered ref moves to one fetched source commit; each move has its own CAS guard. */
115
+ export type MoveRefsRequest = Readonly<{
116
+ /** Installation-local repository name. */
117
+ repository: string;
118
+ /** Branch whose fetched head must be the selected commit. */
119
+ source: string;
120
+ /** Commit shared by all ref moves. */
121
+ commit: string;
122
+ /** Ordered compare-and-swap moves; an accepted prefix is never rolled back. */
123
+ updates: readonly Readonly<{
124
+ ref: string;
125
+ expected: string | null;
126
+ }>[];
127
+ }>;
128
+ /** Earlier moves remain accepted if a later ref is stale; this is not an atomic push. */
129
+ export type GitRefMovesOutcome = Readonly<{
130
+ kind: "updated";
131
+ }> | Readonly<{
132
+ kind: "stale";
133
+ ref: string;
134
+ head: string | null;
135
+ }>;
136
+ /** Commit files using independently resumable, CAS-guarded Git steps. */
137
+ export declare function commitFiles(store: GitCommitStore, request: CommitFilesRequest, beforeWrite?: () => Promise<void>): Promise<GitWriteOutcome>;
138
+ /** Point a ref at the head commit of `source`, compare-and-swap on the ref. */
139
+ export declare function moveRef(store: GitCommitStore, request: MoveRefRequest): Promise<GitWriteOutcome>;
140
+ /** Fetch a source once, then move refs in order with separate guarded pushes. */
141
+ export declare function moveRefs(store: GitCommitStore, request: MoveRefsRequest): Promise<GitRefMovesOutcome>;
142
+ /** Merge branches through the same one-exchange step engine as background owners. */
143
+ export declare function mergeBranch(store: GitCommitStore, request: MergeBranchRequest): Promise<GitMergeOutcome>;