diff --git a/packages/client/src/contract.ts b/packages/client/src/contract.ts index f117e058bb0..61dda7c6107 100644 --- a/packages/client/src/contract.ts +++ b/packages/client/src/contract.ts @@ -35,6 +35,7 @@ export const groupNames = { "server.pty": "ptys", "server.question": "questions", "server.reference": "references", + "server.project": "project", "server.projectCopy": "projectCopies", } as const diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts index bd1ab7845c5..ed041ed50f1 100644 --- a/packages/client/src/generated-effect/client.ts +++ b/packages/client/src/generated-effect/client.ts @@ -410,36 +410,57 @@ const Endpoint9_1 = (raw: RawClient["server.credential"]) => (input: Endpoint9_1 const adaptGroup9 = (raw: RawClient["server.credential"]) => ({ update: Endpoint9_0(raw), remove: Endpoint9_1(raw) }) -type Endpoint10_0Request = Parameters[0] +type Endpoint10_0Request = Parameters[0] type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } -const Endpoint10_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint10_0Input) => +const Endpoint10_0 = (raw: RawClient["server.project"]) => (input?: Endpoint10_0Input) => + raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint10_1Request = Parameters[0] +type Endpoint10_1Input = { + readonly projectID: Endpoint10_1Request["params"]["projectID"] + readonly location?: Endpoint10_1Request["query"]["location"] +} +const Endpoint10_1 = (raw: RawClient["server.project"]) => (input: Endpoint10_1Input) => + raw["project.directories"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)) + +const adaptGroup10 = (raw: RawClient["server.project"]) => ({ + current: Endpoint10_0(raw), + directories: Endpoint10_1(raw), +}) + +type Endpoint11_0Request = Parameters[0] +type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } +const Endpoint11_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint11_0Input) => raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint10_1Request = Parameters[0] -type Endpoint10_1Input = { readonly projectID?: Endpoint10_1Request["query"]["projectID"] } -const Endpoint10_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint10_1Input) => +type Endpoint11_1Request = Parameters[0] +type Endpoint11_1Input = { readonly projectID?: Endpoint11_1Request["query"]["projectID"] } +const Endpoint11_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint11_1Input) => raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint10_2Request = Parameters[0] -type Endpoint10_2Input = { readonly id: Endpoint10_2Request["params"]["id"] } -const Endpoint10_2 = (raw: RawClient["server.permission"]) => (input: Endpoint10_2Input) => +type Endpoint11_2Request = Parameters[0] +type Endpoint11_2Input = { readonly id: Endpoint11_2Request["params"]["id"] } +const Endpoint11_2 = (raw: RawClient["server.permission"]) => (input: Endpoint11_2Input) => raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint10_3Request = Parameters[0] -type Endpoint10_3Input = { - readonly sessionID: Endpoint10_3Request["params"]["sessionID"] - readonly id?: Endpoint10_3Request["payload"]["id"] - readonly action: Endpoint10_3Request["payload"]["action"] - readonly resources: Endpoint10_3Request["payload"]["resources"] - readonly save?: Endpoint10_3Request["payload"]["save"] - readonly metadata?: Endpoint10_3Request["payload"]["metadata"] - readonly source?: Endpoint10_3Request["payload"]["source"] - readonly agent?: Endpoint10_3Request["payload"]["agent"] +type Endpoint11_3Request = Parameters[0] +type Endpoint11_3Input = { + readonly sessionID: Endpoint11_3Request["params"]["sessionID"] + readonly id?: Endpoint11_3Request["payload"]["id"] + readonly action: Endpoint11_3Request["payload"]["action"] + readonly resources: Endpoint11_3Request["payload"]["resources"] + readonly save?: Endpoint11_3Request["payload"]["save"] + readonly metadata?: Endpoint11_3Request["payload"]["metadata"] + readonly source?: Endpoint11_3Request["payload"]["source"] + readonly agent?: Endpoint11_3Request["payload"]["agent"] } -const Endpoint10_3 = (raw: RawClient["server.permission"]) => (input: Endpoint10_3Input) => +const Endpoint11_3 = (raw: RawClient["server.permission"]) => (input: Endpoint11_3Input) => raw["session.permission.create"]({ params: { sessionID: input["sessionID"] }, payload: { @@ -456,87 +477,87 @@ const Endpoint10_3 = (raw: RawClient["server.permission"]) => (input: Endpoint10 Effect.map((value) => value.data), ) -type Endpoint10_4Request = Parameters[0] -type Endpoint10_4Input = { readonly sessionID: Endpoint10_4Request["params"]["sessionID"] } -const Endpoint10_4 = (raw: RawClient["server.permission"]) => (input: Endpoint10_4Input) => +type Endpoint11_4Request = Parameters[0] +type Endpoint11_4Input = { readonly sessionID: Endpoint11_4Request["params"]["sessionID"] } +const Endpoint11_4 = (raw: RawClient["server.permission"]) => (input: Endpoint11_4Input) => raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint10_5Request = Parameters[0] -type Endpoint10_5Input = { - readonly sessionID: Endpoint10_5Request["params"]["sessionID"] - readonly requestID: Endpoint10_5Request["params"]["requestID"] +type Endpoint11_5Request = Parameters[0] +type Endpoint11_5Input = { + readonly sessionID: Endpoint11_5Request["params"]["sessionID"] + readonly requestID: Endpoint11_5Request["params"]["requestID"] } -const Endpoint10_5 = (raw: RawClient["server.permission"]) => (input: Endpoint10_5Input) => +const Endpoint11_5 = (raw: RawClient["server.permission"]) => (input: Endpoint11_5Input) => raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint10_6Request = Parameters[0] -type Endpoint10_6Input = { - readonly sessionID: Endpoint10_6Request["params"]["sessionID"] - readonly requestID: Endpoint10_6Request["params"]["requestID"] - readonly reply: Endpoint10_6Request["payload"]["reply"] - readonly message?: Endpoint10_6Request["payload"]["message"] +type Endpoint11_6Request = Parameters[0] +type Endpoint11_6Input = { + readonly sessionID: Endpoint11_6Request["params"]["sessionID"] + readonly requestID: Endpoint11_6Request["params"]["requestID"] + readonly reply: Endpoint11_6Request["payload"]["reply"] + readonly message?: Endpoint11_6Request["payload"]["message"] } -const Endpoint10_6 = (raw: RawClient["server.permission"]) => (input: Endpoint10_6Input) => +const Endpoint11_6 = (raw: RawClient["server.permission"]) => (input: Endpoint11_6Input) => raw["session.permission.reply"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] }, payload: { reply: input["reply"], message: input["message"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup10 = (raw: RawClient["server.permission"]) => ({ - listRequests: Endpoint10_0(raw), - listSaved: Endpoint10_1(raw), - removeSaved: Endpoint10_2(raw), - create: Endpoint10_3(raw), - list: Endpoint10_4(raw), - get: Endpoint10_5(raw), - reply: Endpoint10_6(raw), +const adaptGroup11 = (raw: RawClient["server.permission"]) => ({ + listRequests: Endpoint11_0(raw), + listSaved: Endpoint11_1(raw), + removeSaved: Endpoint11_2(raw), + create: Endpoint11_3(raw), + list: Endpoint11_4(raw), + get: Endpoint11_5(raw), + reply: Endpoint11_6(raw), }) -type Endpoint11_0Request = Parameters[0] -type Endpoint11_0Input = { - readonly location?: Endpoint11_0Request["query"]["location"] - readonly path?: Endpoint11_0Request["query"]["path"] +type Endpoint12_0Request = Parameters[0] +type Endpoint12_0Input = { + readonly location?: Endpoint12_0Request["query"]["location"] + readonly path?: Endpoint12_0Request["query"]["path"] } -const Endpoint11_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint11_0Input) => +const Endpoint12_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint12_0Input) => raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint11_1Request = Parameters[0] -type Endpoint11_1Input = { - readonly location?: Endpoint11_1Request["query"]["location"] - readonly query: Endpoint11_1Request["query"]["query"] - readonly type?: Endpoint11_1Request["query"]["type"] - readonly limit?: Endpoint11_1Request["query"]["limit"] +type Endpoint12_1Request = Parameters[0] +type Endpoint12_1Input = { + readonly location?: Endpoint12_1Request["query"]["location"] + readonly query: Endpoint12_1Request["query"]["query"] + readonly type?: Endpoint12_1Request["query"]["type"] + readonly limit?: Endpoint12_1Request["query"]["limit"] } -const Endpoint11_1 = (raw: RawClient["server.fs"]) => (input: Endpoint11_1Input) => +const Endpoint12_1 = (raw: RawClient["server.fs"]) => (input: Endpoint12_1Input) => raw["fs.find"]({ query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup11 = (raw: RawClient["server.fs"]) => ({ list: Endpoint11_0(raw), find: Endpoint11_1(raw) }) +const adaptGroup12 = (raw: RawClient["server.fs"]) => ({ list: Endpoint12_0(raw), find: Endpoint12_1(raw) }) -type Endpoint12_0Request = Parameters[0] -type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] } -const Endpoint12_0 = (raw: RawClient["server.command"]) => (input?: Endpoint12_0Input) => +type Endpoint13_0Request = Parameters[0] +type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] } +const Endpoint13_0 = (raw: RawClient["server.command"]) => (input?: Endpoint13_0Input) => raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -const adaptGroup12 = (raw: RawClient["server.command"]) => ({ list: Endpoint12_0(raw) }) +const adaptGroup13 = (raw: RawClient["server.command"]) => ({ list: Endpoint13_0(raw) }) -type Endpoint13_0Request = Parameters[0] -type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] } -const Endpoint13_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint13_0Input) => +type Endpoint14_0Request = Parameters[0] +type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } +const Endpoint14_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint14_0Input) => raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -const adaptGroup13 = (raw: RawClient["server.skill"]) => ({ list: Endpoint13_0(raw) }) +const adaptGroup14 = (raw: RawClient["server.skill"]) => ({ list: Endpoint14_0(raw) }) -const Endpoint14_0 = (raw: RawClient["server.event"]) => () => +const Endpoint15_0 = (raw: RawClient["server.event"]) => () => Stream.unwrap( raw["event.subscribe"]({}).pipe( Effect.mapError(mapClientError), @@ -544,23 +565,23 @@ const Endpoint14_0 = (raw: RawClient["server.event"]) => () => ), ) -const adaptGroup14 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint14_0(raw) }) +const adaptGroup15 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint15_0(raw) }) -type Endpoint15_0Request = Parameters[0] -type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } -const Endpoint15_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint15_0Input) => +type Endpoint16_0Request = Parameters[0] +type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] } +const Endpoint16_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint16_0Input) => raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint15_1Request = Parameters[0] -type Endpoint15_1Input = { - readonly location?: Endpoint15_1Request["query"]["location"] - readonly command?: Endpoint15_1Request["payload"]["command"] - readonly args?: Endpoint15_1Request["payload"]["args"] - readonly cwd?: Endpoint15_1Request["payload"]["cwd"] - readonly title?: Endpoint15_1Request["payload"]["title"] - readonly env?: Endpoint15_1Request["payload"]["env"] +type Endpoint16_1Request = Parameters[0] +type Endpoint16_1Input = { + readonly location?: Endpoint16_1Request["query"]["location"] + readonly command?: Endpoint16_1Request["payload"]["command"] + readonly args?: Endpoint16_1Request["payload"]["args"] + readonly cwd?: Endpoint16_1Request["payload"]["cwd"] + readonly title?: Endpoint16_1Request["payload"]["title"] + readonly env?: Endpoint16_1Request["payload"]["env"] } -const Endpoint15_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint15_1Input) => +const Endpoint16_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint16_1Input) => raw["pty.create"]({ query: { location: input?.["location"] }, payload: { @@ -572,201 +593,201 @@ const Endpoint15_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint15_1Inpu }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint15_2Request = Parameters[0] -type Endpoint15_2Input = { - readonly ptyID: Endpoint15_2Request["params"]["ptyID"] - readonly location?: Endpoint15_2Request["query"]["location"] +type Endpoint16_2Request = Parameters[0] +type Endpoint16_2Input = { + readonly ptyID: Endpoint16_2Request["params"]["ptyID"] + readonly location?: Endpoint16_2Request["query"]["location"] } -const Endpoint15_2 = (raw: RawClient["server.pty"]) => (input: Endpoint15_2Input) => +const Endpoint16_2 = (raw: RawClient["server.pty"]) => (input: Endpoint16_2Input) => raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint15_3Request = Parameters[0] -type Endpoint15_3Input = { - readonly ptyID: Endpoint15_3Request["params"]["ptyID"] - readonly location?: Endpoint15_3Request["query"]["location"] - readonly title?: Endpoint15_3Request["payload"]["title"] - readonly size?: Endpoint15_3Request["payload"]["size"] +type Endpoint16_3Request = Parameters[0] +type Endpoint16_3Input = { + readonly ptyID: Endpoint16_3Request["params"]["ptyID"] + readonly location?: Endpoint16_3Request["query"]["location"] + readonly title?: Endpoint16_3Request["payload"]["title"] + readonly size?: Endpoint16_3Request["payload"]["size"] } -const Endpoint15_3 = (raw: RawClient["server.pty"]) => (input: Endpoint15_3Input) => +const Endpoint16_3 = (raw: RawClient["server.pty"]) => (input: Endpoint16_3Input) => raw["pty.update"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] }, payload: { title: input["title"], size: input["size"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint15_4Request = Parameters[0] -type Endpoint15_4Input = { - readonly ptyID: Endpoint15_4Request["params"]["ptyID"] - readonly location?: Endpoint15_4Request["query"]["location"] +type Endpoint16_4Request = Parameters[0] +type Endpoint16_4Input = { + readonly ptyID: Endpoint16_4Request["params"]["ptyID"] + readonly location?: Endpoint16_4Request["query"]["location"] } -const Endpoint15_4 = (raw: RawClient["server.pty"]) => (input: Endpoint15_4Input) => +const Endpoint16_4 = (raw: RawClient["server.pty"]) => (input: Endpoint16_4Input) => raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ) -const adaptGroup15 = (raw: RawClient["server.pty"]) => ({ - list: Endpoint15_0(raw), - create: Endpoint15_1(raw), - get: Endpoint15_2(raw), - update: Endpoint15_3(raw), - remove: Endpoint15_4(raw), +const adaptGroup16 = (raw: RawClient["server.pty"]) => ({ + list: Endpoint16_0(raw), + create: Endpoint16_1(raw), + get: Endpoint16_2(raw), + update: Endpoint16_3(raw), + remove: Endpoint16_4(raw), }) -type Endpoint16_0Request = Parameters[0] -type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] } -const Endpoint16_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint16_0Input) => +type Endpoint17_0Request = Parameters[0] +type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } +const Endpoint17_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint17_0Input) => raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint16_1Request = Parameters[0] -type Endpoint16_1Input = { - readonly location?: Endpoint16_1Request["query"]["location"] - readonly command: Endpoint16_1Request["payload"]["command"] - readonly cwd?: Endpoint16_1Request["payload"]["cwd"] - readonly timeout?: Endpoint16_1Request["payload"]["timeout"] - readonly metadata?: Endpoint16_1Request["payload"]["metadata"] +type Endpoint17_1Request = Parameters[0] +type Endpoint17_1Input = { + readonly location?: Endpoint17_1Request["query"]["location"] + readonly command: Endpoint17_1Request["payload"]["command"] + readonly cwd?: Endpoint17_1Request["payload"]["cwd"] + readonly timeout?: Endpoint17_1Request["payload"]["timeout"] + readonly metadata?: Endpoint17_1Request["payload"]["metadata"] } -const Endpoint16_1 = (raw: RawClient["server.shell"]) => (input: Endpoint16_1Input) => +const Endpoint17_1 = (raw: RawClient["server.shell"]) => (input: Endpoint17_1Input) => raw["shell.create"]({ query: { location: input["location"] }, payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint16_2Request = Parameters[0] -type Endpoint16_2Input = { - readonly id: Endpoint16_2Request["params"]["id"] - readonly location?: Endpoint16_2Request["query"]["location"] +type Endpoint17_2Request = Parameters[0] +type Endpoint17_2Input = { + readonly id: Endpoint17_2Request["params"]["id"] + readonly location?: Endpoint17_2Request["query"]["location"] } -const Endpoint16_2 = (raw: RawClient["server.shell"]) => (input: Endpoint16_2Input) => +const Endpoint17_2 = (raw: RawClient["server.shell"]) => (input: Endpoint17_2Input) => raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint16_3Request = Parameters[0] -type Endpoint16_3Input = { - readonly id: Endpoint16_3Request["params"]["id"] - readonly location?: Endpoint16_3Request["query"]["location"] - readonly cursor?: Endpoint16_3Request["query"]["cursor"] - readonly limit?: Endpoint16_3Request["query"]["limit"] +type Endpoint17_3Request = Parameters[0] +type Endpoint17_3Input = { + readonly id: Endpoint17_3Request["params"]["id"] + readonly location?: Endpoint17_3Request["query"]["location"] + readonly cursor?: Endpoint17_3Request["query"]["cursor"] + readonly limit?: Endpoint17_3Request["query"]["limit"] } -const Endpoint16_3 = (raw: RawClient["server.shell"]) => (input: Endpoint16_3Input) => +const Endpoint17_3 = (raw: RawClient["server.shell"]) => (input: Endpoint17_3Input) => raw["shell.output"]({ params: { id: input["id"] }, query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint16_4Request = Parameters[0] -type Endpoint16_4Input = { - readonly id: Endpoint16_4Request["params"]["id"] - readonly location?: Endpoint16_4Request["query"]["location"] +type Endpoint17_4Request = Parameters[0] +type Endpoint17_4Input = { + readonly id: Endpoint17_4Request["params"]["id"] + readonly location?: Endpoint17_4Request["query"]["location"] } -const Endpoint16_4 = (raw: RawClient["server.shell"]) => (input: Endpoint16_4Input) => +const Endpoint17_4 = (raw: RawClient["server.shell"]) => (input: Endpoint17_4Input) => raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ) -const adaptGroup16 = (raw: RawClient["server.shell"]) => ({ - list: Endpoint16_0(raw), - create: Endpoint16_1(raw), - get: Endpoint16_2(raw), - output: Endpoint16_3(raw), - remove: Endpoint16_4(raw), +const adaptGroup17 = (raw: RawClient["server.shell"]) => ({ + list: Endpoint17_0(raw), + create: Endpoint17_1(raw), + get: Endpoint17_2(raw), + output: Endpoint17_3(raw), + remove: Endpoint17_4(raw), }) -type Endpoint17_0Request = Parameters[0] -type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } -const Endpoint17_0 = (raw: RawClient["server.question"]) => (input?: Endpoint17_0Input) => +type Endpoint18_0Request = Parameters[0] +type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } +const Endpoint18_0 = (raw: RawClient["server.question"]) => (input?: Endpoint18_0Input) => raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint17_1Request = Parameters[0] -type Endpoint17_1Input = { readonly sessionID: Endpoint17_1Request["params"]["sessionID"] } -const Endpoint17_1 = (raw: RawClient["server.question"]) => (input: Endpoint17_1Input) => +type Endpoint18_1Request = Parameters[0] +type Endpoint18_1Input = { readonly sessionID: Endpoint18_1Request["params"]["sessionID"] } +const Endpoint18_1 = (raw: RawClient["server.question"]) => (input: Endpoint18_1Input) => raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint17_2Request = Parameters[0] -type Endpoint17_2Input = { - readonly sessionID: Endpoint17_2Request["params"]["sessionID"] - readonly requestID: Endpoint17_2Request["params"]["requestID"] - readonly answers: Endpoint17_2Request["payload"]["answers"] +type Endpoint18_2Request = Parameters[0] +type Endpoint18_2Input = { + readonly sessionID: Endpoint18_2Request["params"]["sessionID"] + readonly requestID: Endpoint18_2Request["params"]["requestID"] + readonly answers: Endpoint18_2Request["payload"]["answers"] } -const Endpoint17_2 = (raw: RawClient["server.question"]) => (input: Endpoint17_2Input) => +const Endpoint18_2 = (raw: RawClient["server.question"]) => (input: Endpoint18_2Input) => raw["session.question.reply"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] }, payload: { answers: input["answers"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint17_3Request = Parameters[0] -type Endpoint17_3Input = { - readonly sessionID: Endpoint17_3Request["params"]["sessionID"] - readonly requestID: Endpoint17_3Request["params"]["requestID"] +type Endpoint18_3Request = Parameters[0] +type Endpoint18_3Input = { + readonly sessionID: Endpoint18_3Request["params"]["sessionID"] + readonly requestID: Endpoint18_3Request["params"]["requestID"] } -const Endpoint17_3 = (raw: RawClient["server.question"]) => (input: Endpoint17_3Input) => +const Endpoint18_3 = (raw: RawClient["server.question"]) => (input: Endpoint18_3Input) => raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( Effect.mapError(mapClientError), ) -const adaptGroup17 = (raw: RawClient["server.question"]) => ({ - listRequests: Endpoint17_0(raw), - list: Endpoint17_1(raw), - reply: Endpoint17_2(raw), - reject: Endpoint17_3(raw), +const adaptGroup18 = (raw: RawClient["server.question"]) => ({ + listRequests: Endpoint18_0(raw), + list: Endpoint18_1(raw), + reply: Endpoint18_2(raw), + reject: Endpoint18_3(raw), }) -type Endpoint18_0Request = Parameters[0] -type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } -const Endpoint18_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint18_0Input) => +type Endpoint19_0Request = Parameters[0] +type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] } +const Endpoint19_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint19_0Input) => raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -const adaptGroup18 = (raw: RawClient["server.reference"]) => ({ list: Endpoint18_0(raw) }) +const adaptGroup19 = (raw: RawClient["server.reference"]) => ({ list: Endpoint19_0(raw) }) -type Endpoint19_0Request = Parameters[0] -type Endpoint19_0Input = { - readonly projectID: Endpoint19_0Request["params"]["projectID"] - readonly location?: Endpoint19_0Request["query"]["location"] - readonly strategy: Endpoint19_0Request["payload"]["strategy"] - readonly directory: Endpoint19_0Request["payload"]["directory"] - readonly name?: Endpoint19_0Request["payload"]["name"] +type Endpoint20_0Request = Parameters[0] +type Endpoint20_0Input = { + readonly projectID: Endpoint20_0Request["params"]["projectID"] + readonly location?: Endpoint20_0Request["query"]["location"] + readonly strategy: Endpoint20_0Request["payload"]["strategy"] + readonly directory: Endpoint20_0Request["payload"]["directory"] + readonly name?: Endpoint20_0Request["payload"]["name"] } -const Endpoint19_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint19_0Input) => +const Endpoint20_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint20_0Input) => raw["projectCopy.create"]({ params: { projectID: input["projectID"] }, query: { location: input["location"] }, payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint19_1Request = Parameters[0] -type Endpoint19_1Input = { - readonly projectID: Endpoint19_1Request["params"]["projectID"] - readonly location?: Endpoint19_1Request["query"]["location"] - readonly directory: Endpoint19_1Request["payload"]["directory"] - readonly force: Endpoint19_1Request["payload"]["force"] +type Endpoint20_1Request = Parameters[0] +type Endpoint20_1Input = { + readonly projectID: Endpoint20_1Request["params"]["projectID"] + readonly location?: Endpoint20_1Request["query"]["location"] + readonly directory: Endpoint20_1Request["payload"]["directory"] + readonly force: Endpoint20_1Request["payload"]["force"] } -const Endpoint19_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint19_1Input) => +const Endpoint20_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint20_1Input) => raw["projectCopy.remove"]({ params: { projectID: input["projectID"] }, query: { location: input["location"] }, payload: { directory: input["directory"], force: input["force"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint19_2Request = Parameters[0] -type Endpoint19_2Input = { - readonly projectID: Endpoint19_2Request["params"]["projectID"] - readonly location?: Endpoint19_2Request["query"]["location"] +type Endpoint20_2Request = Parameters[0] +type Endpoint20_2Input = { + readonly projectID: Endpoint20_2Request["params"]["projectID"] + readonly location?: Endpoint20_2Request["query"]["location"] } -const Endpoint19_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint19_2Input) => +const Endpoint20_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint20_2Input) => raw["projectCopy.refresh"]({ params: { projectID: input["projectID"] }, query: { location: input["location"] }, }).pipe(Effect.mapError(mapClientError)) -const adaptGroup19 = (raw: RawClient["server.projectCopy"]) => ({ - create: Endpoint19_0(raw), - remove: Endpoint19_1(raw), - refresh: Endpoint19_2(raw), +const adaptGroup20 = (raw: RawClient["server.projectCopy"]) => ({ + create: Endpoint20_0(raw), + remove: Endpoint20_1(raw), + refresh: Endpoint20_2(raw), }) const adaptClient = (raw: RawClient) => ({ @@ -780,16 +801,17 @@ const adaptClient = (raw: RawClient) => ({ providers: adaptGroup7(raw["server.provider"]), integrations: adaptGroup8(raw["server.integration"]), credentials: adaptGroup9(raw["server.credential"]), - permissions: adaptGroup10(raw["server.permission"]), - files: adaptGroup11(raw["server.fs"]), - commands: adaptGroup12(raw["server.command"]), - skills: adaptGroup13(raw["server.skill"]), - events: adaptGroup14(raw["server.event"]), - ptys: adaptGroup15(raw["server.pty"]), - "server.shell": adaptGroup16(raw["server.shell"]), - questions: adaptGroup17(raw["server.question"]), - references: adaptGroup18(raw["server.reference"]), - projectCopies: adaptGroup19(raw["server.projectCopy"]), + project: adaptGroup10(raw["server.project"]), + permissions: adaptGroup11(raw["server.permission"]), + files: adaptGroup12(raw["server.fs"]), + commands: adaptGroup13(raw["server.command"]), + skills: adaptGroup14(raw["server.skill"]), + events: adaptGroup15(raw["server.event"]), + ptys: adaptGroup16(raw["server.pty"]), + "server.shell": adaptGroup17(raw["server.shell"]), + questions: adaptGroup18(raw["server.question"]), + references: adaptGroup19(raw["server.reference"]), + projectCopies: adaptGroup20(raw["server.projectCopy"]), }) export const make = (options?: { readonly baseUrl?: URL | string }) => diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts index 56802f8c6c3..e0ceb3bf2a6 100644 --- a/packages/client/src/generated/client.ts +++ b/packages/client/src/generated/client.ts @@ -67,6 +67,10 @@ import type { CredentialsUpdateOutput, CredentialsRemoveInput, CredentialsRemoveOutput, + ProjectCurrentInput, + ProjectCurrentOutput, + ProjectDirectoriesInput, + ProjectDirectoriesOutput, PermissionsListRequestsInput, PermissionsListRequestsOutput, PermissionsListSavedInput, @@ -704,6 +708,32 @@ export function make(options: ClientOptions) { requestOptions, ), }, + project: { + current: (input?: ProjectCurrentInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/project/current`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + directories: (input: ProjectDirectoriesInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/project/${encodeURIComponent(input.projectID)}/directories`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, permissions: { listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) => request( diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index cffc6fac5c5..50aa2221177 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -2339,6 +2339,23 @@ export type CredentialsRemoveInput = { export type CredentialsRemoveOutput = void +export type ProjectCurrentInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProjectCurrentOutput = { readonly id: string; readonly directory: string } + +export type ProjectDirectoriesInput = { + readonly projectID: { readonly projectID: string }["projectID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ProjectDirectoriesOutput = ReadonlyArray<{ readonly directory: string; readonly strategy?: string }> + export type PermissionsListRequestsInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined diff --git a/packages/client/test/contract-identity.test.ts b/packages/client/test/contract-identity.test.ts index 64a2e958ce2..dc75a8e6503 100644 --- a/packages/client/test/contract-identity.test.ts +++ b/packages/client/test/contract-identity.test.ts @@ -3,6 +3,7 @@ import { Schema } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" import { Location as CoreLocation } from "@opencode-ai/core/location" import { ModelV2 } from "@opencode-ai/core/model" +import { ProjectV2 } from "@opencode-ai/core/project" import { SessionV2 } from "@opencode-ai/core/session" import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/input" import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message" @@ -26,10 +27,14 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () => expect(CoreLocation.Ref).toBe(Location.Ref) expect(ModelV2.Ref).toBe(Model.Ref) expect(SessionV2.Info).toBe(Session.Info) + expect(ProjectV2.Current).toBe(Project.Current) + expect(ProjectV2.Directory).toBe(Project.Directory) + expect(ProjectV2.Directories).toBe(Project.Directories) expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted) expect(CoreSessionMessage.Message).toBe(SessionMessage.Message) expect(CorePrompt).toBe(Prompt) expect(Api.groups["server.session"].identifier).toBe("server.session") + expect(Api.groups["server.project"].identifier).toBe("server.project") expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups)) expect(Session.ID.create()).toStartWith("ses_") expect(Project.ID.global).toBe("global") diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index dfc88909ce9..dba04e6706c 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -15,12 +15,14 @@ test("exposes every standard HTTP API group", () => { "providers", "integrations", "credentials", + "project", "permissions", "files", "commands", "skills", "events", "ptys", + "server.shell", "questions", "references", "projectCopies", @@ -37,6 +39,33 @@ test("exposes every standard HTTP API group", () => { ]) expect(Object.keys(client.files)).toEqual(["list", "find"]) expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"]) + expect(Object.keys(client.project)).toEqual(["current", "directories"]) +}) + +test("project methods use the public HTTP contract", async () => { + const requests: string[] = [] + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url + requests.push(url) + if (url.includes("/directories")) return Response.json([]) + return Response.json({ id: "proj_test", directory: "/tmp/project" }) + }, + }) + + const current = await client.project.current({ location: { workspace: "wrk_test" } }) + const directories = await client.project.directories({ + projectID: current.id, + location: { directory: current.directory }, + }) + + expect(current).toEqual({ id: "proj_test", directory: "/tmp/project" }) + expect(directories).toEqual([]) + expect(requests).toEqual([ + "http://localhost:3000/api/project/current?location%5Bworkspace%5D=wrk_test", + "http://localhost:3000/api/project/proj_test/directories?location%5Bdirectory%5D=%2Ftmp%2Fproject", + ]) }) test("sessions.get returns the wire projection", async () => { diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index a0ae0228c7f..cdc10ff3c81 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -20,6 +20,7 @@ import { PermissionV2 } from "./permission" import { PluginV2 } from "./plugin" import { PluginInternal } from "./plugin/internal" import { Policy } from "./policy" +import { Project } from "./project" import { ProjectCopy } from "./project/copy" import { Pty } from "./pty" import { QuestionV2 } from "./question" @@ -43,6 +44,7 @@ import { ToolOutputStore } from "./tool-output-store" export { LocationServiceMap } from "./location-service-map" export const locationServices = LayerNode.group([ + Project.node, Location.node, Policy.node, Config.node, diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index 5804ff93ffb..0ed2efa8668 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -17,14 +17,20 @@ export type ID = ProjectSchema.ID export const Vcs = ProjectSchema.Vcs export type Vcs = ProjectSchema.Vcs +export const Current = ProjectSchema.Current +export type Current = ProjectSchema.Current + +export const Directory = ProjectSchema.Directory +export type Directory = ProjectSchema.Directory + export class Info extends Schema.Class("Project.Info")({ id: ID, }) {} -export const DirectoriesInput = ProjectDirectories.ListInput +export const DirectoriesInput = ProjectSchema.DirectoriesInput export type DirectoriesInput = typeof DirectoriesInput.Type -export const Directories = ProjectDirectories.ListOutput +export const Directories = ProjectSchema.Directories export type Directories = typeof Directories.Type export interface Resolved { diff --git a/packages/core/src/project/directories.ts b/packages/core/src/project/directories.ts index 6f7e36c35e3..7fffb900bcf 100644 --- a/packages/core/src/project/directories.ts +++ b/packages/core/src/project/directories.ts @@ -4,15 +4,13 @@ import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm" import { Context, Effect, Layer, Schema } from "effect" import { Database } from "../database/database" import { makeGlobalNode } from "../effect/app-node" -import { AbsolutePath, optional } from "../schema" +import { AbsolutePath } from "../schema" import { ProjectSchema } from "./schema" import { ProjectDirectoryTable } from "./sql" import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" +import type { Project } from "../project" -export interface Directory { - readonly directory: AbsolutePath - readonly strategy?: string -} +export type Directory = Project.Directory export const CreateInput = Schema.Struct({ projectID: ProjectSchema.ID, @@ -31,17 +29,10 @@ export type RemoveInput = typeof RemoveInput.Type type DatabaseClient = EffectDrizzleSqlite.EffectSQLiteDatabase export type Transaction = Parameters[0]>[0] -export const ListInput = Schema.Struct({ - projectID: ProjectSchema.ID, -}).annotate({ identifier: "Project.DirectoriesInput" }) +export const ListInput = ProjectSchema.DirectoriesInput export type ListInput = typeof ListInput.Type -export const ListOutput = Schema.Array( - Schema.Struct({ - directory: AbsolutePath, - strategy: optional(Schema.String), - }), -).annotate({ identifier: "Project.Directories" }) +export const ListOutput = ProjectSchema.Directories export type ListOutput = typeof ListOutput.Type export interface Interface { diff --git a/packages/core/src/project/schema.ts b/packages/core/src/project/schema.ts index eed359abad2..5866551e4e4 100644 --- a/packages/core/src/project/schema.ts +++ b/packages/core/src/project/schema.ts @@ -7,6 +7,18 @@ import { AbsolutePath } from "../schema" export const ID = Project.ID export type ID = typeof ID.Type +export const Current = Project.Current +export type Current = typeof Current.Type + +export const Directory = Project.Directory +export type Directory = typeof Directory.Type + +export const DirectoriesInput = Project.DirectoriesInput +export type DirectoriesInput = typeof DirectoriesInput.Type + +export const Directories = Project.Directories +export type Directories = typeof Directories.Type + export const Vcs = Schema.Union([ Schema.Struct({ type: Schema.Literal("git"), diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 5c4f21ef1ff..ad6257a4929 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -121,12 +121,12 @@ describe("LocationServiceMap", () => { expect(blockedState.tools.map((tool) => tool.name).sort()).toEqual([ "application_context", "apply_patch", - "bash", "edit", "glob", "grep", "question", "read", + "shell", "skill", "todowrite", "webfetch", @@ -138,12 +138,12 @@ describe("LocationServiceMap", () => { expect(allowedState.tools.map((tool) => tool.name).sort()).toEqual([ "application_context", "apply_patch", - "bash", "edit", "glob", "grep", "question", "read", + "shell", "skill", "todowrite", "webfetch", diff --git a/packages/core/test/project-directories.test.ts b/packages/core/test/project-directories.test.ts index d91683cf422..d070376b68e 100644 --- a/packages/core/test/project-directories.test.ts +++ b/packages/core/test/project-directories.test.ts @@ -44,6 +44,14 @@ describe("ProjectDirectories", () => { }), ) + it.effect("returns an empty list for missing projects", () => + Effect.gen(function* () { + const service = yield* ProjectDirectories.Service + + expect(yield* service.list(Project.ID.make("missing-project"))).toEqual([]) + }), + ) + it.effect("replaces the strategy when requested", () => Effect.gen(function* () { yield* setup() diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index 959024cd4a9..e7556a8b7c4 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/packages/core/test/shared-schema.test.ts @@ -134,6 +134,10 @@ test("Core reuses the canonical shared schemas", async () => { [corePty.Info, Pty.Info], [corePty.Event, Pty.Event], [coreProject.ID, Project.ID], + [coreProject.Current, Project.Current], + [coreProject.Directory, Project.Directory], + [coreProject.DirectoriesInput, Project.DirectoriesInput], + [coreProject.Directories, Project.Directories], [coreReference.LocalSource, Reference.LocalSource], [coreReference.GitSource, Reference.GitSource], [coreReference.Source, Reference.Source], diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index a97d31dd111..8f760aa1239 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -88,6 +88,18 @@ const call = (input: typeof ShellTool.Input.Type, id = "call-shell") => ({ call: { type: "tool-call" as const, id, name: "shell", input }, }) +const isWindows = process.platform === "win32" +const cwdCommand = isWindows ? "(Get-Location).Path; Start-Sleep -Milliseconds 100" : "pwd" +const helloCommand = isWindows ? "[Console]::Out.Write('hello'); Start-Sleep -Milliseconds 100" : "printf hello" +const idleCommand = isWindows ? "Start-Sleep -Seconds 60" : "sleep 60" +const bodyExitCommand = isWindows + ? "[Console]::Out.Write('body'); Start-Sleep -Milliseconds 100; exit 7" + : "printf body && exit 7" +const overflowCommand = (bytes: number) => + isWindows + ? `[Console]::Out.Write(('x' * ${bytes})); Start-Sleep -Milliseconds 100` + : `head -c ${bytes} /dev/zero | tr '\\0' 'x'` + const it = testEffect(Layer.empty) describe("ShellTool", () => { @@ -103,14 +115,14 @@ describe("ShellTool", () => { expect(definitions[0]?.outputSchema).not.toHaveProperty("properties.output") expect(yield* toolDefinitions(registry, [{ action: "shell", resource: "*", effect: "deny" }])).toEqual([]) - const settled = yield* settleTool(registry, call({ command: "printf hello" })) + const settled = yield* settleTool(registry, call({ command: helloCommand })) expect(settled.output?.structured).toMatchObject({ exit: 0, truncated: false }) expect(settled.output?.content[0]).toEqual({ type: "text", text: "hello" }) expect(settled.output?.content[1]).toMatchObject({ type: "text", text: expect.stringContaining("Command exited with code 0."), }) - expect(assertions).toMatchObject([{ sessionID, action: "shell", resources: ["printf hello"] }]) + expect(assertions).toMatchObject([{ sessionID, action: "shell", resources: [helloCommand] }]) }), ) }, @@ -128,7 +140,9 @@ describe("ShellTool", () => { reset() return Effect.promise(() => fs.mkdir(path.join(tmp.path, "src"))).pipe( Effect.andThen( - withTool(data.path, tmp.path, (registry) => settleTool(registry, call({ command: "pwd", workdir: "src" }))), + withTool(data.path, tmp.path, (registry) => + settleTool(registry, call({ command: cwdCommand, workdir: "src" })), + ), ), Effect.andThen((settled) => Effect.sync(() => @@ -163,7 +177,7 @@ describe("ShellTool", () => { return Effect.promise(() => fs.mkdir(workdir)).pipe( Effect.andThen( withTool(data.path, tmp.path, (registry) => - executeTool(registry, call({ command: "pwd", workdir: "src" })), + executeTool(registry, call({ command: cwdCommand, workdir: "src" })), ), ), Effect.andThen(Effect.sync(() => expect(assertions.map((input) => input.action)).toEqual(["shell"]))), @@ -182,7 +196,7 @@ describe("ShellTool", () => { ([data, active, outside]) => { reset() return withTool(data.path, active.path, (registry) => - executeTool(registry, call({ command: "pwd", workdir: outside.path })), + executeTool(registry, call({ command: cwdCommand, workdir: outside.path })), ).pipe( Effect.andThen( Effect.sync(() => { @@ -213,13 +227,13 @@ describe("ShellTool", () => { reset() denyAction = "external_directory" yield* withTool(data.path, active.path, (registry) => - executeTool(registry, call({ command: "pwd", workdir: outside.path })), + executeTool(registry, call({ command: cwdCommand, workdir: outside.path })), ) expect(assertions.map((item) => item.action)).toEqual(["external_directory"]) reset() denyAction = "shell" - yield* withTool(data.path, active.path, (registry) => executeTool(registry, call({ command: "pwd" }))) + yield* withTool(data.path, active.path, (registry) => executeTool(registry, call({ command: cwdCommand }))) expect(assertions.map((item) => item.action)).toEqual(["shell"]) }), ([data, active, outside]) => @@ -272,7 +286,7 @@ describe("ShellTool", () => { ([data, tmp]) => { reset() return withTool(data.path, tmp.path, (registry) => - settleTool(registry, call({ command: "printf body && exit 7" }, "call-nonzero")), + settleTool(registry, call({ command: bodyExitCommand }, "call-nonzero")), ).pipe( Effect.andThen((settled) => Effect.sync(() => { @@ -300,7 +314,7 @@ describe("ShellTool", () => { reset() const bytes = ShellTool.MAX_CAPTURE_BYTES + 1024 return withTool(data.path, tmp.path, (registry) => - settleTool(registry, call({ command: `head -c ${bytes} /dev/zero | tr '\\0' 'x'` }, "call-overflow")), + settleTool(registry, call({ command: overflowCommand(bytes) }, "call-overflow")), ).pipe( Effect.andThen((settled) => Effect.sync(() => { @@ -326,7 +340,7 @@ describe("ShellTool", () => { ([data, tmp]) => { reset() return withTool(data.path, tmp.path, (registry) => - settleTool(registry, call({ command: "sleep 60", timeout: 50 })), + settleTool(registry, call({ command: idleCommand, timeout: 50 })), ).pipe( Effect.andThen((settled) => Effect.sync(() => { diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index e9e8e8e18dc..e879c99d7a1 100644 --- a/packages/protocol/src/api.ts +++ b/packages/protocol/src/api.ts @@ -22,6 +22,7 @@ import { Authorization } from "./middleware/authorization" import { LocationGroup } from "./groups/location" import { IntegrationGroup } from "./groups/integration" import { CredentialGroup } from "./groups/credential" +import { ProjectGroup } from "./groups/project" import { ProjectCopyGroup } from "./groups/project-copy" // Protocol owns middleware placement, while Server injects concrete keys so Core service identities stay downstream. @@ -47,6 +48,7 @@ const makeApiFromGroup = < .add(ProviderGroup.middleware(locationMiddleware)) .add(IntegrationGroup.middleware(locationMiddleware)) .add(CredentialGroup.middleware(locationMiddleware)) + .add(ProjectGroup.middleware(locationMiddleware)) .add(makePermissionGroup(locationMiddleware, sessionLocationMiddleware)) .add(FileSystemGroup.middleware(locationMiddleware)) .add(CommandGroup.middleware(locationMiddleware)) diff --git a/packages/protocol/src/groups/project.ts b/packages/protocol/src/groups/project.ts new file mode 100644 index 00000000000..7fbf7ac94d5 --- /dev/null +++ b/packages/protocol/src/groups/project.ts @@ -0,0 +1,42 @@ +import { Project } from "@opencode-ai/schema/project" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { LocationQuery, locationQueryOpenApi } from "./location" + +const root = "/api/project" + +export const ProjectGroup = HttpApiGroup.make("server.project") + .add( + HttpApiEndpoint.get("project.current", `${root}/current`, { + query: LocationQuery, + success: Project.Current, + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.project.current", + summary: "Get current project", + description: "Resolve the project for the requested location.", + }), + ), + ) + .add( + HttpApiEndpoint.get("project.directories", `${root}/:projectID/directories`, { + params: { projectID: Project.ID }, + query: LocationQuery, + success: Project.Directories, + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.project.directories", + summary: "List project directories", + description: "List known local absolute directories for a project.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "projects", + description: "Location-scoped project routes.", + }), + ) diff --git a/packages/schema/src/project.ts b/packages/schema/src/project.ts index 7e73530a6d5..e854482de33 100644 --- a/packages/schema/src/project.ts +++ b/packages/schema/src/project.ts @@ -2,13 +2,29 @@ export * as Project from "./project" import { Schema } from "effect" import { define, inventory } from "./event" -import { NonNegativeInt, optional } from "./schema" +import { AbsolutePath, NonNegativeInt, optional } from "./schema" import { ProjectID } from "./project-id" export const ID = ProjectID export type ID = typeof ID.Type export const Vcs = Schema.Literal("git").annotate({ identifier: "Project.Vcs" }) +export const Current = Schema.Struct({ + id: ID, + directory: AbsolutePath, +}).annotate({ identifier: "Project.Current" }) +export interface Current extends Schema.Schema.Type {} +export const Directory = Schema.Struct({ + directory: AbsolutePath, + strategy: optional(Schema.String), +}).annotate({ identifier: "Project.Directory" }) +export interface Directory extends Schema.Schema.Type {} +export const DirectoriesInput = Schema.Struct({ + projectID: ID, +}).annotate({ identifier: "Project.DirectoriesInput" }) +export interface DirectoriesInput extends Schema.Schema.Type {} +export const Directories = Schema.Array(Directory).annotate({ identifier: "Project.Directories" }) +export type Directories = typeof Directories.Type export const Icon = Schema.Struct({ url: optional(Schema.String), override: optional(Schema.String), diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index cf83dbb2889..e9455c350b9 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -7,7 +7,6 @@ import { Project } from "../src/project" import { Pty } from "../src/pty" import { Question } from "../src/question" import { Session } from "../src/session" -import { SessionEvent } from "../src/session-event" import { SessionTodo } from "../src/session-todo" import { optional } from "../src/schema" @@ -41,6 +40,10 @@ describe("contract hygiene", () => { Model.Capabilities, Model.Cost, Model.Api, + Project.Current, + Project.Directory, + Project.DirectoriesInput, + Project.Directories, Project.Icon, Project.Commands, Project.Time, diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index fbf11b5d792..b7269495fd1 100644 --- a/packages/server/src/handlers.ts +++ b/packages/server/src/handlers.ts @@ -18,6 +18,7 @@ import { ReferenceHandler } from "./handlers/reference" import { LocationHandler } from "./handlers/location" import { IntegrationHandler } from "./handlers/integration" import { CredentialHandler } from "./handlers/credential" +import { ProjectHandler } from "./handlers/project" import { ProjectCopyHandler } from "./handlers/project-copy" export const handlers = Layer.mergeAll( @@ -31,6 +32,7 @@ export const handlers = Layer.mergeAll( ProviderHandler, IntegrationHandler, CredentialHandler, + ProjectHandler, PermissionHandler, FileSystemHandler, CommandHandler, diff --git a/packages/server/src/handlers/project.ts b/packages/server/src/handlers/project.ts new file mode 100644 index 00000000000..c35b019b419 --- /dev/null +++ b/packages/server/src/handlers/project.ts @@ -0,0 +1,17 @@ +import { Location } from "@opencode-ai/core/location" +import { Project } from "@opencode-ai/core/project" +import { Effect } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { Api } from "../api" + +export const ProjectHandler = HttpApiBuilder.group(Api, "server.project", (handlers) => + handlers + .handle("project.current", () => + Location.Service.use((location) => + Effect.succeed({ id: location.project.id, directory: location.project.directory }), + ), + ) + .handle("project.directories", (ctx) => + Project.Service.use((project) => project.directories({ projectID: ctx.params.projectID })), + ), +) diff --git a/packages/tui/src/component/dialog-move-session.tsx b/packages/tui/src/component/dialog-move-session.tsx index 200c18d97af..e4f29686d5b 100644 --- a/packages/tui/src/component/dialog-move-session.tsx +++ b/packages/tui/src/component/dialog-move-session.tsx @@ -17,18 +17,18 @@ import { useCommandShortcut } from "../keymap" import { useProject } from "../context/project" import { Spinner } from "./spinner" import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes" -import type { ProjectDirectories } from "@opencode-ai/sdk/v2" +import type { ProjectDirectoriesOutput } from "@opencode-ai/client" import { useRoute } from "../context/route" export type MoveSessionSelection = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new" } -type ProjectDirectory = ProjectDirectories[number] +type ProjectDirectory = ProjectDirectoriesOutput[number] type DialogMoveSessionProps = { projectID: string current?: MoveSessionSelection onSelect: (selection: MoveSessionSelection) => void onCurrentChange?: (selection: MoveSessionSelection) => void - initialDirectories?: ProjectDirectory[] + initialDirectories?: ReadonlyArray initialRemoving?: string } @@ -58,12 +58,13 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { // A failed current-checkout lookup only affects which row is highlighted, so // swallow it and let the directory list render without a current marker. + // Once the current project is known, a mismatch is a guaranteed miss. const [loadedProject] = createResource( - () => (projectContext.project() === props.projectID ? undefined : props.projectID), + () => (projectContext.project() === undefined ? props.projectID : undefined), (projectID) => - sdk.client.project - .current({}, { throwOnError: true }) - .then((result) => (result.data?.id === projectID ? result.data.worktree : undefined)) + sdk.api.project + .current({ location: { directory: projectContext.instance.directory() || paths.cwd } }) + .then((project) => (project.id === projectID ? project.directory : undefined)) .catch(() => undefined), ) const currentCheckout = createMemo(() => { @@ -73,15 +74,19 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { const [directories, { refetch }] = createResource( () => (props.initialRemoving ? undefined : props.projectID), - async (projectID, info): Promise => { + async (projectID, info): Promise | undefined> => { try { + const location = { directory: projectContext.instance.directory() || paths.cwd } await sdk.api.projectCopies.refresh({ projectID, - location: { directory: projectContext.instance.directory() || paths.cwd }, + location, + }) + const directories = await sdk.api.project.directories({ + projectID, + location, }) - const directories = await sdk.client.project.directories({ projectID }, { throwOnError: true }) setLoadError(undefined) - return directories.data ?? [] + return directories } catch (error) { setLoadError(error) // An initial load with no data surfaces the inline error view below. A diff --git a/packages/tui/src/context/project.tsx b/packages/tui/src/context/project.tsx index a6130783064..97d51253928 100644 --- a/packages/tui/src/context/project.tsx +++ b/packages/tui/src/context/project.tsx @@ -37,18 +37,17 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex async function sync() { const workspace = store.workspace.current + const location = { workspace } const [instancePath, project] = await Promise.all([ sdk.client.path.get({ workspace }), - sdk.client.project.current({ workspace }), + sdk.api.project.current({ location }), ]) - const directories = project.data?.id - ? await sdk.client.project.directories({ projectID: project.data.id, workspace }) - : undefined + const directories = await sdk.api.project.directories({ projectID: project.id, location }) batch(() => { setStore("instance", "path", reconcile(instancePath.data || defaultPath)) - setStore("project", "id", project.data?.id) - setStore("project", "worktree", project.data?.worktree) - setStore("project", "mainDir", directories?.data?.findLast((item) => item.strategy === undefined)?.directory) + setStore("project", "id", project.id) + setStore("project", "worktree", project.directory) + setStore("project", "mainDir", directories.findLast((item) => item.strategy === undefined)?.directory) }) } diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-sdk.ts index 8cb704d626b..079617db6f7 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-sdk.ts @@ -93,6 +93,9 @@ export function createFetch(override?: FetchHandler, events?: ReturnType