@openspecui/core 1.2.0 → 1.5.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.
package/dist/index.mjs CHANGED
@@ -3,10 +3,10 @@ import { dirname, join } from "path";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { readFile as readFile$1, readdir, stat } from "node:fs/promises";
5
5
  import { dirname as dirname$1, join as join$1, matchesGlob, relative, resolve, sep } from "node:path";
6
- import { existsSync, realpathSync, utimesSync } from "node:fs";
6
+ import { existsSync, lstatSync, realpathSync } from "node:fs";
7
7
  import { z } from "zod";
8
- import { watch } from "fs";
9
8
  import { EventEmitter } from "events";
9
+ import { watch } from "fs";
10
10
  import { exec, spawn } from "child_process";
11
11
  import { promisify } from "util";
12
12
  import { parse } from "yaml";
@@ -220,14 +220,14 @@ var MarkdownParser = class {
220
220
  if (currentOperation === "RENAMED") {
221
221
  const fromMatch = line.match(/FROM:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
222
222
  const toMatch = line.match(/TO:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
223
- if (fromMatch) renameBuffer = {
224
- ...renameBuffer ?? {},
225
- from: fromMatch[1].trim()
226
- };
227
- if (toMatch) renameBuffer = {
228
- ...renameBuffer ?? {},
229
- to: toMatch[1].trim()
230
- };
223
+ if (fromMatch) {
224
+ if (!renameBuffer) renameBuffer = {};
225
+ renameBuffer.from = fromMatch[1].trim();
226
+ }
227
+ if (toMatch) {
228
+ if (!renameBuffer) renameBuffer = {};
229
+ renameBuffer.to = toMatch[1].trim();
230
+ }
231
231
  if (renameBuffer?.from && renameBuffer?.to) {
232
232
  deltas.push({
233
233
  spec: deltaSpec.specId,
@@ -312,86 +312,6 @@ var MarkdownParser = class {
312
312
  }
313
313
  };
314
314
 
315
- //#endregion
316
- //#region src/validator.ts
317
- /**
318
- * Validator for OpenSpec documents
319
- */
320
- var Validator = class {
321
- /**
322
- * Validate a spec document
323
- */
324
- validateSpec(spec) {
325
- const issues = [];
326
- if (!spec.overview || spec.overview.trim().length === 0) issues.push({
327
- severity: "ERROR",
328
- message: "Spec must have a Purpose/Overview section",
329
- path: "overview"
330
- });
331
- if (spec.requirements.length === 0) issues.push({
332
- severity: "ERROR",
333
- message: "Spec must have at least one requirement",
334
- path: "requirements"
335
- });
336
- for (const req of spec.requirements) {
337
- if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
338
- severity: "WARNING",
339
- message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
340
- path: `requirements.${req.id}`
341
- });
342
- if (req.scenarios.length === 0) issues.push({
343
- severity: "WARNING",
344
- message: `Requirement should have at least one scenario: ${req.id}`,
345
- path: `requirements.${req.id}.scenarios`
346
- });
347
- if (req.text.length > 1e3) issues.push({
348
- severity: "WARNING",
349
- message: `Requirement text is too long (max 1000 chars): ${req.id}`,
350
- path: `requirements.${req.id}.text`
351
- });
352
- }
353
- return {
354
- valid: issues.filter((i) => i.severity === "ERROR").length === 0,
355
- issues
356
- };
357
- }
358
- /**
359
- * Validate a change proposal
360
- */
361
- validateChange(change) {
362
- const issues = [];
363
- if (!change.why || change.why.length < 50) issues.push({
364
- severity: "ERROR",
365
- message: "Change \"Why\" section must be at least 50 characters",
366
- path: "why"
367
- });
368
- if (change.why && change.why.length > 500) issues.push({
369
- severity: "WARNING",
370
- message: "Change \"Why\" section should be under 500 characters",
371
- path: "why"
372
- });
373
- if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
374
- severity: "ERROR",
375
- message: "Change must have a \"What Changes\" section",
376
- path: "whatChanges"
377
- });
378
- if (change.deltas.length === 0) issues.push({
379
- severity: "WARNING",
380
- message: "Change should have at least one delta",
381
- path: "deltas"
382
- });
383
- if (change.deltas.length > 50) issues.push({
384
- severity: "WARNING",
385
- message: "Change has too many deltas (max 50)",
386
- path: "deltas"
387
- });
388
- return {
389
- valid: issues.filter((i) => i.severity === "ERROR").length === 0,
390
- issues
391
- };
392
- }
393
- };
394
-
395
315
  //#endregion
396
316
  //#region src/reactive-fs/reactive-state.ts
397
317
  /**
@@ -578,8 +498,10 @@ const DEFAULT_IGNORE = [
578
498
  ".git",
579
499
  "**/.DS_Store"
580
500
  ];
581
- /** 健康检查间隔 (ms) - 3秒 */
582
- const HEALTH_CHECK_INTERVAL_MS = 3e3;
501
+ /** 恢复重试间隔 (ms) */
502
+ const RECOVERY_INTERVAL_MS = 3e3;
503
+ /** 路径语义检查间隔 (ms) */
504
+ const PATH_LIVENESS_INTERVAL_MS = 3e3;
583
505
  /**
584
506
  * 项目监听器
585
507
  *
@@ -602,17 +524,25 @@ var ProjectWatcher = class {
602
524
  ignore;
603
525
  initialized = false;
604
526
  initPromise = null;
605
- healthCheckTimer = null;
606
- lastEventTime = 0;
607
- healthCheckPending = false;
608
- enableHealthCheck;
609
527
  reinitializeTimer = null;
610
528
  reinitializePending = false;
529
+ reinitializeReasonPending = null;
530
+ pathLivenessTimer = null;
531
+ projectDirFingerprint = null;
532
+ generation = 0;
533
+ reinitializeCount = 0;
534
+ lastReinitializeReason = null;
535
+ reinitializeReasonCounts = {
536
+ "drop-events": 0,
537
+ "watcher-error": 0,
538
+ "missing-project-dir": 0,
539
+ "project-dir-replaced": 0,
540
+ manual: 0
541
+ };
611
542
  constructor(projectDir, options = {}) {
612
543
  this.projectDir = getRealPath$1(projectDir);
613
544
  this.debounceMs = options.debounceMs ?? DEBOUNCE_MS$1;
614
545
  this.ignore = options.ignore ?? DEFAULT_IGNORE;
615
- this.enableHealthCheck = options.enableHealthCheck ?? true;
616
546
  }
617
547
  /**
618
548
  * 初始化 watcher
@@ -621,8 +551,11 @@ var ProjectWatcher = class {
621
551
  async init() {
622
552
  if (this.initialized) return;
623
553
  if (this.initPromise) return this.initPromise;
624
- this.initPromise = this.doInit();
625
- await this.initPromise;
554
+ this.initPromise = this.doInit().catch((error) => {
555
+ this.initPromise = null;
556
+ throw error;
557
+ });
558
+ return this.initPromise;
626
559
  }
627
560
  async doInit() {
628
561
  this.subscription = await (await import("@parcel/watcher")).subscribe(this.projectDir, (err, events) => {
@@ -633,43 +566,98 @@ var ProjectWatcher = class {
633
566
  this.handleEvents(events);
634
567
  }, { ignore: this.ignore });
635
568
  this.initialized = true;
636
- this.lastEventTime = Date.now();
637
- if (this.enableHealthCheck) this.startHealthCheck();
569
+ this.generation += 1;
570
+ this.projectDirFingerprint = this.getProjectDirFingerprint();
571
+ this.startPathLivenessMonitor();
638
572
  }
639
573
  /**
640
574
  * 处理 watcher 错误
641
- * 对于 FSEvents dropped 错误,触发延迟重建
575
+ * 统一走错误驱动重建流程
642
576
  */
643
577
  handleWatcherError(err) {
644
578
  if ((err.message || String(err)).includes("Events were dropped")) {
645
579
  if (!this.reinitializePending) {
646
580
  console.warn("[ProjectWatcher] FSEvents dropped events, scheduling reinitialize...");
647
- this.scheduleReinitialize();
581
+ this.scheduleReinitialize("drop-events");
648
582
  }
649
583
  return;
650
584
  }
651
- console.error("[ProjectWatcher] Error:", err);
585
+ console.error("[ProjectWatcher] Watcher error, scheduling reinitialize:", err);
586
+ this.scheduleReinitialize("watcher-error");
652
587
  }
653
588
  /**
654
589
  * 延迟重建 watcher(防抖,避免频繁重建)
655
590
  */
656
- scheduleReinitialize() {
591
+ scheduleReinitialize(reason) {
592
+ this.reinitializeReasonPending = reason;
657
593
  if (this.reinitializePending) return;
658
594
  this.reinitializePending = true;
659
595
  if (this.reinitializeTimer) clearTimeout(this.reinitializeTimer);
660
596
  this.reinitializeTimer = setTimeout(() => {
661
597
  this.reinitializeTimer = null;
662
598
  this.reinitializePending = false;
663
- console.log("[ProjectWatcher] Reinitializing due to FSEvents error...");
664
- this.reinitialize();
665
- }, 1e3);
599
+ const pendingReason = this.reinitializeReasonPending ?? reason;
600
+ this.reinitializeReasonPending = null;
601
+ console.log(`[ProjectWatcher] Reinitializing (reason: ${pendingReason})...`);
602
+ this.reinitialize(pendingReason);
603
+ }, RECOVERY_INTERVAL_MS);
604
+ this.reinitializeTimer.unref();
605
+ }
606
+ /**
607
+ * 读取项目目录指纹(目录不存在时返回 null)
608
+ * 用于检测 path 对应实体是否被替换(inode/dev 漂移)
609
+ */
610
+ getProjectDirFingerprint() {
611
+ try {
612
+ const stat$1 = lstatSync(this.projectDir);
613
+ return `${stat$1.dev}:${stat$1.ino}`;
614
+ } catch {
615
+ return null;
616
+ }
617
+ }
618
+ /**
619
+ * 启动路径语义监测(避免 watcher 绑定到已失效句柄)
620
+ */
621
+ startPathLivenessMonitor() {
622
+ this.stopPathLivenessMonitor();
623
+ this.pathLivenessTimer = setInterval(() => {
624
+ this.checkPathLiveness();
625
+ }, PATH_LIVENESS_INTERVAL_MS);
626
+ this.pathLivenessTimer.unref();
627
+ }
628
+ /**
629
+ * 停止路径语义监测
630
+ */
631
+ stopPathLivenessMonitor() {
632
+ if (this.pathLivenessTimer) {
633
+ clearInterval(this.pathLivenessTimer);
634
+ this.pathLivenessTimer = null;
635
+ }
636
+ }
637
+ /**
638
+ * 只读检查 projectDir 是否仍指向初始化时的目录实体
639
+ */
640
+ checkPathLiveness() {
641
+ if (!this.initialized || this.reinitializePending) return;
642
+ const current = this.getProjectDirFingerprint();
643
+ if (current === null) {
644
+ console.warn("[ProjectWatcher] Project directory missing, scheduling reinitialize...");
645
+ this.scheduleReinitialize("missing-project-dir");
646
+ return;
647
+ }
648
+ if (this.projectDirFingerprint === null) {
649
+ this.projectDirFingerprint = current;
650
+ return;
651
+ }
652
+ if (current !== this.projectDirFingerprint) {
653
+ console.warn("[ProjectWatcher] Project directory replaced, scheduling reinitialize...");
654
+ this.scheduleReinitialize("project-dir-replaced");
655
+ }
666
656
  }
667
657
  /**
668
658
  * 处理原始事件
669
659
  */
670
660
  handleEvents(events) {
671
- this.lastEventTime = Date.now();
672
- this.healthCheckPending = false;
673
661
  const watchEvents = events.map((e) => ({
674
662
  type: e.type,
675
663
  path: e.path
@@ -757,60 +745,29 @@ var ProjectWatcher = class {
757
745
  return this.initialized;
758
746
  }
759
747
  /**
760
- * 启动健康检查定时器
761
- */
762
- startHealthCheck() {
763
- this.stopHealthCheck();
764
- this.healthCheckTimer = setInterval(() => {
765
- this.performHealthCheck();
766
- }, HEALTH_CHECK_INTERVAL_MS);
767
- this.healthCheckTimer.unref();
768
- }
769
- /**
770
- * 停止健康检查定时器
748
+ * 获取 watcher 运行时状态
771
749
  */
772
- stopHealthCheck() {
773
- if (this.healthCheckTimer) {
774
- clearInterval(this.healthCheckTimer);
775
- this.healthCheckTimer = null;
776
- }
777
- this.healthCheckPending = false;
778
- }
779
- /**
780
- * 执行健康检查
781
- *
782
- * 工作流程:
783
- * 1. 如果最近有事件,无需检查
784
- * 2. 如果上次探测还在等待中,说明 watcher 可能失效,尝试重建
785
- * 3. 否则,创建临时文件触发事件,等待下次检查验证
786
- */
787
- async performHealthCheck() {
788
- if (Date.now() - this.lastEventTime < HEALTH_CHECK_INTERVAL_MS) {
789
- this.healthCheckPending = false;
790
- return;
791
- }
792
- if (this.healthCheckPending) {
793
- console.warn("[ProjectWatcher] Health check failed, watcher appears stale. Reinitializing...");
794
- await this.reinitialize();
795
- return;
796
- }
797
- this.healthCheckPending = true;
798
- this.sendProbe();
750
+ get runtimeStatus() {
751
+ return {
752
+ generation: this.generation,
753
+ reinitializeCount: this.reinitializeCount,
754
+ lastReinitializeReason: this.lastReinitializeReason,
755
+ reinitializeReasonCounts: { ...this.reinitializeReasonCounts }
756
+ };
799
757
  }
800
758
  /**
801
- * 发送探测:通过 utimesSync 修改项目目录的时间戳来触发 watcher 事件
759
+ * 记录重建统计
802
760
  */
803
- sendProbe() {
804
- try {
805
- const now = /* @__PURE__ */ new Date();
806
- utimesSync(this.projectDir, now, now);
807
- } catch {}
761
+ markReinitialized(reason) {
762
+ this.reinitializeCount += 1;
763
+ this.lastReinitializeReason = reason;
764
+ this.reinitializeReasonCounts[reason] += 1;
808
765
  }
809
766
  /**
810
767
  * 重新初始化 watcher
811
768
  */
812
- async reinitialize() {
813
- this.stopHealthCheck();
769
+ async reinitialize(reason) {
770
+ this.stopPathLivenessMonitor();
814
771
  if (this.subscription) {
815
772
  try {
816
773
  await this.subscription.unsubscribe();
@@ -819,38 +776,50 @@ var ProjectWatcher = class {
819
776
  }
820
777
  this.initialized = false;
821
778
  this.initPromise = null;
822
- this.healthCheckPending = false;
779
+ this.projectDirFingerprint = null;
823
780
  if (!existsSync(this.projectDir)) {
824
781
  console.warn("[ProjectWatcher] Project directory does not exist, waiting for it to be created...");
825
- this.waitForProjectDir();
782
+ this.waitForProjectDir("missing-project-dir");
826
783
  return;
827
784
  }
828
785
  try {
829
786
  await this.init();
787
+ this.markReinitialized(reason);
830
788
  console.log("[ProjectWatcher] Reinitialized successfully");
831
789
  } catch (err) {
832
790
  console.error("[ProjectWatcher] Failed to reinitialize:", err);
833
- setTimeout(() => this.reinitialize(), HEALTH_CHECK_INTERVAL_MS);
791
+ this.scheduleReinitialize(reason);
834
792
  }
835
793
  }
836
794
  /**
837
795
  * 等待项目目录被创建
838
796
  */
839
- waitForProjectDir() {
840
- const checkInterval = setInterval(() => {
841
- if (existsSync(this.projectDir)) {
842
- clearInterval(checkInterval);
843
- console.log("[ProjectWatcher] Project directory created, reinitializing...");
844
- this.reinitialize();
797
+ waitForProjectDir(reason) {
798
+ this.reinitializeReasonPending = reason;
799
+ this.reinitializePending = true;
800
+ if (this.reinitializeTimer) {
801
+ clearTimeout(this.reinitializeTimer);
802
+ this.reinitializeTimer = null;
803
+ }
804
+ this.reinitializeTimer = setTimeout(() => {
805
+ this.reinitializeTimer = null;
806
+ this.reinitializePending = false;
807
+ if (!existsSync(this.projectDir)) {
808
+ this.waitForProjectDir(reason);
809
+ return;
845
810
  }
846
- }, HEALTH_CHECK_INTERVAL_MS);
847
- checkInterval.unref();
811
+ const pendingReason = this.reinitializeReasonPending ?? reason;
812
+ this.reinitializeReasonPending = null;
813
+ console.log("[ProjectWatcher] Project directory created, reinitializing...");
814
+ this.reinitialize(pendingReason);
815
+ }, RECOVERY_INTERVAL_MS);
816
+ this.reinitializeTimer.unref();
848
817
  }
849
818
  /**
850
819
  * 关闭 watcher
851
820
  */
852
821
  async close() {
853
- this.stopHealthCheck();
822
+ this.stopPathLivenessMonitor();
854
823
  if (this.debounceTimer) {
855
824
  clearTimeout(this.debounceTimer);
856
825
  this.debounceTimer = null;
@@ -860,6 +829,7 @@ var ProjectWatcher = class {
860
829
  this.reinitializeTimer = null;
861
830
  }
862
831
  this.reinitializePending = false;
832
+ this.reinitializeReasonPending = null;
863
833
  if (this.subscription) {
864
834
  await this.subscription.unsubscribe();
865
835
  this.subscription = null;
@@ -868,6 +838,7 @@ var ProjectWatcher = class {
868
838
  this.pendingEvents = [];
869
839
  this.initialized = false;
870
840
  this.initPromise = null;
841
+ this.projectDirFingerprint = null;
871
842
  }
872
843
  };
873
844
  /**
@@ -1032,6 +1003,22 @@ function isWatcherPoolInitialized() {
1032
1003
  function getWatchedProjectDir() {
1033
1004
  return globalProjectDir;
1034
1005
  }
1006
+ /**
1007
+ * 获取 watcher 运行时状态
1008
+ */
1009
+ function getWatcherRuntimeStatus() {
1010
+ if (!globalProjectWatcher) return null;
1011
+ const runtime = globalProjectWatcher.runtimeStatus;
1012
+ return {
1013
+ projectDir: globalProjectDir,
1014
+ initialized: globalProjectWatcher.isInitialized,
1015
+ subscriptionCount: globalProjectWatcher.subscriptionCount,
1016
+ generation: runtime.generation,
1017
+ reinitializeCount: runtime.reinitializeCount,
1018
+ lastReinitializeReason: runtime.lastReinitializeReason,
1019
+ reinitializeReasonCounts: runtime.reinitializeReasonCounts
1020
+ };
1021
+ }
1035
1022
 
1036
1023
  //#endregion
1037
1024
  //#region src/reactive-fs/reactive-fs.ts
@@ -1229,6 +1216,86 @@ function getCacheSize() {
1229
1216
  return stateCache$1.size;
1230
1217
  }
1231
1218
 
1219
+ //#endregion
1220
+ //#region src/validator.ts
1221
+ /**
1222
+ * Validator for OpenSpec documents
1223
+ */
1224
+ var Validator = class {
1225
+ /**
1226
+ * Validate a spec document
1227
+ */
1228
+ validateSpec(spec) {
1229
+ const issues = [];
1230
+ if (!spec.overview || spec.overview.trim().length === 0) issues.push({
1231
+ severity: "ERROR",
1232
+ message: "Spec must have a Purpose/Overview section",
1233
+ path: "overview"
1234
+ });
1235
+ if (spec.requirements.length === 0) issues.push({
1236
+ severity: "ERROR",
1237
+ message: "Spec must have at least one requirement",
1238
+ path: "requirements"
1239
+ });
1240
+ for (const req of spec.requirements) {
1241
+ if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
1242
+ severity: "WARNING",
1243
+ message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
1244
+ path: `requirements.${req.id}`
1245
+ });
1246
+ if (req.scenarios.length === 0) issues.push({
1247
+ severity: "WARNING",
1248
+ message: `Requirement should have at least one scenario: ${req.id}`,
1249
+ path: `requirements.${req.id}.scenarios`
1250
+ });
1251
+ if (req.text.length > 1e3) issues.push({
1252
+ severity: "WARNING",
1253
+ message: `Requirement text is too long (max 1000 chars): ${req.id}`,
1254
+ path: `requirements.${req.id}.text`
1255
+ });
1256
+ }
1257
+ return {
1258
+ valid: issues.filter((i) => i.severity === "ERROR").length === 0,
1259
+ issues
1260
+ };
1261
+ }
1262
+ /**
1263
+ * Validate a change proposal
1264
+ */
1265
+ validateChange(change) {
1266
+ const issues = [];
1267
+ if (!change.why || change.why.length < 50) issues.push({
1268
+ severity: "ERROR",
1269
+ message: "Change \"Why\" section must be at least 50 characters",
1270
+ path: "why"
1271
+ });
1272
+ if (change.why && change.why.length > 500) issues.push({
1273
+ severity: "WARNING",
1274
+ message: "Change \"Why\" section should be under 500 characters",
1275
+ path: "why"
1276
+ });
1277
+ if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
1278
+ severity: "ERROR",
1279
+ message: "Change must have a \"What Changes\" section",
1280
+ path: "whatChanges"
1281
+ });
1282
+ if (change.deltas.length === 0) issues.push({
1283
+ severity: "WARNING",
1284
+ message: "Change should have at least one delta",
1285
+ path: "deltas"
1286
+ });
1287
+ if (change.deltas.length > 50) issues.push({
1288
+ severity: "WARNING",
1289
+ message: "Change has too many deltas (max 50)",
1290
+ path: "deltas"
1291
+ });
1292
+ return {
1293
+ valid: issues.filter((i) => i.severity === "ERROR").length === 0,
1294
+ issues
1295
+ };
1296
+ }
1297
+ };
1298
+
1232
1299
  //#endregion
1233
1300
  //#region src/adapter.ts
1234
1301
  /**
@@ -1890,6 +1957,11 @@ const CURSOR_STYLE_VALUES = [
1890
1957
  "underline",
1891
1958
  "bar"
1892
1959
  ];
1960
+ const TERMINAL_RENDERER_ENGINE_VALUES = ["xterm", "ghostty"];
1961
+ const TerminalRendererEngineSchema = z.enum(TERMINAL_RENDERER_ENGINE_VALUES);
1962
+ function isTerminalRendererEngine(value) {
1963
+ return TERMINAL_RENDERER_ENGINE_VALUES.includes(value);
1964
+ }
1893
1965
  const BASE_PACKAGE_MANAGER_RUNNERS = [
1894
1966
  {
1895
1967
  id: "npx",
@@ -2227,8 +2299,10 @@ const TerminalConfigSchema = z.object({
2227
2299
  fontFamily: z.string().default(""),
2228
2300
  cursorBlink: z.boolean().default(true),
2229
2301
  cursorStyle: z.enum(CURSOR_STYLE_VALUES).default("block"),
2230
- scrollback: z.number().min(0).max(1e5).default(1e3)
2302
+ scrollback: z.number().min(0).max(1e5).default(1e3),
2303
+ rendererEngine: z.string().default("xterm")
2231
2304
  });
2305
+ const DashboardConfigSchema = z.object({ trendPointLimit: z.number().int().min(20).max(500).default(100) });
2232
2306
  /**
2233
2307
  * OpenSpecUI 配置 Schema
2234
2308
  *
@@ -2240,13 +2314,15 @@ const OpenSpecUIConfigSchema = z.object({
2240
2314
  args: z.array(z.string()).optional()
2241
2315
  }).default({}),
2242
2316
  theme: z.enum(THEME_VALUES).default("system"),
2243
- terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({}))
2317
+ terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({})),
2318
+ dashboard: DashboardConfigSchema.default(DashboardConfigSchema.parse({}))
2244
2319
  });
2245
2320
  /** 默认配置(静态,用于测试和类型) */
2246
2321
  const DEFAULT_CONFIG = {
2247
2322
  cli: {},
2248
2323
  theme: "system",
2249
- terminal: TerminalConfigSchema.parse({})
2324
+ terminal: TerminalConfigSchema.parse({}),
2325
+ dashboard: DashboardConfigSchema.parse({})
2250
2326
  };
2251
2327
  /**
2252
2328
  * 配置管理器
@@ -2312,6 +2388,10 @@ var ConfigManager = class {
2312
2388
  terminal: {
2313
2389
  ...current.terminal,
2314
2390
  ...config.terminal
2391
+ },
2392
+ dashboard: {
2393
+ ...current.dashboard,
2394
+ ...config.dashboard
2315
2395
  }
2316
2396
  };
2317
2397
  const serialized = JSON.stringify(merged, null, 2);
@@ -3048,6 +3128,17 @@ async function isToolConfigured(projectDir, toolId) {
3048
3128
  return (await getConfiguredTools(projectDir)).includes(toolId);
3049
3129
  }
3050
3130
 
3131
+ //#endregion
3132
+ //#region src/dashboard-types.ts
3133
+ const DASHBOARD_METRIC_KEYS = [
3134
+ "specifications",
3135
+ "requirements",
3136
+ "activeChanges",
3137
+ "inProgressChanges",
3138
+ "completedChanges",
3139
+ "taskCompletionPercent"
3140
+ ];
3141
+
3051
3142
  //#endregion
3052
3143
  //#region src/opsx-types.ts
3053
3144
  /** Check if an outputPath contains glob pattern characters */
@@ -3908,4 +3999,4 @@ const PtyServerMessageSchema = z.discriminatedUnion("type", [
3908
3999
  ]);
3909
4000
 
3910
4001
  //#endregion
3911
- export { AI_TOOLS, ApplyInstructionsSchema, ApplyTaskSchema, ArtifactInstructionsSchema, ArtifactStatusSchema, ChangeFileSchema, ChangeSchema, ChangeStatusSchema, CliExecutor, ConfigManager, DEFAULT_CONFIG, DeltaOperationType, DeltaSchema, DeltaSpecSchema, DependencyInfoSchema, MarkdownParser, OpenSpecAdapter, OpenSpecUIConfigSchema, OpenSpecWatcher, OpsxKernel, ProjectWatcher, PtyAttachMessageSchema, PtyBufferResponseSchema, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, PtyPlatformSchema, PtyResizeMessageSchema, PtyServerMessageSchema, PtyTitleResponseSchema, ReactiveContext, ReactiveState, RequirementSchema, SchemaArtifactSchema, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, SpecSchema, TaskSchema, TemplatesSchema, TerminalConfigSchema, Validator, acquireWatcher, buildCliRunnerCandidates, clearCache, closeAllProjectWatchers, closeAllWatchers, contextStorage, createCleanCliEnv, createFileChangeObservable, getActiveWatcherCount, getAllToolIds, getAllTools, getAvailableToolIds, getAvailableTools, getCacheSize, getConfiguredTools, getDefaultCliCommand, getDefaultCliCommandString, getProjectWatcher, getToolById, getWatchedProjectDir, initWatcherPool, isGlobPattern, isToolConfigured, isWatcherPoolInitialized, parseCliCommand, reactiveExists, reactiveReadDir, reactiveReadFile, reactiveStat, sniffGlobalCli };
4002
+ export { AI_TOOLS, ApplyInstructionsSchema, ApplyTaskSchema, ArtifactInstructionsSchema, ArtifactStatusSchema, ChangeFileSchema, ChangeSchema, ChangeStatusSchema, CliExecutor, ConfigManager, DASHBOARD_METRIC_KEYS, DEFAULT_CONFIG, DashboardConfigSchema, DeltaOperationType, DeltaSchema, DeltaSpecSchema, DependencyInfoSchema, MarkdownParser, OpenSpecAdapter, OpenSpecUIConfigSchema, OpenSpecWatcher, OpsxKernel, ProjectWatcher, PtyAttachMessageSchema, PtyBufferResponseSchema, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, PtyPlatformSchema, PtyResizeMessageSchema, PtyServerMessageSchema, PtyTitleResponseSchema, ReactiveContext, ReactiveState, RequirementSchema, SchemaArtifactSchema, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, SpecSchema, TaskSchema, TemplatesSchema, TerminalConfigSchema, TerminalRendererEngineSchema, Validator, acquireWatcher, buildCliRunnerCandidates, clearCache, closeAllProjectWatchers, closeAllWatchers, contextStorage, createCleanCliEnv, createFileChangeObservable, getActiveWatcherCount, getAllToolIds, getAllTools, getAvailableToolIds, getAvailableTools, getCacheSize, getConfiguredTools, getDefaultCliCommand, getDefaultCliCommandString, getProjectWatcher, getToolById, getWatchedProjectDir, getWatcherRuntimeStatus, initWatcherPool, isGlobPattern, isTerminalRendererEngine, isToolConfigured, isWatcherPoolInitialized, parseCliCommand, reactiveExists, reactiveReadDir, reactiveReadFile, reactiveStat, sniffGlobalCli };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openspecui/core",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "description": "Core OpenSpec adapter and parser",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",