sync
This commit is contained in:
@@ -1,151 +0,0 @@
|
||||
import "zod-openapi/extend"
|
||||
import { Log } from "../util/log"
|
||||
import { Context } from "../util/context"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Global } from "../global"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { z } from "zod"
|
||||
import { Project } from "../project/project"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export namespace App {
|
||||
const log = Log.create({ service: "app" })
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
hostname: z.string(),
|
||||
git: z.boolean(),
|
||||
path: z.object({
|
||||
home: z.string(),
|
||||
config: z.string(),
|
||||
data: z.string(),
|
||||
root: z.string(),
|
||||
cwd: z.string(),
|
||||
state: z.string(),
|
||||
}),
|
||||
time: z.object({
|
||||
initialized: z.number().optional(),
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "App",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
const ctx = Context.create<{
|
||||
info: Info
|
||||
services: Map<any, { state: any; shutdown?: (input: any) => Promise<void> }>
|
||||
}>("app")
|
||||
|
||||
export const use = ctx.use
|
||||
|
||||
const APP_JSON = "app.json"
|
||||
|
||||
export type Input = {
|
||||
cwd: string
|
||||
}
|
||||
|
||||
export const provideExisting = ctx.provide
|
||||
export async function provide<T>(input: Input, cb: (app: App.Info) => Promise<T>) {
|
||||
log.info("creating", {
|
||||
cwd: input.cwd,
|
||||
})
|
||||
const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => (x ? path.dirname(x) : undefined))
|
||||
log.info("git", { git })
|
||||
|
||||
const data = path.join(Global.Path.data, "project", git ? directory(git) : "global")
|
||||
const stateFile = Bun.file(path.join(data, APP_JSON))
|
||||
const state = (await stateFile.json().catch(() => ({}))) as {
|
||||
initialized: number
|
||||
}
|
||||
await stateFile.write(JSON.stringify(state))
|
||||
|
||||
const services = new Map<
|
||||
any,
|
||||
{
|
||||
state: any
|
||||
shutdown?: (input: any) => Promise<void>
|
||||
}
|
||||
>()
|
||||
|
||||
const root = git ?? input.cwd
|
||||
|
||||
const info: Info = {
|
||||
hostname: os.hostname(),
|
||||
time: {
|
||||
initialized: state.initialized,
|
||||
},
|
||||
git: git !== undefined,
|
||||
path: {
|
||||
home: os.homedir(),
|
||||
config: Global.Path.config,
|
||||
state: Global.Path.state,
|
||||
data,
|
||||
root,
|
||||
cwd: input.cwd,
|
||||
},
|
||||
}
|
||||
const app = {
|
||||
services,
|
||||
info,
|
||||
}
|
||||
|
||||
const projects = await Project.list()
|
||||
const project = projects
|
||||
.toSorted((a, b) => a.worktree.length - b.worktree.length)
|
||||
.find((x) => Filesystem.contains(x.worktree, input.cwd))
|
||||
|
||||
if (!project) {
|
||||
throw new Error("No project found")
|
||||
}
|
||||
|
||||
return ctx.provide(app, async () => {
|
||||
return Project.provide(project, async () => {
|
||||
const result = await Instance.provide(
|
||||
{
|
||||
worktree: app.info.path.root,
|
||||
directory: app.info.path.cwd,
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
const result = await cb(app.info)
|
||||
return result
|
||||
} finally {
|
||||
for (const [key, entry] of app.services.entries()) {
|
||||
if (!entry.shutdown) continue
|
||||
log.info("shutdown", { name: key })
|
||||
await entry.shutdown?.(await entry.state)
|
||||
}
|
||||
await Instance.dispose()
|
||||
}
|
||||
},
|
||||
)
|
||||
return result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function info() {
|
||||
return ctx.use().info
|
||||
}
|
||||
|
||||
export async function initialize() {
|
||||
const { info } = ctx.use()
|
||||
info.time.initialized = Date.now()
|
||||
await Bun.write(
|
||||
path.join(info.path.data, APP_JSON),
|
||||
JSON.stringify({
|
||||
initialized: Date.now(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function directory(input: string): string {
|
||||
return input
|
||||
.split(path.sep)
|
||||
.filter(Boolean)
|
||||
.join("-")
|
||||
.replace(/[^A-Za-z0-9_]/g, "-")
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { App } from "../app/app"
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { Plugin } from "../plugin"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Share } from "../share/share"
|
||||
import { Snapshot } from "../snapshot"
|
||||
|
||||
export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
|
||||
return App.provide(input, async (app) => {
|
||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||
return Instance.provide(directory, async () => {
|
||||
await Plugin.init()
|
||||
Share.init()
|
||||
Format.init()
|
||||
LSP.init()
|
||||
Snapshot.init()
|
||||
return cb(app)
|
||||
return cb()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,25 +5,26 @@ import { Global } from "../../global"
|
||||
import { Agent } from "../../agent/agent"
|
||||
import path from "path"
|
||||
import matter from "gray-matter"
|
||||
import { App } from "../../app/app"
|
||||
import { Instance } from "../../project/instance"
|
||||
|
||||
const AgentCreateCommand = cmd({
|
||||
command: "create",
|
||||
describe: "create a new agent",
|
||||
async handler() {
|
||||
await App.provide({ cwd: process.cwd() }, async (app) => {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
UI.empty()
|
||||
prompts.intro("Create agent")
|
||||
const project = Instance.project
|
||||
|
||||
let scope: "global" | "project" = "global"
|
||||
if (app.git) {
|
||||
if (project.vcs === "git") {
|
||||
const scopeResult = await prompts.select({
|
||||
message: "Location",
|
||||
options: [
|
||||
{
|
||||
label: "Current project",
|
||||
value: "project" as const,
|
||||
hint: app.path.root,
|
||||
hint: Instance.worktree,
|
||||
},
|
||||
{
|
||||
label: "Global",
|
||||
@@ -116,7 +117,7 @@ const AgentCreateCommand = cmd({
|
||||
|
||||
const content = matter.stringify(generated.systemPrompt, frontmatter)
|
||||
const filePath = path.join(
|
||||
scope === "global" ? Global.Path.config : path.join(app.path.root, ".opencode"),
|
||||
scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
|
||||
`agent`,
|
||||
`${generated.identifier}.md`,
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import path from "path"
|
||||
import os from "os"
|
||||
import { Global } from "../../global"
|
||||
import { Plugin } from "../../plugin"
|
||||
import { App } from "../../app/app"
|
||||
import { Instance } from "../../project/instance"
|
||||
|
||||
export const AuthCommand = cmd({
|
||||
command: "auth",
|
||||
@@ -74,7 +74,7 @@ export const AuthLoginCommand = cmd({
|
||||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
UI.empty()
|
||||
prompts.intro("Add credential")
|
||||
if (args.url) {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { App } from "../../../app/app"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
const AppInfoCommand = cmd({
|
||||
command: "info",
|
||||
builder: (yargs) => yargs,
|
||||
async handler() {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const app = App.info()
|
||||
console.log(JSON.stringify(app, null, 2))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const AppCommand = cmd({
|
||||
command: "app",
|
||||
builder: (yargs) => yargs.command(AppInfoCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
@@ -11,7 +11,7 @@ const FileReadCommand = cmd({
|
||||
description: "File path to read",
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const content = await File.read(args.path)
|
||||
console.log(content)
|
||||
})
|
||||
@@ -22,7 +22,7 @@ const FileStatusCommand = cmd({
|
||||
command: "status",
|
||||
builder: (yargs) => yargs,
|
||||
async handler() {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const status = await File.status()
|
||||
console.log(JSON.stringify(status, null, 2))
|
||||
})
|
||||
@@ -38,7 +38,7 @@ const FileListCommand = cmd({
|
||||
description: "File path to list",
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const files = await File.list(args.path)
|
||||
console.log(JSON.stringify(files, null, 2))
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Global } from "../../../global"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
import { AppCommand } from "./app"
|
||||
import { FileCommand } from "./file"
|
||||
import { LSPCommand } from "./lsp"
|
||||
import { RipgrepCommand } from "./ripgrep"
|
||||
@@ -12,7 +11,6 @@ export const DebugCommand = cmd({
|
||||
command: "debug",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(AppCommand)
|
||||
.command(LSPCommand)
|
||||
.command(RipgrepCommand)
|
||||
.command(FileCommand)
|
||||
@@ -22,7 +20,7 @@ export const DebugCommand = cmd({
|
||||
.command({
|
||||
command: "wait",
|
||||
async handler() {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24))
|
||||
})
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ const DiagnosticsCommand = cmd({
|
||||
command: "diagnostics <file>",
|
||||
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
await LSP.touchFile(args.file, true)
|
||||
console.log(JSON.stringify(await LSP.diagnostics(), null, 2))
|
||||
})
|
||||
@@ -25,7 +25,7 @@ export const SymbolsCommand = cmd({
|
||||
command: "symbols <query>",
|
||||
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
using _ = Log.Default.time("symbols")
|
||||
const results = await LSP.workspaceSymbol(args.query)
|
||||
console.log(JSON.stringify(results, null, 2))
|
||||
@@ -37,7 +37,7 @@ export const DocumentSymbolsCommand = cmd({
|
||||
command: "document-symbols <uri>",
|
||||
builder: (yargs) => yargs.positional("uri", { type: "string", demandOption: true }),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
using _ = Log.Default.time("document-symbols")
|
||||
const results = await LSP.documentSymbol(args.uri)
|
||||
console.log(JSON.stringify(results, null, 2))
|
||||
|
||||
@@ -16,7 +16,7 @@ const TreeCommand = cmd({
|
||||
type: "number",
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
console.log(await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit }))
|
||||
})
|
||||
},
|
||||
@@ -39,7 +39,7 @@ const FilesCommand = cmd({
|
||||
description: "Limit number of results",
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const files = await Ripgrep.files({
|
||||
cwd: Instance.directory,
|
||||
query: args.query,
|
||||
|
||||
@@ -11,7 +11,7 @@ export const SnapshotCommand = cmd({
|
||||
const TrackCommand = cmd({
|
||||
command: "track",
|
||||
async handler() {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
console.log(await Snapshot.track())
|
||||
})
|
||||
},
|
||||
@@ -26,7 +26,7 @@ const PatchCommand = cmd({
|
||||
demandOption: true,
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
console.log(await Snapshot.patch(args.hash))
|
||||
})
|
||||
},
|
||||
@@ -41,7 +41,7 @@ const DiffCommand = cmd({
|
||||
demandOption: true,
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
console.log(await Snapshot.diff(args.hash))
|
||||
})
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ExportCommand = cmd({
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
let sessionID = args.sessionID
|
||||
|
||||
if (!sessionID) {
|
||||
|
||||
@@ -6,8 +6,7 @@ import { map, pipe, sortBy, values } from "remeda"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { App } from "../../app/app"
|
||||
import { Project } from "../../project/project"
|
||||
import { Instance } from "../../project/instance"
|
||||
|
||||
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
|
||||
|
||||
@@ -22,7 +21,7 @@ export const GithubInstallCommand = cmd({
|
||||
command: "install",
|
||||
describe: "install the GitHub agent",
|
||||
async handler() {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
UI.empty()
|
||||
prompts.intro("Install GitHub agent")
|
||||
const app = await getAppInfo()
|
||||
@@ -64,8 +63,7 @@ export const GithubInstallCommand = cmd({
|
||||
}
|
||||
|
||||
async function getAppInfo() {
|
||||
const app = App.info()
|
||||
const project = Project.use()
|
||||
const project = Instance.project
|
||||
if (project.vcs !== "git") {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
throw new UI.CancelledError()
|
||||
@@ -90,7 +88,7 @@ export const GithubInstallCommand = cmd({
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
const [, owner, repo] = parsed
|
||||
return { owner, repo, root: app.path.root }
|
||||
return { owner, repo, root: Instance.worktree }
|
||||
}
|
||||
|
||||
async function promptProvider() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App } from "../../app/app"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { cmd } from "./cmd"
|
||||
|
||||
@@ -6,7 +6,7 @@ export const ModelsCommand = cmd({
|
||||
command: "models",
|
||||
describe: "list all available models",
|
||||
handler: async () => {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
await Instance.provide(process.cwd(), async () => {
|
||||
const providers = await Provider.list()
|
||||
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
|
||||
@@ -69,7 +69,7 @@ export const RunCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const session = await (async () => {
|
||||
if (args.continue) {
|
||||
const it = Session.list()
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Server } from "../../server/server"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { cmd } from "./cmd"
|
||||
|
||||
export const ServeCommand = cmd({
|
||||
@@ -21,26 +19,14 @@ export const ServeCommand = cmd({
|
||||
}),
|
||||
describe: "starts a headless opencode server",
|
||||
handler: async (args) => {
|
||||
const cwd = process.cwd()
|
||||
await bootstrap({ cwd }, async () => {
|
||||
const providers = await Provider.list()
|
||||
if (Object.keys(providers).length === 0) {
|
||||
return "needs_provider"
|
||||
}
|
||||
|
||||
const hostname = args.hostname
|
||||
const port = args.port
|
||||
|
||||
const server = Server.listen({
|
||||
port,
|
||||
hostname,
|
||||
})
|
||||
|
||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||
|
||||
await new Promise(() => {})
|
||||
|
||||
server.stop()
|
||||
const hostname = args.hostname
|
||||
const port = args.port
|
||||
const server = Server.listen({
|
||||
port,
|
||||
hostname,
|
||||
})
|
||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||
await new Promise(() => {})
|
||||
server.stop()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -79,7 +79,7 @@ export const TuiCommand = cmd({
|
||||
UI.error("Failed to change directory to " + cwd)
|
||||
return
|
||||
}
|
||||
const result = await bootstrap({ cwd }, async (app) => {
|
||||
const result = await bootstrap(cwd, async () => {
|
||||
const sessionID = await (async () => {
|
||||
if (args.continue) {
|
||||
const it = Session.list()
|
||||
@@ -146,7 +146,6 @@ export const TuiCommand = cmd({
|
||||
...process.env,
|
||||
CGO_ENABLED: "0",
|
||||
OPENCODE_SERVER: server.url.toString(),
|
||||
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||
},
|
||||
onExit: () => {
|
||||
server.stop()
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Bus } from "../bus"
|
||||
import { $ } from "bun"
|
||||
import { createPatch } from "diff"
|
||||
import path from "path"
|
||||
import { App } from "../app/app"
|
||||
import fs from "fs"
|
||||
import ignore from "ignore"
|
||||
import { Log } from "../util/log"
|
||||
@@ -48,7 +47,7 @@ export namespace File {
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
const project = Project.use()
|
||||
const project = Instance.project
|
||||
if (project.vcs !== "git") return []
|
||||
|
||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
@@ -119,7 +118,7 @@ export namespace File {
|
||||
|
||||
export async function read(file: string) {
|
||||
using _ = log.time("read", { file })
|
||||
const project = Project.use()
|
||||
const project = Instance.project
|
||||
const full = path.join(Instance.directory, file)
|
||||
const content = await Bun.file(full)
|
||||
.text()
|
||||
@@ -141,22 +140,22 @@ export namespace File {
|
||||
|
||||
export async function list(dir?: string) {
|
||||
const exclude = [".git", ".DS_Store"]
|
||||
const app = App.info()
|
||||
const project = Instance.project
|
||||
let ignored = (_: string) => false
|
||||
if (app.git) {
|
||||
const gitignore = Bun.file(path.join(app.path.root, ".gitignore"))
|
||||
if (project.vcs === "git") {
|
||||
const gitignore = Bun.file(path.join(Instance.worktree, ".gitignore"))
|
||||
if (await gitignore.exists()) {
|
||||
const ig = ignore().add(await gitignore.text())
|
||||
ignored = ig.ignores.bind(ig)
|
||||
}
|
||||
}
|
||||
const resolved = dir ? path.join(app.path.cwd, dir) : app.path.cwd
|
||||
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory
|
||||
const nodes: Node[] = []
|
||||
for (const entry of await fs.promises.readdir(resolved, { withFileTypes: true })) {
|
||||
if (exclude.includes(entry.name)) continue
|
||||
const fullPath = path.join(resolved, entry.name)
|
||||
const relativePath = path.relative(app.path.cwd, fullPath)
|
||||
const relativeToRoot = path.relative(app.path.root, fullPath)
|
||||
const relativePath = path.relative(Instance.directory, fullPath)
|
||||
const relativeToRoot = path.relative(Instance.worktree, fullPath)
|
||||
const type = entry.isDirectory() ? "directory" : "file"
|
||||
nodes.push({
|
||||
name: entry.name,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { z } from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import fs from "fs"
|
||||
import { App } from "../app/app"
|
||||
import { Log } from "../util/log"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Instance } from "../project/instance"
|
||||
@@ -20,19 +19,14 @@ export namespace FileWatcher {
|
||||
}
|
||||
const state = Instance.state(
|
||||
() => {
|
||||
const app = App.use()
|
||||
if (!app.info.git) return {}
|
||||
if (Instance.project.vcs !== "git") return {}
|
||||
try {
|
||||
const watcher = fs.watch(app.info.path.cwd, { recursive: true }, (event, file) => {
|
||||
const watcher = fs.watch(Instance.directory, { recursive: true }, (event, file) => {
|
||||
log.info("change", { file, event })
|
||||
if (!file) return
|
||||
// for some reason async local storage is lost here
|
||||
// https://github.com/oven-sh/bun/issues/20754
|
||||
App.provideExisting(app, async () => {
|
||||
Bus.publish(Event.Updated, {
|
||||
file,
|
||||
event,
|
||||
})
|
||||
Bus.publish(Event.Updated, {
|
||||
file,
|
||||
event,
|
||||
})
|
||||
})
|
||||
return { watcher }
|
||||
|
||||
@@ -107,6 +107,7 @@ try {
|
||||
name: e.name,
|
||||
message: e.message,
|
||||
cause: e.cause?.toString(),
|
||||
stack: e.stack,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Server } from "../server/server"
|
||||
import { BunProc } from "../bun"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export namespace Plugin {
|
||||
const log = Log.create({ service: "plugin" })
|
||||
@@ -15,13 +14,15 @@ export namespace Plugin {
|
||||
const state = Instance.state(async () => {
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: "http://localhost:4096",
|
||||
fetch: async (...args) => Server.app().fetch(...args),
|
||||
fetch: async (...args) => Server.App.fetch(...args),
|
||||
})
|
||||
const config = await Config.get()
|
||||
const hooks = []
|
||||
const input = {
|
||||
client,
|
||||
app: App.info(),
|
||||
project: Instance.project,
|
||||
worktree: Instance.worktree,
|
||||
directory: Instance.directory,
|
||||
$: Bun.$,
|
||||
}
|
||||
const plugins = [...(config.plugin ?? [])]
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { Context } from "../util/context"
|
||||
import { Project } from "./project"
|
||||
import { State } from "./state"
|
||||
|
||||
const context = Context.create<{ directory: string; worktree: string }>("path")
|
||||
const context = Context.create<{ directory: string; worktree: string; project: Project.Info }>("path")
|
||||
|
||||
export const Instance = {
|
||||
provide: context.provide,
|
||||
async provide<R>(directory: string, cb: () => R): Promise<R> {
|
||||
const project = await Project.fromDirectory(directory)
|
||||
return context.provide({ directory, worktree: project.worktree, project }, cb)
|
||||
},
|
||||
get directory() {
|
||||
return context.use().directory
|
||||
},
|
||||
get worktree() {
|
||||
return context.use().worktree
|
||||
},
|
||||
get project() {
|
||||
return context.use().project
|
||||
},
|
||||
state<S>(init: () => S, dispose?: (state: Awaited<S>) => Promise<void>): () => S {
|
||||
return State.create(() => Instance.directory, init, dispose)
|
||||
},
|
||||
|
||||
@@ -1,83 +1,92 @@
|
||||
import z from "zod"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import path from "path"
|
||||
import { $ } from "bun"
|
||||
import { StorageNext } from "../storage/storage-next"
|
||||
import { Context } from "../util/context"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
export namespace Project {
|
||||
export const Info = z.object({
|
||||
id: z.string(),
|
||||
worktree: z.string(),
|
||||
vcs: z.literal("git").optional(),
|
||||
time: z.object({
|
||||
created: z.number(),
|
||||
initialized: z.number().optional(),
|
||||
}),
|
||||
})
|
||||
const log = Log.create({ service: "project" })
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
worktree: z.string(),
|
||||
vcs: z.literal("git").optional(),
|
||||
time: z.object({
|
||||
created: z.number(),
|
||||
initialized: z.number().optional(),
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Project",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
const context = Context.create<Info>("project")
|
||||
|
||||
export const use = context.use
|
||||
export const provide = context.provide
|
||||
|
||||
const init = lazy(async () => {
|
||||
const cwd = process.cwd()
|
||||
const matches = Filesystem.up({ targets: [".git"], start: cwd })
|
||||
const git = await matches.next().then((x) => x.value)
|
||||
await matches.return()
|
||||
if (!git) {
|
||||
await StorageNext.write<Info>(["project", "global"], {
|
||||
id: "global",
|
||||
worktree: "/",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
let worktree = path.dirname(git)
|
||||
const [id] = await $`git rev-list --max-parents=0 --all`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(worktree)
|
||||
.text()
|
||||
.then((x) =>
|
||||
x
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((x) => x.trim())
|
||||
.toSorted(),
|
||||
)
|
||||
worktree = path.dirname(
|
||||
await $`git rev-parse --path-format=absolute --git-common-dir`
|
||||
const cache = new Map<string, Info>()
|
||||
export async function fromDirectory(directory: string) {
|
||||
log.info("fromDirectory", { directory })
|
||||
const fn = async () => {
|
||||
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
||||
const git = await matches.next().then((x) => x.value)
|
||||
await matches.return()
|
||||
if (!git) {
|
||||
const project: Info = {
|
||||
id: "global",
|
||||
worktree: "/",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
await StorageNext.write<Info>(["project", "global"], project)
|
||||
return project
|
||||
}
|
||||
let worktree = path.dirname(git)
|
||||
const [id] = await $`git rev-list --max-parents=0 --all`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(worktree)
|
||||
.text()
|
||||
.then((x) => x.trim()),
|
||||
)
|
||||
await StorageNext.write<Info>(["project", id], {
|
||||
id,
|
||||
worktree,
|
||||
vcs: "git",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
})
|
||||
})
|
||||
.then((x) =>
|
||||
x
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((x) => x.trim())
|
||||
.toSorted(),
|
||||
)
|
||||
worktree = path.dirname(
|
||||
await $`git rev-parse --path-format=absolute --git-common-dir`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(worktree)
|
||||
.text()
|
||||
.then((x) => x.trim()),
|
||||
)
|
||||
const project: Info = {
|
||||
id,
|
||||
worktree,
|
||||
vcs: "git",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
await StorageNext.write<Info>(["project", id], project)
|
||||
return project
|
||||
}
|
||||
if (cache.has(directory)) {
|
||||
return cache.get(directory)!
|
||||
}
|
||||
const result = await fn()
|
||||
cache.set(directory, result)
|
||||
return result
|
||||
}
|
||||
|
||||
export async function setInitialized() {
|
||||
const project = use()
|
||||
await StorageNext.update<Info>(["project", project.id], (draft) => {
|
||||
export async function setInitialized(projectID: string) {
|
||||
await StorageNext.update<Info>(["project", projectID], (draft) => {
|
||||
draft.time.initialized = Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
export async function list() {
|
||||
await init()
|
||||
const keys = await StorageNext.list(["project"])
|
||||
return await Promise.all(keys.map((x) => StorageNext.read<Info>(x)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute } from "hono-openapi"
|
||||
import { resolver, validator as zValidator } from "hono-openapi/zod"
|
||||
import { Project } from "../project/project"
|
||||
|
||||
export const ProjectRoute = new Hono().get(
|
||||
"/",
|
||||
describeRoute({
|
||||
description: "List all projects",
|
||||
operationId: "project.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of projects",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Project.Info.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const projects = await Project.list()
|
||||
return c.json(projects)
|
||||
},
|
||||
)
|
||||
+1096
-1139
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,6 @@ import { ulid } from "ulid"
|
||||
import { defer } from "../util/defer"
|
||||
import { Command } from "../command"
|
||||
import { $ } from "bun"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -175,11 +174,10 @@ export namespace Session {
|
||||
}
|
||||
|
||||
export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
|
||||
const project = Project.use()
|
||||
const result: Info = {
|
||||
id: Identifier.descending("session", input.id),
|
||||
version: Installation.VERSION,
|
||||
projectID: project.id,
|
||||
projectID: Instance.project.id,
|
||||
directory: input.directory,
|
||||
parentID: input.parentID,
|
||||
title: input.title ?? createDefaultTitle(!!input.parentID),
|
||||
@@ -189,7 +187,7 @@ export namespace Session {
|
||||
},
|
||||
}
|
||||
log.info("created", result)
|
||||
await StorageNext.write(["session", project.id, result.id], result)
|
||||
await StorageNext.write(["session", Instance.project.id, result.id], result)
|
||||
const cfg = await Config.get()
|
||||
if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto"))
|
||||
share(result.id)
|
||||
@@ -208,8 +206,7 @@ export namespace Session {
|
||||
}
|
||||
|
||||
export async function get(id: string) {
|
||||
const project = Project.use()
|
||||
const read = await StorageNext.read<Info>(["session", project.id, id])
|
||||
const read = await StorageNext.read<Info>(["session", Instance.project.id, id])
|
||||
return read as Info
|
||||
}
|
||||
|
||||
@@ -253,7 +250,7 @@ export namespace Session {
|
||||
}
|
||||
|
||||
export async function update(id: string, editor: (session: Info) => void) {
|
||||
const project = Project.use()
|
||||
const project = Instance.project
|
||||
const result = await StorageNext.update<Info>(["session", project.id, id], (draft) => {
|
||||
editor(draft)
|
||||
draft.time.updated = Date.now()
|
||||
@@ -298,14 +295,14 @@ export namespace Session {
|
||||
}
|
||||
|
||||
export async function* list() {
|
||||
const project = Project.use()
|
||||
const project = Instance.project
|
||||
for (const item of await StorageNext.list(["session", project.id])) {
|
||||
yield StorageNext.read<Info>(item)
|
||||
}
|
||||
}
|
||||
|
||||
export async function children(parentID: string) {
|
||||
const project = Project.use()
|
||||
const project = Instance.project
|
||||
const result = [] as Session.Info[]
|
||||
for (const item of await StorageNext.list(["session", project.id])) {
|
||||
const session = await StorageNext.read<Info>(item)
|
||||
@@ -327,7 +324,7 @@ export namespace Session {
|
||||
}
|
||||
|
||||
export async function remove(sessionID: string, emitEvent = true) {
|
||||
const project = Project.use()
|
||||
const project = Instance.project
|
||||
try {
|
||||
abort(sessionID)
|
||||
const session = await get(sessionID)
|
||||
@@ -1125,7 +1122,6 @@ export namespace Session {
|
||||
},
|
||||
}
|
||||
await updatePart(part)
|
||||
const app = App.info()
|
||||
const shell = process.env["SHELL"] ?? "bash"
|
||||
const shellName = path.basename(shell)
|
||||
|
||||
@@ -1145,7 +1141,7 @@ export namespace Session {
|
||||
const args = isFishOrNu ? ["-c", script] : ["-c", "-l", script]
|
||||
|
||||
const proc = spawn(shell, args, {
|
||||
cwd: app.path.cwd,
|
||||
cwd: Instance.directory,
|
||||
signal: abort.signal,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
@@ -1257,8 +1253,6 @@ export namespace Session {
|
||||
},
|
||||
] as ChatInput["parts"]
|
||||
|
||||
const app = App.info()
|
||||
|
||||
for (const match of fileMatches) {
|
||||
const filename = match[1]
|
||||
const filepath = filename.startsWith("~/")
|
||||
|
||||
@@ -31,7 +31,7 @@ export namespace SystemPrompt {
|
||||
}
|
||||
|
||||
export async function environment() {
|
||||
const project = Project.use()
|
||||
const project = Instance.project
|
||||
return [
|
||||
[
|
||||
`Here is some useful information about the environment you are running in:`,
|
||||
|
||||
@@ -45,11 +45,7 @@ export namespace Share {
|
||||
})
|
||||
}
|
||||
|
||||
export function init() {
|
||||
Bus.subscribe(StorageNext.Event.Write, async (payload) => {
|
||||
await sync(payload.properties.key.join("/"), payload.properties.content)
|
||||
})
|
||||
}
|
||||
export function init() {}
|
||||
|
||||
export const URL =
|
||||
process.env["OPENCODE_API"] ??
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Log } from "../util/log"
|
||||
import { Global } from "../global"
|
||||
import { z } from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { Project } from "../project/project"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export namespace Snapshot {
|
||||
@@ -26,8 +25,7 @@ export namespace Snapshot {
|
||||
}
|
||||
|
||||
export async function track() {
|
||||
const project = Project.use()
|
||||
if (project.vcs !== "git") return
|
||||
if (Instance.project.vcs !== "git") return
|
||||
const cfg = await Config.get()
|
||||
if (cfg.snapshot === false) return
|
||||
const git = gitdir()
|
||||
@@ -104,7 +102,7 @@ export namespace Snapshot {
|
||||
}
|
||||
|
||||
function gitdir() {
|
||||
const project = Project.use()
|
||||
const project = Instance.project
|
||||
return path.join(Global.Path.data, "snapshot", project.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,8 @@ import fs from "fs/promises"
|
||||
import { Global } from "../global"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Lock } from "../util/lock"
|
||||
import { Bus } from "../bus"
|
||||
import z from "zod"
|
||||
|
||||
export namespace StorageNext {
|
||||
export const Event = {
|
||||
Write: Bus.event("storage.write", z.object({ key: z.string().array(), content: z.any() })),
|
||||
}
|
||||
|
||||
const log = Log.create({ service: "storage" })
|
||||
|
||||
type Migration = (dir: string) => Promise<void>
|
||||
@@ -55,7 +49,6 @@ export namespace StorageNext {
|
||||
const content = await Bun.file(target).json()
|
||||
fn(content)
|
||||
await Bun.write(target, JSON.stringify(content, null, 2))
|
||||
Bus.publish(Event.Write, { key, content })
|
||||
return content as T
|
||||
}
|
||||
|
||||
@@ -64,7 +57,6 @@ export namespace StorageNext {
|
||||
const target = path.join(dir, ...key) + ".json"
|
||||
using _ = await Lock.write("storage")
|
||||
await Bun.write(target, JSON.stringify(content, null, 2))
|
||||
Bus.publish(Event.Write, { key, content })
|
||||
}
|
||||
|
||||
const glob = new Bun.Glob("**/*")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { App } from "../../src/app/app"
|
||||
import path from "path"
|
||||
import { BashTool } from "../../src/tool/bash"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
@@ -19,7 +19,7 @@ Log.init({ print: false })
|
||||
|
||||
describe("tool.bash", () => {
|
||||
test("basic", async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
const result = await bash.execute(
|
||||
{
|
||||
command: "echo 'test'",
|
||||
@@ -33,7 +33,7 @@ describe("tool.bash", () => {
|
||||
})
|
||||
|
||||
test("cd ../ should fail outside of project root", async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
expect(
|
||||
bash.execute(
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { App } from "../../src/app/app"
|
||||
import { GlobTool } from "../../src/tool/glob"
|
||||
import { ListTool } from "../../src/tool/ls"
|
||||
import path from "path"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
@@ -20,7 +20,7 @@ const fixturePath = path.join(__dirname, "../fixtures/example")
|
||||
|
||||
describe("tool.glob", () => {
|
||||
test("truncate", async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "**/*",
|
||||
@@ -32,7 +32,7 @@ describe("tool.glob", () => {
|
||||
})
|
||||
})
|
||||
test("basic", async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
await Instance.provide(projectRoot, async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "*.json",
|
||||
@@ -50,7 +50,7 @@ describe("tool.glob", () => {
|
||||
|
||||
describe("tool.ls", () => {
|
||||
test("basic", async () => {
|
||||
const result = await App.provide({ cwd: projectRoot }, async () => {
|
||||
const result = await Instance.provide(projectRoot, async () => {
|
||||
return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user