@biznetgio/neo-app 0.4.74 → 0.4.75

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.74",
3
+ "version": "0.4.75",
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": {
package/prompt.md CHANGED
@@ -123,6 +123,12 @@ tools — never the portal UI. The lifecycle is the same for every resource type
123
123
  explicit **Approve & Create**. Individual creates (`neo_create_database`,
124
124
  `neo_create_queue`) instead return the cost and create nothing until you re-call
125
125
  with `confirm: true`.
126
+ **Pricing is resource-based and hourly (#144).** Compute is priced per **vCPU-hour**
127
+ and per **GB-RAM-hour** independently — there are no packages/tiers. Size a service
128
+ with `vcpu` + `memoryMB` (or `cpuLimit`/`memoryLimit`); a legacy tier key still works
129
+ but resolves to its concrete vCPU/RAM. Storage and object storage bill per GB-hour.
130
+ Time-based components accrue hourly and are **capped at 730 billable hours/month**; a
131
+ monthly figure is a non-authoritative estimate (hourly × up to 730).
126
132
  3. **Apply**: `neo_apply_stack({planId, planHash})`. If the price book changed since the
127
133
  plan, apply is refused — re-plan to re-quote. A `pending_approval` result is the
128
134
  approval policy working, not an error; wait for it.
@@ -0,0 +1,93 @@
1
+ // `neo-app db` — terminal discoverability for the managed-database lifecycle (#149 retest).
2
+ // Read-only inspection (list/get) runs here directly; billable/mutating operations
3
+ // (create, verify, query, backup, restore, redis-command, delete) stay on the MCP
4
+ // server's audited cost-consent path and are listed here so a terminal-only user can
5
+ // discover them from the CLI command tree instead of only through an agent.
6
+
7
+ import { resolveConfig } from '../config.js';
8
+ import { getCredential } from '../creds.js';
9
+ import { NeoApi } from '../api.js';
10
+ import { readLink } from '../link.js';
11
+ import { log, ok, fail, c } from '../util.js';
12
+
13
+ // The full lifecycle, with the MCP tool that performs each one. Shown by `neo-app db`.
14
+ const OPS = [
15
+ ['list', 'neo-app db list', 'List databases in the linked project (read-only)'],
16
+ ['get <id>', 'neo-app db get <id>', 'Show one database, incl. redacted connection string (read-only)'],
17
+ ['create', 'neo_create_database', 'Provision PostgreSQL / MySQL / Redis (billable — cost consent)'],
18
+ ['deploy', 'neo_deploy_database', 'Start/roll out the database pods'],
19
+ ['verify', 'neo_verify_database', 'Round-trip health probe (engine-aware)'],
20
+ ['query', 'neo_db_query', 'Read-only SQL (postgres/mysql)'],
21
+ ['redis', 'neo_redis_command', 'Run a safe Redis command (denylist-guarded)'],
22
+ ['metrics', 'neo_get_database_metrics', 'Engine-aware metrics snapshot'],
23
+ ['backup', 'neo_run_backup / neo_create_backup', 'Create and run a logical backup'],
24
+ ['restore', 'neo_restore_database', 'Restore a SQL dump (postgres/mysql)'],
25
+ ['public', 'neo_enable_db_public_access','Enable/disable TLS public access'],
26
+ ['delete', 'neo_delete_database', 'Delete a database (billable stop)'],
27
+ ];
28
+
29
+ function usage() {
30
+ log(`${c.bold('neo-app db')} — managed database lifecycle (PostgreSQL · MySQL · Redis)\n`);
31
+ log(c.bold('Run directly from the CLI (read-only):'));
32
+ log(' neo-app db list');
33
+ log(' neo-app db get <database-id>\n');
34
+ log(c.bold('Mutating / billable operations run through the MCP server'));
35
+ log(`(start it with ${c.bold('neo-app mcp')} — your agent calls these; the Portal exposes them too):`);
36
+ for (const [, how, desc] of OPS.slice(2)) log(` · ${c.bold(how.padEnd(34))} ${c.dim(desc)}`);
37
+ log(`\nPrice a database before creating it: ${c.bold('neo-app estimate --storage <GB>')}`);
38
+ }
39
+
40
+ function connect(opts) {
41
+ const cfg = resolveConfig(opts);
42
+ const cred = getCredential(cfg.baseUrl);
43
+ if (!cred?.token) { fail(`not authenticated for ${cfg.baseUrl} — run 'neo-app login'`); process.exit(1); }
44
+ return new NeoApi({ baseUrl: cfg.baseUrl, token: cred.token });
45
+ }
46
+
47
+ function requireProject(opts) {
48
+ const projectId = opts.project || readLink()?.projectId;
49
+ if (!projectId) { fail("no linked project — run 'neo-app link <project-id>' or pass --project <id>"); process.exit(1); }
50
+ return projectId;
51
+ }
52
+
53
+ export async function db(sub, opts = {}, positionals = []) {
54
+ if (!sub || sub === 'help') return void usage();
55
+
56
+ if (sub === 'list' || sub === 'ls') {
57
+ const api = connect(opts);
58
+ const projectId = requireProject(opts);
59
+ let dbs;
60
+ try { dbs = (await api.get(`/api/v1/projects/${projectId}/databases`)).data?.databases || []; }
61
+ catch (e) { fail(`list failed: ${e.message || e}`); process.exit(1); }
62
+ if (opts.json) return void process.stdout.write(JSON.stringify({ databases: dbs }, null, 2) + '\n');
63
+ if (!dbs.length) return void ok('no databases in this project');
64
+ for (const d of dbs) {
65
+ const status = d.status || (d.host ? 'deployed' : 'not_deployed');
66
+ log(` ${c.bold(d.name)} ${c.dim(`${d.engine} ${d.version || ''}`.trim())} ${status} ${c.dim(d.environment || 'production')} ${c.dim(d.id)}`);
67
+ }
68
+ return;
69
+ }
70
+
71
+ if (sub === 'get' || sub === 'show') {
72
+ const id = positionals[0];
73
+ if (!id) { fail('usage: neo-app db get <database-id>'); process.exit(2); }
74
+ const api = connect(opts);
75
+ let view;
76
+ try { view = (await api.get(`/api/v1/databases/${id}`)).data; }
77
+ catch (e) { fail(`get failed: ${e.message || e}`); process.exit(1); }
78
+ if (opts.json) return void process.stdout.write(JSON.stringify(view, null, 2) + '\n');
79
+ for (const [k, v] of Object.entries(view)) if (v !== undefined && v !== null && v !== '') log(` ${c.dim(k.padEnd(18))} ${v}`);
80
+ return;
81
+ }
82
+
83
+ // Any lifecycle verb typed at the CLI: point the user at the discoverable path.
84
+ const known = OPS.find(([key]) => key.split(' ')[0] === sub);
85
+ if (known) {
86
+ log(`${c.bold(sub)} runs through the MCP server (${c.bold(known[1])}) or the Portal — not as a direct CLI write.`);
87
+ log(`Start the MCP server with ${c.bold('neo-app mcp')} so your agent can call it, or use the Portal.`);
88
+ return;
89
+ }
90
+ fail(`unknown db subcommand: ${sub}`);
91
+ usage();
92
+ process.exit(2);
93
+ }
package/src/index.js CHANGED
@@ -20,6 +20,8 @@ ${c.bold('Commands')}
20
20
  (or pass the same via --source <path|.zip|git-url>)
21
21
  project create [name] Create a project (defaults to the dir name) and link it
22
22
  project list List projects in your organization (find ids to link)
23
+ db [list|get|...] Managed database lifecycle (PostgreSQL/MySQL/Redis);
24
+ list/get run here, the rest via the MCP server
23
25
  status | whoami Show connected account, org, environment, linked project
24
26
  mcp Run the MCP server: local stdio (default), or hosted
25
27
  Streamable HTTP + SSE with --http [--port <n>] [--host <h>]
@@ -38,8 +40,8 @@ ${c.bold('Options')}
38
40
  --yes Non-interactive (headless/CI); also approves a billable deploy without the cost prompt
39
41
  --dry-run Print intended changes without writing
40
42
  --json Machine-readable output incl. the normalized cost (deploy, estimate)
41
- --tier <key> estimate: compute tier (micro|small|medium|large|xlarge)
42
- --vcpu <n> --mem <MB> estimate: custom compute (instead of --tier)
43
+ --vcpu <n> --mem <MB> estimate: compute, priced per vCPU-hour + GB-RAM-hour (#144)
44
+ --tier <key> estimate: legacy size shorthand (micro|…|xlarge) → vCPU/RAM
43
45
  --storage <GB> estimate: database/persistent storage
44
46
  --object <GB> estimate: object-storage quota (billed monthly-upfront)
45
47
  --queue / --cron estimate: a queue (per-message) / cron (per-run) resource
@@ -106,7 +108,7 @@ export async function main(argv = process.argv.slice(2)) {
106
108
  // Root help, and `--help` on a KNOWN command, print usage. An UNKNOWN command must
107
109
  // still error (nonzero exit) even with `--help`, so a bad command isn't silently
108
110
  // accepted (JNG #97). Per-subcommand help is tracked in #89.
109
- const KNOWN_COMMANDS = new Set(['agent', 'setup', 'config', 'login', 'deploy', 'estimate', 'link', 'project', 'status', 'whoami', 'mcp', 'version', 'help']);
111
+ const KNOWN_COMMANDS = new Set(['agent', 'setup', 'config', 'login', 'deploy', 'estimate', 'link', 'project', 'status', 'whoami', 'mcp', 'db', 'version', 'help']);
110
112
  if (positionals.length === 0 || (opts.help && KNOWN_COMMANDS.has(cmd))) return void console.log(HELP);
111
113
 
112
114
  try {
@@ -123,6 +125,7 @@ export async function main(argv = process.argv.slice(2)) {
123
125
  if (cmd === 'project' && sub === 'create') return void (await (await import('./commands/link.js')).createProject(positionals[2], opts));
124
126
  if (cmd === 'project' && (sub === 'list' || sub === 'ls')) return void (await (await import('./commands/link.js')).listProjects(opts));
125
127
  if (cmd === 'status' || cmd === 'whoami') return void (await (await import('./commands/status.js')).status(opts));
128
+ if (cmd === 'db') return void (await (await import('./commands/db.js')).db(sub, opts, positionals.slice(2)));
126
129
  if (cmd === 'mcp') {
127
130
  const mcp = await import('./mcp/server.js');
128
131
  // Hosted mode: Streamable HTTP + SSE over one listener (auth per request). Default: stdio.
package/src/mcp/tools.js CHANGED
@@ -484,9 +484,9 @@ export const TOOLS = [
484
484
  type: 'object',
485
485
  properties: {
486
486
  appId: str('Service/app id'),
487
- computeTier: str('Bundled compute tier (micro | small | medium | large | xlarge)'),
488
- cpuLimit: str('Custom CPU limit, e.g. "500m" (use with memoryLimit; unbundled)'),
489
- memoryLimit: str('Custom memory limit, e.g. "512Mi" (unbundled)'),
487
+ computeTier: str('Legacy size shorthand (micro|small|medium|large|xlarge) — maps to its concrete vCPU/RAM. Prefer cpuLimit+memoryLimit. Compute is resource-based (#144): priced per vCPU-hour + GB-RAM-hour, no packages.'),
488
+ cpuLimit: str('CPU, e.g. "500m" or "1" (use with memoryLimit) — resource-based billing'),
489
+ memoryLimit: str('Memory, e.g. "512Mi" or "1Gi" (use with cpuLimit) — resource-based billing'),
490
490
  replicas: int('Fixed replica count'),
491
491
  autoscaleMin: int('Autoscale min replicas'),
492
492
  autoscaleMax: int('Autoscale max replicas'),
@@ -514,7 +514,9 @@ export const TOOLS = [
514
514
  const { appId, confirm, orgId, computeTier, allowCrossEnvironment, ...rest } = args;
515
515
  await assertResourceEnv(api, `/api/v1/apps/${appId}`, allowCrossEnvironment); // JNG #136 §4
516
516
  const body = { ...rest };
517
- if (computeTier) { body.billingMode = 'bundled'; body.cpuLimit = computeTier; }
517
+ // #144: compute is resource-based. A legacy tier shorthand is passed through as
518
+ // cpuLimit; the control plane maps it to concrete vCPU/RAM and bills unbundled.
519
+ if (computeTier) { body.cpuLimit = computeTier; }
518
520
  else if (args.cpuLimit || args.memoryLimit) { body.billingMode = 'unbundled'; }
519
521
  const costChanging = computeTier || args.cpuLimit || args.memoryLimit || args.replicas !== undefined || args.autoscaleMax !== undefined || args.autoscaleMin !== undefined || args.volumeSize !== undefined || (Array.isArray(args.mounts) && args.mounts.length > 0);
520
522
  if (costChanging && confirm !== true) {
@@ -970,14 +972,14 @@ export const TOOLS = [
970
972
  {
971
973
  name: 'neo_estimate',
972
974
  description:
973
- 'Estimate the hourly and monthly cost (IDR) of a service or resource from the LIVE price book, so you can show the user the cost BEFORE a billable create/deploy (handoff 3.3/3.4). Read-only. Covers compute (a tier, or custom vcpu/memoryMB), persistent storage, and object storage; variable usage (queue messages, cron runs) is excluded and noted. Pass orgId to apply that org\'s price overrides.',
975
+ 'Estimate the hourly and monthly cost (IDR) of a service or resource from the LIVE price book, so you can show the user the cost BEFORE a billable create/deploy (handoff 3.3/3.4). Read-only. Compute is resource-based (#144): priced per vCPU-hour + GB-RAM-hour independently (no packages), hourly, capped at 730 billable hours/month. Covers compute (vcpu+memoryMB, or a legacy tier shorthand that resolves to concrete vCPU/RAM), persistent storage, and object storage (all per GB-hour); variable usage (queue messages, cron runs) is excluded and noted. The monthly figure is a non-authoritative estimate (hourly × up to 730). Pass orgId to apply that org\'s price overrides.',
974
976
  mutating: false,
975
977
  schema: {
976
978
  type: 'object',
977
979
  properties: {
978
- computeTier: str('Compute tier key: micro | small | medium | large | xlarge (bundled compute)'),
979
- vcpu: { type: 'number', description: 'Custom vCPU count (use instead of computeTier)' },
980
- memoryMB: int('Custom memory in MB (use with vcpu)'),
980
+ computeTier: str('Legacy size shorthand (micro|small|medium|large|xlarge) — resolves to concrete vCPU/RAM. Prefer vcpu+memoryMB. Compute is resource-based (#144), not packaged.'),
981
+ vcpu: { type: 'number', description: 'vCPU count — priced per vCPU-hour (preferred over computeTier)' },
982
+ memoryMB: int('Memory in MB — priced per GB-RAM-hour (use with vcpu)'),
981
983
  storageGB: int('Persistent/database storage in GB (optional)'),
982
984
  objectStorageGB: int('Object-storage quota in GB (optional)'),
983
985
  replicas: int('Replica count (default 1)'),
@@ -1779,7 +1781,9 @@ export const TOOLS = [
1779
1781
  branch: str('app: git branch'),
1780
1782
  deps: { type: 'array', items: { type: 'string', enum: ['database', 'queue', 'object_storage'] }, description: 'app: managed-resource dependencies this service needs (from neo_scan_repo `deps`). The plan flags any dep not satisfied by a binding as incompleteTopology and apply refuses it unless acknowledged — so a service needing NATS_URL/DATABASE_URL isn\'t deployed into a 503 (JNG #112 §7).' },
1781
1783
  port: int('app: container port'),
1782
- computeTier: str('app/db/queue: micro | small | medium | large | xlarge'),
1784
+ computeTier: str('app: legacy size shorthand (micro|small|medium|large|xlarge) → concrete vCPU/RAM; prefer cpuLimit+memoryLimit. Compute is resource-based (#144).'),
1785
+ cpuLimit: str('app: CPU, e.g. "0.5" or "1" (use with memoryLimit) — resource-based'),
1786
+ memoryLimit: str('app: memory, e.g. "512M" or "1Gi" (use with cpuLimit) — resource-based'),
1783
1787
  engine: str('database: postgres | mysql'),
1784
1788
  version: str('database: engine version, e.g. 16'),
1785
1789
  volumeSize: str('database: storage size, e.g. 10Gi'),