feat(skills): add registry discovery list/show commands
This commit is contained in:
@@ -50,12 +50,12 @@ Tests:
|
||||
|
||||
Checklist:
|
||||
|
||||
- [ ] Add `flynn skills registry list` command (table/text + `--json`).
|
||||
- [ ] Add `flynn skills registry show <id>` command (entry detail + source fields).
|
||||
- [ ] Add filtering options:
|
||||
- [ ] `--search <term>`
|
||||
- [ ] `--publisher <name>`
|
||||
- [ ] Ensure output clearly marks trust metadata as declared/unverified.
|
||||
- [x] Add `flynn skills registry list` command (table/text + `--json`).
|
||||
- [x] Add `flynn skills registry show <id>` command (entry detail + source fields).
|
||||
- [x] Add filtering options:
|
||||
- [x] `--search <term>`
|
||||
- [x] `--publisher <name>`
|
||||
- [x] Ensure output clearly marks trust metadata as declared/unverified.
|
||||
|
||||
Acceptance:
|
||||
|
||||
@@ -63,7 +63,7 @@ Acceptance:
|
||||
|
||||
Tests:
|
||||
|
||||
- [ ] Command tests for text + JSON output paths.
|
||||
- [x] Command tests for text + JSON output paths.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
"status": "in_progress",
|
||||
"date": "2026-02-16",
|
||||
"updated": "2026-02-16",
|
||||
"summary": "Started ClawHub registry milestone with Phase 1 complete: added validated registry catalog loader/parser with local file + HTTPS source support and tests. Next step is CLI registry discovery UX.",
|
||||
"summary": "Completed Phase 2 CLI discovery UX for ClawHub registry: added `flynn skills registry list/show` with JSON/text output, search and publisher filtering, source resolution via flag/env, and explicit declared/unverified trust metadata labeling.",
|
||||
"files_created": [
|
||||
"docs/plans/2026-02-16-clawhub-registry-checklist.md",
|
||||
"src/skills/registrySource.ts",
|
||||
@@ -224,9 +224,12 @@
|
||||
],
|
||||
"files_modified": [
|
||||
"src/skills/index.ts",
|
||||
"src/cli/skills.ts",
|
||||
"src/cli/skills.test.ts",
|
||||
"docs/plans/2026-02-16-clawhub-registry-checklist.md",
|
||||
"docs/plans/state.json"
|
||||
],
|
||||
"test_status": "pnpm test:run src/skills/registrySource.test.ts + pnpm typecheck passing"
|
||||
"test_status": "pnpm test:run src/skills/registrySource.test.ts src/cli/skills.test.ts + pnpm typecheck passing"
|
||||
},
|
||||
"credential-system-v2-api-and-oauth": {
|
||||
"file": "2026-02-15-credential-system-v2-api-and-oauth-checklist.md",
|
||||
@@ -2767,7 +2770,7 @@
|
||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
||||
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback",
|
||||
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
||||
"next_up": "ClawHub registry Phase 2: add CLI discovery commands (registry list/show with JSON + filtering)"
|
||||
"next_up": "ClawHub registry Phase 3: add install-by-registry-id flow with source resolution and confirmation gates"
|
||||
},
|
||||
"soul_md_and_cron_create": {
|
||||
"date": "2026-02-11",
|
||||
|
||||
@@ -39,6 +39,12 @@ import {
|
||||
resolveSkillInstallerCommandRunner,
|
||||
runSkillExecuteAction,
|
||||
runSkillInstallAction,
|
||||
toSkillRegistryListRows,
|
||||
renderSkillRegistryTable,
|
||||
renderSkillRegistryEntry,
|
||||
filterSkillRegistryEntries,
|
||||
resolveSkillRegistrySource,
|
||||
describeRegistryTrust,
|
||||
registerSkillsCommand,
|
||||
} from './skills.js';
|
||||
import type { Skill } from '../skills/index.js';
|
||||
@@ -102,6 +108,33 @@ function writeSkillsCliConfig(
|
||||
);
|
||||
}
|
||||
|
||||
function writeSkillRegistryCatalog(path: string): void {
|
||||
writeFileSync(
|
||||
path,
|
||||
JSON.stringify({
|
||||
skills: [
|
||||
{
|
||||
id: 'todoist',
|
||||
name: 'Todoist',
|
||||
version: '1.2.3',
|
||||
source: 'https://example.com/skills/todoist.git',
|
||||
summary: 'Task manager integration',
|
||||
publisher: 'Acme',
|
||||
homepage: 'https://example.com/todoist',
|
||||
},
|
||||
{
|
||||
id: 'calendar',
|
||||
name: 'Calendar',
|
||||
version: '2.0.0',
|
||||
source: './skills/calendar',
|
||||
summary: 'Calendar sync',
|
||||
},
|
||||
],
|
||||
}),
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
describe('skills CLI helpers', () => {
|
||||
it('maps and sorts skill rows', () => {
|
||||
const rows = toSkillListRows([
|
||||
@@ -164,6 +197,105 @@ describe('skills CLI helpers', () => {
|
||||
expect(output).toContain('Directory: /opt/flynn/skills/deploy');
|
||||
});
|
||||
|
||||
it('maps and sorts registry rows with trust status', () => {
|
||||
const rows = toSkillRegistryListRows([
|
||||
{
|
||||
id: 'zeta',
|
||||
name: 'Zeta',
|
||||
version: '1.0.0',
|
||||
source: 'https://example.com/zeta.git',
|
||||
summary: 'zeta',
|
||||
},
|
||||
{
|
||||
id: 'alpha',
|
||||
name: 'Alpha',
|
||||
version: '1.0.0',
|
||||
source: 'https://example.com/alpha.git',
|
||||
summary: 'alpha',
|
||||
publisher: 'Acme',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(rows.map((row) => row.id)).toEqual(['alpha', 'zeta']);
|
||||
expect(rows[0]?.trust).toBe('declared_unverified');
|
||||
expect(rows[1]?.trust).toBe('none_declared');
|
||||
});
|
||||
|
||||
it('filters registry entries by search and publisher', () => {
|
||||
const entries = [
|
||||
{
|
||||
id: 'todoist',
|
||||
name: 'Todoist',
|
||||
version: '1.2.3',
|
||||
source: 'https://example.com/todoist.git',
|
||||
summary: 'Task manager',
|
||||
publisher: 'Acme',
|
||||
},
|
||||
{
|
||||
id: 'calendar',
|
||||
name: 'Calendar',
|
||||
version: '2.0.0',
|
||||
source: 'https://example.com/calendar.git',
|
||||
summary: 'Calendar sync',
|
||||
publisher: 'Orbit',
|
||||
},
|
||||
];
|
||||
|
||||
expect(filterSkillRegistryEntries(entries, { search: 'task' }).map((entry) => entry.id)).toEqual(['todoist']);
|
||||
expect(filterSkillRegistryEntries(entries, { publisher: 'acme' }).map((entry) => entry.id)).toEqual(['todoist']);
|
||||
expect(filterSkillRegistryEntries(entries, { search: 'calendar', publisher: 'acme' })).toEqual([]);
|
||||
});
|
||||
|
||||
it('renders no-registry-items text when empty', () => {
|
||||
expect(renderSkillRegistryTable([])).toBe('No registry skills found.');
|
||||
});
|
||||
|
||||
it('renders registry entry with trust note and declared fields', () => {
|
||||
const output = renderSkillRegistryEntry({
|
||||
id: 'todoist',
|
||||
name: 'Todoist',
|
||||
version: '1.2.3',
|
||||
source: 'https://example.com/skills/todoist.git',
|
||||
summary: 'Task manager integration',
|
||||
publisher: 'Acme',
|
||||
homepage: 'https://example.com/todoist',
|
||||
sha256: 'abc123',
|
||||
});
|
||||
|
||||
expect(output).toContain('ID: todoist');
|
||||
expect(output).toContain('Trust: declared (unverified)');
|
||||
expect(output).toContain('Publisher (declared): Acme');
|
||||
expect(output).toContain('SHA256 (declared): abc123');
|
||||
});
|
||||
|
||||
it('describes trust metadata and resolves registry source values', () => {
|
||||
const declaredTrust = describeRegistryTrust({
|
||||
id: 'todoist',
|
||||
name: 'Todoist',
|
||||
version: '1.2.3',
|
||||
source: 'https://example.com/skills/todoist.git',
|
||||
summary: 'Task manager integration',
|
||||
publisher: 'Acme',
|
||||
});
|
||||
expect(declaredTrust.status).toBe('declared_unverified');
|
||||
|
||||
const noneTrust = describeRegistryTrust({
|
||||
id: 'calendar',
|
||||
name: 'Calendar',
|
||||
version: '2.0.0',
|
||||
source: './skills/calendar',
|
||||
summary: 'Calendar sync',
|
||||
});
|
||||
expect(noneTrust.status).toBe('none_declared');
|
||||
|
||||
expect(resolveSkillRegistrySource('./registry.json').source).toEqual({ type: 'file', path: './registry.json' });
|
||||
expect(resolveSkillRegistrySource('https://registry.example/catalog.json').source).toEqual({
|
||||
type: 'url',
|
||||
url: 'https://registry.example/catalog.json',
|
||||
});
|
||||
expect(resolveSkillRegistrySource('http://registry.example/catalog.json').error).toContain('https://');
|
||||
});
|
||||
|
||||
it('renders unavailable reasons when skill is unavailable', () => {
|
||||
const output = renderSkillInfo(
|
||||
buildSkill({
|
||||
@@ -1569,6 +1701,116 @@ describe('skills CLI helpers', () => {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('skills registry list renders text output from source file', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||
const registryPath = join(root, 'registry.json');
|
||||
writeSkillRegistryCatalog(registryPath);
|
||||
|
||||
const program = new Command();
|
||||
registerSkillsCommand(program);
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
process.exitCode = undefined;
|
||||
|
||||
await program.parseAsync(['skills', 'registry', 'list', '--source', registryPath], { from: 'user' });
|
||||
|
||||
expect(logSpy).toHaveBeenCalledTimes(1);
|
||||
const output = String(logSpy.mock.calls[0]?.[0]);
|
||||
expect(output).toContain('ID');
|
||||
expect(output).toContain('todoist');
|
||||
expect(output).toContain('declared_unverified');
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
process.exitCode = undefined;
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('skills registry list supports json output and filtering', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||
const registryPath = join(root, 'registry.json');
|
||||
writeSkillRegistryCatalog(registryPath);
|
||||
|
||||
const program = new Command();
|
||||
registerSkillsCommand(program);
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
process.exitCode = undefined;
|
||||
|
||||
await program.parseAsync(
|
||||
['skills', 'registry', 'list', '--source', registryPath, '--search', 'task', '--publisher', 'acme', '--json'],
|
||||
{ from: 'user' },
|
||||
);
|
||||
|
||||
const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
|
||||
expect(payload).toHaveLength(1);
|
||||
expect(payload[0]?.id).toBe('todoist');
|
||||
expect(payload[0]?.trust_label).toBe('declared (unverified)');
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
|
||||
logSpy.mockRestore();
|
||||
process.exitCode = undefined;
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('skills registry show outputs entry details and json trust metadata', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||
const registryPath = join(root, 'registry.json');
|
||||
writeSkillRegistryCatalog(registryPath);
|
||||
|
||||
const program = new Command();
|
||||
registerSkillsCommand(program);
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
process.exitCode = undefined;
|
||||
|
||||
await program.parseAsync(['skills', 'registry', 'show', 'todoist', '--source', registryPath], { from: 'user' });
|
||||
const text = String(logSpy.mock.calls[0]?.[0]);
|
||||
expect(text).toContain('ID: todoist');
|
||||
expect(text).toContain('Trust note:');
|
||||
|
||||
logSpy.mockClear();
|
||||
|
||||
await program.parseAsync(['skills', 'registry', 'show', 'todoist', '--source', registryPath, '--json'], { from: 'user' });
|
||||
const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
|
||||
expect(payload.id).toBe('todoist');
|
||||
expect(payload.trust_metadata.status).toBe('declared_unverified');
|
||||
|
||||
logSpy.mockRestore();
|
||||
process.exitCode = undefined;
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('skills registry commands report source/lookup errors', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||
const registryPath = join(root, 'registry.json');
|
||||
writeSkillRegistryCatalog(registryPath);
|
||||
|
||||
const program = new Command();
|
||||
registerSkillsCommand(program);
|
||||
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
process.exitCode = undefined;
|
||||
|
||||
await program.parseAsync(['skills', 'registry', 'list', '--source', 'http://registry.example/catalog.json'], { from: 'user' });
|
||||
expect(errorSpy).toHaveBeenCalledWith("Registry URL must use https:// (http://registry.example/catalog.json)");
|
||||
expect(process.exitCode).toBe(1);
|
||||
|
||||
errorSpy.mockClear();
|
||||
process.exitCode = undefined;
|
||||
|
||||
await program.parseAsync(['skills', 'registry', 'show', 'missing', '--source', registryPath], { from: 'user' });
|
||||
expect(errorSpy).toHaveBeenCalledWith("Registry skill 'missing' not found.");
|
||||
expect(process.exitCode).toBe(1);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
process.exitCode = undefined;
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('skills install reports invalid runner via CLI option parsing path', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||
const configPath = join(root, 'config.yaml');
|
||||
|
||||
+241
-2
@@ -8,8 +8,8 @@ import { auditLogger } from '../audit/index.js';
|
||||
import { queryAuditLogs } from '../audit/export.js';
|
||||
import type { AuditEvent } from '../audit/types.js';
|
||||
import type { Config } from '../config/schema.js';
|
||||
import type { Skill } from '../skills/index.js';
|
||||
import { loadAllSkills, SkillInstaller, buildInstallerPlan, loadSkill } from '../skills/index.js';
|
||||
import type { Skill, SkillRegistryEntry, SkillRegistrySource } from '../skills/index.js';
|
||||
import { loadAllSkills, SkillInstaller, buildInstallerPlan, loadSkill, loadSkillRegistryCatalog } from '../skills/index.js';
|
||||
import type { SkillPermissions } from '../skills/types.js';
|
||||
import { loadConfigSafe } from './shared.js';
|
||||
|
||||
@@ -20,6 +20,15 @@ export interface SkillListRow {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface SkillRegistryListRow {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
publisher: string;
|
||||
trust: 'declared_unverified' | 'none_declared';
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface SkillRefreshSummary {
|
||||
total: number;
|
||||
available: number;
|
||||
@@ -27,6 +36,143 @@ export interface SkillRefreshSummary {
|
||||
tiers: Record<Skill['manifest']['tier'], number>;
|
||||
}
|
||||
|
||||
const SKILL_REGISTRY_SOURCE_ENV = 'FLYNN_SKILLS_REGISTRY_SOURCE';
|
||||
|
||||
function hasDeclaredTrustMetadata(entry: SkillRegistryEntry): boolean {
|
||||
return Boolean(entry.publisher || entry.homepage || entry.sha256);
|
||||
}
|
||||
|
||||
export function describeRegistryTrust(entry: SkillRegistryEntry): {
|
||||
status: 'declared_unverified' | 'none_declared';
|
||||
label: string;
|
||||
note: string;
|
||||
} {
|
||||
if (hasDeclaredTrustMetadata(entry)) {
|
||||
return {
|
||||
status: 'declared_unverified',
|
||||
label: 'declared (unverified)',
|
||||
note: 'Trust metadata is declared by registry publishers and not verified by Flynn.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'none_declared',
|
||||
label: 'none declared',
|
||||
note: 'No trust metadata was declared by the registry; all metadata remains unverified by Flynn.',
|
||||
};
|
||||
}
|
||||
|
||||
export function toSkillRegistryListRows(entries: SkillRegistryEntry[]): SkillRegistryListRow[] {
|
||||
return entries
|
||||
.map((entry) => {
|
||||
const trust = describeRegistryTrust(entry);
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
version: entry.version,
|
||||
publisher: entry.publisher ?? '-',
|
||||
trust: trust.status,
|
||||
summary: entry.summary,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
export function filterSkillRegistryEntries(
|
||||
entries: SkillRegistryEntry[],
|
||||
opts: { search?: string; publisher?: string },
|
||||
): SkillRegistryEntry[] {
|
||||
const search = opts.search?.trim().toLowerCase();
|
||||
const publisher = opts.publisher?.trim().toLowerCase();
|
||||
|
||||
return entries.filter((entry) => {
|
||||
if (publisher) {
|
||||
const value = (entry.publisher ?? '').toLowerCase();
|
||||
if (value !== publisher) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const haystack = [entry.id, entry.name, entry.summary, entry.publisher ?? ''].join(' ').toLowerCase();
|
||||
return haystack.includes(search);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderSkillRegistryTable(rows: SkillRegistryListRow[]): string {
|
||||
if (rows.length === 0) {
|
||||
return 'No registry skills found.';
|
||||
}
|
||||
|
||||
const idWidth = Math.max('ID'.length, ...rows.map((row) => row.id.length));
|
||||
const nameWidth = Math.max('NAME'.length, ...rows.map((row) => row.name.length));
|
||||
const versionWidth = Math.max('VERSION'.length, ...rows.map((row) => row.version.length));
|
||||
const publisherWidth = Math.max('PUBLISHER'.length, ...rows.map((row) => row.publisher.length));
|
||||
const trustWidth = Math.max('TRUST'.length, ...rows.map((row) => row.trust.length));
|
||||
|
||||
const lines = [
|
||||
`${'ID'.padEnd(idWidth)} ${'NAME'.padEnd(nameWidth)} ${'VERSION'.padEnd(versionWidth)} ${'PUBLISHER'.padEnd(publisherWidth)} ${'TRUST'.padEnd(trustWidth)} SUMMARY`,
|
||||
`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(versionWidth)} ${'-'.repeat(publisherWidth)} ${'-'.repeat(trustWidth)} -------`,
|
||||
];
|
||||
|
||||
for (const row of rows) {
|
||||
lines.push(
|
||||
`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.version.padEnd(versionWidth)} ${row.publisher.padEnd(publisherWidth)} ${row.trust.padEnd(trustWidth)} ${row.summary}`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function renderSkillRegistryEntry(entry: SkillRegistryEntry): string {
|
||||
const trust = describeRegistryTrust(entry);
|
||||
const lines = [
|
||||
`ID: ${entry.id}`,
|
||||
`Name: ${entry.name}`,
|
||||
`Version: ${entry.version}`,
|
||||
`Summary: ${entry.summary}`,
|
||||
`Source: ${entry.source}`,
|
||||
`Trust: ${trust.label}`,
|
||||
`Trust note: ${trust.note}`,
|
||||
];
|
||||
|
||||
if (entry.publisher) {
|
||||
lines.push(`Publisher (declared): ${entry.publisher}`);
|
||||
}
|
||||
if (entry.homepage) {
|
||||
lines.push(`Homepage (declared): ${entry.homepage}`);
|
||||
}
|
||||
if (entry.sha256) {
|
||||
lines.push(`SHA256 (declared): ${entry.sha256}`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function resolveSkillRegistrySource(
|
||||
sourceArg?: string,
|
||||
): { source?: SkillRegistrySource; error?: string } {
|
||||
const raw = sourceArg?.trim() || process.env[SKILL_REGISTRY_SOURCE_ENV]?.trim();
|
||||
if (!raw) {
|
||||
return {
|
||||
error: `Registry source is required. Pass --source <path-or-https-url> or set ${SKILL_REGISTRY_SOURCE_ENV}.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (raw.startsWith('http://')) {
|
||||
return { error: `Registry URL must use https:// (${raw})` };
|
||||
}
|
||||
|
||||
if (raw.startsWith('https://')) {
|
||||
return { source: { type: 'url', url: raw } };
|
||||
}
|
||||
|
||||
return { source: { type: 'file', path: raw } };
|
||||
}
|
||||
|
||||
export interface SkillInstallerPlanView {
|
||||
skill: {
|
||||
name: string;
|
||||
@@ -1292,6 +1438,99 @@ export function registerSkillsCommand(program: Command): void {
|
||||
console.log(renderSkillInfo(skill));
|
||||
});
|
||||
|
||||
const registry = skills
|
||||
.command('registry')
|
||||
.description('Discover skills from a registry catalog')
|
||||
.action(() => {
|
||||
registry.outputHelp();
|
||||
});
|
||||
|
||||
registry
|
||||
.command('list')
|
||||
.description('List skills from a registry catalog')
|
||||
.option('--source <path-or-url>', 'Registry catalog source (local file path or HTTPS URL)')
|
||||
.option('--search <term>', 'Filter by id/name/summary/publisher')
|
||||
.option('--publisher <name>', 'Filter by exact publisher name (case-insensitive)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (opts: { source?: string; search?: string; publisher?: string; json?: boolean }) => {
|
||||
const sourceResult = resolveSkillRegistrySource(opts.source);
|
||||
if (sourceResult.error || !sourceResult.source) {
|
||||
console.error(sourceResult.error ?? 'Failed to resolve registry source.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const catalog = await loadSkillRegistryCatalog(sourceResult.source);
|
||||
const filtered = filterSkillRegistryEntries(catalog.skills, {
|
||||
search: opts.search,
|
||||
publisher: opts.publisher,
|
||||
});
|
||||
if (opts.json) {
|
||||
const rows = toSkillRegistryListRows(filtered).map((row) => ({
|
||||
...row,
|
||||
trust_label: row.trust === 'declared_unverified' ? 'declared (unverified)' : 'none declared',
|
||||
}));
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(renderSkillRegistryTable(toSkillRegistryListRows(filtered)));
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
registry
|
||||
.command('show <id>')
|
||||
.description('Show details for a registry entry')
|
||||
.option('--source <path-or-url>', 'Registry catalog source (local file path or HTTPS URL)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (id: string, opts: { source?: string; json?: boolean }) => {
|
||||
const sourceResult = resolveSkillRegistrySource(opts.source);
|
||||
if (sourceResult.error || !sourceResult.source) {
|
||||
console.error(sourceResult.error ?? 'Failed to resolve registry source.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const catalog = await loadSkillRegistryCatalog(sourceResult.source);
|
||||
const normalizedId = id.trim().toLowerCase();
|
||||
const entry = catalog.skills.find((item) => item.id.toLowerCase() === normalizedId);
|
||||
if (!entry) {
|
||||
console.error(`Registry skill '${id}' not found.`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const trust = describeRegistryTrust(entry);
|
||||
if (opts.json) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
...entry,
|
||||
trust_metadata: {
|
||||
status: trust.status,
|
||||
label: trust.label,
|
||||
note: trust.note,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(renderSkillRegistryEntry(entry));
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
skills
|
||||
.command('install <path>')
|
||||
.description('Install a skill from a local directory')
|
||||
|
||||
Reference in New Issue
Block a user