Skip to content

Add --global flag and auto-include global packs in mcs doctor#175

Merged
bguidolim merged 3 commits intomainfrom
feature/doctor-global-scope
Mar 1, 2026
Merged

Add --global flag and auto-include global packs in mcs doctor#175
bguidolim merged 3 commits intomainfrom
feature/doctor-global-scope

Conversation

@bguidolim
Copy link
Owner

Summary

  • Add --global flag to mcs doctor so users can explicitly check only globally-configured packs (mirrors mcs sync --global)
  • When running mcs doctor inside a project, automatically include global-only packs (packs configured globally but not in the project) alongside project pack checks
  • Extract pack resolution from run() into resolveCheckScopes() with a CheckScope abstraction for multi-scope iteration

Test plan

  • swift build compiles without errors
  • swift test — all 585 tests pass (7 new DoctorCommandTests)
  • mcs doctor inside a project with global packs shows both project and global packs
  • mcs doctor --global shows only global packs
  • mcs doctor --pack <name> --global checks named pack in global scope
  • mcs doctor --fix --global applies fixes for global scope only

- 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
Copilot AI review requested due to automatic review settings March 1, 2026 12:20
- Add --global flag to CLAUDE.md, README.md, and troubleshooting guide
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --global flag to mcs doctor and pass it through as globalOnly to DoctorRunner.
  • Refactor DoctorRunner.run() pack selection into resolveCheckScopes() with a CheckScope abstraction, enabling project + global-only iteration.
  • Add DoctorCommand argument parsing tests covering --global combinations and skipLock behavior.

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.

Comment on lines +127 to +129
let supplementary = availablePacks
.filter { scope.packIDs.contains($0.identifier) }
.flatMap { $0.supplementaryDoctorChecks }
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
let supplementary = availablePacks
.filter { scope.packIDs.contains($0.identifier) }
.flatMap { $0.supplementaryDoctorChecks }
let supplementary = registry.supplementaryDoctorChecks(installedPacks: scope.packIDs)

Copilot uses AI. Check for mistakes.
Comment on lines +205 to +299
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)]
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
- Extract resolveProjectScope() helper with early-return fallback chain
- Merge double-filter into single scopePacks iteration
- Move projectName into the branch that uses it
@bguidolim bguidolim merged commit 6aaf1bc into main Mar 1, 2026
3 checks passed
@bguidolim bguidolim deleted the feature/doctor-global-scope branch March 1, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants