Port: the installer half of the OpenCode command bridge (#483)

Upstream sha 9736a9f6e9, the part of it that
lives in `cli/bin/commands/skills.mjs` rather than `pin.mjs`.

`copy_provider_commands` mirrors `copy_provider_skills` for a provider's
compiled `commands/` dir: project scope writes `<root>/<configDir>/commands`,
user scope writes the config dir OpenCode actually scans
(`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`),
and a pre-#406 global install at `~/.opencode/commands/` loses exactly the
files just written while siblings, symlinked dirs and home-rooted git repos
are left alone. It runs on install, on the reinstall refresh, on update, and
on link, which is the only path that can deliver the bridge to a linked
install.

`is_up_to_date` now compares the bundle's command files too, so an install
whose skills match but whose bridge is missing or drifted refreshes instead of
reporting success while the slash command stays absent. Only bundle-shipped
files are compared, so a pinned shortcut never affects freshness.

`tests/copy-provider-commands.test.js` arrived with the merge importing the
deleted `cli/bin/commands/skills.mjs`; its scenarios are ported to
`crates/skills/tests/provider_commands_tests.rs` (project scope, the three
user-scope dir resolutions, the legacy migration and its two guards, a
provider with no commands dir, and the four `isUpToDate` command-awareness
cases), and the file is removed and deregistered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
This commit is contained in:
Paul Bakaus
2026-09-03 13:14:48 -07:00
co-authored by Claude Fable 5.1
parent 074c6a715d
commit 88ccc9c42b
6 changed files with 306 additions and 308 deletions
+99 -1
View File
@@ -13,7 +13,9 @@ use once_cell::sync::Lazy;
use regex::Regex;
use sha2::{Digest, Sha256};
use crate::providers::{provider_display_name, Scope, Sys, API_BASE, PROVIDER_DIRS};
use crate::providers::{
opencode_global_config_dir, provider_display_name, Scope, Sys, API_BASE, PROVIDER_DIRS,
};
use crate::util::{self, jsp};
/// Ceiling on any single download this crate performs (triage C4). The
@@ -423,6 +425,31 @@ pub fn is_up_to_date(sys: &Sys, root: &str, providers: &[&str], bundle_dir: &str
}
}
}
// Provider command artifacts (OpenCode's `commands/impeccable.md`) are
// part of "current" too: an install whose skills match but whose
// bridge is missing or drifted must refresh, otherwise
// reinstall/update report success while the slash command stays
// absent (#483). Only bundle-shipped files are checked, so pinned or
// user commands never affect freshness. The commands dir sits next to
// the matched skills dir, so deriving it from `local_skills_dir` stays
// correct for every layout `copy_provider_commands` can write.
let bundle_commands_dir = jsp::join(&[bundle_dir, provider, "commands"]);
if util::exists(&bundle_commands_dir) {
let local_commands_dir = jsp::join(&[&jsp::dirname(&local_skills_dir), "commands"]);
for entry in util::read_dir_names(&bundle_commands_dir).unwrap_or_default() {
let bundle_file = jsp::join(&[&bundle_commands_dir, &entry]);
if !util::is_file(&bundle_file) {
continue;
}
let local_file = jsp::join(&[&local_commands_dir, &entry]);
if !util::exists(&local_file) {
return Ok(false);
}
if hash_skill_file(&bundle_file)? != hash_skill_file(&local_file)? {
return Ok(false);
}
}
}
if !provider_agents_up_to_date(bundle_dir, root, provider, agent_scope)? {
return Ok(false);
}
@@ -497,6 +524,77 @@ pub fn copy_provider_skills(sys: &Sys, bundle_dir: &str, root: &str, targets: &[
Ok(written)
}
/// JS: skills.mjs#providerCommandsDir. Project installs land at
/// `<root>/<configDir>/commands`; a user-scope OpenCode install must target
/// the config dir OpenCode actually scans.
fn provider_commands_dir(sys: &Sys, root: &str, provider_entry: &str, scope: Option<Scope>) -> String {
if scope == Some(Scope::User) {
jsp::join(&[&opencode_global_config_dir(&sys.env, root), "commands"])
} else {
jsp::join(&[root, provider_entry, "commands"])
}
}
/// JS: skills.mjs#copyProviderCommands(bundleDir, root, targets, {scope}).
///
/// OpenCode discovers custom commands from `{command,commands}/**.md` under
/// any active config dir, so this mirrors `copy_provider_skills`: project
/// scope writes `<root>/<configDir>/commands/`, user scope writes
/// `opencode_global_config_dir(home)/commands`.
///
/// Migration guard: a pre-#406 global OpenCode install at
/// `~/.opencode/commands/` is not scanned by OpenCode. After a global
/// install, the commands just written are removed from the stranded legacy
/// copy, sibling commands stay put, symlinked legacy dirs are skipped
/// (deleting through a symlink would empty the real target), and a
/// home-rooted git repo is left alone.
pub fn copy_provider_commands(sys: &Sys, bundle_dir: &str, root: &str, targets: &[&str], scope: Option<Scope>) -> usize {
let mut written = 0usize;
for target in targets {
let dotted = format!(".{target}");
let provider_entry: &str = if PROVIDER_DIRS.contains(&dotted.as_str()) {
&dotted
} else {
target
};
let src_dir = jsp::join(&[bundle_dir, provider_entry, "commands"]);
if !util::exists(&src_dir) {
continue;
}
let local_commands_dir = provider_commands_dir(sys, root, provider_entry, scope);
let _ = std::fs::create_dir_all(&local_commands_dir);
let entries = util::read_dir_names(&src_dir).unwrap_or_default();
for entry in &entries {
let src = jsp::join(&[&src_dir, entry]);
if !util::is_file(&src) {
continue;
}
let dest = jsp::join(&[&local_commands_dir, entry]);
util::rm_rf(&dest);
if std::fs::copy(&src, &dest).is_ok() {
written += 1;
}
}
if scope == Some(Scope::User) && provider_entry == ".opencode" {
let legacy_dir = jsp::join(&[root, ".opencode", "commands"]);
let migratable = util::exists(&legacy_dir)
&& !util::is_symlink(&legacy_dir)
&& util::realpath(&legacy_dir) != util::realpath(&local_commands_dir)
&& !util::exists(&jsp::join(&[root, ".git"]));
if migratable {
for entry in &entries {
if !util::is_file(&jsp::join(&[&src_dir, entry])) {
continue;
}
util::rm_rf(&jsp::join(&[&legacy_dir, entry]));
}
util::rmdir(&legacy_dir);
}
}
}
written
}
/// JS: refreshProviderSkills(bundleDir, root, providers, scope). Returns the
/// skill directories refreshed.
pub fn refresh_provider_skills(sys: &Sys, bundle_dir: &str, root: &str, providers: &[&str], scope: Option<Scope>) -> Result<Vec<String>, String> {
+9
View File
@@ -480,6 +480,12 @@ fn link(flags: &[String], io: &mut Io) -> R<()> {
}
}
let result = bundle::link_provider_skills(io, &source.bundle_root, &root, &targets, force).map_err(Flow::Throw)?;
// Linked installs are excluded from install/update refreshes (overwriting
// a symlink would destroy the link), so this is the only path that can
// deliver the OpenCode command bridge to them. A copy, not a symlink: the
// bridge is static and OpenCode scans the real commands dir. No-ops when
// the source checkout has no built commands (#483).
bundle::copy_provider_commands(&sys, &source.bundle_root, &root, &targets, Some(Scope::Project));
if result.linked == 0 && result.already == 0 {
if result.skipped > 0 {
err(io, "Nothing was linked because matching skill folders already exist.");
@@ -598,6 +604,7 @@ fn install(flags: &[String], io: &mut Io) -> R<()> {
updated = refreshed.len();
let agents = bundle::copy_provider_agents(&sys, &bdir, &install_root, &copy_targets, scope_opt)?;
bundle::report_provider_agents(&sys, io, &agents);
bundle::copy_provider_commands(&sys, &bdir, &install_root, &copy_targets, scope_opt);
let v = sys.get_skills_version(&install_root, scope_opt);
out(io, &format!("Updated {updated} skill(s){}.", to_version_suffix(&v)));
install_engine_binaries(&sys, io, &refreshed);
@@ -682,6 +689,7 @@ fn install(flags: &[String], io: &mut Io) -> R<()> {
let outcome: Result<(Vec<String>, Vec<bundle::AgentResult>, Vec<&'static str>), String> = (|| {
let written = bundle::copy_provider_skills(&sys, &bundle_dir, &install_root, &targets, scope_opt)?;
let agents = bundle::copy_provider_agents(&sys, &bundle_dir, &install_root, &targets, scope_opt)?;
bundle::copy_provider_commands(&sys, &bundle_dir, &install_root, &targets, scope_opt);
let hooks = if want_hooks {
copy_provider_hooks(&sys, &bundle_dir, &hook_root, &targets, force, Some(&install_root))?
} else {
@@ -862,6 +870,7 @@ fn update(flags: &[String], io: &mut Io) -> R<()> {
let refreshed = bundle::refresh_provider_skills(&sys, &tmp_dir, &root, &copy_providers, scope).map_err(Flow::Throw)?;
let agents = bundle::copy_provider_agents(&sys, &tmp_dir, &root, &copy_providers, agent_scope).map_err(Flow::Throw)?;
bundle::report_provider_agents(&sys, io, &agents);
bundle::copy_provider_commands(&sys, &tmp_dir, &root, &copy_providers, scope);
// Repair any stale pre-launcher (`node .../hook.mjs`) manifest a v3
// install left behind, regardless of hook consent (triage E8).
hook_manifest::repair_stale_hook_manifests(&sys, &root, &copy_providers, None).map_err(Flow::Throw)?;
+5
View File
@@ -71,6 +71,11 @@ pub fn is_dir(p: &str) -> bool {
std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false)
}
/// `fs.statSync(p).isFile()`; false on error.
pub fn is_file(p: &str) -> bool {
std::fs::metadata(p).map(|m| m.is_file()).unwrap_or(false)
}
/// `fs.lstatSync(p).isSymbolicLink()`; false on error.
pub fn is_symlink(p: &str) -> bool {
std::fs::symlink_metadata(p)
@@ -0,0 +1,193 @@
//! Port of `tests/copy-provider-commands.test.js` (upstream 9736a9f6, #483):
//! the OpenCode command bridge the installer writes, and the command
//! awareness `isUpToDate` gained so a drifted or missing bridge is not
//! reported as current.
use std::collections::HashMap;
use impeccable_skills::bundle::{copy_provider_commands, is_up_to_date};
use impeccable_skills::providers::{Scope, Sys};
fn temp_root(name: &str) -> String {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!("impeccable-{name}-{}-{nanos}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp root");
dir.canonicalize().unwrap().to_string_lossy().into_owned()
}
fn write(path: &str, content: &str) {
let p = std::path::Path::new(path);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, content).unwrap();
}
fn exists(path: &str) -> bool {
std::path::Path::new(path).exists()
}
fn sys_with(home: &str, extra: &[(&str, &str)]) -> Sys {
let mut env: HashMap<String, String> = HashMap::new();
env.insert("HOME".into(), home.to_string());
env.insert("USERPROFILE".into(), home.to_string());
for (k, v) in extra {
env.insert((*k).to_string(), (*v).to_string());
}
Sys::new(env, home.to_string())
}
/// JS: setupBundleWithCommand(bundleDir, providerName, commandNames)
fn bundle_with_commands(bundle: &str, provider: &str, names: &[&str]) {
for name in names {
write(
&format!("{bundle}/{provider}/commands/{name}.md"),
&format!("description: Impeccable {name} bridge\nagent: build\nsubtask: true\n\nbody {name}\n"),
);
}
}
fn bundle_with_skill(bundle: &str, provider: &str, body: &str) {
write(&format!("{bundle}/{provider}/skills/impeccable/SKILL.md"), body);
}
#[test]
fn writes_commands_to_project_config_dir_by_default() {
let root = temp_root("cmd-project");
let bundle = format!("{root}/bundle");
let project = format!("{root}/project");
std::fs::create_dir_all(&project).unwrap();
bundle_with_commands(&bundle, ".opencode", &["impeccable"]);
let sys = sys_with(&root, &[]);
let written = copy_provider_commands(&sys, &bundle, &project, &["opencode"], Some(Scope::Project));
assert_eq!(written, 1);
let dest = format!("{project}/.opencode/commands/impeccable.md");
assert!(exists(&dest));
assert!(std::fs::read_to_string(&dest).unwrap().contains("impeccable bridge"));
}
#[test]
fn user_scope_resolves_the_config_dir_opencode_scans() {
// Default: <home>/.config/opencode/commands.
let root = temp_root("cmd-user");
let bundle = format!("{root}/bundle");
let home = format!("{root}/home");
std::fs::create_dir_all(&home).unwrap();
bundle_with_commands(&bundle, ".opencode", &["impeccable"]);
let sys = sys_with(&home, &[]);
assert_eq!(
copy_provider_commands(&sys, &bundle, &home, &["opencode"], Some(Scope::User)),
1
);
assert!(exists(&format!("{home}/.config/opencode/commands/impeccable.md")));
// OPENCODE_CONFIG_DIR wins, and the default location stays untouched.
let root = temp_root("cmd-user-env");
let bundle = format!("{root}/bundle");
let home = format!("{root}/home");
let custom = format!("{root}/custom");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&custom).unwrap();
bundle_with_commands(&bundle, ".opencode", &["impeccable"]);
let sys = sys_with(&home, &[("OPENCODE_CONFIG_DIR", &custom)]);
assert_eq!(
copy_provider_commands(&sys, &bundle, &home, &["opencode"], Some(Scope::User)),
1
);
assert!(exists(&format!("{custom}/commands/impeccable.md")));
assert!(!exists(&format!("{home}/.config/opencode/commands")));
// XDG_CONFIG_HOME/opencode when OPENCODE_CONFIG_DIR is unset.
let root = temp_root("cmd-user-xdg");
let bundle = format!("{root}/bundle");
let home = format!("{root}/home");
let xdg = format!("{root}/xdg");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&xdg).unwrap();
bundle_with_commands(&bundle, ".opencode", &["impeccable"]);
let sys = sys_with(&home, &[("XDG_CONFIG_HOME", &xdg)]);
assert_eq!(
copy_provider_commands(&sys, &bundle, &home, &["opencode"], Some(Scope::User)),
1
);
assert!(exists(&format!("{xdg}/opencode/commands/impeccable.md")));
}
#[test]
fn migrates_legacy_global_commands_without_disturbing_siblings() {
let root = temp_root("cmd-migrate");
let bundle = format!("{root}/bundle");
let home = format!("{root}/home");
bundle_with_commands(&bundle, ".opencode", &["impeccable"]);
write(&format!("{home}/.opencode/commands/impeccable.md"), "stale\n");
write(&format!("{home}/.opencode/commands/mine.md"), "keep me\n");
let sys = sys_with(&home, &[]);
assert_eq!(
copy_provider_commands(&sys, &bundle, &home, &["opencode"], Some(Scope::User)),
1
);
assert!(exists(&format!("{home}/.config/opencode/commands/impeccable.md")));
// The stranded copy loses only what was just written.
assert!(!exists(&format!("{home}/.opencode/commands/impeccable.md")));
assert!(exists(&format!("{home}/.opencode/commands/mine.md")));
}
#[test]
fn a_home_rooted_git_repo_is_a_project_install_and_is_left_alone() {
let root = temp_root("cmd-git");
let bundle = format!("{root}/bundle");
let home = format!("{root}/home");
bundle_with_commands(&bundle, ".opencode", &["impeccable"]);
write(&format!("{home}/.opencode/commands/impeccable.md"), "project copy\n");
std::fs::create_dir_all(format!("{home}/.git")).unwrap();
let sys = sys_with(&home, &[]);
copy_provider_commands(&sys, &bundle, &home, &["opencode"], Some(Scope::User));
assert!(exists(&format!("{home}/.opencode/commands/impeccable.md")));
}
#[test]
fn a_provider_without_a_commands_dir_writes_nothing() {
let root = temp_root("cmd-none");
let bundle = format!("{root}/bundle");
let project = format!("{root}/project");
std::fs::create_dir_all(&project).unwrap();
bundle_with_skill(&bundle, ".claude", "---\nname: impeccable\n---\n");
let sys = sys_with(&root, &[]);
assert_eq!(
copy_provider_commands(&sys, &bundle, &project, &["claude"], Some(Scope::Project)),
0
);
assert!(!exists(&format!("{project}/.claude/commands")));
}
#[test]
fn is_up_to_date_tracks_the_command_bridge() {
let root = temp_root("cmd-fresh");
let bundle = format!("{root}/bundle");
let project = format!("{root}/project");
let skill = "---\nname: impeccable\nversion: 1.0.0\n---\n";
bundle_with_skill(&bundle, ".opencode", skill);
bundle_with_commands(&bundle, ".opencode", &["impeccable"]);
write(&format!("{project}/.opencode/skills/impeccable/SKILL.md"), skill);
let sys = sys_with(&root, &[]);
// Skills match but the bridge is missing.
assert!(!is_up_to_date(&sys, &project, &[".opencode"], &bundle, Some(Scope::Project), Some(Scope::Project)).unwrap());
// Bridge in place and identical.
copy_provider_commands(&sys, &bundle, &project, &["opencode"], Some(Scope::Project));
assert!(is_up_to_date(&sys, &project, &[".opencode"], &bundle, Some(Scope::Project), Some(Scope::Project)).unwrap());
// Drifted content is not current.
write(&format!("{project}/.opencode/commands/impeccable.md"), "drifted\n");
assert!(!is_up_to_date(&sys, &project, &[".opencode"], &bundle, Some(Scope::Project), Some(Scope::Project)).unwrap());
// A local-only command (a pinned shortcut) does not affect freshness.
copy_provider_commands(&sys, &bundle, &project, &["opencode"], Some(Scope::Project));
write(&format!("{project}/.opencode/commands/impeccable-polish.md"), "pinned\n");
assert!(is_up_to_date(&sys, &project, &[".opencode"], &bundle, Some(Scope::Project), Some(Scope::Project)).unwrap());
}
-1
View File
@@ -36,7 +36,6 @@ export const SUITES = {
runner: 'bun',
files: [
'tests/build.test.js',
'tests/copy-provider-commands.test.js',
'tests/lib/provider-blocks.test.js',
'tests/lib/transformers/provider-blocks.test.js',
'tests/lib/utils.test.js',
-306
View File
@@ -1,306 +0,0 @@
/**
* Tests for copyProviderCommands. Mirrors the PR #417 migration guards for the
* skills path, applied to <provider>/commands. OpenCode discovers custom
* commands from {command,commands}/**.md in the active config dir, so a
* global install must target $OPENCODE_CONFIG_DIR/commands, $XDG_CONFIG_HOME/
* opencode/commands, or ~/.config/opencode/commands (in that order), never
* ~/.opencode/commands which OpenCode does not scan.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import fs from 'fs';
import path from 'path';
import os from 'os';
import {
mkdtempSync,
mkdirSync,
writeFileSync,
readFileSync,
existsSync,
symlinkSync,
rmSync,
realpathSync,
lstatSync,
} from 'fs';
import { tmpdir } from 'os';
import {
copyProviderCommands,
isUpToDate,
opencodeGlobalConfigDir,
} from '../cli/bin/commands/skills.mjs';
function setupBundleWithCommand(bundleDir, providerName, commandNames) {
mkdirSync(path.join(bundleDir, providerName, 'commands'), { recursive: true });
for (const name of commandNames) {
const file = path.join(bundleDir, providerName, 'commands', `${name}.md`);
writeFileSync(
file,
`description: Impeccable ${name} bridge\nagent: build\nsubtask: true\n\nbody ${name}\n`,
);
}
}
beforeEach(() => {
process.env.IMPECCABLE_BUNDLE_PATH = '';
delete process.env.OPENCODE_CONFIG_DIR;
delete process.env.XDG_CONFIG_HOME;
});
afterEach(() => {
delete process.env.OPENCODE_CONFIG_DIR;
delete process.env.XDG_CONFIG_HOME;
});
describe('copyProviderCommands', () => {
test('writes commands to project .opencode/commands by default', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const project = mkdtempSync(path.join(tmpdir(), 'imp-cmd-proj-'));
setupBundleWithCommand(bundle, '.opencode', ['impeccable']);
try {
const written = copyProviderCommands(bundle, project, ['opencode'], { scope: 'project' });
expect(written).toBe(1);
const dest = path.join(project, '.opencode', 'commands', 'impeccable.md');
expect(existsSync(dest)).toBe(true);
expect(readFileSync(dest, 'utf8')).toContain('impeccable bridge');
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(project, { recursive: true, force: true });
}
});
test('writes commands to ~/.config/opencode/commands for global scope', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const home = mkdtempSync(path.join(tmpdir(), 'imp-cmd-home-'));
setupBundleWithCommand(bundle, '.opencode', ['impeccable']);
try {
const written = copyProviderCommands(bundle, home, ['opencode'], { scope: 'user' });
expect(written).toBe(1);
const dest = path.join(home, '.config', 'opencode', 'commands', 'impeccable.md');
expect(existsSync(dest)).toBe(true);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(home, { recursive: true, force: true });
}
});
test('honours OPENCODE_CONFIG_DIR for global scope', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const home = mkdtempSync(path.join(tmpdir(), 'imp-cmd-home-'));
const customDir = mkdtempSync(path.join(tmpdir(), 'imp-cmd-custom-'));
setupBundleWithCommand(bundle, '.opencode', ['impeccable']);
try {
process.env.OPENCODE_CONFIG_DIR = customDir;
const written = copyProviderCommands(bundle, home, ['opencode'], { scope: 'user' });
expect(written).toBe(1);
const dest = path.join(customDir, 'commands', 'impeccable.md');
expect(existsSync(dest)).toBe(true);
expect(existsSync(path.join(home, '.config', 'opencode', 'commands'))).toBe(false);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(home, { recursive: true, force: true });
rmSync(customDir, { recursive: true, force: true });
}
});
test('honours XDG_CONFIG_HOME/opencode/commands when OPENCODE_CONFIG_DIR is unset', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const home = mkdtempSync(path.join(tmpdir(), 'imp-cmd-home-'));
const xdgRoot = mkdtempSync(path.join(tmpdir(), 'imp-cmd-xdg-'));
setupBundleWithCommand(bundle, '.opencode', ['impeccable']);
try {
process.env.XDG_CONFIG_HOME = xdgRoot;
const written = copyProviderCommands(bundle, home, ['opencode'], { scope: 'user' });
expect(written).toBe(1);
const dest = path.join(xdgRoot, 'opencode', 'commands', 'impeccable.md');
expect(existsSync(dest)).toBe(true);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(home, { recursive: true, force: true });
rmSync(xdgRoot, { recursive: true, force: true });
}
});
test('migrates legacy ~/.opencode/commands entries without disturbing siblings', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const home = mkdtempSync(path.join(tmpdir(), 'imp-cmd-home-'));
setupBundleWithCommand(bundle, '.opencode', ['impeccable']);
// Pre-seed a legacy copy with both a command we want to replace and a
// sibling the install must NOT touch.
const legacyDir = path.join(home, '.opencode', 'commands');
mkdirSync(legacyDir, { recursive: true });
writeFileSync(path.join(legacyDir, 'impeccable.md'), 'stale impeccable\n');
writeFileSync(path.join(legacyDir, 'unrelated-command.md'), 'unrelated\n');
try {
const written = copyProviderCommands(bundle, home, ['opencode'], { scope: 'user' });
expect(written).toBe(1);
const dest = path.join(home, '.config', 'opencode', 'commands', 'impeccable.md');
expect(existsSync(dest)).toBe(true);
expect(existsSync(path.join(legacyDir, 'impeccable.md'))).toBe(false);
expect(existsSync(path.join(legacyDir, 'unrelated-command.md'))).toBe(true);
expect(readFileSync(path.join(legacyDir, 'unrelated-command.md'), 'utf8')).toBe('unrelated\n');
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(home, { recursive: true, force: true });
}
});
test('does not migrate a symlinked legacy dir (shared storage)', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const home = mkdtempSync(path.join(tmpdir(), 'imp-cmd-home-'));
const shared = mkdtempSync(path.join(tmpdir(), 'imp-cmd-shared-'));
setupBundleWithCommand(bundle, '.opencode', ['impeccable']);
mkdirSync(path.join(home, '.opencode'), { recursive: true });
symlinkSync(shared, path.join(home, '.opencode', 'commands'), 'dir');
writeFileSync(path.join(shared, 'unrelated-command.md'), 'unrelated\n');
try {
copyProviderCommands(bundle, home, ['opencode'], { scope: 'user' });
expect(existsSync(path.join(shared, 'unrelated-command.md'))).toBe(true);
expect(lstatSync(path.join(home, '.opencode', 'commands')).isSymbolicLink()).toBe(true);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(home, { recursive: true, force: true });
rmSync(shared, { recursive: true, force: true });
}
});
test('returns 0 when the bundle has no commands dir', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const project = mkdtempSync(path.join(tmpdir(), 'imp-cmd-proj-'));
try {
const written = copyProviderCommands(bundle, project, ['opencode'], { scope: 'project' });
expect(written).toBe(0);
expect(existsSync(path.join(project, '.opencode', 'commands'))).toBe(false);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(project, { recursive: true, force: true });
}
});
test('ignores providers without a commands directory', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const project = mkdtempSync(path.join(tmpdir(), 'imp-cmd-proj-'));
mkdirSync(path.join(bundle, 'claude'), { recursive: true });
try {
const written = copyProviderCommands(bundle, project, ['claude'], { scope: 'project' });
expect(written).toBe(0);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(project, { recursive: true, force: true });
}
});
});
describe('isUpToDate command awareness', () => {
function setupBundleWithSkill(bundleDir, providerName, { withCommands = true } = {}) {
const skillDir = path.join(bundleDir, providerName, 'skills', 'impeccable');
mkdirSync(path.join(skillDir, 'scripts'), { recursive: true });
writeFileSync(path.join(skillDir, 'SKILL.md'), '---\nname: impeccable\n---\nBundle skill.\n');
writeFileSync(path.join(skillDir, 'scripts', 'context.mjs'), 'console.log("bundle");\n');
if (withCommands) setupBundleWithCommand(bundleDir, providerName, ['impeccable']);
}
function mirrorBundleSkills(bundleDir, root, providerName) {
fs.cpSync(
path.join(bundleDir, providerName, 'skills'),
path.join(root, providerName, 'skills'),
{ recursive: true },
);
}
function mirrorBundleCommands(bundleDir, root, providerName) {
fs.cpSync(
path.join(bundleDir, providerName, 'commands'),
path.join(root, providerName, 'commands'),
{ recursive: true },
);
}
test('returns false when skills match but the command bridge is missing', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const project = mkdtempSync(path.join(tmpdir(), 'imp-cmd-proj-'));
setupBundleWithSkill(bundle, '.opencode');
mirrorBundleSkills(bundle, project, '.opencode');
try {
expect(isUpToDate(project, ['.opencode'], bundle, 'project')).toBe(false);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(project, { recursive: true, force: true });
}
});
test('returns true when skills and commands match the bundle', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const project = mkdtempSync(path.join(tmpdir(), 'imp-cmd-proj-'));
setupBundleWithSkill(bundle, '.opencode');
mirrorBundleSkills(bundle, project, '.opencode');
mirrorBundleCommands(bundle, project, '.opencode');
try {
expect(isUpToDate(project, ['.opencode'], bundle, 'project')).toBe(true);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(project, { recursive: true, force: true });
}
});
test('returns false when the command bridge content drifted', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const project = mkdtempSync(path.join(tmpdir(), 'imp-cmd-proj-'));
setupBundleWithSkill(bundle, '.opencode');
mirrorBundleSkills(bundle, project, '.opencode');
mirrorBundleCommands(bundle, project, '.opencode');
writeFileSync(path.join(project, '.opencode', 'commands', 'impeccable.md'), 'user edit drift\n');
try {
expect(isUpToDate(project, ['.opencode'], bundle, 'project')).toBe(false);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(project, { recursive: true, force: true });
}
});
test('ignores local-only command files such as pinned shortcuts', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const project = mkdtempSync(path.join(tmpdir(), 'imp-cmd-proj-'));
setupBundleWithSkill(bundle, '.opencode');
mirrorBundleSkills(bundle, project, '.opencode');
mirrorBundleCommands(bundle, project, '.opencode');
writeFileSync(path.join(project, '.opencode', 'commands', 'impeccable-audit.md'), 'pinned\n');
try {
expect(isUpToDate(project, ['.opencode'], bundle, 'project')).toBe(true);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(project, { recursive: true, force: true });
}
});
test('ignores providers whose bundle has no commands directory', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const project = mkdtempSync(path.join(tmpdir(), 'imp-cmd-proj-'));
setupBundleWithSkill(bundle, '.opencode', { withCommands: false });
mirrorBundleSkills(bundle, project, '.opencode');
try {
expect(isUpToDate(project, ['.opencode'], bundle, 'project')).toBe(true);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(project, { recursive: true, force: true });
}
});
test('user scope resolves the commands dir via OPENCODE_CONFIG_DIR', () => {
const bundle = mkdtempSync(path.join(tmpdir(), 'imp-cmd-bundle-'));
const home = mkdtempSync(path.join(tmpdir(), 'imp-cmd-home-'));
const custom = mkdtempSync(path.join(tmpdir(), 'imp-cmd-custom-'));
setupBundleWithSkill(bundle, '.opencode');
process.env.OPENCODE_CONFIG_DIR = custom;
// User-scope OpenCode skills live at <config>/skills (HOME_SKILLS_DIR_OVERRIDES).
fs.cpSync(path.join(bundle, '.opencode', 'skills'), path.join(custom, 'skills'), { recursive: true });
try {
expect(isUpToDate(home, ['.opencode'], bundle, 'user')).toBe(false);
fs.cpSync(path.join(bundle, '.opencode', 'commands'), path.join(custom, 'commands'), { recursive: true });
expect(isUpToDate(home, ['.opencode'], bundle, 'user')).toBe(true);
} finally {
rmSync(bundle, { recursive: true, force: true });
rmSync(home, { recursive: true, force: true });
rmSync(custom, { recursive: true, force: true });
}
});
});