Repository navigation
Project foundations + mcp install (C1) - #44
thedogwiththedataonit wants to merge 8 commits into
Conversation
40df837 to
df53432
Compare
| const crlf = usesCrlf(existing) | ||
| return this.#commit(absolute, { | ||
| path, | ||
| action: 'updated', | ||
| sha256: sha256Hex(next), | ||
| content: withEol(next, crlf), |
There was a problem hiding this comment.
Block edits change other lines
upsertBlock changes line endings outside its managed block when a file mixes LF and CRLF. applyBlock normalizes the whole file, then withEol changes every line to CRLF if any original line used CRLF. This creates unrelated edits and breaks the helper's promise to leave surrounding text untouched. Preserve the original text outside the block.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/project/fs-writer.ts
Line: 473-478
Comment:
**Block edits change other lines**
`upsertBlock` changes line endings outside its managed block when a file mixes LF and CRLF. `applyBlock` normalizes the whole file, then `withEol` changes every line to CRLF if any original line used CRLF. This creates unrelated edits and breaks the helper's promise to leave surrounding text untouched. Preserve the original text outside the block.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| const isNowIgnored = | ||
| change.action === 'kept-user-version' || planned === 'unbalanced' | ||
| ? false | ||
| : writer.dryRun | ||
| ? matchesGitignore(planned, path) | ||
| : await check(path) |
There was a problem hiding this comment.
Ignore plans report false success
ensureIgnored reports a path as added during a dry run even when the writer refuses the edit. A .gitignore link pointing outside the project returns refused, but this branch still checks the proposed text and reports success. The plan therefore disagrees with the real run. Treat refused as a conflict before checking the planned rules.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/project/gitignore.ts
Line: 228-233
Comment:
**Ignore plans report false success**
`ensureIgnored` reports a path as `added` during a dry run even when the writer refuses the edit. A `.gitignore` link pointing outside the project returns `refused`, but this branch still checks the proposed text and reports success. The plan therefore disagrees with the real run. Treat `refused` as a conflict before checking the planned rules.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| const inline = /^packages\s*:\s*\[(.*)\]\s*$/.exec(line) | ||
| if (inline !== null) { | ||
| for (const item of (inline[1] ?? '').split(',')) { |
There was a problem hiding this comment.
Workspace lists lose their folders
parsePnpmWorkspaceYaml silently drops a valid bracket list split across lines, such as packages: ["services/*", followed by "modules/*"]. It reads bracket lists only when they end on the same line; later entries must start with -. findMonorepo then substitutes conventional folders, losing the actual workspace and app candidates. Read multi-line bracket lists or report that this form is unsupported instead of substituting other folders.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/project/monorepo.ts
Line: 131-133
Comment:
**Workspace lists lose their folders**
`parsePnpmWorkspaceYaml` silently drops a valid bracket list split across lines, such as `packages: ["services/*",` followed by `"modules/*"]`. It reads bracket lists only when they end on the same line; later entries must start with `-`. `findMonorepo` then substitutes conventional folders, losing the actual workspace and app candidates. Read multi-line bracket lists or report that this form is unsupported instead of substituting other folders.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.df53432 to
1620c51
Compare
| const child = nodeSpawn(command, [...args], { | ||
| cwd: options.cwd, | ||
| shell: false, |
There was a problem hiding this comment.
defaultSpawn passes commands directly to nodeSpawn with shell: false. If Windows provides npm through the usual npm.cmd file, this cannot start it. PR #55's src/lib/init/pipeline.ts passes the npm command from addCommand straight to this helper, so init fails to install the SDK and leaves its generated imports without their dependency.
Add a Windows-safe way to run these command files while preserving argument quoting.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/project/process.ts
Line: 36-38
Comment:
**Windows SDK installs fail**
`defaultSpawn` passes commands directly to `nodeSpawn` with `shell: false`. If Windows provides npm through the usual `npm.cmd` file, this cannot start it. PR #55's `src/lib/init/pipeline.ts` passes the `npm` command from `addCommand` straight to this helper, so `init` fails to install the SDK and leaves its generated imports without their dependency.
Add a Windows-safe way to run these command files while preserving argument quoting.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| for (const item of (inline[1] ?? '').split(',')) { | ||
| const value = unquote(item.trim()) |
There was a problem hiding this comment.
parsePnpmWorkspaceYaml splits quoted patterns at every comma. A valid list such as packages: ['{apps,packages}/*'] becomes two broken patterns before expandBraces sees it, so neither folder yields any workspace packages. resolveProjectRoot then loses the current workspace and its app candidates, including for the init caller in PR #55.
Split only between YAML list items, not inside quoted strings.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/project/monorepo.ts
Line: 133-134
Comment:
**Workspace folders disappear**
`parsePnpmWorkspaceYaml` splits quoted patterns at every comma. A valid list such as `packages: ['{apps,packages}/*']` becomes two broken patterns before `expandBraces` sees it, so neither folder yields any workspace packages. `resolveProjectRoot` then loses the current workspace and its app candidates, including for the `init` caller in PR #55.
Split only between YAML list items, not inside quoted strings.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.cb49869 to
d599c0d
Compare
| 'add', | ||
| ...(dev ? ['--save-dev'] : []), | ||
| ...(exact ? ['--save-exact'] : []), | ||
| ...(ws === undefined ? [] : ['--filter', ws.name]), | ||
| ...packages, |
There was a problem hiding this comment.
When no workspace is selected, addCommand produces pnpm add <package>. At a pnpm workspace root, pnpm refuses this without --workspace-root.
PR #55's planInstall reaches this case when the root is the selected app, including init --app .. SDK installation then fails, leaving generated imports without their dependency. Add an explicit root-workspace option and have that caller pass it.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/project/package-manager.ts
Line: 260-264
Comment:
**pnpm root installs fail**
When no workspace is selected, `addCommand` produces `pnpm add <package>`. At a pnpm workspace root, pnpm refuses this without `--workspace-root`.
PR #55's `planInstall` reaches this case when the root is the selected app, including `init --app .`. SDK installation then fails, leaving generated imports without their dependency. Add an explicit root-workspace option and have that caller pass it.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.main released 0.17.0 and renamed its Unreleased section; this PR's items stay under a new Unreleased heading. Also drop the unpublished launcher name from a confirm.ts comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d599c0d to
37325ff
Compare
src/lib/project/ holds the building blocks the coming `init` and
`migrate` commands share, with no new runtime dependency:
- package-manager: npm, pnpm, yarn classic vs berry (PnP from
.yarnrc.yml), bun (bun.lock / bun.lockb), deno; packageManager pin,
then lockfiles, then npm_config_user_agent; add/install/exec/dlx
command builders with workspace filters.
- monorepo: pnpm-workspace.yaml, package.json workspaces, Turborepo,
Nx; app candidates by framework dependency; resolveProjectRoot.
- fs-writer: create-only files (created, unchanged, kept-user-version,
missing), sha256 managed-file hashes, marked-block upserts, CRLF
tolerant, atomic temp+rename writes, file modes, dry-run plans with
unified diffs.
- env-file: dotenv round-trips byte for byte; ensureVar only adds or
fills, never replaces or returns a value, and refuses secrets under
NEXT_PUBLIC_, VITE_, PUBLIC_, EXPO_PUBLIC_.
- state: brew.json read/validate/write with a stable key order, plus
the JSON Schema in schemas/brew.json.
- gitignore: git check-ignore through the spawn seam, fallback parser,
managed block.
- diff: a small unified line diff.
- vendor-http: third-party fetch bounded like rawFetch, never recorded
in ctx.transport, VENDOR_ERROR {vendor, status} with credentials
redacted, bounded retry-after on 429.
CliIo gains optional spawn, openUrl and cwd seams; tests run every
library against fixture projects in bounded temp directories.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getInvocationPrefix() reads BREW_CLI_INVOKED_AS, which launcher packages (`npx -y brew.new@<version>`) will set, and defaults to `brew-cli`. confirmCommand, the confirmation envelope's command, retryCommand, and every "run ..." / "see ..." hint (missing key, doctor, types --check, preview polling, usage errors) now start with it, so an agent re-runs through the same launcher instead of a brew-cli binary that is not on the PATH. Anything other than plain shell words falls back to brew-cli. The manifest lists the variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndsurf, Codex `brew-cli mcp install` merges a `brew` server entry into each client's config and keeps every other server: .cursor/mcp.json, .vscode/mcp.json, .mcp.json, .gemini/settings.json (project scope, the default) or their user-scope files; Windsurf (~/.codeium/windsurf/mcp_config.json) and Codex (~/.codex/config.toml, appended once) only when named. Without --client it uses the clients the project shows (.cursor/, .vscode/, .claude/ or CLAUDECODE=1, .gemini/). --auth oauth writes the URL only; --auth env adds an Authorization header each client expands from BREW_API_KEY, never a literal key. A config with comments is reported `manual` with the snippet; --print and --dry-run write nothing. The MCP URL follows the --api-url origin. Cursor's deeplink is byte-identical to brew-v2 buildCursorInstallUrl; VS Code's is vscode:mcp/install?<json>. Also: scripts/generate-skills.ts embeds skills/**/SKILL.md into src/generated/skills.ts (generate:skills:check in CI, release and `check`); the brew-cli skill documents mcp install and drops its stale command count; npm keywords for MCP, transactional/auth email, Resend and React Email; the package ships schemas/; README section, CHANGELOG Unreleased entry, regenerated docs/commands/README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ses)
- mcp install: a Codex config that already defines mcp_servers.brew in
any other form (quoted table names, dotted keys, sub-tables, an
[mcp_servers] table or inline table with `brew =`) is reported
`manual` instead of getting a second, config-breaking table.
- fs-writer: a symbolic link (a dotfiles-managed config) is written
through to its target and survives; `sensitive` writes and block
upserts carry no diff, so a dry-run plan never prints a secret.
- env-file: `KEY= # todo` reads as empty and gets filled; each line
keeps its own ending, so mixed LF/CRLF files round-trip byte for byte.
- vendor-http: credentials are redacted before the 300-character cut, so
no fragment survives it.
- gitignore: ensureIgnored re-checks after editing and reports paths a
later rule still re-includes as `conflicts`, never as added.
- schemas/brew.json: contractsPath and files keys use the same path rule
as the CLI's validator (tested against it).
- monorepo: brace patterns (`{apps,packages}/*`) expand in workspace
globs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A comment or string that merely mentions mcp_servers.brew (`# … was removed`, a commented-out header, `notes = "…"`, a multi-line array or string) no longer reads as an existing definition: the checks run on the TOML structure with comments stripped and string values blanked, quoted keys kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…evin Desktop path, symlink containment)
mcp install
- Claude Code user scope: `--header` is variadic in `claude mcp add`, so it
now follows the name and URL (before them it swallowed both: "missing
required argument 'name'"). Verified against claude 2.1.212.
- Codex: the regex checks are replaced by a small TOML key reader over the
comment- and value-blanked structure. An inline or empty
`mcp_servers = {...}`, `[[mcp_servers]]`, `brew.url = ...` under
`[mcp_servers]`, or a `[mcp_servers.brew.*]` sub-table now comes back
`manual` with the snippet (each shape checked against `codex mcp list`);
dotted root keys (`mcp_servers.docs.url = ...`) still take the table.
Unreadable TOML is `manual`, never appended to.
- Codex OAuth results carry `nextStep: "codex mcp login brew"`.
- Windsurf is now Devin Desktop: its config is
`$XDG_CONFIG_HOME/devin/mcp_config.json` (`~/.config/devin`,
`%APPDATA%\devin` on Windows) per docs.devin.ai, read by both its agents,
and the entry uses `url` (Devin Local documents only `url`). The legacy
`~/.codeium/windsurf` path is gone: the docs no longer mention it.
`.devin/` is detected beside `.windsurf/`.
- An existing `brew` entry is never replaced: equal is `unchanged`, other
settings (an env header, a stale URL) are `manual` with the snippet.
- JSON configs that start with a UTF-8 BOM are parsed, and keep the mark.
- No package.json up to the repo: the project is the git root
(`git rev-parse --show-toplevel` through the spawn seam), not the cwd.
project foundations
- fs-writer resolves where a write really lands: a symbolic link (or a
linked parent directory) whose target leaves the project root, or the
home directory for user configs, is `refused`; so is a dangling link,
and a secret is never written through any link. A home config linked
into ~/dotfiles still works.
- An unchanged file with a looser mode than requested (0644 .env.local)
is tightened to 0600 and the change reports `permissions`.
- env-file: an unterminated quote reads as one unquoted line (as dotenv
does), `KEY: value` is an assignment, and a line that names the
variable inside text the parser cannot read yields `needs-review`
instead of a second BREW_API_KEY. No value is ever returned.
- pnpm-workspace.yaml comments at column 0 no longer end the package list;
package.json, brew.json and lockfile JSON are read past a BOM.
- BREW_CLI_INVOKED_AS accepts only npx, npx -y, pnpm dlx, bunx, yarn dlx,
npm exec or npm create followed by one package spec (or brew-cli).
Tests never spawn host programs: run-cli's default spawn fails as if the
program were missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The npm names `brew.new` and `create-brew` are not ours yet, so README, the brew-cli skill, `docs --agent` and the changelog show the package-runner prefix with `@brew.new/cli`, which is on npm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main released 0.17.0 and renamed its Unreleased section; this PR's items stay under a new Unreleased heading. Also drop the unpublished launcher name from a confirm.ts comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
37325ff to
8b352aa
Compare
| function segmentRegex(segment: string): RegExp { | ||
| const body = segment | ||
| .replace(/[.+^${}()|[\]\\]/g, '\\$&') | ||
| .replaceAll('*', '[^/]*') | ||
| .replaceAll('?', '[^/]') |
There was a problem hiding this comment.
Setup targets the wrong package
expandGlob misses valid workspace patterns such as apps/[ab]* because segmentRegex escapes the brackets instead of matching either character. Patterns containing only a character choice also take the literal-directory branch.
This loses the current workspace and its app candidates. PR #55's detectProject then falls back to the monorepo root, so running init inside an app can generate files and install dependencies in the wrong package. Support character choices, or return a clear unsupported-pattern error instead of silently finding no matches.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/project/monorepo.ts
Line: 207-211
Comment:
**Setup targets the wrong package**
`expandGlob` misses valid workspace patterns such as `apps/[ab]*` because `segmentRegex` escapes the brackets instead of matching either character. Patterns containing only a character choice also take the literal-directory branch.
This loses the current workspace and its app candidates. PR #55's `detectProject` then falls back to the monorepo root, so running `init` inside an app can generate files and install dependencies in the wrong package. Support character choices, or return a clear unsupported-pattern error instead of silently finding no matches.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
First CLI step of the DX push (plan wave 0, "brew-cli C1"): the libraries
initandmigratewill stand on, a launcher-aware invocation prefix, and a purely localbrew-cli mcp install.brew-cli mcp installbrewentry into each client's MCP config and keeps every other server and setting (indentation too):--scope user).cursor/mcp.json{mcpServers:{brew:{url}}}~/.cursor/mcp.json.vscode/mcp.json{servers:{brew:{type:"http",url}}}mcp.json.mcp.json{mcpServers:{brew:{type:"http",url}}}claude mcp add --scope user ….gemini/settings.json{mcpServers:{brew:{httpUrl}}}~/.gemini/settings.json~/.config/devin/mcp_config.json{url}($XDG_CONFIG_HOME,%APPDATA%\devinon Windows)~/.codex/config.toml[mcp_servers.brew](appended once,$CODEX_HOMEhonoured).cursor/,.vscode/,.claude/orCLAUDECODE=1,.gemini/. If.windsurf/,.devin/,.codex/orCODEX_*shows up, that client is reportedmanualwith its snippet and written only when you name it. If no client is found, the command exits 2.--auth oauth(default) writes the URL only.--auth envaddsAuthorization: Bearer ${env:BREW_API_KEY}(Cursor, VS Code, Windsurf),Bearer ${BREW_API_KEY}(Claude Code, Gemini) or Codex'sbearer_token_env_var. It never writes a literal key, and a test checks that.manualplus a snippet.--printand--dry-runwrite nothing.--api-urlorigin +/api/mcp. The Cursor deeplink is byte-identical to brew-v2lib/mcp/handoff.ts#buildCursorInstallUrl(base64url, unpadded). The VS Code deeplink isvscode:mcp/install?<encodeURIComponent(JSON)>.{ mcpUrl, dryRun, clients: [{ client, path?, action: created|updated|unchanged|manual|printed, deeplink?, snippet?, note?, nextStep? }] }.mcpUrl,dryRun,snippet,noteandnextStep(codex mcp login brewafter a Codex OAuth entry) are additions to the planned shape.brewentry is never replaced: equal settings reportunchanged, other settings come backmanualwith the snippet. A config that is a symbolic link is written through only when its target stays in the project (or the home directory for user files).write,sdkMethod: null, no route, noderivedFrom, the same asconfig. Registry invariants and both parity sentinels pass with no new skips.src/lib/project/(no new runtime dependency)package-manager, monorepo, fs-writer (create-only, managed hashes, block upsert, CRLF, atomic writes, modes, dry-run plans with diffs), env-file (round-trips byte for byte; adds or fills, never replaces or prints a value; refuses
NEXT_PUBLIC_/VITE_/PUBLIC_/EXPO_PUBLIC_for secrets), state (brew.jsonwith hand validation, stable order, unknown keys preserved; JSON Schema shipped inschemas/brew.jsonat the$schemaURL), gitignore (git check-ignore -qthrough the spawn seam, plus a fallback parser), diff, vendor-http (bounded likerawFetch, never recorded inctx.transport,VENDOR_ERROR {vendor,status}with credentials redacted, exits 1, boundedretry-afteron 429).CliIogains optionalspawn,openUrlandcwd.Invocation prefix
getInvocationPrefix(env)readsBREW_CLI_INVOKED_ASand falls back tobrew-clifor anything that isn't plain shell words. It now leadsconfirmCommand, the confirm envelope'scommand,retryCommand, and the "run … login",types --checkand preview-poll hints, plus the usage-error "see…" hints.Skills, metadata, docs
scripts/generate-skills.tsembedsskills/**/SKILL.mdintosrc/generated/skills.ts.generate:skills:checkruns in CI, release andbun run check.brew-cliskill documentsmcp installand no longer hard-codes a command count.mcp,model-context-protocol,transactional-email,resend,resend-alternative,react-email,ai-agent,agents,auth-emails(emailwas already present).filesnow includesschemas.docs/commands/README.md.Review round 1 (Greptile, commit 5912641)
Fixes for all 8 Greptile findings:
mcp_servers.brew(quoted names, dotted keys, sub-tables, inline tables) now come backmanualinstead of getting a second table.KEY= # todoreads as empty.ensureIgnoredreportsconflictsfor paths that a later rule re-includes.One addition of my own: sensitive writes (
.env.local) carry no dry-run diff.Round 2 (commit 40df837): the Codex checks now run on the TOML structure, with comments stripped and string values blanked. A comment or string that only mentions
mcp_servers.brewno longer blocks the install.Not in this PR
brew.new/create-brew: that is a publish, which this PR does not do.mcp installdoes not writebrew.json#mcp.init(C4) owns that file.Test plan
bun run tscbun run lintbun run test: 52 files, 688 tests. Covers project libs on fixture projects (tests/fixtures/projects/*: pnpm+turbo, npm workspaces, nx, yarn berry PnP, yarn classic, bun, deno, next app, no package.json);mcp installend to end on temp dirs (create, merge, unchanged, JSONC manual, broken JSON, dry-run, print, user scope, Codex append/unchanged, CODEX_HOME,--api-url, monorepo root, no literal key, deeplink bytes); invocation prefix in confirm/retry envelopes;generate-skills --check. Everything runs in-process under MSW (onUnhandledRequest: 'error'); one gitignore test calls the realgitbinary when it is installed.bun run docs:commands:check,bun run generate:skills:checkbun run build, bin smoke,bun run smoke:signals🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Hold merging until workspace selection and the outstanding Windows and pnpm install failures are fixed.
Fix with agent prompt
Summary
Adds local
mcp install, project-file helpers, launcher-aware command hints, and embedded agent skills.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[mcp install] --> B[Resolve project root] B --> C[Select named or detected clients] C --> D{Print or report only?} D -->|Yes| E[Return snippets] D -->|No| F[Read client config] F --> G{Safe to merge?} G -->|No| E G -->|Yes| H[Plan or write Brew entry] H --> I[Return actions and next steps]Reviews (9) · Last reviewed commit: "docs(changelog): #44's entries are unrel..." · Reviewed by Greptile