feat(mosaic): add secure skill registration CLI (#826)
This commit was merged in pull request #826.
This commit is contained in:
@@ -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'));
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user