@openspecui/server 3.6.1 → 3.7.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/index.mjs +501 -83
- package/package.json +7 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, GitConfigSchema,
|
|
2
|
-
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
1
|
+
import { CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, GlobalSettingsManager, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecUIGlobalSettingsSchema, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheWriteInputSchema, buildBackendHealthPayload, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getToolInitStates, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
|
|
2
|
+
import { basename, dirname, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { access, mkdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { createServer as createServer$1 } from "node:net";
|
|
@@ -12,7 +12,7 @@ import { readFileSync } from "node:fs";
|
|
|
12
12
|
import { WebSocketServer } from "ws";
|
|
13
13
|
import { CustomSoundHashSchema as CustomSoundHashSchema$1, CustomSoundIdSchema, CustomSoundMetadataFileSchema, customHashFromSoundId, soundIdFromCustomHash } from "@openspecui/core/sounds";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
|
-
import { homedir } from "node:os";
|
|
15
|
+
import { homedir, platform } from "node:os";
|
|
16
16
|
import { EventEmitter } from "node:events";
|
|
17
17
|
import { execFile } from "node:child_process";
|
|
18
18
|
import { promisify } from "node:util";
|
|
@@ -35,6 +35,9 @@ function toErrorDiagnostic$1(error) {
|
|
|
35
35
|
function isNotNull(value) {
|
|
36
36
|
return value !== null;
|
|
37
37
|
}
|
|
38
|
+
function normalizeChangeFilePath(path) {
|
|
39
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
40
|
+
}
|
|
38
41
|
var DocumentService = class {
|
|
39
42
|
parser = new MarkdownParser();
|
|
40
43
|
constructor(projectDir, adapter, hookRuntime) {
|
|
@@ -176,37 +179,125 @@ var DocumentService = class {
|
|
|
176
179
|
return null;
|
|
177
180
|
}
|
|
178
181
|
}
|
|
182
|
+
async readEntityDetail(stage, changeId, consumer = "view", mode = "processed", options = {}) {
|
|
183
|
+
const detail = await this.adapter.readEntityDetail(stage, changeId, options);
|
|
184
|
+
if (!detail) return null;
|
|
185
|
+
const root = stage === "change" ? `openspec/changes/${changeId}` : `openspec/changes/archive/${changeId}`;
|
|
186
|
+
const processedByPath = /* @__PURE__ */ new Map();
|
|
187
|
+
const processArtifactFile = async (artifact, file) => {
|
|
188
|
+
const artifactFile = {
|
|
189
|
+
...await this.processEntityFile({
|
|
190
|
+
stage,
|
|
191
|
+
changeId,
|
|
192
|
+
root,
|
|
193
|
+
file,
|
|
194
|
+
consumer,
|
|
195
|
+
mode,
|
|
196
|
+
schemaName: detail.schemaName,
|
|
197
|
+
artifactId: artifact.id,
|
|
198
|
+
artifactOutputPath: artifact.outputPath
|
|
199
|
+
}),
|
|
200
|
+
type: "file"
|
|
201
|
+
};
|
|
202
|
+
processedByPath.set(file.path, artifactFile);
|
|
203
|
+
return artifactFile;
|
|
204
|
+
};
|
|
205
|
+
const artifacts = await Promise.all(detail.artifacts.map(async (artifact) => ({
|
|
206
|
+
...artifact,
|
|
207
|
+
files: await Promise.all(artifact.files.map((file) => processArtifactFile(artifact, file)))
|
|
208
|
+
})));
|
|
209
|
+
const files = await Promise.all(detail.files.map(async (file) => {
|
|
210
|
+
const processed = processedByPath.get(file.path);
|
|
211
|
+
if (processed) return processed;
|
|
212
|
+
return this.processEntityFile({
|
|
213
|
+
stage,
|
|
214
|
+
changeId,
|
|
215
|
+
root,
|
|
216
|
+
file,
|
|
217
|
+
consumer,
|
|
218
|
+
mode,
|
|
219
|
+
schemaName: detail.schemaName
|
|
220
|
+
});
|
|
221
|
+
}));
|
|
222
|
+
const filesByPath = new Map(files.map((file) => [file.path, file]));
|
|
223
|
+
const ungroupedFiles = detail.ungroupedFiles.map((file) => filesByPath.get(file.path) ?? file);
|
|
224
|
+
return {
|
|
225
|
+
...detail,
|
|
226
|
+
files,
|
|
227
|
+
artifacts,
|
|
228
|
+
ungroupedFiles
|
|
229
|
+
};
|
|
230
|
+
}
|
|
179
231
|
async readChangeFiles(changeId, consumer = "view", mode = "processed") {
|
|
180
232
|
const files = await this.adapter.readChangeFiles(changeId);
|
|
181
233
|
return this.processChangeFiles("change", changeId, files, consumer, mode);
|
|
182
234
|
}
|
|
235
|
+
async readChangeArtifactOutput(changeId, outputPath, consumer = "view", mode = "processed") {
|
|
236
|
+
const normalizedPath = normalizeChangeFilePath(outputPath);
|
|
237
|
+
return (await this.readChangeArtifactFiles(changeId, normalizedPath, consumer, mode)).find((file) => file.path === normalizedPath)?.content ?? null;
|
|
238
|
+
}
|
|
239
|
+
async readChangeGlobArtifactFiles(changeId, outputPath, consumer = "view", mode = "processed") {
|
|
240
|
+
const normalizedPattern = normalizeChangeFilePath(outputPath);
|
|
241
|
+
return this.readChangeArtifactFiles(changeId, normalizedPattern, consumer, mode);
|
|
242
|
+
}
|
|
183
243
|
async readArchivedChangeFiles(changeId, consumer = "view", mode = "processed") {
|
|
184
244
|
const files = await this.adapter.readArchivedChangeFiles(changeId);
|
|
185
245
|
return this.processChangeFiles("archive", changeId, files, consumer, mode);
|
|
186
246
|
}
|
|
187
247
|
async processChangeFiles(stage, changeId, files, consumer, mode) {
|
|
188
248
|
const root = stage === "change" ? `openspec/changes/${changeId}` : `openspec/changes/archive/${changeId}`;
|
|
189
|
-
return (await Promise.all(files.map(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
249
|
+
return (await Promise.all(files.map((file) => this.processChangeFile(stage, changeId, root, file, consumer, mode)))).filter(isNotNull);
|
|
250
|
+
}
|
|
251
|
+
async readChangeArtifactFiles(changeId, outputPath, consumer, mode) {
|
|
252
|
+
const matchingFiles = (await this.adapter.readChangeFiles(changeId)).filter((file) => {
|
|
253
|
+
if (file.type !== "file" || file.content === void 0) return false;
|
|
254
|
+
return matchesGlob(file.path, outputPath) || file.path === outputPath;
|
|
255
|
+
});
|
|
256
|
+
const root = `openspec/changes/${changeId}`;
|
|
257
|
+
return (await Promise.all(matchingFiles.map((file) => this.processChangeFile("change", changeId, root, file, consumer, mode)))).filter(isNotNull).filter((file) => file.type === "file" && file.content !== void 0);
|
|
258
|
+
}
|
|
259
|
+
async processChangeFile(stage, changeId, root, file, consumer, mode) {
|
|
260
|
+
if (file.type !== "file" || file.content === void 0 || !file.path.endsWith(".md")) return file;
|
|
261
|
+
const kind = this.inferChangeFileKind(file.path);
|
|
262
|
+
if (!kind) return file;
|
|
263
|
+
const result = await this.processDocument({
|
|
264
|
+
consumer,
|
|
265
|
+
mode,
|
|
266
|
+
document: {
|
|
267
|
+
stage,
|
|
268
|
+
kind,
|
|
269
|
+
changeId,
|
|
270
|
+
relativePath: `${root}/${file.path}`,
|
|
271
|
+
absolutePath: join(this.projectDir, root, file.path)
|
|
272
|
+
},
|
|
273
|
+
source: file.content
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
...file,
|
|
277
|
+
content: result.markdown
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async processEntityFile(input) {
|
|
281
|
+
if (input.file.type !== "file" || input.file.content === void 0 || !input.file.path.endsWith(".md")) return input.file;
|
|
282
|
+
const result = await this.processDocument({
|
|
283
|
+
consumer: input.consumer,
|
|
284
|
+
mode: input.mode,
|
|
285
|
+
document: {
|
|
286
|
+
stage: input.stage,
|
|
287
|
+
kind: "artifact",
|
|
288
|
+
changeId: input.changeId,
|
|
289
|
+
schemaName: input.schemaName,
|
|
290
|
+
artifactId: input.artifactId,
|
|
291
|
+
artifactOutputPath: input.artifactOutputPath,
|
|
292
|
+
relativePath: `${input.root}/${input.file.path}`,
|
|
293
|
+
absolutePath: join(this.projectDir, input.root, input.file.path)
|
|
294
|
+
},
|
|
295
|
+
source: input.file.content
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
...input.file,
|
|
299
|
+
content: result.markdown
|
|
300
|
+
};
|
|
210
301
|
}
|
|
211
302
|
inferChangeFileKind(path) {
|
|
212
303
|
if (path === "proposal.md") return "proposal";
|
|
@@ -1194,16 +1285,11 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
1194
1285
|
updatedAt: changeMeta.updatedAt
|
|
1195
1286
|
}));
|
|
1196
1287
|
const activeChanges = selectRecentDashboardItems(allActiveChanges);
|
|
1197
|
-
const archivedChanges =
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
createdAt: meta.createdAt,
|
|
1203
|
-
updatedAt: meta.updatedAt,
|
|
1204
|
-
tasksCompleted: change.tasks.filter((task) => task.completed).length
|
|
1205
|
-
};
|
|
1206
|
-
}))).filter((item) => item !== null);
|
|
1288
|
+
const archivedChanges = archiveMetas.map((meta) => ({
|
|
1289
|
+
id: meta.id,
|
|
1290
|
+
createdAt: meta.createdAt,
|
|
1291
|
+
updatedAt: meta.updatedAt
|
|
1292
|
+
}));
|
|
1207
1293
|
const allSpecifications = (await Promise.all(specMetas.map(async (meta) => {
|
|
1208
1294
|
const spec = await ctx.adapter.readSpec(meta.id);
|
|
1209
1295
|
if (!spec) return null;
|
|
@@ -1218,7 +1304,7 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
1218
1304
|
const requirements = allSpecifications.reduce((sum, spec) => sum + spec.requirements, 0);
|
|
1219
1305
|
const tasksTotal = allActiveChanges.reduce((sum, change) => sum + change.progress.total, 0);
|
|
1220
1306
|
const tasksCompleted = allActiveChanges.reduce((sum, change) => sum + change.progress.completed, 0);
|
|
1221
|
-
const archivedTasksCompleted =
|
|
1307
|
+
const archivedTasksCompleted = 0;
|
|
1222
1308
|
const taskCompletionPercent = tasksTotal > 0 ? Math.round(tasksCompleted / tasksTotal * 100) : null;
|
|
1223
1309
|
const inProgressChanges = allActiveChanges.filter((change) => change.progress.total > 0 && change.progress.completed < change.progress.total).length;
|
|
1224
1310
|
const specificationTrendEvents = specMetas.flatMap((spec) => {
|
|
@@ -1232,7 +1318,7 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
1232
1318
|
const ts = parseDatedIdTimestamp(archive.id) ?? resolveTrendTimestamp(archive.updatedAt, archive.createdAt);
|
|
1233
1319
|
return ts === null ? [] : [{
|
|
1234
1320
|
ts,
|
|
1235
|
-
value:
|
|
1321
|
+
value: 1
|
|
1236
1322
|
}];
|
|
1237
1323
|
});
|
|
1238
1324
|
const specMetaById = new Map(specMetas.map((meta) => [meta.id, meta]));
|
|
@@ -1339,6 +1425,29 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
1339
1425
|
};
|
|
1340
1426
|
}
|
|
1341
1427
|
|
|
1428
|
+
//#endregion
|
|
1429
|
+
//#region src/entity-read-options.ts
|
|
1430
|
+
async function readEntityMetadata(ctx, stage, id) {
|
|
1431
|
+
return (stage === "change" ? await ctx.adapter.readChangeFiles(id) : await ctx.adapter.readArchivedChangeFiles(id)).find((file) => file.type === "file" && file.path === ".openspec.yaml")?.content ?? null;
|
|
1432
|
+
}
|
|
1433
|
+
async function buildEntityReadOptions(ctx, stage, id) {
|
|
1434
|
+
const schemaName = parseOpsxEntityMetadata(await readEntityMetadata(ctx, stage, id)).schemaName;
|
|
1435
|
+
if (!schemaName) return {};
|
|
1436
|
+
try {
|
|
1437
|
+
await ctx.kernel.waitForWarmup();
|
|
1438
|
+
await ctx.kernel.ensureSchemaDetail(schemaName);
|
|
1439
|
+
await ctx.kernel.ensureSchemaYaml(schemaName);
|
|
1440
|
+
const schemaYaml = ctx.kernel.getSchemaYaml(schemaName);
|
|
1441
|
+
const diagnostics = schemaYaml ? parseOpsxSchemaDetail(schemaYaml, schemaName, { path: `schema:${schemaName}` }).diagnostics : [];
|
|
1442
|
+
return {
|
|
1443
|
+
schemas: { [schemaName]: ctx.kernel.getSchemaDetail(schemaName) },
|
|
1444
|
+
schemaDiagnostics: diagnostics.length > 0 ? { [schemaName]: diagnostics } : void 0
|
|
1445
|
+
};
|
|
1446
|
+
} catch {
|
|
1447
|
+
return { schemas: {} };
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1342
1451
|
//#endregion
|
|
1343
1452
|
//#region src/notification-service.ts
|
|
1344
1453
|
var NotificationService = class {
|
|
@@ -1619,8 +1728,8 @@ function detectPtyPlatform() {
|
|
|
1619
1728
|
if (process.platform === "darwin") return "macos";
|
|
1620
1729
|
return "common";
|
|
1621
1730
|
}
|
|
1622
|
-
function resolveDefaultShell(platform, env) {
|
|
1623
|
-
if (platform === "windows") return env.ComSpec?.trim() || "cmd.exe";
|
|
1731
|
+
function resolveDefaultShell(platform$1, env) {
|
|
1732
|
+
if (platform$1 === "windows") return env.ComSpec?.trim() || "cmd.exe";
|
|
1624
1733
|
return env.SHELL?.trim() || "/bin/sh";
|
|
1625
1734
|
}
|
|
1626
1735
|
function resolvePtyShellDefaults(opts) {
|
|
@@ -2724,6 +2833,20 @@ async function buildGitWorktreeOverview(options) {
|
|
|
2724
2833
|
};
|
|
2725
2834
|
});
|
|
2726
2835
|
}
|
|
2836
|
+
async function resolveGitWorktreeSwitchTarget(options) {
|
|
2837
|
+
const resolvedProjectDir = resolve(options.projectDir);
|
|
2838
|
+
const resolvedInputPath = resolve(options.targetPath);
|
|
2839
|
+
const worktrees = await listGitWorktrees(resolvedProjectDir, options.runGit ?? defaultRunGit);
|
|
2840
|
+
for (const worktree of worktrees) {
|
|
2841
|
+
const worktreePath = resolve(worktree.path);
|
|
2842
|
+
if (!await sameGitPath(worktreePath, resolvedInputPath)) continue;
|
|
2843
|
+
return {
|
|
2844
|
+
path: worktreePath,
|
|
2845
|
+
pathAvailable: await pathExists(worktreePath)
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
return null;
|
|
2849
|
+
}
|
|
2727
2850
|
async function listCurrentWorktreeGitEntries(options) {
|
|
2728
2851
|
const resolvedProjectDir = resolve(options.projectDir);
|
|
2729
2852
|
const limit = clampEntryLimit(options.limit);
|
|
@@ -2958,6 +3081,35 @@ const soundsRouter = router({
|
|
|
2958
3081
|
return { success: true };
|
|
2959
3082
|
})
|
|
2960
3083
|
});
|
|
3084
|
+
const globalSettingsRouter = router({
|
|
3085
|
+
get: publicProcedure.query(({ ctx }) => {
|
|
3086
|
+
return ctx.globalSettingsManager.readSettings();
|
|
3087
|
+
}),
|
|
3088
|
+
update: publicProcedure.input(OpenSpecUIGlobalSettingsSchema.partial().extend({ translationCache: TranslationCacheSettingsSchema.partial().optional() })).mutation(async ({ ctx, input }) => {
|
|
3089
|
+
await ctx.globalSettingsManager.writeSettings(input);
|
|
3090
|
+
return { success: true };
|
|
3091
|
+
}),
|
|
3092
|
+
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
3093
|
+
return createReactiveSubscription(() => ctx.globalSettingsManager.readSettings());
|
|
3094
|
+
})
|
|
3095
|
+
});
|
|
3096
|
+
const translationCacheRouter = router({
|
|
3097
|
+
stats: publicProcedure.query(({ ctx }) => {
|
|
3098
|
+
return ctx.translationCacheService.getStats();
|
|
3099
|
+
}),
|
|
3100
|
+
read: publicProcedure.input(TranslationCacheReadInputSchema).query(({ ctx, input }) => {
|
|
3101
|
+
return ctx.translationCacheService.read(input.keyHash);
|
|
3102
|
+
}),
|
|
3103
|
+
write: publicProcedure.input(TranslationCacheWriteInputSchema).mutation(({ ctx, input }) => {
|
|
3104
|
+
return ctx.translationCacheService.write(input);
|
|
3105
|
+
}),
|
|
3106
|
+
clean: publicProcedure.mutation(({ ctx }) => {
|
|
3107
|
+
return ctx.translationCacheService.clean();
|
|
3108
|
+
}),
|
|
3109
|
+
clear: publicProcedure.mutation(({ ctx }) => {
|
|
3110
|
+
return ctx.translationCacheService.clear();
|
|
3111
|
+
})
|
|
3112
|
+
});
|
|
2961
3113
|
const OPSX_CORE_PROFILE_WORKFLOWS = [
|
|
2962
3114
|
"propose",
|
|
2963
3115
|
"explore",
|
|
@@ -3283,19 +3435,21 @@ const archiveRouter = router({
|
|
|
3283
3435
|
return ctx.adapter.listArchivedChangesWithMeta();
|
|
3284
3436
|
}),
|
|
3285
3437
|
get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
|
|
3286
|
-
return ctx.documentService.
|
|
3438
|
+
return ctx.documentService.readEntityDetail("archive", input.id, "view", "processed", await buildEntityReadOptions(ctx, "archive", input.id));
|
|
3287
3439
|
}),
|
|
3288
3440
|
getRaw: publicProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
|
|
3289
|
-
return ctx.
|
|
3441
|
+
return ctx.documentService.readEntityDetail("archive", input.id, "view", "source", await buildEntityReadOptions(ctx, "archive", input.id));
|
|
3290
3442
|
}),
|
|
3291
3443
|
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
3292
3444
|
return createReactiveSubscription(() => ctx.adapter.listArchivedChangesWithMeta());
|
|
3293
3445
|
}),
|
|
3294
3446
|
subscribeOne: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
|
|
3295
|
-
return createReactiveSubscriptionWithInput((id) => ctx.documentService.
|
|
3447
|
+
return createReactiveSubscriptionWithInput(async (id) => ctx.documentService.readEntityDetail("archive", id, "view", "processed", await buildEntityReadOptions(ctx, "archive", id)))(input.id);
|
|
3296
3448
|
}),
|
|
3297
3449
|
subscribeFiles: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
|
|
3298
|
-
return createReactiveSubscriptionWithInput((id) =>
|
|
3450
|
+
return createReactiveSubscriptionWithInput(async (id) => {
|
|
3451
|
+
return (await ctx.documentService.readEntityDetail("archive", id, "view", "processed", await buildEntityReadOptions(ctx, "archive", id)))?.files ?? [];
|
|
3452
|
+
})(input.id);
|
|
3299
3453
|
})
|
|
3300
3454
|
});
|
|
3301
3455
|
z.object({
|
|
@@ -3397,13 +3551,14 @@ const configRouter = router({
|
|
|
3397
3551
|
terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
|
|
3398
3552
|
dashboard: DashboardConfigSchema.partial().optional(),
|
|
3399
3553
|
git: GitConfigSchema.partial().optional(),
|
|
3400
|
-
notifications: NotificationSettingsSchema.partial().optional()
|
|
3554
|
+
notifications: NotificationSettingsSchema.partial().optional(),
|
|
3555
|
+
translation: DocumentTranslationConfigSchema.partial().optional()
|
|
3401
3556
|
})).mutation(async ({ ctx, input }) => {
|
|
3402
3557
|
const hasCliCommand = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "command");
|
|
3403
3558
|
const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
|
|
3404
3559
|
if (hasCliCommand && !hasCliArgs) {
|
|
3405
3560
|
await ctx.configManager.setCliCommand(input.cli?.command ?? "");
|
|
3406
|
-
if (input.theme !== void 0 || input.codeEditor !== void 0 || input.appBaseUrl !== void 0 || input.opsx !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0 || input.git !== void 0 || input.notifications !== void 0) await ctx.configManager.writeConfig({
|
|
3561
|
+
if (input.theme !== void 0 || input.codeEditor !== void 0 || input.appBaseUrl !== void 0 || input.opsx !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0 || input.git !== void 0 || input.notifications !== void 0 || input.translation !== void 0) await ctx.configManager.writeConfig({
|
|
3407
3562
|
theme: input.theme,
|
|
3408
3563
|
codeEditor: input.codeEditor,
|
|
3409
3564
|
appBaseUrl: input.appBaseUrl,
|
|
@@ -3411,7 +3566,8 @@ const configRouter = router({
|
|
|
3411
3566
|
terminal: input.terminal,
|
|
3412
3567
|
dashboard: input.dashboard,
|
|
3413
3568
|
git: input.git,
|
|
3414
|
-
notifications: input.notifications
|
|
3569
|
+
notifications: input.notifications,
|
|
3570
|
+
translation: input.translation
|
|
3415
3571
|
});
|
|
3416
3572
|
return { success: true };
|
|
3417
3573
|
}
|
|
@@ -3820,7 +3976,7 @@ const opsxRouter = router({
|
|
|
3820
3976
|
})).query(async ({ ctx, input }) => {
|
|
3821
3977
|
await ctx.kernel.waitForWarmup();
|
|
3822
3978
|
await ctx.kernel.ensureArtifactOutput(input.changeId, input.outputPath);
|
|
3823
|
-
return ctx.
|
|
3979
|
+
return ctx.documentService.readChangeArtifactOutput(input.changeId, input.outputPath, "view", "processed");
|
|
3824
3980
|
}),
|
|
3825
3981
|
subscribeArtifactOutput: publicProcedure.input(z.object({
|
|
3826
3982
|
changeId: z.string(),
|
|
@@ -3829,7 +3985,7 @@ const opsxRouter = router({
|
|
|
3829
3985
|
return createReactiveSubscription(async () => {
|
|
3830
3986
|
await ctx.kernel.waitForWarmup();
|
|
3831
3987
|
await ctx.kernel.ensureArtifactOutput(input.changeId, input.outputPath);
|
|
3832
|
-
return ctx.
|
|
3988
|
+
return ctx.documentService.readChangeArtifactOutput(input.changeId, input.outputPath, "view", "processed");
|
|
3833
3989
|
});
|
|
3834
3990
|
}),
|
|
3835
3991
|
readGlobArtifactFiles: publicProcedure.input(z.object({
|
|
@@ -3838,7 +3994,7 @@ const opsxRouter = router({
|
|
|
3838
3994
|
})).query(async ({ ctx, input }) => {
|
|
3839
3995
|
await ctx.kernel.waitForWarmup();
|
|
3840
3996
|
await ctx.kernel.ensureGlobArtifactFiles(input.changeId, input.outputPath);
|
|
3841
|
-
return ctx.
|
|
3997
|
+
return ctx.documentService.readChangeGlobArtifactFiles(input.changeId, input.outputPath, "view", "processed");
|
|
3842
3998
|
}),
|
|
3843
3999
|
subscribeGlobArtifactFiles: publicProcedure.input(z.object({
|
|
3844
4000
|
changeId: z.string(),
|
|
@@ -3847,7 +4003,7 @@ const opsxRouter = router({
|
|
|
3847
4003
|
return createReactiveSubscription(async () => {
|
|
3848
4004
|
await ctx.kernel.waitForWarmup();
|
|
3849
4005
|
await ctx.kernel.ensureGlobArtifactFiles(input.changeId, input.outputPath);
|
|
3850
|
-
return ctx.
|
|
4006
|
+
return ctx.documentService.readChangeGlobArtifactFiles(input.changeId, input.outputPath, "view", "processed");
|
|
3851
4007
|
});
|
|
3852
4008
|
}),
|
|
3853
4009
|
writeArtifactOutput: publicProcedure.input(z.object({
|
|
@@ -4028,14 +4184,10 @@ const gitRouter = router({
|
|
|
4028
4184
|
}),
|
|
4029
4185
|
switchWorktree: publicProcedure.input(z.object({ path: z.string().min(1) })).mutation(async ({ ctx, input }) => {
|
|
4030
4186
|
if (!ctx.gitWorktreeHandoff) throw new Error("Worktree handoff is unavailable in this runtime.");
|
|
4031
|
-
const
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
else for (const worktree of overview.otherWorktrees) if (await sameGitPath(worktree.path, resolvedInputPath)) {
|
|
4036
|
-
target = worktree;
|
|
4037
|
-
break;
|
|
4038
|
-
}
|
|
4187
|
+
const target = await resolveGitWorktreeSwitchTarget({
|
|
4188
|
+
projectDir: ctx.projectDir,
|
|
4189
|
+
targetPath: input.path
|
|
4190
|
+
});
|
|
4039
4191
|
if (!target) throw new Error("Worktree not found.");
|
|
4040
4192
|
if (!target.pathAvailable) throw new Error("Worktree path is no longer available. Remove the stale worktree entry first.");
|
|
4041
4193
|
return ctx.gitWorktreeHandoff.ensureWorktreeServer({ targetPath: target.path });
|
|
@@ -4053,6 +4205,8 @@ const appRouter = router({
|
|
|
4053
4205
|
init: initRouter,
|
|
4054
4206
|
realtime: realtimeRouter,
|
|
4055
4207
|
config: configRouter,
|
|
4208
|
+
globalSettings: globalSettingsRouter,
|
|
4209
|
+
translationCache: translationCacheRouter,
|
|
4056
4210
|
notifications: notificationsRouter,
|
|
4057
4211
|
sounds: soundsRouter,
|
|
4058
4212
|
cli: cliRouter,
|
|
@@ -4067,7 +4221,7 @@ const appRouter = router({
|
|
|
4067
4221
|
function joinParts(parts) {
|
|
4068
4222
|
return parts.map((part) => part?.trim() ?? "").filter((part) => part.length > 0).join("\n\n");
|
|
4069
4223
|
}
|
|
4070
|
-
async function collectSearchDocuments(adapter, documentService) {
|
|
4224
|
+
async function collectSearchDocuments(adapter, documentService, resolveEntityReadOptions) {
|
|
4071
4225
|
const docs = [];
|
|
4072
4226
|
const specs = await adapter.listSpecsWithMeta();
|
|
4073
4227
|
for (const spec of specs) {
|
|
@@ -4104,20 +4258,16 @@ async function collectSearchDocuments(adapter, documentService) {
|
|
|
4104
4258
|
}
|
|
4105
4259
|
const archives = await adapter.listArchivedChangesWithMeta();
|
|
4106
4260
|
for (const archive of archives) {
|
|
4107
|
-
const
|
|
4108
|
-
|
|
4261
|
+
const entityOptions = resolveEntityReadOptions ? await resolveEntityReadOptions("archive", archive.id) : void 0;
|
|
4262
|
+
const entity = documentService ? await documentService.readEntityDetail("archive", archive.id, "search", "processed", entityOptions) : await adapter.readEntityDetail("archive", archive.id);
|
|
4263
|
+
if (!entity) continue;
|
|
4109
4264
|
docs.push({
|
|
4110
4265
|
id: `archive:${archive.id}`,
|
|
4111
4266
|
kind: "archive",
|
|
4112
4267
|
title: archive.name,
|
|
4113
4268
|
href: `/archive/${encodeURIComponent(archive.id)}`,
|
|
4114
4269
|
path: `openspec/changes/archive/${archive.id}`,
|
|
4115
|
-
content: joinParts(
|
|
4116
|
-
typeof raw.proposal === "string" ? raw.proposal : raw.proposal.markdown,
|
|
4117
|
-
typeof raw.tasks === "string" ? raw.tasks : raw.tasks.markdown,
|
|
4118
|
-
typeof raw.design === "string" ? raw.design : raw.design?.markdown,
|
|
4119
|
-
...raw.deltaSpecs.map((deltaSpec) => deltaSpec.content)
|
|
4120
|
-
]),
|
|
4270
|
+
content: joinParts(entity.files.filter((file) => file.type === "file").map((file) => file.content)),
|
|
4121
4271
|
updatedAt: archive.updatedAt
|
|
4122
4272
|
});
|
|
4123
4273
|
}
|
|
@@ -4133,9 +4283,10 @@ var SearchService = class {
|
|
|
4133
4283
|
initPromise = null;
|
|
4134
4284
|
rebuildPromise = null;
|
|
4135
4285
|
rebuildTimer = null;
|
|
4136
|
-
constructor(adapter, watcher, provider = new NodeWorkerSearchProvider(), documentService) {
|
|
4286
|
+
constructor(adapter, watcher, provider = new NodeWorkerSearchProvider(), documentService, resolveEntityReadOptions) {
|
|
4137
4287
|
this.adapter = adapter;
|
|
4138
4288
|
this.documentService = documentService;
|
|
4289
|
+
this.resolveEntityReadOptions = resolveEntityReadOptions;
|
|
4139
4290
|
this.provider = provider;
|
|
4140
4291
|
watcher?.on("change", () => {
|
|
4141
4292
|
this.scheduleRebuild();
|
|
@@ -4181,7 +4332,7 @@ var SearchService = class {
|
|
|
4181
4332
|
if (!forceInit && !this.initialized) return;
|
|
4182
4333
|
if (this.rebuildPromise) return this.rebuildPromise;
|
|
4183
4334
|
this.rebuildPromise = (async () => {
|
|
4184
|
-
const docs = await collectSearchDocuments(this.adapter, this.documentService);
|
|
4335
|
+
const docs = await collectSearchDocuments(this.adapter, this.documentService, this.resolveEntityReadOptions);
|
|
4185
4336
|
if (this.initialized) await this.provider.replaceAll(docs);
|
|
4186
4337
|
else {
|
|
4187
4338
|
await this.provider.init(docs);
|
|
@@ -4196,6 +4347,227 @@ var SearchService = class {
|
|
|
4196
4347
|
}
|
|
4197
4348
|
};
|
|
4198
4349
|
|
|
4350
|
+
//#endregion
|
|
4351
|
+
//#region src/translation-cache-adapter.ts
|
|
4352
|
+
var SqliteTranslationCacheAdapter = class {
|
|
4353
|
+
database = null;
|
|
4354
|
+
constructor(databasePath, createDatabase) {
|
|
4355
|
+
this.databasePath = databasePath;
|
|
4356
|
+
this.createDatabase = createDatabase;
|
|
4357
|
+
}
|
|
4358
|
+
async init() {
|
|
4359
|
+
if (this.database) return;
|
|
4360
|
+
await mkdir(dirname(this.databasePath), { recursive: true });
|
|
4361
|
+
const database = this.createDatabase(this.databasePath);
|
|
4362
|
+
database.exec(`
|
|
4363
|
+
CREATE TABLE IF NOT EXISTS translation_cache_entries (
|
|
4364
|
+
key_hash TEXT PRIMARY KEY,
|
|
4365
|
+
cache_key TEXT NOT NULL,
|
|
4366
|
+
source_text TEXT NOT NULL,
|
|
4367
|
+
translated_text TEXT NOT NULL,
|
|
4368
|
+
target_nodes_json TEXT,
|
|
4369
|
+
source_language TEXT NOT NULL,
|
|
4370
|
+
target_language TEXT NOT NULL,
|
|
4371
|
+
placeholder_topology_hash TEXT NOT NULL,
|
|
4372
|
+
attribute_topology_hash TEXT NOT NULL,
|
|
4373
|
+
display_policy_version INTEGER NOT NULL,
|
|
4374
|
+
created_at INTEGER NOT NULL,
|
|
4375
|
+
last_accessed_at INTEGER NOT NULL
|
|
4376
|
+
);
|
|
4377
|
+
CREATE INDEX IF NOT EXISTS translation_cache_entries_lru_idx
|
|
4378
|
+
ON translation_cache_entries(last_accessed_at ASC);
|
|
4379
|
+
`);
|
|
4380
|
+
ensureTargetNodesJsonColumn(database);
|
|
4381
|
+
this.database = database;
|
|
4382
|
+
}
|
|
4383
|
+
async read(keyHash, now) {
|
|
4384
|
+
const database = await this.requireDatabase();
|
|
4385
|
+
const row = database.prepare(`SELECT key_hash, cache_key, source_text, translated_text, target_nodes_json, source_language,
|
|
4386
|
+
target_language, placeholder_topology_hash, attribute_topology_hash,
|
|
4387
|
+
display_policy_version, created_at, last_accessed_at
|
|
4388
|
+
FROM translation_cache_entries
|
|
4389
|
+
WHERE key_hash = ?`).get(keyHash);
|
|
4390
|
+
if (!isSqliteTranslationCacheRow(row)) return null;
|
|
4391
|
+
database.prepare("UPDATE translation_cache_entries SET last_accessed_at = ? WHERE key_hash = ?").run(now, keyHash);
|
|
4392
|
+
return {
|
|
4393
|
+
keyHash: row.key_hash,
|
|
4394
|
+
key: row.cache_key,
|
|
4395
|
+
sourceText: row.source_text,
|
|
4396
|
+
translatedText: row.translated_text,
|
|
4397
|
+
...row.target_nodes_json ? { targetNodesJson: row.target_nodes_json } : {},
|
|
4398
|
+
sourceLanguage: row.source_language,
|
|
4399
|
+
targetLanguage: row.target_language,
|
|
4400
|
+
placeholderTopologyHash: row.placeholder_topology_hash,
|
|
4401
|
+
attributeTopologyHash: row.attribute_topology_hash,
|
|
4402
|
+
displayPolicyVersion: row.display_policy_version,
|
|
4403
|
+
createdAt: row.created_at,
|
|
4404
|
+
lastAccessedAt: now
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
4407
|
+
async write(input, now) {
|
|
4408
|
+
(await this.requireDatabase()).prepare(`INSERT INTO translation_cache_entries (
|
|
4409
|
+
key_hash, cache_key, source_text, translated_text, target_nodes_json, source_language,
|
|
4410
|
+
target_language, placeholder_topology_hash, attribute_topology_hash,
|
|
4411
|
+
display_policy_version, created_at, last_accessed_at
|
|
4412
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4413
|
+
ON CONFLICT(key_hash) DO UPDATE SET
|
|
4414
|
+
cache_key = excluded.cache_key,
|
|
4415
|
+
source_text = excluded.source_text,
|
|
4416
|
+
translated_text = excluded.translated_text,
|
|
4417
|
+
target_nodes_json = excluded.target_nodes_json,
|
|
4418
|
+
source_language = excluded.source_language,
|
|
4419
|
+
target_language = excluded.target_language,
|
|
4420
|
+
placeholder_topology_hash = excluded.placeholder_topology_hash,
|
|
4421
|
+
attribute_topology_hash = excluded.attribute_topology_hash,
|
|
4422
|
+
display_policy_version = excluded.display_policy_version,
|
|
4423
|
+
last_accessed_at = excluded.last_accessed_at`).run(input.keyHash, input.key, input.sourceText, input.translatedText, input.targetNodesJson ?? null, input.sourceLanguage, input.targetLanguage, input.placeholderTopologyHash, input.attributeTopologyHash, input.displayPolicyVersion, now, now);
|
|
4424
|
+
}
|
|
4425
|
+
async count() {
|
|
4426
|
+
return readSqliteCount((await this.requireDatabase()).prepare("SELECT COUNT(*) AS count FROM translation_cache_entries").get());
|
|
4427
|
+
}
|
|
4428
|
+
async deleteLeastRecentlyUsed(targetEntryCount) {
|
|
4429
|
+
const database = await this.requireDatabase();
|
|
4430
|
+
const currentCount = await this.count();
|
|
4431
|
+
const deleteCount = Math.max(0, currentCount - targetEntryCount);
|
|
4432
|
+
if (deleteCount === 0) return 0;
|
|
4433
|
+
database.prepare(`DELETE FROM translation_cache_entries
|
|
4434
|
+
WHERE key_hash IN (
|
|
4435
|
+
SELECT key_hash FROM translation_cache_entries
|
|
4436
|
+
ORDER BY last_accessed_at ASC, key_hash ASC
|
|
4437
|
+
LIMIT ?
|
|
4438
|
+
)`).run(deleteCount);
|
|
4439
|
+
return deleteCount;
|
|
4440
|
+
}
|
|
4441
|
+
async clean(entryLimit) {
|
|
4442
|
+
const before = await this.count();
|
|
4443
|
+
const target = Math.floor(entryLimit * .6);
|
|
4444
|
+
const deleted = await this.deleteLeastRecentlyUsed(target);
|
|
4445
|
+
return {
|
|
4446
|
+
before,
|
|
4447
|
+
after: await this.count(),
|
|
4448
|
+
deleted
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
async clear() {
|
|
4452
|
+
const database = await this.requireDatabase();
|
|
4453
|
+
const before = await this.count();
|
|
4454
|
+
database.prepare("DELETE FROM translation_cache_entries").run();
|
|
4455
|
+
return before;
|
|
4456
|
+
}
|
|
4457
|
+
close() {
|
|
4458
|
+
this.database?.close?.();
|
|
4459
|
+
this.database = null;
|
|
4460
|
+
}
|
|
4461
|
+
async requireDatabase() {
|
|
4462
|
+
await this.init();
|
|
4463
|
+
if (!this.database) throw new Error("Translation cache database is not initialized.");
|
|
4464
|
+
return this.database;
|
|
4465
|
+
}
|
|
4466
|
+
};
|
|
4467
|
+
async function createRuntimeSqliteTranslationCacheAdapter(databasePath) {
|
|
4468
|
+
return new SqliteTranslationCacheAdapter(databasePath, await resolveRuntimeSqliteDatabaseFactory());
|
|
4469
|
+
}
|
|
4470
|
+
async function resolveRuntimeSqliteDatabaseFactory() {
|
|
4471
|
+
if (isBunRuntime()) {
|
|
4472
|
+
const Database$1 = (await dynamicImport("bun:sqlite")).Database;
|
|
4473
|
+
return (databasePath) => new Database$1(databasePath);
|
|
4474
|
+
}
|
|
4475
|
+
const Database = (await import("better-sqlite3")).default;
|
|
4476
|
+
return (databasePath) => new Database(databasePath);
|
|
4477
|
+
}
|
|
4478
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
4479
|
+
function isBunRuntime() {
|
|
4480
|
+
return typeof process.versions.bun === "string";
|
|
4481
|
+
}
|
|
4482
|
+
function ensureTargetNodesJsonColumn(database) {
|
|
4483
|
+
if (!database.prepare("PRAGMA table_info(translation_cache_entries)").all().some((row) => {
|
|
4484
|
+
if (!row || typeof row !== "object") return false;
|
|
4485
|
+
return row.name === "target_nodes_json";
|
|
4486
|
+
})) database.exec("ALTER TABLE translation_cache_entries ADD COLUMN target_nodes_json TEXT");
|
|
4487
|
+
}
|
|
4488
|
+
function isSqliteTranslationCacheRow(value) {
|
|
4489
|
+
if (!value || typeof value !== "object") return false;
|
|
4490
|
+
const row = value;
|
|
4491
|
+
return typeof row.key_hash === "string" && typeof row.cache_key === "string" && typeof row.source_text === "string" && typeof row.translated_text === "string" && (typeof row.target_nodes_json === "string" || row.target_nodes_json === null) && typeof row.source_language === "string" && typeof row.target_language === "string" && typeof row.placeholder_topology_hash === "string" && typeof row.attribute_topology_hash === "string" && typeof row.display_policy_version === "number" && typeof row.created_at === "number" && typeof row.last_accessed_at === "number";
|
|
4492
|
+
}
|
|
4493
|
+
function readSqliteCount(value) {
|
|
4494
|
+
if (!value || typeof value !== "object") return 0;
|
|
4495
|
+
const count = value.count;
|
|
4496
|
+
return typeof count === "number" ? count : 0;
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
//#endregion
|
|
4500
|
+
//#region src/translation-cache-path.ts
|
|
4501
|
+
function getDefaultTranslationCacheDatabasePath() {
|
|
4502
|
+
return join(getOpenSpecUICacheDir(), "translation-cache.sqlite");
|
|
4503
|
+
}
|
|
4504
|
+
function getOpenSpecUICacheDir() {
|
|
4505
|
+
const currentPlatform = platform();
|
|
4506
|
+
if (currentPlatform === "darwin") return join(homedir(), "Library", "Caches", "openspecui");
|
|
4507
|
+
if (currentPlatform === "win32") return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "OpenSpecUI", "Cache");
|
|
4508
|
+
return join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "openspecui");
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
//#endregion
|
|
4512
|
+
//#region src/translation-cache-service.ts
|
|
4513
|
+
var TranslationCacheService = class {
|
|
4514
|
+
configManager;
|
|
4515
|
+
globalSettingsManager;
|
|
4516
|
+
adapter;
|
|
4517
|
+
now;
|
|
4518
|
+
onWriteError;
|
|
4519
|
+
constructor(options) {
|
|
4520
|
+
this.configManager = options.configManager;
|
|
4521
|
+
this.globalSettingsManager = options.globalSettingsManager;
|
|
4522
|
+
this.adapter = options.adapter;
|
|
4523
|
+
this.now = options.now ?? Date.now;
|
|
4524
|
+
this.onWriteError = options.onWriteError ?? (() => void 0);
|
|
4525
|
+
}
|
|
4526
|
+
async getStats() {
|
|
4527
|
+
const [{ translation }, globalSettings] = await Promise.all([this.configManager.readConfig(), this.globalSettingsManager.readSettings()]);
|
|
4528
|
+
const entryLimit = globalSettings.translationCache.entryLimit;
|
|
4529
|
+
const enabled = translation.cacheEnabled;
|
|
4530
|
+
return {
|
|
4531
|
+
enabled,
|
|
4532
|
+
entryLimit,
|
|
4533
|
+
entries: enabled ? await this.adapter.count() : 0,
|
|
4534
|
+
...this.adapter.databasePath ? { databasePath: this.adapter.databasePath } : {}
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
async read(keyHash) {
|
|
4538
|
+
if (!(await this.configManager.readConfig()).translation.cacheEnabled) return null;
|
|
4539
|
+
try {
|
|
4540
|
+
return await this.adapter.read(keyHash, this.now());
|
|
4541
|
+
} catch {
|
|
4542
|
+
return null;
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
async write(input) {
|
|
4546
|
+
const [{ translation }, globalSettings] = await Promise.all([this.configManager.readConfig(), this.globalSettingsManager.readSettings()]);
|
|
4547
|
+
if (!translation.cacheEnabled) return { accepted: false };
|
|
4548
|
+
this.writeAndClean(input, globalSettings.translationCache.entryLimit);
|
|
4549
|
+
return { accepted: true };
|
|
4550
|
+
}
|
|
4551
|
+
async clean() {
|
|
4552
|
+
const globalSettings = await this.globalSettingsManager.readSettings();
|
|
4553
|
+
return this.adapter.clean(globalSettings.translationCache.entryLimit);
|
|
4554
|
+
}
|
|
4555
|
+
async clear() {
|
|
4556
|
+
return { deleted: await this.adapter.clear() };
|
|
4557
|
+
}
|
|
4558
|
+
close() {
|
|
4559
|
+
this.adapter.close?.();
|
|
4560
|
+
}
|
|
4561
|
+
async writeAndClean(input, entryLimit) {
|
|
4562
|
+
try {
|
|
4563
|
+
await this.adapter.write(input, this.now());
|
|
4564
|
+
if (await this.adapter.count() >= Math.floor(entryLimit * .9)) await this.adapter.clean(entryLimit);
|
|
4565
|
+
} catch (error) {
|
|
4566
|
+
this.onWriteError(error);
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
};
|
|
4570
|
+
|
|
4199
4571
|
//#endregion
|
|
4200
4572
|
//#region src/workflow-invocation-service.ts
|
|
4201
4573
|
const COMMAND_CAPABLE_ACTIONS = new Set([
|
|
@@ -4436,12 +4808,21 @@ const SERVER_PACKAGE_VERSION = getServerPackageVersion();
|
|
|
4436
4808
|
function buildEmbeddedUiUrlForPort(port) {
|
|
4437
4809
|
return `http://localhost:${port}`;
|
|
4438
4810
|
}
|
|
4811
|
+
function initializeWatcherPoolInBackground(projectDir) {
|
|
4812
|
+
initWatcherPool(projectDir).catch((err) => {
|
|
4813
|
+
console.error("Watcher pool initialization failed:", err);
|
|
4814
|
+
});
|
|
4815
|
+
}
|
|
4816
|
+
function deferBackgroundTask(task) {
|
|
4817
|
+
setTimeout(task, 0);
|
|
4818
|
+
}
|
|
4439
4819
|
/**
|
|
4440
4820
|
* Create an OpenSpecUI HTTP server with optional WebSocket support
|
|
4441
4821
|
*/
|
|
4442
4822
|
function createServer(config) {
|
|
4443
4823
|
const adapter = new OpenSpecAdapter(config.projectDir);
|
|
4444
4824
|
const configManager = new ConfigManager(config.projectDir);
|
|
4825
|
+
const globalSettingsManager = new GlobalSettingsManager();
|
|
4445
4826
|
const cliExecutor = new CliExecutor(configManager, config.projectDir);
|
|
4446
4827
|
const kernel = config.kernel;
|
|
4447
4828
|
const hookRuntime = createHookRuntime(config.projectDir);
|
|
@@ -4453,8 +4834,37 @@ function createServer(config) {
|
|
|
4453
4834
|
});
|
|
4454
4835
|
const notificationService = new NotificationService();
|
|
4455
4836
|
const customSoundService = new CustomSoundService();
|
|
4837
|
+
let translationCacheAdapterPromise = null;
|
|
4838
|
+
const getTranslationCacheAdapter = () => {
|
|
4839
|
+
translationCacheAdapterPromise ??= createRuntimeSqliteTranslationCacheAdapter(getDefaultTranslationCacheDatabasePath());
|
|
4840
|
+
return translationCacheAdapterPromise;
|
|
4841
|
+
};
|
|
4842
|
+
const translationCacheService = new TranslationCacheService({
|
|
4843
|
+
configManager,
|
|
4844
|
+
globalSettingsManager,
|
|
4845
|
+
adapter: {
|
|
4846
|
+
databasePath: getDefaultTranslationCacheDatabasePath(),
|
|
4847
|
+
init: async () => (await getTranslationCacheAdapter()).init(),
|
|
4848
|
+
read: async (keyHash, now) => (await getTranslationCacheAdapter()).read(keyHash, now),
|
|
4849
|
+
write: async (input, now) => (await getTranslationCacheAdapter()).write(input, now),
|
|
4850
|
+
count: async () => (await getTranslationCacheAdapter()).count(),
|
|
4851
|
+
deleteLeastRecentlyUsed: async (targetEntryCount) => (await getTranslationCacheAdapter()).deleteLeastRecentlyUsed(targetEntryCount),
|
|
4852
|
+
clean: async (entryLimit) => (await getTranslationCacheAdapter()).clean(entryLimit),
|
|
4853
|
+
clear: async () => (await getTranslationCacheAdapter()).clear(),
|
|
4854
|
+
close: () => {
|
|
4855
|
+
translationCacheAdapterPromise?.then((cacheAdapter) => cacheAdapter.close()).catch(() => {});
|
|
4856
|
+
}
|
|
4857
|
+
},
|
|
4858
|
+
onWriteError(error) {
|
|
4859
|
+
console.warn("Translation cache write failed:", error);
|
|
4860
|
+
}
|
|
4861
|
+
});
|
|
4456
4862
|
const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
|
|
4457
|
-
const
|
|
4863
|
+
const entityReadOptionsContext = {
|
|
4864
|
+
adapter,
|
|
4865
|
+
kernel
|
|
4866
|
+
};
|
|
4867
|
+
const searchService = new SearchService(adapter, watcher, void 0, documentService, (stage, id) => buildEntityReadOptions(entityReadOptionsContext, stage, id));
|
|
4458
4868
|
const dashboardOverviewService = new DashboardOverviewService((reason) => loadDashboardOverview({
|
|
4459
4869
|
adapter,
|
|
4460
4870
|
configManager,
|
|
@@ -4471,15 +4881,13 @@ function createServer(config) {
|
|
|
4471
4881
|
credentials: true
|
|
4472
4882
|
}));
|
|
4473
4883
|
app.get("/api/health", (c) => {
|
|
4474
|
-
return c.json({
|
|
4475
|
-
status: "ok",
|
|
4884
|
+
return c.json(buildBackendHealthPayload({
|
|
4476
4885
|
projectDir: config.projectDir,
|
|
4477
4886
|
projectName: basename(config.projectDir) || config.projectDir,
|
|
4478
4887
|
watcherEnabled: !!watcher,
|
|
4479
4888
|
openspecuiVersion: SERVER_PACKAGE_VERSION,
|
|
4480
|
-
hostedShellProtocolVersion: HOSTED_SHELL_PROTOCOL_VERSION,
|
|
4481
4889
|
embeddedUiUrl: buildEmbeddedUiUrlForPort(config.port ?? 3100)
|
|
4482
|
-
});
|
|
4890
|
+
}));
|
|
4483
4891
|
});
|
|
4484
4892
|
app.post("/api/notifications", async (c) => {
|
|
4485
4893
|
const body = await c.req.json().catch(() => null);
|
|
@@ -4530,6 +4938,8 @@ function createServer(config) {
|
|
|
4530
4938
|
projectRecoveryService,
|
|
4531
4939
|
notificationService,
|
|
4532
4940
|
customSoundService,
|
|
4941
|
+
globalSettingsManager,
|
|
4942
|
+
translationCacheService,
|
|
4533
4943
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
4534
4944
|
watcher,
|
|
4535
4945
|
projectDir: config.projectDir
|
|
@@ -4548,6 +4958,8 @@ function createServer(config) {
|
|
|
4548
4958
|
projectRecoveryService,
|
|
4549
4959
|
notificationService,
|
|
4550
4960
|
customSoundService,
|
|
4961
|
+
globalSettingsManager,
|
|
4962
|
+
translationCacheService,
|
|
4551
4963
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
4552
4964
|
watcher,
|
|
4553
4965
|
projectDir: config.projectDir
|
|
@@ -4565,6 +4977,8 @@ function createServer(config) {
|
|
|
4565
4977
|
projectRecoveryService,
|
|
4566
4978
|
notificationService,
|
|
4567
4979
|
customSoundService,
|
|
4980
|
+
globalSettingsManager,
|
|
4981
|
+
translationCacheService,
|
|
4568
4982
|
hookRuntime,
|
|
4569
4983
|
watcher,
|
|
4570
4984
|
createContext,
|
|
@@ -4575,7 +4989,7 @@ function createServer(config) {
|
|
|
4575
4989
|
* Create WebSocket server for tRPC subscriptions and PTY terminals
|
|
4576
4990
|
*/
|
|
4577
4991
|
async function createWebSocketServer(server, httpServer, config) {
|
|
4578
|
-
if (!isWatcherPoolInitialized())
|
|
4992
|
+
if (!isWatcherPoolInitialized()) deferBackgroundTask(() => initializeWatcherPoolInBackground(config.projectDir));
|
|
4579
4993
|
const wss = new WebSocketServer({ noServer: true });
|
|
4580
4994
|
const handler = applyWSSHandler({
|
|
4581
4995
|
wss,
|
|
@@ -4615,6 +5029,7 @@ async function createWebSocketServer(server, httpServer, config) {
|
|
|
4615
5029
|
server.searchService.dispose().catch(() => {});
|
|
4616
5030
|
server.dashboardOverviewService.dispose();
|
|
4617
5031
|
server.projectRecoveryService.dispose();
|
|
5032
|
+
server.translationCacheService.close();
|
|
4618
5033
|
}
|
|
4619
5034
|
};
|
|
4620
5035
|
}
|
|
@@ -4631,7 +5046,7 @@ async function startServer(config, setupApp) {
|
|
|
4631
5046
|
const port = await findAvailablePort(preferredPort);
|
|
4632
5047
|
const cliExecutor = new CliExecutor(new ConfigManager(config.projectDir), config.projectDir);
|
|
4633
5048
|
const kernel = new OpsxKernel(config.projectDir, cliExecutor);
|
|
4634
|
-
|
|
5049
|
+
deferBackgroundTask(() => initializeWatcherPoolInBackground(config.projectDir));
|
|
4635
5050
|
const server = createServer({
|
|
4636
5051
|
...config,
|
|
4637
5052
|
port,
|
|
@@ -4644,14 +5059,16 @@ async function startServer(config, setupApp) {
|
|
|
4644
5059
|
});
|
|
4645
5060
|
const wsServer = await createWebSocketServer(server, httpServer, { projectDir: config.projectDir });
|
|
4646
5061
|
const url = `http://localhost:${port}`;
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
5062
|
+
deferBackgroundTask(() => {
|
|
5063
|
+
kernel.warmup().catch((err) => {
|
|
5064
|
+
console.error("Kernel warmup failed:", err);
|
|
5065
|
+
});
|
|
5066
|
+
server.searchService.init().catch((err) => {
|
|
5067
|
+
console.error("Search service warmup failed:", err);
|
|
5068
|
+
});
|
|
5069
|
+
server.dashboardOverviewService.init().catch((err) => {
|
|
5070
|
+
console.error("Dashboard overview warmup failed:", err);
|
|
5071
|
+
});
|
|
4655
5072
|
});
|
|
4656
5073
|
return {
|
|
4657
5074
|
url,
|
|
@@ -4660,6 +5077,7 @@ async function startServer(config, setupApp) {
|
|
|
4660
5077
|
close: async () => {
|
|
4661
5078
|
kernel.dispose();
|
|
4662
5079
|
await server.hookRuntime.dispose();
|
|
5080
|
+
server.translationCacheService.close();
|
|
4663
5081
|
wsServer.close();
|
|
4664
5082
|
httpServer.close();
|
|
4665
5083
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openspecui/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
-
"
|
|
8
|
+
"development": "./src/index.ts",
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"default": "./dist/index.mjs"
|
|
9
11
|
}
|
|
10
12
|
},
|
|
11
13
|
"files": [
|
|
@@ -14,7 +16,7 @@
|
|
|
14
16
|
"scripts": {
|
|
15
17
|
"build": "tsdown src/index.ts --format esm --no-dts",
|
|
16
18
|
"typecheck": "tsc -p tsconfig.check.json --noEmit",
|
|
17
|
-
"dev": "tsx watch --include '../core/dist/**' --include '../search/dist/**' src/standalone.ts",
|
|
19
|
+
"dev": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=development\" tsx watch --include '../core/dist/**' --include '../search/dist/**' src/standalone.ts",
|
|
18
20
|
"test": "vitest run",
|
|
19
21
|
"test:watch": "vitest"
|
|
20
22
|
},
|
|
@@ -24,6 +26,8 @@
|
|
|
24
26
|
"@openspecui/core": "workspace:*",
|
|
25
27
|
"@openspecui/search": "workspace:*",
|
|
26
28
|
"@trpc/server": "^11.0.0",
|
|
29
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
30
|
+
"better-sqlite3": "^12.5.0",
|
|
27
31
|
"hono": "^4.7.3",
|
|
28
32
|
"tsx": "^4.19.2",
|
|
29
33
|
"ws": "^8.18.0",
|