@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.30",
3
+ "version": "0.4.31",
4
4
  "description": "NEO App CLI — one-command agent onboarding, MCP server, and project ops for coding agents (Codex, Claude Code).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- log(`[dry-run] estimated cost (default micro compute, ${svcType}): ~${estimatedCost.monthly} ${estimatedCost.currency}/mo (${estimatedCost.hourly}/h) — ${estimatedCost.assumptions}. Use neo_estimate for a specific size.`);
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 injects DATABASE_URL/NATS_URL server-side at deploy; the secret is never returned (handoff 5)' },
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
- path: s.path || '.',
391
- name: s.name,
392
- stack: s.type, // docker | node | go | python | java | php | ruby | rust | static
393
- suggestedServiceType: s.type === 'static' ? 'static_site' : 'web',
394
- suggestedBuildType: s.type === 'docker' ? 'dockerfile' : 'railpack',
395
- contextDir: s.path || undefined,
396
- detectedDependencies: s.deps || [], // database | queue | object_storage, inferred from the manifest
397
- // Confidence + evidence (handoff 6): an explicit Dockerfile/index.html is high
398
- // confidence; a language marker is inferred (medium). The stack marker + any
399
- // detectedDependencies ARE the evidence.
400
- typeConfidence: (s.type === 'docker' || s.type === 'static') ? 'high' : 'medium',
401
- evidence: { marker: s.type, manifestDeps: s.deps || [] },
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
- (components.length > 1
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). Worker vs web cannot be inferred from the tree alone; confirm the type with the user.'
411
- : components.length === 1
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 (no Dockerfile/package.json/go.mod/requirements.txt/index.html/… at any root).')
414
- + (anyDeps ? ' A component lists detectedDependencies (database/queue/object_storage) from its manifest — consider planning those resources and binding them (neo_bind_resource); confirm with the user first.' : ''),
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 { projectId, ...body } = args;
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 { projectId, ...body } = args;
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 an env var at deploy, server-side).',
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 allowlist, e.g. ["203.0.113.0/24"] (optional; empty = allow any source IP)' },
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 allowlist (optional; empty = allow any source IP)' },
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: '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.',
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 proceed — guards against accidental deletion of an entire project.' },
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', 'confirm'],
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 { error: 'refused', message: 'Deleting a project removes ALL its resources and is irreversible. Re-call with confirm:true to proceed.' };
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
- const { status, data } = await api.del(`/api/v1/projects/${args.projectId}`);
1246
- return result(status, data);
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
  ];