Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sources/mcs/Core/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ enum Constants {
/// The update check cache file (timestamp + results).
static let updateCheckCache = "update-check.json"

/// The per-project settings file written by `mcs sync`.
static let settingsLocal = "settings.local.json"

/// The user preferences file.
static let mcsConfig = "config.yaml"
}
Expand Down
37 changes: 36 additions & 1 deletion Sources/mcs/Doctor/CoreDoctorChecks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,14 @@ struct MCPServerCheck: DoctorCheck {

struct PluginCheck: DoctorCheck {
let pluginRef: PluginRef
var environment: Environment = .init()
let projectRoot: URL?
let environment: Environment

init(pluginRef: PluginRef, projectRoot: URL? = nil, environment: Environment = Environment()) {
self.pluginRef = pluginRef
self.projectRoot = projectRoot
self.environment = environment
}

var name: String {
pluginRef.bareName
Expand All @@ -102,8 +109,30 @@ struct PluginCheck: DoctorCheck {
}

func check() -> CheckResult {
var projectSettingsError: String?

// Tier 1: Project-scoped settings.local.json
if let root = projectRoot {
let projectSettingsURL = root
.appendingPathComponent(Constants.FileNames.claudeDirectory)
.appendingPathComponent(Constants.FileNames.settingsLocal)
do {
let projectSettings = try Settings.load(from: projectSettingsURL)
if projectSettings.enabledPlugins?[pluginRef.bareName] == true {
return .pass("enabled (project)")
}
} catch {
// Corrupt project settings — fall through to global, but note for diagnostics
projectSettingsError = error.localizedDescription
}
}

// Tier 2: Global settings.json
let settingsURL = environment.claudeSettings
guard FileManager.default.fileExists(atPath: settingsURL.path) else {
if projectSettingsError != nil {
return .fail("settings.local.json is corrupt and settings.json not found")
}
return .fail("settings.json not found")
}
let settings: Settings
Expand All @@ -113,8 +142,14 @@ struct PluginCheck: DoctorCheck {
return .fail("settings.json is invalid: \(error.localizedDescription)")
}
if settings.enabledPlugins?[pluginRef.bareName] == true {
if let errorDesc = projectSettingsError {
return .warn("enabled (global) — settings.local.json is unreadable: \(errorDesc)")
}
return .pass("enabled")
}
if projectSettingsError != nil {
return .fail("not enabled (settings.local.json is corrupt)")
}
return .fail("not enabled")
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/mcs/Doctor/DerivedDoctorChecks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension ComponentDefinition {
return MCPServerCheck(name: displayName, serverName: config.name, projectRoot: projectRoot, environment: environment)

case let .plugin(pluginName):
return PluginCheck(pluginRef: PluginRef(pluginName), environment: environment)
return PluginCheck(pluginRef: PluginRef(pluginName), projectRoot: projectRoot, environment: environment)

case let .brewInstall(package):
return CommandCheck(
Expand Down
2 changes: 1 addition & 1 deletion Sources/mcs/Doctor/DoctorRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ struct DoctorRunner {
if !artifacts.hookCommands.isEmpty || !artifacts.settingsKeys.isEmpty {
let settingsPath: URL = if let root = scope.effectiveProjectRoot {
root.appendingPathComponent(Constants.FileNames.claudeDirectory)
.appendingPathComponent("settings.local.json")
.appendingPathComponent(Constants.FileNames.settingsLocal)
} else {
env.claudeSettings
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/mcs/Export/ConfigurationDiscovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ struct ConfigurationDiscovery {
agentsDir = environment.agentsDirectory
case let .project(projectRoot):
let claudeDir = projectRoot.appendingPathComponent(Constants.FileNames.claudeDirectory)
settingsPath = claudeDir.appendingPathComponent("settings.local.json")
settingsPath = claudeDir.appendingPathComponent(Constants.FileNames.settingsLocal)
claudeFilePath = projectRoot.appendingPathComponent(Constants.FileNames.claudeLocalMD)
hooksDir = claudeDir.appendingPathComponent("hooks")
skillsDir = claudeDir.appendingPathComponent("skills")
Expand Down
2 changes: 1 addition & 1 deletion Sources/mcs/Sync/ProjectSyncStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ struct ProjectSyncStrategy: SyncStrategy {
output.error("Could not write settings.local.json: \(error.localizedDescription)")
output.error("Hooks and plugins will not be active. Re-run '\(scope.syncHint)' after fixing the issue.")
throw MCSError.fileOperationFailed(
path: "settings.local.json",
path: Constants.FileNames.settingsLocal,
reason: error.localizedDescription
)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/mcs/Sync/SyncScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ extension SyncScope {
label: "Project",
targetPath: claudeDir,
stateFile: claudeDir.appendingPathComponent(Constants.FileNames.mcsProject),
settingsPath: claudeDir.appendingPathComponent("settings.local.json"),
settingsPath: claudeDir.appendingPathComponent(Constants.FileNames.settingsLocal),
claudeFilePath: projectPath.appendingPathComponent(Constants.FileNames.claudeLocalMD),
scopeIdentifier: projectPath.path,
mcpScopeOverride: nil,
Expand Down
Loading
Loading