@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 +1 -1
- package/prompt.md +6 -0
- package/src/commands/db.js +93 -0
- package/src/index.js +6 -3
- package/src/mcp/tools.js +13 -9
package/package.json
CHANGED
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
|
-
--
|
|
42
|
-
--
|
|
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('
|
|
488
|
-
cpuLimit: str('
|
|
489
|
-
memoryLimit: str('
|
|
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
|
-
|
|
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
|
|
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('
|
|
979
|
-
vcpu: { type: 'number', description: '
|
|
980
|
-
memoryMB: int('
|
|
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
|
|
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'),
|