diff options
| author | Dax Raad <[email protected]> | 2025-05-17 21:31:42 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-05-26 12:40:17 -0400 |
| commit | a34d020bc6b252e842f042d935c7a0e6444460cf (patch) | |
| tree | ea3484499dff80e82d421e879ab639133ae9c3b4 /js/src | |
| parent | 96fbc37f0175052291f8a096d530bd4480f6cb19 (diff) | |
| download | opencode-a34d020bc6b252e842f042d935c7a0e6444460cf.tar.gz opencode-a34d020bc6b252e842f042d935c7a0e6444460cf.zip | |
sync
Diffstat (limited to 'js/src')
| -rw-r--r-- | js/src/app/index.ts | 46 | ||||
| -rw-r--r-- | js/src/app/path.ts | 11 | ||||
| -rw-r--r-- | js/src/config/config.ts | 42 | ||||
| -rw-r--r-- | js/src/id/id.ts | 23 | ||||
| -rw-r--r-- | js/src/index.ts | 13 | ||||
| -rw-r--r-- | js/src/server/server.ts | 34 | ||||
| -rw-r--r-- | js/src/session/session.ts | 22 | ||||
| -rw-r--r-- | js/src/storage/storage.ts | 39 | ||||
| -rw-r--r-- | js/src/util/context.ts | 25 | ||||
| -rw-r--r-- | js/src/util/log.ts | 27 |
10 files changed, 282 insertions, 0 deletions
diff --git a/js/src/app/index.ts b/js/src/app/index.ts new file mode 100644 index 000000000..1c063ebd3 --- /dev/null +++ b/js/src/app/index.ts @@ -0,0 +1,46 @@ +import fs from "fs/promises"; +import { AppPath } from "./path"; +import { Log } from "../util/log"; +import { Context } from "../util/context"; + +export namespace App { + const log = Log.create({ service: "app" }); + + export type Info = Awaited<ReturnType<typeof create>>; + + const ctx = Context.create<Info>("app"); + + export async function create(input: { directory: string }) { + log.info("creating"); + + const dataDir = AppPath.data(input.directory); + await fs.mkdir(dataDir, { recursive: true }); + log.info("created", { path: dataDir }); + + const services = new Map<any, any>(); + + return { + get root() { + return input.directory; + }, + service<T extends () => any>(service: any, init: T) { + if (!services.has(service)) { + log.info("registering service", { name: service }); + services.set(service, init()); + } + return services.get(service) as ReturnType<T>; + }, + }; + } + + export function service<T extends () => any>(key: any, init: T) { + const app = ctx.use(); + return app.service(key, init); + } + + export async function use() { + return ctx.use(); + } + + export const provide = ctx.provide; +} diff --git a/js/src/app/path.ts b/js/src/app/path.ts new file mode 100644 index 000000000..972d18c41 --- /dev/null +++ b/js/src/app/path.ts @@ -0,0 +1,11 @@ +import path from "path"; + +export namespace AppPath { + export function data(input: string) { + return path.join(input, ".opencode"); + } + + export function storage(input: string) { + return path.join(data(input), "storage"); + } +} diff --git a/js/src/config/config.ts b/js/src/config/config.ts new file mode 100644 index 000000000..d76b1a842 --- /dev/null +++ b/js/src/config/config.ts @@ -0,0 +1,42 @@ +import path from "node:path"; +import { Log } from "../util/log"; +import { App } from "../app"; + +export namespace Config { + const log = Log.create({ service: "config" }); + + // TODO: this should be zod + export interface Info { + mcp: any; // TODO + lsp: any; // TODO + } + + function state() { + return App.service("config", async () => { + const app = await App.use(); + let result: Info = { + mcp: {}, + lsp: {}, + }; + for (const file of ["opencode.jsonc", "opencode.json"]) { + const resolved = path.join(app.root, file); + log.info("searching", { path: resolved }); + try { + result = await import(path.join(app.root, file)).then( + (mod) => mod.default, + ); + log.info("found", { path: resolved }); + break; + } catch (e) { + continue; + } + } + log.info("loaded", result); + return result; + }); + } + + function get() { + return state(); + } +} diff --git a/js/src/id/id.ts b/js/src/id/id.ts new file mode 100644 index 000000000..a63716041 --- /dev/null +++ b/js/src/id/id.ts @@ -0,0 +1,23 @@ +import { ulid } from "ulid"; +import { z } from "zod"; + +export namespace Identifier { + const prefixes = { + session: "ses", + } as const; + + export function create( + prefix: keyof typeof prefixes, + given?: string, + ): string { + if (given) { + if (given.startsWith(prefixes[prefix])) return given; + throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`); + } + return [prefixes[prefix], ulid()].join("_"); + } + + export function schema(prefix: keyof typeof prefixes) { + return z.string().startsWith(prefixes[prefix]); + } +} diff --git a/js/src/index.ts b/js/src/index.ts new file mode 100644 index 000000000..9ccbfda6c --- /dev/null +++ b/js/src/index.ts @@ -0,0 +1,13 @@ +import { App } from "./app"; +import process from "node:process"; +import { RPC } from "./server/server"; +import { Session } from "./session/session"; + +const app = await App.create({ + directory: process.cwd(), +}); + +App.provide(app, async () => { + const session = await Session.create(); + const rpc = RPC.listen(); +}); diff --git a/js/src/server/server.ts b/js/src/server/server.ts new file mode 100644 index 000000000..6266e9421 --- /dev/null +++ b/js/src/server/server.ts @@ -0,0 +1,34 @@ +import { Log } from "../util/log"; + +export namespace RPC { + const log = Log.create({ service: "rpc" }); + const PORT = 16713; + export function listen(input?: { port?: number }) { + const port = input?.port ?? PORT; + log.info("trying", { port }); + try { + const server = Bun.serve({ + port, + websocket: { + open() {}, + message() {}, + }, + routes: { + "/ws": (req, server) => { + if (server.upgrade(req)) return; + return new Response("Not a websocket request", { status: 400 }); + }, + }, + }); + log.info("listening", { port }); + return { + server, + }; + } catch (e: any) { + if (e?.code === "EADDRINUSE") { + return listen({ port: port + 1 }); + } + throw e; + } + } +} diff --git a/js/src/session/session.ts b/js/src/session/session.ts new file mode 100644 index 000000000..0925584de --- /dev/null +++ b/js/src/session/session.ts @@ -0,0 +1,22 @@ +import { Identifier } from "../id/id"; +import { Storage } from "../storage/storage"; +import { Log } from "../util/log"; + +export namespace Session { + const log = Log.create({ service: "session" }); + + export interface Info { + id: string; + title: string; + } + + export async function create() { + const result: Info = { + id: Identifier.create("session"), + title: "New Session - " + new Date().toISOString(), + }; + log.info("created", result); + await Storage.write("session/info/" + result.id, JSON.stringify(result)); + return result; + } +} diff --git a/js/src/storage/storage.ts b/js/src/storage/storage.ts new file mode 100644 index 000000000..b84e089cc --- /dev/null +++ b/js/src/storage/storage.ts @@ -0,0 +1,39 @@ +import { FileStorage } from "@flystorage/file-storage"; +import { LocalStorageAdapter } from "@flystorage/local-fs"; +import fs from "fs/promises"; +import { Log } from "../util/log"; +import { App } from "../app"; +import { AppPath } from "../app/path"; + +export namespace Storage { + const log = Log.create({ service: "storage" }); + + function state() { + return App.service("storage", async () => { + const app = await App.use(); + const storageDir = AppPath.storage(app.root); + await fs.mkdir(storageDir, { recursive: true }); + const storage = new FileStorage(new LocalStorageAdapter(storageDir)); + await storage.write("test", "test"); + log.info("created", { path: storageDir }); + return { + storage, + }; + }); + } + + function expose<T extends keyof FileStorage>(key: T) { + const fn = FileStorage.prototype[key]; + return async ( + ...args: Parameters<typeof fn> + ): Promise<ReturnType<typeof fn>> => { + const { storage } = await state(); + const match = storage[key]; + // @ts-ignore + return match.call(storage, ...args); + }; + } + + export const write = expose("write"); + export const read = expose("read"); +} diff --git a/js/src/util/context.ts b/js/src/util/context.ts new file mode 100644 index 000000000..ec686293e --- /dev/null +++ b/js/src/util/context.ts @@ -0,0 +1,25 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +export namespace Context { + export class NotFound extends Error { + constructor(public readonly name: string) { + super(`No context found for ${name}`); + } + } + + export function create<T>(name: string) { + const storage = new AsyncLocalStorage<T>(); + return { + use() { + const result = storage.getStore(); + if (!result) { + throw new NotFound(name); + } + return result; + }, + provide<R>(value: T, fn: () => R) { + return storage.run<R>(value, fn); + }, + }; + } +} diff --git a/js/src/util/log.ts b/js/src/util/log.ts new file mode 100644 index 000000000..9de4eb495 --- /dev/null +++ b/js/src/util/log.ts @@ -0,0 +1,27 @@ +export namespace Log { + export function create(tags?: Record<string, any>) { + tags = tags || {}; + + const result = { + info(message?: any, extra?: Record<string, any>) { + const prefix = Object.entries({ + ...tags, + ...extra, + }) + .map(([key, value]) => `${key}=${value}`) + .join(" "); + console.log(prefix, message); + return result; + }, + tag(key: string, value: string) { + if (tags) tags[key] = value; + return result; + }, + clone() { + return Log.create({ ...tags }); + }, + }; + + return result; + } +} |
