Fix OpenCode slash command bridge (#483)

Add a first-class OpenCode command bridge across builds, installs, updates, linked installs, and pinned shortcuts. Preserve current provider behavior while backfilling missing or drifted command files.\n\nAI assistance: contributor and maintainer work used AI tools as disclosed in the PR discussion and commits.
This commit is contained in:
4nibhal
2026-09-01 19:05:37 -04:00
committed by GitHub
parent 74cf3ec605
commit 9736a9f6e9
12 changed files with 1118 additions and 11 deletions
+306
View File
@@ -0,0 +1,306 @@
/**
* 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 });
}
});
});
@@ -0,0 +1,109 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import fs from 'fs';
import path from 'path';
import { createTransformer } from '../../../scripts/lib/transformers/factory.js';
import { PROVIDERS } from '../../../scripts/lib/transformers/providers.js';
const config = PROVIDERS.opencode;
const transform = createTransformer(config);
const TEST_DIR = path.join(process.cwd(), 'test-tmp-opencode-commands');
const COMMAND_PATH = path.join(
TEST_DIR,
`${config.provider}/${config.configDir}/commands/impeccable.md`,
);
const SAMPLE_SKILL = {
name: 'impeccable',
description: 'Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface.',
body: '# Impeccable\n\nSkill body here.',
references: [],
scripts: [],
agents: [],
};
beforeEach(() => {
if (fs.existsSync(TEST_DIR)) {
fs.rmSync(TEST_DIR, { recursive: true, force: true });
}
});
afterEach(() => {
if (fs.existsSync(TEST_DIR)) {
fs.rmSync(TEST_DIR, { recursive: true, force: true });
}
});
describe('opencode commands bridge', () => {
test('emits .opencode/commands/impeccable.md alongside the skill', () => {
transform([SAMPLE_SKILL], TEST_DIR);
expect(fs.existsSync(COMMAND_PATH)).toBe(true);
});
test('command frontmatter uses only fields OpenCode recognises', () => {
transform([SAMPLE_SKILL], TEST_DIR);
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
const fm = content.match(/^---\n([\s\S]*?)\n---/);
expect(fm).not.toBeNull();
const lines = fm[1].split('\n').map(l => l.trim()).filter(Boolean);
const keys = lines.map(l => l.split(':')[0]);
// OpenCode only recognises: description, agent, model, variant, subtask (per
// opencode/packages/core/src/v1/config/command.ts:5-13).
const allowed = new Set(['description', 'agent', 'model', 'variant', 'subtask']);
for (const key of keys) {
expect(allowed.has(key)).toBe(true);
}
});
test('command description mirrors the skill description exactly', () => {
transform([SAMPLE_SKILL], TEST_DIR);
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
const fm = content.match(/^---\n([\s\S]*?)\n---/)[1];
const line = fm.split('\n').find(l => l.startsWith('description:'));
const value = line.slice('description:'.length).trim().replace(/^"(.*)"$/, '$1');
expect(value).toBe(SAMPLE_SKILL.description);
});
test('command body delegates to the impeccable skill', () => {
transform([SAMPLE_SKILL], TEST_DIR);
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
const body = content.replace(/^---\n[\s\S]*?\n---\n/, '');
expect(body).toContain('skill({');
expect(body).toContain("name: \"impeccable\"");
expect(body).toContain('Setup');
expect(body).toContain('Commands');
expect(body).toContain('$ARGUMENTS');
});
test('command declares agent: build and subtask: true', () => {
transform([SAMPLE_SKILL], TEST_DIR);
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
expect(content).toMatch(/^agent: build$/m);
expect(content).toMatch(/^subtask: true$/m);
});
test('does not emit Claude-only frontmatter fields on the command', () => {
transform([SAMPLE_SKILL], TEST_DIR);
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
const fm = content.match(/^---\n([\s\S]*?)\n---/)[1];
expect(fm).not.toMatch(/^version:/m);
expect(fm).not.toMatch(/^user-invocable:/m);
expect(fm).not.toMatch(/^argument-hint:/m);
expect(fm).not.toMatch(/^allowed-tools:/m);
});
test('emits no command when the skill is empty', () => {
transform([], TEST_DIR);
expect(fs.existsSync(path.dirname(COMMAND_PATH))).toBe(false);
});
test('keeps emitting the skill alongside the command', () => {
transform([SAMPLE_SKILL], TEST_DIR);
const skillPath = path.join(
TEST_DIR,
`${config.provider}/${config.configDir}/skills/impeccable/SKILL.md`,
);
expect(fs.existsSync(skillPath)).toBe(true);
expect(fs.existsSync(COMMAND_PATH)).toBe(true);
});
});
+208
View File
@@ -8,6 +8,18 @@ import { spawnSync } from 'node:child_process';
const ROOT = process.cwd();
const PIN_SCRIPT = path.join(ROOT, 'skill', 'scripts', 'pin.mjs');
// Neutralize any real user-scope OpenCode config so tests never write into the
// developer's actual global install. Points the resolution at a path that does
// not exist unless a test creates it.
function cleanEnv(overrides = {}) {
return {
...process.env,
OPENCODE_CONFIG_DIR: path.join(os.tmpdir(), 'impeccable-pin-no-config'),
XDG_CONFIG_HOME: path.join(os.tmpdir(), 'impeccable-pin-no-xdg'),
...overrides,
};
}
describe('pin command provider syntax', () => {
let project;
@@ -27,6 +39,7 @@ describe('pin command provider syntax', () => {
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv(),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
@@ -49,3 +62,198 @@ describe('pin command provider syntax', () => {
}
});
});
describe('pin command OpenCode target', () => {
let project;
beforeEach(() => {
project = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-pin-oc-'));
fs.writeFileSync(path.join(project, 'package.json'), '{}\n');
fs.mkdirSync(path.join(project, '.opencode', 'skills', 'impeccable'), { recursive: true });
});
afterEach(() => {
fs.rmSync(project, { recursive: true, force: true });
});
it('writes a slash command bridge for OpenCode, not a skill shortcut', () => {
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv(),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
const commandPath = path.join(project, '.opencode', 'commands', 'impeccable-audit.md');
assert.ok(fs.existsSync(commandPath), `expected ${commandPath}`);
const content = fs.readFileSync(commandPath, 'utf8');
assert.match(content, /---\ndescription:.*audit/);
assert.match(content, /agent: build/);
assert.match(content, /subtask: true/);
assert.match(content, /<skill-base-dir>\/reference\/audit\.md/);
assert.doesNotMatch(content, /user-invocable:/);
assert.doesNotMatch(content, /argument-hint:/);
const skillPath = path.join(project, '.opencode', 'skills', 'audit', 'SKILL.md');
assert.equal(fs.existsSync(skillPath), false, 'OpenCode pin must not create a skill shortcut');
});
it('unpin removes only the OpenCode command bridge', () => {
spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], { cwd: project, encoding: 'utf8', env: cleanEnv() });
const commandPath = path.join(project, '.opencode', 'commands', 'impeccable-audit.md');
assert.ok(fs.existsSync(commandPath));
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'unpin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv(),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(fs.existsSync(commandPath), false);
});
it('unpin cleans the project command after the skill was removed', () => {
spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], { cwd: project, encoding: 'utf8', env: cleanEnv() });
const commandPath = path.join(project, '.opencode', 'commands', 'impeccable-audit.md');
assert.ok(fs.existsSync(commandPath));
// Skill removed before unpin (e.g. uninstall): cleanup must still find the pin.
fs.rmSync(path.join(project, '.opencode', 'skills', 'impeccable'), { recursive: true, force: true });
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'unpin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv(),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(fs.existsSync(commandPath), false, 'stale pin must be removed after skill removal');
});
it('unpin after skill removal leaves non-pinned user commands alone', () => {
const commandsDir = path.join(project, '.opencode', 'commands');
fs.mkdirSync(commandsDir, { recursive: true });
const commandPath = path.join(commandsDir, 'impeccable-audit.md');
fs.writeFileSync(commandPath, 'my own command, not a pin\n');
fs.rmSync(path.join(project, '.opencode', 'skills', 'impeccable'), { recursive: true, force: true });
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'unpin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv(),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.ok(fs.existsSync(commandPath), 'non-pinned user command must survive cleanup');
});
});
describe('pin command OpenCode user scope', () => {
let project;
let config;
beforeEach(() => {
project = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-pin-usr-'));
fs.writeFileSync(path.join(project, 'package.json'), '{}\n');
config = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-pin-cfg-'));
});
afterEach(() => {
fs.rmSync(project, { recursive: true, force: true });
fs.rmSync(config, { recursive: true, force: true });
});
function installUserScopeSkill(dir = config) {
fs.mkdirSync(path.join(dir, 'skills', 'impeccable'), { recursive: true });
}
it('pins into the user config dir when only a global install exists', () => {
installUserScopeSkill();
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv({ OPENCODE_CONFIG_DIR: config }),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.doesNotMatch(result.stdout, /No harness directories/);
const commandPath = path.join(config, 'commands', 'impeccable-audit.md');
assert.ok(fs.existsSync(commandPath), `expected ${commandPath}`);
assert.match(fs.readFileSync(commandPath, 'utf8'), /impeccable-pinned-command/);
assert.equal(
fs.existsSync(path.join(project, '.opencode', 'commands')),
false,
'must not create a project commands dir for a user-scope install',
);
});
it('unpin removes the user-scope pinned command', () => {
installUserScopeSkill();
spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv({ OPENCODE_CONFIG_DIR: config }),
});
const commandPath = path.join(config, 'commands', 'impeccable-audit.md');
assert.ok(fs.existsSync(commandPath));
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'unpin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv({ OPENCODE_CONFIG_DIR: config }),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(fs.existsSync(commandPath), false);
});
it('unpin removes the user-scope pinned command after the global skill was removed', () => {
installUserScopeSkill();
spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv({ OPENCODE_CONFIG_DIR: config }),
});
const commandPath = path.join(config, 'commands', 'impeccable-audit.md');
assert.ok(fs.existsSync(commandPath));
// Global skill removed before unpin: cleanup must still find the pin.
fs.rmSync(path.join(config, 'skills', 'impeccable'), { recursive: true, force: true });
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'unpin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv({ OPENCODE_CONFIG_DIR: config }),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(fs.existsSync(commandPath), false, 'stale user-scope pin must be removed after skill removal');
});
it('pins in both scopes when project and user installs coexist', () => {
installUserScopeSkill();
fs.mkdirSync(path.join(project, '.opencode', 'skills', 'impeccable'), { recursive: true });
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv({ OPENCODE_CONFIG_DIR: config }),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.ok(fs.existsSync(path.join(config, 'commands', 'impeccable-audit.md')), 'user-scope pin');
assert.ok(fs.existsSync(path.join(project, '.opencode', 'commands', 'impeccable-audit.md')), 'project pin');
});
it('honours XDG_CONFIG_HOME when OPENCODE_CONFIG_DIR is unset', () => {
const xdg = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-pin-xdg-'));
installUserScopeSkill(path.join(xdg, 'opencode'));
try {
const result = spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], {
cwd: project,
encoding: 'utf8',
env: cleanEnv({ OPENCODE_CONFIG_DIR: undefined, XDG_CONFIG_HOME: xdg }),
});
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.ok(fs.existsSync(path.join(xdg, 'opencode', 'commands', 'impeccable-audit.md')));
} finally {
fs.rmSync(xdg, { recursive: true, force: true });
}
});
});
+73
View File
@@ -0,0 +1,73 @@
/**
* Tests for syncRootCommands. The post-merge release sync must mirror
* generated provider command files (e.g. OpenCode's commands/impeccable.md)
* into the tracked root harness folders, or direct GitHub / submodule /
* npx-skills installs ship OpenCode without the slash command bridge (#483).
*/
import { describe, test, expect } from 'bun:test';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { syncRootCommands } from '../scripts/lib/root-commands-sync.mjs';
function setupDist(distDir, provider, configDir, commands) {
if (commands === null) return;
const dir = join(distDir, provider, configDir, 'commands');
mkdirSync(dir, { recursive: true });
for (const [name, body] of Object.entries(commands)) {
writeFileSync(join(dir, name), body);
}
}
describe('syncRootCommands', () => {
test('mirrors generated command files into the root harness folder', () => {
const dist = mkdtempSync(join(tmpdir(), 'imp-sync-dist-'));
const root = mkdtempSync(join(tmpdir(), 'imp-sync-root-'));
setupDist(dist, 'opencode', '.opencode', { 'impeccable.md': 'bridge v1\n' });
try {
const synced = syncRootCommands(dist, root, [{ provider: 'opencode', configDir: '.opencode' }]);
expect(synced).toEqual(['.opencode']);
expect(readFileSync(join(root, '.opencode', 'commands', 'impeccable.md'), 'utf8')).toBe('bridge v1\n');
} finally {
rmSync(dist, { recursive: true, force: true });
rmSync(root, { recursive: true, force: true });
}
});
test('preserves repo-local or pinned command files already at the destination', () => {
const dist = mkdtempSync(join(tmpdir(), 'imp-sync-dist-'));
const root = mkdtempSync(join(tmpdir(), 'imp-sync-root-'));
setupDist(dist, 'opencode', '.opencode', { 'impeccable.md': 'bridge v2\n' });
const destDir = join(root, '.opencode', 'commands');
mkdirSync(destDir, { recursive: true });
writeFileSync(join(destDir, 'impeccable-audit.md'), 'pinned by user\n');
writeFileSync(join(destDir, 'impeccable.md'), 'stale bridge\n');
try {
syncRootCommands(dist, root, [{ provider: 'opencode', configDir: '.opencode' }]);
expect(readFileSync(join(destDir, 'impeccable.md'), 'utf8')).toBe('bridge v2\n');
expect(readFileSync(join(destDir, 'impeccable-audit.md'), 'utf8')).toBe('pinned by user\n');
} finally {
rmSync(dist, { recursive: true, force: true });
rmSync(root, { recursive: true, force: true });
}
});
test('skips providers whose dist variant has no commands dir', () => {
const dist = mkdtempSync(join(tmpdir(), 'imp-sync-dist-'));
const root = mkdtempSync(join(tmpdir(), 'imp-sync-root-'));
setupDist(dist, 'opencode', '.opencode', { 'impeccable.md': 'bridge\n' });
setupDist(dist, 'claude-code', '.claude', null);
try {
const synced = syncRootCommands(dist, root, [
{ provider: 'opencode', configDir: '.opencode' },
{ provider: 'claude-code', configDir: '.claude' },
]);
expect(synced).toEqual(['.opencode']);
expect(existsSync(join(root, '.claude', 'commands'))).toBe(false);
} finally {
rmSync(dist, { recursive: true, force: true });
rmSync(root, { recursive: true, force: true });
}
});
});
+135 -1
View File
@@ -11,7 +11,7 @@
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { execSync, execFileSync } from 'child_process';
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, chmodSync, rmSync, lstatSync, realpathSync, readlinkSync, symlinkSync, statSync } from 'fs';
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, chmodSync, rmSync, lstatSync, realpathSync, readlinkSync, symlinkSync, statSync, cpSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import {
@@ -69,6 +69,18 @@ function createFakeLinkSource(root, providers = ['.claude']) {
for (const provider of providers) {
writeSkill(join(root, '.impeccable', 'dist', 'universal'), provider, 'impeccable');
}
if (providers.includes('.opencode')) {
const commandsDir = join(root, '.impeccable', 'dist', 'universal', '.opencode', 'commands');
mkdirSync(commandsDir, { recursive: true });
writeFileSync(join(commandsDir, 'impeccable.md'), [
'description: Impeccable impeccable bridge',
'agent: build',
'subtask: true',
'',
'body impeccable',
'',
].join('\n'));
}
}
function createFakeUniversalBundle(root, providers = ['.claude', '.agents', '.cursor']) {
@@ -111,6 +123,18 @@ function createFakeUniversalBundle(root, providers = ['.claude', '.agents', '.cu
hooks: { PostToolUse: [{ matcher: 'apply_patch', hooks: [{ type: 'command', command: 'node ".codex/skills/impeccable/scripts/hook.mjs"' }] }] },
}, null, 2));
}
if (providers.includes('.opencode')) {
const commandsDir = join(bundleRoot, '.opencode', 'commands');
mkdirSync(commandsDir, { recursive: true });
writeFileSync(join(commandsDir, 'impeccable.md'), [
'description: Impeccable impeccable bridge',
'agent: build',
'subtask: true',
'',
'body impeccable',
'',
].join('\n'));
}
if (providers.includes('.grok')) {
mkdirSync(join(bundleRoot, '.grok', 'hooks'), { recursive: true });
writeFileSync(join(bundleRoot, '.grok', 'hooks', 'impeccable.json'), JSON.stringify({
@@ -560,6 +584,23 @@ describe('skills install: already-installed detection', () => {
// ─── Submodule/link installs ────────────────────────────────────────────────
describe('skills link: submodule installs', () => {
test('writes the OpenCode command bridge alongside linked skills', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-link-bridge-'));
execSync('git init', { cwd: tmp });
createFakeLinkSource(tmp, ['.opencode']);
const output = run('skills link --source=.impeccable --providers=opencode -y', { cwd: tmp });
expect(output).toContain('Linked impeccable into: .opencode');
const dest = join(tmp, '.opencode', 'skills', 'impeccable');
expect(lstatSync(dest).isSymbolicLink()).toBe(true);
const bridge = join(tmp, '.opencode', 'commands', 'impeccable.md');
expect(existsSync(bridge)).toBe(true);
expect(readFileSync(bridge, 'utf8')).toContain('impeccable bridge');
rmSync(tmp, { recursive: true, force: true });
}, 15000);
test('creates relative skill symlinks from dist/universal', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-link-'));
execSync('git init', { cwd: tmp });
@@ -1754,6 +1795,99 @@ describe('skills install/update: local universal bundle e2e', () => {
rmSync(tmp, { recursive: true, force: true });
}, 15000);
test('reinstall backfills a missing OpenCode command bridge when skills are current', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-reinstall-backfill-'));
execSync('git init', { cwd: tmp });
const bundleRoot = createFakeUniversalBundle(tmp, ['.opencode']);
const env = { ...process.env, IMPECCABLE_BUNDLE_PATH: bundleRoot };
run('skills install -y --providers=opencode --no-hooks', { cwd: tmp, env });
const bridge = join(tmp, '.opencode', 'commands', 'impeccable.md');
expect(existsSync(bridge)).toBe(true);
rmSync(bridge);
const output = run('skills install -y --providers=opencode --no-hooks', { cwd: tmp, env });
expect(output).toContain('already installed');
expect(existsSync(bridge)).toBe(true);
rmSync(tmp, { recursive: true, force: true });
}, 15000);
test('skills update backfills a missing OpenCode command bridge when skills are current', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-update-backfill-'));
execSync('git init', { cwd: tmp });
const bundleRoot = createFakeUniversalBundle(tmp, ['.opencode']);
const env = { ...process.env, IMPECCABLE_BUNDLE_PATH: bundleRoot };
run('skills install -y --providers=opencode --no-hooks', { cwd: tmp, env });
const bridge = join(tmp, '.opencode', 'commands', 'impeccable.md');
expect(existsSync(bridge)).toBe(true);
rmSync(bridge);
run('skills update -y --no-hooks', { cwd: tmp, env });
expect(existsSync(bridge)).toBe(true);
rmSync(tmp, { recursive: true, force: true });
}, 15000);
test('skills update restores a command bridge whose content drifted', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-update-drifted-bridge-'));
execSync('git init', { cwd: tmp });
const bundleRoot = createFakeUniversalBundle(tmp, ['.opencode']);
const env = { ...process.env, IMPECCABLE_BUNDLE_PATH: bundleRoot };
run('skills install -y --providers=opencode --no-hooks', { cwd: tmp, env });
const bridge = join(tmp, '.opencode', 'commands', 'impeccable.md');
writeFileSync(bridge, 'user edit drift\n');
run('skills update -y --no-hooks', { cwd: tmp, env });
expect(readFileSync(bridge, 'utf8')).toContain('impeccable bridge');
rmSync(tmp, { recursive: true, force: true });
}, 15000);
test('skills check from the home dir recognises a global OpenCode install as current', () => {
// Bugbot scenario: `skills check` runs scope-less, so a home-rooted run
// matches the GLOBAL skills dir via HOME_SKILLS_DIR_OVERRIDES. The command
// bridge must be resolved next to that matched skills dir, not at
// <home>/.opencode/commands. os.homedir() only honours HOME at process
// start, so this must run through the CLI subprocess, not in-process.
const home = mkdtempSync(join(tmpdir(), 'imp-test-check-home-'));
execSync('git init', { cwd: home });
const bundleRoot = createFakeUniversalBundle(home, ['.opencode']);
const configHome = mkdtempSync(join(tmpdir(), 'imp-test-check-config-'));
cpSync(join(bundleRoot, '.opencode', 'skills'), join(configHome, 'skills'), { recursive: true });
cpSync(join(bundleRoot, '.opencode', 'commands'), join(configHome, 'commands'), { recursive: true });
const output = run('skills check', {
cwd: home,
env: { ...process.env, HOME: home, OPENCODE_CONFIG_DIR: configHome, IMPECCABLE_BUNDLE_PATH: bundleRoot },
});
expect(output).toContain('Skills are up to date');
rmSync(home, { recursive: true, force: true });
rmSync(configHome, { recursive: true, force: true });
}, 15000);
test('skills update leaves an intact command bridge and pinned siblings alone', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-update-bridge-intact-'));
execSync('git init', { cwd: tmp });
const bundleRoot = createFakeUniversalBundle(tmp, ['.opencode']);
const env = { ...process.env, IMPECCABLE_BUNDLE_PATH: bundleRoot };
run('skills install -y --providers=opencode --no-hooks', { cwd: tmp, env });
const bridge = join(tmp, '.opencode', 'commands', 'impeccable.md');
const pinned = join(tmp, '.opencode', 'commands', 'impeccable-audit.md');
writeFileSync(pinned, 'pinned by user\n');
const output = run('skills update -y --no-hooks', { cwd: tmp, env });
expect(output).toContain('Skills are up to date');
expect(readFileSync(bridge, 'utf8')).toContain('impeccable bridge');
expect(readFileSync(pinned, 'utf8')).toBe('pinned by user\n');
rmSync(tmp, { recursive: true, force: true });
}, 15000);
test('skills update reports malformed hook manifests cleanly on the up-to-date path', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-update-bad-hooks-'));
execSync('git init', { cwd: tmp });