@biznetgio/neo-app 0.4.28 → 0.4.30
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/prompt.md +31 -2
- package/src/mcp/tools.js +170 -9
package/package.json
CHANGED
package/prompt.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
NEO App setup prompt — served at https://portal.neoapp.biznetgio.dev/prompt.md (staging)
|
|
3
3
|
and https://portal.app.neoapp.id/prompt.md (production).
|
|
4
4
|
Contains NO credentials or user-specific secrets. Versioned; keep last-updated current.
|
|
5
|
-
Keep this in sync with
|
|
5
|
+
Keep this in sync with cli/prompt.md (the npm package's bundled copy).
|
|
6
6
|
-->
|
|
7
7
|
# Set this repo up on NEO App
|
|
8
8
|
|
|
9
|
-
**Version:** 0.4.
|
|
9
|
+
**Version:** 0.4.4 · **Last updated:** 2026-10-15
|
|
10
10
|
|
|
11
11
|
These are the official setup instructions from NEO App (Biznet Gio's managed-app
|
|
12
12
|
platform). If you are an AI coding agent, complete the steps below yourself by running
|
|
@@ -109,6 +109,35 @@ tool. Report the connected account, organization, environment, detected agent
|
|
|
109
109
|
integrations, and any unresolved errors. **Do not deploy unless the human separately
|
|
110
110
|
asked for a deployment.**
|
|
111
111
|
|
|
112
|
+
## Deploying a resource (the MCP lifecycle)
|
|
113
|
+
|
|
114
|
+
Once setup is verified and the human has asked for a deployment, use the `neo_*` MCP
|
|
115
|
+
tools — never the portal UI. The lifecycle is the same for every resource type:
|
|
116
|
+
|
|
117
|
+
1. **Understand the repo** (for code deploys): `neo_scan_repo` enumerates deployable
|
|
118
|
+
components in a monorepo. Ask the human which to deploy; never collapse a monorepo
|
|
119
|
+
into one service.
|
|
120
|
+
2. **Plan and show the cost**: `neo_plan_stack` returns an immutable, priced plan
|
|
121
|
+
(`planId`, `planHash`, cost in IDR, expiry). Show the human the cost and get an
|
|
122
|
+
explicit **Approve & Create**. Individual creates (`neo_create_database`,
|
|
123
|
+
`neo_create_queue`) instead return the cost and create nothing until you re-call
|
|
124
|
+
with `confirm: true`.
|
|
125
|
+
3. **Apply**: `neo_apply_stack({planId, planHash})`. If the price book changed since the
|
|
126
|
+
plan, apply is refused — re-plan to re-quote. A `pending_approval` result is the
|
|
127
|
+
approval policy working, not an error; wait for it.
|
|
128
|
+
4. **Watch it reach health**: `neo_wait_operation` until terminal — the plan deploys in
|
|
129
|
+
dependency order and gates on health.
|
|
130
|
+
5. **Prove it works**: `neo_verify_database` (SQL round-trip), read logs, hit the URL.
|
|
131
|
+
6. **Clean up** when asked: `neo_delete_*` / `neo_delete_stack`, or `neo_delete_project`
|
|
132
|
+
(confirm-guarded) for the whole project.
|
|
133
|
+
|
|
134
|
+
For a **scheduled job**, call `neo_cron_questionnaire` first to collect the schedule and
|
|
135
|
+
target, then `neo_create_cron`. To expose a managed database or queue to the internet,
|
|
136
|
+
use `neo_enable_db_public_access` / `neo_enable_queue_public_access` (deploy it first).
|
|
137
|
+
|
|
138
|
+
Never deploy to **production** without an explicit request, and never default a missing
|
|
139
|
+
environment — every create/deploy names `staging` or `production`.
|
|
140
|
+
|
|
112
141
|
## Environment
|
|
113
142
|
|
|
114
143
|
The CLI targets production by default. Set `NEO_APP_URL` to point it at a different
|
package/src/mcp/tools.js
CHANGED
|
@@ -39,6 +39,32 @@ function requireEnv(env) {
|
|
|
39
39
|
return e;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// gbFromSize parses "10Gi"/"10G"/"10" → 10 (integer GB), else undefined.
|
|
43
|
+
function gbFromSize(v) {
|
|
44
|
+
if (v == null) return undefined;
|
|
45
|
+
const m = String(v).match(/(\d+)/);
|
|
46
|
+
return m ? parseInt(m[1], 10) : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// confirmCost is the Approve & Create gate for billable one-shot creates: unless
|
|
50
|
+
// args.confirm===true it returns the live-priced cost and creates NOTHING — the
|
|
51
|
+
// agent shows the user, then re-calls with confirm:true. plan/apply remains the
|
|
52
|
+
// server-immutable costed path; this is the lighter client-side gate (handoff 3.3).
|
|
53
|
+
async function confirmCost(api, args, spec) {
|
|
54
|
+
if (args.confirm === true) return null;
|
|
55
|
+
let orgId = null;
|
|
56
|
+
try { orgId = await api.resolveOrgId(args.orgId); } catch { orgId = null; }
|
|
57
|
+
let book = null;
|
|
58
|
+
try { book = (await api.get(orgId ? `/api/v1/orgs/${orgId}/pricing` : '/api/v1/pricing')).data; }
|
|
59
|
+
catch { try { book = (await api.get('/api/v1/pricing')).data; } catch { book = null; } }
|
|
60
|
+
return {
|
|
61
|
+
requiresConfirmation: true,
|
|
62
|
+
cost: book ? estimateCost(book, spec) : null,
|
|
63
|
+
priceBook: orgId ? `org:${orgId}` : 'global-defaults',
|
|
64
|
+
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.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey)/i;
|
|
43
69
|
function redactSecrets(value) {
|
|
44
70
|
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
@@ -94,10 +120,10 @@ export const TOOLS = [
|
|
|
94
120
|
{ type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
|
|
95
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' },
|
|
96
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)' },
|
|
97
|
-
{ type: 'database', create: true, get: true, list: true, update: true, backup: true, verify: true, delete: true, note: 'neo_create/list/get/update(resize)/delete_database + backup + verify (SQL round-trip probe); restore still TODO (handoff 4.4)' },
|
|
98
|
-
{ type: 'queue', create: true, get: true, list: true, update: true, delete: true, note: 'neo_create/list/get/update/
|
|
99
|
-
{ type: 'cron', create: true, get: true, list: true, run: true, runs: true, delete: true, note: '
|
|
100
|
-
{ type: 'object_storage', create: true, get: true, list: true, objects: true, upload: true, delete: true, note: 'neo_create/list/get/delete_bucket + list/upload/presign(get)/delete objects (
|
|
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)' },
|
|
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
|
+
{ 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)' },
|
|
101
127
|
];
|
|
102
128
|
|
|
103
129
|
return {
|
|
@@ -105,7 +131,7 @@ export const TOOLS = [
|
|
|
105
131
|
controlPlane,
|
|
106
132
|
resourceTypes,
|
|
107
133
|
// Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
|
|
108
|
-
lifecycleVerbs: ['plan', 'apply', 'get_operation', 'wait_operation', 'delete_stack', 'reconcile_usage', 'service_usage', 'create', 'list', 'get', 'update', 'verify', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'estimate', 'scan_repo', 'bind', 'unbind', 'run_cron', 'presign_object', 'backup', 'delete', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
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'],
|
|
109
135
|
bindings: { supported: true, targets: ['neo_db', 'neo_queue'], writeOnly: true, note: 'neo_bind_resource injects DATABASE_URL/NATS_URL server-side at deploy; the secret is never returned (handoff 5)' },
|
|
110
136
|
sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
|
|
111
137
|
environments: ['staging', 'production'],
|
|
@@ -496,12 +522,15 @@ export const TOOLS = [
|
|
|
496
522
|
volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
|
|
497
523
|
replicas: int('Read/failover replicas for HA (optional; postgres/CNPG only)'),
|
|
498
524
|
deploymentMode: { type: 'string', description: 'single | ha (optional)' },
|
|
525
|
+
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.' },
|
|
499
526
|
},
|
|
500
527
|
required: ['projectId', 'name', 'engine', 'environment'],
|
|
501
528
|
},
|
|
502
529
|
async call(api, args) {
|
|
503
530
|
requireEnv(args.environment);
|
|
504
|
-
const
|
|
531
|
+
const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize), replicas: args.replicas });
|
|
532
|
+
if (gate) return gate;
|
|
533
|
+
const { projectId, confirm, orgId, ...body } = args;
|
|
505
534
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/databases`, body);
|
|
506
535
|
return redactSecrets(result(status, data));
|
|
507
536
|
},
|
|
@@ -555,6 +584,26 @@ export const TOOLS = [
|
|
|
555
584
|
return result(status, data);
|
|
556
585
|
},
|
|
557
586
|
},
|
|
587
|
+
{
|
|
588
|
+
name: 'neo_verify_queue',
|
|
589
|
+
description: 'Functionally verify a managed queue (handoff §8): runs an in-cluster probe that connects to NATS JetStream, publishes a message to a temporary stream, and confirms it was persisted — a real publish→persist round-trip, returning {verified, log} WITHOUT handing you any credentials. The queue must be deployed. Synchronous (~90s).',
|
|
590
|
+
mutating: true,
|
|
591
|
+
schema: { type: 'object', properties: { queueId: str('Queue id (deployed)') }, required: ['queueId'] },
|
|
592
|
+
async call(api, args) {
|
|
593
|
+
const { status, data } = await api.post(`/api/v1/queues/${args.queueId}/verify`);
|
|
594
|
+
return result(status, data);
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: 'neo_verify_bucket',
|
|
599
|
+
description: 'Functionally verify an object-storage bucket (handoff §8/§9): does a put → list → delete round-trip of a tiny temporary object through the real OBS backend, proving the bucket credentials and endpoint work, returning {verified, put, listed, deleted}. The probe object is always cleaned up. No credentials returned.',
|
|
600
|
+
mutating: true,
|
|
601
|
+
schema: { type: 'object', properties: { bucketId: str('Bucket id') }, required: ['bucketId'] },
|
|
602
|
+
async call(api, args) {
|
|
603
|
+
const { status, data } = await api.post(`/api/v1/buckets/${args.bucketId}/verify`);
|
|
604
|
+
return result(status, data);
|
|
605
|
+
},
|
|
606
|
+
},
|
|
558
607
|
{
|
|
559
608
|
name: 'neo_list_backups',
|
|
560
609
|
description: 'List configured database backups in a project (handoff 4.4): schedule, destination, last run status.',
|
|
@@ -606,13 +655,16 @@ export const TOOLS = [
|
|
|
606
655
|
planClass: str('Plan/engine class (optional)'),
|
|
607
656
|
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
608
657
|
volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
|
|
609
|
-
dlqEnabled: { type: 'boolean', description: 'Enable a dead-letter queue (optional)' },
|
|
658
|
+
dlqEnabled: { type: 'boolean', description: 'Enable a dead-letter queue (optional, default true). Pass false to disable — this is now honored.' },
|
|
659
|
+
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.' },
|
|
610
660
|
},
|
|
611
661
|
required: ['projectId', 'name', 'environment'],
|
|
612
662
|
},
|
|
613
663
|
async call(api, args) {
|
|
614
664
|
requireEnv(args.environment);
|
|
615
|
-
const
|
|
665
|
+
const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize) });
|
|
666
|
+
if (gate) return gate;
|
|
667
|
+
const { projectId, confirm, orgId, ...body } = args;
|
|
616
668
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/queues`, body);
|
|
617
669
|
return redactSecrets(result(status, data));
|
|
618
670
|
},
|
|
@@ -656,9 +708,43 @@ export const TOOLS = [
|
|
|
656
708
|
return redactSecrets(result(status, data));
|
|
657
709
|
},
|
|
658
710
|
},
|
|
711
|
+
{
|
|
712
|
+
name: 'neo_cron_questionnaire',
|
|
713
|
+
description: 'Return the ordered questions (with allowed values and which neo_create_cron field each answer fills) needed to define a scheduled job. Read-only — call this FIRST when a user wants a cron but has not given a full schedule/target, ask them these questions, then call neo_create_cron with the collected answers (handoff 4.6).',
|
|
714
|
+
mutating: false,
|
|
715
|
+
schema: { type: 'object', properties: {} },
|
|
716
|
+
async call() {
|
|
717
|
+
return {
|
|
718
|
+
questions: [
|
|
719
|
+
{ field: 'name', ask: 'What should this job be called?', type: 'string' },
|
|
720
|
+
{ field: 'mode', ask: 'What does the job run?', type: 'enum', options: [
|
|
721
|
+
{ value: 'app_command', when: 'run a command inside an existing service, or a one-off image', fills: ['serviceId OR image', 'command'] },
|
|
722
|
+
{ value: 'http_endpoint', when: 'call a URL on a schedule (webhook/ping)', fills: ['url', 'method'] },
|
|
723
|
+
] },
|
|
724
|
+
{ field: 'scheduleType', ask: 'How often should it run?', type: 'enum', options: [
|
|
725
|
+
{ value: 'cron', fills: ['schedule'], hint: 'cron expression, e.g. "0 * * * *" (hourly)' },
|
|
726
|
+
{ value: 'interval', fills: ['intervalMinutes'], hint: 'every N minutes' },
|
|
727
|
+
{ value: 'seconds', fills: ['intervalMinutes'], hint: 'sub-minute/seconds cadence' },
|
|
728
|
+
{ value: 'once', fills: [], hint: 'run a single time' },
|
|
729
|
+
] },
|
|
730
|
+
{ field: 'timezone', ask: 'Which timezone for the schedule? (optional, IANA, e.g. Asia/Jakarta)', type: 'string', optional: true },
|
|
731
|
+
{ field: 'environment', ask: 'staging or production? (never defaulted)', type: 'enum', options: ['staging', 'production'], required: true },
|
|
732
|
+
],
|
|
733
|
+
branches: {
|
|
734
|
+
app_command: 'Provide serviceId (an existing service) OR image, plus command (argv array).',
|
|
735
|
+
http_endpoint: 'Provide url and method (GET/POST/…).',
|
|
736
|
+
},
|
|
737
|
+
examples: [
|
|
738
|
+
{ intent: 'Run `node cleanup.js` in my web service every night at 2am Jakarta', args: { name: 'nightly-cleanup', mode: 'app_command', scheduleType: 'cron', schedule: '0 2 * * *', timezone: 'Asia/Jakarta', serviceId: '<web service id>', command: ['node', 'cleanup.js'], environment: 'staging' } },
|
|
739
|
+
{ intent: 'Ping a health URL every 15 minutes', args: { name: 'healthcheck', mode: 'http_endpoint', scheduleType: 'interval', intervalMinutes: 15, url: 'https://example.app/health', method: 'GET', environment: 'production' } },
|
|
740
|
+
],
|
|
741
|
+
next: 'Collect the answers, then call neo_create_cron. It may return pending_approval (expected).',
|
|
742
|
+
};
|
|
743
|
+
},
|
|
744
|
+
},
|
|
659
745
|
{
|
|
660
746
|
name: 'neo_create_cron',
|
|
661
|
-
description: 'Create a scheduled job (cron) in a project (handoff 4.6). Target is either an existing service command (mode=app_command, serviceId+command) or an HTTP endpoint (mode=http_endpoint, url+method). Schedule via scheduleType (cron|interval|once|seconds) + schedule/intervalMinutes. May return pending_approval.',
|
|
747
|
+
description: 'Create a scheduled job (cron) in a project (handoff 4.6). If the user gave an incomplete spec, call neo_cron_questionnaire first. Target is either an existing service command (mode=app_command, serviceId+command) or an HTTP endpoint (mode=http_endpoint, url+method). Schedule via scheduleType (cron|interval|once|seconds) + schedule/intervalMinutes. May return pending_approval.',
|
|
662
748
|
mutating: true,
|
|
663
749
|
schema: {
|
|
664
750
|
type: 'object',
|
|
@@ -1085,4 +1171,79 @@ export const TOOLS = [
|
|
|
1085
1171
|
return result(status, data);
|
|
1086
1172
|
},
|
|
1087
1173
|
},
|
|
1174
|
+
{
|
|
1175
|
+
name: 'neo_enable_db_public_access',
|
|
1176
|
+
description: 'Enable internet access to a managed postgres database through the TLS gateway (phase 3). Provisions a least-privilege external role, assigns a public host, and flips publicEnabled. The DB must be DEPLOYED first. Optionally restrict to allowlistCidrs. Credentials are write-only — retrieve the full external connection string from the dashboard; this returns the public host/port, external username, and CA URL.',
|
|
1177
|
+
mutating: true,
|
|
1178
|
+
schema: {
|
|
1179
|
+
type: 'object',
|
|
1180
|
+
properties: {
|
|
1181
|
+
databaseId: str('Database id (postgres, already deployed)'),
|
|
1182
|
+
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR allowlist, e.g. ["203.0.113.0/24"] (optional; empty = allow any source IP)' },
|
|
1183
|
+
},
|
|
1184
|
+
required: ['databaseId'],
|
|
1185
|
+
},
|
|
1186
|
+
async call(api, args) {
|
|
1187
|
+
const { status, data } = await api.post(`/api/v1/databases/${args.databaseId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
1188
|
+
return redactSecrets(result(status, data));
|
|
1189
|
+
},
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
name: 'neo_disable_db_public_access',
|
|
1193
|
+
description: 'Disable internet access to a managed database. Non-destructive: the external role is set NOLOGIN and its objects are preserved; routing stops.',
|
|
1194
|
+
mutating: true,
|
|
1195
|
+
schema: { type: 'object', properties: { databaseId: str('Database id') }, required: ['databaseId'] },
|
|
1196
|
+
async call(api, args) {
|
|
1197
|
+
const { status, data } = await api.del(`/api/v1/databases/${args.databaseId}/public-access`);
|
|
1198
|
+
return result(status, data);
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
name: 'neo_enable_queue_public_access',
|
|
1203
|
+
description: 'Enable internet access to a managed queue through the TLS gateway (phase 3). Assigns a public host and flips publicEnabled. The queue must be DEPLOYED first. Optionally restrict to allowlistCidrs. Credentials are write-only — retrieve the full connection string from the dashboard.',
|
|
1204
|
+
mutating: true,
|
|
1205
|
+
schema: {
|
|
1206
|
+
type: 'object',
|
|
1207
|
+
properties: {
|
|
1208
|
+
queueId: str('Queue id (already deployed)'),
|
|
1209
|
+
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR allowlist (optional; empty = allow any source IP)' },
|
|
1210
|
+
},
|
|
1211
|
+
required: ['queueId'],
|
|
1212
|
+
},
|
|
1213
|
+
async call(api, args) {
|
|
1214
|
+
const { status, data } = await api.post(`/api/v1/queues/${args.queueId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
1215
|
+
return redactSecrets(result(status, data));
|
|
1216
|
+
},
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
name: 'neo_disable_queue_public_access',
|
|
1220
|
+
description: 'Disable internet access to a managed queue. Non-destructive: routing stops; the queue keeps running for in-cluster consumers.',
|
|
1221
|
+
mutating: true,
|
|
1222
|
+
schema: { type: 'object', properties: { queueId: str('Queue id') }, required: ['queueId'] },
|
|
1223
|
+
async call(api, args) {
|
|
1224
|
+
const { status, data } = await api.del(`/api/v1/queues/${args.queueId}/public-access`);
|
|
1225
|
+
return result(status, data);
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
name: 'neo_delete_project',
|
|
1230
|
+
description: 'Permanently delete a project and ALL its resources (apps, databases, queues, crons, buckets, and the project namespace with its PVCs/CNPG clusters). Destructive and irreversible — nothing is recoverable. Requires confirm:true. Only the project creator or an org owner/admin may delete it.',
|
|
1231
|
+
mutating: true,
|
|
1232
|
+
destructive: true,
|
|
1233
|
+
schema: {
|
|
1234
|
+
type: 'object',
|
|
1235
|
+
properties: {
|
|
1236
|
+
projectId: str('Project id'),
|
|
1237
|
+
confirm: { type: 'boolean', description: 'Must be true to proceed — guards against accidental deletion of an entire project.' },
|
|
1238
|
+
},
|
|
1239
|
+
required: ['projectId', 'confirm'],
|
|
1240
|
+
},
|
|
1241
|
+
async call(api, args) {
|
|
1242
|
+
if (args.confirm !== true) {
|
|
1243
|
+
return { error: 'refused', message: 'Deleting a project removes ALL its resources and is irreversible. Re-call with confirm:true to proceed.' };
|
|
1244
|
+
}
|
|
1245
|
+
const { status, data } = await api.del(`/api/v1/projects/${args.projectId}`);
|
|
1246
|
+
return result(status, data);
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1088
1249
|
];
|