feat(mosaic): add secure skill registration CLI (#826)
All checks were successful
ci/woodpecker/push/ci-image Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #826.
This commit is contained in:
2026-07-17 23:45:35 +00:00
parent d3bf52898b
commit d801d6c4c8
18 changed files with 1240 additions and 26 deletions

View File

@@ -7,6 +7,7 @@ import {
mkdirSync,
readdirSync,
readFileSync,
readlinkSync,
rmSync,
statSync,
symlinkSync,
@@ -300,6 +301,50 @@ describe('repairFleetCommsTools', () => {
});
describe('runFrameworkReseed', () => {
it('auto-registers every canonical skill after a successful upgrade re-seed', () => {
const root = mkdtempSync(join(tmpdir(), 'mosaic-reseed-skills-'));
const framework = join(root, 'framework');
const home = join(root, 'mosaic');
const claudeSkills = join(root, '.claude', 'skills');
mkdirSync(framework, { recursive: true });
mkdirSync(join(home, 'skills', 'added-after-setup'), { recursive: true });
mkdirSync(join(home, 'skills', 'another-new-skill'), { recursive: true });
writeFileSync(join(framework, 'install.sh'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
const res = runFrameworkReseed(framework, home, claudeSkills);
expect(res.ok).toBe(true);
expect(res.skillSync).toMatchObject({
registered: ['added-after-setup', 'another-new-skill'],
conflicts: [],
});
expect(readlinkSync(join(claudeSkills, 'added-after-setup'))).toBe(
join(home, 'skills', 'added-after-setup'),
);
expect(readlinkSync(join(claudeSkills, 'another-new-skill'))).toBe(
join(home, 'skills', 'another-new-skill'),
);
rmSync(root, { recursive: true, force: true });
});
it('keeps a successful framework re-seed successful when bridge reconciliation fails', () => {
const root = mkdtempSync(join(tmpdir(), 'mosaic-reseed-bridge-failure-'));
const framework = join(root, 'framework');
const home = join(root, 'mosaic');
const claudeSkills = join(root, '.claude', 'skills');
mkdirSync(framework, { recursive: true });
mkdirSync(home, { recursive: true });
writeFileSync(join(home, 'skills'), 'invalid canonical root\n');
writeFileSync(join(framework, 'install.sh'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
const res = runFrameworkReseed(framework, home, claudeSkills);
expect(res.ok).toBe(true);
expect(res.skillSync).toBeUndefined();
expect(res.skillSyncError).toMatch(/not a directory/i);
rmSync(root, { recursive: true, force: true });
});
it('reports not-ok (not throw) when the installer is absent', () => {
const missing = mkdtempSync(join(tmpdir(), 'mosaic-noinstaller-'));
const res = runFrameworkReseed(missing, join(missing, 'home'));

View File

@@ -43,6 +43,7 @@ import {
ensureManagedDirectory,
readRegularFileSecure,
} from '../fleet/secure-file.js';
import { getDefaultSkillPaths, syncClaudeSkills, type SkillSyncResult } from '../commands/skill.js';
// ─── Types ──────────────────────────────────────────────────────────────────
@@ -871,19 +872,39 @@ export function repairFleetCommsTools(
* describing what happened (so callers can message + decide on relaunch).
* Best-effort: a missing installer or a non-zero exit is reported, not thrown.
*/
export interface FrameworkReseedResult {
ok: boolean;
reason?: string;
skillSync?: SkillSyncResult;
skillSyncError?: string;
}
export function runFrameworkReseed(
frameworkRoot = resolveBundledFrameworkRoot(),
mosaicHome = join(homedir(), '.config', 'mosaic'),
): { ok: boolean; reason?: string } {
claudeSkillsDir = getDefaultSkillPaths().claudeSkillsDir,
): FrameworkReseedResult {
const { installer, command, env } = buildReseedCommand(frameworkRoot, mosaicHome);
if (!existsSync(installer)) {
return { ok: false, reason: `installer not found: ${installer}` };
}
try {
execSync(command, { stdio: 'inherit', env: { ...process.env, ...env }, timeout: 120_000 });
return { ok: true };
} catch (err) {
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
} catch (error: unknown) {
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
}
try {
const skillSync = syncClaudeSkills({
mosaicSkillsDir: join(mosaicHome, 'skills'),
claudeSkillsDir,
});
return { ok: true, skillSync };
} catch (error: unknown) {
return {
ok: true,
skillSyncError: error instanceof Error ? error.message : String(error),
};
}
}