@framers/agentos-ext-cloud-flyio 0.1.0 → 0.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.
- package/manifest.json +4 -4
- package/package.json +15 -7
- 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/test/tools.test.ts
DELETED
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import { FlyCreateVolumeTool } from '../src/tools/createVolume';
|
|
4
|
-
import { FlyDeployAppTool } from '../src/tools/deployApp';
|
|
5
|
-
import { FlyListAppsTool } from '../src/tools/listApps';
|
|
6
|
-
import { FlyScaleAppTool } from '../src/tools/scaleApp';
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// FlyCreateVolumeTool
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
describe('FlyCreateVolumeTool', () => {
|
|
13
|
-
let service: any;
|
|
14
|
-
let tool: FlyCreateVolumeTool;
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
service = {
|
|
18
|
-
createVolume: vi.fn(),
|
|
19
|
-
};
|
|
20
|
-
tool = new FlyCreateVolumeTool(service);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should have correct metadata', () => {
|
|
24
|
-
expect(tool.id).toBe('flyCreateVolume');
|
|
25
|
-
expect(tool.name).toBe('flyCreateVolume');
|
|
26
|
-
expect(tool.displayName).toBe('Create Fly.io Volume');
|
|
27
|
-
expect(tool.inputSchema.type).toBe('object');
|
|
28
|
-
expect(tool.inputSchema.required).toEqual(
|
|
29
|
-
expect.arrayContaining(['appName', 'name', 'region', 'sizeGb']),
|
|
30
|
-
);
|
|
31
|
-
expect(tool.category).toBe('cloud');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should create a volume successfully', async () => {
|
|
35
|
-
service.createVolume.mockResolvedValue({
|
|
36
|
-
id: 'vol_abc123',
|
|
37
|
-
name: 'data',
|
|
38
|
-
region: 'iad',
|
|
39
|
-
sizeGb: 10,
|
|
40
|
-
encrypted: true,
|
|
41
|
-
state: 'created',
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const result = await tool.execute({
|
|
45
|
-
appName: 'my-app',
|
|
46
|
-
name: 'data',
|
|
47
|
-
region: 'iad',
|
|
48
|
-
sizeGb: 10,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
expect(result.success).toBe(true);
|
|
52
|
-
expect(result.data).toHaveProperty('id', 'vol_abc123');
|
|
53
|
-
expect(result.data).toHaveProperty('sizeGb', 10);
|
|
54
|
-
expect(service.createVolume).toHaveBeenCalledWith('my-app', {
|
|
55
|
-
name: 'data',
|
|
56
|
-
region: 'iad',
|
|
57
|
-
sizeGb: 10,
|
|
58
|
-
encrypted: undefined,
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should pass encrypted flag', async () => {
|
|
63
|
-
service.createVolume.mockResolvedValue({ id: 'vol_1', encrypted: false });
|
|
64
|
-
|
|
65
|
-
await tool.execute({
|
|
66
|
-
appName: 'my-app',
|
|
67
|
-
name: 'data',
|
|
68
|
-
region: 'lax',
|
|
69
|
-
sizeGb: 5,
|
|
70
|
-
encrypted: false,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
expect(service.createVolume).toHaveBeenCalledWith('my-app', expect.objectContaining({
|
|
74
|
-
encrypted: false,
|
|
75
|
-
}));
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should handle errors gracefully', async () => {
|
|
79
|
-
service.createVolume.mockRejectedValue(new Error('Insufficient capacity'));
|
|
80
|
-
|
|
81
|
-
const result = await tool.execute({
|
|
82
|
-
appName: 'my-app',
|
|
83
|
-
name: 'data',
|
|
84
|
-
region: 'iad',
|
|
85
|
-
sizeGb: 500,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
expect(result.success).toBe(false);
|
|
89
|
-
expect(result.error).toContain('Insufficient capacity');
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// FlyDeployAppTool
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
describe('FlyDeployAppTool', () => {
|
|
98
|
-
let service: any;
|
|
99
|
-
let tool: FlyDeployAppTool;
|
|
100
|
-
|
|
101
|
-
beforeEach(() => {
|
|
102
|
-
service = {
|
|
103
|
-
getApp: vi.fn(),
|
|
104
|
-
createApp: vi.fn(),
|
|
105
|
-
createMachine: vi.fn(),
|
|
106
|
-
};
|
|
107
|
-
tool = new FlyDeployAppTool(service);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should have correct metadata', () => {
|
|
111
|
-
expect(tool.id).toBe('flyDeployApp');
|
|
112
|
-
expect(tool.name).toBe('flyDeployApp');
|
|
113
|
-
expect(tool.inputSchema.type).toBe('object');
|
|
114
|
-
expect(tool.inputSchema.required).toContain('appName');
|
|
115
|
-
expect(tool.inputSchema.required).toContain('image');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should deploy to an existing app', async () => {
|
|
119
|
-
service.getApp.mockResolvedValue({
|
|
120
|
-
name: 'my-app',
|
|
121
|
-
status: 'deployed',
|
|
122
|
-
organization: { slug: 'personal' },
|
|
123
|
-
});
|
|
124
|
-
service.createMachine.mockResolvedValue({
|
|
125
|
-
id: 'machine-1',
|
|
126
|
-
state: 'started',
|
|
127
|
-
region: 'iad',
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const result = await tool.execute({
|
|
131
|
-
appName: 'my-app',
|
|
132
|
-
image: 'nginx:alpine',
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
expect(result.success).toBe(true);
|
|
136
|
-
expect(result.data?.app.name).toBe('my-app');
|
|
137
|
-
expect(result.data?.machine.id).toBe('machine-1');
|
|
138
|
-
expect(service.getApp).toHaveBeenCalledWith('my-app');
|
|
139
|
-
expect(service.createApp).not.toHaveBeenCalled();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('should create app if it does not exist', async () => {
|
|
143
|
-
service.getApp.mockRejectedValue(new Error('Not found'));
|
|
144
|
-
service.createApp.mockResolvedValue({ name: 'new-app', status: 'pending' });
|
|
145
|
-
service.createMachine.mockResolvedValue({ id: 'machine-2', state: 'started' });
|
|
146
|
-
|
|
147
|
-
const result = await tool.execute({
|
|
148
|
-
appName: 'new-app',
|
|
149
|
-
image: 'node:20-alpine',
|
|
150
|
-
org: 'my-org',
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
expect(result.success).toBe(true);
|
|
154
|
-
expect(service.createApp).toHaveBeenCalledWith('new-app', 'my-org');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should fail if app does not exist and createApp is false', async () => {
|
|
158
|
-
service.getApp.mockRejectedValue(new Error('Not found'));
|
|
159
|
-
|
|
160
|
-
const result = await tool.execute({
|
|
161
|
-
appName: 'nonexistent',
|
|
162
|
-
image: 'nginx:alpine',
|
|
163
|
-
createApp: false,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
expect(result.success).toBe(false);
|
|
167
|
-
expect(result.error).toContain('not found');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should configure machine with custom resources', async () => {
|
|
171
|
-
service.getApp.mockResolvedValue({ name: 'my-app' });
|
|
172
|
-
service.createMachine.mockResolvedValue({ id: 'machine-3' });
|
|
173
|
-
|
|
174
|
-
await tool.execute({
|
|
175
|
-
appName: 'my-app',
|
|
176
|
-
image: 'nginx:alpine',
|
|
177
|
-
cpus: 2,
|
|
178
|
-
cpuKind: 'performance',
|
|
179
|
-
memoryMb: 512,
|
|
180
|
-
internalPort: 3000,
|
|
181
|
-
env: { NODE_ENV: 'production' },
|
|
182
|
-
region: 'cdg',
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
expect(service.createMachine).toHaveBeenCalledWith(
|
|
186
|
-
'my-app',
|
|
187
|
-
expect.objectContaining({
|
|
188
|
-
image: 'nginx:alpine',
|
|
189
|
-
env: { NODE_ENV: 'production' },
|
|
190
|
-
guest: expect.objectContaining({
|
|
191
|
-
cpus: 2,
|
|
192
|
-
cpu_kind: 'performance',
|
|
193
|
-
memory_mb: 512,
|
|
194
|
-
}),
|
|
195
|
-
}),
|
|
196
|
-
{ region: 'cdg' },
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should handle errors gracefully', async () => {
|
|
201
|
-
service.getApp.mockResolvedValue({ name: 'my-app' });
|
|
202
|
-
service.createMachine.mockRejectedValue(new Error('Image not found'));
|
|
203
|
-
|
|
204
|
-
const result = await tool.execute({
|
|
205
|
-
appName: 'my-app',
|
|
206
|
-
image: 'nonexistent:latest',
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
expect(result.success).toBe(false);
|
|
210
|
-
expect(result.error).toContain('Image not found');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// FlyListAppsTool
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
describe('FlyListAppsTool', () => {
|
|
219
|
-
let service: any;
|
|
220
|
-
let tool: FlyListAppsTool;
|
|
221
|
-
|
|
222
|
-
beforeEach(() => {
|
|
223
|
-
service = {
|
|
224
|
-
listApps: vi.fn(),
|
|
225
|
-
getApp: vi.fn(),
|
|
226
|
-
listMachines: vi.fn(),
|
|
227
|
-
};
|
|
228
|
-
tool = new FlyListAppsTool(service);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('should have correct metadata', () => {
|
|
232
|
-
expect(tool.id).toBe('flyListApps');
|
|
233
|
-
expect(tool.name).toBe('flyListApps');
|
|
234
|
-
expect(tool.inputSchema.type).toBe('object');
|
|
235
|
-
expect(tool.inputSchema.required).toEqual([]);
|
|
236
|
-
expect(tool.hasSideEffects).toBe(false);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('should list all apps with machines', async () => {
|
|
240
|
-
service.listApps.mockResolvedValue([
|
|
241
|
-
{ name: 'app-1', status: 'deployed', machines: undefined },
|
|
242
|
-
{ name: 'app-2', status: 'deployed', machines: undefined },
|
|
243
|
-
]);
|
|
244
|
-
service.listMachines.mockResolvedValue([
|
|
245
|
-
{ id: 'm-1', state: 'started', region: 'iad' },
|
|
246
|
-
]);
|
|
247
|
-
|
|
248
|
-
const result = await tool.execute({});
|
|
249
|
-
|
|
250
|
-
expect(result.success).toBe(true);
|
|
251
|
-
expect(result.data).toHaveLength(2);
|
|
252
|
-
expect(service.listMachines).toHaveBeenCalledTimes(2);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('should get a single app by name', async () => {
|
|
256
|
-
service.getApp.mockResolvedValue({ name: 'my-app', status: 'deployed' });
|
|
257
|
-
service.listMachines.mockResolvedValue([{ id: 'm-1' }]);
|
|
258
|
-
|
|
259
|
-
const result = await tool.execute({ appName: 'my-app' });
|
|
260
|
-
|
|
261
|
-
expect(result.success).toBe(true);
|
|
262
|
-
expect(result.data).toHaveLength(1);
|
|
263
|
-
expect(result.data![0].name).toBe('my-app');
|
|
264
|
-
expect(service.getApp).toHaveBeenCalledWith('my-app');
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('should skip machines when includeMachines is false', async () => {
|
|
268
|
-
service.listApps.mockResolvedValue([{ name: 'app-1' }]);
|
|
269
|
-
|
|
270
|
-
const result = await tool.execute({ includeMachines: false });
|
|
271
|
-
|
|
272
|
-
expect(result.success).toBe(true);
|
|
273
|
-
expect(service.listMachines).not.toHaveBeenCalled();
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('should handle machine listing errors gracefully per-app', async () => {
|
|
277
|
-
service.listApps.mockResolvedValue([
|
|
278
|
-
{ name: 'app-1', machines: undefined },
|
|
279
|
-
]);
|
|
280
|
-
service.listMachines.mockRejectedValue(new Error('Forbidden'));
|
|
281
|
-
|
|
282
|
-
const result = await tool.execute({});
|
|
283
|
-
|
|
284
|
-
expect(result.success).toBe(true);
|
|
285
|
-
expect(result.data![0].machines).toEqual([]);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('should handle errors gracefully', async () => {
|
|
289
|
-
service.listApps.mockRejectedValue(new Error('Unauthorized'));
|
|
290
|
-
|
|
291
|
-
const result = await tool.execute({});
|
|
292
|
-
|
|
293
|
-
expect(result.success).toBe(false);
|
|
294
|
-
expect(result.error).toContain('Unauthorized');
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// ---------------------------------------------------------------------------
|
|
299
|
-
// FlyScaleAppTool
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
|
|
302
|
-
describe('FlyScaleAppTool', () => {
|
|
303
|
-
let service: any;
|
|
304
|
-
let tool: FlyScaleAppTool;
|
|
305
|
-
|
|
306
|
-
beforeEach(() => {
|
|
307
|
-
service = {
|
|
308
|
-
listMachines: vi.fn(),
|
|
309
|
-
createMachine: vi.fn(),
|
|
310
|
-
destroyMachine: vi.fn(),
|
|
311
|
-
updateMachine: vi.fn(),
|
|
312
|
-
};
|
|
313
|
-
tool = new FlyScaleAppTool(service);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('should have correct metadata', () => {
|
|
317
|
-
expect(tool.id).toBe('flyScaleApp');
|
|
318
|
-
expect(tool.name).toBe('flyScaleApp');
|
|
319
|
-
expect(tool.inputSchema.type).toBe('object');
|
|
320
|
-
expect(tool.inputSchema.required).toContain('appName');
|
|
321
|
-
expect(tool.inputSchema.required).toContain('count');
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('should scale up by creating new machines', async () => {
|
|
325
|
-
service.listMachines
|
|
326
|
-
.mockResolvedValueOnce([
|
|
327
|
-
{ id: 'm-1', config: { image: 'nginx', guest: { cpus: 1, cpu_kind: 'shared', memory_mb: 256 } } },
|
|
328
|
-
])
|
|
329
|
-
.mockResolvedValueOnce([
|
|
330
|
-
{ id: 'm-1', config: {} },
|
|
331
|
-
{ id: 'm-2', config: {} },
|
|
332
|
-
{ id: 'm-3', config: {} },
|
|
333
|
-
]);
|
|
334
|
-
service.createMachine.mockResolvedValue({ id: 'm-new' });
|
|
335
|
-
|
|
336
|
-
const result = await tool.execute({ appName: 'my-app', count: 3 });
|
|
337
|
-
|
|
338
|
-
expect(result.success).toBe(true);
|
|
339
|
-
expect(result.data?.created).toBe(2);
|
|
340
|
-
expect(result.data?.destroyed).toBe(0);
|
|
341
|
-
expect(service.createMachine).toHaveBeenCalledTimes(2);
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it('should scale down by destroying excess machines', async () => {
|
|
345
|
-
service.listMachines
|
|
346
|
-
.mockResolvedValueOnce([
|
|
347
|
-
{ id: 'm-1', config: {} },
|
|
348
|
-
{ id: 'm-2', config: {} },
|
|
349
|
-
{ id: 'm-3', config: {} },
|
|
350
|
-
])
|
|
351
|
-
.mockResolvedValueOnce([{ id: 'm-1', config: {} }]);
|
|
352
|
-
service.destroyMachine.mockResolvedValue(undefined);
|
|
353
|
-
|
|
354
|
-
const result = await tool.execute({ appName: 'my-app', count: 1 });
|
|
355
|
-
|
|
356
|
-
expect(result.success).toBe(true);
|
|
357
|
-
expect(result.data?.destroyed).toBe(2);
|
|
358
|
-
expect(service.destroyMachine).toHaveBeenCalledTimes(2);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('should resize existing machines when resource params change', async () => {
|
|
362
|
-
service.listMachines
|
|
363
|
-
.mockResolvedValueOnce([
|
|
364
|
-
{ id: 'm-1', config: { guest: { cpus: 1, cpu_kind: 'shared', memory_mb: 256 } } },
|
|
365
|
-
])
|
|
366
|
-
.mockResolvedValueOnce([{ id: 'm-1' }]);
|
|
367
|
-
service.updateMachine.mockResolvedValue({});
|
|
368
|
-
|
|
369
|
-
const result = await tool.execute({
|
|
370
|
-
appName: 'my-app',
|
|
371
|
-
count: 1,
|
|
372
|
-
cpus: 2,
|
|
373
|
-
memoryMb: 512,
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
expect(result.success).toBe(true);
|
|
377
|
-
expect(result.data?.resized).toBe(1);
|
|
378
|
-
expect(service.updateMachine).toHaveBeenCalledWith('my-app', 'm-1', {
|
|
379
|
-
guest: expect.objectContaining({ cpus: 2, memory_mb: 512 }),
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('should scale to zero by destroying all machines', async () => {
|
|
384
|
-
service.listMachines
|
|
385
|
-
.mockResolvedValueOnce([
|
|
386
|
-
{ id: 'm-1', config: {} },
|
|
387
|
-
{ id: 'm-2', config: {} },
|
|
388
|
-
])
|
|
389
|
-
.mockResolvedValueOnce([]);
|
|
390
|
-
service.destroyMachine.mockResolvedValue(undefined);
|
|
391
|
-
|
|
392
|
-
const result = await tool.execute({ appName: 'my-app', count: 0 });
|
|
393
|
-
|
|
394
|
-
expect(result.success).toBe(true);
|
|
395
|
-
expect(result.data?.destroyed).toBe(2);
|
|
396
|
-
expect(result.data?.machines).toEqual([]);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('should handle errors gracefully', async () => {
|
|
400
|
-
service.listMachines.mockRejectedValue(new Error('App not found'));
|
|
401
|
-
|
|
402
|
-
const result = await tool.execute({ appName: 'nonexistent', count: 2 });
|
|
403
|
-
|
|
404
|
-
expect(result.success).toBe(false);
|
|
405
|
-
expect(result.error).toContain('App not found');
|
|
406
|
-
});
|
|
407
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2022"],
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"noEmitOnError": false,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"declaration": true,
|
|
14
|
-
"declarationMap": true,
|
|
15
|
-
"sourceMap": true,
|
|
16
|
-
"resolveJsonModule": true,
|
|
17
|
-
"moduleResolution": "Bundler",
|
|
18
|
-
"types": ["node"]
|
|
19
|
-
},
|
|
20
|
-
"include": ["src/**/*"],
|
|
21
|
-
"exclude": ["node_modules", "dist", "test"]
|
|
22
|
-
}
|