@openspecui/server 3.7.0 → 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 +330 -26
- package/package.json +7 -3
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema,
|
|
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
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";
|
|
@@ -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";
|
|
@@ -1728,8 +1728,8 @@ function detectPtyPlatform() {
|
|
|
1728
1728
|
if (process.platform === "darwin") return "macos";
|
|
1729
1729
|
return "common";
|
|
1730
1730
|
}
|
|
1731
|
-
function resolveDefaultShell(platform, env) {
|
|
1732
|
-
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";
|
|
1733
1733
|
return env.SHELL?.trim() || "/bin/sh";
|
|
1734
1734
|
}
|
|
1735
1735
|
function resolvePtyShellDefaults(opts) {
|
|
@@ -2833,6 +2833,20 @@ async function buildGitWorktreeOverview(options) {
|
|
|
2833
2833
|
};
|
|
2834
2834
|
});
|
|
2835
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
|
+
}
|
|
2836
2850
|
async function listCurrentWorktreeGitEntries(options) {
|
|
2837
2851
|
const resolvedProjectDir = resolve(options.projectDir);
|
|
2838
2852
|
const limit = clampEntryLimit(options.limit);
|
|
@@ -3067,6 +3081,35 @@ const soundsRouter = router({
|
|
|
3067
3081
|
return { success: true };
|
|
3068
3082
|
})
|
|
3069
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
|
+
});
|
|
3070
3113
|
const OPSX_CORE_PROFILE_WORKFLOWS = [
|
|
3071
3114
|
"propose",
|
|
3072
3115
|
"explore",
|
|
@@ -4141,14 +4184,10 @@ const gitRouter = router({
|
|
|
4141
4184
|
}),
|
|
4142
4185
|
switchWorktree: publicProcedure.input(z.object({ path: z.string().min(1) })).mutation(async ({ ctx, input }) => {
|
|
4143
4186
|
if (!ctx.gitWorktreeHandoff) throw new Error("Worktree handoff is unavailable in this runtime.");
|
|
4144
|
-
const
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
else for (const worktree of overview.otherWorktrees) if (await sameGitPath(worktree.path, resolvedInputPath)) {
|
|
4149
|
-
target = worktree;
|
|
4150
|
-
break;
|
|
4151
|
-
}
|
|
4187
|
+
const target = await resolveGitWorktreeSwitchTarget({
|
|
4188
|
+
projectDir: ctx.projectDir,
|
|
4189
|
+
targetPath: input.path
|
|
4190
|
+
});
|
|
4152
4191
|
if (!target) throw new Error("Worktree not found.");
|
|
4153
4192
|
if (!target.pathAvailable) throw new Error("Worktree path is no longer available. Remove the stale worktree entry first.");
|
|
4154
4193
|
return ctx.gitWorktreeHandoff.ensureWorktreeServer({ targetPath: target.path });
|
|
@@ -4166,6 +4205,8 @@ const appRouter = router({
|
|
|
4166
4205
|
init: initRouter,
|
|
4167
4206
|
realtime: realtimeRouter,
|
|
4168
4207
|
config: configRouter,
|
|
4208
|
+
globalSettings: globalSettingsRouter,
|
|
4209
|
+
translationCache: translationCacheRouter,
|
|
4169
4210
|
notifications: notificationsRouter,
|
|
4170
4211
|
sounds: soundsRouter,
|
|
4171
4212
|
cli: cliRouter,
|
|
@@ -4306,6 +4347,227 @@ var SearchService = class {
|
|
|
4306
4347
|
}
|
|
4307
4348
|
};
|
|
4308
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
|
+
|
|
4309
4571
|
//#endregion
|
|
4310
4572
|
//#region src/workflow-invocation-service.ts
|
|
4311
4573
|
const COMMAND_CAPABLE_ACTIONS = new Set([
|
|
@@ -4546,12 +4808,21 @@ const SERVER_PACKAGE_VERSION = getServerPackageVersion();
|
|
|
4546
4808
|
function buildEmbeddedUiUrlForPort(port) {
|
|
4547
4809
|
return `http://localhost:${port}`;
|
|
4548
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
|
+
}
|
|
4549
4819
|
/**
|
|
4550
4820
|
* Create an OpenSpecUI HTTP server with optional WebSocket support
|
|
4551
4821
|
*/
|
|
4552
4822
|
function createServer(config) {
|
|
4553
4823
|
const adapter = new OpenSpecAdapter(config.projectDir);
|
|
4554
4824
|
const configManager = new ConfigManager(config.projectDir);
|
|
4825
|
+
const globalSettingsManager = new GlobalSettingsManager();
|
|
4555
4826
|
const cliExecutor = new CliExecutor(configManager, config.projectDir);
|
|
4556
4827
|
const kernel = config.kernel;
|
|
4557
4828
|
const hookRuntime = createHookRuntime(config.projectDir);
|
|
@@ -4563,6 +4834,31 @@ function createServer(config) {
|
|
|
4563
4834
|
});
|
|
4564
4835
|
const notificationService = new NotificationService();
|
|
4565
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
|
+
});
|
|
4566
4862
|
const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
|
|
4567
4863
|
const entityReadOptionsContext = {
|
|
4568
4864
|
adapter,
|
|
@@ -4585,15 +4881,13 @@ function createServer(config) {
|
|
|
4585
4881
|
credentials: true
|
|
4586
4882
|
}));
|
|
4587
4883
|
app.get("/api/health", (c) => {
|
|
4588
|
-
return c.json({
|
|
4589
|
-
status: "ok",
|
|
4884
|
+
return c.json(buildBackendHealthPayload({
|
|
4590
4885
|
projectDir: config.projectDir,
|
|
4591
4886
|
projectName: basename(config.projectDir) || config.projectDir,
|
|
4592
4887
|
watcherEnabled: !!watcher,
|
|
4593
4888
|
openspecuiVersion: SERVER_PACKAGE_VERSION,
|
|
4594
|
-
hostedShellProtocolVersion: HOSTED_SHELL_PROTOCOL_VERSION,
|
|
4595
4889
|
embeddedUiUrl: buildEmbeddedUiUrlForPort(config.port ?? 3100)
|
|
4596
|
-
});
|
|
4890
|
+
}));
|
|
4597
4891
|
});
|
|
4598
4892
|
app.post("/api/notifications", async (c) => {
|
|
4599
4893
|
const body = await c.req.json().catch(() => null);
|
|
@@ -4644,6 +4938,8 @@ function createServer(config) {
|
|
|
4644
4938
|
projectRecoveryService,
|
|
4645
4939
|
notificationService,
|
|
4646
4940
|
customSoundService,
|
|
4941
|
+
globalSettingsManager,
|
|
4942
|
+
translationCacheService,
|
|
4647
4943
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
4648
4944
|
watcher,
|
|
4649
4945
|
projectDir: config.projectDir
|
|
@@ -4662,6 +4958,8 @@ function createServer(config) {
|
|
|
4662
4958
|
projectRecoveryService,
|
|
4663
4959
|
notificationService,
|
|
4664
4960
|
customSoundService,
|
|
4961
|
+
globalSettingsManager,
|
|
4962
|
+
translationCacheService,
|
|
4665
4963
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
4666
4964
|
watcher,
|
|
4667
4965
|
projectDir: config.projectDir
|
|
@@ -4679,6 +4977,8 @@ function createServer(config) {
|
|
|
4679
4977
|
projectRecoveryService,
|
|
4680
4978
|
notificationService,
|
|
4681
4979
|
customSoundService,
|
|
4980
|
+
globalSettingsManager,
|
|
4981
|
+
translationCacheService,
|
|
4682
4982
|
hookRuntime,
|
|
4683
4983
|
watcher,
|
|
4684
4984
|
createContext,
|
|
@@ -4689,7 +4989,7 @@ function createServer(config) {
|
|
|
4689
4989
|
* Create WebSocket server for tRPC subscriptions and PTY terminals
|
|
4690
4990
|
*/
|
|
4691
4991
|
async function createWebSocketServer(server, httpServer, config) {
|
|
4692
|
-
if (!isWatcherPoolInitialized())
|
|
4992
|
+
if (!isWatcherPoolInitialized()) deferBackgroundTask(() => initializeWatcherPoolInBackground(config.projectDir));
|
|
4693
4993
|
const wss = new WebSocketServer({ noServer: true });
|
|
4694
4994
|
const handler = applyWSSHandler({
|
|
4695
4995
|
wss,
|
|
@@ -4729,6 +5029,7 @@ async function createWebSocketServer(server, httpServer, config) {
|
|
|
4729
5029
|
server.searchService.dispose().catch(() => {});
|
|
4730
5030
|
server.dashboardOverviewService.dispose();
|
|
4731
5031
|
server.projectRecoveryService.dispose();
|
|
5032
|
+
server.translationCacheService.close();
|
|
4732
5033
|
}
|
|
4733
5034
|
};
|
|
4734
5035
|
}
|
|
@@ -4745,7 +5046,7 @@ async function startServer(config, setupApp) {
|
|
|
4745
5046
|
const port = await findAvailablePort(preferredPort);
|
|
4746
5047
|
const cliExecutor = new CliExecutor(new ConfigManager(config.projectDir), config.projectDir);
|
|
4747
5048
|
const kernel = new OpsxKernel(config.projectDir, cliExecutor);
|
|
4748
|
-
|
|
5049
|
+
deferBackgroundTask(() => initializeWatcherPoolInBackground(config.projectDir));
|
|
4749
5050
|
const server = createServer({
|
|
4750
5051
|
...config,
|
|
4751
5052
|
port,
|
|
@@ -4758,14 +5059,16 @@ async function startServer(config, setupApp) {
|
|
|
4758
5059
|
});
|
|
4759
5060
|
const wsServer = await createWebSocketServer(server, httpServer, { projectDir: config.projectDir });
|
|
4760
5061
|
const url = `http://localhost:${port}`;
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
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
|
+
});
|
|
4769
5072
|
});
|
|
4770
5073
|
return {
|
|
4771
5074
|
url,
|
|
@@ -4774,6 +5077,7 @@ async function startServer(config, setupApp) {
|
|
|
4774
5077
|
close: async () => {
|
|
4775
5078
|
kernel.dispose();
|
|
4776
5079
|
await server.hookRuntime.dispose();
|
|
5080
|
+
server.translationCacheService.close();
|
|
4777
5081
|
wsServer.close();
|
|
4778
5082
|
httpServer.close();
|
|
4779
5083
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openspecui/server",
|
|
3
|
-
"version": "3.7.
|
|
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",
|