@dyyz1993/codenomad 0.15.1 → 0.15.3
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/background-processes/manager.js +18 -6
- 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 +1 -1
- 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/index.html +1 -1
- package/public/loading.html +1 -1
|
@@ -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
|
}
|
|
@@ -149,7 +149,7 @@ export class BackgroundProcessManager {
|
|
|
149
149
|
}
|
|
150
150
|
async readOutput(workspaceId, processId, options) {
|
|
151
151
|
const outputPath = this.getOutputPath(workspaceId, processId);
|
|
152
|
-
if (!existsSync(outputPath)) {
|
|
152
|
+
if (!outputPath || !existsSync(outputPath)) {
|
|
153
153
|
return { id: processId, content: "", truncated: false, sizeBytes: 0 };
|
|
154
154
|
}
|
|
155
155
|
const stats = await fs.stat(outputPath);
|
|
@@ -184,7 +184,7 @@ export class BackgroundProcessManager {
|
|
|
184
184
|
}
|
|
185
185
|
async streamOutput(workspaceId, processId, reply) {
|
|
186
186
|
const outputPath = this.getOutputPath(workspaceId, processId);
|
|
187
|
-
if (!existsSync(outputPath)) {
|
|
187
|
+
if (!outputPath || !existsSync(outputPath)) {
|
|
188
188
|
reply.code(404).send({ error: "Output not found" });
|
|
189
189
|
return;
|
|
190
190
|
}
|
|
@@ -356,6 +356,9 @@ export class BackgroundProcessManager {
|
|
|
356
356
|
}
|
|
357
357
|
async ensureProcessDir(workspaceId, processId) {
|
|
358
358
|
const root = await this.ensureWorkspaceDir(workspaceId);
|
|
359
|
+
if (!root) {
|
|
360
|
+
throw new Error("Workspace not found");
|
|
361
|
+
}
|
|
359
362
|
const processDir = path.join(root, processId);
|
|
360
363
|
await fs.mkdir(processDir, { recursive: true });
|
|
361
364
|
return processDir;
|
|
@@ -363,7 +366,7 @@ export class BackgroundProcessManager {
|
|
|
363
366
|
async ensureWorkspaceDir(workspaceId) {
|
|
364
367
|
const workspace = this.deps.workspaceManager.get(workspaceId);
|
|
365
368
|
if (!workspace) {
|
|
366
|
-
|
|
369
|
+
return null;
|
|
367
370
|
}
|
|
368
371
|
const root = path.join(workspace.path, ROOT_DIR, workspaceId);
|
|
369
372
|
await fs.mkdir(root, { recursive: true });
|
|
@@ -372,7 +375,7 @@ export class BackgroundProcessManager {
|
|
|
372
375
|
getOutputPath(workspaceId, processId) {
|
|
373
376
|
const workspace = this.deps.workspaceManager.get(workspaceId);
|
|
374
377
|
if (!workspace) {
|
|
375
|
-
|
|
378
|
+
return null;
|
|
376
379
|
}
|
|
377
380
|
return path.join(workspace.path, ROOT_DIR, workspaceId, processId, OUTPUT_FILE);
|
|
378
381
|
}
|
|
@@ -382,6 +385,8 @@ export class BackgroundProcessManager {
|
|
|
382
385
|
}
|
|
383
386
|
async readIndex(workspaceId) {
|
|
384
387
|
const indexPath = await this.getIndexPath(workspaceId);
|
|
388
|
+
if (!indexPath)
|
|
389
|
+
return [];
|
|
385
390
|
if (!existsSync(indexPath))
|
|
386
391
|
return [];
|
|
387
392
|
try {
|
|
@@ -411,13 +416,15 @@ export class BackgroundProcessManager {
|
|
|
411
416
|
}
|
|
412
417
|
async writeIndex(workspaceId, records) {
|
|
413
418
|
const indexPath = await this.getIndexPath(workspaceId);
|
|
419
|
+
if (!indexPath)
|
|
420
|
+
return;
|
|
414
421
|
await fs.mkdir(path.dirname(indexPath), { recursive: true });
|
|
415
422
|
await fs.writeFile(indexPath, JSON.stringify(records, null, 2));
|
|
416
423
|
}
|
|
417
424
|
async getIndexPath(workspaceId) {
|
|
418
425
|
const workspace = this.deps.workspaceManager.get(workspaceId);
|
|
419
426
|
if (!workspace) {
|
|
420
|
-
|
|
427
|
+
return null;
|
|
421
428
|
}
|
|
422
429
|
return path.join(workspace.path, ROOT_DIR, workspaceId, INDEX_FILE);
|
|
423
430
|
}
|
|
@@ -439,7 +446,7 @@ export class BackgroundProcessManager {
|
|
|
439
446
|
}
|
|
440
447
|
async getOutputSize(workspaceId, processId) {
|
|
441
448
|
const outputPath = this.getOutputPath(workspaceId, processId);
|
|
442
|
-
if (!existsSync(outputPath)) {
|
|
449
|
+
if (!outputPath || !existsSync(outputPath)) {
|
|
443
450
|
return 0;
|
|
444
451
|
}
|
|
445
452
|
try {
|
|
@@ -475,6 +482,11 @@ export class BackgroundProcessManager {
|
|
|
475
482
|
};
|
|
476
483
|
}
|
|
477
484
|
async finalizeRecord(workspaceId, record, completion) {
|
|
485
|
+
if (!this.deps.workspaceManager.get(workspaceId)) {
|
|
486
|
+
this.deps.logger.debug({ workspaceId, processId: record.id }, "Skipping finalizeRecord: workspace already removed");
|
|
487
|
+
this.running.delete(record.id);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
478
490
|
if (this.shouldSendCompletionPrompt(record, completion)) {
|
|
479
491
|
try {
|
|
480
492
|
await this.sendCompletionPrompt(workspaceId, record);
|
|
@@ -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
|
@@ -232,7 +232,7 @@ async function main() {
|
|
|
232
232
|
});
|
|
233
233
|
const instanceStore = new InstanceStore(configLocation.instancesDir);
|
|
234
234
|
const speechService = new SpeechService(settings, logger.child({ component: "speech" }));
|
|
235
|
-
const sidecarManager =
|
|
235
|
+
const sidecarManager = await SideCarManager.create({
|
|
236
236
|
settings,
|
|
237
237
|
eventBus,
|
|
238
238
|
logger: logger.child({ component: "sidecars" }),
|
|
@@ -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) {
|
package/dist/speech/service.js
CHANGED
|
@@ -23,27 +23,27 @@ export class SpeechService {
|
|
|
23
23
|
this.settings = settings;
|
|
24
24
|
this.logger = logger;
|
|
25
25
|
}
|
|
26
|
-
getCapabilities() {
|
|
27
|
-
return this.createProvider().getCapabilities();
|
|
26
|
+
async getCapabilities() {
|
|
27
|
+
return (await this.createProvider()).getCapabilities();
|
|
28
28
|
}
|
|
29
29
|
async transcribe(input) {
|
|
30
|
-
return this.createProvider().transcribe(input);
|
|
30
|
+
return (await this.createProvider()).transcribe(input);
|
|
31
31
|
}
|
|
32
32
|
async synthesize(input) {
|
|
33
|
-
return this.createProvider().synthesize(input);
|
|
33
|
+
return (await this.createProvider()).synthesize(input);
|
|
34
34
|
}
|
|
35
35
|
async synthesizeStream(input) {
|
|
36
|
-
return this.createProvider().synthesizeStream(input);
|
|
36
|
+
return (await this.createProvider()).synthesizeStream(input);
|
|
37
37
|
}
|
|
38
|
-
createProvider() {
|
|
39
|
-
const settings = this.resolveSettings();
|
|
38
|
+
async createProvider() {
|
|
39
|
+
const settings = await this.resolveSettings();
|
|
40
40
|
return new OpenAICompatibleSpeechProvider({
|
|
41
41
|
settings,
|
|
42
42
|
logger: this.logger.child({ provider: settings.provider }),
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
-
resolveSettings() {
|
|
46
|
-
const parsed = ServerSpeechSettingsSchema.parse(this.settings.getOwner("config", "server") ?? {});
|
|
45
|
+
async resolveSettings() {
|
|
46
|
+
const parsed = ServerSpeechSettingsSchema.parse(await this.settings.getOwner("config", "server") ?? {});
|
|
47
47
|
const speech = parsed.speech ?? {};
|
|
48
48
|
return {
|
|
49
49
|
provider: speech.provider?.trim() || DEFAULT_PROVIDER,
|
|
@@ -3,10 +3,12 @@ import { Agent as UndiciAgent } from "undici";
|
|
|
3
3
|
const INSTANCE_HOST = "127.0.0.1";
|
|
4
4
|
const STREAM_AGENT = new UndiciAgent({ bodyTimeout: 0, headersTimeout: 0 });
|
|
5
5
|
const RECONNECT_DELAY_MS = 1000;
|
|
6
|
+
const LOG_THROTTLE_MS = 50;
|
|
6
7
|
export class InstanceEventBridge {
|
|
7
8
|
constructor(options) {
|
|
8
9
|
this.options = options;
|
|
9
10
|
this.streams = new Map();
|
|
11
|
+
this.lastLogTime = new Map();
|
|
10
12
|
const bus = this.options.eventBus;
|
|
11
13
|
bus.on("workspace.started", (event) => this.startStream(event.workspace.id));
|
|
12
14
|
bus.on("workspace.stopped", (event) => this.stopStream(event.workspaceId, "workspace stopped"));
|
|
@@ -46,6 +48,7 @@ export class InstanceEventBridge {
|
|
|
46
48
|
}
|
|
47
49
|
active.controller.abort();
|
|
48
50
|
this.streams.delete(workspaceId);
|
|
51
|
+
this.lastLogTime.delete(workspaceId);
|
|
49
52
|
this.publishStatus(workspaceId, "disconnected", reason);
|
|
50
53
|
}
|
|
51
54
|
async runStream(workspaceId, signal) {
|
|
@@ -151,6 +154,14 @@ export class InstanceEventBridge {
|
|
|
151
154
|
if (this.options.logger.isLevelEnabled("trace")) {
|
|
152
155
|
this.options.logger.trace({ workspaceId, event }, "Instance SSE event payload");
|
|
153
156
|
}
|
|
157
|
+
if (event.type === "workspace.log") {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
const last = this.lastLogTime.get(workspaceId) ?? 0;
|
|
160
|
+
if (now - last < LOG_THROTTLE_MS) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.lastLogTime.set(workspaceId, now);
|
|
164
|
+
}
|
|
154
165
|
this.options.eventBus.publish({ type: "instance.event", instanceId: workspaceId, event });
|
|
155
166
|
}
|
|
156
167
|
catch (error) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import {
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
3
4
|
import { connect } from "net";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
4
6
|
import { FileSystemBrowser } from "../filesystem/browser";
|
|
5
7
|
import { searchWorkspaceFiles } from "../filesystem/search";
|
|
6
8
|
import { clearWorkspaceSearchCache } from "../filesystem/search-cache";
|
|
@@ -9,10 +11,30 @@ import { getOpencodeConfigDir } from "../opencode-config.js";
|
|
|
9
11
|
import { OPENCODE_SERVER_BASE_URL_ENV, buildOpencodeBasicAuthHeader, OPENCODE_SERVER_PASSWORD_ENV, OPENCODE_SERVER_USERNAME_ENV, resolveOpencodeServerAuth, } from "./opencode-auth";
|
|
10
12
|
const STARTUP_STABILITY_DELAY_MS = 1500;
|
|
11
13
|
export class WorkspaceManager {
|
|
14
|
+
async acquireStartupSlot() {
|
|
15
|
+
if (this.activeStartups < this.MAX_CONCURRENT_STARTUPS) {
|
|
16
|
+
this.activeStartups++;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
this.startupQueue.push(resolve);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
releaseStartupSlot() {
|
|
24
|
+
this.activeStartups--;
|
|
25
|
+
const next = this.startupQueue.shift();
|
|
26
|
+
if (next) {
|
|
27
|
+
this.activeStartups++;
|
|
28
|
+
next();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
constructor(options) {
|
|
13
32
|
this.options = options;
|
|
14
33
|
this.workspaces = new Map();
|
|
15
34
|
this.opencodeAuth = new Map();
|
|
35
|
+
this.MAX_CONCURRENT_STARTUPS = 3;
|
|
36
|
+
this.activeStartups = 0;
|
|
37
|
+
this.startupQueue = [];
|
|
16
38
|
this.runtime = new WorkspaceRuntime(this.options.eventBus, this.options.logger);
|
|
17
39
|
this.opencodeConfigDir = getOpencodeConfigDir();
|
|
18
40
|
}
|
|
@@ -33,7 +55,7 @@ export class WorkspaceManager {
|
|
|
33
55
|
const browser = new FileSystemBrowser({ rootDir: workspace.path });
|
|
34
56
|
return browser.list(relativePath);
|
|
35
57
|
}
|
|
36
|
-
searchFiles(workspaceId, query, options) {
|
|
58
|
+
async searchFiles(workspaceId, query, options) {
|
|
37
59
|
const workspace = this.requireWorkspace(workspaceId);
|
|
38
60
|
return searchWorkspaceFiles(workspace.path, query, options);
|
|
39
61
|
}
|
|
@@ -53,9 +75,10 @@ export class WorkspaceManager {
|
|
|
53
75
|
browser.writeFile(relativePath, contents);
|
|
54
76
|
}
|
|
55
77
|
async create(folder, name) {
|
|
78
|
+
await this.acquireStartupSlot();
|
|
56
79
|
const id = `${Date.now().toString(36)}`;
|
|
57
|
-
const binary = this.options.binaryResolver.resolveDefault();
|
|
58
|
-
const resolvedBinaryPath = this.resolveBinaryPath(binary.path);
|
|
80
|
+
const binary = await this.options.binaryResolver.resolveDefault();
|
|
81
|
+
const resolvedBinaryPath = await this.resolveBinaryPath(binary.path);
|
|
59
82
|
const workspacePath = path.isAbsolute(folder) ? folder : path.resolve(this.options.rootDir, folder);
|
|
60
83
|
clearWorkspaceSearchCache(workspacePath);
|
|
61
84
|
this.options.logger.info({ workspaceId: id, folder: workspacePath, binary: resolvedBinaryPath }, "Creating workspace");
|
|
@@ -74,7 +97,7 @@ export class WorkspaceManager {
|
|
|
74
97
|
};
|
|
75
98
|
this.workspaces.set(id, descriptor);
|
|
76
99
|
this.options.eventBus.publish({ type: "workspace.created", workspace: descriptor });
|
|
77
|
-
const serverConfig = this.options.settings.getOwner("config", "server");
|
|
100
|
+
const serverConfig = await this.options.settings.getOwner("config", "server");
|
|
78
101
|
const envVars = serverConfig?.environmentVariables;
|
|
79
102
|
const userEnvironment = envVars && typeof envVars === "object" && !Array.isArray(envVars) ? envVars : {};
|
|
80
103
|
const serverBaseUrl = this.options.getServerBaseUrl();
|
|
@@ -128,6 +151,9 @@ export class WorkspaceManager {
|
|
|
128
151
|
this.options.logger.error({ workspaceId: id, err: error }, "Workspace failed to start");
|
|
129
152
|
throw error;
|
|
130
153
|
}
|
|
154
|
+
finally {
|
|
155
|
+
this.releaseStartupSlot();
|
|
156
|
+
}
|
|
131
157
|
}
|
|
132
158
|
async delete(id) {
|
|
133
159
|
const workspace = this.workspaces.get(id);
|
|
@@ -175,7 +201,7 @@ export class WorkspaceManager {
|
|
|
175
201
|
}
|
|
176
202
|
return workspace;
|
|
177
203
|
}
|
|
178
|
-
resolveBinaryPath(identifier) {
|
|
204
|
+
async resolveBinaryPath(identifier) {
|
|
179
205
|
if (!identifier) {
|
|
180
206
|
return identifier;
|
|
181
207
|
}
|
|
@@ -185,9 +211,23 @@ export class WorkspaceManager {
|
|
|
185
211
|
}
|
|
186
212
|
const locator = process.platform === "win32" ? "where" : "which";
|
|
187
213
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
214
|
+
let stdout;
|
|
215
|
+
let stderr;
|
|
216
|
+
try {
|
|
217
|
+
const result = await execFileAsync(locator, [identifier], { encoding: "utf8" });
|
|
218
|
+
stdout = result.stdout;
|
|
219
|
+
stderr = result.stderr;
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
stdout = err?.stdout ?? "";
|
|
223
|
+
stderr = err?.stderr ?? "";
|
|
224
|
+
if (!stdout && err?.code === "ENOENT") {
|
|
225
|
+
this.options.logger.warn({ identifier, err }, "Failed to resolve binary path via locator command");
|
|
226
|
+
return identifier;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (stdout) {
|
|
230
|
+
const candidates = stdout
|
|
191
231
|
.split(/\r?\n/)
|
|
192
232
|
.map((line) => line.trim())
|
|
193
233
|
.filter((line) => line.length > 0)
|
|
@@ -198,8 +238,8 @@ export class WorkspaceManager {
|
|
|
198
238
|
return resolved;
|
|
199
239
|
}
|
|
200
240
|
}
|
|
201
|
-
|
|
202
|
-
this.options.logger.warn({ identifier,
|
|
241
|
+
if (stderr) {
|
|
242
|
+
this.options.logger.warn({ identifier, stderr }, "Locator command reported errors");
|
|
203
243
|
}
|
|
204
244
|
}
|
|
205
245
|
catch (error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { access, stat } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
3
4
|
import path from "path";
|
|
4
5
|
import { buildSpawnSpec, buildWslSignalSpec } from "./spawn";
|
|
5
6
|
const SENSITIVE_ENV_KEY = /(PASSWORD|TOKEN|SECRET)/i;
|
|
@@ -22,7 +23,7 @@ export class WorkspaceRuntime {
|
|
|
22
23
|
this.processes = new Map();
|
|
23
24
|
}
|
|
24
25
|
async launch(options) {
|
|
25
|
-
this.validateFolder(options.folder);
|
|
26
|
+
await this.validateFolder(options.folder);
|
|
26
27
|
const logLevel = typeof options.logLevel === "string" ? options.logLevel.toUpperCase() : "DEBUG";
|
|
27
28
|
const args = ["serve", "--port", "0", "--print-logs", "--log-level", logLevel];
|
|
28
29
|
const env = { ...process.env, ...(options.environment ?? {}) };
|
|
@@ -353,12 +354,15 @@ export class WorkspaceRuntime {
|
|
|
353
354
|
};
|
|
354
355
|
this.eventBus.publish({ type: "workspace.log", entry });
|
|
355
356
|
}
|
|
356
|
-
validateFolder(folder) {
|
|
357
|
+
async validateFolder(folder) {
|
|
357
358
|
const resolved = path.resolve(folder);
|
|
358
|
-
|
|
359
|
+
try {
|
|
360
|
+
await access(resolved, constants.F_OK);
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
359
363
|
throw new Error(`Folder does not exist: ${resolved}`);
|
|
360
364
|
}
|
|
361
|
-
const stats =
|
|
365
|
+
const stats = await stat(resolved);
|
|
362
366
|
if (!stats.isDirectory()) {
|
|
363
367
|
throw new Error(`Path is not a directory: ${resolved}`);
|
|
364
368
|
}
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
<link rel="modulepreload" crossorigin href="/assets/git-diff-vendor-CSgooKT_.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/monaco-viewer-YY9yfE-p.js">
|
|
32
32
|
<link rel="modulepreload" crossorigin href="/assets/index-BCGPLzO4.js">
|
|
33
|
-
<link rel="stylesheet" crossorigin href="/assets/git-diff-vendor-HAZkIolJ.css">
|
|
34
33
|
<link rel="stylesheet" crossorigin href="/assets/index-DXI9DoVB.css">
|
|
34
|
+
<link rel="stylesheet" crossorigin href="/assets/git-diff-vendor-HAZkIolJ.css">
|
|
35
35
|
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script>
|
|
36
36
|
<meta name="theme-color" content="#1a1a1a">
|
|
37
37
|
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
package/public/loading.html
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
<link rel="modulepreload" crossorigin href="/assets/git-diff-vendor-CSgooKT_.js">
|
|
21
21
|
<link rel="modulepreload" crossorigin href="/assets/monaco-viewer-YY9yfE-p.js">
|
|
22
22
|
<link rel="modulepreload" crossorigin href="/assets/index-BCGPLzO4.js">
|
|
23
|
-
<link rel="stylesheet" crossorigin href="/assets/index-DXI9DoVB.css">
|
|
24
23
|
<link rel="stylesheet" crossorigin href="/assets/git-diff-vendor-HAZkIolJ.css">
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DXI9DoVB.css">
|
|
25
25
|
<link rel="stylesheet" crossorigin href="/assets/loading-CmEVQgyj.css">
|
|
26
26
|
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script>
|
|
27
27
|
<meta name="theme-color" content="#1a1a1a">
|