diff --git a/packages/cli/package.json b/packages/cli/package.json index 87ea6292..4faaf744 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,6 +41,7 @@ "dependencies": { "@profullstack/sh1pt-action-packs": "workspace:^", "@profullstack/sh1pt-actions-fleet-core": "workspace:^", + "@profullstack/sh1pt-registry": "workspace:^", "@profullstack/sh1pt-core": "workspace:^", "@profullstack/sh1pt-openapi": "workspace:^", "@profullstack/sh1pt-policy": "workspace:^", diff --git a/packages/cli/src/lib/registry.ts b/packages/cli/src/lib/registry.ts new file mode 100644 index 00000000..dee019ff --- /dev/null +++ b/packages/cli/src/lib/registry.ts @@ -0,0 +1,8 @@ +export { + loadActionsRegistry, + loadSkillsRegistry, + loadPacksRegistry, + type ActionRegistryEntry, + type SkillRegistryEntry, + type PackRegistryEntry, +} from '@profullstack/sh1pt-registry'; diff --git a/packages/registry/actions.json b/packages/registry/actions.json new file mode 100644 index 00000000..effd2f37 --- /dev/null +++ b/packages/registry/actions.json @@ -0,0 +1,29 @@ +[ + { + "name": "vu1nz-scan", + "publisher": "profullstack", + "version": "1.0.0", + "description": "AI-powered pull request vulnerability scanning with vu1nz.", + "trustLevel": "verified", + "category": "security", + "path": "packages/actions/vu1nz-scan/sh1pt.actionpack.yaml" + }, + { + "name": "node-pnpm-ci", + "publisher": "profullstack", + "version": "1.0.0", + "description": "Least-privilege Node/TypeScript CI workflow using pnpm.", + "trustLevel": "official", + "category": "ci", + "path": "packages/actions/node-pnpm-ci/sh1pt.actionpack.yaml" + }, + { + "name": "node-pnpm-test", + "publisher": "profullstack", + "version": "1.0.0", + "description": "Node/pnpm test workflow based on sh1pt's own repository test workflow.", + "trustLevel": "official", + "category": "test", + "path": "packages/actions/node-pnpm-test/sh1pt.actionpack.yaml" + } +] diff --git a/packages/registry/package.json b/packages/registry/package.json new file mode 100644 index 00000000..e95e1904 --- /dev/null +++ b/packages/registry/package.json @@ -0,0 +1,39 @@ +{ + "name": "@profullstack/sh1pt-registry", + "version": "0.0.1", + "description": "Local registry index files (actions, skills, packs) for the sh1pt CLI.", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/profullstack/sh1pt.git", + "directory": "packages/registry" + }, + "homepage": "https://sh1pt.com", + "bugs": "https://github.com/profullstack/sh1pt/issues", + "keywords": [ + "sh1pt", + "registry", + "actions", + "skills", + "packs" + ], + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "files": [ + "dist", + "actions.json", + "skills.json", + "packs.json" + ], + "publishConfig": { + "access": "public", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "prepublishOnly": "pnpm build" + } +} diff --git a/packages/registry/packs.json b/packages/registry/packs.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/packages/registry/packs.json @@ -0,0 +1 @@ +[] diff --git a/packages/registry/skills.json b/packages/registry/skills.json new file mode 100644 index 00000000..0c65881e --- /dev/null +++ b/packages/registry/skills.json @@ -0,0 +1,11 @@ +[ + { + "name": "modern-web", + "publisher": "profullstack", + "version": "0.1.0", + "description": "Install reviewable coding-agent guidance for safe, modern web app changes.", + "trustLevel": "verified", + "category": "web", + "path": "packages/cli/skills/modern-web/sh1pt.skill.json" + } +] diff --git a/packages/registry/src/index.test.ts b/packages/registry/src/index.test.ts new file mode 100644 index 00000000..92574d26 --- /dev/null +++ b/packages/registry/src/index.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from 'vitest'; +import { + loadActionsRegistry, + loadSkillsRegistry, + loadPacksRegistry, +} from './index.js'; + +describe('loadActionsRegistry', () => { + it('loads at least one action entry', async () => { + const entries = await loadActionsRegistry(); + expect(entries.length).toBeGreaterThan(0); + }); + + it('includes the vu1nz-scan action', async () => { + const entries = await loadActionsRegistry(); + const entry = entries.find((e) => e.name === 'vu1nz-scan'); + expect(entry).toBeDefined(); + expect(entry?.publisher).toBe('profullstack'); + expect(entry?.trustLevel).toBe('verified'); + expect(entry?.category).toBe('security'); + expect(entry?.path).toMatch(/sh1pt\.actionpack\.yaml$/); + }); + + it('includes the node-pnpm-ci action', async () => { + const entries = await loadActionsRegistry(); + const entry = entries.find((e) => e.name === 'node-pnpm-ci'); + expect(entry).toBeDefined(); + expect(entry?.trustLevel).toBe('official'); + expect(entry?.category).toBe('ci'); + }); + + it('includes the node-pnpm-test action', async () => { + const entries = await loadActionsRegistry(); + const entry = entries.find((e) => e.name === 'node-pnpm-test'); + expect(entry).toBeDefined(); + expect(entry?.trustLevel).toBe('official'); + expect(entry?.category).toBe('test'); + }); + + it('every entry has required fields', async () => { + const entries = await loadActionsRegistry(); + for (const entry of entries) { + expect(entry.name).toBeTruthy(); + expect(entry.publisher).toBeTruthy(); + expect(entry.version).toBeTruthy(); + expect(entry.description).toBeTruthy(); + expect(entry.trustLevel).toBeTruthy(); + expect(entry.category).toBeTruthy(); + expect(entry.path).toBeTruthy(); + } + }); +}); + +describe('loadSkillsRegistry', () => { + it('loads at least one skill entry', async () => { + const entries = await loadSkillsRegistry(); + expect(entries.length).toBeGreaterThan(0); + }); + + it('includes the modern-web skill', async () => { + const entries = await loadSkillsRegistry(); + const entry = entries.find((e) => e.name === 'modern-web'); + expect(entry).toBeDefined(); + expect(entry?.publisher).toBe('profullstack'); + expect(entry?.trustLevel).toBe('verified'); + expect(entry?.category).toBe('web'); + expect(entry?.path).toMatch(/sh1pt\.skill\.json$/); + }); + + it('every entry has required fields', async () => { + const entries = await loadSkillsRegistry(); + for (const entry of entries) { + expect(entry.name).toBeTruthy(); + expect(entry.publisher).toBeTruthy(); + expect(entry.version).toBeTruthy(); + expect(entry.description).toBeTruthy(); + expect(entry.trustLevel).toBeTruthy(); + expect(entry.category).toBeTruthy(); + expect(entry.path).toBeTruthy(); + } + }); +}); + +describe('loadPacksRegistry', () => { + it('returns an array (may be empty)', async () => { + const entries = await loadPacksRegistry(); + expect(Array.isArray(entries)).toBe(true); + }); +}); diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts new file mode 100644 index 00000000..9be83cc5 --- /dev/null +++ b/packages/registry/src/index.ts @@ -0,0 +1,80 @@ +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const REGISTRY_DIR = join(here, '..'); + +/** A single entry in the actions registry index. */ +export interface ActionRegistryEntry { + name: string; + publisher: string; + version: string; + description: string; + trustLevel: 'official' | 'verified' | 'community' | 'experimental' | 'untrusted'; + category: string; + /** Relative path (from the monorepo root) to the pack manifest file. */ + path: string; +} + +/** A single entry in the skills registry index. */ +export interface SkillRegistryEntry { + name: string; + publisher: string; + version: string; + description: string; + trustLevel: 'official' | 'verified' | 'community' | 'experimental' | 'untrusted'; + category: string; + /** Relative path (from the monorepo root) to the skill manifest file. */ + path: string; +} + +/** A single entry in the packs registry index. */ +export interface PackRegistryEntry { + name: string; + publisher: string; + version: string; + description: string; + trustLevel: 'official' | 'verified' | 'community' | 'experimental' | 'untrusted'; + category: string; + /** Relative path (from the monorepo root) to the pack manifest file. */ + path: string; +} + +async function loadJsonFile(filePath: string): Promise { + let text: string; + try { + text = await readFile(filePath, 'utf8'); + } catch (cause) { + throw new Error(`Failed to read registry file "${filePath}": ${(cause as Error).message}`, { cause }); + } + try { + return JSON.parse(text) as T[]; + } catch (cause) { + throw new Error(`Failed to parse registry file "${filePath}": ${(cause as Error).message}`, { cause }); + } +} + +/** + * Load the actions registry index from `actions.json`. + * Returns an array of {@link ActionRegistryEntry} objects. + */ +export async function loadActionsRegistry(): Promise { + return loadJsonFile(join(REGISTRY_DIR, 'actions.json')); +} + +/** + * Load the skills registry index from `skills.json`. + * Returns an array of {@link SkillRegistryEntry} objects. + */ +export async function loadSkillsRegistry(): Promise { + return loadJsonFile(join(REGISTRY_DIR, 'skills.json')); +} + +/** + * Load the packs registry index from `packs.json`. + * Returns an array of {@link PackRegistryEntry} objects. + */ +export async function loadPacksRegistry(): Promise { + return loadJsonFile(join(REGISTRY_DIR, 'packs.json')); +} diff --git a/packages/registry/tsconfig.json b/packages/registry/tsconfig.json new file mode 100644 index 00000000..2481fe54 --- /dev/null +++ b/packages/registry/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "src/**/__tests__/**"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8690b89d..cdf631fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -829,6 +829,9 @@ importers: '@profullstack/sh1pt-policy': specifier: workspace:^ version: link:../policy + '@profullstack/sh1pt-registry': + specifier: workspace:^ + version: link:../registry commander: specifier: ^12.1.0 version: 12.1.0 @@ -1320,6 +1323,8 @@ importers: specifier: workspace:* version: link:../../core + packages/registry: {} + packages/sdk: {} packages/secrets/doppler: