Skip to content
Closed
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
8 changes: 4 additions & 4 deletions crates/skillpouch/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub fn run(ctx: &Context, action: SettingsAction) -> Result<ExitCode> {
.iter()
.flat_map(|item| item.entries().into_keys())
.chain(entries.iter().flat_map(|item| {
sp_sync::settings::local_entries(target.id, &target.root, item)
sp_sync::settings::local_entries(target.id, &target.settings, item)
}))
.chain(
baseline
Expand All @@ -51,7 +51,7 @@ pub fn run(ctx: &Context, action: SettingsAction) -> Result<ExitCode> {
let status = if baseline.is_some_and(|state| state.excluded.contains(&key)) {
"excluded on this computer"
} else if entries.as_ref().is_some_and(|setting| {
sp_sync::settings::translated_on(target.id, &target.root, setting, &key)
sp_sync::settings::translated_on(target.id, &target.settings, setting, &key)
}) {
"translated"
} else if baseline.is_some_and(|state| state.baseline.contains_key(&key)) {
Expand Down Expand Up @@ -80,7 +80,7 @@ pub fn run(ctx: &Context, action: SettingsAction) -> Result<ExitCode> {
.iter()
.find(|target| target.id == harness)
.is_some_and(|target| {
sp_sync::settings::local_entries(&harness, &target.root, item)
sp_sync::settings::local_entries(&harness, &target.settings, item)
.contains(&entry)
})
}) {
Expand All @@ -89,7 +89,7 @@ pub fn run(ctx: &Context, action: SettingsAction) -> Result<ExitCode> {
let root = targets
.iter()
.find(|target| target.id == harness)
.map(|target| target.root.clone())
.map(|target| target.settings.clone())
.ok_or_else(|| anyhow::anyhow!("assistant is not installed"))?;
let device = state.harnesses.entry(harness.clone()).or_default();
if exclude {
Expand Down
28 changes: 25 additions & 3 deletions crates/skillpouch/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,9 +794,18 @@ mod service {
use std::path::Path;
use std::process::{Command, Stdio};

/// Settings passed on so the service uses the same folder. The server
/// isn't: the service syncs with whichever one this computer logged in to.
const PASSED_ON: [&str; 2] = ["HOME", "SKILLPOUCH_HOME"];
/// Settings passed on so the service uses the same folders, its own and
/// the assistants' (`sp_harness::Places::of`). The server isn't: the
/// service syncs with whichever one this computer logged in to.
pub(super) const PASSED_ON: [&str; 7] = [
"HOME",
"SKILLPOUCH_HOME",
"CLAUDE_CONFIG_DIR",
"XDG_CONFIG_HOME",
"CODEX_HOME",
"COPILOT_HOME",
"CURSOR_CONFIG_DIR",
];

fn quiet(command: &mut Command) -> bool {
command
Expand Down Expand Up @@ -972,6 +981,19 @@ mod service {
mod tests {
use super::*;

#[test]
fn the_service_finds_the_assistants_where_this_shell_does() {
for name in [
"CLAUDE_CONFIG_DIR",
"XDG_CONFIG_HOME",
"CODEX_HOME",
"COPILOT_HOME",
"CURSOR_CONFIG_DIR",
] {
assert!(service::PASSED_ON.contains(&name), "{name}");
}
}

#[test]
fn the_windows_login_command_quotes_its_paths() {
let command = service::login_command(
Expand Down
7 changes: 3 additions & 4 deletions crates/skillpouch/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -853,10 +853,9 @@ fn bring_in_new(
// again on every sync: only names the pouch doesn't have come in.
let read_only = sp_harness::git_checkout(&harness.root).is_some();
found.extend(adopt::found_in(harness).into_iter().filter(|f| {
let ours = f
.origin
.as_ref()
.is_some_and(|origin| projection.copies.contains_key(origin));
let ours = f.origin.as_ref().is_some_and(|origin| {
projection.copies.contains_key(origin) || projection.plugin_parts.contains(origin)
});
let key = item_key(&f.item);
let known =
read_only && (existing.contains_key(&key) || projection.dismissed.contains(&key));
Expand Down
114 changes: 103 additions & 11 deletions crates/sp-harness/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ pub const POUCH_KINDS: [(&str, &str); 8] = [
];

/// User preferences only; integrations (MCP, hooks, plugins and files) have
/// separate item kinds. Paths are relative to the assistant's root.
/// separate item kinds. Paths are relative to the assistant's settings
/// folder (`Target::settings`).
pub struct SettingsFile {
pub file: &'static str,
pub keys: &'static [&'static str],
Expand Down Expand Up @@ -233,6 +234,57 @@ pub fn settings(id: &str) -> &'static [SettingsFile] {
],
maps: &[],
}],
// The CLI's `cli-config.json`; not `version`, the network or what it
// counts for itself. The editor's own settings are VS Code's.
"cursor" => &[SettingsFile {
file: "cli-config.json",
keys: &[
"model",
"maxMode",
"approvalMode",
"autoAcceptWebSearch",
"editor",
"display",
"notifications",
"hints",
"modelSlashCommands",
"steering",
"rewind",
"suggestNextPrompt",
"exploreSubagentModel",
"permissions",
"sandbox",
"attribution",
],
maps: &[],
}],
// The CLI's `settings.json`; not `trustedWorkspaces` (this computer's
// folders) or `useG1Credits` (spending).
"antigravity" => &[SettingsFile {
file: "settings.json",
keys: &[
"colorScheme",
"altScreenMode",
"toolPermission",
"artifactReviewPolicy",
"agentMode",
"notifications",
"showTips",
"showFeedbackSurvey",
"editor",
"editorMode",
"vimInsertFirst",
"allowNonWorkspaceAccess",
"enableTerminalSandbox",
"enableTelemetry",
"verbosity",
"runningLightSpeed",
"statusLine",
"title",
"permissions",
],
maps: &[],
}],
_ => &[],
}
}
Expand Down Expand Up @@ -285,7 +337,7 @@ mod settings_tests {
use super::*;

#[test]
fn settings_are_only_the_eight_global_assistants() {
fn settings_are_every_assistant_but_windsurf() {
for id in [
"opencode",
"claude-code",
Expand All @@ -295,11 +347,22 @@ mod settings_tests {
"copilot",
"crush",
"factory",
"cursor",
"antigravity",
] {
assert!(!settings(id).is_empty(), "{id}");
}
for id in ["cursor", "windsurf", "antigravity"] {
assert!(settings(id).is_empty(), "{id}");
// Its preferences are a binary file (`~/.codeium/user_settings.pb`).
assert!(settings("windsurf").is_empty());
assert_eq!(settings("cursor")[0].file, "cli-config.json");
for private in ["version", "network", "hasChangedDefaultModel"] {
assert!(!settings("cursor")[0].keys.contains(&private), "{private}");
}
for private in ["trustedWorkspaces", "useG1Credits"] {
assert!(
!settings("antigravity")[0].keys.contains(&private),
"{private}"
);
}
assert!(
settings("gemini-cli")
Expand Down Expand Up @@ -444,6 +507,11 @@ pub struct Places {
pub antigravity: PathBuf,
pub copilot: PathBuf,
pub cursor: PathBuf,
/// Where Cursor's CLI keeps `cli-config.json`: `~/.cursor`, unless
/// `CURSOR_CONFIG_DIR` or (on Linux) `XDG_CONFIG_HOME` moves it.
pub cursor_config: PathBuf,
/// `~/.gemini/antigravity-cli`: the Antigravity CLI's own settings.
pub antigravity_cli: PathBuf,
pub windsurf: PathBuf,
pub qwen: PathBuf,
pub factory: PathBuf,
Expand All @@ -467,6 +535,8 @@ impl Places {
antigravity: home.join(".gemini/config"),
copilot: home.join(".copilot"),
cursor: home.join(".cursor"),
cursor_config: home.join(".cursor"),
antigravity_cli: home.join(".gemini/antigravity-cli"),
windsurf: home.join(".codeium/windsurf"),
qwen: home.join(".qwen"),
factory: home.join(".factory"),
Expand All @@ -478,7 +548,7 @@ impl Places {

/// The places under `home`, moved by the variables each assistant
/// honours (`CLAUDE_CONFIG_DIR`, `XDG_CONFIG_HOME`, `CODEX_HOME`,
/// `COPILOT_HOME`).
/// `COPILOT_HOME`, `CURSOR_CONFIG_DIR`).
pub fn of(home: &Path, skillpouch: &Path) -> Self {
let var = |name: &str| {
std::env::var_os(name)
Expand All @@ -493,6 +563,12 @@ impl Places {
if let Some(config) = var("XDG_CONFIG_HOME") {
places.opencode = config.join("opencode");
places.crush = config.join("crush");
if cfg!(target_os = "linux") {
places.cursor_config = config.join("cursor");
}
}
if let Some(dir) = var("CURSOR_CONFIG_DIR") {
places.cursor_config = dir;
}
if let Some(dir) = var("CODEX_HOME") {
places.codex = dir;
Expand Down Expand Up @@ -520,13 +596,15 @@ pub enum McpFormat {
Copilot,
/// `mcpServers`, `serverUrl` for remote servers (Windsurf, Antigravity).
Windsurf,
/// Claude's entries under `mcp` in `crush.json` (Crush).
Crush,
}

impl McpFormat {
/// The key holding the servers.
pub fn key(self) -> &'static str {
match self {
Self::OpenCode => "mcp",
Self::OpenCode | Self::Crush => "mcp",
Self::Codex => "mcp_servers",
_ => "mcpServers",
}
Expand Down Expand Up @@ -617,8 +695,12 @@ pub enum HookFormat {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginFormat {
/// `enabledPlugins` and `extraKnownMarketplaces` in `settings.json`
/// (Claude Code, Factory).
/// (Claude Code).
Claude,
/// The same keys in Factory's `settings.json`. Droid only loads plugins
/// it installed itself, so plugin folders are unpacked into its folders
/// instead.
Factory,
/// npm packages in the `plugins` list of `opencode.json` (OpenCode;
/// `plugin` in OpenCode 1).
OpenCode,
Expand Down Expand Up @@ -801,7 +883,8 @@ fn layouts(places: &Places) -> Vec<Layout> {
id: "cursor",
name: "Cursor",
root: places.cursor.clone(),
markers: vec![],
// The CLI's folder, when a variable moved it.
markers: vec![places.cursor_config.clone()],
instructions: None,
skills: vec![places.cursor.join("skills")],
agents: &[],
Expand Down Expand Up @@ -834,7 +917,8 @@ fn layouts(places: &Places) -> Vec<Layout> {
root: places.qwen.clone(),
markers: vec![],
instructions: Some(places.qwen.join("QWEN.md")),
skills: vec![],
// Qwen Code reads both; the shared one is Codex's too.
skills: vec![shared_skills.clone(), places.qwen.join("skills")],
agents: &["agents"],
commands: (&["commands"], CommandFormat::Toml),
agent_format: AgentFormat::Gemini,
Expand All @@ -861,7 +945,7 @@ fn layouts(places: &Places) -> Vec<Layout> {
}),
plugins: Some((
Some(places.factory.join("settings.json")),
PluginFormat::Claude,
PluginFormat::Factory,
)),
},
Layout {
Expand All @@ -874,7 +958,7 @@ fn layouts(places: &Places) -> Vec<Layout> {
agents: &[],
commands: (&[], CommandFormat::Markdown),
agent_format: AgentFormat::Claude,
mcp: None,
mcp: Some((vec![places.crush.join("crush.json")], McpFormat::Crush)),
hooks: None,
plugins: None,
},
Expand All @@ -897,6 +981,9 @@ pub struct Target {
pub id: &'static str,
pub name: &'static str,
pub root: PathBuf,
/// The folder its settings files are in: `root`, except for Cursor's
/// CLI and Antigravity's.
pub settings: PathBuf,
/// The global instructions file (`CLAUDE.md`).
pub instructions: Option<PathBuf>,
/// One folder per skill.
Expand Down Expand Up @@ -956,6 +1043,11 @@ pub fn targets(places: &Places) -> Vec<Target> {
hooks: layout.hooks,
plugins,
read_only,
settings: match layout.id {
"cursor" => places.cursor_config.clone(),
"antigravity" => places.antigravity_cli.clone(),
_ => layout.root.clone(),
},
root: layout.root,
}
})
Expand Down
1 change: 1 addition & 0 deletions crates/sp-sync/src/adopt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub fn format_of(harness_id: &str) -> McpFormat {
"gemini-cli" | "qwen-code" => McpFormat::Gemini,
"copilot" => McpFormat::Copilot,
"windsurf" | "antigravity" => McpFormat::Windsurf,
"crush" => McpFormat::Crush,
_ => McpFormat::Claude,
}
}
Expand Down
Loading
Loading