@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 +4 -2
- package/package.json +4 -4
- package/src/cli.mjs +34 -12
- package/src/lib/project.mjs +35 -9
- package/src/mcp.mjs +1 -1
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
|
|
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.
|
|
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": "
|
|
9
|
-
"vibingview": "
|
|
10
|
-
"vibingview-mcp": "
|
|
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.
|
|
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
|
-
|
|
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}.
|
|
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.
|
|
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}.
|
|
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}.
|
|
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}`}.
|
|
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";
|
package/src/lib/project.mjs
CHANGED
|
@@ -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
|
-
//
|
|
31
|
-
".
|
|
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
|
-
/**
|
|
43
|
-
export function
|
|
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
|
-
*
|
|
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.
|
|
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") {
|