@okrlinkhub/agent-factory 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +235 -31
  2. package/dist/client/bridge.d.ts +1 -0
  3. package/dist/client/bridge.d.ts.map +1 -1
  4. package/dist/client/bridge.js.map +1 -1
  5. package/dist/client/index.d.ts +29 -3
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/index.js +59 -3
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/component/_generated/api.d.ts +2 -0
  10. package/dist/component/_generated/api.d.ts.map +1 -1
  11. package/dist/component/_generated/api.js.map +1 -1
  12. package/dist/component/_generated/component.d.ts +140 -2
  13. package/dist/component/_generated/component.d.ts.map +1 -1
  14. package/dist/component/flyCleanup.d.ts +32 -0
  15. package/dist/component/flyCleanup.d.ts.map +1 -0
  16. package/dist/component/flyCleanup.js +272 -0
  17. package/dist/component/flyCleanup.js.map +1 -0
  18. package/dist/component/identity.d.ts +60 -2
  19. package/dist/component/identity.d.ts.map +1 -1
  20. package/dist/component/identity.js +372 -32
  21. package/dist/component/identity.js.map +1 -1
  22. package/dist/component/lib.d.ts +2 -1
  23. package/dist/component/lib.d.ts.map +1 -1
  24. package/dist/component/lib.js +2 -1
  25. package/dist/component/lib.js.map +1 -1
  26. package/dist/component/providers/fly.d.ts +23 -2
  27. package/dist/component/providers/fly.d.ts.map +1 -1
  28. package/dist/component/providers/fly.js +15 -3
  29. package/dist/component/providers/fly.js.map +1 -1
  30. package/dist/component/pushing.d.ts +4 -4
  31. package/dist/component/queue.d.ts +12 -7
  32. package/dist/component/queue.d.ts.map +1 -1
  33. package/dist/component/queue.js +9 -0
  34. package/dist/component/queue.js.map +1 -1
  35. package/dist/component/scheduler.d.ts +8 -8
  36. package/dist/component/scheduler.d.ts.map +1 -1
  37. package/dist/component/scheduler.js +22 -2
  38. package/dist/component/scheduler.js.map +1 -1
  39. package/dist/component/schema.d.ts +16 -4
  40. package/dist/component/schema.d.ts.map +1 -1
  41. package/dist/component/schema.js +16 -0
  42. package/dist/component/schema.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/client/bridge.ts +1 -0
  45. package/src/client/index.ts +68 -3
  46. package/src/component/_generated/api.ts +2 -0
  47. package/src/component/_generated/component.ts +188 -8
  48. package/src/component/flyCleanup.ts +386 -0
  49. package/src/component/identity.ts +425 -31
  50. package/src/component/lib.test.ts +197 -3
  51. package/src/component/lib.ts +3 -0
  52. package/src/component/providers/fly.ts +39 -5
  53. package/src/component/queue.ts +11 -0
  54. package/src/component/scheduler.ts +23 -2
  55. package/src/component/schema.ts +16 -0
@@ -0,0 +1,386 @@
1
+ import { v } from "convex/values";
2
+ import { api, internal } from "./_generated/api.js";
3
+ import { action } from "./_generated/server.js";
4
+ import type { ActionCtx } from "./_generated/server.js";
5
+ import { DEFAULT_CONFIG, providerConfigValidator } from "./config.js";
6
+
7
+ const DEFAULT_MACHINE_CONCURRENCY = 6;
8
+ const DEFAULT_VOLUME_CONCURRENCY = 16;
9
+
10
+ const cleanupReportValidator = v.object({
11
+ appName: v.string(),
12
+ machinesFound: v.number(),
13
+ machinesDeleted: v.number(),
14
+ machinesRemaining: v.number(),
15
+ machineIdsDeleted: v.array(v.string()),
16
+ machineIdsRemaining: v.array(v.string()),
17
+ volumesFound: v.number(),
18
+ volumesDeleted: v.number(),
19
+ volumesRemaining: v.number(),
20
+ volumeIdsDeleted: v.array(v.string()),
21
+ volumeIdsRemaining: v.array(v.string()),
22
+ warnings: v.array(v.string()),
23
+ errors: v.array(v.string()),
24
+ });
25
+
26
+ type CleanupReport = {
27
+ appName: string;
28
+ machinesFound: number;
29
+ machinesDeleted: number;
30
+ machinesRemaining: number;
31
+ machineIdsDeleted: Array<string>;
32
+ machineIdsRemaining: Array<string>;
33
+ volumesFound: number;
34
+ volumesDeleted: number;
35
+ volumesRemaining: number;
36
+ volumeIdsDeleted: Array<string>;
37
+ volumeIdsRemaining: Array<string>;
38
+ warnings: Array<string>;
39
+ errors: Array<string>;
40
+ };
41
+
42
+ type ProviderConfig = typeof DEFAULT_CONFIG.provider;
43
+
44
+ type FlyMachine = {
45
+ id: string;
46
+ name?: string;
47
+ };
48
+
49
+ type FlyVolume = {
50
+ id: string;
51
+ name?: string;
52
+ };
53
+
54
+ type MachineCleanupResult = {
55
+ machineId: string;
56
+ deleted: boolean;
57
+ warnings: Array<string>;
58
+ error: string | null;
59
+ };
60
+
61
+ type VolumeCleanupResult = {
62
+ volumeId: string;
63
+ deleted: boolean;
64
+ warnings: Array<string>;
65
+ error: string | null;
66
+ };
67
+
68
+ export const runFlyCleanup = action({
69
+ args: {
70
+ flyApiToken: v.optional(v.string()),
71
+ machineConcurrency: v.optional(v.number()),
72
+ providerConfig: v.optional(providerConfigValidator),
73
+ volumeConcurrency: v.optional(v.number()),
74
+ },
75
+ returns: cleanupReportValidator,
76
+ handler: async (ctx, args): Promise<CleanupReport> => {
77
+ const providerConfig = await resolveProviderConfig(ctx, args.providerConfig);
78
+ if (providerConfig.kind !== "fly") {
79
+ throw new Error("Fly cleanup requires a Fly provider configuration.");
80
+ }
81
+
82
+ const appName = providerConfig.appName.trim();
83
+ if (appName.length === 0) {
84
+ throw new Error("Fly cleanup requires a non-empty Fly app name.");
85
+ }
86
+
87
+ const flyApiToken = await resolveFlyApiToken(ctx, args.flyApiToken);
88
+ const client = new FlyApiClient(flyApiToken);
89
+ const machineConcurrency = normalizeConcurrency(
90
+ args.machineConcurrency,
91
+ DEFAULT_MACHINE_CONCURRENCY,
92
+ );
93
+ const volumeConcurrency = normalizeConcurrency(
94
+ args.volumeConcurrency,
95
+ DEFAULT_VOLUME_CONCURRENCY,
96
+ );
97
+
98
+ await client.verifyAppAccess(appName);
99
+
100
+ const warnings: Array<string> = [];
101
+ const errors: Array<string> = [];
102
+
103
+ const initialMachines = await client.listMachines(appName);
104
+ const machineResults = await runWithConcurrency(
105
+ initialMachines,
106
+ machineConcurrency,
107
+ async (machine) => cleanupMachine(client, appName, machine),
108
+ );
109
+ for (const result of machineResults) {
110
+ warnings.push(...result.warnings);
111
+ if (result.error) {
112
+ errors.push(result.error);
113
+ }
114
+ }
115
+
116
+ const remainingMachines = await client.listMachines(appName);
117
+ if (remainingMachines.length > 0) {
118
+ warnings.push(
119
+ `Fly cleanup left ${remainingMachines.length} machine(s) after verification: ${remainingMachines
120
+ .map((machine) => machine.id)
121
+ .join(", ")}`,
122
+ );
123
+ }
124
+
125
+ const initialVolumes = await client.listVolumes(appName);
126
+ const volumeResults = await runWithConcurrency(
127
+ initialVolumes,
128
+ volumeConcurrency,
129
+ async (volume) => cleanupVolume(client, appName, volume),
130
+ );
131
+ for (const result of volumeResults) {
132
+ warnings.push(...result.warnings);
133
+ if (result.error) {
134
+ errors.push(result.error);
135
+ }
136
+ }
137
+
138
+ const remainingVolumes = await client.listVolumes(appName);
139
+ if (remainingVolumes.length > 0) {
140
+ warnings.push(
141
+ `Fly cleanup left ${remainingVolumes.length} volume(s) after verification: ${remainingVolumes
142
+ .map((volume) => volume.id)
143
+ .join(", ")}`,
144
+ );
145
+ }
146
+
147
+ return {
148
+ appName,
149
+ machinesFound: initialMachines.length,
150
+ machinesDeleted: machineResults.filter((result) => result.deleted).length,
151
+ machinesRemaining: remainingMachines.length,
152
+ machineIdsDeleted: machineResults
153
+ .filter((result) => result.deleted)
154
+ .map((result) => result.machineId),
155
+ machineIdsRemaining: remainingMachines.map((machine) => machine.id),
156
+ volumesFound: initialVolumes.length,
157
+ volumesDeleted: volumeResults.filter((result) => result.deleted).length,
158
+ volumesRemaining: remainingVolumes.length,
159
+ volumeIdsDeleted: volumeResults
160
+ .filter((result) => result.deleted)
161
+ .map((result) => result.volumeId),
162
+ volumeIdsRemaining: remainingVolumes.map((volume) => volume.id),
163
+ warnings,
164
+ errors,
165
+ };
166
+ },
167
+ });
168
+
169
+ async function resolveProviderConfig(
170
+ ctx: Pick<ActionCtx, "runQuery">,
171
+ providerConfigOverride: ProviderConfig | undefined,
172
+ ): Promise<ProviderConfig> {
173
+ if (providerConfigOverride) {
174
+ return providerConfigOverride;
175
+ }
176
+ const runtimeConfig = await ctx.runQuery(api.queue.providerRuntimeConfig, {});
177
+ return runtimeConfig ?? DEFAULT_CONFIG.provider;
178
+ }
179
+
180
+ async function resolveFlyApiToken(
181
+ ctx: Pick<ActionCtx, "runQuery">,
182
+ flyApiTokenOverride: string | undefined,
183
+ ): Promise<string> {
184
+ const inlineToken = flyApiTokenOverride?.trim();
185
+ if (inlineToken) {
186
+ return inlineToken;
187
+ }
188
+ const storedToken = await ctx.runQuery(internal.queue.getActiveSecretPlaintext, {
189
+ secretRef: "fly.apiToken",
190
+ });
191
+ const normalized = storedToken?.trim();
192
+ if (!normalized) {
193
+ throw new Error("Missing active 'fly.apiToken' secret. Import it before running Fly cleanup.");
194
+ }
195
+ return normalized;
196
+ }
197
+
198
+ async function cleanupMachine(
199
+ client: FlyApiClient,
200
+ appName: string,
201
+ machine: FlyMachine,
202
+ ): Promise<MachineCleanupResult> {
203
+ const warnings: Array<string> = [];
204
+
205
+ try {
206
+ await client.cordonMachine(appName, machine.id);
207
+ } catch (error) {
208
+ if (isFlyNotFoundError(error)) {
209
+ return { machineId: machine.id, deleted: true, warnings, error: null };
210
+ }
211
+ warnings.push(`Machine ${machine.id}: cordon warning (${describeError(error)})`);
212
+ }
213
+
214
+ try {
215
+ await client.stopMachine(appName, machine.id);
216
+ } catch (error) {
217
+ if (isFlyNotFoundError(error)) {
218
+ return { machineId: machine.id, deleted: true, warnings, error: null };
219
+ }
220
+ warnings.push(`Machine ${machine.id}: stop warning (${describeError(error)})`);
221
+ }
222
+
223
+ try {
224
+ await client.deleteMachine(appName, machine.id);
225
+ return { machineId: machine.id, deleted: true, warnings, error: null };
226
+ } catch (error) {
227
+ if (isFlyNotFoundError(error)) {
228
+ return { machineId: machine.id, deleted: true, warnings, error: null };
229
+ }
230
+ return {
231
+ machineId: machine.id,
232
+ deleted: false,
233
+ warnings,
234
+ error: `Machine ${machine.id}: destroy failed (${describeError(error)})`,
235
+ };
236
+ }
237
+ }
238
+
239
+ async function cleanupVolume(
240
+ client: FlyApiClient,
241
+ appName: string,
242
+ volume: FlyVolume,
243
+ ): Promise<VolumeCleanupResult> {
244
+ try {
245
+ await client.deleteVolume(appName, volume.id);
246
+ return { volumeId: volume.id, deleted: true, warnings: [], error: null };
247
+ } catch (error) {
248
+ if (isFlyNotFoundError(error)) {
249
+ return { volumeId: volume.id, deleted: true, warnings: [], error: null };
250
+ }
251
+ return {
252
+ volumeId: volume.id,
253
+ deleted: false,
254
+ warnings: [],
255
+ error: `Volume ${volume.id}: destroy failed (${describeError(error)})`,
256
+ };
257
+ }
258
+ }
259
+
260
+ async function runWithConcurrency<TInput, TOutput>(
261
+ items: Array<TInput>,
262
+ concurrency: number,
263
+ worker: (item: TInput, index: number) => Promise<TOutput>,
264
+ ): Promise<Array<TOutput>> {
265
+ if (items.length === 0) {
266
+ return [];
267
+ }
268
+
269
+ const results = new Array<TOutput>(items.length);
270
+ let nextIndex = 0;
271
+ const workerCount = Math.min(concurrency, items.length);
272
+
273
+ await Promise.all(
274
+ Array.from({ length: workerCount }, async () => {
275
+ while (nextIndex < items.length) {
276
+ const currentIndex = nextIndex;
277
+ nextIndex += 1;
278
+ results[currentIndex] = await worker(items[currentIndex], currentIndex);
279
+ }
280
+ }),
281
+ );
282
+
283
+ return results;
284
+ }
285
+
286
+ function normalizeConcurrency(value: number | undefined, fallback: number): number {
287
+ if (value === undefined || !Number.isFinite(value)) {
288
+ return fallback;
289
+ }
290
+ return Math.max(1, Math.floor(value));
291
+ }
292
+
293
+ class FlyApiClient {
294
+ constructor(private readonly apiToken: string) {}
295
+
296
+ async verifyAppAccess(appName: string): Promise<void> {
297
+ await this.request<void>({
298
+ method: "GET",
299
+ path: `/apps/${encodeURIComponent(appName)}`,
300
+ });
301
+ }
302
+
303
+ async listMachines(appName: string): Promise<Array<FlyMachine>> {
304
+ return await this.request<Array<FlyMachine>>({
305
+ method: "GET",
306
+ path: `/apps/${encodeURIComponent(appName)}/machines`,
307
+ });
308
+ }
309
+
310
+ async listVolumes(appName: string): Promise<Array<FlyVolume>> {
311
+ return await this.request<Array<FlyVolume>>({
312
+ method: "GET",
313
+ path: `/apps/${encodeURIComponent(appName)}/volumes`,
314
+ });
315
+ }
316
+
317
+ async cordonMachine(appName: string, machineId: string): Promise<void> {
318
+ await this.request<void>({
319
+ method: "POST",
320
+ path: `/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}/cordon`,
321
+ });
322
+ }
323
+
324
+ async stopMachine(appName: string, machineId: string): Promise<void> {
325
+ await this.request<void>({
326
+ method: "POST",
327
+ path: `/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}/stop`,
328
+ });
329
+ }
330
+
331
+ async deleteMachine(appName: string, machineId: string): Promise<void> {
332
+ await this.request<void>({
333
+ method: "DELETE",
334
+ path: `/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}`,
335
+ });
336
+ }
337
+
338
+ async deleteVolume(appName: string, volumeId: string): Promise<void> {
339
+ await this.request<void>({
340
+ method: "DELETE",
341
+ path: `/apps/${encodeURIComponent(appName)}/volumes/${encodeURIComponent(volumeId)}`,
342
+ });
343
+ }
344
+
345
+ private async request<T>(input: {
346
+ method: "GET" | "POST" | "DELETE";
347
+ path: string;
348
+ }): Promise<T> {
349
+ const response = await fetch(`https://api.machines.dev/v1${input.path}`, {
350
+ method: input.method,
351
+ headers: {
352
+ Authorization: `Bearer ${this.apiToken}`,
353
+ "Content-Type": "application/json",
354
+ },
355
+ });
356
+
357
+ if (!response.ok) {
358
+ const body = await response.text();
359
+ throw new Error(`Fly API ${input.method} ${input.path} failed: ${body || response.statusText}`);
360
+ }
361
+
362
+ if (response.status === 204 || input.method === "DELETE") {
363
+ return undefined as T;
364
+ }
365
+
366
+ const text = await response.text();
367
+ if (!text) {
368
+ return undefined as T;
369
+ }
370
+ return JSON.parse(text) as T;
371
+ }
372
+ }
373
+
374
+ function isFlyNotFoundError(error: unknown): boolean {
375
+ if (!(error instanceof Error)) {
376
+ return false;
377
+ }
378
+ return /not found|unknown machine|does not exist/i.test(error.message);
379
+ }
380
+
381
+ function describeError(error: unknown): string {
382
+ if (error instanceof Error) {
383
+ return error.message;
384
+ }
385
+ return String(error);
386
+ }