Add --global flag and auto-include global packs in mcs doctor#175
Add --global flag and auto-include global packs in mcs doctor#175
Conversation
- Add --global flag to check only globally-configured packs - Auto-include global-only packs when running inside a project - Extract scope resolution into resolveCheckScopes() with CheckScope abstraction
- Add --global flag to CLAUDE.md, README.md, and troubleshooting guide
There was a problem hiding this comment.
Pull request overview
Adds support for checking globally-configured packs via mcs doctor --global, and expands default project-mode behavior to also include “global-only” packs (globally configured but not present in the project). This is implemented by refactoring pack/scope resolution into a new multi-scope abstraction.
Changes:
- Add
--globalflag tomcs doctorand pass it through asglobalOnlytoDoctorRunner. - Refactor
DoctorRunner.run()pack selection intoresolveCheckScopes()with aCheckScopeabstraction, enabling project + global-only iteration. - Add
DoctorCommandargument parsing tests covering--globalcombinations andskipLockbehavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
Sources/mcs/Commands/DoctorCommand.swift |
Introduces --global flag and wires it into DoctorRunner. |
Sources/mcs/Doctor/DoctorRunner.swift |
Refactors scope/pack resolution to support project + global-only packs and global-only mode. |
Tests/MCSTests/DoctorCommandTests.swift |
Adds parsing-focused tests for the new --global flag and related combinations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let supplementary = availablePacks | ||
| .filter { scope.packIDs.contains($0.identifier) } | ||
| .flatMap { $0.supplementaryDoctorChecks } |
There was a problem hiding this comment.
Pack-level supplementary checks are reimplemented here by filtering availablePacks and flattening supplementaryDoctorChecks, but TechPackRegistry.supplementaryDoctorChecks(installedPacks:) already encapsulates this logic. Using the registry helper (with scope.packIDs) would remove duplication and keep scope/pack selection behavior consistent if the registry implementation changes later.
| let supplementary = availablePacks | |
| .filter { scope.packIDs.contains($0.identifier) } | |
| .flatMap { $0.supplementaryDoctorChecks } | |
| let supplementary = registry.supplementaryDoctorChecks(installedPacks: scope.packIDs) |
| private func resolveCheckScopes( | ||
| projectRoot: URL?, | ||
| globallyConfiguredPackIDs: Set<String> | ||
| ) -> [CheckScope] { | ||
| let projectName = projectRoot?.lastPathComponent | ||
|
|
||
| // --pack flag: single scope, use globalOnly to determine project root | ||
| if let filter = packFilter { | ||
| let packIDs = Set(filter.components(separatedBy: ",")) | ||
| let root = globalOnly ? nil : projectRoot | ||
| return [CheckScope( | ||
| packIDs: packIDs, | ||
| effectiveProjectRoot: root, | ||
| excludedComponentIDs: [], | ||
| label: globalOnly ? "--pack flag (global)" : "--pack flag" | ||
| )] | ||
| } | ||
|
|
||
| // --global flag: single global scope | ||
| if globalOnly { | ||
| return [globalScope(globallyConfiguredPackIDs)] | ||
| } | ||
|
|
||
| // In a project: resolve project packs, then add global-only packs | ||
| if let root = projectRoot { | ||
| var projectState: ProjectState? | ||
| do { | ||
| let state = try ProjectState(projectRoot: root) | ||
| if state.exists, !state.configuredPacks.isEmpty { | ||
| projectState = state | ||
| } | ||
| } catch { | ||
| output.warn("Could not read .mcs-project: \(error.localizedDescription) — falling back to section markers") | ||
| } | ||
|
|
||
| var scopes: [CheckScope] = [] | ||
| var projectPackIDs: Set<String>? | ||
|
|
||
| if let state = projectState { | ||
| // Tier 2: Project .mcs-project file | ||
| let excludedIDs = Set(state.allExcludedComponents.values.flatMap { $0 }) | ||
| scopes.append(CheckScope( | ||
| packIDs: state.configuredPacks, | ||
| effectiveProjectRoot: root, | ||
| excludedComponentIDs: excludedIDs, | ||
| label: "project: \(projectName ?? "unknown")" | ||
| )) | ||
| projectPackIDs = state.configuredPacks | ||
| } else { | ||
| // Tier 3: Fallback — infer from CLAUDE.local.md section markers | ||
| let claudeLocal = root.appendingPathComponent(Constants.FileNames.claudeLocalMD) | ||
| let claudeLocalContent: String? | ||
| if FileManager.default.fileExists(atPath: claudeLocal.path) { | ||
| do { | ||
| claudeLocalContent = try String(contentsOf: claudeLocal, encoding: .utf8) | ||
| } catch { | ||
| output.warn("Could not read \(Constants.FileNames.claudeLocalMD): \(error.localizedDescription)") | ||
| claudeLocalContent = nil | ||
| } | ||
| } else { | ||
| claudeLocalContent = nil | ||
| } | ||
|
|
||
| if let content = claudeLocalContent { | ||
| let sections = TemplateComposer.parseSections(from: content) | ||
| let inferred = Set(sections.map(\.identifier)) | ||
| if !inferred.isEmpty { | ||
| scopes.append(CheckScope( | ||
| packIDs: inferred, | ||
| effectiveProjectRoot: root, | ||
| excludedComponentIDs: [], | ||
| label: "project: \(projectName ?? "unknown") (inferred)" | ||
| )) | ||
| projectPackIDs = inferred | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Add global-only packs (not already checked via project scope) | ||
| let alreadyChecked = projectPackIDs ?? [] | ||
| let globalOnlyIDs = globallyConfiguredPackIDs.subtracting(alreadyChecked) | ||
| if !globalOnlyIDs.isEmpty { | ||
| scopes.append(globalScope(globalOnlyIDs)) | ||
| } | ||
|
|
||
| // If no project packs found and no global packs, fall back to full global set | ||
| if scopes.isEmpty { | ||
| scopes.append(globalScope(globallyConfiguredPackIDs)) | ||
| } | ||
|
|
||
| return scopes | ||
| } | ||
|
|
||
| // Tier 4: Not in a project — global packs only | ||
| return [globalScope(globallyConfiguredPackIDs)] |
There was a problem hiding this comment.
resolveCheckScopes() introduces multiple behavior branches (project vs non-project, --global, inferred packs, and global-only subtraction), but there are no unit tests covering scope resolution or that global-only packs are included when inside a project. Adding focused tests around scope resolution (e.g., temp project with .mcs-project + global state, and project with inferred CLAUDE.local.md) would help prevent regressions in this control flow.
- Extract resolveProjectScope() helper with early-return fallback chain - Merge double-filter into single scopePacks iteration - Move projectName into the branch that uses it
Summary
--globalflag tomcs doctorso users can explicitly check only globally-configured packs (mirrorsmcs sync --global)mcs doctorinside a project, automatically include global-only packs (packs configured globally but not in the project) alongside project pack checksrun()intoresolveCheckScopes()with aCheckScopeabstraction for multi-scope iterationTest plan
swift buildcompiles without errorsswift test— all 585 tests pass (7 newDoctorCommandTests)mcs doctorinside a project with global packs shows both project and global packsmcs doctor --globalshows only global packsmcs doctor --pack <name> --globalchecks named pack in global scopemcs doctor --fix --globalapplies fixes for global scope only