Fix: install missing explicitly selected providers without --force (#536)

* Fix: install missing explicitly selected providers without --force (#500)

An explicit --providers list now treats "already installed" per selected
target: providers with an existing install take the update path, providers
with none get a fresh install (skills + hooks) in the same run. Previously
any existing install (e.g. .claude) made `install --providers=grok` exit 0
without writing .grok, leaving Grok Build on the Claude-variant fallback.

Written with AI assistance (Cursor agent), reviewed and tested by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix: copy provider agents for freshly installed mixed-install targets

Bugbot caught that the mixed explicit-providers path installed skills and
hooks for missing targets but skipped copyProviderAgents, which both the
update and fresh-install paths run. Written with AI assistance (Cursor agent).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-08-08 18:33:23 -07:00
committed by GitHub
co-authored by Cursor
parent 5d10bc842c
commit 477484aaee
2 changed files with 78 additions and 9 deletions
+51
View File
@@ -1446,6 +1446,57 @@ describe('skills install/update: local universal bundle e2e', () => {
rmSync(tmp, { recursive: true, force: true });
}, 15000);
test('explicit --providers installs a missing provider without --force (#500)', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-explicit-missing-'));
execSync('git init', { cwd: tmp });
const bundleRoot = createFakeUniversalBundle(tmp, ['.claude', '.cursor']);
// Seed an existing .claude install; .cursor has nothing yet.
const skillDir = join(tmp, '.claude', 'skills', 'impeccable');
mkdirSync(skillDir, { recursive: true });
writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: impeccable\nversion: 9.9.9-local\n---\nSeeded install.\n');
const output = run('skills install -y --providers=cursor --no-hooks', {
cwd: tmp,
env: { ...process.env, IMPECCABLE_BUNDLE_PATH: bundleRoot },
});
expect(output).toContain('Installed impeccable into: .cursor');
expect(readFileSync(join(tmp, '.cursor', 'skills', 'impeccable', 'SKILL.md'), 'utf8')).toContain('version: 9.9.9-local');
// The unselected .claude install is left alone.
expect(readFileSync(join(skillDir, 'SKILL.md'), 'utf8')).toContain('Seeded install.');
rmSync(tmp, { recursive: true, force: true });
}, 15000);
test('explicit --providers mixes per-target updates and fresh installs (#500)', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-explicit-mixed-'));
execSync('git init', { cwd: tmp });
const bundleRoot = createFakeUniversalBundle(tmp, ['.claude', '.cursor']);
// Stale .claude install; .cursor has nothing yet.
const skillDir = join(tmp, '.claude', 'skills', 'impeccable');
mkdirSync(join(skillDir, 'scripts'), { recursive: true });
writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: impeccable\nstale: .claude\n---\nOld content.\n');
writeFileSync(join(skillDir, 'scripts', 'context.mjs'), 'console.log("old script");\n');
const output = run('skills install -y --providers=claude,cursor', {
cwd: tmp,
env: { ...process.env, IMPECCABLE_BUNDLE_PATH: bundleRoot },
});
expect(output).toContain('already installed');
expect(output).toContain('Updated');
expect(output).toContain('Installed impeccable into: .cursor');
expect(readFileSync(join(skillDir, 'SKILL.md'), 'utf8')).toContain('version: 9.9.9-local');
expect(readFileSync(join(tmp, '.cursor', 'skills', 'impeccable', 'SKILL.md'), 'utf8')).toContain('version: 9.9.9-local');
// The freshly installed provider gets its hooks and agents too.
expect(existsSync(join(tmp, '.cursor', 'hooks.json'))).toBe(true);
expect(existsSync(join(tmp, '.cursor', 'agents', 'impeccable-finish-reviewer.md'))).toBe(true);
rmSync(tmp, { recursive: true, force: true });
}, 15000);
test('skills update --no-hooks refreshes skills without touching malformed hook manifests', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-update-no-hooks-'));
execSync('git init', { cwd: tmp });