@@ -3,26 +3,59 @@ export * as MarketplaceLocal from "./marketplace.js"
33import { randomUUID } from "node:crypto"
44import path from "node:path"
55import { eq } from "drizzle-orm"
6- import { Clock , Context , Effect , FileSystem , Layer , Schema } from "effect"
6+ import { Clock , Context , Effect , FileSystem , Layer , Record , Schema } from "effect"
77import { Marketplace } from "@opencode/schema/marketplace"
88import { Config } from "@opencode/schema/config"
9- import { AbsolutePath } from "@opencode/schema/schema"
9+ import { Agent } from "@opencode/schema/agent"
10+ import { AbsolutePath , NonNegativeInt } from "@opencode/schema/schema"
1011import { Hash } from "@opencode/util/hash"
1112import { makeGlobalNode } from "@opencode/util/effect/app-node"
1213import { filesystem } from "@opencode/util/effect/app-node-platform"
1314import { Database } from "./database/database.js"
1415import { MarketplaceTable } from "./marketplace/sql.js"
1516
17+ export const Key = Schema . String . check ( Schema . isPattern ( / ^ p k g _ [ a - f 0 - 9 ] { 64 } $ / ) ) . pipe (
18+ Schema . brand ( "MarketplaceLocal.Key" ) ,
19+ Schema . annotate ( { identifier : "MarketplaceLocal.Key" } ) ,
20+ )
21+ export type Key = typeof Key . Type
22+
23+ export interface Installed extends Schema . Schema . Type < typeof Installed > { }
24+ export const Installed = Schema . Struct ( {
25+ file : AbsolutePath ,
26+ catalog : Marketplace . Name ,
27+ item : Marketplace . Name ,
28+ name : Schema . NonEmptyString ,
29+ version : Schema . NonEmptyString ,
30+ digest : Schema . String . check ( Schema . isPattern ( / ^ s h a 2 5 6 : [ a - f 0 - 9 ] { 64 } $ / ) ) ,
31+ agents : Marketplace . AgentItem . fields . install . fields . agents ,
32+ } ) . annotate ( { identifier : "MarketplaceLocal.Installed" } )
33+
34+ export interface State extends Schema . Schema . Type < typeof State > { }
35+ export const State = Schema . Struct ( {
36+ revision : NonNegativeInt ,
37+ installed : Schema . Record ( Key , Installed ) ,
38+ } ) . annotate ( { identifier : "MarketplaceLocal.State" } )
39+
40+ export interface Plan extends Schema . Schema . Type < typeof Plan > { }
41+ export const Plan = Schema . Struct ( {
42+ id : Schema . String ,
43+ revision : NonNegativeInt ,
44+ expires : NonNegativeInt ,
45+ key : Key ,
46+ installed : Installed ,
47+ } ) . annotate ( { identifier : "MarketplaceLocal.Plan" } )
48+
1649export class Failed extends Schema . TaggedError < Failed > ( ) ( "Marketplace.Failed" , {
17- reason : Schema . Literals ( [ "catalog" , "item" , "plan" , "expired" , "revision" , "collision" , " capacity"] ) ,
50+ reason : Schema . Literals ( [ "catalog" , "item" , "plan" , "expired" , "revision" , "capacity" ] ) ,
1851 message : Schema . String ,
1952} ) { }
2053
2154export interface Interface {
22- readonly read : Effect . Effect < Marketplace . State >
23- readonly plan : ( input : { file : AbsolutePath ; item : string } ) => Effect . Effect < Marketplace . Plan , Failed >
24- readonly install : ( id : string ) => Effect . Effect < Marketplace . State , Failed >
25- readonly uninstall : ( input : { key : Marketplace . Key ; revision : number } ) => Effect . Effect < Marketplace . State , Failed >
55+ readonly read : Effect . Effect < State >
56+ readonly plan : ( input : { file : AbsolutePath ; item : string } ) => Effect . Effect < Plan , Failed >
57+ readonly install : ( id : string ) => Effect . Effect < State , Failed >
58+ readonly uninstall : ( input : { key : Key ; revision : number } ) => Effect . Effect < State , Failed >
2659}
2760
2861export class Service extends Context . Service < Service , Interface > ( ) ( "@opencode/MarketplaceLocal" ) { }
@@ -36,10 +69,10 @@ export const node = makeGlobalNode({
3669 Effect . gen ( function * ( ) {
3770 const database = yield * Database . Service
3871 const fs = yield * FileSystem . FileSystem
39- const plans = new Map < string , { json : string ; revision : number ; expires : number ; key : Marketplace . Key } > ( )
40- const installedJSON = Schema . fromJsonString ( Marketplace . Installed )
72+ const plans = new Map < string , { json : string ; revision : number ; expires : number ; key : Key } > ( )
73+ const installedJSON = Schema . fromJsonString ( Installed )
4174 const decodeInstalled = Schema . decodeUnknownEffect ( installedJSON )
42- const decodeState = Schema . decodeUnknownEffect ( Marketplace . State )
75+ const decodeState = Schema . decodeUnknownEffect ( State )
4376 const read = database . db
4477 . select ( )
4578 . from ( MarketplaceTable )
@@ -48,13 +81,11 @@ export const node = makeGlobalNode({
4881 . pipe (
4982 Effect . orDie ,
5083 Effect . flatMap ( ( row ) =>
51- row
52- ? decodeState ( row ) . pipe ( Effect . orDie )
53- : Effect . succeed ( Marketplace . State . make ( { revision : 0 , installed : { } } ) ) ,
84+ row ? decodeState ( row ) . pipe ( Effect . orDie ) : Effect . succeed ( State . make ( { revision : 0 , installed : { } } ) ) ,
5485 ) ,
5586 )
56- const write = Effect . fnUntraced ( function * ( next : Marketplace . State ) {
57- const encoded = yield * Schema . encodeEffect ( Marketplace . State ) ( next ) . pipe ( Effect . orDie )
87+ const write = Effect . fnUntraced ( function * ( next : State ) {
88+ const encoded = yield * Schema . encodeEffect ( State ) ( next ) . pipe ( Effect . orDie )
5889 yield * database . db
5990 . insert ( MarketplaceTable )
6091 . values ( { id : 0 , ...encoded } )
@@ -86,7 +117,7 @@ export const node = makeGlobalNode({
86117 } ) ( text ) . pipe ( Effect . mapError ( ( error ) => new Failed ( { reason : "catalog" , message : error . message } ) ) )
87118 const item = catalog . items . find ( ( item ) => item . id === input . item )
88119 if ( ! item ) return yield * new Failed ( { reason : "item" , message : `Catalog item not found: ${ input . item } ` } )
89- const installed : Marketplace . Installed = {
120+ const installed : Installed = {
90121 file : AbsolutePath . make ( file ) ,
91122 catalog : catalog . id ,
92123 item : item . id ,
@@ -96,7 +127,7 @@ export const node = makeGlobalNode({
96127 agents : item . install . agents ,
97128 }
98129 const id = randomUUID ( )
99- const key = Marketplace . Key . make ( `pkg_${ Hash . sha256 ( JSON . stringify ( [ file , catalog . id , item . id ] ) ) } ` )
130+ const key = Key . make ( `pkg_${ Hash . sha256 ( JSON . stringify ( [ file , catalog . id , item . id ] ) ) } ` )
100131 const state = yield * read
101132 const json = yield * Schema . encodeEffect ( installedJSON ) ( installed ) . pipe ( Effect . orDie )
102133 const now = yield * Clock . currentTimeMillis
@@ -138,16 +169,6 @@ export const node = makeGlobalNode({
138169 reason : "revision" ,
139170 message : "Marketplace state changed; prepare a new plan" ,
140171 } )
141- const names = Object . keys ( installed . agents )
142- if (
143- Object . entries ( state . installed ) . some (
144- ( [ key , value ] ) => key !== stored . key && names . some ( ( name ) => Object . hasOwn ( value . agents , name ) ) ,
145- )
146- )
147- return yield * new Failed ( {
148- reason : "collision" ,
149- message : "An installed package already owns an agent with this name" ,
150- } )
151172 return yield * write ( {
152173 revision : state . revision + 1 ,
153174 installed : { ...state . installed , [ stored . key ] : installed } ,
@@ -159,10 +180,7 @@ export const node = makeGlobalNode({
159180 return next
160181 } )
161182
162- const uninstall = Effect . fn ( "MarketplaceLocal.uninstall" ) ( function * ( input : {
163- key : Marketplace . Key
164- revision : number
165- } ) {
183+ const uninstall = Effect . fn ( "MarketplaceLocal.uninstall" ) ( function * ( input : { key : Key ; revision : number } ) {
166184 return yield * database . db
167185 . transaction ( ( ) =>
168186 Effect . gen ( function * ( ) {
@@ -186,9 +204,22 @@ export const node = makeGlobalNode({
186204 ) ,
187205} )
188206
189- /** Explicit projection only; the caller owns where these entries sit in its Location's config order. */
190- export function documents ( state : Marketplace . State ) {
191- return Object . entries ( state . installed )
207+ /** Catalog-local names cannot address the qualified IDs of other packages or ordinary config agents. */
208+ export function agentID ( key : Key , name : string ) {
209+ return Agent . ID . make ( `marketplace/${ key } /${ name } ` )
210+ }
211+
212+ /** Explicit projection only. Agent identities are namespaced independently of config-entry order. */
213+ export function documents ( state : State ) {
214+ return Record . toEntries ( state . installed )
192215 . toSorted ( ( [ left ] , [ right ] ) => left . localeCompare ( right ) )
193- . map ( ( [ , item ] ) => new Config . Document ( { type : "document" , info : new Config . Info ( { agents : item . agents } ) } ) )
216+ . map (
217+ ( [ key , item ] ) =>
218+ new Config . Document ( {
219+ type : "document" ,
220+ info : new Config . Info ( {
221+ agents : Object . fromEntries ( Object . entries ( item . agents ) . map ( ( [ name , info ] ) => [ agentID ( key , name ) , info ] ) ) ,
222+ } ) ,
223+ } ) ,
224+ )
194225}
0 commit comments