Commit Graph

4 Commits

Author SHA1 Message Date
nick 4a4a7c9079 feat(installer): auto-inject BMAD version badge into project README
- Add badge.js module for git remote resolution, README detection, and badge injection
- Integrate badge prompt into install flow with --no-badge opt-out
- Support badge update when owner/repo changes on re-install
- Auto-create README.md with badge if missing
- Pass badge config through Config.build() for both install and quick-update paths

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 22:10:42 +08:00
Brian 91a57499e9
feat(installer): add --set and --list-options for non-interactive config (#2354)
Closes #1663.

Adds two installer flags so module config options can be set without
interactive prompts. Designed for CI scripts, Dockerfiles, and
enterprise rollouts where the user wants to bake answers into the
install command rather than answer prompts.

`--set <module>.<key>=<value>` (repeatable) sets any module config
option. `--list-options [module]` lists every key the installer can
discover locally — built-in modules (`core`, `bmm`) plus any cached
official modules. One flag scales to every module without growing the
CLI surface per option.

```bash
npx bmad-method install --yes \
  --modules bmm --tools claude-code \
  --set bmm.project_knowledge=research \
  --set bmm.user_skill_level=expert \
  --set core.user_name=Brian
```

## How it works

`--set` is a post-install patch. The installer runs its normal flow
untouched, then `applySetOverrides` upserts each value into the
relevant config files:

- `_bmad/config.toml` (team scope, default)
- `_bmad/config.user.toml` (user scope, when the key already lives
  there — so user-scope keys like `core.user_name` and
  `bmm.user_skill_level` keep their proper file)
- `_bmad/<module>/config.yaml` (so declared schema keys carry forward
  via the existingValue path on the next install)

A module without `_bmad/<module>/config.yaml` is skipped silently —
no orphan sections in `config.toml` for uninstalled modules.

## Tradeoffs documented in install-bmad.md

- **Verbatim values.** `--set bmm.project_knowledge=research` writes
  `"research"`, not `"{project-root}/research"`. The `result:`
  template is not applied. Pass it explicitly if you want the
  rendered form: `--set bmm.project_knowledge='{project-root}/research'`.
- **Carry-forward, declared keys.** Free — values land in the
  per-module `config.yaml`, so the next install reads them as
  `existingValue` and they become the prompt default (accepted under
  `--yes`).
- **Carry-forward, undeclared keys.** Best-effort. The value lives in
  `config.toml` for the current install but won't be re-emitted on
  the next install (the manifest writer's schema-strict partition
  drops unknown keys). Re-pass `--set` if needed.
- **No "key not in schema" validation.** Whatever you assert is
  written.

## Security

Prototype-pollution defense: `--set __proto__.x=1` would otherwise
reach `overrides.__proto__[x] = 1` and pollute `Object.prototype`,
cascading into every plain-object lookup in the process. Defense-in-
depth via parser-level reserved-name rejection (`__proto__`,
`prototype`, `constructor`) AND `Object.create(null)` for the
override maps. Verified the attack reproduces without the guard and
is blocked with it.

## What's intentionally NOT integrated

`--set` deliberately does not touch the prompt / template / schema
collection flow. No pre-seeding answers, no question filtering, no
function-default evaluation, no schema-strict partition exemption.
That earlier integration approach was tried and scrapped: it
spread state across `Config`, `OfficialModules`,
`manifest-generator`, both collection helpers, and required parallel
plumbing for quick-update — every bug fix touched a different layer.
The post-install patch model covers the actual user need (set a
config value from CI) in ~330 lines of `set-overrides.js` without
the schema gymnastics.

## Files

- `tools/installer/set-overrides.js` (new): parser, prototype-pollution
  guard, `applySetOverrides` post-install patch, `upsertTomlKey` /
  `tomlString` / `tomlHasKey` line-based TOML helpers
- `tools/installer/list-options.js` (new): module.yaml discovery +
  formatter for `--list-options`
- `tools/installer/commands/install.js`: register `--set` /
  `--list-options` flags, early validation, `--list-options` exit-code
  handling (await `stream.write` callback then `process.exitCode` to
  avoid truncating piped output), thread `setOverrides` through to
  quick-update
- `tools/installer/core/config.js`: carry `setOverrides` field for
  the post-install patch step
- `tools/installer/core/installer.js`: invoke `applySetOverrides`
  after `writeCentralConfig` (covers regular install + quick-update
  via the shared install path)
- `tools/installer/ui.js`: parse `--set` for early validation, warn
  about overrides targeting modules not in `--modules`, drop those
  entries before threading
- `docs/how-to/install-bmad.md`, `README.md`: usage, routing rules,
  carry-forward semantics, tradeoffs

## Test plan

Suite 44 (24 cases): parser, prototype-pollution guard, `tomlString`
escaping, `upsertTomlKey` across insert/replace/missing-section/
empty-file/preserved-newline cases, `applySetOverrides` happy path +
uninstalled-module skip + missing-user-toml-creation + empty-input
no-op, `discoverOfficialModuleYamls` / `formatOptionsList` sanity
(hermetic via `BMAD_EXTERNAL_MODULES_CACHE` temp dir). 355 total
passing. Lint + prettier + markdownlint clean.

E2E smoke verified across:

- [x] `--set` writes correct files (team toml / user toml / per-module
  yaml) for declared and undeclared keys
- [x] Quick-update without `--set` carries forward declared keys via
  `existingValue` path
- [x] Quick-update WITH `--set` applies cleanly (uniform behavior
  across action types)
- [x] `--set` for unselected module: warned, no orphan section
- [x] Prototype pollution: rejected with non-zero exit
- [x] `--list-options bmm` exit 0 with full output through pipe;
  `--list-options nope` exit 1
- [x] Translated docs (`docs/{cs,fr,vi-vn,zh-cn}/`) intentionally not
  touched — they'll lag behind English until the translation pipeline
  runs
2026-04-28 20:15:57 -05:00
Brian 3d824d4c0f
feat(installer): channel-based version resolution + interactive channel management (#2305)
* feat(installer): channel-based version resolution for external modules

Adds stable/next/pinned channel resolution so external/community modules
install at released git tags by default instead of tracking main HEAD.
Manifest now records channel, resolved version, and SHA per module for
reproducible installs.

CLI flags: --channel, --all-stable, --all-next, --next=CODE (repeatable),
--pin CODE=TAG (repeatable). Precedence: pin > next > channel > registry
default > stable. --yes accepts patch/minor upgrades but refuses majors.

Interactive "Ready to install (all stable)?" gate with a per-module
picker (stable/next/pin) when declined. Re-install prompts classify tag
diffs as patch/minor/major with semver-class-dependent defaults.
Legacy version:null manifests get a one-time migration prompt.

Custom modules gain an optional @<ref> URL suffix for pinning (https,
ssh, /tree/<ref>/subdir forms supported; local paths rejected).
Community modules honor --next/--pin overrides with a curator-bypass
warning; default path still enforces the approved SHA.

Quick-update now reads the manifest's recorded channel per module so
pinned installs don't silently roll forward.

* feat(installer): interactive channel switch, upgrade refusal, unified docs

Builds on the channel-resolution foundation. The installer now lets users
flip a module between stable, next, and pinned after install — either
interactively via a "Review channel assignments?" gate, or by flag. Quick
and modify re-installs classify stable upgrades; under non-interactive
flows, patches and minors apply automatically but majors are refused with
a pointer to --pin.

Fallback behavior for GitHub rate-limit / network failures is now cache-
aware: re-installs reuse the recorded ref silently; fresh installs abort
with actionable guidance (set GITHUB_TOKEN or use --next/--pin). Bundled
modules (core, bmm) warn when targeted by --pin or --next so users aren't
left wondering why the flag had no effect.

Install summary labels no longer mangle "main" into "vmain"; next-channel
entries render as "main @ <short-sha>" instead. Bundled modules are now
correctly skipped from all channel prompts and tag-API lookups.

Docs consolidated into a single how-to. install-bmad.md now covers the
interactive flow, the channel model (stable/next/pinned plus the npm
dist-tag axis for core/bmm), the re-install upgrade prompts, the full
flag reference, copy-paste recipes, and troubleshooting. The old
non-interactive-installation.md is reduced to a redirect stub.

* fix(installer): review fixes + unit tests for channel resolution

- ui.js: import parseGitHubRepo; fixes ReferenceError in the
  interactive channel picker's stable-tag pre-resolve path.
- community-manager: pinned modules now fetch+checkout the pin tag
  on cache refresh instead of resetting to origin/HEAD (was silently
  drifting to main on re-install).
- channel-plan: parseChannelOptions returns acceptBypass so --yes
  auto-confirms the curator-bypass prompt; headless --next/--pin
  installs of community modules no longer hang.
- community-manager: simplify recordedVersion (dead ternary branch).
- custom-module-manager: drop "or sha" from the @<ref> comment
  (git clone --branch rejects raw SHAs); update-path fetches
  origin <ref> so /tree/<branch>/ URLs work too.
- install-bmad.md: rename "Headless / CI installs" to "Headless CI
  installs" so the stub's #headless-ci-installs anchor resolves.
- test/test-installer-channels.js: 83 unit tests for channel-plan
  and channel-resolver pure modules; wired into npm test as
  test:channels.

* fix(installer): address CodeRabbit review findings

- ui.js: skip stable-channel upgrade classification when the user has
  already declared intent via --pin/--next=/--channel or the review
  gate. Prevents the decline / major-refused / fetch-error branches
  from silently overwriting an explicit pin with prev.version.
- external-manager.js: short-circuit cloneExternalModule when the
  requested plan matches an existing in-process resolution and the
  cache is valid. Avoids redundant resolveChannel() + git fetch on
  every same-plan lookup in a single install.
- installer.js: fall back to CommunityModuleManager.getResolution()
  when no external resolution exists, so community module result
  rows carry newChannel/newSha instead of null under --next/--pin.
- installer.js: don't label a module as "no change" when its version
  string is 'main'/'HEAD' — the SHA may have moved and preVersions
  doesn't track the prior SHA. Show "(refreshed)" instead.
- official-modules.js: match versionInfo.version to the manifest's
  cloneRef || (hasGitClone ? 'main' : version) expression so summary
  lines report the cloned ref for git-backed custom installs.
- install-bmad.md: clarify that sha is only written for git-backed
  modules and that rerunning the same --modules on another machine
  does not reproduce stable-channel installs — convert recorded tags
  into explicit --pin flags for cross-machine reproducibility.
2026-04-24 08:20:30 -05:00
Alex Verkhovsky 513f440a23
refactor(installer): restructure installer with clean separation of concerns (#2129)
* refactor(installer): restructure installer with clean separation of concerns

Move tools/cli/ to tools/installer/ with major structural cleanup:

- InstallPaths async factory for path resolution and directory creation
- Config value object (frozen) replaces mutable config bag
- ExistingInstall value object replaces stateful Detector class
- OfficialModules + CustomModules + ExternalModuleManager replace monolithic ModuleManager
- install() is prompt-free; all user interaction in ui.js
- Update state returned explicitly instead of mutating customConfig
- Delete dead code: dependency-resolver, _base-ide, IdeConfigManager,
  platform-codes helpers, npx wrapper, xml-utils
- Flatten directory structure: custom/handler → custom-handler,
  tools/cli/ → tools/installer/, lib/ directories removed
- Update all path references in package.json, tests, CI, and docs

* fix(installer): guard ExistingInstall.version and surface module.yaml errors

Guard ExistingInstall.version access with .installed check in
uninstall.js, ui.js, and installer.js to prevent throwing on
empty/partial _bmad dirs. Surface invalid module.yaml parse errors
as warnings instead of silently returning empty results.
2026-03-27 06:50:07 -06:00