@occam-scaly/mcp-server 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.
Files changed (91) hide show
  1. package/README.md +166 -0
  2. package/bin/scaly-mcp.js +6 -0
  3. package/dist/audit.d.ts +25 -0
  4. package/dist/audit.d.ts.map +1 -0
  5. package/dist/audit.js +182 -0
  6. package/dist/audit.js.map +1 -0
  7. package/dist/client.d.ts +16 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +142 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/config.d.ts +24 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +76 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +279 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/resources/index.d.ts +17 -0
  20. package/dist/resources/index.d.ts.map +1 -0
  21. package/dist/resources/index.js +505 -0
  22. package/dist/resources/index.js.map +1 -0
  23. package/dist/tools/addons.d.ts +3 -0
  24. package/dist/tools/addons.d.ts.map +1 -0
  25. package/dist/tools/addons.js +128 -0
  26. package/dist/tools/addons.js.map +1 -0
  27. package/dist/tools/apps.d.ts +3 -0
  28. package/dist/tools/apps.d.ts.map +1 -0
  29. package/dist/tools/apps.js +121 -0
  30. package/dist/tools/apps.js.map +1 -0
  31. package/dist/tools/compute.d.ts +3 -0
  32. package/dist/tools/compute.d.ts.map +1 -0
  33. package/dist/tools/compute.js +135 -0
  34. package/dist/tools/compute.js.map +1 -0
  35. package/dist/tools/db.d.ts +3 -0
  36. package/dist/tools/db.d.ts.map +1 -0
  37. package/dist/tools/db.js +597 -0
  38. package/dist/tools/db.js.map +1 -0
  39. package/dist/tools/deployments.d.ts +3 -0
  40. package/dist/tools/deployments.d.ts.map +1 -0
  41. package/dist/tools/deployments.js +354 -0
  42. package/dist/tools/deployments.js.map +1 -0
  43. package/dist/tools/diagnose.d.ts +3 -0
  44. package/dist/tools/diagnose.d.ts.map +1 -0
  45. package/dist/tools/diagnose.js +213 -0
  46. package/dist/tools/diagnose.js.map +1 -0
  47. package/dist/tools/env.d.ts +3 -0
  48. package/dist/tools/env.d.ts.map +1 -0
  49. package/dist/tools/env.js +216 -0
  50. package/dist/tools/env.js.map +1 -0
  51. package/dist/tools/index.d.ts +38 -0
  52. package/dist/tools/index.d.ts.map +1 -0
  53. package/dist/tools/index.js +48 -0
  54. package/dist/tools/index.js.map +1 -0
  55. package/dist/tools/jobs.d.ts +3 -0
  56. package/dist/tools/jobs.d.ts.map +1 -0
  57. package/dist/tools/jobs.js +160 -0
  58. package/dist/tools/jobs.js.map +1 -0
  59. package/dist/tools/logs.d.ts +3 -0
  60. package/dist/tools/logs.d.ts.map +1 -0
  61. package/dist/tools/logs.js +157 -0
  62. package/dist/tools/logs.js.map +1 -0
  63. package/dist/tools/operations.d.ts +3 -0
  64. package/dist/tools/operations.d.ts.map +1 -0
  65. package/dist/tools/operations.js +262 -0
  66. package/dist/tools/operations.js.map +1 -0
  67. package/dist/tools/project.d.ts +3 -0
  68. package/dist/tools/project.d.ts.map +1 -0
  69. package/dist/tools/project.js +341 -0
  70. package/dist/tools/project.js.map +1 -0
  71. package/dist/tools/s3.d.ts +3 -0
  72. package/dist/tools/s3.d.ts.map +1 -0
  73. package/dist/tools/s3.js +256 -0
  74. package/dist/tools/s3.js.map +1 -0
  75. package/dist/tools/status.d.ts +3 -0
  76. package/dist/tools/status.d.ts.map +1 -0
  77. package/dist/tools/status.js +139 -0
  78. package/dist/tools/status.js.map +1 -0
  79. package/dist/tools/write.d.ts +3 -0
  80. package/dist/tools/write.d.ts.map +1 -0
  81. package/dist/tools/write.js +900 -0
  82. package/dist/tools/write.js.map +1 -0
  83. package/dist/utils/jwt.d.ts +3 -0
  84. package/dist/utils/jwt.d.ts.map +1 -0
  85. package/dist/utils/jwt.js +32 -0
  86. package/dist/utils/jwt.js.map +1 -0
  87. package/dist/utils/paths.d.ts +5 -0
  88. package/dist/utils/paths.d.ts.map +1 -0
  89. package/dist/utils/paths.js +26 -0
  90. package/dist/utils/paths.js.map +1 -0
  91. package/package.json +50 -0
@@ -0,0 +1,900 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeTools = void 0;
4
+ const zod_1 = require("zod");
5
+ const client_js_1 = require("../client.js");
6
+ const index_js_1 = require("./index.js");
7
+ function estimateDbMonthlyUsd(size) {
8
+ return ({
9
+ Micro: 10,
10
+ Small: 20,
11
+ Medium: 40,
12
+ Large: 80,
13
+ XLarge: 160
14
+ }[size] ?? 20);
15
+ }
16
+ function estimateStackMonthlyUsd(size) {
17
+ return ({
18
+ Eco: 8,
19
+ Basic: 16,
20
+ Standard_1X: 32,
21
+ Standard_2X: 64,
22
+ Performance_M: 128,
23
+ Performance_L: 256
24
+ }[size] ?? 16);
25
+ }
26
+ async function resolveAccountId(explicit) {
27
+ if (explicit)
28
+ return explicit;
29
+ const result = await (0, client_js_1.query)(`
30
+ query ResolveAccountId {
31
+ listStacks(take: 1) { accountId }
32
+ }
33
+ `);
34
+ return result.listStacks?.[0]?.accountId ?? null;
35
+ }
36
+ async function getStackById(stackId) {
37
+ const result = await (0, client_js_1.query)(`
38
+ query GetStack($where: StackWhereUniqueInput!) {
39
+ getStack(where: $where) { id name accountId size minIdle }
40
+ }
41
+ `, { where: { id: stackId } });
42
+ return result.getStack || null;
43
+ }
44
+ async function getAppById(appId) {
45
+ const result = await (0, client_js_1.query)(`
46
+ query GetApp($where: AppWhereUniqueInput!) {
47
+ getApp(where: $where) { id name accountId stackId }
48
+ }
49
+ `, { where: { id: appId } });
50
+ return result.getApp || null;
51
+ }
52
+ async function getAddOnById(addonId) {
53
+ const result = await (0, client_js_1.query)(`
54
+ query GetAddOn($where: AddOnWhereUniqueInput!) {
55
+ getAddOn(where: $where) { id name type accountId }
56
+ }
57
+ `, { where: { id: addonId } });
58
+ return result.getAddOn || null;
59
+ }
60
+ async function stackExistsByName(accountId, name) {
61
+ const gql = `
62
+ query FindStackByName($accountId: String!, $name: String!) {
63
+ listStacks(
64
+ where: {
65
+ AND: [
66
+ { accountId: { equals: $accountId } }
67
+ { name: { equals: $name } }
68
+ { isDeleted: { equals: false } }
69
+ ]
70
+ }
71
+ take: 5
72
+ ) { id name }
73
+ }
74
+ `;
75
+ const result = await (0, client_js_1.query)(gql, { accountId, name });
76
+ return result.listStacks || [];
77
+ }
78
+ async function appExistsByName(accountId, name) {
79
+ const gql = `
80
+ query FindAppByName($accountId: String!, $name: String!) {
81
+ listApps(
82
+ where: {
83
+ AND: [
84
+ { accountId: { equals: $accountId } }
85
+ { name: { equals: $name } }
86
+ { isDeleted: { equals: false } }
87
+ ]
88
+ }
89
+ take: 5
90
+ ) { id name stackId }
91
+ }
92
+ `;
93
+ const result = await (0, client_js_1.query)(gql, { accountId, name });
94
+ return result.listApps || [];
95
+ }
96
+ async function addOnExistsByName(accountId, name) {
97
+ const gql = `
98
+ query FindAddOnByName($accountId: String!, $name: String!) {
99
+ listAddOns(
100
+ where: {
101
+ AND: [
102
+ { accountId: { equals: $accountId } }
103
+ { name: { equals: $name } }
104
+ { isDeleted: { equals: false } }
105
+ ]
106
+ }
107
+ take: 5
108
+ ) { id name type }
109
+ }
110
+ `;
111
+ const result = await (0, client_js_1.query)(gql, { accountId, name });
112
+ return result.listAddOns || [];
113
+ }
114
+ const CreateStackSchema = zod_1.z.object({
115
+ account_id: zod_1.z
116
+ .string()
117
+ .optional()
118
+ .describe('Account ID. If omitted, Scaly will infer from your accessible stacks.'),
119
+ name: zod_1.z.string().min(1).describe('Stack name'),
120
+ size: zod_1.z
121
+ .enum([
122
+ 'Eco',
123
+ 'Basic',
124
+ 'Standard_1X',
125
+ 'Standard_2X',
126
+ 'Performance_M',
127
+ 'Performance_L'
128
+ ])
129
+ .describe('Stack size'),
130
+ region: zod_1.z
131
+ .enum(['CANADA', 'US', 'EU', 'ASIA_PACIFIC'])
132
+ .optional()
133
+ .describe('Informational only (region is derived from account today)'),
134
+ min_idle: zod_1.z
135
+ .number()
136
+ .int()
137
+ .min(0)
138
+ .optional()
139
+ .describe('Minimum warm instances'),
140
+ preview: zod_1.z
141
+ .boolean()
142
+ .default(true)
143
+ .describe('If true, returns what would change without executing')
144
+ });
145
+ const UpdateStackSchema = zod_1.z.object({
146
+ stack_id: zod_1.z.string().describe('Stack ID'),
147
+ size: zod_1.z
148
+ .enum([
149
+ 'Eco',
150
+ 'Basic',
151
+ 'Standard_1X',
152
+ 'Standard_2X',
153
+ 'Performance_M',
154
+ 'Performance_L'
155
+ ])
156
+ .optional(),
157
+ min_idle: zod_1.z.number().int().min(0).optional(),
158
+ preview: zod_1.z.boolean().default(true)
159
+ });
160
+ const DeleteStackSchema = zod_1.z.object({
161
+ stack_id: zod_1.z.string().describe('Stack ID'),
162
+ confirm: zod_1.z
163
+ .boolean()
164
+ .optional()
165
+ .default(false)
166
+ .describe('Must be true to delete'),
167
+ preview: zod_1.z.boolean().default(true)
168
+ });
169
+ const CreateDatabaseSchema = zod_1.z.object({
170
+ account_id: zod_1.z
171
+ .string()
172
+ .optional()
173
+ .describe('Account ID. If omitted, Scaly will infer from your accessible stacks.'),
174
+ name: zod_1.z.string().min(1).describe('Database add-on name'),
175
+ engine: zod_1.z
176
+ .enum(['POSTGRES_14', 'POSTGRES_16', 'MYSQL_80'])
177
+ .default('POSTGRES_16'),
178
+ size: zod_1.z
179
+ .enum(['Micro', 'Small', 'Medium', 'Large', 'XLarge'])
180
+ .default('Small'),
181
+ preview: zod_1.z.boolean().default(true)
182
+ });
183
+ const CreateStorageSchema = zod_1.z.object({
184
+ account_id: zod_1.z
185
+ .string()
186
+ .optional()
187
+ .describe('Account ID. If omitted, Scaly will infer from your accessible stacks.'),
188
+ name: zod_1.z.string().min(1).describe('Storage add-on name'),
189
+ preview: zod_1.z.boolean().default(true)
190
+ });
191
+ const PasswordPolicySchema = zod_1.z
192
+ .object({
193
+ minimum_length: zod_1.z.number().int().min(6).optional(),
194
+ require_lowercase: zod_1.z.boolean().optional(),
195
+ require_uppercase: zod_1.z.boolean().optional(),
196
+ require_numbers: zod_1.z.boolean().optional(),
197
+ require_symbols: zod_1.z.boolean().optional(),
198
+ temporary_password_validity_days: zod_1.z.number().int().min(1).optional(),
199
+ // Accept doc-style keys too (mapped internally).
200
+ minLength: zod_1.z.number().int().min(6).optional(),
201
+ requireLowercase: zod_1.z.boolean().optional(),
202
+ requireUppercase: zod_1.z.boolean().optional(),
203
+ requireNumbers: zod_1.z.boolean().optional(),
204
+ requireSymbols: zod_1.z.boolean().optional(),
205
+ temporaryPasswordValidityDays: zod_1.z.number().int().min(1).optional()
206
+ })
207
+ .optional();
208
+ const CreateUserPoolSchema = zod_1.z.object({
209
+ account_id: zod_1.z
210
+ .string()
211
+ .optional()
212
+ .describe('Account ID. If omitted, Scaly will infer from your accessible stacks.'),
213
+ name: zod_1.z.string().min(1).describe('Cognito user pool add-on name'),
214
+ password_policy: PasswordPolicySchema,
215
+ mfa: zod_1.z.enum(['off', 'optional', 'required']).optional().default('optional'),
216
+ preview: zod_1.z.boolean().default(true)
217
+ });
218
+ const CreateAppSchema = zod_1.z.object({
219
+ account_id: zod_1.z
220
+ .string()
221
+ .optional()
222
+ .describe('Account ID. If omitted, Scaly will infer from the stack.'),
223
+ stack_id: zod_1.z.string().describe('Stack ID'),
224
+ name: zod_1.z.string().min(1).describe('App name'),
225
+ framework: zod_1.z
226
+ .string()
227
+ .optional()
228
+ .describe('Optional framework label (used for guidance; not enforced by API)'),
229
+ min_idle: zod_1.z.number().int().min(0).optional(),
230
+ preview: zod_1.z.boolean().default(true)
231
+ });
232
+ const LinkAddOnSchema = zod_1.z.object({
233
+ app_id: zod_1.z.string().describe('App ID'),
234
+ addon_id: zod_1.z.string().describe('Add-on ID'),
235
+ preview: zod_1.z.boolean().default(true)
236
+ });
237
+ const ConfigureAuthSchema = zod_1.z.object({
238
+ app_id: zod_1.z.string().describe('App ID'),
239
+ user_pool_id: zod_1.z.string().describe('Cognito add-on ID to use for auth'),
240
+ groups: zod_1.z
241
+ .array(zod_1.z.string())
242
+ .optional()
243
+ .default([])
244
+ .describe('Optional list of allowed groups (empty means no group restriction)'),
245
+ preview: zod_1.z.boolean().default(true)
246
+ });
247
+ function normalizePasswordPolicy(input) {
248
+ if (!input)
249
+ return undefined;
250
+ return {
251
+ minimumLength: input.minimum_length ?? input.minLength,
252
+ requireLowercase: input.require_lowercase ?? input.requireLowercase,
253
+ requireUppercase: input.require_uppercase ?? input.requireUppercase,
254
+ requireNumbers: input.require_numbers ?? input.requireNumbers,
255
+ requireSymbols: input.require_symbols ?? input.requireSymbols,
256
+ temporaryPasswordValidityDays: input.temporary_password_validity_days ??
257
+ input.temporaryPasswordValidityDays
258
+ };
259
+ }
260
+ function mfaToSettings(mfa) {
261
+ if (mfa === 'off')
262
+ return { enabled: false, required: false };
263
+ if (mfa === 'optional')
264
+ return { enabled: true, required: false };
265
+ return { enabled: true, required: true };
266
+ }
267
+ exports.writeTools = [
268
+ {
269
+ name: 'scaly_create_stack',
270
+ description: 'Create a new stack. Use preview=true first; execute with preview=false.',
271
+ inputSchema: CreateStackSchema,
272
+ handler: async (input) => {
273
+ try {
274
+ const accountId = await resolveAccountId(input.account_id);
275
+ if (!accountId) {
276
+ return (0, index_js_1.fail)({
277
+ code: 'VALIDATION_ERROR',
278
+ message: 'account_id is required (unable to infer account from accessible stacks).',
279
+ retriable: false
280
+ });
281
+ }
282
+ const existing = await stackExistsByName(accountId, input.name);
283
+ if (existing.length) {
284
+ return (0, index_js_1.fail)({
285
+ code: 'RESOURCE_EXISTS',
286
+ message: `Stack already exists: ${input.name}`,
287
+ retriable: false,
288
+ details: { matches: existing }
289
+ }, { existing: existing[0] });
290
+ }
291
+ if (input.preview) {
292
+ return (0, index_js_1.ok)({
293
+ preview: true,
294
+ drift_warning: true,
295
+ would_create: {
296
+ type: 'stack',
297
+ name: input.name,
298
+ size: input.size,
299
+ min_idle: input.min_idle ?? 0,
300
+ account_id: accountId
301
+ },
302
+ estimated_cost: {
303
+ monthly_usd: estimateStackMonthlyUsd(input.size)
304
+ },
305
+ estimated_duration_seconds: 30,
306
+ notes: input.region
307
+ ? [
308
+ 'region is derived from account today; region is informational only'
309
+ ]
310
+ : []
311
+ });
312
+ }
313
+ const gql = `
314
+ mutation CreateStack($data: StackCreateInput!) {
315
+ createStack(data: $data) { id name accountId size minIdle status }
316
+ }
317
+ `;
318
+ const result = await (0, client_js_1.query)(gql, {
319
+ data: {
320
+ name: input.name,
321
+ size: input.size,
322
+ minIdle: input.min_idle ?? 0,
323
+ account: { connect: { id: accountId } }
324
+ }
325
+ });
326
+ return (0, index_js_1.ok)({
327
+ preview: false,
328
+ drift_warning: true,
329
+ stack: result.createStack
330
+ });
331
+ }
332
+ catch (err) {
333
+ return (0, index_js_1.fail)({
334
+ code: 'INTERNAL_ERROR',
335
+ message: err.message,
336
+ retriable: true
337
+ });
338
+ }
339
+ }
340
+ },
341
+ {
342
+ name: 'scaly_update_stack',
343
+ description: 'Update stack settings (size/min_idle). Use preview=true first; execute with preview=false.',
344
+ inputSchema: UpdateStackSchema,
345
+ handler: async (input) => {
346
+ try {
347
+ const existing = await getStackById(input.stack_id);
348
+ if (!existing) {
349
+ return (0, index_js_1.fail)({
350
+ code: 'RESOURCE_NOT_FOUND',
351
+ message: `Stack not found: ${input.stack_id}`,
352
+ retriable: false
353
+ });
354
+ }
355
+ if (input.preview) {
356
+ return (0, index_js_1.ok)({
357
+ preview: true,
358
+ drift_warning: true,
359
+ would_update: {
360
+ stack_id: input.stack_id,
361
+ stack_name: existing.name,
362
+ changes: {
363
+ size: input.size,
364
+ min_idle: input.min_idle
365
+ }
366
+ }
367
+ });
368
+ }
369
+ const gql = `
370
+ mutation UpdateStack($where: StackWhereUniqueInput!, $data: StackUpdateInput) {
371
+ updateStack(where: $where, data: $data) { id name accountId size minIdle status }
372
+ }
373
+ `;
374
+ const result = await (0, client_js_1.query)(gql, {
375
+ where: { id: input.stack_id },
376
+ data: {
377
+ size: input.size,
378
+ minIdle: typeof input.min_idle === 'number' ? input.min_idle : undefined
379
+ }
380
+ });
381
+ return (0, index_js_1.ok)({
382
+ preview: false,
383
+ drift_warning: true,
384
+ stack: result.updateStack
385
+ });
386
+ }
387
+ catch (err) {
388
+ return (0, index_js_1.fail)({
389
+ code: 'INTERNAL_ERROR',
390
+ message: err.message,
391
+ retriable: true
392
+ });
393
+ }
394
+ }
395
+ },
396
+ {
397
+ name: 'scaly_delete_stack',
398
+ description: 'Delete a stack. Use preview=true first. When ready, set preview=false and confirm=true.',
399
+ inputSchema: DeleteStackSchema,
400
+ handler: async (input) => {
401
+ try {
402
+ const existing = await getStackById(input.stack_id);
403
+ if (!existing) {
404
+ return (0, index_js_1.fail)({
405
+ code: 'RESOURCE_NOT_FOUND',
406
+ message: `Stack not found: ${input.stack_id}`,
407
+ retriable: false
408
+ });
409
+ }
410
+ if (input.preview) {
411
+ return (0, index_js_1.ok)({
412
+ preview: true,
413
+ drift_warning: true,
414
+ would_delete: {
415
+ type: 'stack',
416
+ stack_id: input.stack_id,
417
+ stack_name: existing.name
418
+ },
419
+ warning: 'Deleting a stack is destructive. Ensure apps are removed or migrated first.'
420
+ });
421
+ }
422
+ if (!input.confirm) {
423
+ return (0, index_js_1.fail)({
424
+ code: 'VALIDATION_ERROR',
425
+ message: 'confirm must be true to delete a stack.',
426
+ retriable: false
427
+ });
428
+ }
429
+ const gql = `
430
+ mutation DeleteStack($where: StackWhereUniqueInput!) {
431
+ deleteStack(where: $where) { id name }
432
+ }
433
+ `;
434
+ const result = await (0, client_js_1.query)(gql, {
435
+ where: { id: input.stack_id }
436
+ });
437
+ return (0, index_js_1.ok)({
438
+ preview: false,
439
+ drift_warning: true,
440
+ deleted: result.deleteStack
441
+ });
442
+ }
443
+ catch (err) {
444
+ return (0, index_js_1.fail)({
445
+ code: 'INTERNAL_ERROR',
446
+ message: err.message,
447
+ retriable: true
448
+ });
449
+ }
450
+ }
451
+ },
452
+ {
453
+ name: 'scaly_create_database',
454
+ description: 'Create a managed database add-on (Postgres/MySQL). Use preview=true first; execute with preview=false.',
455
+ inputSchema: CreateDatabaseSchema,
456
+ handler: async (input) => {
457
+ try {
458
+ const accountId = await resolveAccountId(input.account_id);
459
+ if (!accountId) {
460
+ return (0, index_js_1.fail)({
461
+ code: 'VALIDATION_ERROR',
462
+ message: 'account_id is required (unable to infer account from accessible stacks).',
463
+ retriable: false
464
+ });
465
+ }
466
+ const existing = await addOnExistsByName(accountId, input.name);
467
+ if (existing.length) {
468
+ return (0, index_js_1.fail)({
469
+ code: 'RESOURCE_EXISTS',
470
+ message: `Add-on already exists: ${input.name}`,
471
+ retriable: false,
472
+ details: { matches: existing }
473
+ }, { existing: existing[0] });
474
+ }
475
+ if (input.preview) {
476
+ return (0, index_js_1.ok)({
477
+ preview: true,
478
+ drift_warning: true,
479
+ would_create: {
480
+ type: 'database',
481
+ name: input.name,
482
+ engine: input.engine,
483
+ size: input.size,
484
+ account_id: accountId
485
+ },
486
+ estimated_cost: { monthly_usd: estimateDbMonthlyUsd(input.size) },
487
+ estimated_duration_seconds: 150
488
+ });
489
+ }
490
+ const gql = `
491
+ mutation CreateAddOn($data: AddOnCreateInput!) {
492
+ createAddOn(data: $data) {
493
+ id
494
+ name
495
+ type
496
+ accountId
497
+ status
498
+ addOnDatabase { size type }
499
+ }
500
+ }
501
+ `;
502
+ const result = await (0, client_js_1.query)(gql, {
503
+ data: {
504
+ type: 'DATABASE',
505
+ name: input.name,
506
+ account: { connect: { id: accountId } },
507
+ addOnDatabase: {
508
+ create: {
509
+ size: input.size,
510
+ type: input.engine
511
+ }
512
+ }
513
+ }
514
+ });
515
+ return (0, index_js_1.ok)({
516
+ preview: false,
517
+ drift_warning: true,
518
+ addon: result.createAddOn
519
+ });
520
+ }
521
+ catch (err) {
522
+ return (0, index_js_1.fail)({
523
+ code: 'INTERNAL_ERROR',
524
+ message: err.message,
525
+ retriable: true
526
+ });
527
+ }
528
+ }
529
+ },
530
+ {
531
+ name: 'scaly_create_storage',
532
+ description: 'Create an S3 storage add-on. Use preview=true first; execute with preview=false.',
533
+ inputSchema: CreateStorageSchema,
534
+ handler: async (input) => {
535
+ try {
536
+ const accountId = await resolveAccountId(input.account_id);
537
+ if (!accountId) {
538
+ return (0, index_js_1.fail)({
539
+ code: 'VALIDATION_ERROR',
540
+ message: 'account_id is required (unable to infer account from accessible stacks).',
541
+ retriable: false
542
+ });
543
+ }
544
+ const existing = await addOnExistsByName(accountId, input.name);
545
+ if (existing.length) {
546
+ return (0, index_js_1.fail)({
547
+ code: 'RESOURCE_EXISTS',
548
+ message: `Add-on already exists: ${input.name}`,
549
+ retriable: false,
550
+ details: { matches: existing }
551
+ }, { existing: existing[0] });
552
+ }
553
+ if (input.preview) {
554
+ return (0, index_js_1.ok)({
555
+ preview: true,
556
+ drift_warning: true,
557
+ would_create: {
558
+ type: 'storage',
559
+ name: input.name,
560
+ account_id: accountId
561
+ },
562
+ estimated_cost: { monthly_usd: 0 },
563
+ notes: ['Storage is billed by usage (GB-month + requests).']
564
+ });
565
+ }
566
+ const gql = `
567
+ mutation CreateAddOn($data: AddOnCreateInput!) {
568
+ createAddOn(data: $data) {
569
+ id
570
+ name
571
+ type
572
+ accountId
573
+ status
574
+ addOnStorageS3 { bucket }
575
+ }
576
+ }
577
+ `;
578
+ const result = await (0, client_js_1.query)(gql, {
579
+ data: {
580
+ type: 'STORAGE_S3',
581
+ name: input.name,
582
+ account: { connect: { id: accountId } }
583
+ }
584
+ });
585
+ return (0, index_js_1.ok)({
586
+ preview: false,
587
+ drift_warning: true,
588
+ addon: result.createAddOn
589
+ });
590
+ }
591
+ catch (err) {
592
+ return (0, index_js_1.fail)({
593
+ code: 'INTERNAL_ERROR',
594
+ message: err.message,
595
+ retriable: true
596
+ });
597
+ }
598
+ }
599
+ },
600
+ {
601
+ name: 'scaly_create_user_pool',
602
+ description: 'Create a Cognito user pool add-on. Use preview=true first; execute with preview=false. ' +
603
+ 'Note: userPoolName is derived from add-on name; password policy defaults to AWS defaults if omitted.',
604
+ inputSchema: CreateUserPoolSchema,
605
+ handler: async (input) => {
606
+ try {
607
+ const accountId = await resolveAccountId(input.account_id);
608
+ if (!accountId) {
609
+ return (0, index_js_1.fail)({
610
+ code: 'VALIDATION_ERROR',
611
+ message: 'account_id is required (unable to infer account from accessible stacks).',
612
+ retriable: false
613
+ });
614
+ }
615
+ const existing = await addOnExistsByName(accountId, input.name);
616
+ if (existing.length) {
617
+ return (0, index_js_1.fail)({
618
+ code: 'RESOURCE_EXISTS',
619
+ message: `Add-on already exists: ${input.name}`,
620
+ retriable: false,
621
+ details: { matches: existing }
622
+ }, { existing: existing[0] });
623
+ }
624
+ const policy = normalizePasswordPolicy(input.password_policy);
625
+ const mfa = mfaToSettings(input.mfa);
626
+ if (input.preview) {
627
+ return (0, index_js_1.ok)({
628
+ preview: true,
629
+ drift_warning: true,
630
+ would_create: {
631
+ type: 'cognito',
632
+ name: input.name,
633
+ account_id: accountId,
634
+ password_policy: policy,
635
+ mfa
636
+ },
637
+ estimated_cost: { monthly_usd: 0 },
638
+ notes: ['Cognito is billed by MAU; estimates depend on usage.']
639
+ });
640
+ }
641
+ const gql = `
642
+ mutation CreateAddOn($data: AddOnCreateInput!) {
643
+ createAddOn(data: $data) {
644
+ id
645
+ name
646
+ type
647
+ accountId
648
+ status
649
+ addOnCognito { userPoolName userPoolId }
650
+ }
651
+ }
652
+ `;
653
+ const result = await (0, client_js_1.query)(gql, {
654
+ data: {
655
+ type: 'COGNITO',
656
+ name: input.name,
657
+ account: { connect: { id: accountId } },
658
+ addOnCognito: {
659
+ create: {
660
+ passwordPolicy: policy,
661
+ mfa
662
+ }
663
+ }
664
+ }
665
+ });
666
+ return (0, index_js_1.ok)({
667
+ preview: false,
668
+ drift_warning: true,
669
+ addon: result.createAddOn
670
+ });
671
+ }
672
+ catch (err) {
673
+ return (0, index_js_1.fail)({
674
+ code: 'INTERNAL_ERROR',
675
+ message: err.message,
676
+ retriable: true
677
+ });
678
+ }
679
+ }
680
+ },
681
+ {
682
+ name: 'scaly_create_app',
683
+ description: 'Create an app on a stack. Use preview=true first; execute with preview=false.',
684
+ inputSchema: CreateAppSchema,
685
+ handler: async (input) => {
686
+ try {
687
+ // Infer account from stack.
688
+ const stackRes = await (0, client_js_1.query)(`
689
+ query GetStack($where: StackWhereUniqueInput!) {
690
+ getStack(where: $where) { id name accountId }
691
+ }
692
+ `, { where: { id: input.stack_id } });
693
+ const stack = stackRes.getStack;
694
+ if (!stack) {
695
+ return (0, index_js_1.fail)({
696
+ code: 'RESOURCE_NOT_FOUND',
697
+ message: `Stack not found: ${input.stack_id}`,
698
+ retriable: false
699
+ });
700
+ }
701
+ const accountId = input.account_id || stack.accountId;
702
+ const existing = await appExistsByName(accountId, input.name);
703
+ if (existing.length) {
704
+ return (0, index_js_1.fail)({
705
+ code: 'RESOURCE_EXISTS',
706
+ message: `App already exists: ${input.name}`,
707
+ retriable: false,
708
+ details: { matches: existing }
709
+ }, { existing: existing[0] });
710
+ }
711
+ if (input.preview) {
712
+ return (0, index_js_1.ok)({
713
+ preview: true,
714
+ drift_warning: true,
715
+ would_create: {
716
+ type: 'app',
717
+ name: input.name,
718
+ framework: input.framework ?? null,
719
+ stack_id: input.stack_id,
720
+ stack_name: stack.name,
721
+ account_id: accountId,
722
+ min_idle: input.min_idle ?? 0
723
+ },
724
+ notes: input.framework
725
+ ? [
726
+ 'framework is used for guidance/detection; API does not enforce it'
727
+ ]
728
+ : []
729
+ });
730
+ }
731
+ const gql = `
732
+ mutation CreateApp($data: AppCreateInput!) {
733
+ createApp(data: $data) {
734
+ id
735
+ name
736
+ accountId
737
+ stackId
738
+ status
739
+ minIdle
740
+ createdAt
741
+ updatedAt
742
+ }
743
+ }
744
+ `;
745
+ const result = await (0, client_js_1.query)(gql, {
746
+ data: {
747
+ name: input.name,
748
+ minIdle: input.min_idle ?? 0,
749
+ account: { connect: { id: accountId } },
750
+ stack: { connect: { id: input.stack_id } }
751
+ }
752
+ });
753
+ return (0, index_js_1.ok)({
754
+ preview: false,
755
+ drift_warning: true,
756
+ app: result.createApp
757
+ });
758
+ }
759
+ catch (err) {
760
+ return (0, index_js_1.fail)({
761
+ code: 'INTERNAL_ERROR',
762
+ message: err.message,
763
+ retriable: true
764
+ });
765
+ }
766
+ }
767
+ },
768
+ {
769
+ name: 'scaly_link_addon',
770
+ description: 'Link an add-on to an app (connect relationship). Use preview=true first; execute with preview=false.',
771
+ inputSchema: LinkAddOnSchema,
772
+ handler: async (input) => {
773
+ try {
774
+ const app = await getAppById(input.app_id);
775
+ if (!app) {
776
+ return (0, index_js_1.fail)({
777
+ code: 'RESOURCE_NOT_FOUND',
778
+ message: `App not found: ${input.app_id}`,
779
+ retriable: false
780
+ });
781
+ }
782
+ const addon = await getAddOnById(input.addon_id);
783
+ if (!addon) {
784
+ return (0, index_js_1.fail)({
785
+ code: 'RESOURCE_NOT_FOUND',
786
+ message: `Add-on not found: ${input.addon_id}`,
787
+ retriable: false
788
+ });
789
+ }
790
+ if (input.preview) {
791
+ return (0, index_js_1.ok)({
792
+ preview: true,
793
+ drift_warning: true,
794
+ would_link: {
795
+ app: { id: app.id, name: app.name },
796
+ addon: { id: addon.id, name: addon.name, type: addon.type }
797
+ }
798
+ });
799
+ }
800
+ const gql = `
801
+ mutation UpdateApp($where: AppWhereUniqueInput!, $data: AppUpdateInput) {
802
+ updateApp(where: $where, data: $data) { id name }
803
+ }
804
+ `;
805
+ const result = await (0, client_js_1.query)(gql, {
806
+ where: { id: input.app_id },
807
+ data: {
808
+ addOns: {
809
+ connect: [{ id: input.addon_id }]
810
+ }
811
+ }
812
+ });
813
+ return (0, index_js_1.ok)({
814
+ preview: false,
815
+ drift_warning: true,
816
+ linked: true,
817
+ app: result.updateApp
818
+ });
819
+ }
820
+ catch (err) {
821
+ return (0, index_js_1.fail)({
822
+ code: 'INTERNAL_ERROR',
823
+ message: err.message,
824
+ retriable: true
825
+ });
826
+ }
827
+ }
828
+ },
829
+ {
830
+ name: 'scaly_configure_auth',
831
+ description: 'Configure app auth by linking a Cognito user pool add-on to the app and setting allowed groups. Use preview=true first; execute with preview=false.',
832
+ inputSchema: ConfigureAuthSchema,
833
+ handler: async (input) => {
834
+ try {
835
+ const app = await getAppById(input.app_id);
836
+ if (!app) {
837
+ return (0, index_js_1.fail)({
838
+ code: 'RESOURCE_NOT_FOUND',
839
+ message: `App not found: ${input.app_id}`,
840
+ retriable: false
841
+ });
842
+ }
843
+ const addon = await getAddOnById(input.user_pool_id);
844
+ if (!addon) {
845
+ return (0, index_js_1.fail)({
846
+ code: 'RESOURCE_NOT_FOUND',
847
+ message: `Add-on not found: ${input.user_pool_id}`,
848
+ retriable: false
849
+ });
850
+ }
851
+ if (addon.type !== 'COGNITO') {
852
+ return (0, index_js_1.fail)({
853
+ code: 'VALIDATION_ERROR',
854
+ message: `Add-on ${input.user_pool_id} is type ${addon.type}; expected COGNITO.`,
855
+ retriable: false,
856
+ details: { addon }
857
+ });
858
+ }
859
+ if (input.preview) {
860
+ return (0, index_js_1.ok)({
861
+ preview: true,
862
+ drift_warning: true,
863
+ would_configure_auth: {
864
+ app: { id: app.id, name: app.name },
865
+ user_pool: { id: addon.id, name: addon.name, type: addon.type },
866
+ groups: input.groups
867
+ }
868
+ });
869
+ }
870
+ const gql = `
871
+ mutation UpdateApp($where: AppWhereUniqueInput!, $data: AppUpdateInput) {
872
+ updateApp(where: $where, data: $data) { id name }
873
+ }
874
+ `;
875
+ const result = await (0, client_js_1.query)(gql, {
876
+ where: { id: input.app_id },
877
+ data: {
878
+ addOns: { connect: [{ id: input.user_pool_id }] },
879
+ auth: { upsert: { groups: input.groups } }
880
+ }
881
+ });
882
+ return (0, index_js_1.ok)({
883
+ preview: false,
884
+ drift_warning: true,
885
+ configured: true,
886
+ app: result.updateApp,
887
+ groups: input.groups
888
+ });
889
+ }
890
+ catch (err) {
891
+ return (0, index_js_1.fail)({
892
+ code: 'INTERNAL_ERROR',
893
+ message: err.message,
894
+ retriable: true
895
+ });
896
+ }
897
+ }
898
+ }
899
+ ];
900
+ //# sourceMappingURL=write.js.map