@framers/agentos-ext-cloud-flyio 0.1.0 → 0.1.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.
- package/manifest.json +2 -2
- package/package.json +14 -6
- package/src/FlyService.ts +0 -364
- package/src/index.ts +0 -109
- package/src/tools/createVolume.ts +0 -53
- package/src/tools/deployApp.ts +0 -104
- package/src/tools/listApps.ts +0 -62
- package/src/tools/scaleApp.ts +0 -116
- package/test/FlyService.test.ts +0 -484
- package/test/index.test.ts +0 -49
- package/test/tools.test.ts +0 -407
- package/tsconfig.json +0 -22
package/manifest.json
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
|
|
3
3
|
"id": "com.framers.cloud.flyio",
|
|
4
4
|
"name": "Fly.io Cloud Deployment",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"description": "Deploy and manage applications on Fly.io using the Machines API. Create apps, launch machines with custom configs, scale instances by count and size, and provision persistent volumes for stateful workloads.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Frame.dev",
|
|
9
9
|
"email": "support@frame.dev",
|
|
10
|
-
"url": "https://github.com/
|
|
10
|
+
"url": "https://github.com/framerslab"
|
|
11
11
|
},
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"keywords": ["cloud", "flyio", "fly", "deploy", "machines", "edge", "volumes"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@framers/agentos-ext-cloud-flyio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Fly.io cloud deployment extension — deploy apps, manage machines, scale instances, and create persistent volumes via the Fly Machines API.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -39,26 +39,34 @@
|
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "https://github.com/
|
|
42
|
+
"url": "git+https://github.com/framerslab/agentos-extensions.git",
|
|
43
43
|
"directory": "registry/curated/cloud/flyio"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@framers/agentos": "
|
|
49
|
+
"@framers/agentos": ">=0.10.40"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@framers/agentos": "^0.6.6",
|
|
53
52
|
"@types/node": "^20.12.12",
|
|
54
53
|
"rimraf": "^5.0.7",
|
|
55
54
|
"typescript": "^5.4.5",
|
|
56
|
-
"vitest": "^
|
|
55
|
+
"vitest": "^3.2.7",
|
|
56
|
+
"@framers/agentos": "^0.10.40"
|
|
57
57
|
},
|
|
58
|
+
"homepage": "https://github.com/framerslab/agentos-extensions/tree/master/registry/curated/cloud/flyio#readme",
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/framerslab/agentos-extensions/issues"
|
|
61
|
+
},
|
|
62
|
+
"files": [
|
|
63
|
+
"dist",
|
|
64
|
+
"manifest.json"
|
|
65
|
+
],
|
|
58
66
|
"scripts": {
|
|
59
67
|
"build": "tsc",
|
|
60
68
|
"dev": "tsc --watch",
|
|
61
|
-
"test": "vitest run",
|
|
69
|
+
"test": "vitest run --passWithNoTests",
|
|
62
70
|
"test:watch": "vitest",
|
|
63
71
|
"typecheck": "tsc --noEmit",
|
|
64
72
|
"clean": "rimraf dist"
|
package/src/FlyService.ts
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview Fly.io Machines API service layer.
|
|
4
|
-
*
|
|
5
|
-
* Wraps the Fly Machines API for app creation, machine management,
|
|
6
|
-
* scaling, and volume provisioning. Uses the GraphQL API for listing
|
|
7
|
-
* apps (organization-scoped) and the Machines REST API for everything else.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Types
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
export interface FlyConfig {
|
|
15
|
-
/** Fly.io API token (from `flyctl tokens create`) */
|
|
16
|
-
token: string;
|
|
17
|
-
/** Machines API base URL (defaults to https://api.machines.dev/v1) */
|
|
18
|
-
baseUrl?: string;
|
|
19
|
-
/** GraphQL API base URL for org-level queries (defaults to https://api.fly.io/graphql) */
|
|
20
|
-
graphqlUrl?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface FlyApp {
|
|
24
|
-
id: string;
|
|
25
|
-
name: string;
|
|
26
|
-
organization: string;
|
|
27
|
-
status: string;
|
|
28
|
-
hostname: string;
|
|
29
|
-
createdAt?: string;
|
|
30
|
-
machines?: FlyMachine[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface FlyMachine {
|
|
34
|
-
id: string;
|
|
35
|
-
name: string;
|
|
36
|
-
state: string;
|
|
37
|
-
region: string;
|
|
38
|
-
instanceId: string;
|
|
39
|
-
privateIp: string;
|
|
40
|
-
config: FlyMachineConfig;
|
|
41
|
-
imageRef?: { repository: string; tag: string; digest: string };
|
|
42
|
-
createdAt: string;
|
|
43
|
-
updatedAt: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface FlyMachineConfig {
|
|
47
|
-
image: string;
|
|
48
|
-
env?: Record<string, string>;
|
|
49
|
-
services?: FlyMachineService[];
|
|
50
|
-
guest?: { cpus: number; cpu_kind: string; memory_mb: number };
|
|
51
|
-
size?: string;
|
|
52
|
-
auto_destroy?: boolean;
|
|
53
|
-
restart?: { policy: string; max_retries?: number };
|
|
54
|
-
mounts?: Array<{ volume: string; path: string }>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface FlyMachineService {
|
|
58
|
-
ports: Array<{ port: number; handlers: string[]; force_https?: boolean }>;
|
|
59
|
-
protocol: string;
|
|
60
|
-
internal_port: number;
|
|
61
|
-
autostop?: boolean;
|
|
62
|
-
autostart?: boolean;
|
|
63
|
-
min_machines_running?: number;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface FlyVolume {
|
|
67
|
-
id: string;
|
|
68
|
-
name: string;
|
|
69
|
-
state: string;
|
|
70
|
-
sizeGb: number;
|
|
71
|
-
region: string;
|
|
72
|
-
encrypted: boolean;
|
|
73
|
-
createdAt: string;
|
|
74
|
-
attachedMachineId?: string;
|
|
75
|
-
attachedAllocId?: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Service
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
export class FlyService {
|
|
83
|
-
private config: FlyConfig;
|
|
84
|
-
private running = false;
|
|
85
|
-
|
|
86
|
-
constructor(config: FlyConfig) {
|
|
87
|
-
this.config = {
|
|
88
|
-
...config,
|
|
89
|
-
baseUrl: config.baseUrl ?? 'https://api.machines.dev/v1',
|
|
90
|
-
graphqlUrl: config.graphqlUrl ?? 'https://api.fly.io/graphql',
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async initialize(): Promise<void> {
|
|
95
|
-
// Validate token by listing apps (lightweight org query)
|
|
96
|
-
const res = await this.gql<{ currentUser: { id: string } }>(`
|
|
97
|
-
query { currentUser { id email } }
|
|
98
|
-
`);
|
|
99
|
-
if (!res.currentUser?.id) {
|
|
100
|
-
throw new Error('Fly.io auth failed: could not retrieve user');
|
|
101
|
-
}
|
|
102
|
-
this.running = true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async shutdown(): Promise<void> {
|
|
106
|
-
this.running = false;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
get isRunning(): boolean {
|
|
110
|
-
return this.running;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Apps ─────────────────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
/** Create a new Fly app. */
|
|
116
|
-
async createApp(name: string, org?: string): Promise<FlyApp> {
|
|
117
|
-
const body: Record<string, unknown> = {
|
|
118
|
-
app_name: name,
|
|
119
|
-
org_slug: org ?? 'personal',
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const res = await this.fetch('/apps', {
|
|
123
|
-
method: 'POST',
|
|
124
|
-
body: JSON.stringify(body),
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Machines API returns minimal data on create; fetch full app
|
|
128
|
-
if (!res.ok) {
|
|
129
|
-
const text = await res.text();
|
|
130
|
-
throw new Error(`Fly.io API ${res.status}: ${text.slice(0, 500)}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return this.getApp(name);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Get an app by name. */
|
|
137
|
-
async getApp(appName: string): Promise<FlyApp> {
|
|
138
|
-
const result = await this.gql<{ app: any }>(`
|
|
139
|
-
query($name: String!) {
|
|
140
|
-
app(name: $name) {
|
|
141
|
-
id
|
|
142
|
-
name
|
|
143
|
-
organization { slug }
|
|
144
|
-
status
|
|
145
|
-
hostname
|
|
146
|
-
createdAt
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
`, { name: appName });
|
|
150
|
-
|
|
151
|
-
const a = result.app;
|
|
152
|
-
return {
|
|
153
|
-
id: a.id,
|
|
154
|
-
name: a.name,
|
|
155
|
-
organization: a.organization?.slug ?? '',
|
|
156
|
-
status: a.status ?? 'pending',
|
|
157
|
-
hostname: a.hostname ?? `${a.name}.fly.dev`,
|
|
158
|
-
createdAt: a.createdAt,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** List all apps in the personal org (via GraphQL). */
|
|
163
|
-
async listApps(): Promise<FlyApp[]> {
|
|
164
|
-
const result = await this.gql<{ apps: { nodes: any[] } }>(`
|
|
165
|
-
query {
|
|
166
|
-
apps {
|
|
167
|
-
nodes {
|
|
168
|
-
id
|
|
169
|
-
name
|
|
170
|
-
organization { slug }
|
|
171
|
-
status
|
|
172
|
-
hostname
|
|
173
|
-
createdAt
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
`);
|
|
178
|
-
|
|
179
|
-
return (result.apps?.nodes ?? []).map((a: any) => ({
|
|
180
|
-
id: a.id,
|
|
181
|
-
name: a.name,
|
|
182
|
-
organization: a.organization?.slug ?? '',
|
|
183
|
-
status: a.status ?? 'pending',
|
|
184
|
-
hostname: a.hostname ?? `${a.name}.fly.dev`,
|
|
185
|
-
createdAt: a.createdAt,
|
|
186
|
-
}));
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ── Machines ─────────────────────────────────────────────────────────────
|
|
190
|
-
|
|
191
|
-
/** Create a machine in an app. */
|
|
192
|
-
async createMachine(appName: string, config: FlyMachineConfig, opts?: {
|
|
193
|
-
name?: string;
|
|
194
|
-
region?: string;
|
|
195
|
-
}): Promise<FlyMachine> {
|
|
196
|
-
const body: Record<string, unknown> = { config };
|
|
197
|
-
if (opts?.name) body.name = opts.name;
|
|
198
|
-
if (opts?.region) body.region = opts.region;
|
|
199
|
-
|
|
200
|
-
const res = await this.fetch(`/apps/${encodeURIComponent(appName)}/machines`, {
|
|
201
|
-
method: 'POST',
|
|
202
|
-
body: JSON.stringify(body),
|
|
203
|
-
});
|
|
204
|
-
const data = await this.json(res);
|
|
205
|
-
return this.mapMachine(data);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** List all machines in an app. */
|
|
209
|
-
async listMachines(appName: string): Promise<FlyMachine[]> {
|
|
210
|
-
const res = await this.fetch(`/apps/${encodeURIComponent(appName)}/machines`);
|
|
211
|
-
const data = await this.json(res);
|
|
212
|
-
return (data as any[]).map((m: any) => this.mapMachine(m));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Stop and destroy a machine. */
|
|
216
|
-
async destroyMachine(appName: string, machineId: string, force = false): Promise<void> {
|
|
217
|
-
// Stop first
|
|
218
|
-
try {
|
|
219
|
-
await this.fetch(
|
|
220
|
-
`/apps/${encodeURIComponent(appName)}/machines/${machineId}/stop`,
|
|
221
|
-
{ method: 'POST', body: JSON.stringify({}) },
|
|
222
|
-
);
|
|
223
|
-
} catch {
|
|
224
|
-
// Machine may already be stopped
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const url = `/apps/${encodeURIComponent(appName)}/machines/${machineId}${force ? '?force=true' : ''}`;
|
|
228
|
-
const res = await this.fetch(url, { method: 'DELETE' });
|
|
229
|
-
if (!res.ok && res.status !== 404) {
|
|
230
|
-
const text = await res.text();
|
|
231
|
-
throw new Error(`Fly.io API ${res.status}: ${text.slice(0, 500)}`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** Update machine config (e.g. to change size/image). */
|
|
236
|
-
async updateMachine(appName: string, machineId: string, config: Partial<FlyMachineConfig>): Promise<FlyMachine> {
|
|
237
|
-
const res = await this.fetch(
|
|
238
|
-
`/apps/${encodeURIComponent(appName)}/machines/${machineId}`,
|
|
239
|
-
{
|
|
240
|
-
method: 'POST',
|
|
241
|
-
body: JSON.stringify({ config }),
|
|
242
|
-
},
|
|
243
|
-
);
|
|
244
|
-
const data = await this.json(res);
|
|
245
|
-
return this.mapMachine(data);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ── Volumes ──────────────────────────────────────────────────────────────
|
|
249
|
-
|
|
250
|
-
/** Create a persistent volume. */
|
|
251
|
-
async createVolume(appName: string, opts: {
|
|
252
|
-
name: string;
|
|
253
|
-
region: string;
|
|
254
|
-
sizeGb: number;
|
|
255
|
-
encrypted?: boolean;
|
|
256
|
-
}): Promise<FlyVolume> {
|
|
257
|
-
const res = await this.fetch(`/apps/${encodeURIComponent(appName)}/volumes`, {
|
|
258
|
-
method: 'POST',
|
|
259
|
-
body: JSON.stringify({
|
|
260
|
-
name: opts.name,
|
|
261
|
-
region: opts.region,
|
|
262
|
-
size_gb: opts.sizeGb,
|
|
263
|
-
encrypted: opts.encrypted ?? true,
|
|
264
|
-
}),
|
|
265
|
-
});
|
|
266
|
-
const data = await this.json(res);
|
|
267
|
-
return this.mapVolume(data);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** List volumes for an app. */
|
|
271
|
-
async listVolumes(appName: string): Promise<FlyVolume[]> {
|
|
272
|
-
const res = await this.fetch(`/apps/${encodeURIComponent(appName)}/volumes`);
|
|
273
|
-
const data = await this.json(res);
|
|
274
|
-
return (data as any[]).map((v: any) => this.mapVolume(v));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
278
|
-
|
|
279
|
-
private async fetch(path: string, init?: RequestInit): Promise<Response> {
|
|
280
|
-
const url = `${this.config.baseUrl}${path}`;
|
|
281
|
-
return globalThis.fetch(url, {
|
|
282
|
-
...init,
|
|
283
|
-
headers: {
|
|
284
|
-
'Authorization': `Bearer ${this.config.token}`,
|
|
285
|
-
'Content-Type': 'application/json',
|
|
286
|
-
...(init?.headers as Record<string, string> ?? {}),
|
|
287
|
-
},
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private async json(res: Response): Promise<any> {
|
|
292
|
-
if (!res.ok) {
|
|
293
|
-
const text = await res.text();
|
|
294
|
-
throw new Error(`Fly.io API ${res.status}: ${text.slice(0, 500)}`);
|
|
295
|
-
}
|
|
296
|
-
return res.json();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private async gql<T = any>(query: string, variables?: Record<string, unknown>): Promise<T> {
|
|
300
|
-
const res = await globalThis.fetch(this.config.graphqlUrl!, {
|
|
301
|
-
method: 'POST',
|
|
302
|
-
headers: {
|
|
303
|
-
'Authorization': `Bearer ${this.config.token}`,
|
|
304
|
-
'Content-Type': 'application/json',
|
|
305
|
-
},
|
|
306
|
-
body: JSON.stringify({ query, variables }),
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
if (!res.ok) {
|
|
310
|
-
const text = await res.text();
|
|
311
|
-
throw new Error(`Fly.io GraphQL ${res.status}: ${text.slice(0, 500)}`);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const json = await res.json() as { data?: T; errors?: Array<{ message: string }> };
|
|
315
|
-
|
|
316
|
-
if (json.errors && json.errors.length > 0) {
|
|
317
|
-
throw new Error(`Fly.io GraphQL: ${json.errors.map(e => e.message).join('; ')}`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return json.data as T;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
private mapMachine(data: any): FlyMachine {
|
|
324
|
-
return {
|
|
325
|
-
id: data.id,
|
|
326
|
-
name: data.name ?? '',
|
|
327
|
-
state: data.state ?? 'unknown',
|
|
328
|
-
region: data.region ?? '',
|
|
329
|
-
instanceId: data.instance_id ?? '',
|
|
330
|
-
privateIp: data.private_ip ?? '',
|
|
331
|
-
config: {
|
|
332
|
-
image: data.config?.image ?? '',
|
|
333
|
-
env: data.config?.env,
|
|
334
|
-
services: data.config?.services,
|
|
335
|
-
guest: data.config?.guest,
|
|
336
|
-
size: data.config?.size,
|
|
337
|
-
auto_destroy: data.config?.auto_destroy,
|
|
338
|
-
restart: data.config?.restart,
|
|
339
|
-
mounts: data.config?.mounts,
|
|
340
|
-
},
|
|
341
|
-
imageRef: data.image_ref ? {
|
|
342
|
-
repository: data.image_ref.repository ?? '',
|
|
343
|
-
tag: data.image_ref.tag ?? '',
|
|
344
|
-
digest: data.image_ref.digest ?? '',
|
|
345
|
-
} : undefined,
|
|
346
|
-
createdAt: data.created_at ?? '',
|
|
347
|
-
updatedAt: data.updated_at ?? '',
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
private mapVolume(data: any): FlyVolume {
|
|
352
|
-
return {
|
|
353
|
-
id: data.id,
|
|
354
|
-
name: data.name ?? '',
|
|
355
|
-
state: data.state ?? 'created',
|
|
356
|
-
sizeGb: data.size_gb ?? 0,
|
|
357
|
-
region: data.region ?? '',
|
|
358
|
-
encrypted: data.encrypted ?? true,
|
|
359
|
-
createdAt: data.created_at ?? '',
|
|
360
|
-
attachedMachineId: data.attached_machine_id,
|
|
361
|
-
attachedAllocId: data.attached_alloc_id,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview Fly.io Cloud Extension for AgentOS.
|
|
4
|
-
*
|
|
5
|
-
* Provides 4 tools for deploying apps, listing apps/machines, scaling
|
|
6
|
-
* instances, and creating persistent volumes via the Fly Machines API.
|
|
7
|
-
*
|
|
8
|
-
* @module @framers/agentos-ext-cloud-flyio
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { FlyService } from './FlyService.js';
|
|
12
|
-
import type { FlyConfig } from './FlyService.js';
|
|
13
|
-
import { FlyDeployAppTool } from './tools/deployApp.js';
|
|
14
|
-
import { FlyListAppsTool } from './tools/listApps.js';
|
|
15
|
-
import { FlyScaleAppTool } from './tools/scaleApp.js';
|
|
16
|
-
import { FlyCreateVolumeTool } from './tools/createVolume.js';
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Options
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
export interface FlyCloudOptions {
|
|
23
|
-
token?: string;
|
|
24
|
-
secrets?: Record<string, string>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Secret Resolution
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
function resolveConfig(opts: FlyCloudOptions, secrets: Record<string, string>): FlyConfig {
|
|
32
|
-
return {
|
|
33
|
-
token:
|
|
34
|
-
opts.token ?? secrets['fly.token']
|
|
35
|
-
?? process.env.FLY_API_TOKEN ?? '',
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
// Extension Context
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
export interface ExtensionContext {
|
|
44
|
-
options?: Record<string, unknown>;
|
|
45
|
-
secrets?: Record<string, string>;
|
|
46
|
-
getSecret?: (key: string) => string | undefined;
|
|
47
|
-
logger?: { info: (msg: string) => void };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface ExtensionPack {
|
|
51
|
-
name: string;
|
|
52
|
-
version: string;
|
|
53
|
-
descriptors: Array<{ id: string; kind: string; priority?: number; payload: unknown }>;
|
|
54
|
-
onActivate?: () => Promise<void>;
|
|
55
|
-
onDeactivate?: () => Promise<void>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Factory
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
export function createExtensionPack(context: ExtensionContext): ExtensionPack {
|
|
63
|
-
const opts = (context.options ?? {}) as FlyCloudOptions;
|
|
64
|
-
const secrets = opts.secrets ?? context.secrets ?? {};
|
|
65
|
-
const config = resolveConfig(opts, secrets);
|
|
66
|
-
|
|
67
|
-
const service = new FlyService(config);
|
|
68
|
-
|
|
69
|
-
const deployAppTool = new FlyDeployAppTool(service);
|
|
70
|
-
const listAppsTool = new FlyListAppsTool(service);
|
|
71
|
-
const scaleAppTool = new FlyScaleAppTool(service);
|
|
72
|
-
const createVolumeTool = new FlyCreateVolumeTool(service);
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
name: '@framers/agentos-ext-cloud-flyio',
|
|
76
|
-
version: '0.1.0',
|
|
77
|
-
descriptors: [
|
|
78
|
-
{ id: 'flyDeployApp', kind: 'tool', priority: 40, payload: deployAppTool },
|
|
79
|
-
{ id: 'flyListApps', kind: 'tool', priority: 40, payload: listAppsTool },
|
|
80
|
-
{ id: 'flyScaleApp', kind: 'tool', priority: 40, payload: scaleAppTool },
|
|
81
|
-
{ id: 'flyCreateVolume', kind: 'tool', priority: 40, payload: createVolumeTool },
|
|
82
|
-
],
|
|
83
|
-
onActivate: async () => {
|
|
84
|
-
if (!config.token) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
'Fly.io: no API token provided. Set FLY_API_TOKEN environment variable, '
|
|
87
|
-
+ 'or provide it via secrets["fly.token"].',
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
await service.initialize();
|
|
91
|
-
},
|
|
92
|
-
onDeactivate: async () => {
|
|
93
|
-
await service.shutdown();
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Public exports
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
export { FlyService } from './FlyService.js';
|
|
103
|
-
export type { FlyConfig, FlyApp, FlyMachine, FlyMachineConfig, FlyMachineService, FlyVolume } from './FlyService.js';
|
|
104
|
-
export { FlyDeployAppTool } from './tools/deployApp.js';
|
|
105
|
-
export { FlyListAppsTool } from './tools/listApps.js';
|
|
106
|
-
export { FlyScaleAppTool } from './tools/scaleApp.js';
|
|
107
|
-
export { FlyCreateVolumeTool } from './tools/createVolume.js';
|
|
108
|
-
|
|
109
|
-
export default createExtensionPack;
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview FlyCreateVolumeTool — create a persistent volume on Fly.io.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { FlyService, FlyVolume } from '../FlyService.js';
|
|
7
|
-
|
|
8
|
-
export interface FlyCreateVolumeInput {
|
|
9
|
-
appName: string;
|
|
10
|
-
name: string;
|
|
11
|
-
region: string;
|
|
12
|
-
sizeGb: number;
|
|
13
|
-
encrypted?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class FlyCreateVolumeTool {
|
|
17
|
-
readonly id = 'flyCreateVolume';
|
|
18
|
-
readonly name = 'flyCreateVolume';
|
|
19
|
-
readonly displayName = 'Create Fly.io Volume';
|
|
20
|
-
readonly description = 'Create a persistent volume for a Fly.io app. Volumes persist data across machine restarts and redeployments. Must be in the same region as the machine that mounts it. Encrypted by default.';
|
|
21
|
-
readonly category = 'cloud';
|
|
22
|
-
readonly version = '0.1.0';
|
|
23
|
-
readonly hasSideEffects = true;
|
|
24
|
-
|
|
25
|
-
readonly inputSchema = {
|
|
26
|
-
type: 'object' as const,
|
|
27
|
-
properties: {
|
|
28
|
-
appName: { type: 'string', description: 'Name of the Fly app to create the volume in' },
|
|
29
|
-
name: { type: 'string', description: 'Volume name (used in mount config, e.g. "data", "postgres_data")' },
|
|
30
|
-
region: { type: 'string', description: 'Region for the volume (must match the machine region, e.g. "iad", "lax", "cdg")' },
|
|
31
|
-
sizeGb: { type: 'number', description: 'Volume size in GB (minimum: 1)' },
|
|
32
|
-
encrypted: { type: 'boolean', description: 'Whether the volume should be encrypted (default: true)' },
|
|
33
|
-
},
|
|
34
|
-
required: ['appName', 'name', 'region', 'sizeGb'],
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
constructor(private service: FlyService) {}
|
|
38
|
-
|
|
39
|
-
async execute(args: FlyCreateVolumeInput): Promise<{ success: boolean; data?: FlyVolume; error?: string }> {
|
|
40
|
-
try {
|
|
41
|
-
const volume = await this.service.createVolume(args.appName, {
|
|
42
|
-
name: args.name,
|
|
43
|
-
region: args.region,
|
|
44
|
-
sizeGb: args.sizeGb,
|
|
45
|
-
encrypted: args.encrypted,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
return { success: true, data: volume };
|
|
49
|
-
} catch (err: any) {
|
|
50
|
-
return { success: false, error: err.message };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
package/src/tools/deployApp.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview FlyDeployAppTool — deploy an app on Fly.io by creating a machine with a config.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { FlyService, FlyMachine, FlyMachineConfig, FlyApp } from '../FlyService.js';
|
|
7
|
-
|
|
8
|
-
export interface FlyDeployAppInput {
|
|
9
|
-
appName: string;
|
|
10
|
-
image: string;
|
|
11
|
-
region?: string;
|
|
12
|
-
cpus?: number;
|
|
13
|
-
cpuKind?: string;
|
|
14
|
-
memoryMb?: number;
|
|
15
|
-
env?: Record<string, string>;
|
|
16
|
-
internalPort?: number;
|
|
17
|
-
autoDestroy?: boolean;
|
|
18
|
-
createApp?: boolean;
|
|
19
|
-
org?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class FlyDeployAppTool {
|
|
23
|
-
readonly id = 'flyDeployApp';
|
|
24
|
-
readonly name = 'flyDeployApp';
|
|
25
|
-
readonly displayName = 'Deploy Fly.io App';
|
|
26
|
-
readonly description = 'Deploy an application on Fly.io by creating a machine with a Docker image. Creates the app first if it does not exist. Configure CPU, memory, region, environment variables, and exposed ports.';
|
|
27
|
-
readonly category = 'cloud';
|
|
28
|
-
readonly version = '0.1.0';
|
|
29
|
-
readonly hasSideEffects = true;
|
|
30
|
-
|
|
31
|
-
readonly inputSchema = {
|
|
32
|
-
type: 'object' as const,
|
|
33
|
-
properties: {
|
|
34
|
-
appName: { type: 'string', description: 'Fly app name (e.g. "my-web-app")' },
|
|
35
|
-
image: { type: 'string', description: 'Docker image to deploy (e.g. "registry.fly.io/my-app:latest" or "nginx:alpine")' },
|
|
36
|
-
region: { type: 'string', description: 'Fly region code (e.g. "iad", "lax", "cdg", "nrt"). Defaults to closest.' },
|
|
37
|
-
cpus: { type: 'number', description: 'Number of CPUs (default: 1)' },
|
|
38
|
-
cpuKind: { type: 'string', enum: ['shared', 'performance'], description: 'CPU type (default: shared)' },
|
|
39
|
-
memoryMb: { type: 'number', description: 'Memory in MB (default: 256)' },
|
|
40
|
-
env: { type: 'object', description: 'Environment variables (key-value pairs)' },
|
|
41
|
-
internalPort: { type: 'number', description: 'Internal port the app listens on (default: 8080)' },
|
|
42
|
-
autoDestroy: { type: 'boolean', description: 'Automatically destroy the machine when it exits (default: false)' },
|
|
43
|
-
createApp: { type: 'boolean', description: 'Create the app if it does not exist (default: true)' },
|
|
44
|
-
org: { type: 'string', description: 'Fly organization slug (default: "personal")' },
|
|
45
|
-
},
|
|
46
|
-
required: ['appName', 'image'],
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
constructor(private service: FlyService) {}
|
|
50
|
-
|
|
51
|
-
async execute(args: FlyDeployAppInput): Promise<{ success: boolean; data?: { app: FlyApp; machine: FlyMachine }; error?: string }> {
|
|
52
|
-
try {
|
|
53
|
-
// Create app if needed
|
|
54
|
-
const shouldCreate = args.createApp !== false;
|
|
55
|
-
let app: FlyApp;
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
app = await this.service.getApp(args.appName);
|
|
59
|
-
} catch {
|
|
60
|
-
if (!shouldCreate) {
|
|
61
|
-
return { success: false, error: `App "${args.appName}" not found. Set createApp to true to create it.` };
|
|
62
|
-
}
|
|
63
|
-
app = await this.service.createApp(args.appName, args.org);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Build machine config
|
|
67
|
-
const port = args.internalPort ?? 8080;
|
|
68
|
-
const config: FlyMachineConfig = {
|
|
69
|
-
image: args.image,
|
|
70
|
-
env: args.env,
|
|
71
|
-
guest: {
|
|
72
|
-
cpus: args.cpus ?? 1,
|
|
73
|
-
cpu_kind: args.cpuKind ?? 'shared',
|
|
74
|
-
memory_mb: args.memoryMb ?? 256,
|
|
75
|
-
},
|
|
76
|
-
auto_destroy: args.autoDestroy ?? false,
|
|
77
|
-
services: [
|
|
78
|
-
{
|
|
79
|
-
ports: [
|
|
80
|
-
{ port: 80, handlers: ['http'], force_https: true },
|
|
81
|
-
{ port: 443, handlers: ['http', 'tls'] },
|
|
82
|
-
],
|
|
83
|
-
protocol: 'tcp',
|
|
84
|
-
internal_port: port,
|
|
85
|
-
autostart: true,
|
|
86
|
-
autostop: true,
|
|
87
|
-
min_machines_running: 0,
|
|
88
|
-
},
|
|
89
|
-
],
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const machine = await this.service.createMachine(args.appName, config, {
|
|
93
|
-
region: args.region,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
success: true,
|
|
98
|
-
data: { app, machine },
|
|
99
|
-
};
|
|
100
|
-
} catch (err: any) {
|
|
101
|
-
return { success: false, error: err.message };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|