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 | |
| parent | 96fbc37f0175052291f8a096d530bd4480f6cb19 (diff) | |
| download | opencode-a34d020bc6b252e842f042d935c7a0e6444460cf.tar.gz opencode-a34d020bc6b252e842f042d935c7a0e6444460cf.zip | |
sync
Diffstat (limited to 'js')
| -rw-r--r-- | js/.gitignore | 34 | ||||
| -rw-r--r-- | js/README.md | 15 | ||||
| -rw-r--r-- | js/bun.lock | 85 | ||||
| -rw-r--r-- | js/opencode.jsonc | 3 | ||||
| -rw-r--r-- | js/package.json | 19 | ||||
| -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 | ||||
| -rw-r--r-- | js/tsconfig.json | 5 |
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": {} +} |
