@vibingview/cli 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -31,7 +31,7 @@ vibingview logout # revokes the token on the server and forgets it loc
31
31
  ## Deploy
32
32
 
33
33
  ```bash
34
- vibingview deploy [dir] [--workspace <id|name>] [--name <app>] [--port <n>] [--domain <suffix|hostname>] [--force] [--yes]
34
+ vibingview deploy [dir] [--workspace <id|name>] [--name <app>] [--port <n>] [--domain <suffix|hostname>] [--technique 1|2|3|4|5] [--force] [--yes]
35
35
  vibingview status [app]
36
36
  vibingview logs [app] [--build|--runtime] [--limit <n>] [--ai]
37
37
  vibingview restart [app]
@@ -40,7 +40,9 @@ vibingview workspaces
40
40
  vibingview apps
41
41
  ```
42
42
 
43
- `deploy` packages the directory in memory, excluding `.git`, `node_modules`, build output, virtualenvs, caches, `.env*` files, credentials, private keys, archives, binaries, local databases and large media, and refusing symlinks that escape the project. It skips an unchanged project unless `--force`, uploads it, follows the build until it succeeds or fails, and links the directory in `.vibingview/config.json` (added to `.gitignore`; it never contains your token).
43
+ `deploy` packages the directory in memory, excluding `.git`, `node_modules`, build output, virtualenvs, caches, `.env*` files, credentials, private keys, archives, binaries, local databases and design source files (`.psd`, `.ai`, `.sketch`), and refusing symlinks that escape the project. Folders starting with a dot are skipped except `.well-known`; video and audio files are uploaded only from a folder the site serves files from (`public/`, `static/`, `assets/`, `media/`, `images/`…). Skipped files the site may need (stray media, unknown hidden folders, `.env` files) are listed by name after packaging. It skips an unchanged project unless `--force`, uploads it, follows the build until it succeeds or fails, and links the directory in `.vibingview/config.json` (added to `.gitignore`; it never contains your token).
44
+
45
+ `--technique` picks how a new app is built (default 5, "Smart": detects npm/pnpm/yarn/bun and the Node version, reuses installed packages between builds, ships the smallest safe image; other languages use technique 1). Changing environment variables or linking a database restarts a running app on its current build instead of rebuilding.
44
46
 
45
47
  ## Environment variables, ports, databases and catalog apps
46
48
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@vibingview/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",
7
7
  "bin": {
8
- "cli": "./bin/vibingview.mjs",
9
- "vibingview": "./bin/vibingview.mjs",
10
- "vibingview-mcp": "./bin/vibingview-mcp.mjs"
8
+ "cli": "bin/vibingview.mjs",
9
+ "vibingview": "bin/vibingview.mjs",
10
+ "vibingview-mcp": "bin/vibingview-mcp.mjs"
11
11
  },
12
12
  "exports": {
13
13
  ".": "./src/cli.mjs",
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.1";
13
+ export const VERSION = "0.1.3";
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}
@@ -25,7 +25,7 @@ Account
25
25
  vibingview mcp Run the stdio MCP bridge (used by AI clients)
26
26
 
27
27
  Deploy
28
- vibingview deploy [dir] [--workspace <id|name>] [--name <app>] [--port <n>] [--domain <suffix|hostname>] [--force] [--yes] [--no-wait] [--timeout <s>]
28
+ vibingview deploy [dir] [--workspace <id|name>] [--name <app>] [--port <n>] [--domain <suffix|hostname>] [--technique 1|2|3|4|5] [--force] [--yes] [--no-wait] [--timeout <s>]
29
29
  vibingview status [app] Application status and recent deployments
30
30
  vibingview logs [app] [--build|--runtime] [--limit <n>] [--ai]
31
31
  vibingview restart|rebuild [app]
@@ -54,7 +54,7 @@ Control plane: --url <origin> on any command, else VIBINGVIEW_URL, else the inst
54
54
  else ${DEFAULT_URL}. VIBINGVIEW_TOKEN overrides the saved token.`;
55
55
 
56
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"]);
57
+ const VALUE_FLAGS = new Set(["workspace", "name", "port", "domain", "app", "limit", "engine", "client", "transport", "timeout", "url", "token", "technique", "builder"]);
58
58
 
59
59
  export function parseArgs(argv) {
60
60
  const positional = [];
@@ -81,6 +81,8 @@ export function parseArgs(argv) {
81
81
  // ---------------------------------------------------------------------------
82
82
  // Credentials
83
83
  // ---------------------------------------------------------------------------
84
+ // The control plane restarts on the current image when only runtime settings changed (rebuild: false).
85
+ const queuedNote = result => result.rebuild === false ? `Restarting on the current build, no rebuild needed (deployment ${result.deploymentId}).` : `Redeploy queued (deployment ${result.deploymentId}).`;
84
86
  const home = () => os.homedir();
85
87
  const configDir = () => path.join(home(), ".vibingview");
86
88
  const configFile = () => path.join(configDir(), "config.json");
@@ -135,6 +137,8 @@ const json = (method, body) => ({ method, headers: { "Content-Type": "applicatio
135
137
  // Output helpers
136
138
  // ---------------------------------------------------------------------------
137
139
  const out = text => process.stdout.write(`${text}\n`);
140
+ // Log lines are third-party program output: strip ANSI/OSC escapes and C0 controls (except tab) before they reach the terminal.
141
+ const clean = text => String(text ?? "").replace(/\x1b\[[0-9;?]*[ -\/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
138
142
  const warn = text => process.stderr.write(`${text}\n`);
139
143
  function table(rows, columns) {
140
144
  if (!rows.length) return out("(none)");
@@ -364,6 +368,7 @@ export async function main(argv = process.argv.slice(2)) {
364
368
  const pkg = await packageProject(directory);
365
369
  out(`Packaged ${pkg.files.length} files (${(pkg.archive.length / 1024).toFixed(1)} KB, sha256 ${pkg.sha256.slice(0, 12)}…); excluded ${pkg.skipped.length} paths.`);
366
370
  for (const message of pkg.warnings) warn(message);
371
+ reportNotableSkips(pkg.notable ?? []);
367
372
  if (isIdenticalDeploy(linked, pkg.sha256, flags.force)) { out("No changes since the last deploy. Use --force to redeploy anyway."); return 0; }
368
373
 
369
374
  const { applications = [] } = await call("/api/apps");
@@ -381,6 +386,11 @@ export async function main(argv = process.argv.slice(2)) {
381
386
  }
382
387
  if (!suffix || !suffixes.includes(suffix)) suffix = suffixes[0];
383
388
  const port = flags.port === undefined ? undefined : Number(flags.port);
389
+ // --technique 1|2 (or --builder auto|classic|railpack|technique-2) chooses how a new app is built.
390
+ const builderFlags = { "1": "auto", "2": "technique-2", "3": "technique-3", "4": "technique-4", "5": "technique-5", "technique-1": "auto", "technique-2": "technique-2", "technique-3": "technique-3", "technique-4": "technique-4", "technique-5": "technique-5", auto: "auto", classic: "classic", railpack: "railpack" };
391
+ const builderFlag = flags.technique ?? flags.builder;
392
+ const builder = builderFlag === undefined ? undefined : builderFlags[String(builderFlag).toLowerCase()];
393
+ if (builderFlag !== undefined && !builder) throw new CliError("--technique must be 1, 2, 3, 4 or 5 (or --builder auto|classic|railpack|technique-2|technique-3|technique-4|technique-5)");
384
394
  if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65535)) throw new CliError("--port must be 1-65535");
385
395
 
386
396
  let deploymentId;
@@ -388,6 +398,7 @@ export async function main(argv = process.argv.slice(2)) {
388
398
  if (app) {
389
399
  workspaceId = app.workspaceId;
390
400
  if (port !== undefined && port !== app.internalPort) warn(`The port is unchanged by redeploys; run vibingview port set ${port} to change it.`);
401
+ if (builder) warn("--technique applies when an app is created; change an existing app's build technique in its build settings on the dashboard.");
391
402
  if (!(await confirm(`Redeploy ${app.name} (${app.url ?? app.id}) with this source?`, flags))) return 1;
392
403
  const form = new FormData();
393
404
  form.set("file", new Blob([pkg.archive], { type: "application/zip" }), `${app.name}.zip`);
@@ -403,6 +414,7 @@ export async function main(argv = process.argv.slice(2)) {
403
414
  form.set("name", name);
404
415
  if (suffix) form.set("domainSuffix", suffix);
405
416
  if (port !== undefined) form.set("port", String(port));
417
+ if (builder) form.set("builder", builder);
406
418
  form.set("file", new Blob([pkg.archive], { type: "application/zip" }), `${name}.zip`);
407
419
  app = (await call("/api/apps", { method: "POST", body: form })).application;
408
420
  deploymentId = app.lastDeploymentId;
@@ -414,7 +426,9 @@ export async function main(argv = process.argv.slice(2)) {
414
426
  if (customDomain && customDomain !== linked.customDomain) {
415
427
  try {
416
428
  const result = await call(`/api/apps/${encodeURIComponent(app.id)}/domains`, json("POST", { hostname: customDomain }));
417
- out(`Custom domain ${customDomain} added. Prove ownership and route traffic with DNS:\n CNAME ${result.instructions.cname.name} → ${result.instructions.cname.value}\n or TXT ${result.instructions.txt.name} = ${result.instructions.txt.value}\nThen verify it in the dashboard. HTTPS is issued only after verification.`);
429
+ // The server returns the records to add for each hostname it added (the main domain also adds www).
430
+ const lines = (result.domains ?? []).flatMap((d) => (d.records ?? []).map((r) => ` ${d.hostname}: ${r.type} ${r.name} → ${r.value}`));
431
+ out(`Custom domain ${customDomain} added. Add ${lines.length === 1 ? "this record" : "these records"} where you bought the domain:\n${lines.join("\n")}\nVibingView checks every few minutes and connects it with HTTPS by itself.`);
418
432
  } catch (error) { warn(`Could not add custom domain: ${error.message}`); }
419
433
  }
420
434
 
@@ -448,9 +462,9 @@ export async function main(argv = process.argv.slice(2)) {
448
462
  const { logs = [] } = await call(`/api/apps/${encodeURIComponent(app.id)}/logs?kind=${kind}&limit=${max}`);
449
463
  if (flags.ai) {
450
464
  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`);
451
- for (const line of logs) out(`${line.recorded_at ? new Date(line.recorded_at).toISOString() + " " : ""}${line.stream ? `[${line.stream}] ` : ""}${line.message}`);
465
+ for (const line of logs) out(`${line.recorded_at ? new Date(line.recorded_at).toISOString() + " " : ""}${line.stream ? `[${line.stream}] ` : ""}${clean(line.message)}`);
452
466
  out("```\n\nSecrets are redacted by the workspace agent before storage, but review the output before sharing it.");
453
- } else for (const line of logs) out(line.message);
467
+ } else for (const line of logs) out(clean(line.message));
454
468
  if (!logs.length) warn(`No ${kind} logs yet.`);
455
469
  return 0;
456
470
  }
@@ -472,7 +486,7 @@ export async function main(argv = process.argv.slice(2)) {
472
486
  if (!id || !name || value === undefined) throw new CliError("Usage: vibingview env get|set|remove … (see vibingview help)");
473
487
  warn("Deprecated: use vibingview env set NAME=value --app <app>. Values passed as arguments may be kept in shell history.");
474
488
  const result = await call(`/api/apps/${encodeURIComponent(id)}/environment`, json("POST", { name, value }));
475
- out(`Set ${name}. Redeploy queued (deployment ${result.deploymentId}).`);
489
+ out(`Set ${name}. ${queuedNote(result)}`);
476
490
  return 0;
477
491
  }
478
492
  const app = await resolveApp();
@@ -513,13 +527,13 @@ export async function main(argv = process.argv.slice(2)) {
513
527
  }
514
528
  for (const name of Object.keys(environment)) if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(name)) throw new CliError(`Invalid variable name ${name}`);
515
529
  const result = await call(`/api/apps/${encodeURIComponent(app.id)}/settings`, json("PATCH", { environment }));
516
- out(`Set ${Object.keys(environment).join(", ")} on ${app.name}. Values stored encrypted. Redeploy queued (deployment ${result.deploymentId}).`);
530
+ out(`Set ${Object.keys(environment).join(", ")} on ${app.name}. Values stored encrypted. ${queuedNote(result)}`);
517
531
  return 0;
518
532
  }
519
533
  const names = args.slice(1);
520
534
  if (!names.length) throw new CliError("Usage: vibingview env remove NAME [NAME…]");
521
535
  const result = await call(`/api/apps/${encodeURIComponent(app.id)}/settings`, json("PATCH", { deleteVariables: names }));
522
- out(`Removed ${names.join(", ")} from ${app.name}. Redeploy queued (deployment ${result.deploymentId}).`);
536
+ out(`Removed ${names.join(", ")} from ${app.name}. ${queuedNote(result)}`);
523
537
  return 0;
524
538
  }
525
539
 
@@ -530,7 +544,7 @@ export async function main(argv = process.argv.slice(2)) {
530
544
  const app = await resolveApp();
531
545
  const current = await call(`/api/apps/${encodeURIComponent(app.id)}/settings`);
532
546
  const result = await call(`/api/apps/${encodeURIComponent(app.id)}/settings`, json("PATCH", { settings: { ...(current.application?.build_settings ?? {}), port } }));
533
- out(`${app.name} now expects port ${port}. Redeploy queued (deployment ${result.deploymentId}).`);
547
+ out(`${app.name} now expects port ${port}. ${queuedNote(result)}`);
534
548
  return 0;
535
549
  }
536
550
 
@@ -576,7 +590,7 @@ export async function main(argv = process.argv.slice(2)) {
576
590
  databaseId = found.id;
577
591
  } else if (!(await confirm(`Unlink the database from ${app.name}? DATABASE_URL will be removed and the app redeployed.`, flags))) return 1;
578
592
  const result = await call(`/api/apps/${encodeURIComponent(app.id)}/settings`, json("PATCH", { databaseId }));
579
- out(`${sub === "link" ? `Linked database to ${app.name} as DATABASE_URL` : `Unlinked database from ${app.name}`}. Redeploy queued (deployment ${result.deploymentId}).`);
593
+ out(`${sub === "link" ? `Linked database to ${app.name} as DATABASE_URL` : `Unlinked database from ${app.name}`}. ${queuedNote(result)}`);
580
594
  return 0;
581
595
  }
582
596
  throw new CliError("Usage: vibingview db list|create|link|unlink");
@@ -624,6 +638,14 @@ async function legacyDeploy(call, [workspaceId, name, file]) {
624
638
  return 0;
625
639
  }
626
640
 
641
+ /** Names the skipped files a website may need, so nothing disappears without a word. */
642
+ function reportNotableSkips(notable) {
643
+ const show = (items, text) => { if (!items.length) return; warn(`${text}\n${items.slice(0, 10).map(item => ` ${item.name}`).join("\n")}${items.length > 10 ? `\n …and ${items.length - 10} more` : ""}`); };
644
+ show(notable.filter(item => item.reason === "media"), "Not uploaded: video and audio files are only uploaded from a folder your site serves files from (public/, static/ or assets/). Move these there if the site uses them:");
645
+ show(notable.filter(item => item.reason === "hidden-folder"), "Not uploaded: folders whose names start with a dot (only .well-known is uploaded). If the site needs these files, move them to a normal folder:");
646
+ show(notable.filter(item => item.reason === "env"), "Not uploaded: .env files (they can hold secrets). Set the values the app needs with vibingview env set NAME1=value1 NAME2=value2 --app <app> after the first deploy:");
647
+ }
648
+
627
649
  async function waitForDeployment(call, applicationId, deploymentId, timeoutMs) {
628
650
  const deadline = Date.now() + timeoutMs;
629
651
  let printed = 0;
@@ -635,7 +657,7 @@ async function waitForDeployment(call, applicationId, deploymentId, timeoutMs) {
635
657
  ]);
636
658
  const deployment = deployments.find(item => item.id === deploymentId) ?? deployments[0];
637
659
  if (deployment && deployment.id === deploymentId) {
638
- for (const line of logs.slice(printed)) out(` ${line.message}`);
660
+ for (const line of logs.slice(printed)) out(` ${clean(line.message)}`);
639
661
  printed = Math.max(printed, logs.length);
640
662
  }
641
663
  const status = deployment?.status ?? "queued";
@@ -27,21 +27,39 @@ const EXCLUDED_EXTENSIONS = new Set([
27
27
  ".exe", ".dll", ".so", ".dylib", ".bin", ".o", ".a", ".obj", ".class", ".jar", ".war", ".pyc", ".pyo", ".dmg", ".iso", ".img", ".msi", ".deb", ".rpm", ".apk",
28
28
  // local databases
29
29
  ".sqlite", ".sqlite3", ".db",
30
- // large media
31
- ".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v", ".wmv", ".flv", ".mp3", ".wav", ".flac", ".aac", ".ogg", ".psd", ".ai", ".sketch",
30
+ // design source files
31
+ ".psd", ".ai", ".sketch",
32
32
  // logs
33
33
  ".log",
34
34
  ]);
35
+ // Video and audio are uploaded when they sit in a folder a website serves files from (public/hero.mp4,
36
+ // src/assets/intro.webm); elsewhere they are usually raw footage and only make the upload bigger.
37
+ const MEDIA_EXTENSIONS = new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v", ".wmv", ".flv", ".mp3", ".wav", ".flac", ".aac", ".ogg", ".oga", ".opus", ".m4a"]);
38
+ const ASSET_FOLDERS = new Set(["public", "static", "assets", "media", "images", "img", "video", "videos", "audio", "sounds", "fonts", "www", "wwwroot", "htdocs"]);
39
+ // Hidden folders a website really serves (domain checks, app links, payment-provider verification).
40
+ const ALLOWED_DOT_DIRECTORIES = new Set([".well-known"]);
41
+ // Hidden folders that are tools, caches or editor settings: skipped without a warning.
42
+ const TOOL_DOT_DIRECTORIES = new Set([".git", ".next", ".nuxt", ".svelte-kit", ".vercel", ".netlify", ".output", ".turbo", ".cache", ".parcel-cache", ".idea", ".vscode", ".fleet", ".zed", ".github", ".gitlab", ".husky", ".devcontainer", ".expo", ".angular", ".yarn", ".pnpm-store", ".venv", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".tox", ".terraform", ".serverless", ".docusaurus", ".astro", ".vite", ".wrangler", ".firebase", ".aws-sam", ".claude", ".cursor", ".windsurf", ".vibingview", ".vibed"]);
35
43
  const ALLOWED_DOTFILES = new Set([".dockerignore", ".nvmrc", ".node-version", ".python-version", ".tool-versions", ".gitignore", ".htaccess", ".babelrc", ".browserslistrc", ".prettierrc", ".editorconfig", ".npmignore", ".eslintrc", ".eslintrc.json", ".eslintrc.js", ".eslintrc.cjs", ".env.example", ".env.sample", ".env.template"]);
36
44
 
37
45
  /** Whether a directory name must never be uploaded. */
38
46
  export function isExcludedDirectory(name) {
47
+ if (ALLOWED_DOT_DIRECTORIES.has(name.toLowerCase())) return false;
39
48
  return name.startsWith(".") || EXCLUDED_DIRECTORIES.has(name) || /^\.?venv[\w.-]*$/i.test(name);
40
49
  }
41
50
 
42
- /** Whether a file name must never be uploaded (secrets, archives, binaries, large media). */
43
- export function isExcludedFile(name) {
51
+ /** True when a project path sits in a folder a website serves files from (public/, static/, assets/…). */
52
+ export function inAssetFolder(relativePath) {
53
+ return relativePath.split("/").slice(0, -1).some(part => ASSET_FOLDERS.has(part.toLowerCase()));
54
+ }
55
+
56
+ /**
57
+ * Whether a file must never be uploaded (secrets, archives, binaries, local databases, logs). Video and audio are
58
+ * uploaded only from asset folders, so pass the project-relative path to allow them there.
59
+ */
60
+ export function isExcludedFile(name, relativePath = name) {
44
61
  const lower = name.toLowerCase();
62
+ if (MEDIA_EXTENSIONS.has(path.extname(lower))) return !inAssetFolder(relativePath);
45
63
  if (SECRET_FILE_NAMES.has(lower)) return true;
46
64
  if (lower.startsWith(".env")) return !ALLOWED_DOTFILES.has(lower);
47
65
  if (lower.startsWith(".") && !ALLOWED_DOTFILES.has(lower)) return true;
@@ -60,7 +78,9 @@ function inside(root, candidate) {
60
78
  * Recursively list uploadable files. Symlinks that resolve outside the project
61
79
  * root abort the scan; symlinks to files inside the root are followed and
62
80
  * symlinked directories are skipped to avoid loops.
63
- * @returns {Promise<{files:{name:string,absolute:string,size:number}[],skipped:string[],warnings:string[]}>}
81
+ * `notable` lists skipped paths the site may really need (media outside asset folders, unknown hidden folders)
82
+ * and .env files whose settings must be set on the server instead; the CLI prints them by name.
83
+ * @returns {Promise<{files:{name:string,absolute:string,size:number}[],skipped:string[],warnings:string[],notable:{name:string,reason:string}[]}>}
64
84
  */
65
85
  export async function scanProject(directory) {
66
86
  const root = await realpath(path.resolve(directory));
@@ -69,6 +89,12 @@ export async function scanProject(directory) {
69
89
  const files = [];
70
90
  const skipped = [];
71
91
  const warnings = [];
92
+ const notable = [];
93
+ const note = (relative, base) => {
94
+ const lower = base.toLowerCase();
95
+ if (MEDIA_EXTENSIONS.has(path.extname(lower))) notable.push({ name: relative, reason: "media" });
96
+ else if (lower.startsWith(".env") && !ALLOWED_DOTFILES.has(lower)) notable.push({ name: relative, reason: "env" });
97
+ };
72
98
  let total = 0;
73
99
  async function walk(current, prefix) {
74
100
  const entries = (await readdir(current, { withFileTypes: true })).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
@@ -84,14 +110,14 @@ export async function scanProject(directory) {
84
110
  const targetInfo = await stat(target);
85
111
  if (targetInfo.isDirectory()) { warnings.push(`Skipped symlinked directory ${name}`); continue; }
86
112
  if (!targetInfo.isFile()) continue;
87
- if (isExcludedFile(entry.name) || isExcludedFile(path.basename(target))) { skipped.push(name); continue; }
113
+ if (isExcludedFile(entry.name, name) || isExcludedFile(path.basename(target), name)) { skipped.push(name); note(name, entry.name); continue; }
88
114
  total += targetInfo.size;
89
115
  files.push({ name, absolute: target, size: targetInfo.size, mode: targetInfo.mode });
90
116
  } else if (info.isDirectory()) {
91
- if (isExcludedDirectory(entry.name)) { skipped.push(`${name}/`); continue; }
117
+ if (isExcludedDirectory(entry.name)) { skipped.push(`${name}/`); if (entry.name.startsWith(".") && !TOOL_DOT_DIRECTORIES.has(entry.name.toLowerCase()) && !/^\.?venv/i.test(entry.name)) notable.push({ name: `${name}/`, reason: "hidden-folder" }); continue; }
92
118
  await walk(absolute, name);
93
119
  } else if (info.isFile()) {
94
- if (isExcludedFile(entry.name)) { skipped.push(name); continue; }
120
+ if (isExcludedFile(entry.name, name)) { skipped.push(name); note(name, entry.name); continue; }
95
121
  total += info.size;
96
122
  files.push({ name, absolute, size: info.size, mode: info.mode });
97
123
  }
@@ -101,7 +127,7 @@ export async function scanProject(directory) {
101
127
  }
102
128
  await walk(root, "");
103
129
  if (!files.length) throw new Error("No deployable files found after exclusions");
104
- return { root, files, skipped, warnings };
130
+ return { root, files, skipped, warnings, notable };
105
131
  }
106
132
 
107
133
  /** Package a project directory into an in-memory ZIP and hash it. */
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.1" }, instructions: INSTRUCTIONS };
49
+ return { protocolVersion, capabilities: { tools: { listChanged: false } }, serverInfo: { name: "vibingview", version: "0.1.3" }, instructions: INSTRUCTIONS };
50
50
  }
51
51
  if (message.method === "ping") return {};
52
52
  if (message.method === "tools/list") {