feat: set up a project on add (scaffold + analyze with Claude), observably - #278
Conversation
The scaffold + repo-detection + headless-Claude-run helpers that `webmux init` uses (buildStarterTemplate, detectInitProjectContext, buildInitPromptSpec, buildInitAgentCommand, runInitAgentCommand, stream parsing) move from bin/src/init-helpers.ts to backend/src/services/init-authoring.ts so the server can drive the same flow for an upcoming "set up project on add" feature. `bin/src/init.ts` now imports them from the backend. Generic process/git/repo primitives (run, which, getGitRoot, detectProjectName, RunResult) move to backend/src/lib/shell.ts and are re-exported from bin/src/shared.ts, so every existing `./shared` consumer is unchanged and the backend can share them without a backend→bin import. Test moves alongside the code into backend/src/__tests__. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vably
Adding a repo that has no .webmux.yaml now runs setup automatically instead
of registering an unconfigured project: scaffold a starter .webmux.yaml →
analyze the repo with Claude to fill it in → register. Each phase is surfaced
in the dashboard and the CLI ("Creating .webmux.yaml" → "Analyzing project
structure with Claude" → ready).
Backend:
- ProjectInitTracker + runProjectInit (services/project-init-service.ts):
hub-level, keyed by repo path; phases creating_config → analyzing → ready,
or failed. Analysis is best-effort (skipped if no agent on PATH, non-fatal
on error) so the starter config always ships. Terminal states TTL-expire.
- apiAddProject: repos with config (or already served) register immediately;
repos without kick off the async job and return { initializing: true }.
New GET /api/projects/init reports progress. The setup uses the lifted
init-authoring helpers with a timeout so a hung agent can't stall it.
Contract: AddProjectResponse ({ initializing, path, project }), ProjectInit
phase/state schemas, and the projectInits endpoint.
Frontend: setUpProject() POSTs then polls projectInits, reporting phases;
EmptyProjects + ProjectSwitcher show the steps and open the project when ready.
CLI: `webmux project add` on an unconfigured repo prints the same phases via a
DI-seamed awaitProjectSetup(), then the added line. Parity with the UI.
Tests cover the tracker, orchestration (skip/best-effort/failure), and the CLI
poller; init-authoring gains a run timeout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @hugocasa's task in 2m 50s —— View job Code ReviewReviewed the full diff against A few things worth addressing, mostly minor: 1. Phase label says "with Claude" but the agent may be Codex
On a Codex-only machine the UI/CLI will claim Claude is doing the analysis. Either make the copy agent-neutral ("Analyzing project structure…") or thread the chosen agent through. Low severity but user-visible. Fix this → 2.
|
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
webmux | 0d04707 | Commit Preview URL Branch Preview URL |
Jun 22 2026, 02:43 PM |
Mirrors the CLI awaitProjectSetup coverage on the frontend surface: the immediate (already-a-project) path, the initializing → poll → ready path (resolves with the prefix, reports phases), and the failed path (rejects with the server error). Stubs global fetch since the ts-rest client uses it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Agent-neutral phase label: setup may run Codex (authoringAgent falls back
to it), so "Analyzing project structure with Claude" was wrong on a
Codex-only machine. Drop the agent name from the label + EmptyProjects copy.
- Default the scaffolded config to Claude when neither agent is installed
(was Codex): mirror init's `which("codex") && !which("claude")` rule, so a
no-agent machine writes `defaultAgent: claude`.
- Drop the redundant `creating_config` set in apiAddProject — runProjectInit
sets it synchronously before its first await, so the concurrent-launch
guard still holds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously the "other webmux servers detected" nudge only fired in `webmux project`. Broaden it to the other occasional, server-talking commands (oneshot, linear) by hoisting the check into the webmux.ts dispatch — the registry read is cheap and the warning is silent unless peers actually exist. Deliberately excludes the high-frequency worktree commands (list/open/ send/…), where a per-invocation warning would be noisy, and `project migrate` itself (which does the consolidating). Centralizing the call also removes the now-redundant one in project-commands.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the previous commit, which narrowed the warning to exclude worktree commands. That call was driven by a non-hermetic e2e test (it read the dev machine's real ~/.webmux registry), not by merit — so revert the narrowing: the nudge now fires on every command that reaches a project (oneshot/linear/project + worktree commands), still excluding `project migrate`. It's silent unless other servers exist, transient until you consolidate, and on stderr. Make the webmux.test.ts e2e spawns hermetic by isolating HOME, so the registry read sees an empty dir regardless of what's running locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It's a "you could consolidate" hint, not an error, so color it amber to match the dashboard banner. ANSI only when stderr is a TTY, so piped or redirected output stays plain. Stays on stderr (standard for diagnostics, keeps stdout parseable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @hugocasa's task in 2m 27s —— View job Code Review — PR #278Reviewed the full diff against I also confirmed the three items from the earlier review were applied: agent-neutral phase labels ( A few new things, all low severity — nothing blocking. 1. Concurrent add of the same fresh repo can register with the un-analyzed configThe if (hasProjectConfig(root)) {
const project = manager.add(root); // registers from the STARTER config…
reloadRoutes(); // …while runProjectInit is still analyzing
return jsonResponse({ initializing: false, … });
}
2. A transient poll failure aborts the client flow while setup keeps runningIn both Nits
CoverageThe new tests exercise Overall: solid architecture and error handling. Items 1–2 are quick hardening for the concurrent / flaky-network edges; the rest is cosmetic. |
From the PR review: - apiAddProject: check projectInitTracker.isActive(root) BEFORE hasProjectConfig. Setup writes .webmux.yaml mid-run, so a second concurrent POST for the same repo would otherwise see that half-written config and register from it before analysis finished — bypassing the enriched result and the phase UI. Now it joins the in-flight setup's polling path instead. - setUpProject (frontend) + awaitProjectSetup (CLI): swallow a transient poll failure and retry until the deadline, instead of surfacing "failed" for a setup that's still running fine on the server. - Trim a trailing blank line in project-commands.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The only server-side log the feature added was an error-level line when the best-effort analysis fails — but that path recovers (keeps the starter config and still registers), so it's a warn, not an error. Also the happy path and the terminal-failure path logged nothing, leaving a stuck "analyzing" or a real setup failure invisible in server logs. Now runProjectInit logs: "setting up <root>" at start, "<root> ready as <prefix>" on success, warn when analysis is skipped/fails (recoverable), and error on a genuine (terminal) setup failure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adding a repo that has no
.webmux.yamlnow sets it up automatically instead of registering an unconfigured project, and the setup is observable in both the dashboard and the CLI:.webmux.yaml— scaffold the starter config (the same templatewebmux initwrites).Repos that already have a
.webmux.yaml(or are already served) register immediately, unchanged.How it works
ProjectInitTracker+runProjectInit(backend/src/services/project-init-service.ts): a hub-level tracker keyed by repo path drivescreating_config → analyzing → ready(orfailed). Analysis is best-effort — skipped if no agent is on PATH, non-fatal on error — so the starter config always ships. Terminal states expire via TTL so the map can't grow unbounded.apiAddProject: repos with config register immediately and return{ initializing: false, project }; repos without kick off the async setup job and return{ initializing: true, path }. NewGET /api/projects/initreports progress; the client polls it.webmux inituses moved frombin/src/init-helpers.tstobackend/src/services/init-authoring.ts(with the genericrun/which/getGitRoot/detectProjectNameprimitives inbackend/src/lib/shell.ts, re-exported frombin/src/shared.ts), so the server and the CLI share one implementation.runInitAgentCommandgained an optional timeout so a hung agent can't stall setup.setUpProject()POSTs then pollsprojectInits, reporting each phase;EmptyProjectsand the project switcher show the steps and navigate to the project once ready.webmux project addon an unconfigured repo prints the same phases (via a DI-seamed, testedawaitProjectSetup), then the added line.Observability choice
Phase-level progress over the existing poll pattern (matching worktree creation) rather than a live token stream — the project registers only once setup finishes, which avoids rebuilding a live project's runtime/sockets mid-flight.
Security / trust model
Setup spawns a headless agent (
claude … --permission-mode bypassPermissions/codex … --sandbox workspace-write) with the added repo as cwd, triggered byPOST /api/projects. The dashboard already grants arbitrary shell via terminal panes to anyone who can reach it, so it assumes a trusted/localhost network — this on-add agent run stays within that existing model and does not add a new exposure class. (Bun.servebinds all interfaces; deployments are expected to be localhost or a trusted LAN / behind SSH, as today.)Also in this branch: broadened migration nudge
Tangential to setup-on-add but committed here. The "other webmux servers detected → run
webmux project migrate" hint (from #271, which consolidated the old one-server-per-project model into a single dashboard) wasproject-only; it now fires on every command that reaches a project (oneshot/linear/project+ worktree commands), exceptproject migrateitself. It's a cheap local registry read, silent unless other servers exist, transient until you consolidate, on stderr, and amber (TTY only — plain when piped) to read as a nudge, not an error. The point is to push anyone still on the old per-project-server setup to update + consolidate.Changes
AddProjectResponse,ProjectInitPhase/ProjectInitStateschemas,projectInitsendpoint;addProjectnow returnsAddProjectResponse.project-init-service.ts(new) + wiring inserver.ts;init-authoring.tslift + run timeout;lib/shell.ts(new).setUpProject+projectInitPhaseLabel;EmptyProjects/ProjectSwitcherprogress UI.webmux project addphase output; broadened + amber migration nudge in thewebmux.tsdispatch.Review polish (applied)
defaultAgentfalls back toclaude(notcodex) when neither agent is installed.creating_configtracker write inapiAddProject.Test plan
bun run test— backend 479, contract 4, bin 176, frontend 140;tsc --noEmit(backend) +svelte-checkclean;bun run buildok..webmux.yamlfrom the dashboard → registers immediately, opens..webmux.yaml→ watch "Creating .webmux.yaml" → "Analyzing project structure" → opens;.webmux.yamlis written and fleshed out.webmux project add <repo-without-config>prints the same phases.claude/codexnot on PATH → setup skips analysis and still registers with the starter config.webmux project ls(and oneshot/linear/worktree commands) print the amber consolidation nudge on stderr; piping it shows plain text;project migratedoes not print it.Generated with Claude Code