chore(core): contracts for catalog, presets, scope sync, git and marketplaces
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
792a0a8 parent: e2ab7eb modified
packages/core/src/api.ts +133 -0 | @@ -62,3 +62,136 @@ export interface FileContent { | ||
| 62 | 62 | /** True when the file was larger than 512 KiB and the content was cut. */ |
| 63 | 63 | truncated: boolean |
| 64 | 64 | } |
| 65 | + | |
| 66 | +/* | |
| 67 | + * --------------------------------------------------------------------------- | |
| 68 | + * Steps 2-7: catalog, presets, scope configuration, sync, git, marketplaces. | |
| 69 | + * `:scope` is a project id or the reserved id `home`. | |
| 70 | + * | |
| 71 | + * Settings | |
| 72 | + * GET /api/settings -> Settings | |
| 73 | + * PUT /api/settings Settings -> Settings (catalogPath change re-opens the catalog) | |
| 74 | + * | |
| 75 | + * Catalog | |
| 76 | + * GET /api/catalog -> CatalogResponse | |
| 77 | + * POST /api/catalog/init -> CatalogResponse (creates layout + git init if not a repo) | |
| 78 | + * GET /api/catalog/items/:type/:name -> CatalogItemDetail | |
| 79 | + * PUT /api/catalog/items/:type/:name SaveItemInput -> CatalogItem (create or update; rename via previousName) | |
| 80 | + * DELETE /api/catalog/items/:type/:name -> 204 (409 if used by a scope config, unless ?force=1) | |
| 81 | + * PUT /api/catalog/items/:type/:name/tags { tags: string[] } -> 204 | |
| 82 | + * POST /api/catalog/import ImportBody -> ImportResult | |
| 83 | + * PUT /api/catalog/presets/:name Preset (+ previousName?) -> Preset | |
| 84 | + * DELETE /api/catalog/presets/:name -> 204 (409 if used, unless ?force=1) | |
| 85 | + * | |
| 86 | + * Catalog git | |
| 87 | + * GET /api/catalog/git -> GitStatus | |
| 88 | + * POST /api/catalog/git/remote { url } -> GitStatus | |
| 89 | + * POST /api/catalog/git/commit { message } -> GitStatus | |
| 90 | + * POST /api/catalog/git/pull -> GitStatus | |
| 91 | + * POST /api/catalog/git/push -> GitStatus | |
| 92 | + * POST /api/catalog/git/sync -> GitStatus | |
| 93 | + * POST /api/catalog/git/resolve { path, side: 'ours' | 'theirs' } -> GitStatus | |
| 94 | + * | |
| 95 | + * Scope configuration and sync (projects and Home) | |
| 96 | + * GET /api/scopes/:scope/state -> ScopeState | |
| 97 | + * PUT /api/scopes/:scope/config ScopeConfig -> ScopeState (writes skilly.json / home.json; does not apply) | |
| 98 | + * POST /api/scopes/:scope/apply ApplyBody -> ApplyResponse (re-plans server side, applies, returns fresh state) | |
| 99 | + * POST /api/home/remove-from-global RemoveFromGlobalBody -> RemoveFromGlobalResult | |
| 100 | + * | |
| 101 | + * Marketplaces | |
| 102 | + * GET /api/marketplaces -> MarketplaceSource[] | |
| 103 | + * POST /api/marketplaces { name, kind, url, ref? } -> MarketplaceSource | |
| 104 | + * DELETE /api/marketplaces/:id -> 204 (built-in sources cannot be removed) | |
| 105 | + * POST /api/marketplaces/:id/refresh -> MarketplaceSource | |
| 106 | + * GET /api/marketplaces/:id/skills -> MarketplaceSkill[] | |
| 107 | + * GET /api/marketplaces/:id/skill?path=<rel> -> MarketplaceSkillDetail | |
| 108 | + * POST /api/marketplaces/:id/import { paths: string[], overwrite?: boolean } -> ImportResult | |
| 109 | + * --------------------------------------------------------------------------- | |
| 110 | + */ | |
| 111 | + | |
| 112 | +import type { | |
| 113 | + ApplyResult, | |
| 114 | + CatalogItem, | |
| 115 | + CatalogRef, | |
| 116 | + CatalogSnapshot, | |
| 117 | + ConflictResolution, | |
| 118 | + GitStatus, | |
| 119 | + ItemStatus, | |
| 120 | + LockFile, | |
| 121 | + ScopeConfig, | |
| 122 | + SyncPlan, | |
| 123 | +} from './catalog-model.ts' | |
| 124 | +import type { ScopeInventory } from './model.ts' | |
| 125 | + | |
| 126 | +export interface Settings { | |
| 127 | + /** Absolute path of the catalog folder. Default: `~/.skilly/catalog`. */ | |
| 128 | + catalogPath: string | |
| 129 | + /** Commit, pull and push the catalog automatically after changes (debounced). */ | |
| 130 | + autoSync: boolean | |
| 131 | +} | |
| 132 | + | |
| 133 | +export interface CatalogResponse { | |
| 134 | + snapshot: CatalogSnapshot | |
| 135 | + git: GitStatus | |
| 136 | + /** Scopes (project ids or `home`) whose config references each item / preset. Keys: `type/name`, `preset/name`. */ | |
| 137 | + usage: Record<string, string[]> | |
| 138 | +} | |
| 139 | + | |
| 140 | +export interface ImportBody { | |
| 141 | + /** Project id or `home`. */ | |
| 142 | + scope: string | |
| 143 | + /** Inventory item ids. */ | |
| 144 | + itemIds: string[] | |
| 145 | + overwrite?: boolean | |
| 146 | + tags?: string[] | |
| 147 | +} | |
| 148 | + | |
| 149 | +export interface ImportResult { | |
| 150 | + imported: CatalogItem[] | |
| 151 | + /** Items not imported, with the reason (already exists, unsupported type...). */ | |
| 152 | + skipped: { id: string; reason: string }[] | |
| 153 | +} | |
| 154 | + | |
| 155 | +export interface ScopeState { | |
| 156 | + scope: string | |
| 157 | + /** Null when the scope has no skilly.json yet. */ | |
| 158 | + config: ScopeConfig | null | |
| 159 | + inventory: ScopeInventory | |
| 160 | + /** Inventory item id -> status. */ | |
| 161 | + statuses: Record<string, ItemStatus> | |
| 162 | + /** Null when there is no config. */ | |
| 163 | + plan: SyncPlan | null | |
| 164 | + lock: LockFile | null | |
| 165 | +} | |
| 166 | + | |
| 167 | +export interface ApplyBody { | |
| 168 | + /** Plan action id -> resolution, for conflicts. */ | |
| 169 | + resolutions: Record<string, ConflictResolution> | |
| 170 | +} | |
| 171 | + | |
| 172 | +export interface ApplyResponse { | |
| 173 | + result: ApplyResult | |
| 174 | + state: ScopeState | |
| 175 | +} | |
| 176 | + | |
| 177 | +export interface RemoveFromGlobalBody { | |
| 178 | + /** Home inventory item ids (skill, rule or agent). */ | |
| 179 | + itemIds: string[] | |
| 180 | + /** Project ids in which to enable the items (added to their skilly.json `add`, then applied). */ | |
| 181 | + enableIn: string[] | |
| 182 | +} | |
| 183 | + | |
| 184 | +export interface RemoveFromGlobalResult { | |
| 185 | + removed: string[] | |
| 186 | + imported: CatalogRef[] | |
| 187 | + errors: { id: string; message: string }[] | |
| 188 | +} | |
| 189 | + | |
| 190 | +export interface MarketplaceSkillDetail { | |
| 191 | + path: string | |
| 192 | + name: string | |
| 193 | + description?: string | |
| 194 | + /** SKILL.md content. */ | |
| 195 | + content: string | |
| 196 | + files: string[] | |
| 197 | +} | |
| @@ -62,3 +62,136 @@ export interface FileContent { | |||
| 62 | /** True when the file was larger than 512 KiB and the content was cut. */ | 62 | /** True when the file was larger than 512 KiB and the content was cut. */ |
| 63 | truncated: boolean | 63 | truncated: boolean |
| 64 | } | 64 | } |
| 65 | + | ||
| 66 | +/* | ||
| 67 | + * --------------------------------------------------------------------------- | ||
| 68 | + * Steps 2-7: catalog, presets, scope configuration, sync, git, marketplaces. | ||
| 69 | + * `:scope` is a project id or the reserved id `home`. | ||
| 70 | + * | ||
| 71 | + * Settings | ||
| 72 | + * GET /api/settings -> Settings | ||
| 73 | + * PUT /api/settings Settings -> Settings (catalogPath change re-opens the catalog) | ||
| 74 | + * | ||
| 75 | + * Catalog | ||
| 76 | + * GET /api/catalog -> CatalogResponse | ||
| 77 | + * POST /api/catalog/init -> CatalogResponse (creates layout + git init if not a repo) | ||
| 78 | + * GET /api/catalog/items/:type/:name -> CatalogItemDetail | ||
| 79 | + * PUT /api/catalog/items/:type/:name SaveItemInput -> CatalogItem (create or update; rename via previousName) | ||
| 80 | + * DELETE /api/catalog/items/:type/:name -> 204 (409 if used by a scope config, unless ?force=1) | ||
| 81 | + * PUT /api/catalog/items/:type/:name/tags { tags: string[] } -> 204 | ||
| 82 | + * POST /api/catalog/import ImportBody -> ImportResult | ||
| 83 | + * PUT /api/catalog/presets/:name Preset (+ previousName?) -> Preset | ||
| 84 | + * DELETE /api/catalog/presets/:name -> 204 (409 if used, unless ?force=1) | ||
| 85 | + * | ||
| 86 | + * Catalog git | ||
| 87 | + * GET /api/catalog/git -> GitStatus | ||
| 88 | + * POST /api/catalog/git/remote { url } -> GitStatus | ||
| 89 | + * POST /api/catalog/git/commit { message } -> GitStatus | ||
| 90 | + * POST /api/catalog/git/pull -> GitStatus | ||
| 91 | + * POST /api/catalog/git/push -> GitStatus | ||
| 92 | + * POST /api/catalog/git/sync -> GitStatus | ||
| 93 | + * POST /api/catalog/git/resolve { path, side: 'ours' | 'theirs' } -> GitStatus | ||
| 94 | + * | ||
| 95 | + * Scope configuration and sync (projects and Home) | ||
| 96 | + * GET /api/scopes/:scope/state -> ScopeState | ||
| 97 | + * PUT /api/scopes/:scope/config ScopeConfig -> ScopeState (writes skilly.json / home.json; does not apply) | ||
| 98 | + * POST /api/scopes/:scope/apply ApplyBody -> ApplyResponse (re-plans server side, applies, returns fresh state) | ||
| 99 | + * POST /api/home/remove-from-global RemoveFromGlobalBody -> RemoveFromGlobalResult | ||
| 100 | + * | ||
| 101 | + * Marketplaces | ||
| 102 | + * GET /api/marketplaces -> MarketplaceSource[] | ||
| 103 | + * POST /api/marketplaces { name, kind, url, ref? } -> MarketplaceSource | ||
| 104 | + * DELETE /api/marketplaces/:id -> 204 (built-in sources cannot be removed) | ||
| 105 | + * POST /api/marketplaces/:id/refresh -> MarketplaceSource | ||
| 106 | + * GET /api/marketplaces/:id/skills -> MarketplaceSkill[] | ||
| 107 | + * GET /api/marketplaces/:id/skill?path=<rel> -> MarketplaceSkillDetail | ||
| 108 | + * POST /api/marketplaces/:id/import { paths: string[], overwrite?: boolean } -> ImportResult | ||
| 109 | + * --------------------------------------------------------------------------- | ||
| 110 | + */ | ||
| 111 | + | ||
| 112 | +import type { | ||
| 113 | + ApplyResult, | ||
| 114 | + CatalogItem, | ||
| 115 | + CatalogRef, | ||
| 116 | + CatalogSnapshot, | ||
| 117 | + ConflictResolution, | ||
| 118 | + GitStatus, | ||
| 119 | + ItemStatus, | ||
| 120 | + LockFile, | ||
| 121 | + ScopeConfig, | ||
| 122 | + SyncPlan, | ||
| 123 | +} from './catalog-model.ts' | ||
| 124 | +import type { ScopeInventory } from './model.ts' | ||
| 125 | + | ||
| 126 | +export interface Settings { | ||
| 127 | + /** Absolute path of the catalog folder. Default: `~/.skilly/catalog`. */ | ||
| 128 | + catalogPath: string | ||
| 129 | + /** Commit, pull and push the catalog automatically after changes (debounced). */ | ||
| 130 | + autoSync: boolean | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +export interface CatalogResponse { | ||
| 134 | + snapshot: CatalogSnapshot | ||
| 135 | + git: GitStatus | ||
| 136 | + /** Scopes (project ids or `home`) whose config references each item / preset. Keys: `type/name`, `preset/name`. */ | ||
| 137 | + usage: Record<string, string[]> | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +export interface ImportBody { | ||
| 141 | + /** Project id or `home`. */ | ||
| 142 | + scope: string | ||
| 143 | + /** Inventory item ids. */ | ||
| 144 | + itemIds: string[] | ||
| 145 | + overwrite?: boolean | ||
| 146 | + tags?: string[] | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +export interface ImportResult { | ||
| 150 | + imported: CatalogItem[] | ||
| 151 | + /** Items not imported, with the reason (already exists, unsupported type...). */ | ||
| 152 | + skipped: { id: string; reason: string }[] | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +export interface ScopeState { | ||
| 156 | + scope: string | ||
| 157 | + /** Null when the scope has no skilly.json yet. */ | ||
| 158 | + config: ScopeConfig | null | ||
| 159 | + inventory: ScopeInventory | ||
| 160 | + /** Inventory item id -> status. */ | ||
| 161 | + statuses: Record<string, ItemStatus> | ||
| 162 | + /** Null when there is no config. */ | ||
| 163 | + plan: SyncPlan | null | ||
| 164 | + lock: LockFile | null | ||
| 165 | +} | ||
| 166 | + | ||
| 167 | +export interface ApplyBody { | ||
| 168 | + /** Plan action id -> resolution, for conflicts. */ | ||
| 169 | + resolutions: Record<string, ConflictResolution> | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +export interface ApplyResponse { | ||
| 173 | + result: ApplyResult | ||
| 174 | + state: ScopeState | ||
| 175 | +} | ||
| 176 | + | ||
| 177 | +export interface RemoveFromGlobalBody { | ||
| 178 | + /** Home inventory item ids (skill, rule or agent). */ | ||
| 179 | + itemIds: string[] | ||
| 180 | + /** Project ids in which to enable the items (added to their skilly.json `add`, then applied). */ | ||
| 181 | + enableIn: string[] | ||
| 182 | +} | ||
| 183 | + | ||
| 184 | +export interface RemoveFromGlobalResult { | ||
| 185 | + removed: string[] | ||
| 186 | + imported: CatalogRef[] | ||
| 187 | + errors: { id: string; message: string }[] | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +export interface MarketplaceSkillDetail { | ||
| 191 | + path: string | ||
| 192 | + name: string | ||
| 193 | + description?: string | ||
| 194 | + /** SKILL.md content. */ | ||
| 195 | + content: string | ||
| 196 | + files: string[] | ||
| 197 | +} | ||
added
packages/core/src/catalog-model.ts +300 -0 | new file mode 100644 | ||
| @@ -0,0 +1,300 @@ | ||
| 1 | +/** | |
| 2 | + * Data model for the catalog, presets, scope configuration (skilly.json), | |
| 3 | + * lock file and sync plans. Shared by core modules, server and UI. | |
| 4 | + * | |
| 5 | + * Catalog layout on disk (a plain folder, usually a git repository): | |
| 6 | + * | |
| 7 | + * <catalog>/skills/<name>/SKILL.md (+ any files) standard Agent Skills folder | |
| 8 | + * <catalog>/rules/<name>.md frontmatter: name, description, paths? | |
| 9 | + * <catalog>/agents/<name>.md Claude Code subagent format (frontmatter + prompt) | |
| 10 | + * <catalog>/mcp/<name>.yaml McpTemplate | |
| 11 | + * <catalog>/hooks/<name>.yaml HookTemplate | |
| 12 | + * <catalog>/presets/<name>.yaml Preset | |
| 13 | + * <catalog>/.skilly/index.json CatalogIndex (tags, origins) | |
| 14 | + * | |
| 15 | + * Names are kebab-case: /^[a-z0-9][a-z0-9._-]*$/ (presets too). | |
| 16 | + */ | |
| 17 | + | |
| 18 | +import type { AgentId } from './model.ts' | |
| 19 | + | |
| 20 | +export type CatalogItemType = 'skill' | 'rule' | 'agent' | 'mcp' | 'hook' | |
| 21 | + | |
| 22 | +export const CATALOG_ITEM_TYPES: readonly CatalogItemType[] = ['skill', 'rule', 'agent', 'mcp', 'hook'] | |
| 23 | + | |
| 24 | +export interface CatalogRef { | |
| 25 | + type: CatalogItemType | |
| 26 | + name: string | |
| 27 | +} | |
| 28 | + | |
| 29 | +/** `${type}/${name}`, used as map key. */ | |
| 30 | +export type CatalogKey = string | |
| 31 | + | |
| 32 | +export interface ItemOrigin { | |
| 33 | + /** `import`: copied from Home or a project; `marketplace`: imported from a marketplace source; `created`: written in skilly. */ | |
| 34 | + kind: 'import' | 'marketplace' | 'created' | |
| 35 | + /** Source path (import) or marketplace source id (marketplace). */ | |
| 36 | + source?: string | |
| 37 | + /** Path of the item inside the marketplace source. */ | |
| 38 | + sourcePath?: string | |
| 39 | + /** Content hash of the upstream version at import time (to detect upstream updates). */ | |
| 40 | + upstreamHash?: string | |
| 41 | + importedAt: string | |
| 42 | +} | |
| 43 | + | |
| 44 | +export interface CatalogIndex { | |
| 45 | + version: 1 | |
| 46 | + items: Record<CatalogKey, { tags: string[]; origin?: ItemOrigin }> | |
| 47 | +} | |
| 48 | + | |
| 49 | +export interface McpEnvValue { | |
| 50 | + /** Name of an environment variable forwarded to the server (never its value). */ | |
| 51 | + fromEnv?: string | |
| 52 | + /** Literal, non-secret value. */ | |
| 53 | + value?: string | |
| 54 | +} | |
| 55 | + | |
| 56 | +export interface McpTemplate { | |
| 57 | + name: string | |
| 58 | + description?: string | |
| 59 | + transport: 'stdio' | 'http' | |
| 60 | + command?: string | |
| 61 | + args?: string[] | |
| 62 | + env?: Record<string, McpEnvValue> | |
| 63 | + url?: string | |
| 64 | + headers?: Record<string, McpEnvValue> | |
| 65 | + auth?: { bearerFromEnv: string } | |
| 66 | +} | |
| 67 | + | |
| 68 | +export interface HookTemplate { | |
| 69 | + name: string | |
| 70 | + description?: string | |
| 71 | + /** Event name shared by Claude Code and Codex, e.g. `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`, `SessionStart`. */ | |
| 72 | + event: string | |
| 73 | + matcher?: string | |
| 74 | + handler: { type: 'command'; command: string; timeout?: number } | |
| 75 | + /** Restrict to some agents; default all targets. */ | |
| 76 | + agents?: AgentId[] | |
| 77 | +} | |
| 78 | + | |
| 79 | +export interface Preset { | |
| 80 | + name: string | |
| 81 | + description?: string | |
| 82 | + /** Other presets included (resolved recursively, cycles ignored with a warning). */ | |
| 83 | + includes: string[] | |
| 84 | + items: Partial<Record<CatalogItemType, string[]>> | |
| 85 | +} | |
| 86 | + | |
| 87 | +export interface CatalogItem { | |
| 88 | + type: CatalogItemType | |
| 89 | + name: string | |
| 90 | + description?: string | |
| 91 | + tags: string[] | |
| 92 | + /** Absolute path of the item (folder for skills, file otherwise). */ | |
| 93 | + path: string | |
| 94 | + /** sha256 of the item content (same algorithm as inventory hashes). */ | |
| 95 | + hash: string | |
| 96 | + origin?: ItemOrigin | |
| 97 | + updatedAt: string | |
| 98 | +} | |
| 99 | + | |
| 100 | +export interface CatalogFile { | |
| 101 | + /** Path relative to the item folder (skills) or the file name. */ | |
| 102 | + path: string | |
| 103 | + sizeBytes: number | |
| 104 | +} | |
| 105 | + | |
| 106 | +export interface CatalogItemDetail { | |
| 107 | + item: CatalogItem | |
| 108 | + /** Main file content: SKILL.md, rule/agent markdown, or YAML for mcp/hook. */ | |
| 109 | + content: string | |
| 110 | + files: CatalogFile[] | |
| 111 | + /** Parsed template for mcp / hook items. */ | |
| 112 | + template?: McpTemplate | HookTemplate | |
| 113 | +} | |
| 114 | + | |
| 115 | +export interface SaveItemInput { | |
| 116 | + type: CatalogItemType | |
| 117 | + name: string | |
| 118 | + /** Main file content (see CatalogItemDetail.content). */ | |
| 119 | + content: string | |
| 120 | + /** Extra files for skills (relative path -> content). Omitted files are kept. */ | |
| 121 | + files?: Record<string, string> | |
| 122 | + /** Relative paths of skill files to delete. */ | |
| 123 | + deleteFiles?: string[] | |
| 124 | + tags?: string[] | |
| 125 | + /** Rename: previous name when changing `name`. */ | |
| 126 | + previousName?: string | |
| 127 | +} | |
| 128 | + | |
| 129 | +export interface CatalogSnapshot { | |
| 130 | + root: string | |
| 131 | + exists: boolean | |
| 132 | + items: CatalogItem[] | |
| 133 | + presets: Preset[] | |
| 134 | + errors: { path: string; message: string }[] | |
| 135 | +} | |
| 136 | + | |
| 137 | +/** Contents of `skilly.json` in a project, or `~/.skilly/home.json` for Home. */ | |
| 138 | +export interface ScopeConfig { | |
| 139 | + version: 1 | |
| 140 | + targets: AgentId[] | |
| 141 | + presets: string[] | |
| 142 | + add: Partial<Record<CatalogItemType, string[]>> | |
| 143 | + /** Glob patterns on names (`*` wildcard), applied after presets and additions. */ | |
| 144 | + exclude: Partial<Record<CatalogItemType, string[]>> | |
| 145 | + /** Skill name globs rendered as on-demand (hidden from the model, user-invocable). */ | |
| 146 | + onDemand: { skills: string[] } | |
| 147 | + /** Global (Home) items to hide in this project, where the agent supports it. Name globs. Ignored for Home. */ | |
| 148 | + hideGlobal: { skills: string[]; mcp: string[] } | |
| 149 | +} | |
| 150 | + | |
| 151 | +export interface LockFileEntry { | |
| 152 | + path: string | |
| 153 | + sha256: string | |
| 154 | +} | |
| 155 | + | |
| 156 | +/** A key skilly owns inside a shared config file (settings.json, .mcp.json, config.toml, hooks.json, AGENTS.md block). */ | |
| 157 | +export interface LockConfigEntry { | |
| 158 | + file: string | |
| 159 | + /** Dotted key path, e.g. `mcpServers.github`, `skillOverrides.seo-audit`, `mcp_servers.github`, `hooks.PreToolUse[skilly:fmt]`, `agents-md-block:typescript-strict`. */ | |
| 160 | + key: string | |
| 161 | + sha256: string | |
| 162 | +} | |
| 163 | + | |
| 164 | +export interface LockEntry { | |
| 165 | + type: CatalogItemType | |
| 166 | + name: string | |
| 167 | + agent: AgentId | |
| 168 | + /** Catalog item hash at the time of the last sync. */ | |
| 169 | + sourceHash: string | |
| 170 | + /** Files written for this item, paths relative to the scope root, forward slashes. */ | |
| 171 | + files: LockFileEntry[] | |
| 172 | + configEntries: LockConfigEntry[] | |
| 173 | + /** Preset names, or `add`, that brought the item in. */ | |
| 174 | + via: string[] | |
| 175 | +} | |
| 176 | + | |
| 177 | +/** `skilly.lock` in a project, `~/.skilly/home.lock` for Home. */ | |
| 178 | +export interface LockFile { | |
| 179 | + version: 1 | |
| 180 | + /** Catalog git commit at the last sync, when the catalog is a repo. */ | |
| 181 | + catalogCommit?: string | |
| 182 | + syncedAt: string | |
| 183 | + entries: LockEntry[] | |
| 184 | +} | |
| 185 | + | |
| 186 | +export type PlanActionKind = 'add' | 'update' | 'remove' | 'unchanged' | 'conflict' | |
| 187 | + | |
| 188 | +export type ConflictKind = | |
| 189 | + /** A managed file was edited in the scope since the last sync. */ | |
| 190 | + | 'modified-locally' | |
| 191 | + /** The target path exists but skilly does not own it (not in the lock). */ | |
| 192 | + | 'exists-unmanaged' | |
| 193 | + /** The item is in the lock but no longer in the catalog. */ | |
| 194 | + | 'missing-in-catalog' | |
| 195 | + | |
| 196 | +export type ConflictResolution = | |
| 197 | + /** Keep the scope version; the item stays as is (unmanaged if it was unmanaged). */ | |
| 198 | + | 'keep-local' | |
| 199 | + /** Overwrite with the catalog version. */ | |
| 200 | + | 'take-catalog' | |
| 201 | + /** Copy the scope version into the catalog, then mark it in sync. */ | |
| 202 | + | 'push-to-catalog' | |
| 203 | + | |
| 204 | +export interface PlanAction { | |
| 205 | + /** Stable id within a plan: `${kind}:${agent}:${type}/${name}`. */ | |
| 206 | + id: string | |
| 207 | + kind: PlanActionKind | |
| 208 | + type: CatalogItemType | |
| 209 | + name: string | |
| 210 | + agent: AgentId | |
| 211 | + /** Paths (relative to the scope root) that will be written or removed. */ | |
| 212 | + files: string[] | |
| 213 | + /** Config keys that will be written or removed. */ | |
| 214 | + configKeys: { file: string; key: string }[] | |
| 215 | + reason: string | |
| 216 | + conflict?: ConflictKind | |
| 217 | + /** True when the catalog version changed since the last sync (for `update`). */ | |
| 218 | + catalogUpdated?: boolean | |
| 219 | +} | |
| 220 | + | |
| 221 | +export interface SyncPlan { | |
| 222 | + scope: 'home' | 'project' | |
| 223 | + root: string | |
| 224 | + actions: PlanAction[] | |
| 225 | + /** Non-blocking notes, e.g. unsupported features per agent, Codex trust, AGENTS.md size. */ | |
| 226 | + warnings: string[] | |
| 227 | + /** Items referenced by the config but not found in the catalog. */ | |
| 228 | + missing: CatalogRef[] | |
| 229 | +} | |
| 230 | + | |
| 231 | +export interface ApplyResult { | |
| 232 | + applied: string[] | |
| 233 | + skipped: string[] | |
| 234 | + errors: { actionId: string; message: string }[] | |
| 235 | + lock: LockFile | |
| 236 | +} | |
| 237 | + | |
| 238 | +/** Status of an inventory item relative to the catalog and the lock, computed by `annotateInventory`. */ | |
| 239 | +export interface ItemStatus { | |
| 240 | + /** True when the item was written by skilly (present in the lock). */ | |
| 241 | + managed: boolean | |
| 242 | + catalogRef?: CatalogRef | |
| 243 | + state: | |
| 244 | + | 'in-sync' | |
| 245 | + | 'modified' | |
| 246 | + | 'catalog-updated' | |
| 247 | + | 'not-in-catalog' | |
| 248 | + /** Same name as a catalog item, not managed, different content. */ | |
| 249 | + | 'differs-from-catalog' | |
| 250 | +} | |
| 251 | + | |
| 252 | +/** Where a scope lives on disk. */ | |
| 253 | +export interface SyncTarget { | |
| 254 | + kind: 'home' | 'project' | |
| 255 | + /** Project root, or the home directory for Home. */ | |
| 256 | + root: string | |
| 257 | + homeDir: string | |
| 258 | + codexHome?: string | |
| 259 | + /** Where scope config and lock are stored: project root for projects, `~/.skilly` for Home. */ | |
| 260 | + configDir: string | |
| 261 | +} | |
| 262 | + | |
| 263 | +export interface GitStatus { | |
| 264 | + isRepo: boolean | |
| 265 | + branch?: string | |
| 266 | + remote?: string | |
| 267 | + ahead: number | |
| 268 | + behind: number | |
| 269 | + /** Changed paths relative to the catalog root. */ | |
| 270 | + dirty: string[] | |
| 271 | + lastCommit?: { hash: string; message: string; date: string } | |
| 272 | + conflicts: string[] | |
| 273 | +} | |
| 274 | + | |
| 275 | +export interface MarketplaceSource { | |
| 276 | + id: string | |
| 277 | + name: string | |
| 278 | + /** | |
| 279 | + * `git`: any git repository containing skill folders (SKILL.md) anywhere in its tree. | |
| 280 | + * `claude-marketplace`: a git repository with `.claude-plugin/marketplace.json`; skills come from its plugins. | |
| 281 | + */ | |
| 282 | + kind: 'git' | 'claude-marketplace' | |
| 283 | + url: string | |
| 284 | + ref?: string | |
| 285 | + builtIn?: boolean | |
| 286 | + lastFetchedAt?: string | |
| 287 | +} | |
| 288 | + | |
| 289 | +export interface MarketplaceSkill { | |
| 290 | + sourceId: string | |
| 291 | + name: string | |
| 292 | + description?: string | |
| 293 | + /** Path of the skill folder relative to the source checkout. */ | |
| 294 | + path: string | |
| 295 | + /** Plugin name for claude-marketplace sources. */ | |
| 296 | + plugin?: string | |
| 297 | + hash: string | |
| 298 | + /** Set when the catalog already has an item with this name. */ | |
| 299 | + inCatalog?: 'same' | 'different' | |
| 300 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,300 @@ | |||
| 1 | +/** | ||
| 2 | + * Data model for the catalog, presets, scope configuration (skilly.json), | ||
| 3 | + * lock file and sync plans. Shared by core modules, server and UI. | ||
| 4 | + * | ||
| 5 | + * Catalog layout on disk (a plain folder, usually a git repository): | ||
| 6 | + * | ||
| 7 | + * <catalog>/skills/<name>/SKILL.md (+ any files) standard Agent Skills folder | ||
| 8 | + * <catalog>/rules/<name>.md frontmatter: name, description, paths? | ||
| 9 | + * <catalog>/agents/<name>.md Claude Code subagent format (frontmatter + prompt) | ||
| 10 | + * <catalog>/mcp/<name>.yaml McpTemplate | ||
| 11 | + * <catalog>/hooks/<name>.yaml HookTemplate | ||
| 12 | + * <catalog>/presets/<name>.yaml Preset | ||
| 13 | + * <catalog>/.skilly/index.json CatalogIndex (tags, origins) | ||
| 14 | + * | ||
| 15 | + * Names are kebab-case: /^[a-z0-9][a-z0-9._-]*$/ (presets too). | ||
| 16 | + */ | ||
| 17 | + | ||
| 18 | +import type { AgentId } from './model.ts' | ||
| 19 | + | ||
| 20 | +export type CatalogItemType = 'skill' | 'rule' | 'agent' | 'mcp' | 'hook' | ||
| 21 | + | ||
| 22 | +export const CATALOG_ITEM_TYPES: readonly CatalogItemType[] = ['skill', 'rule', 'agent', 'mcp', 'hook'] | ||
| 23 | + | ||
| 24 | +export interface CatalogRef { | ||
| 25 | + type: CatalogItemType | ||
| 26 | + name: string | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +/** `${type}/${name}`, used as map key. */ | ||
| 30 | +export type CatalogKey = string | ||
| 31 | + | ||
| 32 | +export interface ItemOrigin { | ||
| 33 | + /** `import`: copied from Home or a project; `marketplace`: imported from a marketplace source; `created`: written in skilly. */ | ||
| 34 | + kind: 'import' | 'marketplace' | 'created' | ||
| 35 | + /** Source path (import) or marketplace source id (marketplace). */ | ||
| 36 | + source?: string | ||
| 37 | + /** Path of the item inside the marketplace source. */ | ||
| 38 | + sourcePath?: string | ||
| 39 | + /** Content hash of the upstream version at import time (to detect upstream updates). */ | ||
| 40 | + upstreamHash?: string | ||
| 41 | + importedAt: string | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +export interface CatalogIndex { | ||
| 45 | + version: 1 | ||
| 46 | + items: Record<CatalogKey, { tags: string[]; origin?: ItemOrigin }> | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +export interface McpEnvValue { | ||
| 50 | + /** Name of an environment variable forwarded to the server (never its value). */ | ||
| 51 | + fromEnv?: string | ||
| 52 | + /** Literal, non-secret value. */ | ||
| 53 | + value?: string | ||
| 54 | +} | ||
| 55 | + | ||
| 56 | +export interface McpTemplate { | ||
| 57 | + name: string | ||
| 58 | + description?: string | ||
| 59 | + transport: 'stdio' | 'http' | ||
| 60 | + command?: string | ||
| 61 | + args?: string[] | ||
| 62 | + env?: Record<string, McpEnvValue> | ||
| 63 | + url?: string | ||
| 64 | + headers?: Record<string, McpEnvValue> | ||
| 65 | + auth?: { bearerFromEnv: string } | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +export interface HookTemplate { | ||
| 69 | + name: string | ||
| 70 | + description?: string | ||
| 71 | + /** Event name shared by Claude Code and Codex, e.g. `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`, `SessionStart`. */ | ||
| 72 | + event: string | ||
| 73 | + matcher?: string | ||
| 74 | + handler: { type: 'command'; command: string; timeout?: number } | ||
| 75 | + /** Restrict to some agents; default all targets. */ | ||
| 76 | + agents?: AgentId[] | ||
| 77 | +} | ||
| 78 | + | ||
| 79 | +export interface Preset { | ||
| 80 | + name: string | ||
| 81 | + description?: string | ||
| 82 | + /** Other presets included (resolved recursively, cycles ignored with a warning). */ | ||
| 83 | + includes: string[] | ||
| 84 | + items: Partial<Record<CatalogItemType, string[]>> | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +export interface CatalogItem { | ||
| 88 | + type: CatalogItemType | ||
| 89 | + name: string | ||
| 90 | + description?: string | ||
| 91 | + tags: string[] | ||
| 92 | + /** Absolute path of the item (folder for skills, file otherwise). */ | ||
| 93 | + path: string | ||
| 94 | + /** sha256 of the item content (same algorithm as inventory hashes). */ | ||
| 95 | + hash: string | ||
| 96 | + origin?: ItemOrigin | ||
| 97 | + updatedAt: string | ||
| 98 | +} | ||
| 99 | + | ||
| 100 | +export interface CatalogFile { | ||
| 101 | + /** Path relative to the item folder (skills) or the file name. */ | ||
| 102 | + path: string | ||
| 103 | + sizeBytes: number | ||
| 104 | +} | ||
| 105 | + | ||
| 106 | +export interface CatalogItemDetail { | ||
| 107 | + item: CatalogItem | ||
| 108 | + /** Main file content: SKILL.md, rule/agent markdown, or YAML for mcp/hook. */ | ||
| 109 | + content: string | ||
| 110 | + files: CatalogFile[] | ||
| 111 | + /** Parsed template for mcp / hook items. */ | ||
| 112 | + template?: McpTemplate | HookTemplate | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +export interface SaveItemInput { | ||
| 116 | + type: CatalogItemType | ||
| 117 | + name: string | ||
| 118 | + /** Main file content (see CatalogItemDetail.content). */ | ||
| 119 | + content: string | ||
| 120 | + /** Extra files for skills (relative path -> content). Omitted files are kept. */ | ||
| 121 | + files?: Record<string, string> | ||
| 122 | + /** Relative paths of skill files to delete. */ | ||
| 123 | + deleteFiles?: string[] | ||
| 124 | + tags?: string[] | ||
| 125 | + /** Rename: previous name when changing `name`. */ | ||
| 126 | + previousName?: string | ||
| 127 | +} | ||
| 128 | + | ||
| 129 | +export interface CatalogSnapshot { | ||
| 130 | + root: string | ||
| 131 | + exists: boolean | ||
| 132 | + items: CatalogItem[] | ||
| 133 | + presets: Preset[] | ||
| 134 | + errors: { path: string; message: string }[] | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +/** Contents of `skilly.json` in a project, or `~/.skilly/home.json` for Home. */ | ||
| 138 | +export interface ScopeConfig { | ||
| 139 | + version: 1 | ||
| 140 | + targets: AgentId[] | ||
| 141 | + presets: string[] | ||
| 142 | + add: Partial<Record<CatalogItemType, string[]>> | ||
| 143 | + /** Glob patterns on names (`*` wildcard), applied after presets and additions. */ | ||
| 144 | + exclude: Partial<Record<CatalogItemType, string[]>> | ||
| 145 | + /** Skill name globs rendered as on-demand (hidden from the model, user-invocable). */ | ||
| 146 | + onDemand: { skills: string[] } | ||
| 147 | + /** Global (Home) items to hide in this project, where the agent supports it. Name globs. Ignored for Home. */ | ||
| 148 | + hideGlobal: { skills: string[]; mcp: string[] } | ||
| 149 | +} | ||
| 150 | + | ||
| 151 | +export interface LockFileEntry { | ||
| 152 | + path: string | ||
| 153 | + sha256: string | ||
| 154 | +} | ||
| 155 | + | ||
| 156 | +/** A key skilly owns inside a shared config file (settings.json, .mcp.json, config.toml, hooks.json, AGENTS.md block). */ | ||
| 157 | +export interface LockConfigEntry { | ||
| 158 | + file: string | ||
| 159 | + /** Dotted key path, e.g. `mcpServers.github`, `skillOverrides.seo-audit`, `mcp_servers.github`, `hooks.PreToolUse[skilly:fmt]`, `agents-md-block:typescript-strict`. */ | ||
| 160 | + key: string | ||
| 161 | + sha256: string | ||
| 162 | +} | ||
| 163 | + | ||
| 164 | +export interface LockEntry { | ||
| 165 | + type: CatalogItemType | ||
| 166 | + name: string | ||
| 167 | + agent: AgentId | ||
| 168 | + /** Catalog item hash at the time of the last sync. */ | ||
| 169 | + sourceHash: string | ||
| 170 | + /** Files written for this item, paths relative to the scope root, forward slashes. */ | ||
| 171 | + files: LockFileEntry[] | ||
| 172 | + configEntries: LockConfigEntry[] | ||
| 173 | + /** Preset names, or `add`, that brought the item in. */ | ||
| 174 | + via: string[] | ||
| 175 | +} | ||
| 176 | + | ||
| 177 | +/** `skilly.lock` in a project, `~/.skilly/home.lock` for Home. */ | ||
| 178 | +export interface LockFile { | ||
| 179 | + version: 1 | ||
| 180 | + /** Catalog git commit at the last sync, when the catalog is a repo. */ | ||
| 181 | + catalogCommit?: string | ||
| 182 | + syncedAt: string | ||
| 183 | + entries: LockEntry[] | ||
| 184 | +} | ||
| 185 | + | ||
| 186 | +export type PlanActionKind = 'add' | 'update' | 'remove' | 'unchanged' | 'conflict' | ||
| 187 | + | ||
| 188 | +export type ConflictKind = | ||
| 189 | + /** A managed file was edited in the scope since the last sync. */ | ||
| 190 | + | 'modified-locally' | ||
| 191 | + /** The target path exists but skilly does not own it (not in the lock). */ | ||
| 192 | + | 'exists-unmanaged' | ||
| 193 | + /** The item is in the lock but no longer in the catalog. */ | ||
| 194 | + | 'missing-in-catalog' | ||
| 195 | + | ||
| 196 | +export type ConflictResolution = | ||
| 197 | + /** Keep the scope version; the item stays as is (unmanaged if it was unmanaged). */ | ||
| 198 | + | 'keep-local' | ||
| 199 | + /** Overwrite with the catalog version. */ | ||
| 200 | + | 'take-catalog' | ||
| 201 | + /** Copy the scope version into the catalog, then mark it in sync. */ | ||
| 202 | + | 'push-to-catalog' | ||
| 203 | + | ||
| 204 | +export interface PlanAction { | ||
| 205 | + /** Stable id within a plan: `${kind}:${agent}:${type}/${name}`. */ | ||
| 206 | + id: string | ||
| 207 | + kind: PlanActionKind | ||
| 208 | + type: CatalogItemType | ||
| 209 | + name: string | ||
| 210 | + agent: AgentId | ||
| 211 | + /** Paths (relative to the scope root) that will be written or removed. */ | ||
| 212 | + files: string[] | ||
| 213 | + /** Config keys that will be written or removed. */ | ||
| 214 | + configKeys: { file: string; key: string }[] | ||
| 215 | + reason: string | ||
| 216 | + conflict?: ConflictKind | ||
| 217 | + /** True when the catalog version changed since the last sync (for `update`). */ | ||
| 218 | + catalogUpdated?: boolean | ||
| 219 | +} | ||
| 220 | + | ||
| 221 | +export interface SyncPlan { | ||
| 222 | + scope: 'home' | 'project' | ||
| 223 | + root: string | ||
| 224 | + actions: PlanAction[] | ||
| 225 | + /** Non-blocking notes, e.g. unsupported features per agent, Codex trust, AGENTS.md size. */ | ||
| 226 | + warnings: string[] | ||
| 227 | + /** Items referenced by the config but not found in the catalog. */ | ||
| 228 | + missing: CatalogRef[] | ||
| 229 | +} | ||
| 230 | + | ||
| 231 | +export interface ApplyResult { | ||
| 232 | + applied: string[] | ||
| 233 | + skipped: string[] | ||
| 234 | + errors: { actionId: string; message: string }[] | ||
| 235 | + lock: LockFile | ||
| 236 | +} | ||
| 237 | + | ||
| 238 | +/** Status of an inventory item relative to the catalog and the lock, computed by `annotateInventory`. */ | ||
| 239 | +export interface ItemStatus { | ||
| 240 | + /** True when the item was written by skilly (present in the lock). */ | ||
| 241 | + managed: boolean | ||
| 242 | + catalogRef?: CatalogRef | ||
| 243 | + state: | ||
| 244 | + | 'in-sync' | ||
| 245 | + | 'modified' | ||
| 246 | + | 'catalog-updated' | ||
| 247 | + | 'not-in-catalog' | ||
| 248 | + /** Same name as a catalog item, not managed, different content. */ | ||
| 249 | + | 'differs-from-catalog' | ||
| 250 | +} | ||
| 251 | + | ||
| 252 | +/** Where a scope lives on disk. */ | ||
| 253 | +export interface SyncTarget { | ||
| 254 | + kind: 'home' | 'project' | ||
| 255 | + /** Project root, or the home directory for Home. */ | ||
| 256 | + root: string | ||
| 257 | + homeDir: string | ||
| 258 | + codexHome?: string | ||
| 259 | + /** Where scope config and lock are stored: project root for projects, `~/.skilly` for Home. */ | ||
| 260 | + configDir: string | ||
| 261 | +} | ||
| 262 | + | ||
| 263 | +export interface GitStatus { | ||
| 264 | + isRepo: boolean | ||
| 265 | + branch?: string | ||
| 266 | + remote?: string | ||
| 267 | + ahead: number | ||
| 268 | + behind: number | ||
| 269 | + /** Changed paths relative to the catalog root. */ | ||
| 270 | + dirty: string[] | ||
| 271 | + lastCommit?: { hash: string; message: string; date: string } | ||
| 272 | + conflicts: string[] | ||
| 273 | +} | ||
| 274 | + | ||
| 275 | +export interface MarketplaceSource { | ||
| 276 | + id: string | ||
| 277 | + name: string | ||
| 278 | + /** | ||
| 279 | + * `git`: any git repository containing skill folders (SKILL.md) anywhere in its tree. | ||
| 280 | + * `claude-marketplace`: a git repository with `.claude-plugin/marketplace.json`; skills come from its plugins. | ||
| 281 | + */ | ||
| 282 | + kind: 'git' | 'claude-marketplace' | ||
| 283 | + url: string | ||
| 284 | + ref?: string | ||
| 285 | + builtIn?: boolean | ||
| 286 | + lastFetchedAt?: string | ||
| 287 | +} | ||
| 288 | + | ||
| 289 | +export interface MarketplaceSkill { | ||
| 290 | + sourceId: string | ||
| 291 | + name: string | ||
| 292 | + description?: string | ||
| 293 | + /** Path of the skill folder relative to the source checkout. */ | ||
| 294 | + path: string | ||
| 295 | + /** Plugin name for claude-marketplace sources. */ | ||
| 296 | + plugin?: string | ||
| 297 | + hash: string | ||
| 298 | + /** Set when the catalog already has an item with this name. */ | ||
| 299 | + inCatalog?: 'same' | 'different' | ||
| 300 | +} | ||
added
packages/core/src/catalog/git.ts +6 -0 | new file mode 100644 | ||
| @@ -0,0 +1,6 @@ | ||
| 1 | +import type { OpenCatalogGit } from '../contracts.ts' | |
| 2 | + | |
| 3 | +// Placeholder: replaced by the real implementation. | |
| 4 | +export const openCatalogGit: OpenCatalogGit = () => { | |
| 5 | + throw new Error('not implemented') | |
| 6 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | +import type { OpenCatalogGit } from '../contracts.ts' | ||
| 2 | + | ||
| 3 | +// Placeholder: replaced by the real implementation. | ||
| 4 | +export const openCatalogGit: OpenCatalogGit = () => { | ||
| 5 | + throw new Error('not implemented') | ||
| 6 | +} | ||
added
packages/core/src/catalog/index.ts +6 -0 | new file mode 100644 | ||
| @@ -0,0 +1,6 @@ | ||
| 1 | +import type { OpenCatalog } from '../contracts.ts' | |
| 2 | + | |
| 3 | +// Placeholder: replaced by the real implementation. | |
| 4 | +export const openCatalog: OpenCatalog = () => { | |
| 5 | + throw new Error('not implemented') | |
| 6 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | +import type { OpenCatalog } from '../contracts.ts' | ||
| 2 | + | ||
| 3 | +// Placeholder: replaced by the real implementation. | ||
| 4 | +export const openCatalog: OpenCatalog = () => { | ||
| 5 | + throw new Error('not implemented') | ||
| 6 | +} | ||
added
packages/core/src/contracts.ts +114 -0 | new file mode 100644 | ||
| @@ -0,0 +1,114 @@ | ||
| 1 | +/** | |
| 2 | + * Function contracts between core modules. Each module (catalog, git, sync, | |
| 3 | + * marketplace) implements its part; the server only wires them together. | |
| 4 | + */ | |
| 5 | + | |
| 6 | +import type { | |
| 7 | + ApplyResult, | |
| 8 | + CatalogItem, | |
| 9 | + CatalogItemDetail, | |
| 10 | + CatalogRef, | |
| 11 | + CatalogSnapshot, | |
| 12 | + ConflictResolution, | |
| 13 | + GitStatus, | |
| 14 | + ItemOrigin, | |
| 15 | + ItemStatus, | |
| 16 | + LockFile, | |
| 17 | + MarketplaceSkill, | |
| 18 | + MarketplaceSource, | |
| 19 | + Preset, | |
| 20 | + SaveItemInput, | |
| 21 | + ScopeConfig, | |
| 22 | + SyncPlan, | |
| 23 | + SyncTarget, | |
| 24 | +} from './catalog-model.ts' | |
| 25 | +import type { InventoryItem, ScopeInventory } from './model.ts' | |
| 26 | + | |
| 27 | +/** packages/core/src/catalog — read/write the catalog folder. */ | |
| 28 | +export interface CatalogStore { | |
| 29 | + readonly root: string | |
| 30 | + exists(): Promise<boolean> | |
| 31 | + /** Creates the folder layout and `.skilly/index.json` if missing (idempotent). Does not run git. */ | |
| 32 | + init(): Promise<void> | |
| 33 | + snapshot(): Promise<CatalogSnapshot> | |
| 34 | + getItem(ref: CatalogRef): Promise<CatalogItemDetail | undefined> | |
| 35 | + /** Absolute paths of all files of an item (folder content for skills, the single file otherwise). */ | |
| 36 | + itemFiles(ref: CatalogRef): Promise<{ rel: string; abs: string }[]> | |
| 37 | + /** Create or update (or rename with `previousName`). Validates names and mcp/hook YAML. */ | |
| 38 | + saveItem(input: SaveItemInput, origin?: ItemOrigin): Promise<CatalogItem> | |
| 39 | + deleteItem(ref: CatalogRef): Promise<void> | |
| 40 | + setTags(ref: CatalogRef, tags: string[]): Promise<void> | |
| 41 | + /** | |
| 42 | + * Copies an inventory item into the catalog, converting it to the catalog format: | |
| 43 | + * skill folder as is (minus generated `agents/openai.yaml`), rule/agent markdown as is, | |
| 44 | + * MCP config entry -> McpTemplate (secrets are never copied: literal env/header values become `fromEnv` refs named after the key), | |
| 45 | + * hook entry -> HookTemplate. Instructions items are refused. | |
| 46 | + */ | |
| 47 | + importFromInventory(item: InventoryItem, opts?: { name?: string; tags?: string[]; overwrite?: boolean }): Promise<CatalogItem> | |
| 48 | + /** Copies an external skill folder (marketplace checkout) into the catalog. */ | |
| 49 | + importSkillDirectory(dir: string, name: string, origin: ItemOrigin, opts?: { overwrite?: boolean }): Promise<CatalogItem> | |
| 50 | + savePreset(preset: Preset, previousName?: string): Promise<Preset> | |
| 51 | + deletePreset(name: string): Promise<void> | |
| 52 | +} | |
| 53 | + | |
| 54 | +export type OpenCatalog = (root: string) => CatalogStore | |
| 55 | + | |
| 56 | +/** packages/core/src/catalog/git.ts — git CLI wrapper on the catalog folder. */ | |
| 57 | +export interface CatalogGit { | |
| 58 | + status(): Promise<GitStatus> | |
| 59 | + init(): Promise<void> | |
| 60 | + setRemote(url: string): Promise<void> | |
| 61 | + /** Stages everything and commits; no-op when clean. Returns the commit hash, if any. */ | |
| 62 | + commitAll(message: string): Promise<string | undefined> | |
| 63 | + /** `git pull --rebase=false`; on conflict returns the status with `conflicts` filled, never throws for conflicts. */ | |
| 64 | + pull(): Promise<GitStatus> | |
| 65 | + push(): Promise<GitStatus> | |
| 66 | + /** commitAll + pull + push. */ | |
| 67 | + sync(message?: string): Promise<GitStatus> | |
| 68 | + /** Resolve a conflicted path with `ours` / `theirs`, then commit when no conflict remains. */ | |
| 69 | + resolve(path: string, side: 'ours' | 'theirs'): Promise<GitStatus> | |
| 70 | + currentCommit(): Promise<string | undefined> | |
| 71 | +} | |
| 72 | + | |
| 73 | +export type OpenCatalogGit = (root: string) => CatalogGit | |
| 74 | + | |
| 75 | +/** packages/core/src/sync — scope configuration, rendering per agent, plan/apply, drift. */ | |
| 76 | +export interface SyncEngine { | |
| 77 | + readConfig(target: SyncTarget): Promise<ScopeConfig | undefined> | |
| 78 | + writeConfig(target: SyncTarget, config: ScopeConfig): Promise<void> | |
| 79 | + readLock(target: SyncTarget): Promise<LockFile | undefined> | |
| 80 | + /** Pure computation: never writes. */ | |
| 81 | + plan(target: SyncTarget, config: ScopeConfig, catalog: CatalogStore): Promise<SyncPlan> | |
| 82 | + /** | |
| 83 | + * Applies a plan computed with the same inputs. Conflicts without a resolution are skipped. | |
| 84 | + * `push-to-catalog` writes the scope version into the catalog first. | |
| 85 | + */ | |
| 86 | + apply( | |
| 87 | + target: SyncTarget, | |
| 88 | + config: ScopeConfig, | |
| 89 | + catalog: CatalogStore, | |
| 90 | + plan: SyncPlan, | |
| 91 | + resolutions: Record<string, ConflictResolution>, | |
| 92 | + ): Promise<ApplyResult> | |
| 93 | + /** Status of each inventory item, keyed by inventory item id. */ | |
| 94 | + annotate(inventory: ScopeInventory, catalog: CatalogStore, lock: LockFile | undefined): Promise<Record<string, ItemStatus>> | |
| 95 | + /** | |
| 96 | + * Home cleanup: removes a Home item (skill folder or symlink, rule, agent) after it was imported into the catalog. | |
| 97 | + * Only for Home items of type skill / rule / agent. Symlinks are unlinked, never followed. | |
| 98 | + */ | |
| 99 | + removeHomeItem(target: SyncTarget, item: InventoryItem): Promise<void> | |
| 100 | +} | |
| 101 | + | |
| 102 | +/** packages/core/src/marketplace — browse sources and fetch skills. */ | |
| 103 | +export interface Marketplace { | |
| 104 | + /** Built-in + user sources (user sources stored by the server in ~/.skilly/config.json and passed in). */ | |
| 105 | + sources(): MarketplaceSource[] | |
| 106 | + /** Clones or updates the source in the cache (`git clone --depth 1` / `git pull`). */ | |
| 107 | + refresh(sourceId: string): Promise<MarketplaceSource> | |
| 108 | + /** Lists skills of a fetched source (refreshes first if never fetched). */ | |
| 109 | + listSkills(sourceId: string, catalog?: CatalogStore): Promise<MarketplaceSkill[]> | |
| 110 | + /** Absolute folder of a skill in the cache, for preview and import. */ | |
| 111 | + skillDir(sourceId: string, skillPath: string): Promise<string> | |
| 112 | +} | |
| 113 | + | |
| 114 | +export type CreateMarketplace = (opts: { cacheDir: string; userSources: MarketplaceSource[] }) => Marketplace | |
| new file mode 100644 | |||
| @@ -0,0 +1,114 @@ | |||
| 1 | +/** | ||
| 2 | + * Function contracts between core modules. Each module (catalog, git, sync, | ||
| 3 | + * marketplace) implements its part; the server only wires them together. | ||
| 4 | + */ | ||
| 5 | + | ||
| 6 | +import type { | ||
| 7 | + ApplyResult, | ||
| 8 | + CatalogItem, | ||
| 9 | + CatalogItemDetail, | ||
| 10 | + CatalogRef, | ||
| 11 | + CatalogSnapshot, | ||
| 12 | + ConflictResolution, | ||
| 13 | + GitStatus, | ||
| 14 | + ItemOrigin, | ||
| 15 | + ItemStatus, | ||
| 16 | + LockFile, | ||
| 17 | + MarketplaceSkill, | ||
| 18 | + MarketplaceSource, | ||
| 19 | + Preset, | ||
| 20 | + SaveItemInput, | ||
| 21 | + ScopeConfig, | ||
| 22 | + SyncPlan, | ||
| 23 | + SyncTarget, | ||
| 24 | +} from './catalog-model.ts' | ||
| 25 | +import type { InventoryItem, ScopeInventory } from './model.ts' | ||
| 26 | + | ||
| 27 | +/** packages/core/src/catalog — read/write the catalog folder. */ | ||
| 28 | +export interface CatalogStore { | ||
| 29 | + readonly root: string | ||
| 30 | + exists(): Promise<boolean> | ||
| 31 | + /** Creates the folder layout and `.skilly/index.json` if missing (idempotent). Does not run git. */ | ||
| 32 | + init(): Promise<void> | ||
| 33 | + snapshot(): Promise<CatalogSnapshot> | ||
| 34 | + getItem(ref: CatalogRef): Promise<CatalogItemDetail | undefined> | ||
| 35 | + /** Absolute paths of all files of an item (folder content for skills, the single file otherwise). */ | ||
| 36 | + itemFiles(ref: CatalogRef): Promise<{ rel: string; abs: string }[]> | ||
| 37 | + /** Create or update (or rename with `previousName`). Validates names and mcp/hook YAML. */ | ||
| 38 | + saveItem(input: SaveItemInput, origin?: ItemOrigin): Promise<CatalogItem> | ||
| 39 | + deleteItem(ref: CatalogRef): Promise<void> | ||
| 40 | + setTags(ref: CatalogRef, tags: string[]): Promise<void> | ||
| 41 | + /** | ||
| 42 | + * Copies an inventory item into the catalog, converting it to the catalog format: | ||
| 43 | + * skill folder as is (minus generated `agents/openai.yaml`), rule/agent markdown as is, | ||
| 44 | + * MCP config entry -> McpTemplate (secrets are never copied: literal env/header values become `fromEnv` refs named after the key), | ||
| 45 | + * hook entry -> HookTemplate. Instructions items are refused. | ||
| 46 | + */ | ||
| 47 | + importFromInventory(item: InventoryItem, opts?: { name?: string; tags?: string[]; overwrite?: boolean }): Promise<CatalogItem> | ||
| 48 | + /** Copies an external skill folder (marketplace checkout) into the catalog. */ | ||
| 49 | + importSkillDirectory(dir: string, name: string, origin: ItemOrigin, opts?: { overwrite?: boolean }): Promise<CatalogItem> | ||
| 50 | + savePreset(preset: Preset, previousName?: string): Promise<Preset> | ||
| 51 | + deletePreset(name: string): Promise<void> | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +export type OpenCatalog = (root: string) => CatalogStore | ||
| 55 | + | ||
| 56 | +/** packages/core/src/catalog/git.ts — git CLI wrapper on the catalog folder. */ | ||
| 57 | +export interface CatalogGit { | ||
| 58 | + status(): Promise<GitStatus> | ||
| 59 | + init(): Promise<void> | ||
| 60 | + setRemote(url: string): Promise<void> | ||
| 61 | + /** Stages everything and commits; no-op when clean. Returns the commit hash, if any. */ | ||
| 62 | + commitAll(message: string): Promise<string | undefined> | ||
| 63 | + /** `git pull --rebase=false`; on conflict returns the status with `conflicts` filled, never throws for conflicts. */ | ||
| 64 | + pull(): Promise<GitStatus> | ||
| 65 | + push(): Promise<GitStatus> | ||
| 66 | + /** commitAll + pull + push. */ | ||
| 67 | + sync(message?: string): Promise<GitStatus> | ||
| 68 | + /** Resolve a conflicted path with `ours` / `theirs`, then commit when no conflict remains. */ | ||
| 69 | + resolve(path: string, side: 'ours' | 'theirs'): Promise<GitStatus> | ||
| 70 | + currentCommit(): Promise<string | undefined> | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +export type OpenCatalogGit = (root: string) => CatalogGit | ||
| 74 | + | ||
| 75 | +/** packages/core/src/sync — scope configuration, rendering per agent, plan/apply, drift. */ | ||
| 76 | +export interface SyncEngine { | ||
| 77 | + readConfig(target: SyncTarget): Promise<ScopeConfig | undefined> | ||
| 78 | + writeConfig(target: SyncTarget, config: ScopeConfig): Promise<void> | ||
| 79 | + readLock(target: SyncTarget): Promise<LockFile | undefined> | ||
| 80 | + /** Pure computation: never writes. */ | ||
| 81 | + plan(target: SyncTarget, config: ScopeConfig, catalog: CatalogStore): Promise<SyncPlan> | ||
| 82 | + /** | ||
| 83 | + * Applies a plan computed with the same inputs. Conflicts without a resolution are skipped. | ||
| 84 | + * `push-to-catalog` writes the scope version into the catalog first. | ||
| 85 | + */ | ||
| 86 | + apply( | ||
| 87 | + target: SyncTarget, | ||
| 88 | + config: ScopeConfig, | ||
| 89 | + catalog: CatalogStore, | ||
| 90 | + plan: SyncPlan, | ||
| 91 | + resolutions: Record<string, ConflictResolution>, | ||
| 92 | + ): Promise<ApplyResult> | ||
| 93 | + /** Status of each inventory item, keyed by inventory item id. */ | ||
| 94 | + annotate(inventory: ScopeInventory, catalog: CatalogStore, lock: LockFile | undefined): Promise<Record<string, ItemStatus>> | ||
| 95 | + /** | ||
| 96 | + * Home cleanup: removes a Home item (skill folder or symlink, rule, agent) after it was imported into the catalog. | ||
| 97 | + * Only for Home items of type skill / rule / agent. Symlinks are unlinked, never followed. | ||
| 98 | + */ | ||
| 99 | + removeHomeItem(target: SyncTarget, item: InventoryItem): Promise<void> | ||
| 100 | +} | ||
| 101 | + | ||
| 102 | +/** packages/core/src/marketplace — browse sources and fetch skills. */ | ||
| 103 | +export interface Marketplace { | ||
| 104 | + /** Built-in + user sources (user sources stored by the server in ~/.skilly/config.json and passed in). */ | ||
| 105 | + sources(): MarketplaceSource[] | ||
| 106 | + /** Clones or updates the source in the cache (`git clone --depth 1` / `git pull`). */ | ||
| 107 | + refresh(sourceId: string): Promise<MarketplaceSource> | ||
| 108 | + /** Lists skills of a fetched source (refreshes first if never fetched). */ | ||
| 109 | + listSkills(sourceId: string, catalog?: CatalogStore): Promise<MarketplaceSkill[]> | ||
| 110 | + /** Absolute folder of a skill in the cache, for preview and import. */ | ||
| 111 | + skillDir(sourceId: string, skillPath: string): Promise<string> | ||
| 112 | +} | ||
| 113 | + | ||
| 114 | +export type CreateMarketplace = (opts: { cacheDir: string; userSources: MarketplaceSource[] }) => Marketplace | ||
modified
packages/core/src/index.ts +7 -1 | @@ -1,4 +1,10 @@ | ||
| 1 | 1 | export * from './model.ts' |
| 2 | +export * from './catalog-model.ts' | |
| 2 | 3 | export * from './api.ts' |
| 4 | +export * from './contracts.ts' | |
| 3 | 5 | export * from './inventory.ts' |
| 4 | -export { normalizePath } from './util.ts' | |
| 6 | +export { openCatalog } from './catalog/index.ts' | |
| 7 | +export { openCatalogGit } from './catalog/git.ts' | |
| 8 | +export { syncEngine } from './sync/index.ts' | |
| 9 | +export { createMarketplace } from './marketplace/index.ts' | |
| 10 | +export { normalizePath, hashPath } from './util.ts' | |
| @@ -1,4 +1,10 @@ | |||
| 1 | export * from './model.ts' | 1 | export * from './model.ts' |
| 2 | +export * from './catalog-model.ts' | ||
| 2 | export * from './api.ts' | 3 | export * from './api.ts' |
| 4 | +export * from './contracts.ts' | ||
| 3 | export * from './inventory.ts' | 5 | export * from './inventory.ts' |
| 4 | -export { normalizePath } from './util.ts' | 6 | +export { openCatalog } from './catalog/index.ts' |
| 7 | +export { openCatalogGit } from './catalog/git.ts' | ||
| 8 | +export { syncEngine } from './sync/index.ts' | ||
| 9 | +export { createMarketplace } from './marketplace/index.ts' | ||
| 10 | +export { normalizePath, hashPath } from './util.ts' | ||
added
packages/core/src/marketplace/index.ts +6 -0 | new file mode 100644 | ||
| @@ -0,0 +1,6 @@ | ||
| 1 | +import type { CreateMarketplace } from '../contracts.ts' | |
| 2 | + | |
| 3 | +// Placeholder: replaced by the real implementation. | |
| 4 | +export const createMarketplace: CreateMarketplace = () => { | |
| 5 | + throw new Error('not implemented') | |
| 6 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | +import type { CreateMarketplace } from '../contracts.ts' | ||
| 2 | + | ||
| 3 | +// Placeholder: replaced by the real implementation. | ||
| 4 | +export const createMarketplace: CreateMarketplace = () => { | ||
| 5 | + throw new Error('not implemented') | ||
| 6 | +} | ||
added
packages/core/src/sync/index.ts +16 -0 | new file mode 100644 | ||
| @@ -0,0 +1,16 @@ | ||
| 1 | +import type { SyncEngine } from '../contracts.ts' | |
| 2 | + | |
| 3 | +const notImplemented = async (): Promise<never> => { | |
| 4 | + throw new Error('not implemented') | |
| 5 | +} | |
| 6 | + | |
| 7 | +// Placeholder: replaced by the real implementation. | |
| 8 | +export const syncEngine: SyncEngine = { | |
| 9 | + readConfig: notImplemented, | |
| 10 | + writeConfig: notImplemented, | |
| 11 | + readLock: notImplemented, | |
| 12 | + plan: notImplemented, | |
| 13 | + apply: notImplemented, | |
| 14 | + annotate: notImplemented, | |
| 15 | + removeHomeItem: notImplemented, | |
| 16 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | +import type { SyncEngine } from '../contracts.ts' | ||
| 2 | + | ||
| 3 | +const notImplemented = async (): Promise<never> => { | ||
| 4 | + throw new Error('not implemented') | ||
| 5 | +} | ||
| 6 | + | ||
| 7 | +// Placeholder: replaced by the real implementation. | ||
| 8 | +export const syncEngine: SyncEngine = { | ||
| 9 | + readConfig: notImplemented, | ||
| 10 | + writeConfig: notImplemented, | ||
| 11 | + readLock: notImplemented, | ||
| 12 | + plan: notImplemented, | ||
| 13 | + apply: notImplemented, | ||
| 14 | + annotate: notImplemented, | ||
| 15 | + removeHomeItem: notImplemented, | ||
| 16 | +} | ||