@biznetgio/neo-app 0.4.36 → 0.4.38
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/mcp/tools.js +160 -49
package/package.json
CHANGED
package/src/mcp/tools.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { resolve, basename } from 'node:path';
|
|
6
6
|
import { statSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { createHash } from 'node:crypto';
|
|
8
7
|
import { deploySource } from '../deploySource.js';
|
|
9
8
|
import { pkgVersion } from '../util.js';
|
|
10
9
|
import { estimateCost } from '../pricing.js';
|
|
@@ -44,8 +43,8 @@ function requireEnv(env) {
|
|
|
44
43
|
// actually converges (GET → 404), so a delete isn't reported done while the record
|
|
45
44
|
// still exists. Returns a durable operation reference (`del-<id>`) + convergence
|
|
46
45
|
// state (handoff 3.6; 0.4.34 retest §9 "every delete returns a durable operation ID").
|
|
47
|
-
async function deleteAndConverge(api, path, resourceId, kind) {
|
|
48
|
-
const { status, data } = await api.del(path);
|
|
46
|
+
async function deleteAndConverge(api, path, resourceId, kind, delQuery = '') {
|
|
47
|
+
const { status, data } = await api.del(path + delQuery);
|
|
49
48
|
let converged = false, checks = 0;
|
|
50
49
|
for (let i = 0; i < 12; i++) {
|
|
51
50
|
checks++;
|
|
@@ -67,15 +66,6 @@ function gbFromSize(v) {
|
|
|
67
66
|
return m ? parseInt(m[1], 10) : undefined;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
// deletionHashOf is an immutable approval token for a project deletion: a short hash
|
|
71
|
-
// of the exact inventory being deleted. The preview returns it; a cascade delete must
|
|
72
|
-
// echo it back, and the tool recomputes the live inventory and refuses a mismatch — so
|
|
73
|
-
// a project can't be deleted against a stale/partial inventory (0.4.31 retest §7).
|
|
74
|
-
function deletionHashOf(pid, inv, total) {
|
|
75
|
-
const names = Object.entries(inv).flatMap(([k, a]) => a.map((n) => k + ':' + n)).sort();
|
|
76
|
-
return createHash('sha256').update(JSON.stringify({ pid, total, names })).digest('hex').slice(0, 16);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
69
|
// confirmCost is the Approve & Create gate for billable one-shot creates: unless
|
|
80
70
|
// args.confirm===true it returns the live-priced cost and creates NOTHING — the
|
|
81
71
|
// agent shows the user, then re-calls with confirm:true. plan/apply remains the
|
|
@@ -87,9 +77,11 @@ async function confirmCost(api, args, spec) {
|
|
|
87
77
|
let book = null;
|
|
88
78
|
try { book = (await api.get(orgId ? `/api/v1/orgs/${orgId}/pricing` : '/api/v1/pricing')).data; }
|
|
89
79
|
catch { try { book = (await api.get('/api/v1/pricing')).data; } catch { book = null; } }
|
|
80
|
+
let cost = null;
|
|
81
|
+
if (book) { try { cost = estimateCost(book, spec); } catch { cost = null; } } // never fail the gate on an estimate error
|
|
90
82
|
return {
|
|
91
83
|
requiresConfirmation: true,
|
|
92
|
-
cost
|
|
84
|
+
cost,
|
|
93
85
|
priceBook: orgId ? `org:${orgId}` : 'global-defaults',
|
|
94
86
|
message: 'Billable resource — show the user this cost, then re-call with confirm:true to create. For an immutable, server-pinned quote use neo_plan_stack → neo_apply_stack.',
|
|
95
87
|
};
|
|
@@ -150,10 +142,10 @@ export const TOOLS = [
|
|
|
150
142
|
// Honest support matrix: what THIS build does, not the roadmap. `create`
|
|
151
143
|
// is true, false, or 'via_folder' (created through neo_deploy_folder).
|
|
152
144
|
const resourceTypes = [
|
|
153
|
-
{ type: 'web', create: true, deploy: true, scale: true, logs: true, variables: true, delete: true },
|
|
154
|
-
{ type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
|
|
155
|
-
{ 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
|
|
156
|
-
{ type: 'worker', create: 'via_folder', deploy: true, scale: true, logs: true, variables: true, delete: true, note: 'neo_deploy_folder serviceType=worker
|
|
145
|
+
{ type: 'web', create: true, deploy: true, scale: true, update: true, domains: true, health: true, autoscale: true, logs: true, variables: true, usage: true, delete: true, note: 'neo_get/update_service (compute/replicas/autoscale/port/command/domain/tls/path/health) + scale + add/remove_service_domain + get_logs + service_usage' },
|
|
146
|
+
{ type: 'template', create: true, deploy: true, scale: false, update: true, logs: true, variables: true, delete: true },
|
|
147
|
+
{ type: 'static_site', create: 'via_folder', deploy: true, scale: false, update: true, domains: true, logs: true, variables: true, delete: true, nativeStatic: true, note: 'neo_deploy_folder serviceType=static_site with buildCommand/publishDirectory → native nginx (0.4.27); neo_update_service for domain/tls/path' },
|
|
148
|
+
{ type: 'worker', create: 'via_folder', deploy: true, scale: true, update: true, health: true, autoscale: true, logs: true, variables: true, usage: true, delete: true, note: 'neo_deploy_folder serviceType=worker (no public ingress) + neo_update_service (compute/replicas/autoscale/command/health)' },
|
|
157
149
|
{ type: 'database', create: true, deploy: true, get: true, list: true, update: true, backup: true, restore: true, verify: true, publicAccess: true, delete: true, note: 'neo_create/deploy/list/get/update(resize)/delete_database + backup + restore (postgres, local backups) + verify (SQL round-trip) + enable/disable_db_public_access (handoff 4.4)' },
|
|
158
150
|
{ 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)' },
|
|
159
151
|
{ 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)' },
|
|
@@ -165,7 +157,7 @@ export const TOOLS = [
|
|
|
165
157
|
controlPlane,
|
|
166
158
|
resourceTypes,
|
|
167
159
|
// Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
|
|
168
|
-
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', 'restore', 'delete', 'delete_project', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
160
|
+
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', 'add_domain', 'remove_domain', 'logs', 'get_variables', 'set_variable', 'estimate', 'scan_repo', 'bind', 'unbind', 'run_cron', 'cron_questionnaire', 'presign_object', 'backup', 'restore', 'delete', 'delete_project', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
169
161
|
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)' },
|
|
170
162
|
sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
|
|
171
163
|
environments: ['staging', 'production'],
|
|
@@ -214,6 +206,33 @@ export const TOOLS = [
|
|
|
214
206
|
return (await api.get(`/api/v1/projects/${args.projectId}`)).data;
|
|
215
207
|
},
|
|
216
208
|
},
|
|
209
|
+
{
|
|
210
|
+
name: 'neo_get_confirmation',
|
|
211
|
+
description: 'Read a confirmation record (the server-side approval for a high-risk action) by id — its action, status, redacted summary, whether an exact-name phrase is required, and expiry. Read-only; never contains secrets.',
|
|
212
|
+
mutating: false,
|
|
213
|
+
schema: { type: 'object', properties: { confirmationId: str('Confirmation id (from a destructive preview or a plan)') }, required: ['confirmationId'] },
|
|
214
|
+
async call(api, args) {
|
|
215
|
+
return (await api.get(`/api/v1/confirmations/${args.confirmationId}`)).data;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'neo_confirm_action',
|
|
220
|
+
description: 'Record a human DECISION on a confirmation (NEVER creates or deletes anything itself) — the server-enforced approval step for a high-risk action (project/service deletion, billable create). decision is approve | decline | cancel. Approving a DELETION requires confirmationPhrase to exactly equal the target name (the human must type it). After approve, call the action tool again with this confirmationId to actually perform it (single-use). Show the human the summary from the preview and ask once; never infer consent.',
|
|
221
|
+
mutating: true,
|
|
222
|
+
schema: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: {
|
|
225
|
+
confirmationId: str('Confirmation id (from a destructive preview or a plan)'),
|
|
226
|
+
decision: { type: 'string', enum: ['approve', 'decline', 'cancel'], description: 'approve | decline | cancel' },
|
|
227
|
+
confirmationPhrase: str('For a deletion: the EXACT target name, typed by the human, required to approve'),
|
|
228
|
+
},
|
|
229
|
+
required: ['confirmationId', 'decision'],
|
|
230
|
+
},
|
|
231
|
+
async call(api, args) {
|
|
232
|
+
const { status, data } = await api.post(`/api/v1/confirmations/${args.confirmationId}/decide`, { decision: args.decision, confirmationPhrase: args.confirmationPhrase });
|
|
233
|
+
return result(status, data);
|
|
234
|
+
},
|
|
235
|
+
},
|
|
217
236
|
{
|
|
218
237
|
name: 'neo_list_services',
|
|
219
238
|
description: 'List services/apps in a project.',
|
|
@@ -223,6 +242,81 @@ export const TOOLS = [
|
|
|
223
242
|
return (await api.get(`/api/v1/projects/${args.projectId}/apps`)).data;
|
|
224
243
|
},
|
|
225
244
|
},
|
|
245
|
+
{
|
|
246
|
+
name: 'neo_get_service',
|
|
247
|
+
description: 'Get one service/app by id — its type, compute, replicas/autoscale, port, command, domain/TLS/custom domains, health-check paths, and env keys (secret values redacted). Redeploy for config changes to take effect.',
|
|
248
|
+
mutating: false,
|
|
249
|
+
schema: { type: 'object', properties: { appId: str('Service/app id (from neo_list_services)') }, required: ['appId'] },
|
|
250
|
+
async call(api, args) {
|
|
251
|
+
return redactSecrets((await api.get(`/api/v1/apps/${args.appId}`)).data);
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'neo_update_service',
|
|
256
|
+
description: 'Update a service/app config (handoff §4.1/§4.2). Exposes compute (computeTier OR cpuLimit+memoryLimit), replicas, autoscale, port, command, domain, tls, ingress path, and health/liveness/readiness/startup checks. Cost-changing fields (compute/replicas/autoscale) require confirm:true — without it the new cost is returned and nothing is applied. Redeploy the service for changes to take effect. Omitted fields are left unchanged.',
|
|
257
|
+
mutating: true,
|
|
258
|
+
schema: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
properties: {
|
|
261
|
+
appId: str('Service/app id'),
|
|
262
|
+
computeTier: str('Bundled compute tier (micro | small | medium | large | xlarge)'),
|
|
263
|
+
cpuLimit: str('Custom CPU limit, e.g. "500m" (use with memoryLimit; unbundled)'),
|
|
264
|
+
memoryLimit: str('Custom memory limit, e.g. "512Mi" (unbundled)'),
|
|
265
|
+
replicas: int('Fixed replica count'),
|
|
266
|
+
autoscaleMin: int('Autoscale min replicas'),
|
|
267
|
+
autoscaleMax: int('Autoscale max replicas'),
|
|
268
|
+
autoscaleCpu: int('Autoscale target CPU %'),
|
|
269
|
+
autoscaleMemory: int('Autoscale target memory %'),
|
|
270
|
+
port: int('Container port'),
|
|
271
|
+
command: { type: 'array', items: { type: 'string' }, description: 'Override the image command (argv)' },
|
|
272
|
+
domain: str('Primary domain ("" clears it back to the default host)'),
|
|
273
|
+
tls: { type: 'boolean', description: 'Enable/disable managed TLS on the domain' },
|
|
274
|
+
path: str('Ingress path prefix'),
|
|
275
|
+
healthPath: str('HTTP health-check path'),
|
|
276
|
+
livenessPath: str('Liveness probe path'),
|
|
277
|
+
readinessPath: str('Readiness probe path'),
|
|
278
|
+
startupPath: str('Startup probe path'),
|
|
279
|
+
healthTimeoutSeconds: int('Health-check timeout (s)'),
|
|
280
|
+
healthGraceSeconds: int('Health-check grace period (s)'),
|
|
281
|
+
confirm: { type: 'boolean', description: 'Approve a cost change (compute/replicas/autoscale): without it the new cost is returned and nothing is applied.' },
|
|
282
|
+
},
|
|
283
|
+
required: ['appId'],
|
|
284
|
+
},
|
|
285
|
+
async call(api, args) {
|
|
286
|
+
const { appId, confirm, orgId, computeTier, ...rest } = args;
|
|
287
|
+
const body = { ...rest };
|
|
288
|
+
if (computeTier) { body.billingMode = 'bundled'; body.cpuLimit = computeTier; }
|
|
289
|
+
else if (args.cpuLimit || args.memoryLimit) { body.billingMode = 'unbundled'; }
|
|
290
|
+
const costChanging = computeTier || args.cpuLimit || args.memoryLimit || args.replicas !== undefined || args.autoscaleMax !== undefined;
|
|
291
|
+
if (costChanging && confirm !== true) {
|
|
292
|
+
const gate = await confirmCost(api, args, { computeTier, replicas: args.replicas ?? args.autoscaleMax });
|
|
293
|
+
if (gate) return { ...gate, change: 'update service compute/scaling', message: 'This changes cost — re-call with confirm:true to apply.' };
|
|
294
|
+
}
|
|
295
|
+
const { status, data } = await api.put(`/api/v1/apps/${appId}`, body);
|
|
296
|
+
return redactSecrets(result(status, data));
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'neo_add_service_domain',
|
|
301
|
+
description: 'Attach an additional custom ingress domain to a service (handoff §4.1). Point the domain\'s DNS at the platform first; managed TLS is issued automatically. The default platform host is preserved.',
|
|
302
|
+
mutating: true,
|
|
303
|
+
schema: { type: 'object', properties: { appId: str('Service/app id'), domain: str('Custom domain host, e.g. "app.example.com"') }, required: ['appId', 'domain'] },
|
|
304
|
+
async call(api, args) {
|
|
305
|
+
const { status, data } = await api.post(`/api/v1/apps/${args.appId}/domains`, { domain: args.domain });
|
|
306
|
+
return result(status, data);
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: 'neo_remove_service_domain',
|
|
311
|
+
description: 'Detach a custom domain from a service. The default platform host and other domains are untouched.',
|
|
312
|
+
mutating: true,
|
|
313
|
+
destructive: true,
|
|
314
|
+
schema: { type: 'object', properties: { appId: str('Service/app id'), domain: str('Custom domain host to remove') }, required: ['appId', 'domain'] },
|
|
315
|
+
async call(api, args) {
|
|
316
|
+
const { status, data } = await api.del(`/api/v1/apps/${args.appId}/domains`, { body: { domain: args.domain } });
|
|
317
|
+
return result(status, data);
|
|
318
|
+
},
|
|
319
|
+
},
|
|
226
320
|
{
|
|
227
321
|
name: 'neo_get_deployment',
|
|
228
322
|
description: 'Get a deployment by id (poll this after neo_deploy until status is terminal).',
|
|
@@ -1286,12 +1380,34 @@ export const TOOLS = [
|
|
|
1286
1380
|
// the server hard-deletes the record and tears down the running workload. ---
|
|
1287
1381
|
{
|
|
1288
1382
|
name: 'neo_delete_service',
|
|
1289
|
-
description: '
|
|
1383
|
+
description: 'Delete a service (web / worker / static_site) and tear down its workload. SAFE BY DEFAULT: with no confirmationId it returns the service details + a server-side CONFIRMATION, deleting NOTHING. Then: show the user, call neo_confirm_action(confirmationId, "approve", confirmationPhrase=<EXACT service name>), then re-call with that confirmationId. Server-enforced, single-use, exact-name. Destructive and irreversible.',
|
|
1290
1384
|
mutating: true,
|
|
1291
1385
|
destructive: true,
|
|
1292
|
-
schema: {
|
|
1386
|
+
schema: {
|
|
1387
|
+
type: 'object',
|
|
1388
|
+
properties: {
|
|
1389
|
+
appId: str('Service/app id (from neo_list_services)'),
|
|
1390
|
+
confirmationId: str('An APPROVED confirmation id (from neo_confirm_action). Omit to get the confirmation.'),
|
|
1391
|
+
},
|
|
1392
|
+
required: ['appId'],
|
|
1393
|
+
},
|
|
1293
1394
|
async call(api, args) {
|
|
1294
|
-
|
|
1395
|
+
if (args.confirmationId) {
|
|
1396
|
+
return deleteAndConverge(api, `/api/v1/apps/${args.appId}`, args.appId, 'service', `?confirmationId=${encodeURIComponent(args.confirmationId)}`);
|
|
1397
|
+
}
|
|
1398
|
+
let svc = null;
|
|
1399
|
+
try { svc = (await api.get(`/api/v1/apps/${args.appId}`)).data; } catch { /* preview best-effort */ }
|
|
1400
|
+
let c;
|
|
1401
|
+
try { c = (await api.post('/api/v1/confirmations', { action: 'service_delete', targetId: args.appId })).data; }
|
|
1402
|
+
catch (e) { return { deleted: false, error: 'confirmation_failed', message: 'Could not create a deletion confirmation: ' + (e.message || e) }; }
|
|
1403
|
+
const phrase = (c.summary && c.summary.phraseRequired) || (svc && svc.name);
|
|
1404
|
+
return {
|
|
1405
|
+
deleted: false, requiresConfirmation: true,
|
|
1406
|
+
service: svc ? { id: svc.id, name: svc.name, type: svc.serviceType, environment: svc.environment } : { id: args.appId },
|
|
1407
|
+
warning: 'IRREVERSIBLE: deletes the service and tears down its running workload.',
|
|
1408
|
+
confirmationId: c.confirmationId, phraseRequired: phrase,
|
|
1409
|
+
message: `To delete: call neo_confirm_action("${c.confirmationId}", "approve", confirmationPhrase="${phrase}") — the human must type the exact service name — then re-call neo_delete_service with confirmationId="${c.confirmationId}".`,
|
|
1410
|
+
};
|
|
1295
1411
|
},
|
|
1296
1412
|
},
|
|
1297
1413
|
{
|
|
@@ -1390,21 +1506,24 @@ export const TOOLS = [
|
|
|
1390
1506
|
},
|
|
1391
1507
|
{
|
|
1392
1508
|
name: 'neo_delete_project',
|
|
1393
|
-
description: 'Delete a project and ALL its resources (apps, databases, queues, crons, buckets, namespace + PVCs/CNPG). SAFE BY DEFAULT:
|
|
1509
|
+
description: 'Delete a project and ALL its resources (apps, databases, queues, crons, buckets, namespace + PVCs/CNPG). SAFE BY DEFAULT: with no confirmationId it returns a PREVIEW (name, org, full inventory, metering, no-refund) AND a server-side CONFIRMATION, deleting NOTHING. Then: show the user, call neo_confirm_action(confirmationId, "approve", confirmationPhrase=<EXACT project name>), then re-call this tool with that confirmationId. Server-enforced, single-use, exact-name. Destructive and irreversible. Only the creator or an org owner/admin may delete.',
|
|
1394
1510
|
mutating: true,
|
|
1395
1511
|
destructive: true,
|
|
1396
1512
|
schema: {
|
|
1397
1513
|
type: 'object',
|
|
1398
1514
|
properties: {
|
|
1399
1515
|
projectId: str('Project id'),
|
|
1400
|
-
|
|
1401
|
-
cascade: { type: 'boolean', description: 'Required true to delete a NON-EMPTY project — acknowledges every resource in the inventory is destroyed.' },
|
|
1402
|
-
deletionHash: str('For a non-empty project: the deletionHash from the preview. The tool refuses if the live inventory no longer matches it (immutable approval).'),
|
|
1516
|
+
confirmationId: str('An APPROVED confirmation id (from neo_confirm_action). Omit to get the preview + a fresh confirmation.'),
|
|
1403
1517
|
},
|
|
1404
1518
|
required: ['projectId'],
|
|
1405
1519
|
},
|
|
1406
1520
|
async call(api, args) {
|
|
1407
1521
|
const pid = args.projectId;
|
|
1522
|
+
// With an approved confirmation: the server consumes it atomically and deletes.
|
|
1523
|
+
if (args.confirmationId) {
|
|
1524
|
+
const { status, data } = await api.del(`/api/v1/projects/${pid}?confirmationId=${encodeURIComponent(args.confirmationId)}`);
|
|
1525
|
+
return { deleted: true, project: { id: pid }, result: result(status, data) };
|
|
1526
|
+
}
|
|
1408
1527
|
// Gather inventory + usage BEFORE any delete (this is also the metering snapshot).
|
|
1409
1528
|
const safeGet = async (p) => { try { return (await api.get(p)).data; } catch { return null; } };
|
|
1410
1529
|
const [proj, apps, dbs, queues, crons, buckets, usage] = await Promise.all([
|
|
@@ -1429,36 +1548,28 @@ export const TOOLS = [
|
|
|
1429
1548
|
// project is empty — treat it as non-empty so the empty-path can't silently
|
|
1430
1549
|
// delete hidden resources (the preview must be trustworthy).
|
|
1431
1550
|
const inventoryIncomplete = [apps, dbs, queues, crons, buckets].some((x) => x === null);
|
|
1432
|
-
|
|
1551
|
+
// Create a server-side, single-use, exact-name confirmation for this deletion.
|
|
1552
|
+
let confirmationId, phraseRequired = proj && proj.name;
|
|
1553
|
+
try {
|
|
1554
|
+
const c = (await api.post('/api/v1/confirmations', { action: 'project_delete', projectId: pid })).data;
|
|
1555
|
+
confirmationId = c.confirmationId;
|
|
1556
|
+
phraseRequired = (c.summary && c.summary.phraseRequired) || phraseRequired;
|
|
1557
|
+
} catch (e) {
|
|
1558
|
+
return { deleted: false, error: 'confirmation_failed', message: 'Could not create a deletion confirmation: ' + (e.message || e) };
|
|
1559
|
+
}
|
|
1560
|
+
return {
|
|
1561
|
+
deleted: false,
|
|
1562
|
+
requiresConfirmation: true,
|
|
1433
1563
|
project: proj ? { id: proj.id, name: proj.name, organizationId: proj.organizationId } : { id: pid },
|
|
1434
1564
|
resourceInventory: inv,
|
|
1435
1565
|
resourceCount: total,
|
|
1436
1566
|
meteringSnapshot: usage ? { period: usage.period, total: usage.total ?? usage.totalCost, currency: usage.currency } : null,
|
|
1437
1567
|
inventoryIncomplete: inventoryIncomplete || undefined,
|
|
1438
|
-
|
|
1439
|
-
|
|
1568
|
+
warning: 'IRREVERSIBLE: deletes the project, every resource above, and their data (database/queue volumes included). No refund for prepaid or accrued usage. The organization itself remains.',
|
|
1569
|
+
confirmationId,
|
|
1570
|
+
phraseRequired,
|
|
1571
|
+
message: `Show the user this inventory. To delete: call neo_confirm_action("${confirmationId}", "approve", confirmationPhrase="${phraseRequired}") — the human must type the exact project name — then re-call neo_delete_project with confirmationId="${confirmationId}".`,
|
|
1440
1572
|
};
|
|
1441
|
-
// Treat an unverifiable inventory as non-empty — never allow the no-cascade path
|
|
1442
|
-
// when we could not confirm the project is actually empty.
|
|
1443
|
-
const requireCascade = total > 0 || inventoryIncomplete;
|
|
1444
|
-
if (args.confirm !== true) {
|
|
1445
|
-
return { ...preview, deleted: false, requiresConfirmation: true, message: requireCascade ? 'Show this inventory to the user. To delete, re-call with confirm:true, cascade:true, and deletionHash.' : 'Empty project. To delete, re-call with confirm:true.' };
|
|
1446
|
-
}
|
|
1447
|
-
if (requireCascade && args.cascade !== true) {
|
|
1448
|
-
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.` };
|
|
1449
|
-
}
|
|
1450
|
-
// Immutable approval: a non-empty cascade delete must echo the preview's
|
|
1451
|
-
// deletionHash, and it must still match the live inventory.
|
|
1452
|
-
if (total > 0) {
|
|
1453
|
-
if (!args.deletionHash) {
|
|
1454
|
-
return { ...preview, deleted: false, error: 'deletion_hash_required', message: 'Pass deletionHash (from the preview) to confirm you are deleting exactly this inventory.' };
|
|
1455
|
-
}
|
|
1456
|
-
if (args.deletionHash !== preview.deletionHash) {
|
|
1457
|
-
return { ...preview, deleted: false, error: 'inventory_changed', message: 'The project inventory changed since your preview — re-preview and pass the new deletionHash.' };
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
const { status, data } = await api.del(`/api/v1/projects/${pid}`);
|
|
1461
|
-
return { deleted: true, project: preview.project, removed: inv, resourceCount: total, meteringSnapshot: preview.meteringSnapshot, result: result(status, data) };
|
|
1462
1573
|
},
|
|
1463
1574
|
},
|
|
1464
1575
|
];
|