@dyyz1993/codenomad 0.15.0 → 0.15.2
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/auth/session-manager.js +28 -0
- package/dist/filesystem/__tests__/search-cache.test.js +5 -5
- package/dist/filesystem/search-cache.js +2 -2
- package/dist/filesystem/search.js +6 -6
- package/dist/index.js +14 -5
- package/dist/server/routes/auth-pages/login.html +30 -19
- package/dist/server/routes/events.js +26 -0
- package/dist/server/routes/settings.js +6 -6
- package/dist/server/routes/speech.js +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/settings/binaries.js +8 -8
- package/dist/settings/service.js +17 -17
- package/dist/settings/yaml-doc-store.js +24 -20
- package/dist/sidecars/manager.js +17 -13
- package/dist/speech/service.js +9 -9
- package/dist/workspaces/instance-events.js +11 -0
- package/dist/workspaces/manager.js +51 -11
- package/dist/workspaces/runtime.js +9 -5
- package/package.json +1 -1
- package/public/assets/{ChangesTab-BRNmSjeC.js → ChangesTab-Dt-TsQMo.js} +2 -2
- package/public/assets/{DiffToolbar-DPPgfx42.js → DiffToolbar-BD79zzZs.js} +1 -1
- package/public/assets/{FilesTab-sEeGPOoz.js → FilesTab-CgB87mzq.js} +2 -2
- package/public/assets/{GitChangesTab-tJ-TCq0c.js → GitChangesTab-x-snPvzz.js} +2 -2
- package/public/assets/{SplitFilePanel-CHqNLZvU.js → SplitFilePanel-CA0D92l1.js} +1 -1
- package/public/assets/{StatusTab-DMd0ByCA.js → StatusTab-DJdKuILW.js} +1 -1
- package/public/assets/{align-justify-BWapkGTe.js → align-justify-BnvEPTpu.js} +1 -1
- package/public/assets/{bundle-full-CXlKQh5B.js → bundle-full-DPp09th5.js} +1 -1
- package/public/assets/{diff-viewer-BFtW5J8P.js → diff-viewer-DYUu2t45.js} +1 -1
- package/public/assets/{index-Bdy3MTIn.js → index-4Era_AEC.js} +1 -1
- package/public/assets/{index-D6RMBXUk.js → index-BCGPLzO4.js} +2 -2
- package/public/assets/{index-Dty5bSHf.js → index-BDgGoLTZ.js} +1 -1
- package/public/assets/{index-D-NvysdX.js → index-CbKJK9y3.js} +1 -1
- package/public/assets/{index-Ctq8RqRf.js → index-CpWDdROQ.js} +1 -1
- package/public/assets/{index-DYWkPhKo.js → index-CtBkkWFK.js} +1 -1
- package/public/assets/{index-D_FDiI6a.js → index-D7V1TD3s.js} +1 -1
- package/public/assets/{index-H16e4Rqc.js → index-PHCXc0OA.js} +1 -1
- package/public/assets/{index-BvRe9GiS.js → index-nVoKL-cq.js} +1 -1
- package/public/assets/{loading-W2Y_wR0P.js → loading-B-F_vBbv.js} +1 -1
- package/public/assets/{main-Bala0YJ4.js → main-DBbZ3aZQ.js} +7 -7
- package/public/assets/{markdown-Db8cvZ4Z.js → markdown-B_Zj0Onk.js} +3 -3
- package/public/assets/{monaco-viewer-DWQqXKm3.js → monaco-viewer-YY9yfE-p.js} +8 -8
- package/public/assets/{tool-call-D0BYXlFe.js → tool-call-CwR9rE_d.js} +3 -3
- package/public/assets/{unified-picker-QgONWj_1.js → unified-picker-_n2WoeTG.js} +1 -1
- package/public/assets/{wrap-text-CEH9wOr_.js → wrap-text-B7hZaBx4.js} +1 -1
- package/public/index.html +3 -3
- package/public/loading.html +3 -3
- package/public/sw.js +1 -1
- package/dist/opencode-config/package-lock.json +0 -380
- package/dist/opencode-config/package.json +0 -9
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
|
+
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3
|
+
const CLEANUP_INTERVAL_MS = 30 * 60 * 1000;
|
|
2
4
|
export class SessionManager {
|
|
3
5
|
constructor() {
|
|
4
6
|
this.sessions = new Map();
|
|
7
|
+
this.cleanupTimer = setInterval(() => this.cleanupExpired(), CLEANUP_INTERVAL_MS);
|
|
5
8
|
}
|
|
6
9
|
createSession(username) {
|
|
7
10
|
const id = crypto.randomBytes(32).toString("base64url");
|
|
@@ -14,4 +17,29 @@ export class SessionManager {
|
|
|
14
17
|
return undefined;
|
|
15
18
|
return this.sessions.get(id);
|
|
16
19
|
}
|
|
20
|
+
validateSession(sessionId) {
|
|
21
|
+
const info = this.sessions.get(sessionId);
|
|
22
|
+
if (!info)
|
|
23
|
+
return null;
|
|
24
|
+
if (Date.now() - info.createdAt > SESSION_TTL_MS) {
|
|
25
|
+
this.sessions.delete(sessionId);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return info;
|
|
29
|
+
}
|
|
30
|
+
cleanupExpired() {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
for (const [id, info] of this.sessions) {
|
|
33
|
+
if (now - info.createdAt > SESSION_TTL_MS) {
|
|
34
|
+
this.sessions.delete(id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
shutdown() {
|
|
39
|
+
if (this.cleanupTimer) {
|
|
40
|
+
clearInterval(this.cleanupTimer);
|
|
41
|
+
this.cleanupTimer = undefined;
|
|
42
|
+
}
|
|
43
|
+
this.sessions.clear();
|
|
44
|
+
}
|
|
17
45
|
}
|
|
@@ -5,10 +5,10 @@ describe("workspace search cache", () => {
|
|
|
5
5
|
beforeEach(() => {
|
|
6
6
|
clearWorkspaceSearchCache();
|
|
7
7
|
});
|
|
8
|
-
it("expires cached candidates after the TTL", () => {
|
|
8
|
+
it("expires cached candidates after the TTL", async () => {
|
|
9
9
|
const workspacePath = "/tmp/workspace";
|
|
10
10
|
const startTime = 1000;
|
|
11
|
-
refreshWorkspaceCandidates(workspacePath, () => [createEntry("file-a")], startTime);
|
|
11
|
+
await refreshWorkspaceCandidates(workspacePath, async () => [createEntry("file-a")], startTime);
|
|
12
12
|
const beforeExpiry = getWorkspaceCandidates(workspacePath, startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS - 1);
|
|
13
13
|
assert.ok(beforeExpiry);
|
|
14
14
|
assert.equal(beforeExpiry.length, 1);
|
|
@@ -16,13 +16,13 @@ describe("workspace search cache", () => {
|
|
|
16
16
|
const afterExpiry = getWorkspaceCandidates(workspacePath, startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS + 1);
|
|
17
17
|
assert.equal(afterExpiry, undefined);
|
|
18
18
|
});
|
|
19
|
-
it("replaces cached entries when manually refreshed", () => {
|
|
19
|
+
it("replaces cached entries when manually refreshed", async () => {
|
|
20
20
|
const workspacePath = "/tmp/workspace";
|
|
21
|
-
refreshWorkspaceCandidates(workspacePath, () => [createEntry("file-a")], 5000);
|
|
21
|
+
await refreshWorkspaceCandidates(workspacePath, async () => [createEntry("file-a")], 5000);
|
|
22
22
|
const initial = getWorkspaceCandidates(workspacePath, 5001);
|
|
23
23
|
assert.ok(initial);
|
|
24
24
|
assert.equal(initial[0].name, "file-a");
|
|
25
|
-
refreshWorkspaceCandidates(workspacePath, () => [createEntry("file-b")], 6000);
|
|
25
|
+
await refreshWorkspaceCandidates(workspacePath, async () => [createEntry("file-b")], 6000);
|
|
26
26
|
const refreshed = getWorkspaceCandidates(workspacePath, 6001);
|
|
27
27
|
assert.ok(refreshed);
|
|
28
28
|
assert.equal(refreshed[0].name, "file-b");
|
|
@@ -13,9 +13,9 @@ export function getWorkspaceCandidates(rootDir, now = Date.now()) {
|
|
|
13
13
|
}
|
|
14
14
|
return cloneEntries(cached.candidates);
|
|
15
15
|
}
|
|
16
|
-
export function refreshWorkspaceCandidates(rootDir, builder, now = Date.now()) {
|
|
16
|
+
export async function refreshWorkspaceCandidates(rootDir, builder, now = Date.now()) {
|
|
17
17
|
const key = normalizeKey(rootDir);
|
|
18
|
-
const freshCandidates = builder();
|
|
18
|
+
const freshCandidates = await builder();
|
|
19
19
|
if (!freshCandidates || freshCandidates.length === 0) {
|
|
20
20
|
workspaceCandidateCache.delete(key);
|
|
21
21
|
return [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fuzzysort from "fuzzysort";
|
|
4
4
|
import { clearWorkspaceSearchCache, getWorkspaceCandidates, refreshWorkspaceCandidates } from "./search-cache";
|
|
@@ -6,7 +6,7 @@ const DEFAULT_LIMIT = 100;
|
|
|
6
6
|
const MAX_LIMIT = 200;
|
|
7
7
|
const MAX_CANDIDATES = 8000;
|
|
8
8
|
const IGNORED_DIRECTORIES = new Set([".git", ".hg", ".svn", "node_modules", "dist", "build", ".next", ".nuxt", ".turbo", ".cache", "coverage"].map((name) => name.toLowerCase()));
|
|
9
|
-
export function searchWorkspaceFiles(rootDir, query, options = {}) {
|
|
9
|
+
export async function searchWorkspaceFiles(rootDir, query, options = {}) {
|
|
10
10
|
const trimmedQuery = query.trim();
|
|
11
11
|
if (!trimmedQuery) {
|
|
12
12
|
throw new Error("Search query is required");
|
|
@@ -21,7 +21,7 @@ export function searchWorkspaceFiles(rootDir, query, options = {}) {
|
|
|
21
21
|
entries = getWorkspaceCandidates(normalizedRoot);
|
|
22
22
|
}
|
|
23
23
|
if (!entries) {
|
|
24
|
-
entries = refreshWorkspaceCandidates(normalizedRoot, () => collectCandidates(normalizedRoot));
|
|
24
|
+
entries = await refreshWorkspaceCandidates(normalizedRoot, () => collectCandidates(normalizedRoot));
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
catch (error) {
|
|
@@ -45,7 +45,7 @@ export function searchWorkspaceFiles(rootDir, query, options = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
return matches.map((match) => match.obj.entry);
|
|
47
47
|
}
|
|
48
|
-
function collectCandidates(rootDir) {
|
|
48
|
+
async function collectCandidates(rootDir) {
|
|
49
49
|
const queue = [""];
|
|
50
50
|
const entries = [];
|
|
51
51
|
while (queue.length > 0 && entries.length < MAX_CANDIDATES) {
|
|
@@ -53,7 +53,7 @@ function collectCandidates(rootDir) {
|
|
|
53
53
|
const absoluteDir = relativeDir ? path.join(rootDir, relativeDir) : rootDir;
|
|
54
54
|
let dirents;
|
|
55
55
|
try {
|
|
56
|
-
dirents =
|
|
56
|
+
dirents = await readdir(absoluteDir, { withFileTypes: true });
|
|
57
57
|
}
|
|
58
58
|
catch {
|
|
59
59
|
continue;
|
|
@@ -68,7 +68,7 @@ function collectCandidates(rootDir) {
|
|
|
68
68
|
}
|
|
69
69
|
let stats;
|
|
70
70
|
try {
|
|
71
|
-
stats =
|
|
71
|
+
stats = await stat(absolutePath);
|
|
72
72
|
}
|
|
73
73
|
catch {
|
|
74
74
|
continue;
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,7 @@ function parseCliOptions(argv) {
|
|
|
48
48
|
.addOption(new Option("--http <enabled>", "Enable HTTP listener (true|false)").env("CLI_HTTP").default("false"))
|
|
49
49
|
.addOption(new Option("--https-port <number>", "HTTPS port (0 for auto)").env("CLI_HTTPS_PORT").default(DEFAULT_HTTPS_PORT).argParser(parsePort))
|
|
50
50
|
.addOption(new Option("--http-port <number>", "HTTP port (0 for auto)").env("CLI_HTTP_PORT").default(DEFAULT_HTTP_PORT).argParser(parsePort))
|
|
51
|
+
.addOption(new Option("--port <number>", "Shorthand for --http-port").argParser(parsePort))
|
|
51
52
|
.addOption(new Option("--tls-key <path>", "TLS private key (PEM)").env("CLI_TLS_KEY"))
|
|
52
53
|
.addOption(new Option("--tls-cert <path>", "TLS certificate (PEM)").env("CLI_TLS_CERT"))
|
|
53
54
|
.addOption(new Option("--tls-ca <path>", "TLS CA chain (PEM)").env("CLI_TLS_CA"))
|
|
@@ -94,12 +95,18 @@ function parseCliOptions(argv) {
|
|
|
94
95
|
if (upgrade === undefined && !httpsEnabled && !httpEnabled) {
|
|
95
96
|
throw new InvalidArgumentError("At least one listener must be enabled (--https or --http)");
|
|
96
97
|
}
|
|
98
|
+
const resolvedHttpPort = parsed.port ?? parsed.httpPort;
|
|
99
|
+
const portExplicit = parsed.port !== undefined;
|
|
100
|
+
const httpsPortExplicit = parsed.httpsPort !== DEFAULT_HTTPS_PORT;
|
|
101
|
+
const resolvedHttpsPort = portExplicit && !httpsPortExplicit && resolvedHttpPort !== DEFAULT_HTTP_PORT
|
|
102
|
+
? resolvedHttpPort + 1
|
|
103
|
+
: parsed.httpsPort;
|
|
97
104
|
return {
|
|
98
105
|
host: normalizedHost,
|
|
99
106
|
https: httpsEnabled,
|
|
100
107
|
http: httpEnabled,
|
|
101
|
-
httpsPort:
|
|
102
|
-
httpPort:
|
|
108
|
+
httpsPort: resolvedHttpsPort,
|
|
109
|
+
httpPort: resolvedHttpPort,
|
|
103
110
|
tlsKeyPath: parsed.tlsKey,
|
|
104
111
|
tlsCertPath: parsed.tlsCert,
|
|
105
112
|
tlsCaPath: parsed.tlsCa,
|
|
@@ -225,7 +232,7 @@ async function main() {
|
|
|
225
232
|
});
|
|
226
233
|
const instanceStore = new InstanceStore(configLocation.instancesDir);
|
|
227
234
|
const speechService = new SpeechService(settings, logger.child({ component: "speech" }));
|
|
228
|
-
const sidecarManager =
|
|
235
|
+
const sidecarManager = await SideCarManager.create({
|
|
229
236
|
settings,
|
|
230
237
|
eventBus,
|
|
231
238
|
logger: logger.child({ component: "sidecars" }),
|
|
@@ -247,6 +254,7 @@ async function main() {
|
|
|
247
254
|
overrideUiDir: uiDirOverride,
|
|
248
255
|
uiDevServerUrl: options.uiDevServer,
|
|
249
256
|
manifestUrl: options.uiManifestUrl,
|
|
257
|
+
configDir,
|
|
250
258
|
logger: logger.child({ component: "ui" }),
|
|
251
259
|
});
|
|
252
260
|
serverMeta.serverVersion = packageJson.version;
|
|
@@ -288,8 +296,9 @@ async function main() {
|
|
|
288
296
|
channel: pluginChannel,
|
|
289
297
|
logger: logger.child({ component: "voice-mode" }),
|
|
290
298
|
});
|
|
291
|
-
const
|
|
292
|
-
const
|
|
299
|
+
const portArgPassed = programHasArg(process.argv.slice(2), "--port");
|
|
300
|
+
const httpsPortExplicit = programHasArg(process.argv.slice(2), "--https-port") || Boolean(process.env.CLI_HTTPS_PORT) || portArgPassed;
|
|
301
|
+
const httpPortExplicit = programHasArg(process.argv.slice(2), "--http-port") || programHasArg(process.argv.slice(2), "--port") || Boolean(process.env.CLI_HTTP_PORT);
|
|
293
302
|
const httpsBindPort = httpsPortExplicit ? options.httpsPort : 0;
|
|
294
303
|
const httpBindPort = httpPortExplicit ? options.httpPort : 0;
|
|
295
304
|
// Listener binding rules:
|
|
@@ -101,28 +101,39 @@
|
|
|
101
101
|
showError("Username and password are required.")
|
|
102
102
|
return
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
message = ""
|
|
104
|
+
|
|
105
|
+
const MAX_RETRIES = 3
|
|
106
|
+
const RETRY_DELAY_MS = 2000
|
|
107
|
+
|
|
108
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch("/api/auth/login", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({ username, password }),
|
|
114
|
+
credentials: "include",
|
|
115
|
+
})
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
let message = ""
|
|
118
|
+
try {
|
|
119
|
+
const json = await res.json()
|
|
120
|
+
message = json && json.error ? String(json.error) : ""
|
|
121
|
+
} catch {
|
|
122
|
+
message = ""
|
|
123
|
+
}
|
|
124
|
+
showError(message || `Login failed (${res.status})`)
|
|
125
|
+
return
|
|
118
126
|
}
|
|
119
|
-
|
|
127
|
+
window.location.replace("/")
|
|
120
128
|
return
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (attempt < MAX_RETRIES) {
|
|
131
|
+
showError(`Connection failed, retrying (${attempt}/${MAX_RETRIES})...`)
|
|
132
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS))
|
|
133
|
+
} else {
|
|
134
|
+
showError("Service connection failed. The port may have changed. Please refresh the page and try again.")
|
|
135
|
+
}
|
|
121
136
|
}
|
|
122
|
-
// Replace history entry so Back doesn't return to /login.
|
|
123
|
-
window.location.replace("/")
|
|
124
|
-
} catch (e) {
|
|
125
|
-
showError(e && e.message ? e.message : String(e))
|
|
126
137
|
}
|
|
127
138
|
}
|
|
128
139
|
|
|
@@ -3,6 +3,7 @@ let nextClientId = 0;
|
|
|
3
3
|
const ConnectionQuerySchema = z.object({
|
|
4
4
|
clientId: z.string().trim().min(1),
|
|
5
5
|
connectionId: z.string().trim().min(1),
|
|
6
|
+
workspaceId: z.string().trim().optional(),
|
|
6
7
|
});
|
|
7
8
|
const PongBodySchema = ConnectionQuerySchema.extend({
|
|
8
9
|
pingTs: z.number().optional(),
|
|
@@ -20,7 +21,32 @@ export function registerEventRoutes(app, deps) {
|
|
|
20
21
|
reply.raw.setHeader("Connection", "keep-alive");
|
|
21
22
|
reply.raw.flushHeaders?.();
|
|
22
23
|
reply.hijack();
|
|
24
|
+
const filterWorkspaceId = connection.workspaceId;
|
|
25
|
+
const getEventWorkspaceId = (event) => {
|
|
26
|
+
switch (event.type) {
|
|
27
|
+
case "workspace.created":
|
|
28
|
+
case "workspace.started":
|
|
29
|
+
case "workspace.error":
|
|
30
|
+
return event.workspace?.id;
|
|
31
|
+
case "workspace.stopped":
|
|
32
|
+
return event.workspaceId;
|
|
33
|
+
case "workspace.log":
|
|
34
|
+
return event.entry.workspaceId;
|
|
35
|
+
case "instance.event":
|
|
36
|
+
case "instance.eventStatus":
|
|
37
|
+
case "instance.dataChanged":
|
|
38
|
+
return event.instanceId;
|
|
39
|
+
default:
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
23
43
|
const send = (event) => {
|
|
44
|
+
if (filterWorkspaceId) {
|
|
45
|
+
const eventWsId = getEventWorkspaceId(event);
|
|
46
|
+
if (eventWsId && eventWsId !== filterWorkspaceId) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
24
50
|
deps.logger.debug({ clientId, type: event.type }, "SSE event dispatched");
|
|
25
51
|
if (deps.logger.isLevelEnabled("trace")) {
|
|
26
52
|
deps.logger.trace({ clientId, event }, "SSE event payload");
|
|
@@ -10,10 +10,10 @@ function validateBinaryPath(binaryPath) {
|
|
|
10
10
|
}
|
|
11
11
|
export function registerSettingsRoutes(app, deps) {
|
|
12
12
|
// Full-document access
|
|
13
|
-
app.get("/api/storage/config", async () => sanitizeConfigDoc(deps.settings.getDoc("config")));
|
|
13
|
+
app.get("/api/storage/config", async () => sanitizeConfigDoc(await deps.settings.getDoc("config")));
|
|
14
14
|
app.patch("/api/storage/config", async (request, reply) => {
|
|
15
15
|
try {
|
|
16
|
-
return sanitizeConfigDoc(deps.settings.mergePatchDoc("config", request.body ?? {}));
|
|
16
|
+
return sanitizeConfigDoc(await deps.settings.mergePatchDoc("config", request.body ?? {}));
|
|
17
17
|
}
|
|
18
18
|
catch (error) {
|
|
19
19
|
reply.code(400);
|
|
@@ -21,11 +21,11 @@ export function registerSettingsRoutes(app, deps) {
|
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
23
|
app.get("/api/storage/config/:owner", async (request) => {
|
|
24
|
-
return sanitizeConfigOwner(request.params.owner, deps.settings.getOwner("config", request.params.owner));
|
|
24
|
+
return sanitizeConfigOwner(request.params.owner, await deps.settings.getOwner("config", request.params.owner));
|
|
25
25
|
});
|
|
26
26
|
app.patch("/api/storage/config/:owner", async (request, reply) => {
|
|
27
27
|
try {
|
|
28
|
-
return sanitizeConfigOwner(request.params.owner, deps.settings.mergePatchOwner("config", request.params.owner, request.body ?? {}));
|
|
28
|
+
return sanitizeConfigOwner(request.params.owner, await deps.settings.mergePatchOwner("config", request.params.owner, request.body ?? {}));
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
|
31
31
|
reply.code(400);
|
|
@@ -35,7 +35,7 @@ export function registerSettingsRoutes(app, deps) {
|
|
|
35
35
|
app.get("/api/storage/state", async () => deps.settings.getDoc("state"));
|
|
36
36
|
app.patch("/api/storage/state", async (request, reply) => {
|
|
37
37
|
try {
|
|
38
|
-
return deps.settings.mergePatchDoc("state", request.body ?? {});
|
|
38
|
+
return await deps.settings.mergePatchDoc("state", request.body ?? {});
|
|
39
39
|
}
|
|
40
40
|
catch (error) {
|
|
41
41
|
reply.code(400);
|
|
@@ -47,7 +47,7 @@ export function registerSettingsRoutes(app, deps) {
|
|
|
47
47
|
});
|
|
48
48
|
app.patch("/api/storage/state/:owner", async (request, reply) => {
|
|
49
49
|
try {
|
|
50
|
-
return deps.settings.mergePatchOwner("state", request.params.owner, request.body ?? {});
|
|
50
|
+
return await deps.settings.mergePatchOwner("state", request.params.owner, request.body ?? {});
|
|
51
51
|
}
|
|
52
52
|
catch (error) {
|
|
53
53
|
reply.code(400);
|
|
@@ -23,7 +23,7 @@ function getSpeechErrorMessage(error, fallback) {
|
|
|
23
23
|
return error instanceof Error ? error.message : fallback;
|
|
24
24
|
}
|
|
25
25
|
export function registerSpeechRoutes(app, deps) {
|
|
26
|
-
app.get("/api/speech/capabilities", async () => deps.speechService.getCapabilities());
|
|
26
|
+
app.get("/api/speech/capabilities", async () => await deps.speechService.getCapabilities());
|
|
27
27
|
app.post("/api/speech/transcribe", async (request, reply) => {
|
|
28
28
|
try {
|
|
29
29
|
const body = TranscribeBodySchema.parse(request.body ?? {});
|
|
@@ -77,7 +77,7 @@ export function registerWorkspaceRoutes(app, deps) {
|
|
|
77
77
|
app.get("/api/workspaces/:id/files/search", async (request, reply) => {
|
|
78
78
|
try {
|
|
79
79
|
const query = WorkspaceFileSearchQuerySchema.parse(request.query ?? {});
|
|
80
|
-
return deps.workspaceManager.searchFiles(request.params.id, query.q, {
|
|
80
|
+
return await deps.workspaceManager.searchFiles(request.params.id, query.q, {
|
|
81
81
|
limit: query.limit,
|
|
82
82
|
type: query.type,
|
|
83
83
|
refresh: query.refresh,
|
|
@@ -3,15 +3,15 @@ function prettyLabel(p) {
|
|
|
3
3
|
const last = parts[parts.length - 1] || p;
|
|
4
4
|
return last || p;
|
|
5
5
|
}
|
|
6
|
-
function readUiBinaries(settings) {
|
|
7
|
-
const ui = settings.getOwner("state", "ui");
|
|
6
|
+
async function readUiBinaries(settings) {
|
|
7
|
+
const ui = await settings.getOwner("state", "ui");
|
|
8
8
|
const list = ui?.opencodeBinaries;
|
|
9
9
|
if (!Array.isArray(list))
|
|
10
10
|
return [];
|
|
11
11
|
return list.filter((item) => item && typeof item === "object" && typeof item.path === "string");
|
|
12
12
|
}
|
|
13
|
-
function readDefaultBinaryPath(settings) {
|
|
14
|
-
const server = settings.getOwner("config", "server");
|
|
13
|
+
async function readDefaultBinaryPath(settings) {
|
|
14
|
+
const server = await settings.getOwner("config", "server");
|
|
15
15
|
const value = server?.opencodeBinary;
|
|
16
16
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
17
17
|
}
|
|
@@ -19,12 +19,12 @@ export class BinaryResolver {
|
|
|
19
19
|
constructor(settings) {
|
|
20
20
|
this.settings = settings;
|
|
21
21
|
}
|
|
22
|
-
list() {
|
|
22
|
+
async list() {
|
|
23
23
|
return readUiBinaries(this.settings);
|
|
24
24
|
}
|
|
25
|
-
resolveDefault() {
|
|
26
|
-
const binaries = this.list();
|
|
27
|
-
const configuredDefault = readDefaultBinaryPath(this.settings);
|
|
25
|
+
async resolveDefault() {
|
|
26
|
+
const binaries = await this.list();
|
|
27
|
+
const configuredDefault = await readDefaultBinaryPath(this.settings);
|
|
28
28
|
const fallback = binaries[0]?.path;
|
|
29
29
|
const path = configuredDefault ?? fallback ?? "opencode";
|
|
30
30
|
const entry = binaries.find((b) => b.path === path);
|
package/dist/settings/service.js
CHANGED
|
@@ -51,46 +51,46 @@ export class SettingsService {
|
|
|
51
51
|
this.configStore = new YamlDocStore(location.configYamlPath, logger.child({ component: "settings-config" }));
|
|
52
52
|
this.stateStore = new YamlDocStore(location.stateYamlPath, logger.child({ component: "settings-state" }));
|
|
53
53
|
}
|
|
54
|
-
getDoc(kind) {
|
|
54
|
+
async getDoc(kind) {
|
|
55
55
|
if (kind !== "config") {
|
|
56
56
|
return this.stateStore.get();
|
|
57
57
|
}
|
|
58
|
-
const current = this.configStore.get();
|
|
58
|
+
const current = await this.configStore.get();
|
|
59
59
|
const normalized = normalizeConfigDoc(current);
|
|
60
60
|
if (!isDeepEqual(current, normalized)) {
|
|
61
|
-
this.configStore.replace(normalized);
|
|
61
|
+
await this.configStore.replace(normalized);
|
|
62
62
|
}
|
|
63
63
|
return normalized;
|
|
64
64
|
}
|
|
65
|
-
mergePatchDoc(kind, patch) {
|
|
65
|
+
async mergePatchDoc(kind, patch) {
|
|
66
66
|
const updated = kind === "config"
|
|
67
|
-
? this.configStore.replace(normalizeConfigDoc(this.configStore.mergePatch(patch)))
|
|
68
|
-
: this.stateStore.mergePatch(patch);
|
|
69
|
-
this.publish(kind, "*");
|
|
67
|
+
? await this.configStore.replace(normalizeConfigDoc(await this.configStore.mergePatch(patch)))
|
|
68
|
+
: await this.stateStore.mergePatch(patch);
|
|
69
|
+
await this.publish(kind, "*");
|
|
70
70
|
return updated;
|
|
71
71
|
}
|
|
72
|
-
getOwner(kind, owner) {
|
|
72
|
+
async getOwner(kind, owner) {
|
|
73
73
|
if (kind !== "config") {
|
|
74
74
|
return this.stateStore.getOwner(owner);
|
|
75
75
|
}
|
|
76
76
|
return owner === "server"
|
|
77
|
-
? normalizeServerConfigOwner(this.getDoc("config").server)
|
|
78
|
-
: this.getDoc("config")[owner];
|
|
77
|
+
? normalizeServerConfigOwner((await this.getDoc("config")).server)
|
|
78
|
+
: (await this.getDoc("config"))[owner];
|
|
79
79
|
}
|
|
80
|
-
mergePatchOwner(kind, owner, patch) {
|
|
80
|
+
async mergePatchOwner(kind, owner, patch) {
|
|
81
81
|
const updated = kind === "config"
|
|
82
82
|
? owner === "server"
|
|
83
|
-
? this.configStore.replaceOwner(owner, normalizeServerConfigOwner(this.configStore.mergePatchOwner(owner, patch)))
|
|
84
|
-
: this.configStore.mergePatchOwner(owner, patch)
|
|
85
|
-
: this.stateStore.mergePatchOwner(owner, patch);
|
|
86
|
-
this.publish(kind, owner, updated);
|
|
83
|
+
? await this.configStore.replaceOwner(owner, normalizeServerConfigOwner(await this.configStore.mergePatchOwner(owner, patch)))
|
|
84
|
+
: await this.configStore.mergePatchOwner(owner, patch)
|
|
85
|
+
: await this.stateStore.mergePatchOwner(owner, patch);
|
|
86
|
+
await this.publish(kind, owner, updated);
|
|
87
87
|
return updated;
|
|
88
88
|
}
|
|
89
|
-
publish(kind, owner, value) {
|
|
89
|
+
async publish(kind, owner, value) {
|
|
90
90
|
if (!this.eventBus)
|
|
91
91
|
return;
|
|
92
92
|
const type = kind === "config" ? "storage.configChanged" : "storage.stateChanged";
|
|
93
|
-
const nextValue = value ?? this.getOwner(kind, owner);
|
|
93
|
+
const nextValue = value ?? await this.getOwner(kind, owner);
|
|
94
94
|
const payload = {
|
|
95
95
|
type,
|
|
96
96
|
owner,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
4
5
|
import { applyMergePatch, isPlainObject } from "./merge-patch";
|
|
@@ -20,17 +21,20 @@ export class YamlDocStore {
|
|
|
20
21
|
this.cache = {};
|
|
21
22
|
this.loaded = false;
|
|
22
23
|
}
|
|
23
|
-
load() {
|
|
24
|
+
async load() {
|
|
24
25
|
if (this.loaded) {
|
|
25
26
|
return this.cache;
|
|
26
27
|
}
|
|
27
28
|
try {
|
|
28
|
-
|
|
29
|
+
try {
|
|
30
|
+
await access(this.filePath, constants.F_OK);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
29
33
|
this.cache = {};
|
|
30
34
|
this.loaded = true;
|
|
31
35
|
return this.cache;
|
|
32
36
|
}
|
|
33
|
-
const content =
|
|
37
|
+
const content = await readFile(this.filePath, "utf-8");
|
|
34
38
|
const parsed = parseYaml(content);
|
|
35
39
|
this.cache = normalizeDoc(parsed);
|
|
36
40
|
this.loaded = true;
|
|
@@ -43,51 +47,51 @@ export class YamlDocStore {
|
|
|
43
47
|
return this.cache;
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
|
-
get() {
|
|
50
|
+
async get() {
|
|
47
51
|
return this.load();
|
|
48
52
|
}
|
|
49
|
-
replace(next) {
|
|
53
|
+
async replace(next) {
|
|
50
54
|
const normalized = normalizeDoc(next);
|
|
51
55
|
this.cache = normalized;
|
|
52
56
|
this.loaded = true;
|
|
53
|
-
this.persist();
|
|
57
|
+
await this.persist();
|
|
54
58
|
return this.cache;
|
|
55
59
|
}
|
|
56
|
-
mergePatch(patch) {
|
|
60
|
+
async mergePatch(patch) {
|
|
57
61
|
if (!isPlainObject(patch)) {
|
|
58
62
|
throw new Error("Patch must be a JSON object");
|
|
59
63
|
}
|
|
60
|
-
const current = this.get();
|
|
64
|
+
const current = await this.get();
|
|
61
65
|
const next = applyMergePatch(current, patch);
|
|
62
66
|
return this.replace(next);
|
|
63
67
|
}
|
|
64
|
-
getOwner(owner) {
|
|
65
|
-
const doc = this.get();
|
|
68
|
+
async getOwner(owner) {
|
|
69
|
+
const doc = await this.get();
|
|
66
70
|
const value = doc?.[owner];
|
|
67
71
|
return normalizeDoc(value);
|
|
68
72
|
}
|
|
69
|
-
replaceOwner(owner, value) {
|
|
70
|
-
const doc = this.get();
|
|
73
|
+
async replaceOwner(owner, value) {
|
|
74
|
+
const doc = await this.get();
|
|
71
75
|
const nextDoc = { ...doc, [owner]: normalizeDoc(value) };
|
|
72
|
-
this.replace(nextDoc);
|
|
76
|
+
await this.replace(nextDoc);
|
|
73
77
|
return nextDoc[owner];
|
|
74
78
|
}
|
|
75
|
-
mergePatchOwner(owner, patch) {
|
|
79
|
+
async mergePatchOwner(owner, patch) {
|
|
76
80
|
if (!isPlainObject(patch)) {
|
|
77
81
|
throw new Error("Patch must be a JSON object");
|
|
78
82
|
}
|
|
79
|
-
const doc = this.get();
|
|
83
|
+
const doc = await this.get();
|
|
80
84
|
const currentOwner = normalizeDoc(doc?.[owner]);
|
|
81
85
|
const nextOwner = normalizeDoc(applyMergePatch(currentOwner, patch));
|
|
82
86
|
const nextDoc = { ...doc, [owner]: nextOwner };
|
|
83
|
-
this.replace(nextDoc);
|
|
87
|
+
await this.replace(nextDoc);
|
|
84
88
|
return nextOwner;
|
|
85
89
|
}
|
|
86
|
-
persist() {
|
|
90
|
+
async persist() {
|
|
87
91
|
try {
|
|
88
|
-
|
|
92
|
+
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
89
93
|
const yaml = stringifyYaml(this.cache);
|
|
90
|
-
|
|
94
|
+
await writeFile(this.filePath, ensureTrailingNewline(yaml), "utf-8");
|
|
91
95
|
}
|
|
92
96
|
catch (error) {
|
|
93
97
|
this.logger.warn({ err: error, filePath: this.filePath }, "Failed to persist YAML doc");
|
package/dist/sidecars/manager.js
CHANGED
|
@@ -4,17 +4,21 @@ export class SideCarManager {
|
|
|
4
4
|
this.options = options;
|
|
5
5
|
this.configs = new Map();
|
|
6
6
|
this.runtime = new Map();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
}
|
|
8
|
+
static async create(options) {
|
|
9
|
+
const instance = new SideCarManager(options);
|
|
10
|
+
for (const record of await instance.loadConfiguredSideCars()) {
|
|
11
|
+
instance.configs.set(record.id, record);
|
|
12
|
+
instance.runtime.set(record.id, { status: "stopped" });
|
|
10
13
|
}
|
|
11
14
|
queueMicrotask(() => {
|
|
12
|
-
for (const record of
|
|
13
|
-
void
|
|
14
|
-
|
|
15
|
+
for (const record of instance.configs.values()) {
|
|
16
|
+
void instance.refreshPortSideCar(record.id).catch((error) => {
|
|
17
|
+
instance.options.logger.warn({ sidecarId: record.id, err: error }, "Failed to probe sidecar port");
|
|
15
18
|
});
|
|
16
19
|
}
|
|
17
20
|
});
|
|
21
|
+
return instance;
|
|
18
22
|
}
|
|
19
23
|
async list() {
|
|
20
24
|
await this.refreshPortStatuses();
|
|
@@ -45,7 +49,7 @@ export class SideCarManager {
|
|
|
45
49
|
};
|
|
46
50
|
this.configs.set(record.id, record);
|
|
47
51
|
this.runtime.set(record.id, { status: "stopped" });
|
|
48
|
-
this.persistConfigs();
|
|
52
|
+
await this.persistConfigs();
|
|
49
53
|
await this.refreshPortSideCar(record.id);
|
|
50
54
|
return this.toSideCar(record);
|
|
51
55
|
}
|
|
@@ -56,7 +60,7 @@ export class SideCarManager {
|
|
|
56
60
|
record.insecure = typeof input.insecure === "boolean" ? input.insecure : record.insecure;
|
|
57
61
|
record.prefixMode = typeof input.prefixMode === "string" ? input.prefixMode : record.prefixMode;
|
|
58
62
|
record.updatedAt = new Date().toISOString();
|
|
59
|
-
this.persistConfigs();
|
|
63
|
+
await this.persistConfigs();
|
|
60
64
|
await this.refreshPortSideCar(id);
|
|
61
65
|
return this.toSideCar(record);
|
|
62
66
|
}
|
|
@@ -66,7 +70,7 @@ export class SideCarManager {
|
|
|
66
70
|
return false;
|
|
67
71
|
this.configs.delete(id);
|
|
68
72
|
this.runtime.delete(id);
|
|
69
|
-
this.persistConfigs();
|
|
73
|
+
await this.persistConfigs();
|
|
70
74
|
this.options.eventBus.publish({ type: "sidecar.removed", sidecarId: id });
|
|
71
75
|
return true;
|
|
72
76
|
}
|
|
@@ -140,12 +144,12 @@ export class SideCarManager {
|
|
|
140
144
|
}
|
|
141
145
|
return record;
|
|
142
146
|
}
|
|
143
|
-
persistConfigs() {
|
|
147
|
+
async persistConfigs() {
|
|
144
148
|
const sidecars = Array.from(this.configs.values()).map((record) => ({ ...record }));
|
|
145
|
-
this.options.settings.mergePatchOwner("config", "server", { sidecars });
|
|
149
|
+
await this.options.settings.mergePatchOwner("config", "server", { sidecars });
|
|
146
150
|
}
|
|
147
|
-
loadConfiguredSideCars() {
|
|
148
|
-
const serverConfig = this.options.settings.getOwner("config", "server");
|
|
151
|
+
async loadConfiguredSideCars() {
|
|
152
|
+
const serverConfig = await this.options.settings.getOwner("config", "server");
|
|
149
153
|
const list = Array.isArray(serverConfig?.sidecars) ? serverConfig.sidecars : [];
|
|
150
154
|
const records = [];
|
|
151
155
|
for (const item of list) {
|