summaryrefslogtreecommitdiffhomepage
path: root/js
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
parent96fbc37f0175052291f8a096d530bd4480f6cb19 (diff)
downloadopencode-a34d020bc6b252e842f042d935c7a0e6444460cf.tar.gz
opencode-a34d020bc6b252e842f042d935c7a0e6444460cf.zip
sync
Diffstat (limited to 'js')
-rw-r--r--js/.gitignore34
-rw-r--r--js/README.md15
-rw-r--r--js/bun.lock85
-rw-r--r--js/opencode.jsonc3
-rw-r--r--js/package.json19
-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
-rw-r--r--js/tsconfig.json5
16 files changed, 443 insertions, 0 deletions
diff --git a/js/.gitignore b/js/.gitignore
new file mode 100644
index 000000000..a14702c40
--- /dev/null
+++ b/js/.gitignore
@@ -0,0 +1,34 @@
+# dependencies (bun install)
+node_modules
+
+# output
+out
+dist
+*.tgz
+
+# code coverage
+coverage
+*.lcov
+
+# logs
+logs
+_.log
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# caches
+.eslintcache
+.cache
+*.tsbuildinfo
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/js/README.md b/js/README.md
new file mode 100644
index 000000000..75890119c
--- /dev/null
+++ b/js/README.md
@@ -0,0 +1,15 @@
+# js
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.2.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/js/bun.lock b/js/bun.lock
new file mode 100644
index 000000000..962e2ccfb
--- /dev/null
+++ b/js/bun.lock
@@ -0,0 +1,85 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "js",
+ "dependencies": {
+ "@flystorage/file-storage": "^1.1.0",
+ "@flystorage/local-fs": "^1.1.0",
+ "ai": "^5.0.0-alpha.2",
+ "ulid": "^3.0.0",
+ "zod": "^3.24.4",
+ },
+ "devDependencies": {
+ "@tsconfig/bun": "^1.0.7",
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ },
+ "packages": {
+ "@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-jgRpHhpKmXnUEp41xUZyqJ8VPF9gS6W7SP2iYRaM9jaq66edcg6gTYOJLqM+nSU2tXYfkzfoBGGRvtl9ijH/VQ=="],
+
+ "@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.2", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-oTlF6UlVitSdVPQv0e+kAkZmbuunJAUYdVEh7ZRvoti+kY/T4vOT6p22X0xTaWgl0+MI1igAT+c83j7tCMuo2w=="],
+
+ "@flystorage/dynamic-import": ["@flystorage/[email protected]", "", {}, "sha512-CIbIUrBdaPFyKnkVBaqzksvzNtsMSXITR/G/6zlil3MBnPFq2LX+X4Mv5p2XOmv/3OulFs/ff2SNb+5dc2Twtg=="],
+
+ "@flystorage/file-storage": ["@flystorage/[email protected]", "", {}, "sha512-25Gd5EsXDmhHrK5orpRuVqebQms1Cm9m5ACMZ0sVDX+Sbl1V0G88CbcWt7mEoWRYLvQ1U072htqg6Sav76ZlVA=="],
+
+ "@flystorage/local-fs": ["@flystorage/[email protected]", "", { "dependencies": { "@flystorage/dynamic-import": "^1.0.0", "@flystorage/file-storage": "^1.1.0", "file-type": "^20.5.0", "mime-types": "^3.0.1" } }, "sha512-dbErRhqmCv2UF0zPdeH7iVWuVeTWAJHuJD/mXDe2V370/SL7XIvdE3ditBHWC+1SzBKXJ0lkykOenwlum+oqIA=="],
+
+ "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
+
+ "@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+
+ "@tokenizer/inflate": ["@tokenizer/[email protected]", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
+
+ "@tokenizer/token": ["@tokenizer/[email protected]", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
+
+ "@tsconfig/bun": ["@tsconfig/[email protected]", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
+
+ "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
+
+ "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
+
+ "ai": ["[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.2", "@ai-sdk/provider-utils": "3.0.0-alpha.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-42asUyoFcqjV5AoZezJPawODCPT5Rb1y/UipVlcXn1tpqlypCchSEukjNw/l09YPVucqCbW19IVqojLttkTTVA=="],
+
+ "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
+
+ "debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
+
+ "fflate": ["[email protected]", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
+
+ "file-type": ["[email protected]", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
+
+ "ieee754": ["[email protected]", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+ "json-schema": ["[email protected]", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
+
+ "mime-db": ["[email protected]", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+ "mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
+
+ "ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "peek-readable": ["[email protected]", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
+
+ "strtok3": ["[email protected]", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
+
+ "token-types": ["[email protected]", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
+
+ "typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
+
+ "uint8array-extras": ["[email protected]", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
+
+ "ulid": ["[email protected]", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-yvZYdXInnJve6LdlPIuYmURdS2NP41ZoF4QW7SXwbUKYt53+0eDAySO+rGSvM2O/ciuB/G+8N7GQrZ1mCJpuqw=="],
+
+ "undici-types": ["[email protected]", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "zod": ["[email protected]", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
+
+ "zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
+ }
+}
diff --git a/js/opencode.jsonc b/js/opencode.jsonc
new file mode 100644
index 000000000..02941056e
--- /dev/null
+++ b/js/opencode.jsonc
@@ -0,0 +1,3 @@
+{
+ "lol": "jsonc"
+}
diff --git a/js/package.json b/js/package.json
new file mode 100644
index 000000000..100a726ca
--- /dev/null
+++ b/js/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "opencode",
+ "type": "module",
+ "private": true,
+ "devDependencies": {
+ "@tsconfig/bun": "^1.0.7",
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "5"
+ },
+ "dependencies": {
+ "@flystorage/file-storage": "^1.1.0",
+ "@flystorage/local-fs": "^1.1.0",
+ "ai": "5.0.0-alpha.2",
+ "ulid": "3.0.0",
+ "zod": "3.24.4"
+ }
+}
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;
+ }
+}
diff --git a/js/tsconfig.json b/js/tsconfig.json
new file mode 100644
index 000000000..65fa6c7f3
--- /dev/null
+++ b/js/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@tsconfig/bun/tsconfig.json",
+ "compilerOptions": {}
+}