@vibingview/cli 0.1.4 → 0.2.0

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +333 -11
  3. package/src/mcp.mjs +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibingview/cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "VibingView command-line interface and stdio MCP bridge: pair with browser approval, deploy apps, read logs, manage env vars and databases, and connect AI editors.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.mjs CHANGED
@@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
10
10
  import { ensureGitignore, inferAppName, isIdenticalDeploy, packageProject, readProjectConfig, writeProjectConfig } from "./lib/project.mjs";
11
11
  import { defaultTransport, installMcpConfig, maskToken, maskedEntry, mcpConfigPath, MCP_CLIENTS, NPM_PACKAGE } from "./lib/mcp-install.mjs";
12
12
 
13
- export const VERSION = "0.1.4";
13
+ export const VERSION = "0.2.0";
14
14
  /** Production control plane used when no --url, VIBINGVIEW_URL or saved login selects another. */
15
15
  export const DEFAULT_URL = "https://vibingview.com";
16
16
  const USAGE = `VibingView CLI ${VERSION}
@@ -47,14 +47,36 @@ One-click apps
47
47
  vibingview app create <slug> [--name <app>] [--workspace <id|name>] [--domain <suffix>] [--yes]
48
48
  vibingview app status [app]
49
49
 
50
+ Servers (actions marked * need your approval in the browser; the link shows exactly what happens and its price)
51
+ vibingview server list | info [server] | plans [server]
52
+ vibingview server reboot|start|clean [server]
53
+ vibingview server shutdown [server]*
54
+ vibingview server upgrade <plan> [--server <s>] [--at "YYYY-MM-DD HH:MM"]* upgrade cancel|now [--server <s>]
55
+ vibingview server renew* | billing <1|6|12>* | auto-renew on|off* | reinstall* | delete* [--server <s>]
56
+
57
+ Apps
58
+ vibingview apps [--workspace <id|name>]
59
+ vibingview app deployments [app] | rollback [app] --to <revision|id> | settings [app]
60
+ vibingview app set [app] [--port n] [--root-dir d] [--install-command c] [--build-command c] [--start-command c] [--output-dir d] [--technique 1-5] [--dockerfile <file>]
61
+ vibingview app delete [app]*
62
+ vibingview deploy --git <repository-url> [--branch main] --name <app> --port <n> [--workspace <s>]
63
+
64
+ Domains
65
+ vibingview domains [app] | add <host> | check <host> | remove <host>* [--app <app>]
66
+ vibingview domains owned | connect <domain> --app <app>* | disconnect <domain>*
67
+ vibingview nameservers [app] | connect <domain> | check <domain> | disconnect <domain>* [--app <app>]
68
+
69
+ Billing
70
+ vibingview balance | payments | topup <euros> [--method card|crypto]
71
+
50
72
  Other
51
- vibingview workspaces | apps
73
+ vibingview workspaces | logs … --follow | logs … --json | db logs <db> | db reveal <db>* | db delete <db>* | env get --reveal*
52
74
 
53
75
  Control plane: --url <origin> on any command, else VIBINGVIEW_URL, else the instance you last logged in to,
54
76
  else ${DEFAULT_URL}. VIBINGVIEW_TOKEN overrides the saved token.`;
55
77
 
56
- const BOOLEAN_FLAGS = new Set(["force", "yes", "reveal", "show", "ai", "build", "runtime", "no-wait", "no-browser", "dry-run", "json", "help", "stdin", "local-bridge"]);
57
- const VALUE_FLAGS = new Set(["workspace", "name", "port", "domain", "app", "limit", "engine", "client", "transport", "timeout", "url", "token", "technique", "builder"]);
78
+ const BOOLEAN_FLAGS = new Set(["force", "yes", "reveal", "show", "ai", "build", "runtime", "no-wait", "no-browser", "dry-run", "json", "help", "stdin", "local-bridge", "follow"]);
79
+ const VALUE_FLAGS = new Set(["workspace", "server", "name", "port", "domain", "app", "limit", "engine", "client", "transport", "timeout", "url", "token", "technique", "builder", "to", "at", "method", "git", "branch", "root-dir", "install-command", "build-command", "start-command", "output-dir", "dockerfile"]);
58
80
 
59
81
  export function parseArgs(argv) {
60
82
  const positional = [];
@@ -131,7 +153,7 @@ function createClient(base, token) {
131
153
  throw new CliError(`Could not read the server's answer to ${route.split("?")[0]}. Nothing was assumed; try again (after a deploy, check vibingview status first).`);
132
154
  if (!response.ok) {
133
155
  let message = typeof body.error === "string" ? body.error : body.error?.message ?? `HTTP ${response.status}`;
134
- if (response.status === 401 && token) message += " — the token may be expired or revoked, or lacks the permission this command needs (database create/reveal need a token with the 'manage' permission). Run vibingview login or use an appropriately scoped VIBINGVIEW_TOKEN.";
156
+ if (response.status === 401 && token) message += " — the token may be expired or revoked, or lacks the permission this command needs (sign in again with vibingview login). Run vibingview login or use an appropriately scoped VIBINGVIEW_TOKEN.";
135
157
  throw Object.assign(new CliError(redact(message)), { status: response.status, code: typeof body.code === "string" ? body.code : undefined });
136
158
  }
137
159
  return body;
@@ -351,6 +373,41 @@ export async function main(argv = process.argv.slice(2)) {
351
373
  return matches[0];
352
374
  }
353
375
 
376
+ async function resolveDatabase(reference) {
377
+ const workspaces = flags.workspace || flags.server || project.workspaceId ? [await resolveWorkspace(flags.workspace ?? flags.server, { running: false })] : await workspacesList();
378
+ const found = [];
379
+ for (const workspace of workspaces) {
380
+ const { databases = [] } = await call(`/api/workspaces/${encodeURIComponent(workspace.id)}/databases`);
381
+ for (const item of databases) if (item.id === reference || item.name === reference) found.push({ ...item, workspaceId: workspace.id, workspace: workspace.name });
382
+ }
383
+ if (!found.length) throw new CliError(`Database ${reference} not found`);
384
+ if (found.length > 1) throw new CliError(`Several databases are named ${reference}; pass --workspace or the database ID.`);
385
+ return found[0];
386
+ }
387
+ const euros = cents => `€${(Number(cents) / 100).toFixed(2)}`;
388
+ /**
389
+ * Server actions go through /api/cli/actions. Everyday ones run at once; paying, deleting and revealing secrets
390
+ * answer with a link the account owner approves in the browser (it shows exactly what happens and the price).
391
+ */
392
+ async function approvedAction(action, params) {
393
+ const answer = await call("/api/cli/actions", json("POST", { action, params }));
394
+ if (answer.status === "done") return answer.result ?? {};
395
+ const price = answer.summary && "chargeCents" in answer.summary ? ` It costs ${euros(answer.summary.chargeCents)}.` : "";
396
+ out(`Approval needed: open this link and click Approve (it shows exactly what will happen).${price}\n ${answer.approveUrl}`);
397
+ if (!flags["no-browser"] && new URL(answer.approveUrl).origin === new URL(base).origin) openBrowser(answer.approveUrl);
398
+ out("Waiting for your approval in the browser (up to 10 minutes)…");
399
+ const deadline = Date.parse(answer.expiresAt) || Date.now() + 600000;
400
+ while (Date.now() < deadline + 5000) {
401
+ await sleep(3000);
402
+ const state = await call(`/api/cli/actions/${encodeURIComponent(answer.id)}`).catch(error => { if (error.status === 404) throw error; return { status: "pending" }; });
403
+ if (state.status === "done") return state.result ?? {};
404
+ if (state.status === "denied") throw new CliError("Denied in the browser. Nothing was changed.");
405
+ if (state.status === "expired") throw new CliError("The approval request expired. Run the command again.");
406
+ if (state.status === "failed") throw new CliError(`It did not work: ${state.error ?? "unknown error"}`);
407
+ }
408
+ throw new CliError("The approval request expired. Run the command again.");
409
+ }
410
+
354
411
  // ----- listing ---------------------------------------------------------------
355
412
  if (command === "workspaces") {
356
413
  const rows = await workspacesList();
@@ -358,12 +415,24 @@ export async function main(argv = process.argv.slice(2)) {
358
415
  return 0;
359
416
  }
360
417
  if (command === "apps") {
361
- const { applications = [] } = await call("/api/apps");
418
+ const filter = flags.workspace ?? flags.server ? await resolveWorkspace(flags.workspace ?? flags.server, { running: false }) : null;
419
+ const { applications: all = [] } = await call("/api/apps");
420
+ const applications = filter ? all.filter(app => app.workspaceId === filter.id) : all;
362
421
  if (flags.json) out(JSON.stringify(applications, null, 2)); else table(applications, [["id", "ID"], ["name", "NAME"], ["status", "STATUS"], ["url", "URL"]]);
363
422
  return 0;
364
423
  }
365
424
 
366
425
  // ----- deploy ----------------------------------------------------------------
426
+ if (command === "deploy" && typeof flags.git === "string") {
427
+ // A new app built from a Git repository (later pushes redeploy it once the repository is linked in the dashboard).
428
+ if (!flags.name) throw new CliError("Usage: vibingview deploy --git <repository-url> --name <app> --port <n> [--branch main] [--workspace <s>]");
429
+ const port = Number(flags.port ?? 3000);
430
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new CliError("--port must be 1-65535");
431
+ const workspace = await resolveWorkspace(flags.workspace ?? flags.server);
432
+ const { application } = await call("/api/apps", json("POST", { workspaceId: workspace.id, name: flags.name, gitUrl: flags.git, branch: flags.branch ?? "main", port }));
433
+ out(`${application.name} (${application.id}) is building from ${flags.git}${application.url ? ` → ${application.url}` : ""}.\nFollow it with vibingview logs ${application.name} --build or vibingview status ${application.name}.`);
434
+ return 0;
435
+ }
367
436
  if (command === "deploy") {
368
437
  if (args.length === 3 && args[2].toLowerCase().endsWith(".zip")) return legacyDeploy(call, args);
369
438
  const directory = path.resolve(args[0] ?? ".");
@@ -480,6 +549,19 @@ export async function main(argv = process.argv.slice(2)) {
480
549
  const kind = flags.build || legacyKind === "build" || args[0] === "build" ? "build" : "runtime";
481
550
  const max = Math.min(1000, Math.max(1, Number(flags.limit ?? 200) || 200));
482
551
  const { logs = [] } = await call(`/api/apps/${encodeURIComponent(app.id)}/logs?kind=${kind}&limit=${max}`);
552
+ if (flags.json) { out(JSON.stringify(logs, null, 2)); return 0; }
553
+ if (flags.follow) {
554
+ // Prints new lines every 3 seconds until Ctrl+C (lines are matched on time and text, so restarts are handled).
555
+ const key = line => `${line.recorded_at ?? ""}|${line.message}`;
556
+ let seen = new Set(logs.map(key));
557
+ for (const line of logs) out(clean(line.message));
558
+ for (;;) {
559
+ await sleep(3000);
560
+ const { logs: next = [] } = await call(`/api/apps/${encodeURIComponent(app.id)}/logs?kind=${kind}&limit=200`).catch(() => ({ logs: [] }));
561
+ for (const line of next) if (!seen.has(key(line))) out(clean(line.message));
562
+ seen = new Set([...seen, ...next.map(key)].slice(-5000));
563
+ }
564
+ }
483
565
  if (flags.ai) {
484
566
  out(`## VibingView ${kind} logs: ${app.name}\n\n- Application: ${app.name} (${app.id})\n- Status: ${app.status}\n- URL: ${app.url ?? "private"}\n- Lines: last ${logs.length}\n\n\`\`\`text`);
485
567
  for (const line of logs) out(`${line.recorded_at ? new Date(line.recorded_at).toISOString() + " " : ""}${line.stream ? `[${line.stream}] ` : ""}${clean(line.message)}`);
@@ -513,10 +595,11 @@ export async function main(argv = process.argv.slice(2)) {
513
595
  if (sub === "get" || sub === "list") {
514
596
  const wanted = args[1];
515
597
  if (flags.reveal) {
516
- warn("Revealing secret values. This is recorded in your account audit log; keep the output private.");
517
- const { variables = [] } = await call(`/api/apps/${encodeURIComponent(app.id)}/settings`, json("POST", { action: "reveal" }));
598
+ warn("Showing secret values needs your approval in the browser. Keep the output private.");
599
+ const { variables = [] } = await approvedAction("env.reveal", { applicationId: app.id }) ?? {};
518
600
  const selected = variables.filter(variable => !wanted || variable.name === wanted);
519
601
  if (wanted && !selected.length) throw new CliError(`${wanted} is not set`);
602
+ if (!selected.length) warn("No environment variables.");
520
603
  for (const variable of selected) out(`${variable.name}=${variable.value}`);
521
604
  return 0;
522
605
  }
@@ -591,9 +674,9 @@ export async function main(argv = process.argv.slice(2)) {
591
674
  for (const item of databases) rows.push({ ...item, workspace: workspace.name, workspaceId: workspace.id });
592
675
  }
593
676
  if (flags.reveal) {
594
- warn("Revealing connection strings. Each reveal is audited; keep the output private. Requires a token with the 'manage' permission.");
677
+ warn("Showing a connection string needs your approval in the browser, one database at a time. Keep the output private.");
595
678
  for (const row of rows) {
596
- const { connectionString } = await call(`/api/workspaces/${encodeURIComponent(row.workspaceId)}/databases`, json("PATCH", { databaseId: row.id, action: "reveal" }));
679
+ const { connectionString } = await approvedAction("db.reveal", { workspaceId: row.workspaceId, databaseId: row.id }) ?? {};
597
680
  out(`${row.name} (${row.engine}, ${row.status}): ${connectionString}`);
598
681
  }
599
682
  return 0;
@@ -625,7 +708,246 @@ export async function main(argv = process.argv.slice(2)) {
625
708
  out(`${sub === "link" ? `Linked database to ${app.name} as DATABASE_URL` : `Unlinked database from ${app.name}`}. ${queuedNote(result)}`);
626
709
  return 0;
627
710
  }
628
- throw new CliError("Usage: vibingview db list|create|link|unlink");
711
+ if (sub === "reveal" || sub === "delete" || sub === "logs") {
712
+ if (!args[1]) throw new CliError(`Usage: vibingview db ${sub} <database-id|name> [--workspace <id|name>]`);
713
+ const found = await resolveDatabase(args[1]);
714
+ if (sub === "logs") {
715
+ const { logs = [] } = await call(`/api/workspaces/${encodeURIComponent(found.workspaceId)}/databases?logs=${encodeURIComponent(found.id)}`);
716
+ if (flags.json) out(JSON.stringify(logs, null, 2)); else for (const line of logs) out(clean(line.message));
717
+ if (!logs.length) warn("No database logs yet.");
718
+ return 0;
719
+ }
720
+ if (sub === "reveal") { const { connectionString } = await approvedAction("db.reveal", { workspaceId: found.workspaceId, databaseId: found.id }) ?? {}; out(connectionString); return 0; }
721
+ await approvedAction("db.delete", { workspaceId: found.workspaceId, databaseId: found.id });
722
+ out(`Deleting ${found.name}. Its data is erased once the server confirms.`);
723
+ return 0;
724
+ }
725
+ throw new CliError("Usage: vibingview db list|create|link|unlink|logs|reveal|delete");
726
+ }
727
+
728
+ // ----- servers -------------------------------------------------------------------
729
+ if (command === "server" || command === "servers") {
730
+ const sub = command === "servers" ? "list" : (args[0] ?? "list");
731
+ const serverRef = flags.server ?? flags.workspace;
732
+ if (sub === "list") {
733
+ const rows = await workspacesList();
734
+ if (flags.json) out(JSON.stringify(rows, null, 2)); else table(rows, [["id", "ID"], ["name", "NAME"], ["status", "STATUS"], ["planId", "PLAN"], ["publicIp", "IP"]]);
735
+ return 0;
736
+ }
737
+ const server = await resolveWorkspace(serverRef ?? (["info", "plans", "reboot", "start", "clean", "shutdown", "renew", "reinstall", "delete"].includes(sub) ? args[1] : undefined), { running: false });
738
+ const id = encodeURIComponent(server.id);
739
+ if (sub === "info") {
740
+ const [life, metrics] = await Promise.all([call(`/api/workspaces/${id}/lifecycle`), call(`/api/workspaces/${id}/metrics`).catch(() => ({}))]);
741
+ const last = (metrics.metrics ?? []).at(-1);
742
+ if (flags.json) { out(JSON.stringify({ server, lifecycle: life, latestMetrics: last ?? null, agentLastSeenAt: metrics.agentLastSeenAt ?? null }, null, 2)); return 0; }
743
+ const gb = bytes => `${(Number(bytes) / 1e9).toFixed(1)} GB`;
744
+ out(`${server.name} (${server.id})\n Status: ${String(life.status ?? server.status).toLowerCase()}\n Plan: ${server.planId} · ${euros(life.monthlyPriceCents ?? 0)}/month, billed every ${life.billingMonths ?? 1} month(s)\n IP: ${server.publicIp ?? "(pending)"}\n Renews: ${life.renewsAt ? new Date(life.renewsAt).toISOString().slice(0, 16).replace("T", " ") + " UTC" : "—"} (auto-renew ${life.autoRenew ? "on" : "off"})`);
745
+ if (life.bandwidth) out(` Traffic: ${gb(life.bandwidth.usedBytes)} of ${gb(life.bandwidth.quotaBytes)} this period`);
746
+ if (last) out(` Now: CPU ${Number(last.cpu_percent).toFixed(1)}% · RAM ${gb(last.memory_used_bytes)} · disk ${gb(last.disk_used_bytes)}`);
747
+ if (metrics.agentLastSeenAt) out(` Agent seen: ${new Date(metrics.agentLastSeenAt).toISOString().slice(0, 19).replace("T", " ")} UTC`);
748
+ if (life.upgrade && ["scheduled", "resizing", "needs_review"].includes(life.upgrade.status)) out(` Upgrade: to ${life.upgrade.toPlanName}, ${life.upgrade.status}${life.upgrade.status === "scheduled" ? ` for ${new Date(life.upgrade.scheduledFor).toISOString().slice(0, 16).replace("T", " ")} UTC` : ""}`);
749
+ for (const task of life.pendingTasks ?? []) out(` Task: ${task.label} (${task.status})`);
750
+ return 0;
751
+ }
752
+ if (sub === "plans") {
753
+ const plans = await call("/api/plans");
754
+ const current = (Array.isArray(plans) ? plans : []).find(plan => plan.id === server.planId);
755
+ const rows = (Array.isArray(plans) ? plans : []).map(plan => ({ ...plan, price: `${euros(plan.monthlyPriceCents)}/mo`, size: `${plan.cpu} vCPU · ${plan.ramGb} GB RAM · ${plan.storageGb} GB disk · ${plan.trafficTb} TB`, current: plan.id === server.planId ? "current" : current && plan.ramGb > current.ramGb ? "upgrade" : "" }));
756
+ if (flags.json) out(JSON.stringify(rows, null, 2)); else table(rows, [["id", "PLAN"], ["name", "NAME"], ["size", "SIZE"], ["price", "PRICE"], ["current", ""]]);
757
+ out("Upgrade with: vibingview server upgrade <plan> --server " + server.name + " (the exact charge is shown when you approve).");
758
+ return 0;
759
+ }
760
+ const simple = { reboot: "server.reboot", start: "server.start", clean: "server.clean", shutdown: "server.shutdown", renew: "server.renew", reinstall: "server.reinstall", delete: "server.delete" };
761
+ if (simple[sub]) {
762
+ const result = await approvedAction(simple[sub], { workspaceId: server.id });
763
+ out(result?.message ?? `${sub} requested for ${server.name}.`);
764
+ return 0;
765
+ }
766
+ if (sub === "billing") {
767
+ const months = Number(args[1]);
768
+ if (![1, 6, 12].includes(months)) throw new CliError("Usage: vibingview server billing <1|6|12> [--server <s>]");
769
+ const result = await approvedAction("server.billing", { workspaceId: server.id, months });
770
+ out(result?.message ?? `${server.name} is now billed every ${months} month(s) from the next renewal.`);
771
+ return 0;
772
+ }
773
+ if (sub === "auto-renew") {
774
+ if (!["on", "off"].includes(args[1])) throw new CliError("Usage: vibingview server auto-renew on|off [--server <s>]");
775
+ await approvedAction("server.auto_renew", { workspaceId: server.id, enabled: args[1] === "on" });
776
+ out(`Automatic renewal is ${args[1]} for ${server.name}.`);
777
+ return 0;
778
+ }
779
+ if (sub === "upgrade") {
780
+ if (args[1] === "cancel" || args[1] === "now") {
781
+ const result = await approvedAction(args[1] === "cancel" ? "server.upgrade_cancel" : "server.upgrade_now", { workspaceId: server.id });
782
+ out(result?.message ?? "Done.");
783
+ return 0;
784
+ }
785
+ if (!args[1]) throw new CliError("Usage: vibingview server upgrade <plan> [--server <s>] [--at \"YYYY-MM-DD HH:MM\"] (see vibingview server plans)");
786
+ const plans = await call("/api/plans");
787
+ const plan = (Array.isArray(plans) ? plans : []).find(item => item.id === args[1] || item.name.toLowerCase() === args[1].toLowerCase());
788
+ if (!plan) throw new CliError(`Unknown plan ${args[1]}. Run vibingview server plans.`);
789
+ let scheduledFor;
790
+ if (flags.at) {
791
+ // Local time of this computer, like the dashboard's picker.
792
+ const at = new Date(String(flags.at).replace(" ", "T"));
793
+ if (Number.isNaN(at.getTime())) throw new CliError('--at must look like "2026-11-02 03:00" (your local time)');
794
+ scheduledFor = at.toISOString();
795
+ }
796
+ const result = await approvedAction("server.upgrade", { workspaceId: server.id, planId: plan.id, when: scheduledFor ? "scheduled" : "now", ...(scheduledFor ? { scheduledFor } : {}) });
797
+ out(result?.message ?? `Upgrade of ${server.name} to ${plan.name} ${scheduledFor ? "scheduled" : "started"}.`);
798
+ out("Follow it with vibingview server info " + server.name);
799
+ return 0;
800
+ }
801
+ throw new CliError("Usage: vibingview server list|info|plans|reboot|start|clean|shutdown|upgrade|renew|billing|auto-renew|reinstall|delete");
802
+ }
803
+
804
+ // ----- app management ---------------------------------------------------------------
805
+ if (command === "app" && ["deployments", "rollback", "settings", "set", "delete"].includes(args[0])) {
806
+ const sub = args[0];
807
+ const app = await resolveApp(args[1]);
808
+ const id = encodeURIComponent(app.id);
809
+ if (sub === "deployments") {
810
+ const { deployments = [] } = await call(`/api/apps/${id}/deployments`);
811
+ if (flags.json) out(JSON.stringify(deployments, null, 2)); else table(deployments.map(d => ({ ...d, finished: d.finished_at ?? "" })), [["revision", "REV"], ["status", "STATUS"], ["trigger", "TRIGGER"], ["created_at", "CREATED"], ["finished", "FINISHED"], ["id", "ID"]]);
812
+ return 0;
813
+ }
814
+ if (sub === "rollback") {
815
+ if (!flags.to) throw new CliError("Usage: vibingview app rollback [app] --to <revision|deployment-id> (see vibingview app deployments)");
816
+ const { deployments = [] } = await call(`/api/apps/${id}/deployments`);
817
+ const target = deployments.find(d => d.id === flags.to || String(d.revision) === String(flags.to));
818
+ if (!target) throw new CliError(`Deployment ${flags.to} not found for ${app.name}`);
819
+ if (target.status !== "succeeded") throw new CliError(`Revision ${target.revision} did not succeed; choose a working one.`);
820
+ if (!(await confirm(`Roll ${app.name} back to revision ${target.revision}? The database and settings are not rolled back.`, flags))) return 1;
821
+ const result = await call(`/api/apps/${id}/actions`, json("POST", { action: "rollback", deploymentId: target.id }));
822
+ out(`Rollback to revision ${target.revision} queued${result.deploymentId ? ` (deployment ${result.deploymentId})` : ""}. Check vibingview status ${app.name}.`);
823
+ return 0;
824
+ }
825
+ if (sub === "settings") {
826
+ const current = await call(`/api/apps/${id}/settings`);
827
+ const settings = current.application?.build_settings ?? {};
828
+ if (flags.json) { out(JSON.stringify(settings, null, 2)); return 0; }
829
+ for (const [key, value] of Object.entries(settings)) if (key !== "dockerfile") out(`${key}: ${value === "" ? "(automatic)" : value}`);
830
+ if (settings.dockerfile) out("dockerfile: (custom, set)");
831
+ return 0;
832
+ }
833
+ if (sub === "set") {
834
+ const current = await call(`/api/apps/${id}/settings`);
835
+ const settings = { ...(current.application?.build_settings ?? {}) };
836
+ const map = { "root-dir": "rootDirectory", "install-command": "installCommand", "build-command": "buildCommand", "start-command": "startCommand", "output-dir": "outputDirectory" };
837
+ let changed = 0;
838
+ for (const [flag, key] of Object.entries(map)) if (typeof flags[flag] === "string") { settings[key] = flags[flag]; changed++; }
839
+ if (flags.port !== undefined) { const port = Number(flags.port); if (!Number.isInteger(port) || port < 1 || port > 65535) throw new CliError("--port must be 1-65535"); settings.port = port; changed++; }
840
+ if (flags.technique !== undefined) { const t = String(flags.technique); if (!["1", "2", "3", "4", "5"].includes(t)) throw new CliError("--technique must be 1, 2, 3, 4 or 5"); settings.builder = t === "1" ? "auto" : `technique-${t}`; changed++; }
841
+ if (typeof flags.dockerfile === "string") { settings.dockerfile = flags.dockerfile === "auto" ? "" : await readFile(path.resolve(flags.dockerfile), "utf8"); changed++; }
842
+ if (!changed) throw new CliError("Nothing to change. Pass --root-dir, --install-command, --build-command, --start-command, --output-dir, --port, --technique or --dockerfile <file|auto>.");
843
+ const result = await call(`/api/apps/${id}/settings`, json("PATCH", { settings }));
844
+ out(`Settings saved for ${app.name}. ${queuedNote(result)}`);
845
+ return 0;
846
+ }
847
+ await approvedAction("app.delete", { applicationId: app.id });
848
+ out(`${app.name} is being deleted.`);
849
+ return 0;
850
+ }
851
+
852
+ // ----- domains ---------------------------------------------------------------------
853
+ if (command === "domains" || command === "domain") {
854
+ const sub = args[0];
855
+ if (sub === "owned") {
856
+ const { domains = [] } = await call("/api/domains");
857
+ if (flags.json) out(JSON.stringify(domains, null, 2)); else table(domains, [["name", "DOMAIN"], ["status", "STATUS"], ["expiresAt", "EXPIRES"], ["autoRenew", "AUTO-RENEW"], ["id", "ID"]]);
858
+ return 0;
859
+ }
860
+ if (sub === "connect" || sub === "disconnect") {
861
+ if (!args[1]) throw new CliError(`Usage: vibingview domains ${sub} <domain>${sub === "connect" ? " --app <app>" : ""}`);
862
+ const { domains = [] } = await call("/api/domains");
863
+ const owned = domains.find(item => item.id === args[1] || item.name === args[1].toLowerCase());
864
+ if (!owned) throw new CliError(`${args[1]} is not one of your bought domains (vibingview domains owned)`);
865
+ if (sub === "connect") { const app = await resolveApp(); await approvedAction("domain.connect", { domainId: owned.id, applicationId: app.id }); out(`${owned.name} now points to ${app.name}. HTTPS is ready in a few minutes.`); }
866
+ else { await approvedAction("domain.disconnect", { domainId: owned.id }); out(`${owned.name} is disconnected.`); }
867
+ return 0;
868
+ }
869
+ const app = await resolveApp(sub === undefined || sub === "list" ? args[1] : undefined);
870
+ const id = encodeURIComponent(app.id);
871
+ const { domains = [] } = await call(`/api/apps/${id}/domains`);
872
+ const find = host => { const found = domains.find(item => item.hostname === String(host).toLowerCase() || item.id === host); if (!found) throw new CliError(`${host} is not a domain of ${app.name} (vibingview domains)`); return found; };
873
+ if (sub === undefined || sub === "list") {
874
+ if (flags.json) out(JSON.stringify(domains, null, 2)); else table(domains.map(d => ({ ...d, verified: d.verified_at ? "yes" : "waiting", https: d.certificate_status })), [["hostname", "DOMAIN"], ["type", "TYPE"], ["verified", "VERIFIED"], ["https", "HTTPS"]]);
875
+ return 0;
876
+ }
877
+ if (sub === "add") {
878
+ if (!args[1]) throw new CliError("Usage: vibingview domains add <hostname> [--app <app>]");
879
+ const result = await call(`/api/apps/${id}/domains`, json("POST", { hostname: args[1] }));
880
+ const added = new Set((result.domains ?? []).map(item => item.hostname));
881
+ const { domains: now = [] } = await call(`/api/apps/${id}/domains`);
882
+ out(`Added ${[...added].join(" and ") || args[1]} to ${app.name}. Create these DNS records at your registrar (VibingView then connects the domain by itself within minutes; vibingview domains check ${args[1]} checks now):`);
883
+ for (const item of now.filter(item => added.has(item.hostname))) for (const record of item.records ?? []) out(` ${record.type ?? ""} ${record.name ?? record.host ?? ""} → ${record.value ?? ""}`);
884
+ return 0;
885
+ }
886
+ if (sub === "check") {
887
+ const result = await call(`/api/apps/${id}/domains`, json("PATCH", { domainId: find(args[1]).id }));
888
+ out(result.verified ? `${args[1]} is connected. HTTPS is issued automatically within minutes.` : result.message ?? `${args[1]} is not ready yet.`);
889
+ return result.verified ? 0 : 1;
890
+ }
891
+ if (sub === "remove") {
892
+ await approvedAction("domain.remove", { applicationId: app.id, domainId: find(args[1]).id });
893
+ out(`${args[1]} was removed from ${app.name}.`);
894
+ return 0;
895
+ }
896
+ throw new CliError("Usage: vibingview domains [list|add|check|remove|owned|connect|disconnect]");
897
+ }
898
+
899
+ if (command === "nameservers") {
900
+ const sub = args[0];
901
+ const app = await resolveApp(sub === undefined || sub === "list" ? args[1] : undefined);
902
+ const id = encodeURIComponent(app.id);
903
+ const { zones = [] } = await call(`/api/apps/${id}/nameservers`);
904
+ const find = domain => { const found = zones.find(zone => zone.domain === String(domain).toLowerCase().replace(/^www\./, "") || zone.id === domain); if (!found) throw new CliError(`${domain} is not connected with nameservers on ${app.name}`); return found; };
905
+ if (sub === undefined || sub === "list") {
906
+ if (flags.json) out(JSON.stringify(zones, null, 2)); else table(zones.map(zone => ({ ...zone, ns: zone.nameservers.join(", ") })), [["domain", "DOMAIN"], ["status", "STATUS"], ["ns", "NAMESERVERS"]]);
907
+ return 0;
908
+ }
909
+ if (sub === "connect") {
910
+ if (!args[1]) throw new CliError("Usage: vibingview nameservers connect <domain> [--app <app>]");
911
+ const result = await call(`/api/apps/${id}/nameservers`, json("POST", { domain: args[1] }));
912
+ const zone = result.zone ?? result;
913
+ out(`Set these nameservers at your registrar for ${zone.domain ?? args[1]}:\n${(zone.nameservers ?? []).map(ns => ` ${ns}`).join("\n")}\nVibingView checks automatically; run vibingview nameservers check ${args[1]} to check now.`);
914
+ return 0;
915
+ }
916
+ if (sub === "check") {
917
+ const result = await call(`/api/apps/${id}/nameservers`, json("PATCH", { zoneId: find(args[1]).id, action: "check" }));
918
+ const status = typeof result.status === "string" ? result.status : result.status?.status ?? result.zone?.status ?? "checked";
919
+ out(`${args[1]}: ${status}`);
920
+ return 0;
921
+ }
922
+ if (sub === "disconnect") {
923
+ await approvedAction("nameservers.disconnect", { applicationId: app.id, zoneId: find(args[1]).id });
924
+ out(`${args[1]} was disconnected. Set your nameservers back at your registrar.`);
925
+ return 0;
926
+ }
927
+ throw new CliError("Usage: vibingview nameservers [list|connect|check|disconnect]");
928
+ }
929
+
930
+ // ----- billing ---------------------------------------------------------------------
931
+ if (command === "balance") {
932
+ const { balanceCents = 0 } = await call("/api/account/balance");
933
+ if (flags.json) out(JSON.stringify({ balanceCents }, null, 2)); else out(`Wallet balance: ${euros(balanceCents)}`);
934
+ return 0;
935
+ }
936
+ if (command === "payments") {
937
+ const { payments = [], entries = [] } = await call("/api/payments/activity");
938
+ if (flags.json) { out(JSON.stringify({ payments, entries }, null, 2)); return 0; }
939
+ table(payments.map(p => ({ ...p, amount: euros(p.amount_cents), date: String(p.created_at).slice(0, 10) })), [["date", "DATE"], ["amount", "AMOUNT"], ["provider", "METHOD"], ["status", "STATUS"]]);
940
+ return 0;
941
+ }
942
+ if (command === "topup") {
943
+ const amount = Number(String(args[0] ?? "").replace(",", "."));
944
+ if (!(amount >= 5 && amount <= 1000)) throw new CliError("Usage: vibingview topup <euros 5-1000> [--method card|crypto]");
945
+ const method = flags.method ?? "card";
946
+ if (!["card", "crypto"].includes(method)) throw new CliError("--method must be card or crypto");
947
+ const result = await approvedAction("wallet.topup", { amountCents: Math.round(amount * 100), method });
948
+ out(`Pay here (the wallet is credited when the payment is confirmed):\n ${result.checkoutUrl}`);
949
+ if (!flags["no-browser"]) openBrowser(result.checkoutUrl);
950
+ return 0;
629
951
  }
630
952
 
631
953
  // ----- catalog apps ------------------------------------------------------------
package/src/mcp.mjs CHANGED
@@ -46,7 +46,7 @@ async function handle(message) {
46
46
  const params = message.params && typeof message.params === "object" && !Array.isArray(message.params) ? message.params : {};
47
47
  if (message.method === "initialize") {
48
48
  const protocolVersion = SUPPORTED_VERSIONS.includes(params.protocolVersion) ? params.protocolVersion : SUPPORTED_VERSIONS[0];
49
- return { protocolVersion, capabilities: { tools: { listChanged: false } }, serverInfo: { name: "vibingview", version: "0.1.4" }, instructions: INSTRUCTIONS };
49
+ return { protocolVersion, capabilities: { tools: { listChanged: false } }, serverInfo: { name: "vibingview", version: "0.2.0" }, instructions: INSTRUCTIONS };
50
50
  }
51
51
  if (message.method === "ping") return {};
52
52
  if (message.method === "tools/list") {