@biznetgio/neo-app 0.4.30 → 0.4.32
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 +155 -44
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
|
@@ -65,13 +65,17 @@ async function confirmCost(api, args, spec) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey)/i;
|
|
68
|
+
const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey|authorization)/i;
|
|
69
|
+
// Deployment snapshots (exact keys) embed rendered configs with plaintext creds — a
|
|
70
|
+
// queue's `--pass`, a DB's POSTGRES_PASSWORD. Redact by EXACT key so we don't also
|
|
71
|
+
// hit resourceId/source-of-truth-style names (substring match would catch "reSOURCEid").
|
|
72
|
+
const SECRET_EXACT = new Set(['source', 'installed']);
|
|
69
73
|
function redactSecrets(value) {
|
|
70
74
|
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
71
75
|
if (value && typeof value === 'object') {
|
|
72
76
|
const out = {};
|
|
73
77
|
for (const [k, v] of Object.entries(value)) {
|
|
74
|
-
out[k] = SECRET_KEY.test(k) ? '[redacted — credentials are write-only; bind them to a consumer instead]' : redactSecrets(v);
|
|
78
|
+
out[k] = (SECRET_KEY.test(k) || SECRET_EXACT.has(k.toLowerCase())) ? '[redacted — credentials are write-only; bind them to a consumer instead]' : redactSecrets(v);
|
|
75
79
|
}
|
|
76
80
|
return out;
|
|
77
81
|
}
|
|
@@ -120,8 +124,8 @@ export const TOOLS = [
|
|
|
120
124
|
{ type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
|
|
121
125
|
{ 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
126
|
{ 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)' },
|
|
127
|
+
{ 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)' },
|
|
128
|
+
{ 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
129
|
{ 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
130
|
{ 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
131
|
];
|
|
@@ -132,7 +136,7 @@ export const TOOLS = [
|
|
|
132
136
|
resourceTypes,
|
|
133
137
|
// Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
|
|
134
138
|
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
|
|
139
|
+
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
140
|
sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
|
|
137
141
|
environments: ['staging', 'production'],
|
|
138
142
|
pricing,
|
|
@@ -152,6 +156,25 @@ export const TOOLS = [
|
|
|
152
156
|
return (await api.get(`/api/v1/orgs/${orgId}/projects`)).data;
|
|
153
157
|
},
|
|
154
158
|
},
|
|
159
|
+
{
|
|
160
|
+
name: 'neo_create_project',
|
|
161
|
+
description: 'Create a new project in an organization — use it for a disposable project (e.g. isolated testing) or a fresh project to deploy into. orgId defaults to your primary org. Returns the new project (id, name).',
|
|
162
|
+
mutating: true,
|
|
163
|
+
schema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
name: str('Project name'),
|
|
167
|
+
description: str('Project description (optional)'),
|
|
168
|
+
orgId: str('Organization id (optional; defaults to your primary org)'),
|
|
169
|
+
},
|
|
170
|
+
required: ['name'],
|
|
171
|
+
},
|
|
172
|
+
async call(api, args) {
|
|
173
|
+
const orgId = await api.resolveOrgId(args.orgId);
|
|
174
|
+
const { status, data } = await api.post(`/api/v1/orgs/${orgId}/projects`, { name: args.name, description: args.description || '' });
|
|
175
|
+
return result(status, data);
|
|
176
|
+
},
|
|
177
|
+
},
|
|
155
178
|
{
|
|
156
179
|
name: 'neo_get_project',
|
|
157
180
|
description: 'Get one project by id.',
|
|
@@ -386,32 +409,45 @@ export const TOOLS = [
|
|
|
386
409
|
async call(api, args) {
|
|
387
410
|
const { data } = await api.post('/api/v1/repos/scan', { repoUrl: args.repoUrl, branch: args.branch, gitProviderId: args.gitProviderId });
|
|
388
411
|
const subdirs = data?.subdirs || [];
|
|
389
|
-
const components = subdirs.map((s) =>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
412
|
+
const components = subdirs.map((s) => {
|
|
413
|
+
const role = s.role || 'service'; // service | worker | support
|
|
414
|
+
const support = role === 'support' || s.deployable === false;
|
|
415
|
+
return {
|
|
416
|
+
path: s.path || '.',
|
|
417
|
+
name: s.name,
|
|
418
|
+
stack: s.type, // docker | node | go | python | java | php | ruby | rust | static
|
|
419
|
+
role, // service | worker | support (library/config — not deployable)
|
|
420
|
+
deployable: !support,
|
|
421
|
+
// Don't suggest a service type for a support/library package; a worker-named
|
|
422
|
+
// root suggests worker; static → static_site; else web.
|
|
423
|
+
suggestedServiceType: support ? null : (role === 'worker' ? 'worker' : (s.type === 'static' ? 'static_site' : 'web')),
|
|
424
|
+
suggestedBuildType: s.type === 'docker' ? 'dockerfile' : 'railpack',
|
|
425
|
+
suggestedPort: s.port || undefined, // from the Dockerfile EXPOSE, when present
|
|
426
|
+
contextDir: s.path || undefined,
|
|
427
|
+
detectedDependencies: s.deps || [], // database | queue | object_storage, inferred from the manifest
|
|
428
|
+
// Confidence (handoff 6): a Dockerfile/index.html is high; a worker guess from
|
|
429
|
+
// the tree is low (confirm); a plain language marker is medium.
|
|
430
|
+
typeConfidence: support ? 'low' : (role === 'worker' ? 'low' : ((s.type === 'docker' || s.type === 'static') ? 'high' : 'medium')),
|
|
431
|
+
evidence: { marker: s.type, role, manifestDeps: s.deps || [] },
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
const deployable = components.filter((c) => c.deployable);
|
|
435
|
+
const support = components.filter((c) => !c.deployable);
|
|
403
436
|
const anyDeps = components.some((c) => c.detectedDependencies.length);
|
|
404
437
|
return {
|
|
405
438
|
repoUrl: args.repoUrl,
|
|
406
439
|
count: components.length,
|
|
440
|
+
deployableCount: deployable.length,
|
|
407
441
|
components,
|
|
408
442
|
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
|
-
:
|
|
443
|
+
(deployable.length > 1
|
|
444
|
+
? '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).'
|
|
445
|
+
: deployable.length === 1
|
|
412
446
|
? 'One deployable component found.'
|
|
413
|
-
: 'No deployable component found
|
|
414
|
-
+ (
|
|
447
|
+
: 'No deployable component found.')
|
|
448
|
+
+ (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).` : '')
|
|
449
|
+
+ (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.')
|
|
450
|
+
+ (anyDeps ? ' Some components list detectedDependencies (database/queue/object_storage) — consider planning + binding them (confirm first).' : ''),
|
|
415
451
|
};
|
|
416
452
|
},
|
|
417
453
|
},
|
|
@@ -535,6 +571,16 @@ export const TOOLS = [
|
|
|
535
571
|
return redactSecrets(result(status, data));
|
|
536
572
|
},
|
|
537
573
|
},
|
|
574
|
+
{
|
|
575
|
+
name: 'neo_deploy_database',
|
|
576
|
+
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.',
|
|
577
|
+
mutating: true,
|
|
578
|
+
schema: { type: 'object', properties: { databaseId: str('Database id (from neo_create_database)') }, required: ['databaseId'] },
|
|
579
|
+
async call(api, args) {
|
|
580
|
+
const { status, data } = await api.post(`/api/v1/databases/${args.databaseId}/deploy`);
|
|
581
|
+
return redactSecrets(result(status, data));
|
|
582
|
+
},
|
|
583
|
+
},
|
|
538
584
|
{
|
|
539
585
|
name: 'neo_list_databases',
|
|
540
586
|
description: 'List managed databases in a project (credentials redacted).',
|
|
@@ -669,6 +715,16 @@ export const TOOLS = [
|
|
|
669
715
|
return redactSecrets(result(status, data));
|
|
670
716
|
},
|
|
671
717
|
},
|
|
718
|
+
{
|
|
719
|
+
name: 'neo_deploy_queue',
|
|
720
|
+
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.',
|
|
721
|
+
mutating: true,
|
|
722
|
+
schema: { type: 'object', properties: { queueId: str('Queue id (from neo_create_queue)') }, required: ['queueId'] },
|
|
723
|
+
async call(api, args) {
|
|
724
|
+
const { status, data } = await api.post(`/api/v1/queues/${args.queueId}/deploy`);
|
|
725
|
+
return redactSecrets(result(status, data));
|
|
726
|
+
},
|
|
727
|
+
},
|
|
672
728
|
{
|
|
673
729
|
name: 'neo_list_queues',
|
|
674
730
|
description: 'List managed queues in a project (credentials redacted).',
|
|
@@ -762,12 +818,15 @@ export const TOOLS = [
|
|
|
762
818
|
url: str('Target URL (mode=http_endpoint)'),
|
|
763
819
|
method: str('HTTP method (mode=http_endpoint), e.g. GET/POST'),
|
|
764
820
|
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
821
|
+
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
822
|
},
|
|
766
823
|
required: ['projectId', 'name', 'mode', 'scheduleType', 'environment'],
|
|
767
824
|
},
|
|
768
825
|
async call(api, args) {
|
|
769
826
|
requireEnv(args.environment);
|
|
770
|
-
const
|
|
827
|
+
const gate = await confirmCost(api, args, {}); // cron cost is variable (per-run) — gate surfaces the rate
|
|
828
|
+
if (gate) return gate;
|
|
829
|
+
const { projectId, confirm, orgId, ...body } = args;
|
|
771
830
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/crons`, body);
|
|
772
831
|
return redactSecrets(result(status, data));
|
|
773
832
|
},
|
|
@@ -821,11 +880,14 @@ export const TOOLS = [
|
|
|
821
880
|
region: str('Region (optional; see neo_list_regions on the portal)'),
|
|
822
881
|
acl: { type: 'string', description: 'private | public-read (optional, default private)' },
|
|
823
882
|
quotaGB: int('Storage quota in GB (optional)'),
|
|
883
|
+
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
884
|
},
|
|
825
885
|
required: ['projectId', 'name'],
|
|
826
886
|
},
|
|
827
887
|
async call(api, args) {
|
|
828
|
-
const
|
|
888
|
+
const gate = await confirmCost(api, args, { objectStorageGB: gbFromSize(args.quotaGB) });
|
|
889
|
+
if (gate) return gate;
|
|
890
|
+
const { projectId, confirm, orgId, ...body } = args;
|
|
829
891
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/buckets`, body);
|
|
830
892
|
return redactSecrets(result(status, data));
|
|
831
893
|
},
|
|
@@ -952,7 +1014,7 @@ export const TOOLS = [
|
|
|
952
1014
|
{
|
|
953
1015
|
name: 'neo_plan_stack',
|
|
954
1016
|
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.',
|
|
1017
|
+
'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
1018
|
mutating: false,
|
|
957
1019
|
schema: {
|
|
958
1020
|
type: 'object',
|
|
@@ -961,16 +1023,16 @@ export const TOOLS = [
|
|
|
961
1023
|
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
962
1024
|
resources: {
|
|
963
1025
|
type: 'array',
|
|
964
|
-
description: 'The stack resources. Each needs a unique name + kind (app | database | queue | bucket).',
|
|
1026
|
+
description: 'The stack resources. Each needs a unique name + kind (app | database | queue | bucket | cron).',
|
|
965
1027
|
items: {
|
|
966
1028
|
type: 'object',
|
|
967
1029
|
properties: {
|
|
968
|
-
kind: { type: 'string', enum: ['app', 'database', 'queue', 'bucket'] },
|
|
1030
|
+
kind: { type: 'string', enum: ['app', 'database', 'queue', 'bucket', 'cron'] },
|
|
969
1031
|
name: str('Unique resource name within this stack (bindings reference it)'),
|
|
970
1032
|
serviceType: str('app: web | worker | static_site'),
|
|
971
1033
|
buildCommand: str('app/static_site: build command (e.g. "npm ci && npm run build")'),
|
|
972
1034
|
publishDirectory: str('static_site: built-files directory to serve (e.g. "dist")'),
|
|
973
|
-
image: str('app: prebuilt image reference'),
|
|
1035
|
+
image: str('app/cron: prebuilt image reference'),
|
|
974
1036
|
repoUrl: str('app: git repository URL'),
|
|
975
1037
|
branch: str('app: git branch'),
|
|
976
1038
|
port: int('app: container port'),
|
|
@@ -980,20 +1042,29 @@ export const TOOLS = [
|
|
|
980
1042
|
volumeSize: str('database: storage size, e.g. 10Gi'),
|
|
981
1043
|
quotaGB: int('bucket: storage quota GB'),
|
|
982
1044
|
region: str('bucket: region'),
|
|
1045
|
+
mode: str('cron: app_command | http_endpoint'),
|
|
1046
|
+
scheduleType: str('cron: cron | interval | once | seconds'),
|
|
1047
|
+
schedule: str('cron: cron expression (scheduleType=cron)'),
|
|
1048
|
+
intervalMinutes: int('cron: interval in minutes (scheduleType=interval)'),
|
|
1049
|
+
timezone: str('cron: IANA timezone (optional)'),
|
|
1050
|
+
serviceId: str('cron app_command: target service — another resource NAME in this stack, or a literal service id'),
|
|
1051
|
+
command: { type: 'array', items: { type: 'string' }, description: 'cron app_command: command argv' },
|
|
1052
|
+
url: str('cron http_endpoint: target URL'),
|
|
1053
|
+
method: str('cron http_endpoint: HTTP method'),
|
|
983
1054
|
},
|
|
984
1055
|
required: ['kind', 'name'],
|
|
985
1056
|
},
|
|
986
1057
|
},
|
|
987
1058
|
bindings: {
|
|
988
1059
|
type: 'array',
|
|
989
|
-
description: 'Wire a database/queue into a consumer app (injected as
|
|
1060
|
+
description: 'Wire a database/queue/bucket into a consumer app (injected as env var(s) at deploy, server-side).',
|
|
990
1061
|
items: {
|
|
991
1062
|
type: 'object',
|
|
992
1063
|
properties: {
|
|
993
1064
|
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)'),
|
|
1065
|
+
targetType: { type: 'string', enum: ['neo_db', 'neo_queue', 'neo_object_storage'] },
|
|
1066
|
+
target: str('The database/queue/bucket resource name'),
|
|
1067
|
+
envKey: str('Env var name/prefix (optional; default DATABASE_URL / NATS_URL / S3)'),
|
|
997
1068
|
},
|
|
998
1069
|
required: ['consumer', 'targetType', 'target'],
|
|
999
1070
|
},
|
|
@@ -1179,9 +1250,9 @@ export const TOOLS = [
|
|
|
1179
1250
|
type: 'object',
|
|
1180
1251
|
properties: {
|
|
1181
1252
|
databaseId: str('Database id (postgres, already deployed)'),
|
|
1182
|
-
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR
|
|
1253
|
+
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
1254
|
},
|
|
1184
|
-
required: ['databaseId'],
|
|
1255
|
+
required: ['databaseId', 'allowlistCidrs'],
|
|
1185
1256
|
},
|
|
1186
1257
|
async call(api, args) {
|
|
1187
1258
|
const { status, data } = await api.post(`/api/v1/databases/${args.databaseId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
@@ -1206,9 +1277,9 @@ export const TOOLS = [
|
|
|
1206
1277
|
type: 'object',
|
|
1207
1278
|
properties: {
|
|
1208
1279
|
queueId: str('Queue id (already deployed)'),
|
|
1209
|
-
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR
|
|
1280
|
+
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
1281
|
},
|
|
1211
|
-
required: ['queueId'],
|
|
1282
|
+
required: ['queueId', 'allowlistCidrs'],
|
|
1212
1283
|
},
|
|
1213
1284
|
async call(api, args) {
|
|
1214
1285
|
const { status, data } = await api.post(`/api/v1/queues/${args.queueId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
@@ -1227,23 +1298,63 @@ export const TOOLS = [
|
|
|
1227
1298
|
},
|
|
1228
1299
|
{
|
|
1229
1300
|
name: 'neo_delete_project',
|
|
1230
|
-
description: '
|
|
1301
|
+
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
1302
|
mutating: true,
|
|
1232
1303
|
destructive: true,
|
|
1233
1304
|
schema: {
|
|
1234
1305
|
type: 'object',
|
|
1235
1306
|
properties: {
|
|
1236
1307
|
projectId: str('Project id'),
|
|
1237
|
-
confirm: { type: 'boolean', description: 'Must be true to
|
|
1308
|
+
confirm: { type: 'boolean', description: 'Must be true to delete. Without it you get a preview only (nothing is deleted).' },
|
|
1309
|
+
cascade: { type: 'boolean', description: 'Required true to delete a NON-EMPTY project — acknowledges every resource in the inventory is destroyed.' },
|
|
1238
1310
|
},
|
|
1239
|
-
required: ['projectId'
|
|
1311
|
+
required: ['projectId'],
|
|
1240
1312
|
},
|
|
1241
1313
|
async call(api, args) {
|
|
1314
|
+
const pid = args.projectId;
|
|
1315
|
+
// Gather inventory + usage BEFORE any delete (this is also the metering snapshot).
|
|
1316
|
+
const safeGet = async (p) => { try { return (await api.get(p)).data; } catch { return null; } };
|
|
1317
|
+
const [proj, apps, dbs, queues, crons, buckets, usage] = await Promise.all([
|
|
1318
|
+
safeGet(`/api/v1/projects/${pid}`),
|
|
1319
|
+
safeGet(`/api/v1/projects/${pid}/apps`),
|
|
1320
|
+
safeGet(`/api/v1/projects/${pid}/databases`),
|
|
1321
|
+
safeGet(`/api/v1/projects/${pid}/queues`),
|
|
1322
|
+
safeGet(`/api/v1/projects/${pid}/crons`),
|
|
1323
|
+
safeGet(`/api/v1/projects/${pid}/buckets`),
|
|
1324
|
+
safeGet(`/api/v1/projects/${pid}/usage`),
|
|
1325
|
+
]);
|
|
1326
|
+
const arr = (x, k) => (Array.isArray(x) ? x : (x && Array.isArray(x[k]) ? x[k] : []));
|
|
1327
|
+
const inv = {
|
|
1328
|
+
services: arr(apps, 'applications').map((a) => a.name || a.id), // list endpoint wraps as {applications:[…]}
|
|
1329
|
+
databases: arr(dbs, 'databases').map((d) => d.name || d.id),
|
|
1330
|
+
queues: arr(queues, 'queues').map((q) => q.name || q.id),
|
|
1331
|
+
crons: arr(crons, 'crons').map((c) => c.name || c.id),
|
|
1332
|
+
buckets: arr(buckets, 'buckets').map((b) => b.name || b.id),
|
|
1333
|
+
};
|
|
1334
|
+
const total = Object.values(inv).reduce((n, a) => n + a.length, 0);
|
|
1335
|
+
// Fail-safe: if any inventory fetch FAILED (null, not empty), we can't prove the
|
|
1336
|
+
// project is empty — treat it as non-empty so the empty-path can't silently
|
|
1337
|
+
// delete hidden resources (the preview must be trustworthy).
|
|
1338
|
+
const inventoryIncomplete = [apps, dbs, queues, crons, buckets].some((x) => x === null);
|
|
1339
|
+
const preview = {
|
|
1340
|
+
project: proj ? { id: proj.id, name: proj.name, organizationId: proj.organizationId } : { id: pid },
|
|
1341
|
+
resourceInventory: inv,
|
|
1342
|
+
resourceCount: total,
|
|
1343
|
+
meteringSnapshot: usage ? { period: usage.period, total: usage.total ?? usage.totalCost, currency: usage.currency } : null,
|
|
1344
|
+
inventoryIncomplete: inventoryIncomplete || undefined,
|
|
1345
|
+
warning: 'IRREVERSIBLE: deletes the project, every resource above, and their data (database/queue volumes included). No refund for prepaid or accrued usage.',
|
|
1346
|
+
};
|
|
1347
|
+
// Treat an unverifiable inventory as non-empty — never allow the no-cascade path
|
|
1348
|
+
// when we could not confirm the project is actually empty.
|
|
1349
|
+
const requireCascade = total > 0 || inventoryIncomplete;
|
|
1242
1350
|
if (args.confirm !== true) {
|
|
1243
|
-
return {
|
|
1351
|
+
return { ...preview, deleted: false, requiresConfirmation: true, message: requireCascade ? '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
1352
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1353
|
+
if (requireCascade && args.cascade !== true) {
|
|
1354
|
+
return { ...preview, deleted: false, error: 'cascade_required', message: inventoryIncomplete ? 'Could not fully verify the project inventory — re-call with confirm:true AND cascade:true to delete anyway.' : `This project has ${total} resource(s). Re-call with confirm:true AND cascade:true to delete them all.` };
|
|
1355
|
+
}
|
|
1356
|
+
const { status, data } = await api.del(`/api/v1/projects/${pid}`);
|
|
1357
|
+
return { deleted: true, project: preview.project, removed: inv, resourceCount: total, meteringSnapshot: preview.meteringSnapshot, result: result(status, data) };
|
|
1247
1358
|
},
|
|
1248
1359
|
},
|
|
1249
1360
|
];
|