diff --git a/vibn-frontend/app/api/chat/route.ts b/vibn-frontend/app/api/chat/route.ts index b99d7cd9..cc9ddb1a 100644 --- a/vibn-frontend/app/api/chat/route.ts +++ b/vibn-frontend/app/api/chat/route.ts @@ -219,9 +219,9 @@ ${activeProject.kickoff ? `- Created via: ${activeProject.kickoff.mode} (${JSON. ${decisionsBlock}${tasksBlock}${ideasBlock}${designKitBlock ? `\n${designKitBlock}\n` : ""}${codebaseBlock} When you call tools that take a \`projectId\`, USE this id (\`${activeProject.id}\`) without asking. When the user says "this project" / "the app" / "deploy it" — they mean THIS project. Switch to a different project only if the user names one explicitly. -**Project repo is auto-cloned at \`/workspace/${activeProject.slug ?? ""}/\` inside the dev container.** That path is the project's Gitea repo. ALL code, docs, configs, and other artifacts you intend the user to see in the Product tab MUST live under that path. Anything you write outside it (e.g. \`/workspace/scratch\`, \`/workspace/some-cloned-other-repo\`) is treated as scratch and is invisible in the UI. + **Project repo is auto-cloned at \`/workspace/\` inside the dev container.** That path is the project's Gitea repo. ALL code, docs, configs, and other artifacts you intend the user to see in the Product tab MUST live under that path. Anything you write outside it (e.g. \`/workspace/scratch\`, \`/workspace/some-cloned-other-repo\`) is treated as scratch and is invisible in the UI. -After every assistant turn, the harness automatically runs \`git add -A && git commit && git push\` against \`/workspace/${activeProject.slug ?? ""}/\`. You do NOT need to commit manually unless the user asks for a specific commit message or you want to checkpoint mid-turn. Don't apologize for "forgetting to commit" — the harness handles it.\n` + After every assistant turn, the harness automatically runs \`git add -A && git commit && git push\` against \`/workspace/\`. You do NOT need to commit manually unless the user asks for a specific commit message or you want to checkpoint mid-turn. Don't apologize for "forgetting to commit" — the harness handles it.\n` : ""; return `You are Vibn AI — the technical co-founder of every Vibn user. You turn ideas into shipped software. Treat their projects like they're your own. @@ -303,7 +303,7 @@ Each project has a persistent \`vibn-dev\` container. Edit files via \`fs_*\` an **Dev servers (preview URL via \`*.preview.vibnai.com\` wildcard):** - \`dev_server_start { projectId, command, port: 3000 }\` is a **one-shot** call. It kills old processes on the port, checks the port is free, sets HOST=0.0.0.0 + PORT, launches your command, and returns a clickable \`previewUrl\`. Do NOT pre-flight with \`devcontainer_status\`, \`fs_list\`, \`dev_server_logs\`, or manual \`shell_exec\` kills — the function handles all of that. Just call it. The error tells you what to fix: \`PORT_BUSY\` → pick 3001–3009; \`npm: command not found\` → project needs \`npm install\` first. - **Port:** The primary frontend service MUST ALWAYS be bound to port \`3000\`. Do not use any other port for the user-facing UI. If you are spinning up secondary services (like an API or Storybook) alongside it, you may bind them to ports \`3001–3009\`, but port \`3000\` is reserved exclusively for the primary visual preview. -- **Directory:** The command runs from the root \`/workspace\` directory, but your project code is inside \`/workspace/\${activeProject.slug ?? ""}/\`. You MUST \`cd\` into your project folder first! Example: \`command: "cd \${activeProject.slug ?? ""} && npm run dev"\`. + - **Directory:** The command runs from the root \`/workspace\` directory. Cwd is automatically set to \`/workspace\`. You do NOT need to run \`cd\` commands. Example: \`command: \"npm run dev\"\`. - \`dev_server_stop\` / \`dev_server_list\` / \`dev_server_logs\` — use only AFTER a failed start, and only to diagnose the error the function returned. Never on success. **Verify the page actually renders:** diff --git a/vibn-frontend/app/api/mcp/route.ts b/vibn-frontend/app/api/mcp/route.ts index 03e10726..4ef4c0a1 100644 --- a/vibn-frontend/app/api/mcp/route.ts +++ b/vibn-frontend/app/api/mcp/route.ts @@ -4576,7 +4576,7 @@ function normalizeFsPath( p = p.substring("getacquired-2-0/".length); } - const projectRoot = projectSlug ? `${FS_ROOT}/${projectSlug}` : FS_ROOT; + const projectRoot = FS_ROOT; let abs: string; if (p.startsWith("/")) { abs = p; diff --git a/vibn-frontend/lib/dev-container-git.ts b/vibn-frontend/lib/dev-container-git.ts index a69584e8..a4e39d5a 100644 --- a/vibn-frontend/lib/dev-container-git.ts +++ b/vibn-frontend/lib/dev-container-git.ts @@ -34,21 +34,21 @@ * widen it. */ -import { execInDevContainer } from '@/lib/dev-container'; +import { execInDevContainer } from "@/lib/dev-container"; -const GITEA_API_URL = process.env.GITEA_API_URL ?? ''; +const GITEA_API_URL = process.env.GITEA_API_URL ?? ""; // Falls back to GITEA_ADMIN_USER because production historically only set the // admin var; missing GITEA_USERNAME used to silently disable auto-clone. const GITEA_USERNAME = - process.env.GITEA_USERNAME || process.env.GITEA_ADMIN_USER || ''; -const GITEA_API_TOKEN = process.env.GITEA_API_TOKEN ?? ''; + process.env.GITEA_USERNAME || process.env.GITEA_ADMIN_USER || ""; +const GITEA_API_TOKEN = process.env.GITEA_API_TOKEN ?? ""; -const AI_GIT_AUTHOR_NAME = 'Vibn AI'; -const AI_GIT_AUTHOR_EMAIL = 'ai@vibnai.com'; +const AI_GIT_AUTHOR_NAME = "Vibn AI"; +const AI_GIT_AUTHOR_EMAIL = "ai@vibnai.com"; /** Where each project's repo lives inside the dev container. */ export function projectRepoPath(projectSlug: string): string { - return `/workspace/${projectSlug}`; + return `/workspace`; } function isGiteaConfigured(): boolean { @@ -100,10 +100,14 @@ export async function ensureProjectRepoCloned( opts: EnsureRepoClonedOpts, ): Promise { if (!isGiteaConfigured()) { - return { cloned: false, alreadyPresent: false, reason: 'gitea_not_configured' }; + return { + cloned: false, + alreadyPresent: false, + reason: "gitea_not_configured", + }; } if (!opts.giteaCloneUrl) { - return { cloned: false, alreadyPresent: false, reason: 'no_clone_url' }; + return { cloned: false, alreadyPresent: false, reason: "no_clone_url" }; } const repoDir = projectRepoPath(opts.projectSlug); @@ -121,18 +125,18 @@ export async function ensureProjectRepoCloned( const probe = await execInDevContainer({ projectId: opts.projectId, command: - `if [ -d ${shellQ(repoDir + '/.git')} ]; then echo git; ` + + `if [ -d ${shellQ(repoDir + "/.git")} ]; then echo git; ` + `elif [ -d ${shellQ(repoDir)} ]; then echo dir; ` + `else echo absent; fi`, timeoutMs: 5_000, }); const probeState = probe.stdout.trim(); - if (probeState === 'git') { + if (probeState === "git") { return { cloned: false, alreadyPresent: true }; } - if (probeState === 'dir') { + if (probeState === "dir") { // Init in place, hook up the remote, fetch, set tracking. We // don't try to merge — the directory's contents become whatever // the next auto-commit-and-push picks up. If the remote has @@ -147,7 +151,7 @@ export async function ensureProjectRepoCloned( // Best-effort fetch; if remote is empty this errors out and // we proceed anyway. The `|| true` keeps the chain going. `(git fetch origin main 2>/dev/null && git reset --soft origin/main 2>/dev/null) || true`, - ].join(' && '); + ].join(" && "); const result = await execInDevContainer({ projectId: opts.projectId, @@ -182,7 +186,7 @@ export async function ensureProjectRepoCloned( `git config user.email ${shellQ(AI_GIT_AUTHOR_EMAIL)}`, `git remote set-url origin ${shellQ(authed)}`, `cd /workspace && mv ${shellQ(tmpDir)} ${shellQ(repoDir)}`, - ].join(' && '); + ].join(" && "); const result = await execInDevContainer({ projectId: opts.projectId, @@ -201,7 +205,11 @@ export async function ensureProjectRepoCloned( // (warning: "remote HEAD refers to nonexistent ref"), clone // didn't actually fail — just there's no history to check // out. Make an empty dir + init so subsequent commits land. - if (/empty|warning: You appear to have cloned an empty/i.test(result.stderr + result.stdout)) { + if ( + /empty|warning: You appear to have cloned an empty/i.test( + result.stderr + result.stdout, + ) + ) { const initEmpty = [ `mkdir -p ${shellQ(repoDir)}`, `cd ${shellQ(repoDir)}`, @@ -209,7 +217,7 @@ export async function ensureProjectRepoCloned( `git config user.name ${shellQ(AI_GIT_AUTHOR_NAME)}`, `git config user.email ${shellQ(AI_GIT_AUTHOR_EMAIL)}`, `git remote add origin ${shellQ(authed)}`, - ].join(' && '); + ].join(" && "); const initR = await execInDevContainer({ projectId: opts.projectId, command: initEmpty, @@ -260,21 +268,21 @@ export async function commitAndPushIfDirty( opts: CommitAndPushOpts, ): Promise { const repoDir = projectRepoPath(opts.projectSlug); - const message = (opts.message ?? '').trim() || 'AI checkpoint'; + const message = (opts.message ?? "").trim() || "AI checkpoint"; // Sanitize: keep messages single-line and bounded so we can't be // tricked into shell-escape or commit-message injection. - const safeMessage = message.replace(/[\r\n]+/g, ' ').slice(0, 200); + const safeMessage = message.replace(/[\r\n]+/g, " ").slice(0, 200); // Bail fast if there's no repo to commit against. Don't treat as // an error — projects without a clone (e.g. cloning failed earlier) // should still be able to chat. const probe = await execInDevContainer({ projectId: opts.projectId, - command: `test -d ${shellQ(repoDir + '/.git')} && echo present || echo absent`, + command: `test -d ${shellQ(repoDir + "/.git")} && echo present || echo absent`, timeoutMs: 5_000, }).catch(() => null); - if (!probe || probe.stdout.trim() !== 'present') { - return { committed: false, pushed: false, reason: 'no_repo' }; + if (!probe || probe.stdout.trim() !== "present") { + return { committed: false, pushed: false, reason: "no_repo" }; } const cmd = [ @@ -287,21 +295,24 @@ export async function commitAndPushIfDirty( `SHA=$(git rev-parse --short HEAD)`, `git push -u origin HEAD 2>&1 | tail -n 5`, `echo COMMITTED $SHA`, - ].join(' && '); + ].join(" && "); const result = await execInDevContainer({ projectId: opts.projectId, command: cmd, timeoutMs: 30_000, - }).catch((err: unknown) => ({ - stdout: '', - stderr: err instanceof Error ? err.message : String(err), - exitCode: -1, - } satisfies { stdout: string; stderr: string; exitCode: number })); + }).catch( + (err: unknown) => + ({ + stdout: "", + stderr: err instanceof Error ? err.message : String(err), + exitCode: -1, + }) satisfies { stdout: string; stderr: string; exitCode: number }, + ); - const out = result.stdout || ''; - if (out.includes('CLEAN')) { - return { committed: false, pushed: false, reason: 'clean' }; + const out = result.stdout || ""; + if (out.includes("CLEAN")) { + return { committed: false, pushed: false, reason: "clean" }; } const m = /COMMITTED\s+([0-9a-f]+)/.exec(out); if (m) {