@vibingview/cli 0.1.3 → 0.1.4

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 +42 -10
  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.3",
3
+ "version": "0.1.4",
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.3";
13
+ export const VERSION = "0.1.4";
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}
@@ -122,11 +122,17 @@ function createClient(base, token) {
122
122
  response = await fetch(`${base}${route}`, { ...init, redirect: "error", signal: AbortSignal.timeout(init.timeout ?? 120000), headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...init.headers } });
123
123
  } catch (error) { throw new CliError(redact(`Could not reach ${base}: ${error.cause?.message ?? error.message}`)); }
124
124
  if (response.status === 204) return {};
125
- const body = await response.json().catch(() => ({}));
125
+ let body = {}, unreadable = false;
126
+ const text = await response.text().catch(() => { unreadable = true; return ""; });
127
+ if (text) { try { body = JSON.parse(text); } catch { unreadable = true; } }
128
+ // A success whose body was cut off (e.g. the connection dropped) must not read as "empty": callers would treat an
129
+ // unread app list as "no apps" and create a duplicate.
130
+ if (response.ok && unreadable)
131
+ 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).`);
126
132
  if (!response.ok) {
127
133
  let message = typeof body.error === "string" ? body.error : body.error?.message ?? `HTTP ${response.status}`;
128
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.";
129
- throw Object.assign(new CliError(redact(message)), { status: response.status });
135
+ throw Object.assign(new CliError(redact(message)), { status: response.status, code: typeof body.code === "string" ? body.code : undefined });
130
136
  }
131
137
  return body;
132
138
  };
@@ -315,7 +321,7 @@ export async function main(argv = process.argv.slice(2)) {
315
321
  // ----- lookups -------------------------------------------------------------
316
322
  const cwd = process.cwd();
317
323
  const project = await readProjectConfig(cwd);
318
- const workspacesList = async () => (await call("/api/workspaces")).workspaces ?? [];
324
+ const workspacesList = async () => { const { workspaces } = await call("/api/workspaces"); if (!Array.isArray(workspaces)) throw new CliError("The server did not send the list of workspaces. Nothing was changed; try again."); return workspaces; };
319
325
  const isRunning = workspace => String(workspace.status).toUpperCase() === "RUNNING";
320
326
  async function resolveWorkspace(reference, { running = true } = {}) {
321
327
  const all = await workspacesList();
@@ -371,9 +377,22 @@ export async function main(argv = process.argv.slice(2)) {
371
377
  reportNotableSkips(pkg.notable ?? []);
372
378
  if (isIdenticalDeploy(linked, pkg.sha256, flags.force)) { out("No changes since the last deploy. Use --force to redeploy anyway."); return 0; }
373
379
 
374
- const { applications = [] } = await call("/api/apps");
375
- let app = linked.applicationId && (!flags.name || flags.name === linked.name) ? applications.find(item => item.id === linked.applicationId) : undefined;
376
- if (linked.applicationId && !app && !flags.name) warn("The linked application no longer exists; creating a new one.");
380
+ if (linked.controlPlane && linked.controlPlane !== base) warn(`This folder was deployed to ${linked.controlPlane}; you are now using ${base}.`);
381
+ const appList = async () => { const { applications } = await call("/api/apps"); if (!Array.isArray(applications)) throw new CliError("The server did not send the list of applications. Nothing was changed; try again."); return applications; };
382
+ if (linked.applicationId && flags.name && flags.name !== linked.name && !flags.force) throw new CliError(`This folder is linked to the application "${linked.name}", but --name says "${flags.name}". Drop --name to redeploy it, use --force to create another app, or remove .vibingview/config.json.`);
383
+ const useLinked = linked.applicationId && (!flags.name || flags.name === linked.name);
384
+ let app = useLinked ? (await appList()).find(item => item.id === linked.applicationId) : undefined;
385
+ if (useLinked && !app) {
386
+ // One answer without the app must never create a duplicate: ask for the app itself before assuming it was deleted.
387
+ const deleted = await call(`/api/apps/${encodeURIComponent(linked.applicationId)}/settings`).then(() => false, error => {
388
+ if (error.status === 404 || error.code === "apps.not_found") return true;
389
+ throw new CliError(`Could not confirm that the linked application ${linked.name ?? linked.applicationId} was deleted (${error.message}). Nothing was created; try again.`);
390
+ });
391
+ if (!deleted) {
392
+ app = (await appList()).find(item => item.id === linked.applicationId);
393
+ if (!app) throw new CliError(`The linked application ${linked.name ?? linked.applicationId} exists but was missing from the server's list. Nothing was created; try again in a moment.`);
394
+ } else warn("The linked application was deleted; creating a new one.");
395
+ }
377
396
  const catalog = await call("/api/catalog");
378
397
  const suffixes = catalog.domains ?? [];
379
398
  let suffix = linked.domainSuffix;
@@ -416,7 +435,8 @@ export async function main(argv = process.argv.slice(2)) {
416
435
  if (port !== undefined) form.set("port", String(port));
417
436
  if (builder) form.set("builder", builder);
418
437
  form.set("file", new Blob([pkg.archive], { type: "application/zip" }), `${name}.zip`);
419
- app = (await call("/api/apps", { method: "POST", body: form })).application;
438
+ // A large upload must not time out client-side while the server finishes creating the app (that made a second app on retry).
439
+ app = (await call("/api/apps", { method: "POST", body: form, timeout: 120000 + Math.ceil(pkg.archive.length / 1_000_000) * 4000 })).application;
420
440
  deploymentId = app.lastDeploymentId;
421
441
  }
422
442
  const state = { controlPlane: base, workspaceId, applicationId: app.id, name: app.name, url: app.url, domainSuffix: suffix, customDomain: customDomain ?? linked.customDomain, lastHash: linked.lastHash, deployedAt: new Date().toISOString() };
@@ -515,8 +535,20 @@ export async function main(argv = process.argv.slice(2)) {
515
535
  const environment = {};
516
536
  const pairs = args.slice(1);
517
537
  if (flags.stdin) {
518
- if (pairs.length !== 1) throw new CliError("Usage: vibingview env set NAME --stdin");
519
- environment[pairs[0]] = await readStdin();
538
+ // One name: the whole input is its value. No name: one NAME=value per line (.env style), so secrets never sit in argv.
539
+ if (pairs.length > 1) throw new CliError("Usage: vibingview env set NAME --stdin, or env set --stdin with NAME=value lines on stdin");
540
+ const input = await readStdin();
541
+ if (pairs.length === 1) environment[pairs[0]] = input;
542
+ else for (const line of input.split(/\r?\n/)) {
543
+ const text = line.trim();
544
+ if (!text || text.startsWith("#")) continue;
545
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(text);
546
+ if (!match) throw new CliError(`Not a NAME=value line: ${text.slice(0, 40)}`);
547
+ let value = match[2];
548
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
549
+ environment[match[1]] = value;
550
+ }
551
+ if (!Object.keys(environment).length) throw new CliError("No NAME=value lines were read from stdin.");
520
552
  } else {
521
553
  if (!pairs.length) throw new CliError("Usage: vibingview env set NAME=value [NAME=value…] or NAME --stdin");
522
554
  for (const pair of pairs) {
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.3" }, instructions: INSTRUCTIONS };
49
+ return { protocolVersion, capabilities: { tools: { listChanged: false } }, serverInfo: { name: "vibingview", version: "0.1.4" }, instructions: INSTRUCTIONS };
50
50
  }
51
51
  if (message.method === "ping") return {};
52
52
  if (message.method === "tools/list") {