summaryrefslogtreecommitdiffhomepage
path: root/js/src
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-05-17 21:31:42 -0400
committerDax Raad <[email protected]>2025-05-26 12:40:17 -0400
commita34d020bc6b252e842f042d935c7a0e6444460cf (patch)
treeea3484499dff80e82d421e879ab639133ae9c3b4 /js/src
parent96fbc37f0175052291f8a096d530bd4480f6cb19 (diff)
downloadopencode-a34d020bc6b252e842f042d935c7a0e6444460cf.tar.gz
opencode-a34d020bc6b252e842f042d935c7a0e6444460cf.zip
sync
Diffstat (limited to 'js/src')
-rw-r--r--js/src/app/index.ts46
-rw-r--r--js/src/app/path.ts11
-rw-r--r--js/src/config/config.ts42
-rw-r--r--js/src/id/id.ts23
-rw-r--r--js/src/index.ts13
-rw-r--r--js/src/server/server.ts34
-rw-r--r--js/src/session/session.ts22
-rw-r--r--js/src/storage/storage.ts39
-rw-r--r--js/src/util/context.ts25
-rw-r--r--js/src/util/log.ts27
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;
+ }
+}