@openspecui/core 1.1.2 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenSpecUI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.d.mts CHANGED
@@ -1022,53 +1022,6 @@ declare function clearCache(path?: string): void;
1022
1022
  */
1023
1023
  declare function getCacheSize(): number;
1024
1024
  //#endregion
1025
- //#region src/reactive-fs/watcher-pool.d.ts
1026
- /**
1027
- * 初始化 watcher pool
1028
- *
1029
- * 必须在使用 acquireWatcher 之前调用。
1030
- * 通常由 server 在启动时调用。
1031
- *
1032
- * @param projectDir 项目根目录
1033
- */
1034
- declare function initWatcherPool(projectDir: string): Promise<void>;
1035
- /**
1036
- * 获取或创建文件/目录监听器
1037
- *
1038
- * 特性:
1039
- * - 使用 @parcel/watcher 监听项目根目录
1040
- * - 自动处理新创建的目录(解决 init 后无法监听的问题)
1041
- * - 同一路径共享订阅
1042
- * - 引用计数管理生命周期
1043
- * - 内置防抖机制
1044
- *
1045
- * @param path 要监听的路径
1046
- * @param onChange 变更回调
1047
- * @param options 监听选项
1048
- * @returns 释放函数,调用后取消订阅
1049
- */
1050
- declare function acquireWatcher(path: string, onChange: () => void, options?: {
1051
- recursive?: boolean;
1052
- debounceMs?: number;
1053
- onError?: () => void;
1054
- }): () => void;
1055
- /**
1056
- * 获取当前活跃的监听器数量(用于调试)
1057
- */
1058
- declare function getActiveWatcherCount(): number;
1059
- /**
1060
- * 关闭所有监听器(用于测试清理)
1061
- */
1062
- declare function closeAllWatchers(): Promise<void>;
1063
- /**
1064
- * 检查 watcher pool 是否已初始化
1065
- */
1066
- declare function isWatcherPoolInitialized(): boolean;
1067
- /**
1068
- * 获取当前监听的项目目录
1069
- */
1070
- declare function getWatchedProjectDir(): string | null;
1071
- //#endregion
1072
1025
  //#region src/reactive-fs/project-watcher.d.ts
1073
1026
  /**
1074
1027
  * 事件类型
@@ -1085,6 +1038,15 @@ interface WatchEvent {
1085
1038
  * 路径订阅回调
1086
1039
  */
1087
1040
  type PathCallback = (events: WatchEvent[]) => void;
1041
+ /** watcher 重建原因 */
1042
+ type ProjectWatcherReinitializeReason = 'drop-events' | 'watcher-error' | 'missing-project-dir' | 'project-dir-replaced' | 'manual';
1043
+ /** watcher 运行时状态(用于调试和运维观测) */
1044
+ interface ProjectWatcherRuntimeStatus {
1045
+ generation: number;
1046
+ reinitializeCount: number;
1047
+ lastReinitializeReason: ProjectWatcherReinitializeReason | null;
1048
+ reinitializeReasonCounts: Readonly<Record<ProjectWatcherReinitializeReason, number>>;
1049
+ }
1088
1050
  /**
1089
1051
  * 项目监听器
1090
1052
  *
@@ -1107,17 +1069,18 @@ declare class ProjectWatcher {
1107
1069
  private ignore;
1108
1070
  private initialized;
1109
1071
  private initPromise;
1110
- private healthCheckTimer;
1111
- private lastEventTime;
1112
- private healthCheckPending;
1113
- private enableHealthCheck;
1114
1072
  private reinitializeTimer;
1115
1073
  private reinitializePending;
1074
+ private reinitializeReasonPending;
1075
+ private pathLivenessTimer;
1076
+ private projectDirFingerprint;
1077
+ private generation;
1078
+ private reinitializeCount;
1079
+ private lastReinitializeReason;
1080
+ private reinitializeReasonCounts;
1116
1081
  constructor(projectDir: string, options?: {
1117
1082
  debounceMs?: number;
1118
1083
  ignore?: string[];
1119
- /** 是否启用健康检查(默认 true) */
1120
- enableHealthCheck?: boolean;
1121
1084
  });
1122
1085
  /**
1123
1086
  * 初始化 watcher
@@ -1127,13 +1090,30 @@ declare class ProjectWatcher {
1127
1090
  private doInit;
1128
1091
  /**
1129
1092
  * 处理 watcher 错误
1130
- * 对于 FSEvents dropped 错误,触发延迟重建
1093
+ * 统一走错误驱动重建流程
1131
1094
  */
1132
1095
  private handleWatcherError;
1133
1096
  /**
1134
1097
  * 延迟重建 watcher(防抖,避免频繁重建)
1135
1098
  */
1136
1099
  private scheduleReinitialize;
1100
+ /**
1101
+ * 读取项目目录指纹(目录不存在时返回 null)
1102
+ * 用于检测 path 对应实体是否被替换(inode/dev 漂移)
1103
+ */
1104
+ private getProjectDirFingerprint;
1105
+ /**
1106
+ * 启动路径语义监测(避免 watcher 绑定到已失效句柄)
1107
+ */
1108
+ private startPathLivenessMonitor;
1109
+ /**
1110
+ * 停止路径语义监测
1111
+ */
1112
+ private stopPathLivenessMonitor;
1113
+ /**
1114
+ * 只读检查 projectDir 是否仍指向初始化时的目录实体
1115
+ */
1116
+ private checkPathLiveness;
1137
1117
  /**
1138
1118
  * 处理原始事件
1139
1119
  */
@@ -1180,26 +1160,13 @@ declare class ProjectWatcher {
1180
1160
  */
1181
1161
  get isInitialized(): boolean;
1182
1162
  /**
1183
- * 启动健康检查定时器
1163
+ * 获取 watcher 运行时状态
1184
1164
  */
1185
- private startHealthCheck;
1165
+ get runtimeStatus(): ProjectWatcherRuntimeStatus;
1186
1166
  /**
1187
- * 停止健康检查定时器
1167
+ * 记录重建统计
1188
1168
  */
1189
- private stopHealthCheck;
1190
- /**
1191
- * 执行健康检查
1192
- *
1193
- * 工作流程:
1194
- * 1. 如果最近有事件,无需检查
1195
- * 2. 如果上次探测还在等待中,说明 watcher 可能失效,尝试重建
1196
- * 3. 否则,创建临时文件触发事件,等待下次检查验证
1197
- */
1198
- private performHealthCheck;
1199
- /**
1200
- * 发送探测:通过 utimesSync 修改项目目录的时间戳来触发 watcher 事件
1201
- */
1202
- private sendProbe;
1169
+ private markReinitialized;
1203
1170
  /**
1204
1171
  * 重新初始化 watcher
1205
1172
  */
@@ -1222,6 +1189,63 @@ declare function getProjectWatcher(projectDir: string, options?: ConstructorPara
1222
1189
  */
1223
1190
  declare function closeAllProjectWatchers(): Promise<void>;
1224
1191
  //#endregion
1192
+ //#region src/reactive-fs/watcher-pool.d.ts
1193
+ /** watcher 运行时状态(供 server 订阅) */
1194
+ interface WatcherRuntimeStatus extends ProjectWatcherRuntimeStatus {
1195
+ projectDir: string | null;
1196
+ initialized: boolean;
1197
+ subscriptionCount: number;
1198
+ }
1199
+ /**
1200
+ * 初始化 watcher pool
1201
+ *
1202
+ * 必须在使用 acquireWatcher 之前调用。
1203
+ * 通常由 server 在启动时调用。
1204
+ *
1205
+ * @param projectDir 项目根目录
1206
+ */
1207
+ declare function initWatcherPool(projectDir: string): Promise<void>;
1208
+ /**
1209
+ * 获取或创建文件/目录监听器
1210
+ *
1211
+ * 特性:
1212
+ * - 使用 @parcel/watcher 监听项目根目录
1213
+ * - 自动处理新创建的目录(解决 init 后无法监听的问题)
1214
+ * - 同一路径共享订阅
1215
+ * - 引用计数管理生命周期
1216
+ * - 内置防抖机制
1217
+ *
1218
+ * @param path 要监听的路径
1219
+ * @param onChange 变更回调
1220
+ * @param options 监听选项
1221
+ * @returns 释放函数,调用后取消订阅
1222
+ */
1223
+ declare function acquireWatcher(path: string, onChange: () => void, options?: {
1224
+ recursive?: boolean;
1225
+ debounceMs?: number;
1226
+ onError?: () => void;
1227
+ }): () => void;
1228
+ /**
1229
+ * 获取当前活跃的监听器数量(用于调试)
1230
+ */
1231
+ declare function getActiveWatcherCount(): number;
1232
+ /**
1233
+ * 关闭所有监听器(用于测试清理)
1234
+ */
1235
+ declare function closeAllWatchers(): Promise<void>;
1236
+ /**
1237
+ * 检查 watcher pool 是否已初始化
1238
+ */
1239
+ declare function isWatcherPoolInitialized(): boolean;
1240
+ /**
1241
+ * 获取当前监听的项目目录
1242
+ */
1243
+ declare function getWatchedProjectDir(): string | null;
1244
+ /**
1245
+ * 获取 watcher 运行时状态
1246
+ */
1247
+ declare function getWatcherRuntimeStatus(): WatcherRuntimeStatus | null;
1248
+ //#endregion
1225
1249
  //#region src/watcher.d.ts
1226
1250
  /**
1227
1251
  * File change event types
@@ -1284,6 +1308,9 @@ declare function createFileChangeObservable(watcher: OpenSpecWatcher): {
1284
1308
  };
1285
1309
  //#endregion
1286
1310
  //#region src/config.d.ts
1311
+ declare const TerminalRendererEngineSchema: z.ZodEnum<["xterm", "ghostty"]>;
1312
+ type TerminalRendererEngine = z.infer<typeof TerminalRendererEngineSchema>;
1313
+ declare function isTerminalRendererEngine(value: string): value is TerminalRendererEngine;
1287
1314
  type RunnerId = 'configured' | 'openspec' | 'npx' | 'bunx' | 'deno' | 'pnpm' | 'yarn';
1288
1315
  interface CliRunnerCandidate {
1289
1316
  id: RunnerId;
@@ -1355,20 +1382,31 @@ declare const TerminalConfigSchema: z.ZodObject<{
1355
1382
  cursorBlink: z.ZodDefault<z.ZodBoolean>;
1356
1383
  cursorStyle: z.ZodDefault<z.ZodEnum<["block", "underline", "bar"]>>;
1357
1384
  scrollback: z.ZodDefault<z.ZodNumber>;
1385
+ rendererEngine: z.ZodDefault<z.ZodString>;
1358
1386
  }, "strip", z.ZodTypeAny, {
1359
1387
  fontSize: number;
1360
1388
  fontFamily: string;
1361
1389
  cursorBlink: boolean;
1362
1390
  cursorStyle: "block" | "underline" | "bar";
1363
1391
  scrollback: number;
1392
+ rendererEngine: string;
1364
1393
  }, {
1365
1394
  fontSize?: number | undefined;
1366
1395
  fontFamily?: string | undefined;
1367
1396
  cursorBlink?: boolean | undefined;
1368
1397
  cursorStyle?: "block" | "underline" | "bar" | undefined;
1369
1398
  scrollback?: number | undefined;
1399
+ rendererEngine?: string | undefined;
1370
1400
  }>;
1371
1401
  type TerminalConfig = z.infer<typeof TerminalConfigSchema>;
1402
+ declare const DashboardConfigSchema: z.ZodObject<{
1403
+ trendPointLimit: z.ZodDefault<z.ZodNumber>;
1404
+ }, "strip", z.ZodTypeAny, {
1405
+ trendPointLimit: number;
1406
+ }, {
1407
+ trendPointLimit?: number | undefined;
1408
+ }>;
1409
+ type DashboardConfig = z.infer<typeof DashboardConfigSchema>;
1372
1410
  /**
1373
1411
  * OpenSpecUI 配置 Schema
1374
1412
  *
@@ -1397,18 +1435,29 @@ declare const OpenSpecUIConfigSchema: z.ZodObject<{
1397
1435
  cursorBlink: z.ZodDefault<z.ZodBoolean>;
1398
1436
  cursorStyle: z.ZodDefault<z.ZodEnum<["block", "underline", "bar"]>>;
1399
1437
  scrollback: z.ZodDefault<z.ZodNumber>;
1438
+ rendererEngine: z.ZodDefault<z.ZodString>;
1400
1439
  }, "strip", z.ZodTypeAny, {
1401
1440
  fontSize: number;
1402
1441
  fontFamily: string;
1403
1442
  cursorBlink: boolean;
1404
1443
  cursorStyle: "block" | "underline" | "bar";
1405
1444
  scrollback: number;
1445
+ rendererEngine: string;
1406
1446
  }, {
1407
1447
  fontSize?: number | undefined;
1408
1448
  fontFamily?: string | undefined;
1409
1449
  cursorBlink?: boolean | undefined;
1410
1450
  cursorStyle?: "block" | "underline" | "bar" | undefined;
1411
1451
  scrollback?: number | undefined;
1452
+ rendererEngine?: string | undefined;
1453
+ }>>;
1454
+ /** Dashboard 配置 */
1455
+ dashboard: z.ZodDefault<z.ZodObject<{
1456
+ trendPointLimit: z.ZodDefault<z.ZodNumber>;
1457
+ }, "strip", z.ZodTypeAny, {
1458
+ trendPointLimit: number;
1459
+ }, {
1460
+ trendPointLimit?: number | undefined;
1412
1461
  }>>;
1413
1462
  }, "strip", z.ZodTypeAny, {
1414
1463
  cli: {
@@ -1422,6 +1471,10 @@ declare const OpenSpecUIConfigSchema: z.ZodObject<{
1422
1471
  cursorBlink: boolean;
1423
1472
  cursorStyle: "block" | "underline" | "bar";
1424
1473
  scrollback: number;
1474
+ rendererEngine: string;
1475
+ };
1476
+ dashboard: {
1477
+ trendPointLimit: number;
1425
1478
  };
1426
1479
  }, {
1427
1480
  cli?: {
@@ -1435,6 +1488,10 @@ declare const OpenSpecUIConfigSchema: z.ZodObject<{
1435
1488
  cursorBlink?: boolean | undefined;
1436
1489
  cursorStyle?: "block" | "underline" | "bar" | undefined;
1437
1490
  scrollback?: number | undefined;
1491
+ rendererEngine?: string | undefined;
1492
+ } | undefined;
1493
+ dashboard?: {
1494
+ trendPointLimit?: number | undefined;
1438
1495
  } | undefined;
1439
1496
  }>;
1440
1497
  type OpenSpecUIConfig = z.infer<typeof OpenSpecUIConfigSchema>;
@@ -1445,6 +1502,7 @@ type OpenSpecUIConfigUpdate = {
1445
1502
  };
1446
1503
  theme?: OpenSpecUIConfig['theme'];
1447
1504
  terminal?: Partial<TerminalConfig>;
1505
+ dashboard?: Partial<DashboardConfig>;
1448
1506
  };
1449
1507
  /** 默认配置(静态,用于测试和类型) */
1450
1508
  declare const DEFAULT_CONFIG: OpenSpecUIConfig;
@@ -1663,6 +1721,99 @@ declare function getConfiguredTools(projectDir: string): Promise<string[]>;
1663
1721
  */
1664
1722
  declare function isToolConfigured(projectDir: string, toolId: string): Promise<boolean>;
1665
1723
  //#endregion
1724
+ //#region src/dashboard-types.d.ts
1725
+ declare const DASHBOARD_METRIC_KEYS: readonly ["specifications", "requirements", "activeChanges", "inProgressChanges", "completedChanges", "taskCompletionPercent"];
1726
+ type DashboardMetricKey = (typeof DASHBOARD_METRIC_KEYS)[number];
1727
+ interface DashboardTrendPoint {
1728
+ ts: number;
1729
+ value: number;
1730
+ }
1731
+ interface DashboardTriColorTrendPoint {
1732
+ ts: number;
1733
+ add: number;
1734
+ modify: number;
1735
+ delete: number;
1736
+ }
1737
+ type DashboardTrendKind = 'monotonic' | 'bidirectional';
1738
+ interface DashboardTrendMeta {
1739
+ pointLimit: number;
1740
+ lastUpdatedAt: number;
1741
+ }
1742
+ type DashboardCardAvailability = {
1743
+ state: 'ok';
1744
+ } | {
1745
+ state: 'invalid';
1746
+ reason: 'semantic-uncomputable' | 'objective-history-unavailable';
1747
+ };
1748
+ interface DashboardSummary {
1749
+ specifications: number;
1750
+ requirements: number;
1751
+ activeChanges: number;
1752
+ inProgressChanges: number;
1753
+ completedChanges: number;
1754
+ archivedTasksCompleted: number;
1755
+ tasksTotal: number;
1756
+ tasksCompleted: number;
1757
+ taskCompletionPercent: number | null;
1758
+ }
1759
+ interface DashboardGitDiffStats {
1760
+ files: number;
1761
+ insertions: number;
1762
+ deletions: number;
1763
+ }
1764
+ interface DashboardGitCommitEntry {
1765
+ type: 'commit';
1766
+ hash: string;
1767
+ title: string;
1768
+ relatedChanges: string[];
1769
+ diff: DashboardGitDiffStats;
1770
+ }
1771
+ interface DashboardGitUncommittedEntry {
1772
+ type: 'uncommitted';
1773
+ title: string;
1774
+ relatedChanges: string[];
1775
+ diff: DashboardGitDiffStats;
1776
+ }
1777
+ type DashboardGitEntry = DashboardGitCommitEntry | DashboardGitUncommittedEntry;
1778
+ interface DashboardGitWorktree {
1779
+ path: string;
1780
+ relativePath: string;
1781
+ branchName: string;
1782
+ isCurrent: boolean;
1783
+ ahead: number;
1784
+ behind: number;
1785
+ diff: DashboardGitDiffStats;
1786
+ entries: DashboardGitEntry[];
1787
+ }
1788
+ interface DashboardGitSnapshot {
1789
+ defaultBranch: string;
1790
+ worktrees: DashboardGitWorktree[];
1791
+ }
1792
+ interface DashboardOverview {
1793
+ summary: DashboardSummary;
1794
+ trends: Record<DashboardMetricKey, DashboardTrendPoint[]>;
1795
+ triColorTrends: Record<DashboardMetricKey, DashboardTriColorTrendPoint[]>;
1796
+ trendKinds: Record<DashboardMetricKey, DashboardTrendKind>;
1797
+ cardAvailability: Record<DashboardMetricKey, DashboardCardAvailability>;
1798
+ trendMeta: DashboardTrendMeta;
1799
+ specifications: Array<{
1800
+ id: string;
1801
+ name: string;
1802
+ requirements: number;
1803
+ updatedAt: number;
1804
+ }>;
1805
+ activeChanges: Array<{
1806
+ id: string;
1807
+ name: string;
1808
+ progress: {
1809
+ total: number;
1810
+ completed: number;
1811
+ };
1812
+ updatedAt: number;
1813
+ }>;
1814
+ git: DashboardGitSnapshot;
1815
+ }
1816
+ //#endregion
1666
1817
  //#region src/opsx-types.d.ts
1667
1818
  /** Check if an outputPath contains glob pattern characters */
1668
1819
  declare function isGlobPattern(pattern: string): boolean;
@@ -2067,6 +2218,8 @@ interface ExportSnapshot {
2067
2218
  changesCount: number;
2068
2219
  archivesCount: number;
2069
2220
  };
2221
+ /** OpenSpecUI runtime config captured during export */
2222
+ config?: OpenSpecUIConfig;
2070
2223
  /** All specs with parsed content */
2071
2224
  specs: Array<{
2072
2225
  id: string;
@@ -2251,6 +2404,8 @@ declare const PtySessionInfoSchema: z.ZodObject<{
2251
2404
  platform: z.ZodEnum<["windows", "macos", "common"]>;
2252
2405
  isExited: z.ZodBoolean;
2253
2406
  exitCode: z.ZodNullable<z.ZodNumber>;
2407
+ closeTip: z.ZodOptional<z.ZodString>;
2408
+ closeCallbackUrl: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
2254
2409
  }, "strip", z.ZodTypeAny, {
2255
2410
  command: string;
2256
2411
  args: string[];
@@ -2259,6 +2414,8 @@ declare const PtySessionInfoSchema: z.ZodObject<{
2259
2414
  title: string;
2260
2415
  id: string;
2261
2416
  isExited: boolean;
2417
+ closeTip?: string | undefined;
2418
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2262
2419
  }, {
2263
2420
  command: string;
2264
2421
  args: string[];
@@ -2267,6 +2424,8 @@ declare const PtySessionInfoSchema: z.ZodObject<{
2267
2424
  title: string;
2268
2425
  id: string;
2269
2426
  isExited: boolean;
2427
+ closeTip?: string | undefined;
2428
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2270
2429
  }>;
2271
2430
  declare const PtyCreateMessageSchema: z.ZodObject<{
2272
2431
  type: z.ZodLiteral<"create">;
@@ -2275,6 +2434,8 @@ declare const PtyCreateMessageSchema: z.ZodObject<{
2275
2434
  rows: z.ZodOptional<z.ZodNumber>;
2276
2435
  command: z.ZodOptional<z.ZodString>;
2277
2436
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
2437
+ closeTip: z.ZodOptional<z.ZodString>;
2438
+ closeCallbackUrl: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
2278
2439
  }, "strip", z.ZodTypeAny, {
2279
2440
  type: "create";
2280
2441
  requestId: string;
@@ -2282,6 +2443,8 @@ declare const PtyCreateMessageSchema: z.ZodObject<{
2282
2443
  rows?: number | undefined;
2283
2444
  command?: string | undefined;
2284
2445
  args?: string[] | undefined;
2446
+ closeTip?: string | undefined;
2447
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2285
2448
  }, {
2286
2449
  type: "create";
2287
2450
  requestId: string;
@@ -2289,6 +2452,8 @@ declare const PtyCreateMessageSchema: z.ZodObject<{
2289
2452
  rows?: number | undefined;
2290
2453
  command?: string | undefined;
2291
2454
  args?: string[] | undefined;
2455
+ closeTip?: string | undefined;
2456
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2292
2457
  }>;
2293
2458
  declare const PtyInputMessageSchema: z.ZodObject<{
2294
2459
  type: z.ZodLiteral<"input">;
@@ -2359,6 +2524,8 @@ declare const PtyClientMessageSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
2359
2524
  rows: z.ZodOptional<z.ZodNumber>;
2360
2525
  command: z.ZodOptional<z.ZodString>;
2361
2526
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
2527
+ closeTip: z.ZodOptional<z.ZodString>;
2528
+ closeCallbackUrl: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
2362
2529
  }, "strip", z.ZodTypeAny, {
2363
2530
  type: "create";
2364
2531
  requestId: string;
@@ -2366,6 +2533,8 @@ declare const PtyClientMessageSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
2366
2533
  rows?: number | undefined;
2367
2534
  command?: string | undefined;
2368
2535
  args?: string[] | undefined;
2536
+ closeTip?: string | undefined;
2537
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2369
2538
  }, {
2370
2539
  type: "create";
2371
2540
  requestId: string;
@@ -2373,6 +2542,8 @@ declare const PtyClientMessageSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
2373
2542
  rows?: number | undefined;
2374
2543
  command?: string | undefined;
2375
2544
  args?: string[] | undefined;
2545
+ closeTip?: string | undefined;
2546
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2376
2547
  }>, z.ZodObject<{
2377
2548
  type: z.ZodLiteral<"input">;
2378
2549
  sessionId: z.ZodString;
@@ -2509,6 +2680,8 @@ declare const PtyListResponseSchema: z.ZodObject<{
2509
2680
  platform: z.ZodEnum<["windows", "macos", "common"]>;
2510
2681
  isExited: z.ZodBoolean;
2511
2682
  exitCode: z.ZodNullable<z.ZodNumber>;
2683
+ closeTip: z.ZodOptional<z.ZodString>;
2684
+ closeCallbackUrl: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
2512
2685
  }, "strip", z.ZodTypeAny, {
2513
2686
  command: string;
2514
2687
  args: string[];
@@ -2517,6 +2690,8 @@ declare const PtyListResponseSchema: z.ZodObject<{
2517
2690
  title: string;
2518
2691
  id: string;
2519
2692
  isExited: boolean;
2693
+ closeTip?: string | undefined;
2694
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2520
2695
  }, {
2521
2696
  command: string;
2522
2697
  args: string[];
@@ -2525,6 +2700,8 @@ declare const PtyListResponseSchema: z.ZodObject<{
2525
2700
  title: string;
2526
2701
  id: string;
2527
2702
  isExited: boolean;
2703
+ closeTip?: string | undefined;
2704
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2528
2705
  }>, "many">;
2529
2706
  }, "strip", z.ZodTypeAny, {
2530
2707
  type: "list";
@@ -2536,6 +2713,8 @@ declare const PtyListResponseSchema: z.ZodObject<{
2536
2713
  title: string;
2537
2714
  id: string;
2538
2715
  isExited: boolean;
2716
+ closeTip?: string | undefined;
2717
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2539
2718
  }[];
2540
2719
  }, {
2541
2720
  type: "list";
@@ -2547,6 +2726,8 @@ declare const PtyListResponseSchema: z.ZodObject<{
2547
2726
  title: string;
2548
2727
  id: string;
2549
2728
  isExited: boolean;
2729
+ closeTip?: string | undefined;
2730
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2550
2731
  }[];
2551
2732
  }>;
2552
2733
  declare const PtyErrorCodeSchema: z.ZodEnum<["INVALID_JSON", "INVALID_MESSAGE", "SESSION_NOT_FOUND", "PTY_CREATE_FAILED"]>;
@@ -2639,6 +2820,8 @@ declare const PtyServerMessageSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
2639
2820
  platform: z.ZodEnum<["windows", "macos", "common"]>;
2640
2821
  isExited: z.ZodBoolean;
2641
2822
  exitCode: z.ZodNullable<z.ZodNumber>;
2823
+ closeTip: z.ZodOptional<z.ZodString>;
2824
+ closeCallbackUrl: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
2642
2825
  }, "strip", z.ZodTypeAny, {
2643
2826
  command: string;
2644
2827
  args: string[];
@@ -2647,6 +2830,8 @@ declare const PtyServerMessageSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
2647
2830
  title: string;
2648
2831
  id: string;
2649
2832
  isExited: boolean;
2833
+ closeTip?: string | undefined;
2834
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2650
2835
  }, {
2651
2836
  command: string;
2652
2837
  args: string[];
@@ -2655,6 +2840,8 @@ declare const PtyServerMessageSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
2655
2840
  title: string;
2656
2841
  id: string;
2657
2842
  isExited: boolean;
2843
+ closeTip?: string | undefined;
2844
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2658
2845
  }>, "many">;
2659
2846
  }, "strip", z.ZodTypeAny, {
2660
2847
  type: "list";
@@ -2666,6 +2853,8 @@ declare const PtyServerMessageSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
2666
2853
  title: string;
2667
2854
  id: string;
2668
2855
  isExited: boolean;
2856
+ closeTip?: string | undefined;
2857
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2669
2858
  }[];
2670
2859
  }, {
2671
2860
  type: "list";
@@ -2677,6 +2866,8 @@ declare const PtyServerMessageSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObje
2677
2866
  title: string;
2678
2867
  id: string;
2679
2868
  isExited: boolean;
2869
+ closeTip?: string | undefined;
2870
+ closeCallbackUrl?: string | Record<string, string> | undefined;
2680
2871
  }[];
2681
2872
  }>, z.ZodObject<{
2682
2873
  type: z.ZodLiteral<"error">;
@@ -2699,4 +2890,4 @@ type PtyServerMessage = z.infer<typeof PtyServerMessageSchema>;
2699
2890
  type PtySessionInfo = z.infer<typeof PtySessionInfoSchema>;
2700
2891
  type PtyPlatform = z.infer<typeof PtyPlatformSchema>;
2701
2892
  //#endregion
2702
- export { type AIToolOption, AI_TOOLS, type ApplyInstructions, ApplyInstructionsSchema, type ApplyTask, ApplyTaskSchema, type ArchiveMeta, type ArtifactInstructions, ArtifactInstructionsSchema, type ArtifactStatus, ArtifactStatusSchema, type Change, type ChangeFile, ChangeFileSchema, type ChangeMeta, ChangeSchema, type ChangeStatus, ChangeStatusSchema, CliExecutor, type CliResult, type CliRunnerAttempt, type CliSniffResult, type CliStreamEvent, ConfigManager, DEFAULT_CONFIG, type Delta, type DeltaOperation, DeltaOperationType, DeltaSchema, type DeltaSpec, DeltaSpecSchema, type DependencyInfo, DependencyInfoSchema, type ExportSnapshot, type FileChangeEvent, type FileChangeType, MarkdownParser, OpenSpecAdapter, type OpenSpecUIConfig, OpenSpecUIConfigSchema, type OpenSpecUIConfigUpdate, OpenSpecWatcher, OpsxKernel, type PathCallback, ProjectWatcher, PtyAttachMessageSchema, PtyBufferResponseSchema, type PtyClientMessage, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, type PtyPlatform, PtyPlatformSchema, PtyResizeMessageSchema, type PtyServerMessage, PtyServerMessageSchema, type PtySessionInfo, PtyTitleResponseSchema, ReactiveContext, ReactiveState, type ReactiveStateOptions, type Requirement, RequirementSchema, type ResolvedCliRunner, type SchemaArtifact, SchemaArtifactSchema, type SchemaDetail, SchemaDetailSchema, type SchemaInfo, SchemaInfoSchema, type SchemaResolution, SchemaResolutionSchema, type Spec, type SpecMeta, SpecSchema, type Task, TaskSchema, type TemplateContentMap, type TemplatesMap, TemplatesSchema, type TerminalConfig, TerminalConfigSchema, type ToolConfig, type ValidationIssue, type ValidationResult, Validator, type WatchEvent, type WatchEventType, 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 };
2893
+ export { type AIToolOption, AI_TOOLS, type ApplyInstructions, ApplyInstructionsSchema, type ApplyTask, ApplyTaskSchema, type ArchiveMeta, type ArtifactInstructions, ArtifactInstructionsSchema, type ArtifactStatus, ArtifactStatusSchema, type Change, type ChangeFile, ChangeFileSchema, type ChangeMeta, ChangeSchema, type ChangeStatus, ChangeStatusSchema, CliExecutor, type CliResult, type CliRunnerAttempt, type CliSniffResult, type CliStreamEvent, ConfigManager, DASHBOARD_METRIC_KEYS, DEFAULT_CONFIG, type DashboardCardAvailability, type DashboardConfig, DashboardConfigSchema, type DashboardGitCommitEntry, type DashboardGitDiffStats, type DashboardGitEntry, type DashboardGitSnapshot, type DashboardGitUncommittedEntry, type DashboardGitWorktree, type DashboardMetricKey, type DashboardOverview, type DashboardSummary, type DashboardTrendKind, type DashboardTrendMeta, type DashboardTrendPoint, type DashboardTriColorTrendPoint, type Delta, type DeltaOperation, DeltaOperationType, DeltaSchema, type DeltaSpec, DeltaSpecSchema, type DependencyInfo, DependencyInfoSchema, type ExportSnapshot, type FileChangeEvent, type FileChangeType, MarkdownParser, OpenSpecAdapter, type OpenSpecUIConfig, OpenSpecUIConfigSchema, type OpenSpecUIConfigUpdate, OpenSpecWatcher, OpsxKernel, type PathCallback, ProjectWatcher, type ProjectWatcherReinitializeReason, type ProjectWatcherRuntimeStatus, PtyAttachMessageSchema, PtyBufferResponseSchema, type PtyClientMessage, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, type PtyPlatform, PtyPlatformSchema, PtyResizeMessageSchema, type PtyServerMessage, PtyServerMessageSchema, type PtySessionInfo, PtyTitleResponseSchema, ReactiveContext, ReactiveState, type ReactiveStateOptions, type Requirement, RequirementSchema, type ResolvedCliRunner, type SchemaArtifact, SchemaArtifactSchema, type SchemaDetail, SchemaDetailSchema, type SchemaInfo, SchemaInfoSchema, type SchemaResolution, SchemaResolutionSchema, type Spec, type SpecMeta, SpecSchema, type Task, TaskSchema, type TemplateContentMap, type TemplatesMap, TemplatesSchema, type TerminalConfig, TerminalConfigSchema, type TerminalRendererEngine, TerminalRendererEngineSchema, type ToolConfig, type ValidationIssue, type ValidationResult, Validator, type WatchEvent, type WatchEventType, type WatcherRuntimeStatus, 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/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 */
@@ -3803,6 +3894,7 @@ const PtyPlatformSchema = z.enum([
3803
3894
  "macos",
3804
3895
  "common"
3805
3896
  ]);
3897
+ const CloseCallbackUrlSchema = z.union([z.string(), z.record(z.string())]);
3806
3898
  const PtySessionInfoSchema = z.object({
3807
3899
  id: z.string().min(1),
3808
3900
  title: z.string(),
@@ -3810,7 +3902,9 @@ const PtySessionInfoSchema = z.object({
3810
3902
  args: z.array(z.string()),
3811
3903
  platform: PtyPlatformSchema,
3812
3904
  isExited: z.boolean(),
3813
- exitCode: z.number().int().nullable()
3905
+ exitCode: z.number().int().nullable(),
3906
+ closeTip: z.string().optional(),
3907
+ closeCallbackUrl: CloseCallbackUrlSchema.optional()
3814
3908
  });
3815
3909
  const PtyCreateMessageSchema = z.object({
3816
3910
  type: z.literal("create"),
@@ -3818,7 +3912,9 @@ const PtyCreateMessageSchema = z.object({
3818
3912
  cols: PositiveInt.optional(),
3819
3913
  rows: PositiveInt.optional(),
3820
3914
  command: z.string().min(1).optional(),
3821
- args: z.array(z.string()).optional()
3915
+ args: z.array(z.string()).optional(),
3916
+ closeTip: z.string().optional(),
3917
+ closeCallbackUrl: CloseCallbackUrlSchema.optional()
3822
3918
  });
3823
3919
  const PtyInputMessageSchema = z.object({
3824
3920
  type: z.literal("input"),
@@ -3903,4 +3999,4 @@ const PtyServerMessageSchema = z.discriminatedUnion("type", [
3903
3999
  ]);
3904
4000
 
3905
4001
  //#endregion
3906
- 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.1.2",
3
+ "version": "1.5.0",
4
4
  "description": "Core OpenSpec adapter and parser",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",