@openspecui/core 1.2.0 → 1.6.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 +21 -0
- package/dist/index.d.mts +300 -110
- package/dist/index.mjs +302 -175
- package/dist/opsx-display-path-DYIjMsbM.mjs +63 -0
- package/dist/opsx-display-path-RBULE8_G.d.mts +8 -0
- package/dist/opsx-display-path.d.mts +2 -0
- package/dist/opsx-display-path.mjs +3 -0
- package/package.json +7 -3
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { n as toOpsxDisplayPath, t as VIRTUAL_PROJECT_DIRNAME } from "./opsx-display-path-DYIjMsbM.mjs";
|
|
1
2
|
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
2
3
|
import { dirname, join } from "path";
|
|
3
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
5
|
import { readFile as readFile$1, readdir, stat } from "node:fs/promises";
|
|
5
6
|
import { dirname as dirname$1, join as join$1, matchesGlob, relative, resolve, sep } from "node:path";
|
|
6
|
-
import { existsSync,
|
|
7
|
+
import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
7
8
|
import { z } from "zod";
|
|
8
|
-
import { watch } from "fs";
|
|
9
9
|
import { EventEmitter } from "events";
|
|
10
|
+
import { watch } from "fs";
|
|
10
11
|
import { exec, spawn } from "child_process";
|
|
11
12
|
import { promisify } from "util";
|
|
12
13
|
import { parse } from "yaml";
|
|
@@ -220,14 +221,14 @@ var MarkdownParser = class {
|
|
|
220
221
|
if (currentOperation === "RENAMED") {
|
|
221
222
|
const fromMatch = line.match(/FROM:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
|
|
222
223
|
const toMatch = line.match(/TO:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
|
|
223
|
-
if (fromMatch)
|
|
224
|
-
|
|
225
|
-
from
|
|
226
|
-
}
|
|
227
|
-
if (toMatch)
|
|
228
|
-
|
|
229
|
-
to
|
|
230
|
-
}
|
|
224
|
+
if (fromMatch) {
|
|
225
|
+
if (!renameBuffer) renameBuffer = {};
|
|
226
|
+
renameBuffer.from = fromMatch[1].trim();
|
|
227
|
+
}
|
|
228
|
+
if (toMatch) {
|
|
229
|
+
if (!renameBuffer) renameBuffer = {};
|
|
230
|
+
renameBuffer.to = toMatch[1].trim();
|
|
231
|
+
}
|
|
231
232
|
if (renameBuffer?.from && renameBuffer?.to) {
|
|
232
233
|
deltas.push({
|
|
233
234
|
spec: deltaSpec.specId,
|
|
@@ -312,86 +313,6 @@ var MarkdownParser = class {
|
|
|
312
313
|
}
|
|
313
314
|
};
|
|
314
315
|
|
|
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
316
|
//#endregion
|
|
396
317
|
//#region src/reactive-fs/reactive-state.ts
|
|
397
318
|
/**
|
|
@@ -578,8 +499,10 @@ const DEFAULT_IGNORE = [
|
|
|
578
499
|
".git",
|
|
579
500
|
"**/.DS_Store"
|
|
580
501
|
];
|
|
581
|
-
/**
|
|
582
|
-
const
|
|
502
|
+
/** 恢复重试间隔 (ms) */
|
|
503
|
+
const RECOVERY_INTERVAL_MS = 3e3;
|
|
504
|
+
/** 路径语义检查间隔 (ms) */
|
|
505
|
+
const PATH_LIVENESS_INTERVAL_MS = 3e3;
|
|
583
506
|
/**
|
|
584
507
|
* 项目监听器
|
|
585
508
|
*
|
|
@@ -602,17 +525,25 @@ var ProjectWatcher = class {
|
|
|
602
525
|
ignore;
|
|
603
526
|
initialized = false;
|
|
604
527
|
initPromise = null;
|
|
605
|
-
healthCheckTimer = null;
|
|
606
|
-
lastEventTime = 0;
|
|
607
|
-
healthCheckPending = false;
|
|
608
|
-
enableHealthCheck;
|
|
609
528
|
reinitializeTimer = null;
|
|
610
529
|
reinitializePending = false;
|
|
530
|
+
reinitializeReasonPending = null;
|
|
531
|
+
pathLivenessTimer = null;
|
|
532
|
+
projectDirFingerprint = null;
|
|
533
|
+
generation = 0;
|
|
534
|
+
reinitializeCount = 0;
|
|
535
|
+
lastReinitializeReason = null;
|
|
536
|
+
reinitializeReasonCounts = {
|
|
537
|
+
"drop-events": 0,
|
|
538
|
+
"watcher-error": 0,
|
|
539
|
+
"missing-project-dir": 0,
|
|
540
|
+
"project-dir-replaced": 0,
|
|
541
|
+
manual: 0
|
|
542
|
+
};
|
|
611
543
|
constructor(projectDir, options = {}) {
|
|
612
544
|
this.projectDir = getRealPath$1(projectDir);
|
|
613
545
|
this.debounceMs = options.debounceMs ?? DEBOUNCE_MS$1;
|
|
614
546
|
this.ignore = options.ignore ?? DEFAULT_IGNORE;
|
|
615
|
-
this.enableHealthCheck = options.enableHealthCheck ?? true;
|
|
616
547
|
}
|
|
617
548
|
/**
|
|
618
549
|
* 初始化 watcher
|
|
@@ -621,8 +552,11 @@ var ProjectWatcher = class {
|
|
|
621
552
|
async init() {
|
|
622
553
|
if (this.initialized) return;
|
|
623
554
|
if (this.initPromise) return this.initPromise;
|
|
624
|
-
this.initPromise = this.doInit()
|
|
625
|
-
|
|
555
|
+
this.initPromise = this.doInit().catch((error) => {
|
|
556
|
+
this.initPromise = null;
|
|
557
|
+
throw error;
|
|
558
|
+
});
|
|
559
|
+
return this.initPromise;
|
|
626
560
|
}
|
|
627
561
|
async doInit() {
|
|
628
562
|
this.subscription = await (await import("@parcel/watcher")).subscribe(this.projectDir, (err, events) => {
|
|
@@ -633,43 +567,98 @@ var ProjectWatcher = class {
|
|
|
633
567
|
this.handleEvents(events);
|
|
634
568
|
}, { ignore: this.ignore });
|
|
635
569
|
this.initialized = true;
|
|
636
|
-
this.
|
|
637
|
-
|
|
570
|
+
this.generation += 1;
|
|
571
|
+
this.projectDirFingerprint = this.getProjectDirFingerprint();
|
|
572
|
+
this.startPathLivenessMonitor();
|
|
638
573
|
}
|
|
639
574
|
/**
|
|
640
575
|
* 处理 watcher 错误
|
|
641
|
-
*
|
|
576
|
+
* 统一走错误驱动重建流程
|
|
642
577
|
*/
|
|
643
578
|
handleWatcherError(err) {
|
|
644
579
|
if ((err.message || String(err)).includes("Events were dropped")) {
|
|
645
580
|
if (!this.reinitializePending) {
|
|
646
581
|
console.warn("[ProjectWatcher] FSEvents dropped events, scheduling reinitialize...");
|
|
647
|
-
this.scheduleReinitialize();
|
|
582
|
+
this.scheduleReinitialize("drop-events");
|
|
648
583
|
}
|
|
649
584
|
return;
|
|
650
585
|
}
|
|
651
|
-
console.error("[ProjectWatcher]
|
|
586
|
+
console.error("[ProjectWatcher] Watcher error, scheduling reinitialize:", err);
|
|
587
|
+
this.scheduleReinitialize("watcher-error");
|
|
652
588
|
}
|
|
653
589
|
/**
|
|
654
590
|
* 延迟重建 watcher(防抖,避免频繁重建)
|
|
655
591
|
*/
|
|
656
|
-
scheduleReinitialize() {
|
|
592
|
+
scheduleReinitialize(reason) {
|
|
593
|
+
this.reinitializeReasonPending = reason;
|
|
657
594
|
if (this.reinitializePending) return;
|
|
658
595
|
this.reinitializePending = true;
|
|
659
596
|
if (this.reinitializeTimer) clearTimeout(this.reinitializeTimer);
|
|
660
597
|
this.reinitializeTimer = setTimeout(() => {
|
|
661
598
|
this.reinitializeTimer = null;
|
|
662
599
|
this.reinitializePending = false;
|
|
663
|
-
|
|
664
|
-
this.
|
|
665
|
-
|
|
600
|
+
const pendingReason = this.reinitializeReasonPending ?? reason;
|
|
601
|
+
this.reinitializeReasonPending = null;
|
|
602
|
+
console.log(`[ProjectWatcher] Reinitializing (reason: ${pendingReason})...`);
|
|
603
|
+
this.reinitialize(pendingReason);
|
|
604
|
+
}, RECOVERY_INTERVAL_MS);
|
|
605
|
+
this.reinitializeTimer.unref();
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* 读取项目目录指纹(目录不存在时返回 null)
|
|
609
|
+
* 用于检测 path 对应实体是否被替换(inode/dev 漂移)
|
|
610
|
+
*/
|
|
611
|
+
getProjectDirFingerprint() {
|
|
612
|
+
try {
|
|
613
|
+
const stat$1 = lstatSync(this.projectDir);
|
|
614
|
+
return `${stat$1.dev}:${stat$1.ino}`;
|
|
615
|
+
} catch {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* 启动路径语义监测(避免 watcher 绑定到已失效句柄)
|
|
621
|
+
*/
|
|
622
|
+
startPathLivenessMonitor() {
|
|
623
|
+
this.stopPathLivenessMonitor();
|
|
624
|
+
this.pathLivenessTimer = setInterval(() => {
|
|
625
|
+
this.checkPathLiveness();
|
|
626
|
+
}, PATH_LIVENESS_INTERVAL_MS);
|
|
627
|
+
this.pathLivenessTimer.unref();
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* 停止路径语义监测
|
|
631
|
+
*/
|
|
632
|
+
stopPathLivenessMonitor() {
|
|
633
|
+
if (this.pathLivenessTimer) {
|
|
634
|
+
clearInterval(this.pathLivenessTimer);
|
|
635
|
+
this.pathLivenessTimer = null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* 只读检查 projectDir 是否仍指向初始化时的目录实体
|
|
640
|
+
*/
|
|
641
|
+
checkPathLiveness() {
|
|
642
|
+
if (!this.initialized || this.reinitializePending) return;
|
|
643
|
+
const current = this.getProjectDirFingerprint();
|
|
644
|
+
if (current === null) {
|
|
645
|
+
console.warn("[ProjectWatcher] Project directory missing, scheduling reinitialize...");
|
|
646
|
+
this.scheduleReinitialize("missing-project-dir");
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (this.projectDirFingerprint === null) {
|
|
650
|
+
this.projectDirFingerprint = current;
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (current !== this.projectDirFingerprint) {
|
|
654
|
+
console.warn("[ProjectWatcher] Project directory replaced, scheduling reinitialize...");
|
|
655
|
+
this.scheduleReinitialize("project-dir-replaced");
|
|
656
|
+
}
|
|
666
657
|
}
|
|
667
658
|
/**
|
|
668
659
|
* 处理原始事件
|
|
669
660
|
*/
|
|
670
661
|
handleEvents(events) {
|
|
671
|
-
this.lastEventTime = Date.now();
|
|
672
|
-
this.healthCheckPending = false;
|
|
673
662
|
const watchEvents = events.map((e) => ({
|
|
674
663
|
type: e.type,
|
|
675
664
|
path: e.path
|
|
@@ -757,60 +746,29 @@ var ProjectWatcher = class {
|
|
|
757
746
|
return this.initialized;
|
|
758
747
|
}
|
|
759
748
|
/**
|
|
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
|
-
* 停止健康检查定时器
|
|
749
|
+
* 获取 watcher 运行时状态
|
|
771
750
|
*/
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
this.
|
|
776
|
-
|
|
777
|
-
|
|
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();
|
|
751
|
+
get runtimeStatus() {
|
|
752
|
+
return {
|
|
753
|
+
generation: this.generation,
|
|
754
|
+
reinitializeCount: this.reinitializeCount,
|
|
755
|
+
lastReinitializeReason: this.lastReinitializeReason,
|
|
756
|
+
reinitializeReasonCounts: { ...this.reinitializeReasonCounts }
|
|
757
|
+
};
|
|
799
758
|
}
|
|
800
759
|
/**
|
|
801
|
-
*
|
|
760
|
+
* 记录重建统计
|
|
802
761
|
*/
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
} catch {}
|
|
762
|
+
markReinitialized(reason) {
|
|
763
|
+
this.reinitializeCount += 1;
|
|
764
|
+
this.lastReinitializeReason = reason;
|
|
765
|
+
this.reinitializeReasonCounts[reason] += 1;
|
|
808
766
|
}
|
|
809
767
|
/**
|
|
810
768
|
* 重新初始化 watcher
|
|
811
769
|
*/
|
|
812
|
-
async reinitialize() {
|
|
813
|
-
this.
|
|
770
|
+
async reinitialize(reason) {
|
|
771
|
+
this.stopPathLivenessMonitor();
|
|
814
772
|
if (this.subscription) {
|
|
815
773
|
try {
|
|
816
774
|
await this.subscription.unsubscribe();
|
|
@@ -819,38 +777,50 @@ var ProjectWatcher = class {
|
|
|
819
777
|
}
|
|
820
778
|
this.initialized = false;
|
|
821
779
|
this.initPromise = null;
|
|
822
|
-
this.
|
|
780
|
+
this.projectDirFingerprint = null;
|
|
823
781
|
if (!existsSync(this.projectDir)) {
|
|
824
782
|
console.warn("[ProjectWatcher] Project directory does not exist, waiting for it to be created...");
|
|
825
|
-
this.waitForProjectDir();
|
|
783
|
+
this.waitForProjectDir("missing-project-dir");
|
|
826
784
|
return;
|
|
827
785
|
}
|
|
828
786
|
try {
|
|
829
787
|
await this.init();
|
|
788
|
+
this.markReinitialized(reason);
|
|
830
789
|
console.log("[ProjectWatcher] Reinitialized successfully");
|
|
831
790
|
} catch (err) {
|
|
832
791
|
console.error("[ProjectWatcher] Failed to reinitialize:", err);
|
|
833
|
-
|
|
792
|
+
this.scheduleReinitialize(reason);
|
|
834
793
|
}
|
|
835
794
|
}
|
|
836
795
|
/**
|
|
837
796
|
* 等待项目目录被创建
|
|
838
797
|
*/
|
|
839
|
-
waitForProjectDir() {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
798
|
+
waitForProjectDir(reason) {
|
|
799
|
+
this.reinitializeReasonPending = reason;
|
|
800
|
+
this.reinitializePending = true;
|
|
801
|
+
if (this.reinitializeTimer) {
|
|
802
|
+
clearTimeout(this.reinitializeTimer);
|
|
803
|
+
this.reinitializeTimer = null;
|
|
804
|
+
}
|
|
805
|
+
this.reinitializeTimer = setTimeout(() => {
|
|
806
|
+
this.reinitializeTimer = null;
|
|
807
|
+
this.reinitializePending = false;
|
|
808
|
+
if (!existsSync(this.projectDir)) {
|
|
809
|
+
this.waitForProjectDir(reason);
|
|
810
|
+
return;
|
|
845
811
|
}
|
|
846
|
-
|
|
847
|
-
|
|
812
|
+
const pendingReason = this.reinitializeReasonPending ?? reason;
|
|
813
|
+
this.reinitializeReasonPending = null;
|
|
814
|
+
console.log("[ProjectWatcher] Project directory created, reinitializing...");
|
|
815
|
+
this.reinitialize(pendingReason);
|
|
816
|
+
}, RECOVERY_INTERVAL_MS);
|
|
817
|
+
this.reinitializeTimer.unref();
|
|
848
818
|
}
|
|
849
819
|
/**
|
|
850
820
|
* 关闭 watcher
|
|
851
821
|
*/
|
|
852
822
|
async close() {
|
|
853
|
-
this.
|
|
823
|
+
this.stopPathLivenessMonitor();
|
|
854
824
|
if (this.debounceTimer) {
|
|
855
825
|
clearTimeout(this.debounceTimer);
|
|
856
826
|
this.debounceTimer = null;
|
|
@@ -860,6 +830,7 @@ var ProjectWatcher = class {
|
|
|
860
830
|
this.reinitializeTimer = null;
|
|
861
831
|
}
|
|
862
832
|
this.reinitializePending = false;
|
|
833
|
+
this.reinitializeReasonPending = null;
|
|
863
834
|
if (this.subscription) {
|
|
864
835
|
await this.subscription.unsubscribe();
|
|
865
836
|
this.subscription = null;
|
|
@@ -868,6 +839,7 @@ var ProjectWatcher = class {
|
|
|
868
839
|
this.pendingEvents = [];
|
|
869
840
|
this.initialized = false;
|
|
870
841
|
this.initPromise = null;
|
|
842
|
+
this.projectDirFingerprint = null;
|
|
871
843
|
}
|
|
872
844
|
};
|
|
873
845
|
/**
|
|
@@ -1032,6 +1004,22 @@ function isWatcherPoolInitialized() {
|
|
|
1032
1004
|
function getWatchedProjectDir() {
|
|
1033
1005
|
return globalProjectDir;
|
|
1034
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* 获取 watcher 运行时状态
|
|
1009
|
+
*/
|
|
1010
|
+
function getWatcherRuntimeStatus() {
|
|
1011
|
+
if (!globalProjectWatcher) return null;
|
|
1012
|
+
const runtime = globalProjectWatcher.runtimeStatus;
|
|
1013
|
+
return {
|
|
1014
|
+
projectDir: globalProjectDir,
|
|
1015
|
+
initialized: globalProjectWatcher.isInitialized,
|
|
1016
|
+
subscriptionCount: globalProjectWatcher.subscriptionCount,
|
|
1017
|
+
generation: runtime.generation,
|
|
1018
|
+
reinitializeCount: runtime.reinitializeCount,
|
|
1019
|
+
lastReinitializeReason: runtime.lastReinitializeReason,
|
|
1020
|
+
reinitializeReasonCounts: runtime.reinitializeReasonCounts
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1035
1023
|
|
|
1036
1024
|
//#endregion
|
|
1037
1025
|
//#region src/reactive-fs/reactive-fs.ts
|
|
@@ -1229,6 +1217,86 @@ function getCacheSize() {
|
|
|
1229
1217
|
return stateCache$1.size;
|
|
1230
1218
|
}
|
|
1231
1219
|
|
|
1220
|
+
//#endregion
|
|
1221
|
+
//#region src/validator.ts
|
|
1222
|
+
/**
|
|
1223
|
+
* Validator for OpenSpec documents
|
|
1224
|
+
*/
|
|
1225
|
+
var Validator = class {
|
|
1226
|
+
/**
|
|
1227
|
+
* Validate a spec document
|
|
1228
|
+
*/
|
|
1229
|
+
validateSpec(spec) {
|
|
1230
|
+
const issues = [];
|
|
1231
|
+
if (!spec.overview || spec.overview.trim().length === 0) issues.push({
|
|
1232
|
+
severity: "ERROR",
|
|
1233
|
+
message: "Spec must have a Purpose/Overview section",
|
|
1234
|
+
path: "overview"
|
|
1235
|
+
});
|
|
1236
|
+
if (spec.requirements.length === 0) issues.push({
|
|
1237
|
+
severity: "ERROR",
|
|
1238
|
+
message: "Spec must have at least one requirement",
|
|
1239
|
+
path: "requirements"
|
|
1240
|
+
});
|
|
1241
|
+
for (const req of spec.requirements) {
|
|
1242
|
+
if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
|
|
1243
|
+
severity: "WARNING",
|
|
1244
|
+
message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
|
|
1245
|
+
path: `requirements.${req.id}`
|
|
1246
|
+
});
|
|
1247
|
+
if (req.scenarios.length === 0) issues.push({
|
|
1248
|
+
severity: "WARNING",
|
|
1249
|
+
message: `Requirement should have at least one scenario: ${req.id}`,
|
|
1250
|
+
path: `requirements.${req.id}.scenarios`
|
|
1251
|
+
});
|
|
1252
|
+
if (req.text.length > 1e3) issues.push({
|
|
1253
|
+
severity: "WARNING",
|
|
1254
|
+
message: `Requirement text is too long (max 1000 chars): ${req.id}`,
|
|
1255
|
+
path: `requirements.${req.id}.text`
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
1260
|
+
issues
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Validate a change proposal
|
|
1265
|
+
*/
|
|
1266
|
+
validateChange(change) {
|
|
1267
|
+
const issues = [];
|
|
1268
|
+
if (!change.why || change.why.length < 50) issues.push({
|
|
1269
|
+
severity: "ERROR",
|
|
1270
|
+
message: "Change \"Why\" section must be at least 50 characters",
|
|
1271
|
+
path: "why"
|
|
1272
|
+
});
|
|
1273
|
+
if (change.why && change.why.length > 500) issues.push({
|
|
1274
|
+
severity: "WARNING",
|
|
1275
|
+
message: "Change \"Why\" section should be under 500 characters",
|
|
1276
|
+
path: "why"
|
|
1277
|
+
});
|
|
1278
|
+
if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
|
|
1279
|
+
severity: "ERROR",
|
|
1280
|
+
message: "Change must have a \"What Changes\" section",
|
|
1281
|
+
path: "whatChanges"
|
|
1282
|
+
});
|
|
1283
|
+
if (change.deltas.length === 0) issues.push({
|
|
1284
|
+
severity: "WARNING",
|
|
1285
|
+
message: "Change should have at least one delta",
|
|
1286
|
+
path: "deltas"
|
|
1287
|
+
});
|
|
1288
|
+
if (change.deltas.length > 50) issues.push({
|
|
1289
|
+
severity: "WARNING",
|
|
1290
|
+
message: "Change has too many deltas (max 50)",
|
|
1291
|
+
path: "deltas"
|
|
1292
|
+
});
|
|
1293
|
+
return {
|
|
1294
|
+
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
1295
|
+
issues
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1232
1300
|
//#endregion
|
|
1233
1301
|
//#region src/adapter.ts
|
|
1234
1302
|
/**
|
|
@@ -1890,6 +1958,11 @@ const CURSOR_STYLE_VALUES = [
|
|
|
1890
1958
|
"underline",
|
|
1891
1959
|
"bar"
|
|
1892
1960
|
];
|
|
1961
|
+
const TERMINAL_RENDERER_ENGINE_VALUES = ["xterm", "ghostty"];
|
|
1962
|
+
const TerminalRendererEngineSchema = z.enum(TERMINAL_RENDERER_ENGINE_VALUES);
|
|
1963
|
+
function isTerminalRendererEngine(value) {
|
|
1964
|
+
return TERMINAL_RENDERER_ENGINE_VALUES.includes(value);
|
|
1965
|
+
}
|
|
1893
1966
|
const BASE_PACKAGE_MANAGER_RUNNERS = [
|
|
1894
1967
|
{
|
|
1895
1968
|
id: "npx",
|
|
@@ -2227,8 +2300,10 @@ const TerminalConfigSchema = z.object({
|
|
|
2227
2300
|
fontFamily: z.string().default(""),
|
|
2228
2301
|
cursorBlink: z.boolean().default(true),
|
|
2229
2302
|
cursorStyle: z.enum(CURSOR_STYLE_VALUES).default("block"),
|
|
2230
|
-
scrollback: z.number().min(0).max(1e5).default(1e3)
|
|
2303
|
+
scrollback: z.number().min(0).max(1e5).default(1e3),
|
|
2304
|
+
rendererEngine: z.string().default("xterm")
|
|
2231
2305
|
});
|
|
2306
|
+
const DashboardConfigSchema = z.object({ trendPointLimit: z.number().int().min(20).max(500).default(100) });
|
|
2232
2307
|
/**
|
|
2233
2308
|
* OpenSpecUI 配置 Schema
|
|
2234
2309
|
*
|
|
@@ -2240,13 +2315,15 @@ const OpenSpecUIConfigSchema = z.object({
|
|
|
2240
2315
|
args: z.array(z.string()).optional()
|
|
2241
2316
|
}).default({}),
|
|
2242
2317
|
theme: z.enum(THEME_VALUES).default("system"),
|
|
2243
|
-
terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({}))
|
|
2318
|
+
terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({})),
|
|
2319
|
+
dashboard: DashboardConfigSchema.default(DashboardConfigSchema.parse({}))
|
|
2244
2320
|
});
|
|
2245
2321
|
/** 默认配置(静态,用于测试和类型) */
|
|
2246
2322
|
const DEFAULT_CONFIG = {
|
|
2247
2323
|
cli: {},
|
|
2248
2324
|
theme: "system",
|
|
2249
|
-
terminal: TerminalConfigSchema.parse({})
|
|
2325
|
+
terminal: TerminalConfigSchema.parse({}),
|
|
2326
|
+
dashboard: DashboardConfigSchema.parse({})
|
|
2250
2327
|
};
|
|
2251
2328
|
/**
|
|
2252
2329
|
* 配置管理器
|
|
@@ -2312,6 +2389,10 @@ var ConfigManager = class {
|
|
|
2312
2389
|
terminal: {
|
|
2313
2390
|
...current.terminal,
|
|
2314
2391
|
...config.terminal
|
|
2392
|
+
},
|
|
2393
|
+
dashboard: {
|
|
2394
|
+
...current.dashboard,
|
|
2395
|
+
...config.dashboard
|
|
2315
2396
|
}
|
|
2316
2397
|
};
|
|
2317
2398
|
const serialized = JSON.stringify(merged, null, 2);
|
|
@@ -3048,6 +3129,17 @@ async function isToolConfigured(projectDir, toolId) {
|
|
|
3048
3129
|
return (await getConfiguredTools(projectDir)).includes(toolId);
|
|
3049
3130
|
}
|
|
3050
3131
|
|
|
3132
|
+
//#endregion
|
|
3133
|
+
//#region src/dashboard-types.ts
|
|
3134
|
+
const DASHBOARD_METRIC_KEYS = [
|
|
3135
|
+
"specifications",
|
|
3136
|
+
"requirements",
|
|
3137
|
+
"activeChanges",
|
|
3138
|
+
"inProgressChanges",
|
|
3139
|
+
"completedChanges",
|
|
3140
|
+
"taskCompletionPercent"
|
|
3141
|
+
];
|
|
3142
|
+
|
|
3051
3143
|
//#endregion
|
|
3052
3144
|
//#region src/opsx-types.ts
|
|
3053
3145
|
/** Check if an outputPath contains glob pattern characters */
|
|
@@ -3135,17 +3227,20 @@ const SchemaResolutionSchema = z.object({
|
|
|
3135
3227
|
"package"
|
|
3136
3228
|
]),
|
|
3137
3229
|
path: z.string(),
|
|
3230
|
+
displayPath: z.string().optional(),
|
|
3138
3231
|
shadows: z.array(z.object({
|
|
3139
3232
|
source: z.enum([
|
|
3140
3233
|
"project",
|
|
3141
3234
|
"user",
|
|
3142
3235
|
"package"
|
|
3143
3236
|
]),
|
|
3144
|
-
path: z.string()
|
|
3237
|
+
path: z.string(),
|
|
3238
|
+
displayPath: z.string().optional()
|
|
3145
3239
|
}))
|
|
3146
3240
|
});
|
|
3147
3241
|
const TemplatesSchema = z.record(z.object({
|
|
3148
3242
|
path: z.string(),
|
|
3243
|
+
displayPath: z.string().optional(),
|
|
3149
3244
|
source: z.enum([
|
|
3150
3245
|
"project",
|
|
3151
3246
|
"user",
|
|
@@ -3189,6 +3284,12 @@ function parseCliJson(raw, schema, label) {
|
|
|
3189
3284
|
function toRelativePath(root, absolutePath) {
|
|
3190
3285
|
return relative(root, absolutePath).split(sep).join("/");
|
|
3191
3286
|
}
|
|
3287
|
+
function isAbsoluteFsPath(path) {
|
|
3288
|
+
return path.startsWith("/") || /^[A-Za-z]:\//.test(path);
|
|
3289
|
+
}
|
|
3290
|
+
function toAbsoluteProjectPath(projectDir, path) {
|
|
3291
|
+
return isAbsoluteFsPath(path.replace(/\\/g, "/")) ? path : resolve(projectDir, path);
|
|
3292
|
+
}
|
|
3192
3293
|
async function readEntriesUnderRoot(root) {
|
|
3193
3294
|
if (!(await reactiveStat(root))?.isDirectory) return [];
|
|
3194
3295
|
const collectEntries = async (dir) => {
|
|
@@ -3627,7 +3728,21 @@ var OpsxKernel = class {
|
|
|
3627
3728
|
await touchOpsxProjectDeps(this.projectDir);
|
|
3628
3729
|
const result = await this.cliExecutor.schemaWhich(name);
|
|
3629
3730
|
if (!result.success) throw new Error(result.stderr || `openspec schema which failed (exit ${result.exitCode ?? "null"})`);
|
|
3630
|
-
|
|
3731
|
+
const parsed = parseCliJson(result.stdout, SchemaResolutionSchema, "openspec schema which");
|
|
3732
|
+
return {
|
|
3733
|
+
...parsed,
|
|
3734
|
+
displayPath: toOpsxDisplayPath(parsed.path, {
|
|
3735
|
+
source: parsed.source,
|
|
3736
|
+
projectDir: this.projectDir
|
|
3737
|
+
}),
|
|
3738
|
+
shadows: parsed.shadows.map((shadow) => ({
|
|
3739
|
+
...shadow,
|
|
3740
|
+
displayPath: toOpsxDisplayPath(shadow.path, {
|
|
3741
|
+
source: shadow.source,
|
|
3742
|
+
projectDir: this.projectDir
|
|
3743
|
+
})
|
|
3744
|
+
}))
|
|
3745
|
+
};
|
|
3631
3746
|
}
|
|
3632
3747
|
async fetchSchemaDetail(name) {
|
|
3633
3748
|
await touchOpsxProjectDeps(this.projectDir);
|
|
@@ -3651,7 +3766,15 @@ var OpsxKernel = class {
|
|
|
3651
3766
|
await touchOpsxProjectDeps(this.projectDir);
|
|
3652
3767
|
const result = await this.cliExecutor.templates(schema);
|
|
3653
3768
|
if (!result.success) throw new Error(result.stderr || `openspec templates failed (exit ${result.exitCode ?? "null"})`);
|
|
3654
|
-
|
|
3769
|
+
const templates = parseCliJson(result.stdout, TemplatesSchema, "openspec templates");
|
|
3770
|
+
return Object.fromEntries(Object.entries(templates).map(([artifactId, info]) => [artifactId, {
|
|
3771
|
+
...info,
|
|
3772
|
+
path: toAbsoluteProjectPath(this.projectDir, info.path),
|
|
3773
|
+
displayPath: toOpsxDisplayPath(info.path, {
|
|
3774
|
+
source: info.source,
|
|
3775
|
+
projectDir: this.projectDir
|
|
3776
|
+
})
|
|
3777
|
+
}]));
|
|
3655
3778
|
}
|
|
3656
3779
|
async fetchTemplateContents(schema) {
|
|
3657
3780
|
await this.ensureTemplates(schema);
|
|
@@ -3660,6 +3783,10 @@ var OpsxKernel = class {
|
|
|
3660
3783
|
return [artifactId, {
|
|
3661
3784
|
content: await reactiveReadFile(info.path),
|
|
3662
3785
|
path: info.path,
|
|
3786
|
+
displayPath: info.displayPath ?? toOpsxDisplayPath(info.path, {
|
|
3787
|
+
source: info.source,
|
|
3788
|
+
projectDir: this.projectDir
|
|
3789
|
+
}),
|
|
3663
3790
|
source: info.source
|
|
3664
3791
|
}];
|
|
3665
3792
|
}));
|
|
@@ -3908,4 +4035,4 @@ const PtyServerMessageSchema = z.discriminatedUnion("type", [
|
|
|
3908
4035
|
]);
|
|
3909
4036
|
|
|
3910
4037
|
//#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 };
|
|
4038
|
+
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, VIRTUAL_PROJECT_DIRNAME, 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, toOpsxDisplayPath };
|