feat(client): expose v2 project APIs (#34456)
This commit is contained in:
parent
01edae4a7f
commit
935ac2db91
23 changed files with 473 additions and 244 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Info>("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 {
|
||||
|
|
|
|||
|
|
@ -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<Parameters<DatabaseClient["transaction"]>[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 {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue