@cortexkit/aft-opencode 0.12.0 → 0.12.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/tui.js +386 -0
- package/package.json +8 -10
- package/src/shared/opencode-config-dir.ts +0 -46
- package/src/shared/rpc-client.ts +0 -123
- package/src/shared/rpc-server.ts +0 -135
- package/src/shared/rpc-utils.ts +0 -21
- package/src/shared/runtime.ts +0 -26
- package/src/shared/status.ts +0 -206
- package/src/shared/tui-config.ts +0 -58
- package/src/shared/url-fetch.ts +0 -237
- package/src/tui/index.tsx +0 -209
- package/src/tui/types/opencode-plugin-tui.d.ts +0 -232
package/dist/tui.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/shared/rpc-client.ts
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
|
|
5
|
+
// src/shared/rpc-utils.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
function projectHash(directory) {
|
|
9
|
+
const normalized = directory.replace(/\/+$/, "");
|
|
10
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
11
|
+
}
|
|
12
|
+
function rpcPortFilePath(storageDir, directory) {
|
|
13
|
+
const hash = projectHash(directory);
|
|
14
|
+
return join(storageDir, "rpc", hash, "port");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/shared/rpc-client.ts
|
|
18
|
+
var MAX_RETRIES = 10;
|
|
19
|
+
var RETRY_DELAY_MS = 500;
|
|
20
|
+
var REQUEST_TIMEOUT_MS = 5000;
|
|
21
|
+
|
|
22
|
+
class AftRpcClient {
|
|
23
|
+
port = null;
|
|
24
|
+
portFilePath;
|
|
25
|
+
healthChecked = false;
|
|
26
|
+
constructor(storageDir, directory) {
|
|
27
|
+
this.portFilePath = rpcPortFilePath(storageDir, directory);
|
|
28
|
+
}
|
|
29
|
+
async call(method, params = {}) {
|
|
30
|
+
const port = await this.resolvePort();
|
|
31
|
+
if (!port) {
|
|
32
|
+
throw new Error("AFT RPC server not available");
|
|
33
|
+
}
|
|
34
|
+
const response = await this.fetchWithTimeout(`http://127.0.0.1:${port}/rpc/${method}`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify(params)
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
|
|
42
|
+
}
|
|
43
|
+
return await response.json();
|
|
44
|
+
}
|
|
45
|
+
async isAvailable() {
|
|
46
|
+
try {
|
|
47
|
+
const port = await this.resolvePort();
|
|
48
|
+
return port !== null;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async resolvePort() {
|
|
54
|
+
if (this.port && this.healthChecked) {
|
|
55
|
+
return this.port;
|
|
56
|
+
}
|
|
57
|
+
if (this.port) {
|
|
58
|
+
const alive = await this.healthCheck(this.port);
|
|
59
|
+
if (alive) {
|
|
60
|
+
this.healthChecked = true;
|
|
61
|
+
return this.port;
|
|
62
|
+
}
|
|
63
|
+
this.port = null;
|
|
64
|
+
this.healthChecked = false;
|
|
65
|
+
}
|
|
66
|
+
for (let attempt = 0;attempt < MAX_RETRIES; attempt++) {
|
|
67
|
+
const port = this.readPortFile();
|
|
68
|
+
if (port) {
|
|
69
|
+
const alive = await this.healthCheck(port);
|
|
70
|
+
if (alive) {
|
|
71
|
+
this.port = port;
|
|
72
|
+
this.healthChecked = true;
|
|
73
|
+
return port;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
77
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
readPortFile() {
|
|
83
|
+
try {
|
|
84
|
+
const content = readFileSync(this.portFilePath, "utf-8").trim();
|
|
85
|
+
const port = Number.parseInt(content, 10);
|
|
86
|
+
if (Number.isNaN(port) || port <= 0 || port > 65535) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return port;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async healthCheck(port) {
|
|
95
|
+
try {
|
|
96
|
+
const response = await this.fetchWithTimeout(`http://127.0.0.1:${port}/health`, {
|
|
97
|
+
method: "GET"
|
|
98
|
+
});
|
|
99
|
+
return response.ok;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async fetchWithTimeout(url, options) {
|
|
105
|
+
const controller = new AbortController;
|
|
106
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
107
|
+
try {
|
|
108
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
reset() {
|
|
114
|
+
this.port = null;
|
|
115
|
+
this.healthChecked = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/shared/status.ts
|
|
120
|
+
function asRecord(value) {
|
|
121
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
122
|
+
}
|
|
123
|
+
function readString(value, fallback = "") {
|
|
124
|
+
return typeof value === "string" ? value : fallback;
|
|
125
|
+
}
|
|
126
|
+
function readNullableString(value) {
|
|
127
|
+
return typeof value === "string" ? value : null;
|
|
128
|
+
}
|
|
129
|
+
function readBoolean(value, fallback = false) {
|
|
130
|
+
return typeof value === "boolean" ? value : fallback;
|
|
131
|
+
}
|
|
132
|
+
function readNumber(value, fallback = 0) {
|
|
133
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
134
|
+
}
|
|
135
|
+
function readOptionalNumber(value) {
|
|
136
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
137
|
+
}
|
|
138
|
+
function formatFlag(enabled) {
|
|
139
|
+
return enabled ? "enabled" : "disabled";
|
|
140
|
+
}
|
|
141
|
+
function formatCount(value) {
|
|
142
|
+
return value == null ? "\u2014" : value.toLocaleString("en-US");
|
|
143
|
+
}
|
|
144
|
+
function formatBytes(bytes) {
|
|
145
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
146
|
+
return "0 B";
|
|
147
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
148
|
+
let value = bytes;
|
|
149
|
+
let unitIndex = 0;
|
|
150
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
151
|
+
value /= 1024;
|
|
152
|
+
unitIndex++;
|
|
153
|
+
}
|
|
154
|
+
const decimals = value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
155
|
+
return `${value.toFixed(decimals)} ${units[unitIndex]}`;
|
|
156
|
+
}
|
|
157
|
+
function coerceAftStatus(response) {
|
|
158
|
+
const features = asRecord(response.features);
|
|
159
|
+
const searchIndex = asRecord(response.search_index);
|
|
160
|
+
const semanticIndex = asRecord(response.semantic_index);
|
|
161
|
+
const disk = asRecord(response.disk);
|
|
162
|
+
const symbolCache = asRecord(response.symbol_cache);
|
|
163
|
+
return {
|
|
164
|
+
version: readString(response.version, "unknown"),
|
|
165
|
+
project_root: readNullableString(response.project_root),
|
|
166
|
+
features: {
|
|
167
|
+
format_on_edit: readBoolean(features.format_on_edit),
|
|
168
|
+
validate_on_edit: readString(features.validate_on_edit, "off"),
|
|
169
|
+
restrict_to_project_root: readBoolean(features.restrict_to_project_root),
|
|
170
|
+
experimental_search_index: readBoolean(features.experimental_search_index),
|
|
171
|
+
experimental_semantic_search: readBoolean(features.experimental_semantic_search)
|
|
172
|
+
},
|
|
173
|
+
search_index: {
|
|
174
|
+
status: readString(searchIndex.status, "unknown"),
|
|
175
|
+
files: readOptionalNumber(searchIndex.files),
|
|
176
|
+
trigrams: readOptionalNumber(searchIndex.trigrams)
|
|
177
|
+
},
|
|
178
|
+
semantic_index: {
|
|
179
|
+
status: readString(semanticIndex.status, "unknown"),
|
|
180
|
+
entries: readOptionalNumber(semanticIndex.entries),
|
|
181
|
+
dimension: readOptionalNumber(semanticIndex.dimension)
|
|
182
|
+
},
|
|
183
|
+
disk: {
|
|
184
|
+
storage_dir: readNullableString(disk.storage_dir),
|
|
185
|
+
trigram_disk_bytes: readNumber(disk.trigram_disk_bytes),
|
|
186
|
+
semantic_disk_bytes: readNumber(disk.semantic_disk_bytes)
|
|
187
|
+
},
|
|
188
|
+
lsp_servers: readNumber(response.lsp_servers),
|
|
189
|
+
symbol_cache: {
|
|
190
|
+
local_entries: readNumber(symbolCache.local_entries),
|
|
191
|
+
warm_entries: readNumber(symbolCache.warm_entries)
|
|
192
|
+
},
|
|
193
|
+
storage_dir: readNullableString(response.storage_dir)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function formatStatusDialogMessage(status) {
|
|
197
|
+
const lines = [
|
|
198
|
+
`AFT version: ${status.version}`,
|
|
199
|
+
`Project root: ${status.project_root ?? "(not configured)"}`,
|
|
200
|
+
"",
|
|
201
|
+
"Enabled features",
|
|
202
|
+
`- format_on_edit: ${formatFlag(status.features.format_on_edit)}`,
|
|
203
|
+
`- experimental_search_index: ${formatFlag(status.features.experimental_search_index)}`,
|
|
204
|
+
`- experimental_semantic_search: ${formatFlag(status.features.experimental_semantic_search)}`,
|
|
205
|
+
"",
|
|
206
|
+
"Search index",
|
|
207
|
+
`- status: ${status.search_index.status}`,
|
|
208
|
+
`- files: ${formatCount(status.search_index.files)}`,
|
|
209
|
+
`- trigrams: ${formatCount(status.search_index.trigrams)}`,
|
|
210
|
+
"",
|
|
211
|
+
"Semantic index",
|
|
212
|
+
`- status: ${status.semantic_index.status}`,
|
|
213
|
+
`- entries: ${formatCount(status.semantic_index.entries)}`
|
|
214
|
+
];
|
|
215
|
+
if (status.semantic_index.dimension != null) {
|
|
216
|
+
lines.push(`- dimension: ${formatCount(status.semantic_index.dimension)}`);
|
|
217
|
+
}
|
|
218
|
+
lines.push("", "Disk usage", `- trigram index: ${formatBytes(status.disk.trigram_disk_bytes)}`, `- semantic index: ${formatBytes(status.disk.semantic_disk_bytes)}`, "", "Runtime", `- LSP servers: ${formatCount(status.lsp_servers)}`, `- symbol cache: ${formatCount(status.symbol_cache.local_entries)} local / ${formatCount(status.symbol_cache.warm_entries)} warm`);
|
|
219
|
+
if (status.storage_dir ?? status.disk.storage_dir) {
|
|
220
|
+
lines.push(`- storage dir: ${status.storage_dir ?? status.disk.storage_dir}`);
|
|
221
|
+
}
|
|
222
|
+
return lines.join(`
|
|
223
|
+
`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/tui/index.tsx
|
|
227
|
+
import { jsxDEV } from "@opentui/solid/jsx-dev-runtime";
|
|
228
|
+
var STATUS_COMMAND = "aft-status";
|
|
229
|
+
var rpcClients = new Map;
|
|
230
|
+
function getRpcClient(directory) {
|
|
231
|
+
let client = rpcClients.get(directory);
|
|
232
|
+
if (client)
|
|
233
|
+
return client;
|
|
234
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
235
|
+
const dataHome = process.env.XDG_DATA_HOME || `${home}/.local/share`;
|
|
236
|
+
const storageDir = `${dataHome}/opencode/storage/plugin/aft`;
|
|
237
|
+
client = new AftRpcClient(storageDir, directory);
|
|
238
|
+
rpcClients.set(directory, client);
|
|
239
|
+
return client;
|
|
240
|
+
}
|
|
241
|
+
function getSessionId(api) {
|
|
242
|
+
try {
|
|
243
|
+
const route = api.route.current;
|
|
244
|
+
if (route?.name === "session" && route.params?.sessionID) {
|
|
245
|
+
return route.params.sessionID;
|
|
246
|
+
}
|
|
247
|
+
} catch {}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
async function showStatusDialog(api) {
|
|
251
|
+
const sessionID = getSessionId(api);
|
|
252
|
+
if (!sessionID) {
|
|
253
|
+
api.ui.toast({ message: "No active session", variant: "warning", duration: 5000 });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const directory = api.state.path.directory ?? "";
|
|
257
|
+
if (!directory) {
|
|
258
|
+
api.ui.toast({ message: "No project directory", variant: "warning", duration: 5000 });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const client = getRpcClient(directory);
|
|
262
|
+
let currentMessage = "Connecting to AFT...";
|
|
263
|
+
try {
|
|
264
|
+
const response = await client.call("status", { sessionID });
|
|
265
|
+
if (response.success !== false) {
|
|
266
|
+
const status = coerceAftStatus(response);
|
|
267
|
+
currentMessage = formatStatusDialogMessage(status);
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
currentMessage = "AFT is starting up. Status will refresh automatically...";
|
|
271
|
+
}
|
|
272
|
+
let dialogOpen = true;
|
|
273
|
+
let pollTimer = null;
|
|
274
|
+
api.ui.dialog.setSize("large");
|
|
275
|
+
api.ui.dialog.replace(() => {
|
|
276
|
+
if (!pollTimer) {
|
|
277
|
+
pollTimer = setInterval(async () => {
|
|
278
|
+
if (!dialogOpen) {
|
|
279
|
+
if (pollTimer)
|
|
280
|
+
clearInterval(pollTimer);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const response = await client.call("status", { sessionID });
|
|
285
|
+
if (response.success !== false) {
|
|
286
|
+
const status = coerceAftStatus(response);
|
|
287
|
+
const newMessage = formatStatusDialogMessage(status);
|
|
288
|
+
if (newMessage !== currentMessage) {
|
|
289
|
+
currentMessage = newMessage;
|
|
290
|
+
api.ui.dialog.replace(() => /* @__PURE__ */ jsxDEV(api.ui.DialogAlert, {
|
|
291
|
+
title: "AFT Status",
|
|
292
|
+
message: currentMessage,
|
|
293
|
+
onConfirm: () => {
|
|
294
|
+
dialogOpen = false;
|
|
295
|
+
if (pollTimer)
|
|
296
|
+
clearInterval(pollTimer);
|
|
297
|
+
api.ui.dialog.setSize("medium");
|
|
298
|
+
}
|
|
299
|
+
}, undefined, false, undefined, this), () => {
|
|
300
|
+
dialogOpen = false;
|
|
301
|
+
if (pollTimer)
|
|
302
|
+
clearInterval(pollTimer);
|
|
303
|
+
api.ui.dialog.setSize("medium");
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} catch {}
|
|
308
|
+
}, 1500);
|
|
309
|
+
}
|
|
310
|
+
return /* @__PURE__ */ jsxDEV(api.ui.DialogAlert, {
|
|
311
|
+
title: "AFT Status",
|
|
312
|
+
message: currentMessage,
|
|
313
|
+
onConfirm: () => {
|
|
314
|
+
dialogOpen = false;
|
|
315
|
+
if (pollTimer)
|
|
316
|
+
clearInterval(pollTimer);
|
|
317
|
+
api.ui.dialog.setSize("medium");
|
|
318
|
+
}
|
|
319
|
+
}, undefined, false, undefined, this);
|
|
320
|
+
}, () => {
|
|
321
|
+
dialogOpen = false;
|
|
322
|
+
if (pollTimer)
|
|
323
|
+
clearInterval(pollTimer);
|
|
324
|
+
api.ui.dialog.setSize("medium");
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function showStartupNotifications(api) {
|
|
328
|
+
const directory = api.state.path.directory ?? "";
|
|
329
|
+
if (!directory)
|
|
330
|
+
return;
|
|
331
|
+
const client = getRpcClient(directory);
|
|
332
|
+
try {
|
|
333
|
+
const announcement = await client.call("get-announcement", {});
|
|
334
|
+
if (announcement.show && announcement.version && announcement.features?.length) {
|
|
335
|
+
const featureText = announcement.features.map((f) => ` \u2022 ${f}`).join(`
|
|
336
|
+
`);
|
|
337
|
+
api.ui.dialog.replace(() => /* @__PURE__ */ jsxDEV(api.ui.DialogAlert, {
|
|
338
|
+
title: `AFT v${announcement.version}`,
|
|
339
|
+
message: `What's new:
|
|
340
|
+
|
|
341
|
+
${featureText}`,
|
|
342
|
+
onConfirm: () => {
|
|
343
|
+
client.call("mark-announced", {});
|
|
344
|
+
}
|
|
345
|
+
}, undefined, false, undefined, this), () => {
|
|
346
|
+
client.call("mark-announced", {});
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
} catch {}
|
|
351
|
+
try {
|
|
352
|
+
const result = await client.call("get-warnings", {});
|
|
353
|
+
if (result.warnings?.length) {
|
|
354
|
+
const warningText = result.warnings.join(`
|
|
355
|
+
|
|
356
|
+
`);
|
|
357
|
+
api.ui.dialog.replace(() => /* @__PURE__ */ jsxDEV(api.ui.DialogAlert, {
|
|
358
|
+
title: "AFT Warning",
|
|
359
|
+
message: warningText,
|
|
360
|
+
onConfirm: () => {}
|
|
361
|
+
}, undefined, false, undefined, this), () => {});
|
|
362
|
+
}
|
|
363
|
+
} catch {}
|
|
364
|
+
}
|
|
365
|
+
var tui = async (api) => {
|
|
366
|
+
api.command.register(() => [
|
|
367
|
+
{
|
|
368
|
+
title: "AFT: Status",
|
|
369
|
+
value: "aft.status",
|
|
370
|
+
category: "AFT",
|
|
371
|
+
slash: { name: STATUS_COMMAND },
|
|
372
|
+
onSelect() {
|
|
373
|
+
showStatusDialog(api);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
]);
|
|
377
|
+
showStartupNotifications(api);
|
|
378
|
+
};
|
|
379
|
+
var id = "aft-opencode";
|
|
380
|
+
var tui_default = {
|
|
381
|
+
id,
|
|
382
|
+
tui
|
|
383
|
+
};
|
|
384
|
+
export {
|
|
385
|
+
tui_default as default
|
|
386
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/aft-opencode",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,12 +15,10 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
-
"src/tui",
|
|
19
|
-
"src/shared",
|
|
20
18
|
"README.md"
|
|
21
19
|
],
|
|
22
20
|
"scripts": {
|
|
23
|
-
"build": "bun build src/index.ts --outdir dist --target bun --format esm --external @opencode-ai/plugin && bun build src/cli/index.ts --outfile dist/cli.js --target node --format esm && tsc --emitDeclarationOnly",
|
|
21
|
+
"build": "bun build src/index.ts --outdir dist --target bun --format esm --external @opencode-ai/plugin && bun build src/tui/index.tsx --outfile dist/tui.js --target bun --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opentui/solid --external solid-js && bun build src/cli/index.ts --outfile dist/cli.js --target node --format esm && tsc --emitDeclarationOnly",
|
|
24
22
|
"typecheck": "tsc --noEmit",
|
|
25
23
|
"pretest": "cd ../.. && cargo build",
|
|
26
24
|
"test": "bun test",
|
|
@@ -35,11 +33,11 @@
|
|
|
35
33
|
"zod": "^4.1.8"
|
|
36
34
|
},
|
|
37
35
|
"optionalDependencies": {
|
|
38
|
-
"@cortexkit/aft-darwin-arm64": "0.12.
|
|
39
|
-
"@cortexkit/aft-darwin-x64": "0.12.
|
|
40
|
-
"@cortexkit/aft-linux-arm64": "0.12.
|
|
41
|
-
"@cortexkit/aft-linux-x64": "0.12.
|
|
42
|
-
"@cortexkit/aft-win32-x64": "0.12.
|
|
36
|
+
"@cortexkit/aft-darwin-arm64": "0.12.1",
|
|
37
|
+
"@cortexkit/aft-darwin-x64": "0.12.1",
|
|
38
|
+
"@cortexkit/aft-linux-arm64": "0.12.1",
|
|
39
|
+
"@cortexkit/aft-linux-x64": "0.12.1",
|
|
40
|
+
"@cortexkit/aft-win32-x64": "0.12.1"
|
|
43
41
|
},
|
|
44
42
|
"devDependencies": {
|
|
45
43
|
"@types/node": "^22.0.0",
|
|
@@ -52,7 +50,7 @@
|
|
|
52
50
|
},
|
|
53
51
|
"./tui": {
|
|
54
52
|
"types": "./src/tui/index.tsx",
|
|
55
|
-
"import": "./
|
|
53
|
+
"import": "./dist/tui.js"
|
|
56
54
|
}
|
|
57
55
|
},
|
|
58
56
|
"oc-plugin": [
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
export type OpenCodeBinaryType = "opencode" | "opencode-desktop";
|
|
5
|
-
|
|
6
|
-
export interface OpenCodeConfigDirOptions {
|
|
7
|
-
binary: OpenCodeBinaryType;
|
|
8
|
-
version?: string | null;
|
|
9
|
-
checkExisting?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface OpenCodeConfigPaths {
|
|
13
|
-
configDir: string;
|
|
14
|
-
configJson: string;
|
|
15
|
-
configJsonc: string;
|
|
16
|
-
packageJson: string;
|
|
17
|
-
omoConfig: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getCliConfigDir(): string {
|
|
21
|
-
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
22
|
-
if (envConfigDir) {
|
|
23
|
-
return resolve(envConfigDir);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (process.platform === "win32") {
|
|
27
|
-
return join(homedir(), ".config", "opencode");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function getOpenCodeConfigDir(_options: OpenCodeConfigDirOptions): string {
|
|
34
|
-
return getCliConfigDir();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getOpenCodeConfigPaths(options: OpenCodeConfigDirOptions): OpenCodeConfigPaths {
|
|
38
|
-
const configDir = getOpenCodeConfigDir(options);
|
|
39
|
-
return {
|
|
40
|
-
configDir,
|
|
41
|
-
configJson: join(configDir, "opencode.json"),
|
|
42
|
-
configJsonc: join(configDir, "opencode.jsonc"),
|
|
43
|
-
packageJson: join(configDir, "package.json"),
|
|
44
|
-
omoConfig: join(configDir, "magic-context.jsonc"),
|
|
45
|
-
};
|
|
46
|
-
}
|
package/src/shared/rpc-client.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { rpcPortFilePath } from "./rpc-utils.js";
|
|
3
|
-
|
|
4
|
-
const MAX_RETRIES = 10;
|
|
5
|
-
const RETRY_DELAY_MS = 500;
|
|
6
|
-
const REQUEST_TIMEOUT_MS = 5000;
|
|
7
|
-
|
|
8
|
-
export class AftRpcClient {
|
|
9
|
-
private port: number | null = null;
|
|
10
|
-
private portFilePath: string;
|
|
11
|
-
private healthChecked = false;
|
|
12
|
-
|
|
13
|
-
constructor(storageDir: string, directory: string) {
|
|
14
|
-
this.portFilePath = rpcPortFilePath(storageDir, directory);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Call an RPC method. Retries port resolution if the server isn't ready yet. */
|
|
18
|
-
async call<T = Record<string, unknown>>(
|
|
19
|
-
method: string,
|
|
20
|
-
params: Record<string, unknown> = {},
|
|
21
|
-
): Promise<T> {
|
|
22
|
-
const port = await this.resolvePort();
|
|
23
|
-
if (!port) {
|
|
24
|
-
throw new Error("AFT RPC server not available");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const response = await this.fetchWithTimeout(`http://127.0.0.1:${port}/rpc/${method}`, {
|
|
28
|
-
method: "POST",
|
|
29
|
-
headers: { "Content-Type": "application/json" },
|
|
30
|
-
body: JSON.stringify(params),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
if (!response.ok) {
|
|
34
|
-
const text = await response.text();
|
|
35
|
-
throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return (await response.json()) as T;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Check if the RPC server is reachable. */
|
|
42
|
-
async isAvailable(): Promise<boolean> {
|
|
43
|
-
try {
|
|
44
|
-
const port = await this.resolvePort();
|
|
45
|
-
return port !== null;
|
|
46
|
-
} catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private async resolvePort(): Promise<number | null> {
|
|
52
|
-
if (this.port && this.healthChecked) {
|
|
53
|
-
return this.port;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (this.port) {
|
|
57
|
-
const alive = await this.healthCheck(this.port);
|
|
58
|
-
if (alive) {
|
|
59
|
-
this.healthChecked = true;
|
|
60
|
-
return this.port;
|
|
61
|
-
}
|
|
62
|
-
this.port = null;
|
|
63
|
-
this.healthChecked = false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
67
|
-
const port = this.readPortFile();
|
|
68
|
-
if (port) {
|
|
69
|
-
const alive = await this.healthCheck(port);
|
|
70
|
-
if (alive) {
|
|
71
|
-
this.port = port;
|
|
72
|
-
this.healthChecked = true;
|
|
73
|
-
return port;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (attempt < MAX_RETRIES - 1) {
|
|
78
|
-
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private readPortFile(): number | null {
|
|
86
|
-
try {
|
|
87
|
-
const content = readFileSync(this.portFilePath, "utf-8").trim();
|
|
88
|
-
const port = Number.parseInt(content, 10);
|
|
89
|
-
if (Number.isNaN(port) || port <= 0 || port > 65535) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
return port;
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private async healthCheck(port: number): Promise<boolean> {
|
|
99
|
-
try {
|
|
100
|
-
const response = await this.fetchWithTimeout(`http://127.0.0.1:${port}/health`, {
|
|
101
|
-
method: "GET",
|
|
102
|
-
});
|
|
103
|
-
return response.ok;
|
|
104
|
-
} catch {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private async fetchWithTimeout(url: string, options: RequestInit): Promise<Response> {
|
|
110
|
-
const controller = new AbortController();
|
|
111
|
-
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
112
|
-
try {
|
|
113
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
114
|
-
} finally {
|
|
115
|
-
clearTimeout(timeout);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
reset(): void {
|
|
120
|
-
this.port = null;
|
|
121
|
-
this.healthChecked = false;
|
|
122
|
-
}
|
|
123
|
-
}
|