@biznetgio/neo-app 0.4.30 → 0.4.31
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/package.json +1 -1
- package/src/deploySource.js +2 -1
- package/src/mcp/tools.js +121 -42
package/package.json
CHANGED
package/src/deploySource.js
CHANGED
|
@@ -273,7 +273,8 @@ export async function deploySource(api, { projectId, src, name, port, build, ser
|
|
|
273
273
|
try {
|
|
274
274
|
const book = (await api.get('/api/v1/pricing')).data;
|
|
275
275
|
estimatedCost = estimateCost(book, { computeTier: 'micro' });
|
|
276
|
-
|
|
276
|
+
const build = svcType === 'static_site' ? 'native nginx static hosting (not Railpack)' : 'built server-side';
|
|
277
|
+
log(`[dry-run] estimated cost (${svcType}, ${build}, default micro compute): ~${estimatedCost.monthly} ${estimatedCost.currency}/mo (${estimatedCost.hourly}/h) — ${estimatedCost.assumptions}. Use neo_estimate for a specific size.`);
|
|
277
278
|
} catch { /* price book unavailable — skip the estimate, not the dry-run */ }
|
|
278
279
|
return { dryRun: true, plan, service: svc, environment: targetEnv, serviceType: svcType, estimatedCost };
|
|
279
280
|
}
|
package/src/mcp/tools.js
CHANGED
|
@@ -120,8 +120,8 @@ export const TOOLS = [
|
|
|
120
120
|
{ type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
|
|
121
121
|
{ type: 'static_site', create: 'via_folder', deploy: true, scale: false, logs: true, variables: true, delete: true, nativeStatic: true, note: 'neo_deploy_folder serviceType=static_site with buildCommand/publishDirectory → native nginx static hosting (0.4.27); auto-detected for static-only folders' },
|
|
122
122
|
{ type: 'worker', create: 'via_folder', deploy: true, scale: true, logs: true, variables: true, delete: true, note: 'neo_deploy_folder serviceType=worker — no public ingress; repo auto-detection still TODO (handoff 6)' },
|
|
123
|
-
{ type: 'database', create: true, get: true, list: true, update: true, backup: true, verify: true, publicAccess: true, delete: true, note: 'neo_create/list/get/update(resize)/delete_database + backup + verify (SQL round-trip probe) + enable/disable_db_public_access; restore still TODO (handoff 4.4)' },
|
|
124
|
-
{ type: 'queue', create: true, get: true, list: true, update: true, verify: true, publicAccess: true, delete: true, note: 'neo_create/list/get/update/verify(publish→persist)/delete_queue + enable/disable_queue_public_access (handoff 4.5)' },
|
|
123
|
+
{ type: 'database', create: true, deploy: true, get: true, list: true, update: true, backup: true, verify: true, publicAccess: true, delete: true, note: 'neo_create/deploy/list/get/update(resize)/delete_database + backup + verify (SQL round-trip probe) + enable/disable_db_public_access; restore still TODO (handoff 4.4)' },
|
|
124
|
+
{ type: 'queue', create: true, deploy: true, get: true, list: true, update: true, verify: true, publicAccess: true, delete: true, note: 'neo_create/deploy/list/get/update/verify(publish→persist)/delete_queue + enable/disable_queue_public_access (handoff 4.5)' },
|
|
125
125
|
{ type: 'cron', create: true, get: true, list: true, run: true, runs: true, questionnaire: true, delete: true, note: 'neo_cron_questionnaire (guided) + create/list/get/run/list_runs/delete_cron (handoff 4.6)' },
|
|
126
126
|
{ type: 'object_storage', create: true, get: true, list: true, objects: true, upload: true, verify: true, delete: true, note: 'neo_create/list/get/verify(put→list→delete)/delete_bucket + list/upload/presign(get)/delete objects (handoff 4.7)' },
|
|
127
127
|
];
|
|
@@ -132,7 +132,7 @@ export const TOOLS = [
|
|
|
132
132
|
resourceTypes,
|
|
133
133
|
// Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
|
|
134
134
|
lifecycleVerbs: ['plan', 'apply', 'get_operation', 'wait_operation', 'delete_stack', 'reconcile_usage', 'service_usage', 'create', 'list', 'get', 'update', 'verify', 'enable_public_access', 'disable_public_access', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'estimate', 'scan_repo', 'bind', 'unbind', 'run_cron', 'cron_questionnaire', 'presign_object', 'backup', 'delete', 'delete_project', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
135
|
-
bindings: { supported: true, targets: ['neo_db', 'neo_queue'], writeOnly: true, note: 'neo_bind_resource
|
|
135
|
+
bindings: { supported: true, targets: ['neo_db', 'neo_queue', 'neo_object_storage'], writeOnly: true, note: 'neo_bind_resource + neo_plan_stack bindings inject DATABASE_URL / NATS_URL (+ NATS_HOST/PORT/USER/PASSWORD) / S3_* server-side at deploy; secrets never returned (handoff 5)' },
|
|
136
136
|
sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
|
|
137
137
|
environments: ['staging', 'production'],
|
|
138
138
|
pricing,
|
|
@@ -386,32 +386,44 @@ export const TOOLS = [
|
|
|
386
386
|
async call(api, args) {
|
|
387
387
|
const { data } = await api.post('/api/v1/repos/scan', { repoUrl: args.repoUrl, branch: args.branch, gitProviderId: args.gitProviderId });
|
|
388
388
|
const subdirs = data?.subdirs || [];
|
|
389
|
-
const components = subdirs.map((s) =>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
389
|
+
const components = subdirs.map((s) => {
|
|
390
|
+
const role = s.role || 'service'; // service | worker | support
|
|
391
|
+
const support = role === 'support' || s.deployable === false;
|
|
392
|
+
return {
|
|
393
|
+
path: s.path || '.',
|
|
394
|
+
name: s.name,
|
|
395
|
+
stack: s.type, // docker | node | go | python | java | php | ruby | rust | static
|
|
396
|
+
role, // service | worker | support (library/config — not deployable)
|
|
397
|
+
deployable: !support,
|
|
398
|
+
// Don't suggest a service type for a support/library package; a worker-named
|
|
399
|
+
// root suggests worker; static → static_site; else web.
|
|
400
|
+
suggestedServiceType: support ? null : (role === 'worker' ? 'worker' : (s.type === 'static' ? 'static_site' : 'web')),
|
|
401
|
+
suggestedBuildType: s.type === 'docker' ? 'dockerfile' : 'railpack',
|
|
402
|
+
contextDir: s.path || undefined,
|
|
403
|
+
detectedDependencies: s.deps || [], // database | queue | object_storage, inferred from the manifest
|
|
404
|
+
// Confidence (handoff 6): a Dockerfile/index.html is high; a worker guess from
|
|
405
|
+
// the tree is low (confirm); a plain language marker is medium.
|
|
406
|
+
typeConfidence: support ? 'low' : (role === 'worker' ? 'low' : ((s.type === 'docker' || s.type === 'static') ? 'high' : 'medium')),
|
|
407
|
+
evidence: { marker: s.type, role, manifestDeps: s.deps || [] },
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
const deployable = components.filter((c) => c.deployable);
|
|
411
|
+
const support = components.filter((c) => !c.deployable);
|
|
403
412
|
const anyDeps = components.some((c) => c.detectedDependencies.length);
|
|
404
413
|
return {
|
|
405
414
|
repoUrl: args.repoUrl,
|
|
406
415
|
count: components.length,
|
|
416
|
+
deployableCount: deployable.length,
|
|
407
417
|
components,
|
|
408
418
|
note:
|
|
409
|
-
(
|
|
410
|
-
? 'Monorepo: multiple deployable components. Ask the user which to deploy and create each selected one with its `contextDir` — do NOT collapse them into one service (handoff 6).
|
|
411
|
-
:
|
|
419
|
+
(deployable.length > 1
|
|
420
|
+
? 'Monorepo: multiple deployable components. Ask the user which to deploy and create each selected one with its `contextDir` — do NOT collapse them into one service (handoff 6).'
|
|
421
|
+
: deployable.length === 1
|
|
412
422
|
? 'One deployable component found.'
|
|
413
|
-
: 'No deployable component found
|
|
414
|
-
+ (
|
|
423
|
+
: 'No deployable component found.')
|
|
424
|
+
+ (support.length ? ` ${support.length} support/library package(s) (role:support, deployable:false) were EXCLUDED from deployment — do not deploy config/lib packages (e.g. eslint/tsconfig/shared).` : '')
|
|
425
|
+
+ (components.some((c) => c.role === 'worker') ? ' A component is guessed as a worker (low confidence) — confirm web vs worker with the user.' : ' Worker vs web is a guess; confirm the type with the user.')
|
|
426
|
+
+ (anyDeps ? ' Some components list detectedDependencies (database/queue/object_storage) — consider planning + binding them (confirm first).' : ''),
|
|
415
427
|
};
|
|
416
428
|
},
|
|
417
429
|
},
|
|
@@ -535,6 +547,16 @@ export const TOOLS = [
|
|
|
535
547
|
return redactSecrets(result(status, data));
|
|
536
548
|
},
|
|
537
549
|
},
|
|
550
|
+
{
|
|
551
|
+
name: 'neo_deploy_database',
|
|
552
|
+
description: 'Deploy an individually-created managed database so it actually runs (provisions the cluster, sets its private host/port). Required after neo_create_database before the DB is reachable, bindable, or can have public access enabled. Poll neo_get_database until host is set and neo_verify_database passes. Credentials redacted.',
|
|
553
|
+
mutating: true,
|
|
554
|
+
schema: { type: 'object', properties: { databaseId: str('Database id (from neo_create_database)') }, required: ['databaseId'] },
|
|
555
|
+
async call(api, args) {
|
|
556
|
+
const { status, data } = await api.post(`/api/v1/databases/${args.databaseId}/deploy`);
|
|
557
|
+
return redactSecrets(result(status, data));
|
|
558
|
+
},
|
|
559
|
+
},
|
|
538
560
|
{
|
|
539
561
|
name: 'neo_list_databases',
|
|
540
562
|
description: 'List managed databases in a project (credentials redacted).',
|
|
@@ -669,6 +691,16 @@ export const TOOLS = [
|
|
|
669
691
|
return redactSecrets(result(status, data));
|
|
670
692
|
},
|
|
671
693
|
},
|
|
694
|
+
{
|
|
695
|
+
name: 'neo_deploy_queue',
|
|
696
|
+
description: 'Deploy an individually-created managed queue so it actually runs (provisions the NATS server, sets its private host/port). Required after neo_create_queue before the queue is reachable, bindable, or can have public access enabled. Poll neo_get_queue until host is set and neo_verify_queue passes. Credentials redacted.',
|
|
697
|
+
mutating: true,
|
|
698
|
+
schema: { type: 'object', properties: { queueId: str('Queue id (from neo_create_queue)') }, required: ['queueId'] },
|
|
699
|
+
async call(api, args) {
|
|
700
|
+
const { status, data } = await api.post(`/api/v1/queues/${args.queueId}/deploy`);
|
|
701
|
+
return redactSecrets(result(status, data));
|
|
702
|
+
},
|
|
703
|
+
},
|
|
672
704
|
{
|
|
673
705
|
name: 'neo_list_queues',
|
|
674
706
|
description: 'List managed queues in a project (credentials redacted).',
|
|
@@ -762,12 +794,15 @@ export const TOOLS = [
|
|
|
762
794
|
url: str('Target URL (mode=http_endpoint)'),
|
|
763
795
|
method: str('HTTP method (mode=http_endpoint), e.g. GET/POST'),
|
|
764
796
|
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
797
|
+
confirm: { type: 'boolean', description: 'Approve & Create: without confirm:true the tool returns the cost (crons are billed per run — variable) and creates nothing. Re-call with confirm:true to create.' },
|
|
765
798
|
},
|
|
766
799
|
required: ['projectId', 'name', 'mode', 'scheduleType', 'environment'],
|
|
767
800
|
},
|
|
768
801
|
async call(api, args) {
|
|
769
802
|
requireEnv(args.environment);
|
|
770
|
-
const
|
|
803
|
+
const gate = await confirmCost(api, args, {}); // cron cost is variable (per-run) — gate surfaces the rate
|
|
804
|
+
if (gate) return gate;
|
|
805
|
+
const { projectId, confirm, orgId, ...body } = args;
|
|
771
806
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/crons`, body);
|
|
772
807
|
return redactSecrets(result(status, data));
|
|
773
808
|
},
|
|
@@ -821,11 +856,14 @@ export const TOOLS = [
|
|
|
821
856
|
region: str('Region (optional; see neo_list_regions on the portal)'),
|
|
822
857
|
acl: { type: 'string', description: 'private | public-read (optional, default private)' },
|
|
823
858
|
quotaGB: int('Storage quota in GB (optional)'),
|
|
859
|
+
confirm: { type: 'boolean', description: 'Approve & Create: without confirm:true the tool returns the cost and creates nothing. Re-call with confirm:true to actually create.' },
|
|
824
860
|
},
|
|
825
861
|
required: ['projectId', 'name'],
|
|
826
862
|
},
|
|
827
863
|
async call(api, args) {
|
|
828
|
-
const
|
|
864
|
+
const gate = await confirmCost(api, args, { objectStorageGB: gbFromSize(args.quotaGB) });
|
|
865
|
+
if (gate) return gate;
|
|
866
|
+
const { projectId, confirm, orgId, ...body } = args;
|
|
829
867
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/buckets`, body);
|
|
830
868
|
return redactSecrets(result(status, data));
|
|
831
869
|
},
|
|
@@ -952,7 +990,7 @@ export const TOOLS = [
|
|
|
952
990
|
{
|
|
953
991
|
name: 'neo_plan_stack',
|
|
954
992
|
description:
|
|
955
|
-
'Create an immutable, COSTED plan for a whole stack (services + databases/queues/buckets + bindings) WITHOUT creating anything — zero-write (handoff 3.3). Returns planId, planHash, itemized monthly/hourly cost, and ordered phases. Show the cost to the user for approval, then call neo_apply_stack with planId+planHash. The plan expires in 15 minutes.',
|
|
993
|
+
'Create an immutable, COSTED plan for a whole stack (services + databases/queues/buckets + scheduled jobs + bindings) WITHOUT creating anything — zero-write (handoff 3.3). Supports kind: app|database|queue|bucket|cron and bindings to neo_db|neo_queue|neo_object_storage. Returns planId, planHash, itemized monthly/hourly cost, and ordered phases. Show the cost to the user for approval, then call neo_apply_stack with planId+planHash. The plan expires in 15 minutes.',
|
|
956
994
|
mutating: false,
|
|
957
995
|
schema: {
|
|
958
996
|
type: 'object',
|
|
@@ -961,16 +999,16 @@ export const TOOLS = [
|
|
|
961
999
|
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
962
1000
|
resources: {
|
|
963
1001
|
type: 'array',
|
|
964
|
-
description: 'The stack resources. Each needs a unique name + kind (app | database | queue | bucket).',
|
|
1002
|
+
description: 'The stack resources. Each needs a unique name + kind (app | database | queue | bucket | cron).',
|
|
965
1003
|
items: {
|
|
966
1004
|
type: 'object',
|
|
967
1005
|
properties: {
|
|
968
|
-
kind: { type: 'string', enum: ['app', 'database', 'queue', 'bucket'] },
|
|
1006
|
+
kind: { type: 'string', enum: ['app', 'database', 'queue', 'bucket', 'cron'] },
|
|
969
1007
|
name: str('Unique resource name within this stack (bindings reference it)'),
|
|
970
1008
|
serviceType: str('app: web | worker | static_site'),
|
|
971
1009
|
buildCommand: str('app/static_site: build command (e.g. "npm ci && npm run build")'),
|
|
972
1010
|
publishDirectory: str('static_site: built-files directory to serve (e.g. "dist")'),
|
|
973
|
-
image: str('app: prebuilt image reference'),
|
|
1011
|
+
image: str('app/cron: prebuilt image reference'),
|
|
974
1012
|
repoUrl: str('app: git repository URL'),
|
|
975
1013
|
branch: str('app: git branch'),
|
|
976
1014
|
port: int('app: container port'),
|
|
@@ -980,20 +1018,29 @@ export const TOOLS = [
|
|
|
980
1018
|
volumeSize: str('database: storage size, e.g. 10Gi'),
|
|
981
1019
|
quotaGB: int('bucket: storage quota GB'),
|
|
982
1020
|
region: str('bucket: region'),
|
|
1021
|
+
mode: str('cron: app_command | http_endpoint'),
|
|
1022
|
+
scheduleType: str('cron: cron | interval | once | seconds'),
|
|
1023
|
+
schedule: str('cron: cron expression (scheduleType=cron)'),
|
|
1024
|
+
intervalMinutes: int('cron: interval in minutes (scheduleType=interval)'),
|
|
1025
|
+
timezone: str('cron: IANA timezone (optional)'),
|
|
1026
|
+
serviceId: str('cron app_command: target service — another resource NAME in this stack, or a literal service id'),
|
|
1027
|
+
command: { type: 'array', items: { type: 'string' }, description: 'cron app_command: command argv' },
|
|
1028
|
+
url: str('cron http_endpoint: target URL'),
|
|
1029
|
+
method: str('cron http_endpoint: HTTP method'),
|
|
983
1030
|
},
|
|
984
1031
|
required: ['kind', 'name'],
|
|
985
1032
|
},
|
|
986
1033
|
},
|
|
987
1034
|
bindings: {
|
|
988
1035
|
type: 'array',
|
|
989
|
-
description: 'Wire a database/queue into a consumer app (injected as
|
|
1036
|
+
description: 'Wire a database/queue/bucket into a consumer app (injected as env var(s) at deploy, server-side).',
|
|
990
1037
|
items: {
|
|
991
1038
|
type: 'object',
|
|
992
1039
|
properties: {
|
|
993
1040
|
consumer: str('The consumer app resource name'),
|
|
994
|
-
targetType: { type: 'string', enum: ['neo_db', 'neo_queue'] },
|
|
995
|
-
target: str('The database/queue resource name'),
|
|
996
|
-
envKey: str('Env var name (optional; default DATABASE_URL / NATS_URL)'),
|
|
1041
|
+
targetType: { type: 'string', enum: ['neo_db', 'neo_queue', 'neo_object_storage'] },
|
|
1042
|
+
target: str('The database/queue/bucket resource name'),
|
|
1043
|
+
envKey: str('Env var name/prefix (optional; default DATABASE_URL / NATS_URL / S3)'),
|
|
997
1044
|
},
|
|
998
1045
|
required: ['consumer', 'targetType', 'target'],
|
|
999
1046
|
},
|
|
@@ -1179,9 +1226,9 @@ export const TOOLS = [
|
|
|
1179
1226
|
type: 'object',
|
|
1180
1227
|
properties: {
|
|
1181
1228
|
databaseId: str('Database id (postgres, already deployed)'),
|
|
1182
|
-
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR
|
|
1229
|
+
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'REQUIRED — at least one real source CIDR, e.g. ["203.0.113.0/24"]. The server rejects an empty list and 0.0.0.0/0 (no open-to-the-world).' },
|
|
1183
1230
|
},
|
|
1184
|
-
required: ['databaseId'],
|
|
1231
|
+
required: ['databaseId', 'allowlistCidrs'],
|
|
1185
1232
|
},
|
|
1186
1233
|
async call(api, args) {
|
|
1187
1234
|
const { status, data } = await api.post(`/api/v1/databases/${args.databaseId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
@@ -1206,9 +1253,9 @@ export const TOOLS = [
|
|
|
1206
1253
|
type: 'object',
|
|
1207
1254
|
properties: {
|
|
1208
1255
|
queueId: str('Queue id (already deployed)'),
|
|
1209
|
-
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR
|
|
1256
|
+
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'REQUIRED — at least one real source CIDR, e.g. ["203.0.113.0/24"]. The server rejects an empty list and 0.0.0.0/0.' },
|
|
1210
1257
|
},
|
|
1211
|
-
required: ['queueId'],
|
|
1258
|
+
required: ['queueId', 'allowlistCidrs'],
|
|
1212
1259
|
},
|
|
1213
1260
|
async call(api, args) {
|
|
1214
1261
|
const { status, data } = await api.post(`/api/v1/queues/${args.queueId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
@@ -1227,23 +1274,55 @@ export const TOOLS = [
|
|
|
1227
1274
|
},
|
|
1228
1275
|
{
|
|
1229
1276
|
name: 'neo_delete_project',
|
|
1230
|
-
description: '
|
|
1277
|
+
description: 'Delete a project and ALL its resources (apps, databases, queues, crons, buckets, namespace + PVCs/CNPG). SAFE BY DEFAULT: without confirm:true it returns a PREVIEW (project name, org, full resource inventory, accrued-usage snapshot, no-refund warning) and deletes NOTHING. A NON-EMPTY project additionally requires cascade:true. Destructive and irreversible. Only the creator or an org owner/admin may delete.',
|
|
1231
1278
|
mutating: true,
|
|
1232
1279
|
destructive: true,
|
|
1233
1280
|
schema: {
|
|
1234
1281
|
type: 'object',
|
|
1235
1282
|
properties: {
|
|
1236
1283
|
projectId: str('Project id'),
|
|
1237
|
-
confirm: { type: 'boolean', description: 'Must be true to
|
|
1284
|
+
confirm: { type: 'boolean', description: 'Must be true to delete. Without it you get a preview only (nothing is deleted).' },
|
|
1285
|
+
cascade: { type: 'boolean', description: 'Required true to delete a NON-EMPTY project — acknowledges every resource in the inventory is destroyed.' },
|
|
1238
1286
|
},
|
|
1239
|
-
required: ['projectId'
|
|
1287
|
+
required: ['projectId'],
|
|
1240
1288
|
},
|
|
1241
1289
|
async call(api, args) {
|
|
1290
|
+
const pid = args.projectId;
|
|
1291
|
+
// Gather inventory + usage BEFORE any delete (this is also the metering snapshot).
|
|
1292
|
+
const safeGet = async (p) => { try { return (await api.get(p)).data; } catch { return null; } };
|
|
1293
|
+
const [proj, apps, dbs, queues, crons, buckets, usage] = await Promise.all([
|
|
1294
|
+
safeGet(`/api/v1/projects/${pid}`),
|
|
1295
|
+
safeGet(`/api/v1/projects/${pid}/apps`),
|
|
1296
|
+
safeGet(`/api/v1/projects/${pid}/databases`),
|
|
1297
|
+
safeGet(`/api/v1/projects/${pid}/queues`),
|
|
1298
|
+
safeGet(`/api/v1/projects/${pid}/crons`),
|
|
1299
|
+
safeGet(`/api/v1/projects/${pid}/buckets`),
|
|
1300
|
+
safeGet(`/api/v1/projects/${pid}/usage`),
|
|
1301
|
+
]);
|
|
1302
|
+
const arr = (x, k) => (Array.isArray(x) ? x : (x && Array.isArray(x[k]) ? x[k] : []));
|
|
1303
|
+
const inv = {
|
|
1304
|
+
services: arr(apps, 'apps').map((a) => a.name || a.id),
|
|
1305
|
+
databases: arr(dbs, 'databases').map((d) => d.name || d.id),
|
|
1306
|
+
queues: arr(queues, 'queues').map((q) => q.name || q.id),
|
|
1307
|
+
crons: arr(crons, 'crons').map((c) => c.name || c.id),
|
|
1308
|
+
buckets: arr(buckets, 'buckets').map((b) => b.name || b.id),
|
|
1309
|
+
};
|
|
1310
|
+
const total = Object.values(inv).reduce((n, a) => n + a.length, 0);
|
|
1311
|
+
const preview = {
|
|
1312
|
+
project: proj ? { id: proj.id, name: proj.name, organizationId: proj.organizationId } : { id: pid },
|
|
1313
|
+
resourceInventory: inv,
|
|
1314
|
+
resourceCount: total,
|
|
1315
|
+
meteringSnapshot: usage ? { period: usage.period, total: usage.total ?? usage.totalCost, currency: usage.currency } : null,
|
|
1316
|
+
warning: 'IRREVERSIBLE: deletes the project, every resource above, and their data (database/queue volumes included). No refund for prepaid or accrued usage.',
|
|
1317
|
+
};
|
|
1242
1318
|
if (args.confirm !== true) {
|
|
1243
|
-
return {
|
|
1319
|
+
return { ...preview, deleted: false, requiresConfirmation: true, message: total > 0 ? 'Show this inventory to the user. To delete, re-call with confirm:true AND cascade:true.' : 'Empty project. To delete, re-call with confirm:true.' };
|
|
1244
1320
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1321
|
+
if (total > 0 && args.cascade !== true) {
|
|
1322
|
+
return { ...preview, deleted: false, error: 'cascade_required', message: `This project has ${total} resource(s). Re-call with confirm:true AND cascade:true to delete them all.` };
|
|
1323
|
+
}
|
|
1324
|
+
const { status, data } = await api.del(`/api/v1/projects/${pid}`);
|
|
1325
|
+
return { deleted: true, project: preview.project, removed: inv, resourceCount: total, meteringSnapshot: preview.meteringSnapshot, result: result(status, data) };
|
|
1247
1326
|
},
|
|
1248
1327
|
},
|
|
1249
1328
|
];
|