@cortexkit/aft-opencode 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +18 -4
- package/dist/bridge.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +182 -1
- package/dist/index.js.map +1 -1
- package/dist/notifications.d.ts +48 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +307 -0
- package/dist/notifications.js.map +1 -0
- package/dist/onnx-runtime.d.ts +35 -0
- package/dist/onnx-runtime.d.ts.map +1 -0
- package/dist/onnx-runtime.js +203 -0
- package/dist/onnx-runtime.js.map +1 -0
- package/dist/pool.d.ts +2 -0
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +25 -6
- package/dist/pool.js.map +1 -1
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +15 -4
- package/dist/resolver.js.map +1 -1
- package/dist/shared/opencode-config-dir.d.ts +16 -0
- package/dist/shared/opencode-config-dir.d.ts.map +1 -0
- package/dist/shared/opencode-config-dir.js +26 -0
- package/dist/shared/opencode-config-dir.js.map +1 -0
- package/dist/shared/rpc-client.d.ts +16 -0
- package/dist/shared/rpc-client.d.ts.map +1 -0
- package/dist/shared/rpc-client.js +108 -0
- package/dist/shared/rpc-client.js.map +1 -0
- package/dist/shared/rpc-server.d.ts +17 -0
- package/dist/shared/rpc-server.d.ts.map +1 -0
- package/dist/shared/rpc-server.js +120 -0
- package/dist/shared/rpc-server.js.map +1 -0
- package/dist/shared/rpc-utils.d.ts +11 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -0
- package/dist/shared/rpc-utils.js +20 -0
- package/dist/shared/rpc-utils.js.map +1 -0
- package/dist/shared/runtime.d.ts +10 -0
- package/dist/shared/runtime.d.ts.map +1 -0
- package/dist/shared/runtime.js +14 -0
- package/dist/shared/runtime.js.map +1 -0
- package/dist/shared/status.d.ts +37 -0
- package/dist/shared/status.d.ts.map +1 -0
- package/dist/shared/status.js +135 -0
- package/dist/shared/status.js.map +1 -0
- package/dist/shared/tui-config.d.ts +2 -0
- package/dist/shared/tui-config.d.ts.map +1 -0
- package/dist/shared/tui-config.js +134 -0
- package/dist/shared/tui-config.js.map +1 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +23 -3
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/semantic.d.ts +4 -0
- package/dist/tools/semantic.d.ts.map +1 -0
- package/dist/tools/semantic.js +35 -0
- package/dist/tools/semantic.js.map +1 -0
- package/package.json +26 -6
- package/src/shared/opencode-config-dir.ts +46 -0
- package/src/shared/rpc-client.ts +123 -0
- package/src/shared/rpc-server.ts +135 -0
- package/src/shared/rpc-utils.ts +21 -0
- package/src/shared/runtime.ts +26 -0
- package/src/shared/status.ts +206 -0
- package/src/shared/tui-config.ts +155 -0
- package/src/tui/index.tsx +209 -0
- package/src/tui/types/opencode-plugin-tui.d.ts +232 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { log, warn } from "../logger.js";
|
|
5
|
+
import { rpcPortFilePath } from "./rpc-utils.js";
|
|
6
|
+
|
|
7
|
+
type RpcHandler = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
8
|
+
|
|
9
|
+
export class AftRpcServer {
|
|
10
|
+
private server: Server | null = null;
|
|
11
|
+
private port = 0;
|
|
12
|
+
private handlers = new Map<string, RpcHandler>();
|
|
13
|
+
private portFilePath: string;
|
|
14
|
+
|
|
15
|
+
constructor(storageDir: string, directory: string) {
|
|
16
|
+
this.portFilePath = rpcPortFilePath(storageDir, directory);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Register an RPC method handler. */
|
|
20
|
+
handle(method: string, handler: RpcHandler): void {
|
|
21
|
+
this.handlers.set(method, handler);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Start the server on a random port, write port to disk. */
|
|
25
|
+
async start(): Promise<number> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const server = createServer((req, res) => this.dispatch(req, res));
|
|
28
|
+
|
|
29
|
+
server.on("error", (err) => {
|
|
30
|
+
warn(`RPC server error: ${err.message}`);
|
|
31
|
+
reject(err);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
server.listen(0, "127.0.0.1", () => {
|
|
35
|
+
const addr = server.address();
|
|
36
|
+
if (!addr || typeof addr === "string") {
|
|
37
|
+
reject(new Error("Failed to get server address"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.port = addr.port;
|
|
41
|
+
this.server = server;
|
|
42
|
+
|
|
43
|
+
// Write port file atomically
|
|
44
|
+
try {
|
|
45
|
+
const dir = dirname(this.portFilePath);
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
const tmpPath = `${this.portFilePath}.tmp`;
|
|
48
|
+
writeFileSync(tmpPath, String(this.port), "utf-8");
|
|
49
|
+
renameSync(tmpPath, this.portFilePath);
|
|
50
|
+
log(`RPC server listening on 127.0.0.1:${this.port}`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
warn(`Failed to write RPC port file: ${err}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
resolve(this.port);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Don't keep the process alive just for the RPC server
|
|
59
|
+
server.unref();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Stop the server and clean up port file. */
|
|
64
|
+
stop(): void {
|
|
65
|
+
if (this.server) {
|
|
66
|
+
this.server.close();
|
|
67
|
+
this.server = null;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
unlinkSync(this.portFilePath);
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private dispatch(req: IncomingMessage, res: ServerResponse): void {
|
|
77
|
+
const url = req.url ?? "";
|
|
78
|
+
|
|
79
|
+
if (req.method === "GET" && url === "/health") {
|
|
80
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
81
|
+
res.end(JSON.stringify({ ok: true, pid: process.pid }));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (req.method !== "POST" || !url.startsWith("/rpc/")) {
|
|
86
|
+
res.writeHead(404);
|
|
87
|
+
res.end("Not Found");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const method = url.slice(5);
|
|
92
|
+
const handler = this.handlers.get(method);
|
|
93
|
+
if (!handler) {
|
|
94
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
95
|
+
res.end(JSON.stringify({ error: `Unknown method: ${method}` }));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let body = "";
|
|
100
|
+
req.on("data", (chunk: Buffer) => {
|
|
101
|
+
body += chunk.toString();
|
|
102
|
+
if (body.length > 1_048_576) {
|
|
103
|
+
res.writeHead(413);
|
|
104
|
+
res.end("Request too large");
|
|
105
|
+
req.destroy();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
req.on("end", () => {
|
|
110
|
+
let params: Record<string, unknown> = {};
|
|
111
|
+
try {
|
|
112
|
+
if (body.length > 0) {
|
|
113
|
+
params = JSON.parse(body);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
117
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
log(`RPC call: ${method} params=${JSON.stringify(params).slice(0, 200)}`);
|
|
122
|
+
handler(params)
|
|
123
|
+
.then((result) => {
|
|
124
|
+
log(`RPC result: ${method} => ${JSON.stringify(result).slice(0, 200)}`);
|
|
125
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
126
|
+
res.end(JSON.stringify(result));
|
|
127
|
+
})
|
|
128
|
+
.catch((err) => {
|
|
129
|
+
log(`RPC error: ${method} => ${err}`);
|
|
130
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
131
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compute a stable hash for a project directory.
|
|
6
|
+
* Used to scope RPC port files per-project so multiple
|
|
7
|
+
* OpenCode Desktop instances don't overwrite each other.
|
|
8
|
+
*/
|
|
9
|
+
export function projectHash(directory: string): string {
|
|
10
|
+
// Normalize: strip trailing slashes
|
|
11
|
+
const normalized = directory.replace(/\/+$/, "");
|
|
12
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the per-project RPC port file path.
|
|
17
|
+
*/
|
|
18
|
+
export function rpcPortFilePath(storageDir: string, directory: string): string {
|
|
19
|
+
const hash = projectHash(directory);
|
|
20
|
+
return join(storageDir, "rpc", hash, "port");
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type BridgeLike = {
|
|
2
|
+
getBridge: (
|
|
3
|
+
directory: string,
|
|
4
|
+
sessionID: string,
|
|
5
|
+
) => {
|
|
6
|
+
send: (command: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const GLOBAL_KEY = "__AFT_SHARED_BRIDGE_POOL__";
|
|
11
|
+
|
|
12
|
+
function getGlobalState(): { [GLOBAL_KEY]?: BridgeLike | null } {
|
|
13
|
+
return globalThis as { [GLOBAL_KEY]?: BridgeLike | null };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function setSharedBridgePool(pool: BridgeLike): void {
|
|
17
|
+
getGlobalState()[GLOBAL_KEY] = pool;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getSharedBridgePool(): BridgeLike | null {
|
|
21
|
+
return getGlobalState()[GLOBAL_KEY] ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function clearSharedBridgePool(): void {
|
|
25
|
+
getGlobalState()[GLOBAL_KEY] = null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
export interface AftStatusSnapshot {
|
|
2
|
+
version: string;
|
|
3
|
+
project_root: string | null;
|
|
4
|
+
features: {
|
|
5
|
+
format_on_edit: boolean;
|
|
6
|
+
validate_on_edit: string;
|
|
7
|
+
restrict_to_project_root: boolean;
|
|
8
|
+
experimental_search_index: boolean;
|
|
9
|
+
experimental_semantic_search: boolean;
|
|
10
|
+
};
|
|
11
|
+
search_index: {
|
|
12
|
+
status: string;
|
|
13
|
+
files: number | null;
|
|
14
|
+
trigrams: number | null;
|
|
15
|
+
};
|
|
16
|
+
semantic_index: {
|
|
17
|
+
status: string;
|
|
18
|
+
entries: number | null;
|
|
19
|
+
dimension: number | null;
|
|
20
|
+
};
|
|
21
|
+
disk: {
|
|
22
|
+
storage_dir: string | null;
|
|
23
|
+
trigram_disk_bytes: number;
|
|
24
|
+
semantic_disk_bytes: number;
|
|
25
|
+
};
|
|
26
|
+
lsp_servers: number;
|
|
27
|
+
symbol_cache: {
|
|
28
|
+
local_entries: number;
|
|
29
|
+
warm_entries: number;
|
|
30
|
+
};
|
|
31
|
+
storage_dir: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
35
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readString(value: unknown, fallback = ""): string {
|
|
39
|
+
return typeof value === "string" ? value : fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readNullableString(value: unknown): string | null {
|
|
43
|
+
return typeof value === "string" ? value : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readBoolean(value: unknown, fallback = false): boolean {
|
|
47
|
+
return typeof value === "boolean" ? value : fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readNumber(value: unknown, fallback = 0): number {
|
|
51
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readOptionalNumber(value: unknown): number | null {
|
|
55
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatFlag(enabled: boolean): string {
|
|
59
|
+
return enabled ? "enabled" : "disabled";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatCount(value: number | null): string {
|
|
63
|
+
return value == null ? "—" : value.toLocaleString("en-US");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatBytes(bytes: number): string {
|
|
67
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
68
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
69
|
+
let value = bytes;
|
|
70
|
+
let unitIndex = 0;
|
|
71
|
+
|
|
72
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
73
|
+
value /= 1024;
|
|
74
|
+
unitIndex++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const decimals = value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
78
|
+
return `${value.toFixed(decimals)} ${units[unitIndex]}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function coerceAftStatus(response: Record<string, unknown>): AftStatusSnapshot {
|
|
82
|
+
const features = asRecord(response.features);
|
|
83
|
+
const searchIndex = asRecord(response.search_index);
|
|
84
|
+
const semanticIndex = asRecord(response.semantic_index);
|
|
85
|
+
const disk = asRecord(response.disk);
|
|
86
|
+
const symbolCache = asRecord(response.symbol_cache);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
version: readString(response.version, "unknown"),
|
|
90
|
+
project_root: readNullableString(response.project_root),
|
|
91
|
+
features: {
|
|
92
|
+
format_on_edit: readBoolean(features.format_on_edit),
|
|
93
|
+
validate_on_edit: readString(features.validate_on_edit, "off"),
|
|
94
|
+
restrict_to_project_root: readBoolean(features.restrict_to_project_root),
|
|
95
|
+
experimental_search_index: readBoolean(features.experimental_search_index),
|
|
96
|
+
experimental_semantic_search: readBoolean(features.experimental_semantic_search),
|
|
97
|
+
},
|
|
98
|
+
search_index: {
|
|
99
|
+
status: readString(searchIndex.status, "unknown"),
|
|
100
|
+
files: readOptionalNumber(searchIndex.files),
|
|
101
|
+
trigrams: readOptionalNumber(searchIndex.trigrams),
|
|
102
|
+
},
|
|
103
|
+
semantic_index: {
|
|
104
|
+
status: readString(semanticIndex.status, "unknown"),
|
|
105
|
+
entries: readOptionalNumber(semanticIndex.entries),
|
|
106
|
+
dimension: readOptionalNumber(semanticIndex.dimension),
|
|
107
|
+
},
|
|
108
|
+
disk: {
|
|
109
|
+
storage_dir: readNullableString(disk.storage_dir),
|
|
110
|
+
trigram_disk_bytes: readNumber(disk.trigram_disk_bytes),
|
|
111
|
+
semantic_disk_bytes: readNumber(disk.semantic_disk_bytes),
|
|
112
|
+
},
|
|
113
|
+
lsp_servers: readNumber(response.lsp_servers),
|
|
114
|
+
symbol_cache: {
|
|
115
|
+
local_entries: readNumber(symbolCache.local_entries),
|
|
116
|
+
warm_entries: readNumber(symbolCache.warm_entries),
|
|
117
|
+
},
|
|
118
|
+
storage_dir: readNullableString(response.storage_dir),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
|
|
123
|
+
const lines = [
|
|
124
|
+
`AFT version: ${status.version}`,
|
|
125
|
+
`Project root: ${status.project_root ?? "(not configured)"}`,
|
|
126
|
+
"",
|
|
127
|
+
"Enabled features",
|
|
128
|
+
`- format_on_edit: ${formatFlag(status.features.format_on_edit)}`,
|
|
129
|
+
`- experimental_search_index: ${formatFlag(status.features.experimental_search_index)}`,
|
|
130
|
+
`- experimental_semantic_search: ${formatFlag(status.features.experimental_semantic_search)}`,
|
|
131
|
+
"",
|
|
132
|
+
"Search index",
|
|
133
|
+
`- status: ${status.search_index.status}`,
|
|
134
|
+
`- files: ${formatCount(status.search_index.files)}`,
|
|
135
|
+
`- trigrams: ${formatCount(status.search_index.trigrams)}`,
|
|
136
|
+
"",
|
|
137
|
+
"Semantic index",
|
|
138
|
+
`- status: ${status.semantic_index.status}`,
|
|
139
|
+
`- entries: ${formatCount(status.semantic_index.entries)}`,
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
if (status.semantic_index.dimension != null) {
|
|
143
|
+
lines.push(`- dimension: ${formatCount(status.semantic_index.dimension)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
lines.push(
|
|
147
|
+
"",
|
|
148
|
+
"Disk usage",
|
|
149
|
+
`- trigram index: ${formatBytes(status.disk.trigram_disk_bytes)}`,
|
|
150
|
+
`- semantic index: ${formatBytes(status.disk.semantic_disk_bytes)}`,
|
|
151
|
+
"",
|
|
152
|
+
"Runtime",
|
|
153
|
+
`- LSP servers: ${formatCount(status.lsp_servers)}`,
|
|
154
|
+
`- symbol cache: ${formatCount(status.symbol_cache.local_entries)} local / ${formatCount(status.symbol_cache.warm_entries)} warm`,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (status.storage_dir ?? status.disk.storage_dir) {
|
|
158
|
+
lines.push(`- storage dir: ${status.storage_dir ?? status.disk.storage_dir}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function formatStatusMarkdown(status: AftStatusSnapshot): string {
|
|
165
|
+
const lines = [
|
|
166
|
+
"## AFT Status",
|
|
167
|
+
"",
|
|
168
|
+
`- **Version:** \`${status.version}\``,
|
|
169
|
+
`- **Project root:** \`${status.project_root ?? "(not configured)"}\``,
|
|
170
|
+
"",
|
|
171
|
+
"### Enabled features",
|
|
172
|
+
`- \`format_on_edit\`: ${formatFlag(status.features.format_on_edit)}`,
|
|
173
|
+
`- \`experimental_search_index\`: ${formatFlag(status.features.experimental_search_index)}`,
|
|
174
|
+
`- \`experimental_semantic_search\`: ${formatFlag(status.features.experimental_semantic_search)}`,
|
|
175
|
+
"",
|
|
176
|
+
"### Search index",
|
|
177
|
+
`- **Status:** \`${status.search_index.status}\``,
|
|
178
|
+
`- **Files:** ${formatCount(status.search_index.files)}`,
|
|
179
|
+
`- **Trigrams:** ${formatCount(status.search_index.trigrams)}`,
|
|
180
|
+
"",
|
|
181
|
+
"### Semantic index",
|
|
182
|
+
`- **Status:** \`${status.semantic_index.status}\``,
|
|
183
|
+
`- **Entries:** ${formatCount(status.semantic_index.entries)}`,
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
if (status.semantic_index.dimension != null) {
|
|
187
|
+
lines.push(`- **Dimension:** ${formatCount(status.semantic_index.dimension)}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lines.push(
|
|
191
|
+
"",
|
|
192
|
+
"### Disk usage",
|
|
193
|
+
`- **Trigram index:** ${formatBytes(status.disk.trigram_disk_bytes)}`,
|
|
194
|
+
`- **Semantic index:** ${formatBytes(status.disk.semantic_disk_bytes)}`,
|
|
195
|
+
"",
|
|
196
|
+
"### Runtime",
|
|
197
|
+
`- **LSP servers:** ${formatCount(status.lsp_servers)}`,
|
|
198
|
+
`- **Symbol cache:** ${formatCount(status.symbol_cache.local_entries)} local / ${formatCount(status.symbol_cache.warm_entries)} warm`,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (status.storage_dir ?? status.disk.storage_dir) {
|
|
202
|
+
lines.push(`- **Storage dir:** \`${status.storage_dir ?? status.disk.storage_dir}\``);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { log } from "../logger.js";
|
|
4
|
+
import { getOpenCodeConfigPaths } from "./opencode-config-dir.js";
|
|
5
|
+
|
|
6
|
+
const PLUGIN_NAME = "@cortexkit/aft-opencode";
|
|
7
|
+
const PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
|
|
8
|
+
|
|
9
|
+
function stripJsoncComments(text: string): string {
|
|
10
|
+
let result = "";
|
|
11
|
+
let i = 0;
|
|
12
|
+
let inString = false;
|
|
13
|
+
let escaped = false;
|
|
14
|
+
|
|
15
|
+
while (i < text.length) {
|
|
16
|
+
const ch = text[i];
|
|
17
|
+
|
|
18
|
+
if (inString) {
|
|
19
|
+
result += ch;
|
|
20
|
+
if (escaped) {
|
|
21
|
+
escaped = false;
|
|
22
|
+
} else if (ch === "\\") {
|
|
23
|
+
escaped = true;
|
|
24
|
+
} else if (ch === '"') {
|
|
25
|
+
inString = false;
|
|
26
|
+
}
|
|
27
|
+
i++;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (ch === '"') {
|
|
32
|
+
inString = true;
|
|
33
|
+
result += ch;
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
39
|
+
i += 2;
|
|
40
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
45
|
+
i += 2;
|
|
46
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
47
|
+
i += 2;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
result += ch;
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stripTrailingCommas(text: string): string {
|
|
59
|
+
let result = "";
|
|
60
|
+
let i = 0;
|
|
61
|
+
let inString = false;
|
|
62
|
+
let escaped = false;
|
|
63
|
+
|
|
64
|
+
while (i < text.length) {
|
|
65
|
+
const ch = text[i];
|
|
66
|
+
|
|
67
|
+
if (inString) {
|
|
68
|
+
result += ch;
|
|
69
|
+
if (escaped) {
|
|
70
|
+
escaped = false;
|
|
71
|
+
} else if (ch === "\\") {
|
|
72
|
+
escaped = true;
|
|
73
|
+
} else if (ch === '"') {
|
|
74
|
+
inString = false;
|
|
75
|
+
}
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (ch === '"') {
|
|
81
|
+
inString = true;
|
|
82
|
+
result += ch;
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ch === ",") {
|
|
88
|
+
let j = i + 1;
|
|
89
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
90
|
+
if (text[j] === "}" || text[j] === "]") {
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
result += ch;
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseJsonc<T>(content: string): T {
|
|
104
|
+
return JSON.parse(stripTrailingCommas(stripJsoncComments(content))) as T;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveTuiConfigPath(): string {
|
|
108
|
+
const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
|
|
109
|
+
const jsoncPath = join(configDir, "tui.jsonc");
|
|
110
|
+
const jsonPath = join(configDir, "tui.json");
|
|
111
|
+
|
|
112
|
+
if (existsSync(jsoncPath)) return jsoncPath;
|
|
113
|
+
if (existsSync(jsonPath)) return jsonPath;
|
|
114
|
+
return jsonPath;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function ensureTuiPluginEntry(): boolean {
|
|
118
|
+
try {
|
|
119
|
+
const configPath = resolveTuiConfigPath();
|
|
120
|
+
|
|
121
|
+
let config: Record<string, unknown> = {};
|
|
122
|
+
if (existsSync(configPath)) {
|
|
123
|
+
config = parseJsonc<Record<string, unknown>>(readFileSync(configPath, "utf-8")) ?? {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const plugins = Array.isArray(config.plugin)
|
|
127
|
+
? config.plugin.filter((value): value is string => typeof value === "string")
|
|
128
|
+
: [];
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
plugins.some(
|
|
132
|
+
(plugin) =>
|
|
133
|
+
plugin === PLUGIN_NAME ||
|
|
134
|
+
plugin.startsWith(`${PLUGIN_NAME}@`) ||
|
|
135
|
+
plugin.includes("opencode-plugin") ||
|
|
136
|
+
plugin.includes("aft-opencode"),
|
|
137
|
+
)
|
|
138
|
+
) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
plugins.push(PLUGIN_ENTRY);
|
|
143
|
+
config.plugin = plugins;
|
|
144
|
+
|
|
145
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
146
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
147
|
+
log(`[aft-plugin] added TUI plugin entry to ${configPath}`);
|
|
148
|
+
return true;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
log(
|
|
151
|
+
`[aft-plugin] failed to update tui.json: ${error instanceof Error ? error.message : String(error)}`,
|
|
152
|
+
);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|