hlassiege/panopliepublic Fork
⑂ 792a0a8
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/panoplie.git
git clone ssh://git@rickub.com/hlassiege/panoplie.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

chore(core): contracts for catalog, presets, scope sync, git and marketplaces

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hlassiege committed 2026-10-23T11:57:44+02:00 Browse files
792a0a8 parent: e2ab7eb
modified packages/core/src/api.ts +133 -0
@@ -62,3 +62,136 @@ export interface FileContent {
6262 /** True when the file was larger than 512 KiB and the content was cut. */
6363 truncated: boolean
6464 }
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: boolean63 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 @@
11 export * from './model.ts'
2+export * from './catalog-model.ts'
23 export * from './api.ts'
4+export * from './contracts.ts'
35 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+}