Relationships
Implemented by 9
Related 3
Branches 1
Context
Refrakt's CLI surface is growing in three directions that all want answers from a single source of truth, and we don't have one yet.
Plugin discovery. SPEC-043 introduces an MCP server that needs to enumerate installed plugins at startup. The current CLI does this lazily — runPlugin(namespace, args) in packages/cli/src/bin.ts only attempts to import @refrakt-md/<namespace>/cli-plugin when the user types that namespace. There is no existing list. The natural fallback is scanning package.json for @refrakt-md/* dependencies, but that is heuristic: it can't tell a real plugin from a transitive dep, can't distinguish runes-only packages from CLI-extending ones, and forces every consumer (CLI dispatch, --help, MCP) to repeat the same scan.
Path scattering. Plan paths (--dir, --specs, --out, --css), site paths (contentDir), and theme paths are spread across CLI flags and ad-hoc defaults. A plan-only repo with files in project/plan/ instead of the default plan/ has to pass --dir project/plan to every command. There is no place to record "in this repo, plan lives here."
Two lifecycles, one repo. The current site/refrakt.config.json is site-shaped — contentDir, theme, target, packages, routeRules, icons. It assumes every refrakt user is building a SvelteKit site. Planning-only repos use @refrakt-md/plan with zero config, autodetecting plan/. Repos that want both end up with a site config plus implicit plan conventions, neither aware of the other.
The MCP spec's auto-detection + discoverPlugins() proposal mitigates these in the short term but doesn't resolve them — autodetection is heuristic, and the helper still has to scan dependencies because there is no declared list.
Decision
Adopt option 3. refrakt.config.json becomes the unified root config with optional plugins, plan, and site/sites sections. The file remains optional. The current flat site shape and the new singular site form are both valid shorthands for sites.default; multi-site repos use the sites map. Existing projects keep working without changes.
Consequences
For SPEC-043 (MCP server):
discoverPlugins()readsconfig.pluginsfirst and only falls back to dependency scanning when the field is absent.- Autodetection still applies when no config exists, but consults the config when one is present (e.g.,
plan.diroverrides the defaultplan/lookup; declared sites confirm site context without sniffing the file system). refrakt://detectreports detected context, config source (config-filevsautodetect), and the list of sites (with names) so the agent knows whether the project is single- or multi-site.- Site-scoped MCP tools (
refrakt.inspect,refrakt.contracts,refrakt.validate) accept an optionalsiteinput. When the project has exactly one site, the field is optional; when there are multiple, it is required and the tool's input schema reflects that.
For the CLI:
loadRefraktConfigFile()inpackages/cli/src/config-file.tsgains a normalization layer that accepts the flat, singular-site, and plural-sitesshapes and returns a normalizedRefraktConfigwithplugins,plan, andsites: Record<string, SiteConfig>always populated.RefraktConfiginpackages/typesgrows theplugins,plan,site, andsitesfields. The flat fields and singularsitestay on the type for backwards compatibility but are marked deprecated in JSDoc as shorthand forsites.default.- Site-targeted commands (
inspect,contracts,validate, etc.) gain a--site <name>flag. When the project has exactly one site, the flag is optional and the lone site is selected automatically; when multiple sites are declared and no flag is passed, the command errors with the available names. refrakt plugins list(proposed in SPEC-043) shows the resolution source per plugin —from refrakt.config.jsonvsdiscovered in package.json.
For the SvelteKit plugin:
- The plugin accepts a
siteoption naming which entry from the config to build. When omitted, it errors at config-load time for multi-site repos and selects the lone site for single-site repos. - A monorepo wiring two SvelteKit apps to the same
refrakt.config.jsonpasses a differentsitename in each app'svite.config.ts. Content roots, themes, and package merges resolve from the chosen site's section.
For users:
- Existing site users: zero changes required. Their flat config keeps working.
- Existing planning-only users: zero changes required. Autodetection still runs.
- New users wanting both contexts in one repo: one file declares everything.
- Users wanting explicit plugin control: list packages under
pluginsand the heuristic scan is bypassed. - Multi-site monorepos: declare each site under
sites.<name>and pick the right one per app or per CLI invocation.
For migration:
- A
refrakt config migratecommand (small follow-up work item, not part of this ADR) can rewrite a flat site config into the nested shape on demand, and convert a singularsiteintosites.defaultwhen adding a second site. Not required — the flat and singular shapes stay valid indefinitely. - The SvelteKit plugin's
loadContent()reads from the normalizedsites[name]entry so the runtime contract is unchanged for single-site users (they hitsites.defaulttransparently).
Open follow-up:
- A separate spec should formalize the config schema (TypeScript interface, JSON Schema for editor tooling, validation rules, multi-site selection semantics). This ADR records the decision; the spec records the shape in detail.
- SPEC-043's "Plugin Discovery" section should be updated after this ADR is accepted to reference
config.pluginsas the primary source, with dependency scanning as fallback. The MCP tool input schemas for site-scoped tools should also be updated to include thesiteparameter for multi-site repos.