diff --git a/crates/skills/src/commands.rs b/crates/skills/src/commands.rs index 6e3fdfc99..5428d3ddf 100644 --- a/crates/skills/src/commands.rs +++ b/crates/skills/src/commands.rs @@ -140,25 +140,26 @@ fn locale_compare(a: &str, b: &str) -> std::cmp::Ordering { fn check(io: &mut Io) -> R<()> { let (sys, _) = ctx(io); let root = sys.find_project_root(); - if sys.is_already_installed(&root, None).is_none() { + // Home-rooted checkout = user-level install. Use User scope so leftover + // project-layout dirs and unrelated harness skills cannot make a current + // impeccable install look stale (#824). + let scope = if sys.is_home_dir(&root) { Some(Scope::User) } else { None }; + if sys.is_already_installed(&root, scope).is_none() { out(io, "Impeccable is not installed in this project."); out(io, "Run `npx impeccable install` to install."); return Err(Flow::Exit(0)); } - let providers = sys.find_installed_providers(&root, None); + let providers = sys.find_impeccable_providers(&root, scope); out(io, "Checking for updates...\n"); let result = (|| -> Result { let bundle_dir = bundle::download_and_extract_bundle(&sys)?; - // JS: agentScope 'user' for a home-rooted checkout (d2a9efb9), so - // check() judges agent freshness against the user agent dirs. - let agent_scope = if sys.is_home_dir(&root) { Some(Scope::User) } else { None }; - let up_to_date = bundle::is_up_to_date(&sys, &root, &providers, &bundle_dir, None, agent_scope)?; + let up_to_date = bundle::is_up_to_date(&sys, &root, &providers, &bundle_dir, scope, scope)?; util::rm_rf(&bundle_dir); Ok(up_to_date) })(); match result { Ok(true) => { - let v = sys.get_skills_version(&root, None); + let v = sys.get_skills_version(&root, scope); out(io, &format!("Skills are up to date{}.", version_suffix(&v))); } Ok(false) => { diff --git a/crates/skills/tests/drift_ports_tests.rs b/crates/skills/tests/drift_ports_tests.rs index dfd8b7049..093e76794 100644 --- a/crates/skills/tests/drift_ports_tests.rs +++ b/crates/skills/tests/drift_ports_tests.rs @@ -460,6 +460,101 @@ fn check_accepts_current_copilot_user_agents_in_home_rooted_checkout() { std::fs::remove_dir_all(&root).ok(); } +// ─── check vs update scope parity (#824) ───────────────────────────────────── + +#[test] +fn check_ignores_unrelated_harness_skill_in_home_rooted_tree() { + let root = temp_root("check-824-extra-harness"); + let home = format!("{root}/home"); + let tmpdir = format!("{root}/tmp"); + for d in [&home, &tmpdir] { + std::fs::create_dir_all(d).unwrap(); + } + std::fs::create_dir_all(format!("{home}/.git")).unwrap(); + let bundle_root = create_fake_universal_bundle(&home, &[".claude", ".cursor"]); + let env = base_env(&home, &tmpdir, &bundle_root); + + let r = run_cli( + &["install", "-y", "--scope=global", "--no-hooks", "--providers=claude"], + &home, + &env, + ); + assert_eq!(r.code, 0, "{}\n{}", r.stdout, r.stderr); + write( + &format!("{home}/.cursor/skills/other/SKILL.md"), + "---\nname: other\n---\nUnrelated skill.\n", + ); + + let r = run_cli(&["check"], &home, &env); + assert!(r.stdout.contains("Skills are up to date"), "{}\n{}", r.stdout, r.stderr); + assert!(!r.stdout.contains("Updates available"), "{}", r.stdout); + + let r = run_cli(&["update", "--global", "-y", "--no-hooks"], &home, &env); + assert!(r.stdout.contains("Skills are up to date"), "{}\n{}", r.stdout, r.stderr); + std::fs::remove_dir_all(&root).ok(); +} + +#[test] +fn check_ignores_leftover_pi_project_layout_in_home_rooted_tree() { + let root = temp_root("check-824-pi-leftover"); + let home = format!("{root}/home"); + let tmpdir = format!("{root}/tmp"); + for d in [&home, &tmpdir] { + std::fs::create_dir_all(d).unwrap(); + } + std::fs::create_dir_all(format!("{home}/.git")).unwrap(); + let bundle_root = create_fake_universal_bundle(&home, &[".pi"]); + let env = base_env(&home, &tmpdir, &bundle_root); + + let r = run_cli( + &["install", "-y", "--scope=global", "--no-hooks", "--providers=pi"], + &home, + &env, + ); + assert_eq!(r.code, 0, "{}\n{}", r.stdout, r.stderr); + assert!(std::path::Path::new(&format!("{home}/.pi/agent/skills/impeccable/SKILL.md")).exists()); + write( + &format!("{home}/.pi/skills/other/SKILL.md"), + "---\nname: other\n---\nLeftover project-layout skill.\n", + ); + + let r = run_cli(&["check"], &home, &env); + assert!(r.stdout.contains("Skills are up to date"), "{}\n{}", r.stdout, r.stderr); + assert!(!r.stdout.contains("Updates available"), "{}", r.stdout); + + let r = run_cli(&["update", "--global", "-y", "--no-hooks"], &home, &env); + assert!(r.stdout.contains("Skills are up to date"), "{}\n{}", r.stdout, r.stderr); + std::fs::remove_dir_all(&root).ok(); +} + +#[test] +fn check_reports_stale_impeccable_in_home_rooted_tree() { + let root = temp_root("check-824-stale"); + let home = format!("{root}/home"); + let tmpdir = format!("{root}/tmp"); + for d in [&home, &tmpdir] { + std::fs::create_dir_all(d).unwrap(); + } + std::fs::create_dir_all(format!("{home}/.git")).unwrap(); + let bundle_root = create_fake_universal_bundle(&home, &[".claude"]); + let env = base_env(&home, &tmpdir, &bundle_root); + + let r = run_cli( + &["install", "-y", "--scope=global", "--no-hooks", "--providers=claude"], + &home, + &env, + ); + assert_eq!(r.code, 0, "{}\n{}", r.stdout, r.stderr); + write( + &format!("{home}/.claude/skills/impeccable/SKILL.md"), + "---\nname: impeccable\nversion: 0.0.0-stale\n---\n\nStale copy.\n", + ); + + let r = run_cli(&["check"], &home, &env); + assert!(r.stdout.contains("Updates available"), "{}\n{}", r.stdout, r.stderr); + std::fs::remove_dir_all(&root).ok(); +} + // ─── inferred agent update scope (d2a9efb9) ────────────────────────────────── #[test]