mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-27 12:44:22 +01:00
sync
This commit is contained in:
46
js/src/app/index.ts
Normal file
46
js/src/app/index.ts
Normal file
@@ -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;
|
||||
}
|
||||
11
js/src/app/path.ts
Normal file
11
js/src/app/path.ts
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
42
js/src/config/config.ts
Normal file
42
js/src/config/config.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
23
js/src/id/id.ts
Normal file
23
js/src/id/id.ts
Normal file
@@ -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]);
|
||||
}
|
||||
}
|
||||
13
js/src/index.ts
Normal file
13
js/src/index.ts
Normal file
@@ -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();
|
||||
});
|
||||
34
js/src/server/server.ts
Normal file
34
js/src/server/server.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
js/src/session/session.ts
Normal file
22
js/src/session/session.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
39
js/src/storage/storage.ts
Normal file
39
js/src/storage/storage.ts
Normal file
@@ -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");
|
||||
}
|
||||
25
js/src/util/context.ts
Normal file
25
js/src/util/context.ts
Normal file
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
27
js/src/util/log.ts
Normal file
27
js/src/util/log.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user