@openspecui/server 2.0.0 → 2.1.0

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.
Files changed (2) hide show
  1. package/dist/index.mjs +62 -60
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,10 +1,13 @@
1
1
  import { createServer as createServer$1 } from "node:net";
2
2
  import { serve } from "@hono/node-server";
3
- import { CliExecutor, CodeEditorThemeSchema, ConfigManager, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalRendererEngineSchema, contextStorage, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, reactiveExists, reactiveReadDir, reactiveReadFile, reactiveStat, sniffGlobalCli } from "@openspecui/core";
3
+ import { CliExecutor, CodeEditorThemeSchema, ConfigManager, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, OpenSpecAdapter, OpenSpecWatcher, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalRendererEngineSchema, contextStorage, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, reactiveReadDir, reactiveReadFile, sniffGlobalCli } from "@openspecui/core";
4
4
  import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
5
5
  import { applyWSSHandler } from "@trpc/server/adapters/ws";
6
6
  import { Hono } from "hono";
7
7
  import { cors } from "hono/cors";
8
+ import { readFileSync } from "node:fs";
9
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
10
+ import { fileURLToPath } from "node:url";
8
11
  import { WebSocketServer } from "ws";
9
12
  import * as pty from "@lydell/node-pty";
10
13
  import { EventEmitter } from "events";
@@ -13,8 +16,7 @@ import { initTRPC } from "@trpc/server";
13
16
  import { observable } from "@trpc/server/observable";
14
17
  import { execFile } from "node:child_process";
15
18
  import { EventEmitter as EventEmitter$1 } from "node:events";
16
- import { mkdir, rm, writeFile } from "node:fs/promises";
17
- import { dirname, join, relative, resolve, sep } from "node:path";
19
+ import { mkdir, rm, stat, writeFile } from "node:fs/promises";
18
20
  import { promisify } from "node:util";
19
21
  import { z } from "zod";
20
22
  import { NodeWorkerSearchProvider } from "@openspecui/search/node";
@@ -999,43 +1001,51 @@ function endDashboardGitTask(error) {
999
1001
  if (error) dashboardGitTaskStatus.lastError = error instanceof Error ? error.message : String(error);
1000
1002
  emitDashboardGitTaskStatus();
1001
1003
  }
1002
- function parseGitDirFromDotGitFile(content) {
1003
- const line = content.split(/\r?\n/).map((item) => item.trim()).find((item) => item.startsWith("gitdir:"));
1004
- if (!line) return null;
1005
- const rawPath = line.slice(7).trim();
1006
- return rawPath.length > 0 ? rawPath : null;
1004
+ const DASHBOARD_GIT_REFRESH_STAMP_NAME = "openspecui-dashboard-git-refresh.stamp";
1005
+ async function resolveGitMetadataDir(projectDir) {
1006
+ try {
1007
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--git-dir"], {
1008
+ cwd: projectDir,
1009
+ maxBuffer: 1024 * 1024,
1010
+ encoding: "utf8"
1011
+ });
1012
+ const gitDirRaw = stdout.trim();
1013
+ if (!gitDirRaw) return null;
1014
+ const gitDirPath = resolve(projectDir, gitDirRaw);
1015
+ if (!(await stat(gitDirPath)).isDirectory()) return null;
1016
+ return gitDirPath;
1017
+ } catch {
1018
+ return null;
1019
+ }
1007
1020
  }
1008
- function getDashboardGitRefreshStampPath(projectDir) {
1009
- return join(projectDir, "openspec", ".openspecui-dashboard-git-refresh.stamp");
1021
+ async function resolveGitMetadataDirReactive(projectDir) {
1022
+ const gitMetadataDir = await resolveGitMetadataDir(projectDir);
1023
+ if (!gitMetadataDir) return null;
1024
+ await reactiveReadDir(gitMetadataDir, { includeHidden: true });
1025
+ return gitMetadataDir;
1026
+ }
1027
+ function getDashboardGitRefreshStampPath(gitMetadataDir) {
1028
+ return join(gitMetadataDir, DASHBOARD_GIT_REFRESH_STAMP_NAME);
1010
1029
  }
1011
1030
  async function touchDashboardGitRefreshStamp(projectDir, reason) {
1012
- const stampPath = getDashboardGitRefreshStampPath(projectDir);
1031
+ const gitMetadataDir = await resolveGitMetadataDir(projectDir);
1032
+ if (!gitMetadataDir) return { skipped: true };
1033
+ const stampPath = getDashboardGitRefreshStampPath(gitMetadataDir);
1013
1034
  await mkdir(dirname(stampPath), { recursive: true });
1014
1035
  await writeFile(stampPath, `${Date.now()} ${reason}\n`, "utf8");
1036
+ return { skipped: false };
1015
1037
  }
1016
1038
  async function registerDashboardGitReactiveDeps(projectDir) {
1017
1039
  await reactiveReadDir(projectDir, {
1018
1040
  includeHidden: true,
1019
1041
  exclude: ["node_modules"]
1020
1042
  });
1021
- await reactiveReadFile(getDashboardGitRefreshStampPath(projectDir));
1022
- const dotGitPath = join(projectDir, ".git");
1023
- if (!await reactiveExists(dotGitPath)) return;
1024
- const dotGitFileContent = await reactiveReadFile(dotGitPath);
1025
- if (dotGitFileContent !== null) {
1026
- const gitDirRaw = parseGitDirFromDotGitFile(dotGitFileContent);
1027
- if (!gitDirRaw) return;
1028
- const gitDirPath = resolve(projectDir, gitDirRaw);
1029
- await reactiveReadDir(gitDirPath, { includeHidden: true });
1030
- await reactiveReadFile(join(gitDirPath, "HEAD"));
1031
- await reactiveReadFile(join(gitDirPath, "index"));
1032
- await reactiveReadFile(join(gitDirPath, "packed-refs"));
1033
- return;
1034
- }
1035
- await reactiveReadDir(dotGitPath, { includeHidden: true });
1036
- await reactiveReadFile(join(dotGitPath, "HEAD"));
1037
- await reactiveReadFile(join(dotGitPath, "index"));
1038
- await reactiveReadFile(join(dotGitPath, "packed-refs"));
1043
+ const gitMetadataDir = await resolveGitMetadataDirReactive(projectDir);
1044
+ if (!gitMetadataDir) return;
1045
+ await reactiveReadFile(getDashboardGitRefreshStampPath(gitMetadataDir));
1046
+ await reactiveReadFile(join(gitMetadataDir, "HEAD"));
1047
+ await reactiveReadFile(join(gitMetadataDir, "index"));
1048
+ await reactiveReadFile(join(gitMetadataDir, "packed-refs"));
1039
1049
  }
1040
1050
  function requireChangeId(changeId) {
1041
1051
  if (!changeId) throw new Error("change is required");
@@ -1251,35 +1261,12 @@ async function fetchDashboardOverview(ctx, reason = "dashboard-refresh") {
1251
1261
  ctx.adapter.listChangesWithMeta(),
1252
1262
  ctx.adapter.listArchivedChangesWithMeta()
1253
1263
  ]);
1254
- await ctx.kernel.waitForWarmup();
1255
- await ctx.kernel.ensureStatusList();
1256
- const statusList = ctx.kernel.getStatusList();
1257
- const changeMetaMap = new Map(changeMetas.map((change) => [change.id, change]));
1258
- const activeChangeIds = new Set([...changeMetas.map((change) => change.id), ...statusList.map((status) => status.changeName)]);
1259
- const statusByChange = new Map(statusList.map((status) => [status.changeName, status]));
1260
- const activeChanges = (await Promise.all([...activeChangeIds].map(async (changeId) => {
1261
- const status = statusByChange.get(changeId);
1262
- const changeMeta = changeMetaMap.get(changeId);
1263
- const statInfo = await reactiveStat(join(ctx.projectDir, "openspec", "changes", changeId));
1264
- let progress = changeMeta?.progress ?? {
1265
- total: 0,
1266
- completed: 0
1267
- };
1268
- if (status) try {
1269
- await ctx.kernel.ensureApplyInstructions(changeId, status.schemaName);
1270
- const apply = ctx.kernel.getApplyInstructions(changeId, status.schemaName);
1271
- progress = {
1272
- total: apply.progress.total,
1273
- completed: apply.progress.complete
1274
- };
1275
- } catch {}
1276
- return {
1277
- id: changeId,
1278
- name: changeMeta?.name ?? changeId,
1279
- progress,
1280
- updatedAt: changeMeta?.updatedAt ?? statInfo?.mtime ?? 0
1281
- };
1282
- }))).sort((a, b) => b.updatedAt - a.updatedAt);
1264
+ const activeChanges = changeMetas.map((changeMeta) => ({
1265
+ id: changeMeta.id,
1266
+ name: changeMeta.name ?? changeMeta.id,
1267
+ progress: changeMeta.progress,
1268
+ updatedAt: changeMeta.updatedAt
1269
+ })).sort((a, b) => b.updatedAt - a.updatedAt);
1283
1270
  const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
1284
1271
  const change = await ctx.adapter.readArchivedChange(meta.id);
1285
1272
  if (!change) return null;
@@ -1631,6 +1618,7 @@ const configRouter = router({
1631
1618
  "system"
1632
1619
  ]).optional(),
1633
1620
  codeEditor: z.object({ theme: CodeEditorThemeSchema.optional() }).optional(),
1621
+ appBaseUrl: z.string().optional(),
1634
1622
  terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
1635
1623
  dashboard: DashboardConfigSchema.partial().optional()
1636
1624
  })).mutation(async ({ ctx, input }) => {
@@ -1638,9 +1626,10 @@ const configRouter = router({
1638
1626
  const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
1639
1627
  if (hasCliCommand && !hasCliArgs) {
1640
1628
  await ctx.configManager.setCliCommand(input.cli?.command ?? "");
1641
- if (input.theme !== void 0 || input.codeEditor !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0) await ctx.configManager.writeConfig({
1629
+ if (input.theme !== void 0 || input.codeEditor !== void 0 || input.appBaseUrl !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0) await ctx.configManager.writeConfig({
1642
1630
  theme: input.theme,
1643
1631
  codeEditor: input.codeEditor,
1632
+ appBaseUrl: input.appBaseUrl,
1644
1633
  terminal: input.terminal,
1645
1634
  dashboard: input.dashboard
1646
1635
  });
@@ -2315,6 +2304,17 @@ var SearchService = class {
2315
2304
  *
2316
2305
  * @module server
2317
2306
  */
2307
+ const __dirname = dirname(fileURLToPath(import.meta.url));
2308
+ function getServerPackageVersion() {
2309
+ try {
2310
+ const packageJsonPath = join(__dirname, "..", "package.json");
2311
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
2312
+ return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
2313
+ } catch {
2314
+ return "0.0.0";
2315
+ }
2316
+ }
2317
+ const SERVER_PACKAGE_VERSION = getServerPackageVersion();
2318
2318
  /**
2319
2319
  * Create an OpenSpecUI HTTP server with optional WebSocket support
2320
2320
  */
@@ -2335,7 +2335,9 @@ function createServer(config) {
2335
2335
  return c.json({
2336
2336
  status: "ok",
2337
2337
  projectDir: config.projectDir,
2338
- watcherEnabled: !!watcher
2338
+ projectName: basename(config.projectDir) || config.projectDir,
2339
+ watcherEnabled: !!watcher,
2340
+ openspecuiVersion: SERVER_PACKAGE_VERSION
2339
2341
  });
2340
2342
  });
2341
2343
  app.use("/trpc/*", async (c) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openspecui/server",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.mjs",
6
6
  "exports": {
@@ -20,8 +20,8 @@
20
20
  "yaml": "^2.8.0",
21
21
  "yargs": "^18.0.0",
22
22
  "zod": "^3.24.1",
23
- "@openspecui/search": "1.1.0",
24
- "@openspecui/core": "2.0.0"
23
+ "@openspecui/core": "2.1.0",
24
+ "@openspecui/search": "1.1.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.10.2",