openspecui 1.3.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/LICENSE +21 -0
- package/dist/cli.mjs +9 -6
- package/dist/index.mjs +1 -1
- package/dist/{open-BVmQScxd.mjs → open-DDagk2eo.mjs} +2 -2
- package/dist/{src-E2ERj6H4.mjs → src-16GA3our.mjs} +1014 -233
- package/package.json +3 -3
- package/web/assets/{BufferResource-8LHM3mct.js → BufferResource-Bn1UWy0D.js} +1 -1
- package/web/assets/{CanvasRenderer-BSV3tOcF.js → CanvasRenderer-D8NiU8la.js} +1 -1
- package/web/assets/{Filter-C5FD597y.js → Filter-CRwq487x.js} +1 -1
- package/web/assets/{RenderTargetSystem-DlUN0zsW.js → RenderTargetSystem-CtoB_qTm.js} +1 -1
- package/web/assets/{WebGLRenderer-QgEdU6ZZ.js → WebGLRenderer-BgKO8R0a.js} +1 -1
- package/web/assets/{WebGPURenderer-D7lHEg9G.js → WebGPURenderer-CQeL2efC.js} +1 -1
- package/web/assets/{browserAll-C7nnv_eJ.js → browserAll-DP6sOYev.js} +1 -1
- package/web/assets/ghostty-web-evxujSxm.js +13 -0
- package/web/assets/{index-Ar3cmjnK.js → index-4MAU81Qk.js} +1 -1
- package/web/assets/{index-Byo82vIc.js → index-B0IbsqHi.js} +1 -1
- package/web/assets/{index-Nzl3W_bm.js → index-B147AOgf.js} +1 -1
- package/web/assets/{index-CDAgriaJ.js → index-BMashGQn.js} +1 -1
- package/web/assets/{index-B0Q8Tr0G.js → index-BPZ3nG0r.js} +1 -1
- package/web/assets/{index-C8uGGt6w.js → index-BejnsZfY.js} +1 -1
- package/web/assets/{index-Dte58iQe.js → index-BnT52DZ8.js} +1 -1
- package/web/assets/{index-BFbdtMlr.js → index-CBCPR3Qb.js} +1 -1
- package/web/assets/{index-CYijlhdV.js → index-D2Tp4F9B.js} +1 -1
- package/web/assets/{index-JGA2Yc2F.js → index-D6ardy54.js} +1 -1
- package/web/assets/{index-DwUe2J4w.js → index-DJqmTRAR.js} +1 -1
- package/web/assets/{index-DYX6cNJ6.js → index-DTeOcXKn.js} +1 -1
- package/web/assets/{index-BkWa50ks.js → index-DcXyAs0z.js} +1 -1
- package/web/assets/{index-BJlwU-uQ.js → index-T8xoxmUb.js} +222 -219
- package/web/assets/index-Ys2MTD3W.css +1 -0
- package/web/assets/{index-C_c_g9pe.js → index-dSf1u0YV.js} +1 -1
- package/web/assets/{index-BdGDc8qr.js → index-f0QdJSzm.js} +1 -1
- package/web/assets/{webworkerAll-CEAZP48d.js → webworkerAll-DA2HufNb.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-ZMhgIxpu.css +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createServer } from "
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { createServer as createServer$1 } from "http";
|
|
3
4
|
import { Http2ServerRequest } from "http2";
|
|
4
5
|
import { Readable } from "stream";
|
|
5
6
|
import crypto from "crypto";
|
|
@@ -8,14 +9,15 @@ import { dirname, join } from "path";
|
|
|
8
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
10
|
import { mkdir as mkdir$1, readFile as readFile$1, readdir, rm, stat, writeFile as writeFile$1 } from "node:fs/promises";
|
|
10
11
|
import { dirname as dirname$1, join as join$1, matchesGlob, relative as relative$1, resolve as resolve$1, sep } from "node:path";
|
|
11
|
-
import { existsSync, readFileSync, realpathSync, statSync
|
|
12
|
-
import { watch } from "fs";
|
|
12
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
|
+
import { watch } from "fs";
|
|
14
15
|
import { exec, spawn } from "child_process";
|
|
15
16
|
import { promisify } from "util";
|
|
16
|
-
import { createServer as createServer$1 } from "node:net";
|
|
17
17
|
import * as pty from "@lydell/node-pty";
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
18
19
|
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
20
|
+
import { promisify as promisify$1 } from "node:util";
|
|
19
21
|
import { Worker as Worker$1 } from "node:worker_threads";
|
|
20
22
|
import { fileURLToPath } from "node:url";
|
|
21
23
|
|
|
@@ -49,6 +51,41 @@ var __toESM$1 = (mod, isNodeMode, target) => (target = mod != null ? __create$1(
|
|
|
49
51
|
}) : target, mod));
|
|
50
52
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
51
53
|
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region ../server/src/port-utils.ts
|
|
56
|
+
/**
|
|
57
|
+
* Check if a port is available by trying to listen on it.
|
|
58
|
+
* Uses default binding (both IPv4 and IPv6) to detect conflicts.
|
|
59
|
+
*/
|
|
60
|
+
function isPortAvailable(port) {
|
|
61
|
+
return new Promise((resolve$2) => {
|
|
62
|
+
const server = createServer();
|
|
63
|
+
server.once("error", () => {
|
|
64
|
+
resolve$2(false);
|
|
65
|
+
});
|
|
66
|
+
server.once("listening", () => {
|
|
67
|
+
server.close(() => resolve$2(true));
|
|
68
|
+
});
|
|
69
|
+
server.listen(port);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Find an available port starting from the given port.
|
|
74
|
+
* Will try up to maxAttempts ports sequentially.
|
|
75
|
+
*
|
|
76
|
+
* @param startPort - The preferred port to start checking from
|
|
77
|
+
* @param maxAttempts - Maximum number of ports to try (default: 10)
|
|
78
|
+
* @returns The first available port found
|
|
79
|
+
* @throws Error if no available port is found in the range
|
|
80
|
+
*/
|
|
81
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
82
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
83
|
+
const port = startPort + i;
|
|
84
|
+
if (await isPortAvailable(port)) return port;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
52
89
|
//#endregion
|
|
53
90
|
//#region ../../node_modules/.pnpm/@hono+node-server@1.19.6_hono@4.10.6/node_modules/@hono/node-server/dist/index.mjs
|
|
54
91
|
var RequestError = class extends Error {
|
|
@@ -476,7 +513,7 @@ var createAdaptorServer = (options) => {
|
|
|
476
513
|
overrideGlobalObjects: options.overrideGlobalObjects,
|
|
477
514
|
autoCleanupIncoming: options.autoCleanupIncoming
|
|
478
515
|
});
|
|
479
|
-
return (options.createServer || createServer)(options.serverOptions || {}, requestListener);
|
|
516
|
+
return (options.createServer || createServer$1)(options.serverOptions || {}, requestListener);
|
|
480
517
|
};
|
|
481
518
|
var serve = (options, listeningListener) => {
|
|
482
519
|
const server = createAdaptorServer(options);
|
|
@@ -697,14 +734,14 @@ var MarkdownParser = class {
|
|
|
697
734
|
if (currentOperation === "RENAMED") {
|
|
698
735
|
const fromMatch = line.match(/FROM:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
|
|
699
736
|
const toMatch = line.match(/TO:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
|
|
700
|
-
if (fromMatch)
|
|
701
|
-
|
|
702
|
-
from
|
|
703
|
-
}
|
|
704
|
-
if (toMatch)
|
|
705
|
-
|
|
706
|
-
to
|
|
707
|
-
}
|
|
737
|
+
if (fromMatch) {
|
|
738
|
+
if (!renameBuffer) renameBuffer = {};
|
|
739
|
+
renameBuffer.from = fromMatch[1].trim();
|
|
740
|
+
}
|
|
741
|
+
if (toMatch) {
|
|
742
|
+
if (!renameBuffer) renameBuffer = {};
|
|
743
|
+
renameBuffer.to = toMatch[1].trim();
|
|
744
|
+
}
|
|
708
745
|
if (renameBuffer?.from && renameBuffer?.to) {
|
|
709
746
|
deltas.push({
|
|
710
747
|
spec: deltaSpec.specId,
|
|
@@ -789,86 +826,6 @@ var MarkdownParser = class {
|
|
|
789
826
|
}
|
|
790
827
|
};
|
|
791
828
|
|
|
792
|
-
//#endregion
|
|
793
|
-
//#region ../core/src/validator.ts
|
|
794
|
-
/**
|
|
795
|
-
* Validator for OpenSpec documents
|
|
796
|
-
*/
|
|
797
|
-
var Validator = class {
|
|
798
|
-
/**
|
|
799
|
-
* Validate a spec document
|
|
800
|
-
*/
|
|
801
|
-
validateSpec(spec) {
|
|
802
|
-
const issues = [];
|
|
803
|
-
if (!spec.overview || spec.overview.trim().length === 0) issues.push({
|
|
804
|
-
severity: "ERROR",
|
|
805
|
-
message: "Spec must have a Purpose/Overview section",
|
|
806
|
-
path: "overview"
|
|
807
|
-
});
|
|
808
|
-
if (spec.requirements.length === 0) issues.push({
|
|
809
|
-
severity: "ERROR",
|
|
810
|
-
message: "Spec must have at least one requirement",
|
|
811
|
-
path: "requirements"
|
|
812
|
-
});
|
|
813
|
-
for (const req of spec.requirements) {
|
|
814
|
-
if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
|
|
815
|
-
severity: "WARNING",
|
|
816
|
-
message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
|
|
817
|
-
path: `requirements.${req.id}`
|
|
818
|
-
});
|
|
819
|
-
if (req.scenarios.length === 0) issues.push({
|
|
820
|
-
severity: "WARNING",
|
|
821
|
-
message: `Requirement should have at least one scenario: ${req.id}`,
|
|
822
|
-
path: `requirements.${req.id}.scenarios`
|
|
823
|
-
});
|
|
824
|
-
if (req.text.length > 1e3) issues.push({
|
|
825
|
-
severity: "WARNING",
|
|
826
|
-
message: `Requirement text is too long (max 1000 chars): ${req.id}`,
|
|
827
|
-
path: `requirements.${req.id}.text`
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
return {
|
|
831
|
-
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
832
|
-
issues
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
/**
|
|
836
|
-
* Validate a change proposal
|
|
837
|
-
*/
|
|
838
|
-
validateChange(change) {
|
|
839
|
-
const issues = [];
|
|
840
|
-
if (!change.why || change.why.length < 50) issues.push({
|
|
841
|
-
severity: "ERROR",
|
|
842
|
-
message: "Change \"Why\" section must be at least 50 characters",
|
|
843
|
-
path: "why"
|
|
844
|
-
});
|
|
845
|
-
if (change.why && change.why.length > 500) issues.push({
|
|
846
|
-
severity: "WARNING",
|
|
847
|
-
message: "Change \"Why\" section should be under 500 characters",
|
|
848
|
-
path: "why"
|
|
849
|
-
});
|
|
850
|
-
if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
|
|
851
|
-
severity: "ERROR",
|
|
852
|
-
message: "Change must have a \"What Changes\" section",
|
|
853
|
-
path: "whatChanges"
|
|
854
|
-
});
|
|
855
|
-
if (change.deltas.length === 0) issues.push({
|
|
856
|
-
severity: "WARNING",
|
|
857
|
-
message: "Change should have at least one delta",
|
|
858
|
-
path: "deltas"
|
|
859
|
-
});
|
|
860
|
-
if (change.deltas.length > 50) issues.push({
|
|
861
|
-
severity: "WARNING",
|
|
862
|
-
message: "Change has too many deltas (max 50)",
|
|
863
|
-
path: "deltas"
|
|
864
|
-
});
|
|
865
|
-
return {
|
|
866
|
-
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
867
|
-
issues
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
|
|
872
829
|
//#endregion
|
|
873
830
|
//#region ../core/src/reactive-fs/reactive-state.ts
|
|
874
831
|
/**
|
|
@@ -1055,8 +1012,10 @@ const DEFAULT_IGNORE = [
|
|
|
1055
1012
|
".git",
|
|
1056
1013
|
"**/.DS_Store"
|
|
1057
1014
|
];
|
|
1058
|
-
/**
|
|
1059
|
-
const
|
|
1015
|
+
/** 恢复重试间隔 (ms) */
|
|
1016
|
+
const RECOVERY_INTERVAL_MS = 3e3;
|
|
1017
|
+
/** 路径语义检查间隔 (ms) */
|
|
1018
|
+
const PATH_LIVENESS_INTERVAL_MS = 3e3;
|
|
1060
1019
|
/**
|
|
1061
1020
|
* 项目监听器
|
|
1062
1021
|
*
|
|
@@ -1079,17 +1038,25 @@ var ProjectWatcher = class {
|
|
|
1079
1038
|
ignore;
|
|
1080
1039
|
initialized = false;
|
|
1081
1040
|
initPromise = null;
|
|
1082
|
-
healthCheckTimer = null;
|
|
1083
|
-
lastEventTime = 0;
|
|
1084
|
-
healthCheckPending = false;
|
|
1085
|
-
enableHealthCheck;
|
|
1086
1041
|
reinitializeTimer = null;
|
|
1087
1042
|
reinitializePending = false;
|
|
1043
|
+
reinitializeReasonPending = null;
|
|
1044
|
+
pathLivenessTimer = null;
|
|
1045
|
+
projectDirFingerprint = null;
|
|
1046
|
+
generation = 0;
|
|
1047
|
+
reinitializeCount = 0;
|
|
1048
|
+
lastReinitializeReason = null;
|
|
1049
|
+
reinitializeReasonCounts = {
|
|
1050
|
+
"drop-events": 0,
|
|
1051
|
+
"watcher-error": 0,
|
|
1052
|
+
"missing-project-dir": 0,
|
|
1053
|
+
"project-dir-replaced": 0,
|
|
1054
|
+
manual: 0
|
|
1055
|
+
};
|
|
1088
1056
|
constructor(projectDir, options = {}) {
|
|
1089
1057
|
this.projectDir = getRealPath$1(projectDir);
|
|
1090
1058
|
this.debounceMs = options.debounceMs ?? DEBOUNCE_MS$1;
|
|
1091
1059
|
this.ignore = options.ignore ?? DEFAULT_IGNORE;
|
|
1092
|
-
this.enableHealthCheck = options.enableHealthCheck ?? true;
|
|
1093
1060
|
}
|
|
1094
1061
|
/**
|
|
1095
1062
|
* 初始化 watcher
|
|
@@ -1098,8 +1065,11 @@ var ProjectWatcher = class {
|
|
|
1098
1065
|
async init() {
|
|
1099
1066
|
if (this.initialized) return;
|
|
1100
1067
|
if (this.initPromise) return this.initPromise;
|
|
1101
|
-
this.initPromise = this.doInit()
|
|
1102
|
-
|
|
1068
|
+
this.initPromise = this.doInit().catch((error) => {
|
|
1069
|
+
this.initPromise = null;
|
|
1070
|
+
throw error;
|
|
1071
|
+
});
|
|
1072
|
+
return this.initPromise;
|
|
1103
1073
|
}
|
|
1104
1074
|
async doInit() {
|
|
1105
1075
|
this.subscription = await (await import("@parcel/watcher")).subscribe(this.projectDir, (err, events) => {
|
|
@@ -1110,43 +1080,98 @@ var ProjectWatcher = class {
|
|
|
1110
1080
|
this.handleEvents(events);
|
|
1111
1081
|
}, { ignore: this.ignore });
|
|
1112
1082
|
this.initialized = true;
|
|
1113
|
-
this.
|
|
1114
|
-
|
|
1083
|
+
this.generation += 1;
|
|
1084
|
+
this.projectDirFingerprint = this.getProjectDirFingerprint();
|
|
1085
|
+
this.startPathLivenessMonitor();
|
|
1115
1086
|
}
|
|
1116
1087
|
/**
|
|
1117
1088
|
* 处理 watcher 错误
|
|
1118
|
-
*
|
|
1089
|
+
* 统一走错误驱动重建流程
|
|
1119
1090
|
*/
|
|
1120
1091
|
handleWatcherError(err) {
|
|
1121
1092
|
if ((err.message || String(err)).includes("Events were dropped")) {
|
|
1122
1093
|
if (!this.reinitializePending) {
|
|
1123
1094
|
console.warn("[ProjectWatcher] FSEvents dropped events, scheduling reinitialize...");
|
|
1124
|
-
this.scheduleReinitialize();
|
|
1095
|
+
this.scheduleReinitialize("drop-events");
|
|
1125
1096
|
}
|
|
1126
1097
|
return;
|
|
1127
1098
|
}
|
|
1128
|
-
console.error("[ProjectWatcher]
|
|
1099
|
+
console.error("[ProjectWatcher] Watcher error, scheduling reinitialize:", err);
|
|
1100
|
+
this.scheduleReinitialize("watcher-error");
|
|
1129
1101
|
}
|
|
1130
1102
|
/**
|
|
1131
1103
|
* 延迟重建 watcher(防抖,避免频繁重建)
|
|
1132
1104
|
*/
|
|
1133
|
-
scheduleReinitialize() {
|
|
1105
|
+
scheduleReinitialize(reason) {
|
|
1106
|
+
this.reinitializeReasonPending = reason;
|
|
1134
1107
|
if (this.reinitializePending) return;
|
|
1135
1108
|
this.reinitializePending = true;
|
|
1136
1109
|
if (this.reinitializeTimer) clearTimeout(this.reinitializeTimer);
|
|
1137
1110
|
this.reinitializeTimer = setTimeout(() => {
|
|
1138
1111
|
this.reinitializeTimer = null;
|
|
1139
1112
|
this.reinitializePending = false;
|
|
1140
|
-
|
|
1141
|
-
this.
|
|
1142
|
-
|
|
1113
|
+
const pendingReason = this.reinitializeReasonPending ?? reason;
|
|
1114
|
+
this.reinitializeReasonPending = null;
|
|
1115
|
+
console.log(`[ProjectWatcher] Reinitializing (reason: ${pendingReason})...`);
|
|
1116
|
+
this.reinitialize(pendingReason);
|
|
1117
|
+
}, RECOVERY_INTERVAL_MS);
|
|
1118
|
+
this.reinitializeTimer.unref();
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* 读取项目目录指纹(目录不存在时返回 null)
|
|
1122
|
+
* 用于检测 path 对应实体是否被替换(inode/dev 漂移)
|
|
1123
|
+
*/
|
|
1124
|
+
getProjectDirFingerprint() {
|
|
1125
|
+
try {
|
|
1126
|
+
const stat$1 = lstatSync(this.projectDir);
|
|
1127
|
+
return `${stat$1.dev}:${stat$1.ino}`;
|
|
1128
|
+
} catch {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* 启动路径语义监测(避免 watcher 绑定到已失效句柄)
|
|
1134
|
+
*/
|
|
1135
|
+
startPathLivenessMonitor() {
|
|
1136
|
+
this.stopPathLivenessMonitor();
|
|
1137
|
+
this.pathLivenessTimer = setInterval(() => {
|
|
1138
|
+
this.checkPathLiveness();
|
|
1139
|
+
}, PATH_LIVENESS_INTERVAL_MS);
|
|
1140
|
+
this.pathLivenessTimer.unref();
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* 停止路径语义监测
|
|
1144
|
+
*/
|
|
1145
|
+
stopPathLivenessMonitor() {
|
|
1146
|
+
if (this.pathLivenessTimer) {
|
|
1147
|
+
clearInterval(this.pathLivenessTimer);
|
|
1148
|
+
this.pathLivenessTimer = null;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* 只读检查 projectDir 是否仍指向初始化时的目录实体
|
|
1153
|
+
*/
|
|
1154
|
+
checkPathLiveness() {
|
|
1155
|
+
if (!this.initialized || this.reinitializePending) return;
|
|
1156
|
+
const current = this.getProjectDirFingerprint();
|
|
1157
|
+
if (current === null) {
|
|
1158
|
+
console.warn("[ProjectWatcher] Project directory missing, scheduling reinitialize...");
|
|
1159
|
+
this.scheduleReinitialize("missing-project-dir");
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (this.projectDirFingerprint === null) {
|
|
1163
|
+
this.projectDirFingerprint = current;
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (current !== this.projectDirFingerprint) {
|
|
1167
|
+
console.warn("[ProjectWatcher] Project directory replaced, scheduling reinitialize...");
|
|
1168
|
+
this.scheduleReinitialize("project-dir-replaced");
|
|
1169
|
+
}
|
|
1143
1170
|
}
|
|
1144
1171
|
/**
|
|
1145
1172
|
* 处理原始事件
|
|
1146
1173
|
*/
|
|
1147
1174
|
handleEvents(events) {
|
|
1148
|
-
this.lastEventTime = Date.now();
|
|
1149
|
-
this.healthCheckPending = false;
|
|
1150
1175
|
const watchEvents = events.map((e) => ({
|
|
1151
1176
|
type: e.type,
|
|
1152
1177
|
path: e.path
|
|
@@ -1234,60 +1259,29 @@ var ProjectWatcher = class {
|
|
|
1234
1259
|
return this.initialized;
|
|
1235
1260
|
}
|
|
1236
1261
|
/**
|
|
1237
|
-
*
|
|
1262
|
+
* 获取 watcher 运行时状态
|
|
1238
1263
|
*/
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
this.
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
/**
|
|
1247
|
-
* 停止健康检查定时器
|
|
1248
|
-
*/
|
|
1249
|
-
stopHealthCheck() {
|
|
1250
|
-
if (this.healthCheckTimer) {
|
|
1251
|
-
clearInterval(this.healthCheckTimer);
|
|
1252
|
-
this.healthCheckTimer = null;
|
|
1253
|
-
}
|
|
1254
|
-
this.healthCheckPending = false;
|
|
1255
|
-
}
|
|
1256
|
-
/**
|
|
1257
|
-
* 执行健康检查
|
|
1258
|
-
*
|
|
1259
|
-
* 工作流程:
|
|
1260
|
-
* 1. 如果最近有事件,无需检查
|
|
1261
|
-
* 2. 如果上次探测还在等待中,说明 watcher 可能失效,尝试重建
|
|
1262
|
-
* 3. 否则,创建临时文件触发事件,等待下次检查验证
|
|
1263
|
-
*/
|
|
1264
|
-
async performHealthCheck() {
|
|
1265
|
-
if (Date.now() - this.lastEventTime < HEALTH_CHECK_INTERVAL_MS) {
|
|
1266
|
-
this.healthCheckPending = false;
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
if (this.healthCheckPending) {
|
|
1270
|
-
console.warn("[ProjectWatcher] Health check failed, watcher appears stale. Reinitializing...");
|
|
1271
|
-
await this.reinitialize();
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
this.healthCheckPending = true;
|
|
1275
|
-
this.sendProbe();
|
|
1264
|
+
get runtimeStatus() {
|
|
1265
|
+
return {
|
|
1266
|
+
generation: this.generation,
|
|
1267
|
+
reinitializeCount: this.reinitializeCount,
|
|
1268
|
+
lastReinitializeReason: this.lastReinitializeReason,
|
|
1269
|
+
reinitializeReasonCounts: { ...this.reinitializeReasonCounts }
|
|
1270
|
+
};
|
|
1276
1271
|
}
|
|
1277
1272
|
/**
|
|
1278
|
-
*
|
|
1273
|
+
* 记录重建统计
|
|
1279
1274
|
*/
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
} catch {}
|
|
1275
|
+
markReinitialized(reason) {
|
|
1276
|
+
this.reinitializeCount += 1;
|
|
1277
|
+
this.lastReinitializeReason = reason;
|
|
1278
|
+
this.reinitializeReasonCounts[reason] += 1;
|
|
1285
1279
|
}
|
|
1286
1280
|
/**
|
|
1287
1281
|
* 重新初始化 watcher
|
|
1288
1282
|
*/
|
|
1289
|
-
async reinitialize() {
|
|
1290
|
-
this.
|
|
1283
|
+
async reinitialize(reason) {
|
|
1284
|
+
this.stopPathLivenessMonitor();
|
|
1291
1285
|
if (this.subscription) {
|
|
1292
1286
|
try {
|
|
1293
1287
|
await this.subscription.unsubscribe();
|
|
@@ -1296,38 +1290,50 @@ var ProjectWatcher = class {
|
|
|
1296
1290
|
}
|
|
1297
1291
|
this.initialized = false;
|
|
1298
1292
|
this.initPromise = null;
|
|
1299
|
-
this.
|
|
1293
|
+
this.projectDirFingerprint = null;
|
|
1300
1294
|
if (!existsSync(this.projectDir)) {
|
|
1301
1295
|
console.warn("[ProjectWatcher] Project directory does not exist, waiting for it to be created...");
|
|
1302
|
-
this.waitForProjectDir();
|
|
1296
|
+
this.waitForProjectDir("missing-project-dir");
|
|
1303
1297
|
return;
|
|
1304
1298
|
}
|
|
1305
1299
|
try {
|
|
1306
1300
|
await this.init();
|
|
1301
|
+
this.markReinitialized(reason);
|
|
1307
1302
|
console.log("[ProjectWatcher] Reinitialized successfully");
|
|
1308
1303
|
} catch (err) {
|
|
1309
1304
|
console.error("[ProjectWatcher] Failed to reinitialize:", err);
|
|
1310
|
-
|
|
1305
|
+
this.scheduleReinitialize(reason);
|
|
1311
1306
|
}
|
|
1312
1307
|
}
|
|
1313
1308
|
/**
|
|
1314
1309
|
* 等待项目目录被创建
|
|
1315
1310
|
*/
|
|
1316
|
-
waitForProjectDir() {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1311
|
+
waitForProjectDir(reason) {
|
|
1312
|
+
this.reinitializeReasonPending = reason;
|
|
1313
|
+
this.reinitializePending = true;
|
|
1314
|
+
if (this.reinitializeTimer) {
|
|
1315
|
+
clearTimeout(this.reinitializeTimer);
|
|
1316
|
+
this.reinitializeTimer = null;
|
|
1317
|
+
}
|
|
1318
|
+
this.reinitializeTimer = setTimeout(() => {
|
|
1319
|
+
this.reinitializeTimer = null;
|
|
1320
|
+
this.reinitializePending = false;
|
|
1321
|
+
if (!existsSync(this.projectDir)) {
|
|
1322
|
+
this.waitForProjectDir(reason);
|
|
1323
|
+
return;
|
|
1322
1324
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
+
const pendingReason = this.reinitializeReasonPending ?? reason;
|
|
1326
|
+
this.reinitializeReasonPending = null;
|
|
1327
|
+
console.log("[ProjectWatcher] Project directory created, reinitializing...");
|
|
1328
|
+
this.reinitialize(pendingReason);
|
|
1329
|
+
}, RECOVERY_INTERVAL_MS);
|
|
1330
|
+
this.reinitializeTimer.unref();
|
|
1325
1331
|
}
|
|
1326
1332
|
/**
|
|
1327
1333
|
* 关闭 watcher
|
|
1328
1334
|
*/
|
|
1329
1335
|
async close() {
|
|
1330
|
-
this.
|
|
1336
|
+
this.stopPathLivenessMonitor();
|
|
1331
1337
|
if (this.debounceTimer) {
|
|
1332
1338
|
clearTimeout(this.debounceTimer);
|
|
1333
1339
|
this.debounceTimer = null;
|
|
@@ -1337,6 +1343,7 @@ var ProjectWatcher = class {
|
|
|
1337
1343
|
this.reinitializeTimer = null;
|
|
1338
1344
|
}
|
|
1339
1345
|
this.reinitializePending = false;
|
|
1346
|
+
this.reinitializeReasonPending = null;
|
|
1340
1347
|
if (this.subscription) {
|
|
1341
1348
|
await this.subscription.unsubscribe();
|
|
1342
1349
|
this.subscription = null;
|
|
@@ -1345,6 +1352,7 @@ var ProjectWatcher = class {
|
|
|
1345
1352
|
this.pendingEvents = [];
|
|
1346
1353
|
this.initialized = false;
|
|
1347
1354
|
this.initPromise = null;
|
|
1355
|
+
this.projectDirFingerprint = null;
|
|
1348
1356
|
}
|
|
1349
1357
|
};
|
|
1350
1358
|
/**
|
|
@@ -1472,6 +1480,22 @@ function acquireWatcher(path$1, onChange, options = {}) {
|
|
|
1472
1480
|
function isWatcherPoolInitialized() {
|
|
1473
1481
|
return globalProjectWatcher !== null && globalProjectWatcher.isInitialized;
|
|
1474
1482
|
}
|
|
1483
|
+
/**
|
|
1484
|
+
* 获取 watcher 运行时状态
|
|
1485
|
+
*/
|
|
1486
|
+
function getWatcherRuntimeStatus() {
|
|
1487
|
+
if (!globalProjectWatcher) return null;
|
|
1488
|
+
const runtime = globalProjectWatcher.runtimeStatus;
|
|
1489
|
+
return {
|
|
1490
|
+
projectDir: globalProjectDir,
|
|
1491
|
+
initialized: globalProjectWatcher.isInitialized,
|
|
1492
|
+
subscriptionCount: globalProjectWatcher.subscriptionCount,
|
|
1493
|
+
generation: runtime.generation,
|
|
1494
|
+
reinitializeCount: runtime.reinitializeCount,
|
|
1495
|
+
lastReinitializeReason: runtime.lastReinitializeReason,
|
|
1496
|
+
reinitializeReasonCounts: runtime.reinitializeReasonCounts
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1475
1499
|
|
|
1476
1500
|
//#endregion
|
|
1477
1501
|
//#region ../core/src/reactive-fs/reactive-fs.ts
|
|
@@ -1646,6 +1670,86 @@ async function reactiveStat(path$1) {
|
|
|
1646
1670
|
return state.get();
|
|
1647
1671
|
}
|
|
1648
1672
|
|
|
1673
|
+
//#endregion
|
|
1674
|
+
//#region ../core/src/validator.ts
|
|
1675
|
+
/**
|
|
1676
|
+
* Validator for OpenSpec documents
|
|
1677
|
+
*/
|
|
1678
|
+
var Validator = class {
|
|
1679
|
+
/**
|
|
1680
|
+
* Validate a spec document
|
|
1681
|
+
*/
|
|
1682
|
+
validateSpec(spec) {
|
|
1683
|
+
const issues = [];
|
|
1684
|
+
if (!spec.overview || spec.overview.trim().length === 0) issues.push({
|
|
1685
|
+
severity: "ERROR",
|
|
1686
|
+
message: "Spec must have a Purpose/Overview section",
|
|
1687
|
+
path: "overview"
|
|
1688
|
+
});
|
|
1689
|
+
if (spec.requirements.length === 0) issues.push({
|
|
1690
|
+
severity: "ERROR",
|
|
1691
|
+
message: "Spec must have at least one requirement",
|
|
1692
|
+
path: "requirements"
|
|
1693
|
+
});
|
|
1694
|
+
for (const req of spec.requirements) {
|
|
1695
|
+
if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
|
|
1696
|
+
severity: "WARNING",
|
|
1697
|
+
message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
|
|
1698
|
+
path: `requirements.${req.id}`
|
|
1699
|
+
});
|
|
1700
|
+
if (req.scenarios.length === 0) issues.push({
|
|
1701
|
+
severity: "WARNING",
|
|
1702
|
+
message: `Requirement should have at least one scenario: ${req.id}`,
|
|
1703
|
+
path: `requirements.${req.id}.scenarios`
|
|
1704
|
+
});
|
|
1705
|
+
if (req.text.length > 1e3) issues.push({
|
|
1706
|
+
severity: "WARNING",
|
|
1707
|
+
message: `Requirement text is too long (max 1000 chars): ${req.id}`,
|
|
1708
|
+
path: `requirements.${req.id}.text`
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
1713
|
+
issues
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Validate a change proposal
|
|
1718
|
+
*/
|
|
1719
|
+
validateChange(change) {
|
|
1720
|
+
const issues = [];
|
|
1721
|
+
if (!change.why || change.why.length < 50) issues.push({
|
|
1722
|
+
severity: "ERROR",
|
|
1723
|
+
message: "Change \"Why\" section must be at least 50 characters",
|
|
1724
|
+
path: "why"
|
|
1725
|
+
});
|
|
1726
|
+
if (change.why && change.why.length > 500) issues.push({
|
|
1727
|
+
severity: "WARNING",
|
|
1728
|
+
message: "Change \"Why\" section should be under 500 characters",
|
|
1729
|
+
path: "why"
|
|
1730
|
+
});
|
|
1731
|
+
if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
|
|
1732
|
+
severity: "ERROR",
|
|
1733
|
+
message: "Change must have a \"What Changes\" section",
|
|
1734
|
+
path: "whatChanges"
|
|
1735
|
+
});
|
|
1736
|
+
if (change.deltas.length === 0) issues.push({
|
|
1737
|
+
severity: "WARNING",
|
|
1738
|
+
message: "Change should have at least one delta",
|
|
1739
|
+
path: "deltas"
|
|
1740
|
+
});
|
|
1741
|
+
if (change.deltas.length > 50) issues.push({
|
|
1742
|
+
severity: "WARNING",
|
|
1743
|
+
message: "Change has too many deltas (max 50)",
|
|
1744
|
+
path: "deltas"
|
|
1745
|
+
});
|
|
1746
|
+
return {
|
|
1747
|
+
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
1748
|
+
issues
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1649
1753
|
//#endregion
|
|
1650
1754
|
//#region ../core/src/adapter.ts
|
|
1651
1755
|
/**
|
|
@@ -1825,17 +1929,17 @@ var OpenSpecAdapter = class {
|
|
|
1825
1929
|
const fullPath = join(dir, name);
|
|
1826
1930
|
const statInfo = await reactiveStat(fullPath);
|
|
1827
1931
|
if (!statInfo) continue;
|
|
1828
|
-
const relativePath = fullPath.slice(root.length + 1);
|
|
1932
|
+
const relativePath$1 = fullPath.slice(root.length + 1);
|
|
1829
1933
|
if (statInfo.isDirectory) {
|
|
1830
1934
|
files.push({
|
|
1831
|
-
path: relativePath,
|
|
1935
|
+
path: relativePath$1,
|
|
1832
1936
|
type: "directory"
|
|
1833
1937
|
});
|
|
1834
1938
|
files.push(...await this.collectChangeFiles(root, fullPath));
|
|
1835
1939
|
} else {
|
|
1836
1940
|
const content = await reactiveReadFile(fullPath);
|
|
1837
1941
|
files.push({
|
|
1838
|
-
path: relativePath,
|
|
1942
|
+
path: relativePath$1,
|
|
1839
1943
|
type: "file",
|
|
1840
1944
|
content: content ?? void 0
|
|
1841
1945
|
});
|
|
@@ -5835,6 +5939,8 @@ const CURSOR_STYLE_VALUES = [
|
|
|
5835
5939
|
"underline",
|
|
5836
5940
|
"bar"
|
|
5837
5941
|
];
|
|
5942
|
+
const TERMINAL_RENDERER_ENGINE_VALUES = ["xterm", "ghostty"];
|
|
5943
|
+
const TerminalRendererEngineSchema = enumType(TERMINAL_RENDERER_ENGINE_VALUES);
|
|
5838
5944
|
const BASE_PACKAGE_MANAGER_RUNNERS = [
|
|
5839
5945
|
{
|
|
5840
5946
|
id: "npx",
|
|
@@ -6172,8 +6278,10 @@ const TerminalConfigSchema = objectType({
|
|
|
6172
6278
|
fontFamily: stringType().default(""),
|
|
6173
6279
|
cursorBlink: booleanType().default(true),
|
|
6174
6280
|
cursorStyle: enumType(CURSOR_STYLE_VALUES).default("block"),
|
|
6175
|
-
scrollback: numberType().min(0).max(1e5).default(1e3)
|
|
6281
|
+
scrollback: numberType().min(0).max(1e5).default(1e3),
|
|
6282
|
+
rendererEngine: stringType().default("xterm")
|
|
6176
6283
|
});
|
|
6284
|
+
const DashboardConfigSchema = objectType({ trendPointLimit: numberType().int().min(20).max(500).default(100) });
|
|
6177
6285
|
/**
|
|
6178
6286
|
* OpenSpecUI 配置 Schema
|
|
6179
6287
|
*
|
|
@@ -6185,13 +6293,15 @@ const OpenSpecUIConfigSchema = objectType({
|
|
|
6185
6293
|
args: arrayType(stringType()).optional()
|
|
6186
6294
|
}).default({}),
|
|
6187
6295
|
theme: enumType(THEME_VALUES).default("system"),
|
|
6188
|
-
terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({}))
|
|
6296
|
+
terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({})),
|
|
6297
|
+
dashboard: DashboardConfigSchema.default(DashboardConfigSchema.parse({}))
|
|
6189
6298
|
});
|
|
6190
6299
|
/** 默认配置(静态,用于测试和类型) */
|
|
6191
6300
|
const DEFAULT_CONFIG = {
|
|
6192
6301
|
cli: {},
|
|
6193
6302
|
theme: "system",
|
|
6194
|
-
terminal: TerminalConfigSchema.parse({})
|
|
6303
|
+
terminal: TerminalConfigSchema.parse({}),
|
|
6304
|
+
dashboard: DashboardConfigSchema.parse({})
|
|
6195
6305
|
};
|
|
6196
6306
|
/**
|
|
6197
6307
|
* 配置管理器
|
|
@@ -6257,6 +6367,10 @@ var ConfigManager = class {
|
|
|
6257
6367
|
terminal: {
|
|
6258
6368
|
...current.terminal,
|
|
6259
6369
|
...config.terminal
|
|
6370
|
+
},
|
|
6371
|
+
dashboard: {
|
|
6372
|
+
...current.dashboard,
|
|
6373
|
+
...config.dashboard
|
|
6260
6374
|
}
|
|
6261
6375
|
};
|
|
6262
6376
|
const serialized = JSON.stringify(merged, null, 2);
|
|
@@ -6969,6 +7083,17 @@ async function getConfiguredTools(projectDir) {
|
|
|
6969
7083
|
return state.get();
|
|
6970
7084
|
}
|
|
6971
7085
|
|
|
7086
|
+
//#endregion
|
|
7087
|
+
//#region ../core/src/dashboard-types.ts
|
|
7088
|
+
const DASHBOARD_METRIC_KEYS = [
|
|
7089
|
+
"specifications",
|
|
7090
|
+
"requirements",
|
|
7091
|
+
"activeChanges",
|
|
7092
|
+
"inProgressChanges",
|
|
7093
|
+
"completedChanges",
|
|
7094
|
+
"taskCompletionPercent"
|
|
7095
|
+
];
|
|
7096
|
+
|
|
6972
7097
|
//#endregion
|
|
6973
7098
|
//#region ../core/src/opsx-types.ts
|
|
6974
7099
|
const ArtifactStatusSchema = objectType({
|
|
@@ -13768,10 +13893,10 @@ async function readEntriesUnderRoot(root) {
|
|
|
13768
13893
|
const fullPath = join$1(dir, name);
|
|
13769
13894
|
const statInfo = await reactiveStat(fullPath);
|
|
13770
13895
|
if (!statInfo) continue;
|
|
13771
|
-
const relativePath = toRelativePath(root, fullPath);
|
|
13896
|
+
const relativePath$1 = toRelativePath(root, fullPath);
|
|
13772
13897
|
if (statInfo.isDirectory) {
|
|
13773
13898
|
entries.push({
|
|
13774
|
-
path: relativePath,
|
|
13899
|
+
path: relativePath$1,
|
|
13775
13900
|
type: "directory"
|
|
13776
13901
|
});
|
|
13777
13902
|
entries.push(...await collectEntries(fullPath));
|
|
@@ -13779,7 +13904,7 @@ async function readEntriesUnderRoot(root) {
|
|
|
13779
13904
|
const content = await reactiveReadFile(fullPath);
|
|
13780
13905
|
const size = content ? Buffer.byteLength(content, "utf-8") : void 0;
|
|
13781
13906
|
entries.push({
|
|
13782
|
-
path: relativePath,
|
|
13907
|
+
path: relativePath$1,
|
|
13783
13908
|
type: "file",
|
|
13784
13909
|
content: content ?? void 0,
|
|
13785
13910
|
size
|
|
@@ -22589,41 +22714,6 @@ var import_sender = /* @__PURE__ */ __toESM$1(require_sender(), 1);
|
|
|
22589
22714
|
var import_websocket = /* @__PURE__ */ __toESM$1(require_websocket(), 1);
|
|
22590
22715
|
var import_websocket_server = /* @__PURE__ */ __toESM$1(require_websocket_server(), 1);
|
|
22591
22716
|
|
|
22592
|
-
//#endregion
|
|
22593
|
-
//#region ../server/src/port-utils.ts
|
|
22594
|
-
/**
|
|
22595
|
-
* Check if a port is available by trying to listen on it.
|
|
22596
|
-
* Uses default binding (both IPv4 and IPv6) to detect conflicts.
|
|
22597
|
-
*/
|
|
22598
|
-
function isPortAvailable(port) {
|
|
22599
|
-
return new Promise((resolve$2) => {
|
|
22600
|
-
const server = createServer$1();
|
|
22601
|
-
server.once("error", () => {
|
|
22602
|
-
resolve$2(false);
|
|
22603
|
-
});
|
|
22604
|
-
server.once("listening", () => {
|
|
22605
|
-
server.close(() => resolve$2(true));
|
|
22606
|
-
});
|
|
22607
|
-
server.listen(port);
|
|
22608
|
-
});
|
|
22609
|
-
}
|
|
22610
|
-
/**
|
|
22611
|
-
* Find an available port starting from the given port.
|
|
22612
|
-
* Will try up to maxAttempts ports sequentially.
|
|
22613
|
-
*
|
|
22614
|
-
* @param startPort - The preferred port to start checking from
|
|
22615
|
-
* @param maxAttempts - Maximum number of ports to try (default: 10)
|
|
22616
|
-
* @returns The first available port found
|
|
22617
|
-
* @throws Error if no available port is found in the range
|
|
22618
|
-
*/
|
|
22619
|
-
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
22620
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
22621
|
-
const port = startPort + i;
|
|
22622
|
-
if (await isPortAvailable(port)) return port;
|
|
22623
|
-
}
|
|
22624
|
-
throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
22625
|
-
}
|
|
22626
|
-
|
|
22627
22717
|
//#endregion
|
|
22628
22718
|
//#region ../server/src/pty-manager.ts
|
|
22629
22719
|
const DEFAULT_SCROLLBACK = 1e3;
|
|
@@ -22994,7 +23084,7 @@ function createPtyWebSocketHandler(ptyManager) {
|
|
|
22994
23084
|
}
|
|
22995
23085
|
|
|
22996
23086
|
//#endregion
|
|
22997
|
-
//#region ../search/
|
|
23087
|
+
//#region ../search/src/protocol.ts
|
|
22998
23088
|
const SearchDocumentKindSchema = enumType([
|
|
22999
23089
|
"spec",
|
|
23000
23090
|
"change",
|
|
@@ -23060,6 +23150,9 @@ const SearchWorkerResponseSchema = discriminatedUnionType("type", [
|
|
|
23060
23150
|
message: stringType()
|
|
23061
23151
|
})
|
|
23062
23152
|
]);
|
|
23153
|
+
|
|
23154
|
+
//#endregion
|
|
23155
|
+
//#region ../search/src/worker-source.ts
|
|
23063
23156
|
const sharedRuntimeSource = String.raw`
|
|
23064
23157
|
const DEFAULT_LIMIT = 50;
|
|
23065
23158
|
const MAX_LIMIT = 200;
|
|
@@ -23277,6 +23370,368 @@ function createCliStreamObservable(startStream) {
|
|
|
23277
23370
|
});
|
|
23278
23371
|
}
|
|
23279
23372
|
|
|
23373
|
+
//#endregion
|
|
23374
|
+
//#region ../server/src/dashboard-git-snapshot.ts
|
|
23375
|
+
const execFileAsync$1 = promisify$1(execFile);
|
|
23376
|
+
const EMPTY_DIFF = {
|
|
23377
|
+
files: 0,
|
|
23378
|
+
insertions: 0,
|
|
23379
|
+
deletions: 0
|
|
23380
|
+
};
|
|
23381
|
+
async function defaultRunGit(cwd, args) {
|
|
23382
|
+
try {
|
|
23383
|
+
const { stdout } = await execFileAsync$1("git", args, {
|
|
23384
|
+
cwd,
|
|
23385
|
+
encoding: "utf8",
|
|
23386
|
+
maxBuffer: 8 * 1024 * 1024
|
|
23387
|
+
});
|
|
23388
|
+
return {
|
|
23389
|
+
ok: true,
|
|
23390
|
+
stdout
|
|
23391
|
+
};
|
|
23392
|
+
} catch {
|
|
23393
|
+
return {
|
|
23394
|
+
ok: false,
|
|
23395
|
+
stdout: ""
|
|
23396
|
+
};
|
|
23397
|
+
}
|
|
23398
|
+
}
|
|
23399
|
+
function parseShortStat(output) {
|
|
23400
|
+
const files = Number(/(\d+)\s+files? changed/.exec(output)?.[1] ?? 0);
|
|
23401
|
+
const insertions = Number(/(\d+)\s+insertions?\(\+\)/.exec(output)?.[1] ?? 0);
|
|
23402
|
+
const deletions = Number(/(\d+)\s+deletions?\(-\)/.exec(output)?.[1] ?? 0);
|
|
23403
|
+
return {
|
|
23404
|
+
files: Number.isFinite(files) ? files : 0,
|
|
23405
|
+
insertions: Number.isFinite(insertions) ? insertions : 0,
|
|
23406
|
+
deletions: Number.isFinite(deletions) ? deletions : 0
|
|
23407
|
+
};
|
|
23408
|
+
}
|
|
23409
|
+
function parseNumStat(output) {
|
|
23410
|
+
let files = 0;
|
|
23411
|
+
let insertions = 0;
|
|
23412
|
+
let deletions = 0;
|
|
23413
|
+
for (const line of output.split("\n")) {
|
|
23414
|
+
const trimmed = line.trim();
|
|
23415
|
+
if (!trimmed) continue;
|
|
23416
|
+
const [addRaw, deleteRaw] = trimmed.split(" ");
|
|
23417
|
+
if (!addRaw || !deleteRaw) continue;
|
|
23418
|
+
files += 1;
|
|
23419
|
+
if (addRaw !== "-") insertions += Number(addRaw) || 0;
|
|
23420
|
+
if (deleteRaw !== "-") deletions += Number(deleteRaw) || 0;
|
|
23421
|
+
}
|
|
23422
|
+
return {
|
|
23423
|
+
files,
|
|
23424
|
+
insertions,
|
|
23425
|
+
deletions
|
|
23426
|
+
};
|
|
23427
|
+
}
|
|
23428
|
+
function normalizeGitPath(path$1) {
|
|
23429
|
+
return path$1.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
23430
|
+
}
|
|
23431
|
+
function relativePath(fromDir, target) {
|
|
23432
|
+
const rel = relative$1(fromDir, target);
|
|
23433
|
+
if (!rel || rel.length === 0) return ".";
|
|
23434
|
+
return rel;
|
|
23435
|
+
}
|
|
23436
|
+
function parseBranchName(branchRef, detached) {
|
|
23437
|
+
if (detached) return "(detached)";
|
|
23438
|
+
if (!branchRef) return "(unknown)";
|
|
23439
|
+
return branchRef.replace(/^refs\/heads\//, "");
|
|
23440
|
+
}
|
|
23441
|
+
function parseWorktreeList(porcelain) {
|
|
23442
|
+
const entries = [];
|
|
23443
|
+
let current = null;
|
|
23444
|
+
const flush = () => {
|
|
23445
|
+
if (!current) return;
|
|
23446
|
+
entries.push(current);
|
|
23447
|
+
current = null;
|
|
23448
|
+
};
|
|
23449
|
+
for (const line of porcelain.split("\n")) {
|
|
23450
|
+
if (line.startsWith("worktree ")) {
|
|
23451
|
+
flush();
|
|
23452
|
+
current = {
|
|
23453
|
+
path: line.slice(9).trim(),
|
|
23454
|
+
branchRef: null,
|
|
23455
|
+
detached: false
|
|
23456
|
+
};
|
|
23457
|
+
continue;
|
|
23458
|
+
}
|
|
23459
|
+
if (!current) continue;
|
|
23460
|
+
if (line.startsWith("branch ")) {
|
|
23461
|
+
current.branchRef = line.slice(7).trim();
|
|
23462
|
+
continue;
|
|
23463
|
+
}
|
|
23464
|
+
if (line === "detached") {
|
|
23465
|
+
current.detached = true;
|
|
23466
|
+
continue;
|
|
23467
|
+
}
|
|
23468
|
+
}
|
|
23469
|
+
flush();
|
|
23470
|
+
return entries;
|
|
23471
|
+
}
|
|
23472
|
+
function parseRelatedChanges(paths) {
|
|
23473
|
+
const related = /* @__PURE__ */ new Set();
|
|
23474
|
+
for (const path$1 of paths) {
|
|
23475
|
+
const normalized = normalizeGitPath(path$1);
|
|
23476
|
+
const activeMatch = /^openspec\/changes\/([^/]+)\//.exec(normalized);
|
|
23477
|
+
if (activeMatch?.[1]) {
|
|
23478
|
+
related.add(activeMatch[1]);
|
|
23479
|
+
continue;
|
|
23480
|
+
}
|
|
23481
|
+
const archiveMatch = /^openspec\/changes\/archive\/([^/]+)\//.exec(normalized);
|
|
23482
|
+
if (archiveMatch?.[1]) {
|
|
23483
|
+
const fullName = archiveMatch[1];
|
|
23484
|
+
related.add(fullName.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
|
|
23485
|
+
}
|
|
23486
|
+
}
|
|
23487
|
+
return [...related].sort((a, b) => a.localeCompare(b));
|
|
23488
|
+
}
|
|
23489
|
+
async function resolveDefaultBranch(projectDir, runGit) {
|
|
23490
|
+
const remoteHead = await runGit(projectDir, [
|
|
23491
|
+
"symbolic-ref",
|
|
23492
|
+
"--quiet",
|
|
23493
|
+
"--short",
|
|
23494
|
+
"refs/remotes/origin/HEAD"
|
|
23495
|
+
]);
|
|
23496
|
+
const remoteRef = remoteHead.stdout.trim();
|
|
23497
|
+
if (remoteHead.ok && remoteRef) return remoteRef;
|
|
23498
|
+
const localHead = await runGit(projectDir, [
|
|
23499
|
+
"rev-parse",
|
|
23500
|
+
"--abbrev-ref",
|
|
23501
|
+
"HEAD"
|
|
23502
|
+
]);
|
|
23503
|
+
const localRef = localHead.stdout.trim();
|
|
23504
|
+
if (localHead.ok && localRef && localRef !== "HEAD") return localRef;
|
|
23505
|
+
return "main";
|
|
23506
|
+
}
|
|
23507
|
+
async function collectCommitEntries(options) {
|
|
23508
|
+
const { worktreePath, defaultBranch, maxCommitEntries, runGit } = options;
|
|
23509
|
+
const entries = [];
|
|
23510
|
+
const commits = await runGit(worktreePath, [
|
|
23511
|
+
"log",
|
|
23512
|
+
"--format=%H%x1f%s",
|
|
23513
|
+
`-n${maxCommitEntries}`,
|
|
23514
|
+
`${defaultBranch}..HEAD`
|
|
23515
|
+
]);
|
|
23516
|
+
if (commits.ok) for (const line of commits.stdout.split("\n")) {
|
|
23517
|
+
if (!line.trim()) continue;
|
|
23518
|
+
const [hash, title = ""] = line.split("");
|
|
23519
|
+
if (!hash) continue;
|
|
23520
|
+
const diffResult = await runGit(worktreePath, [
|
|
23521
|
+
"show",
|
|
23522
|
+
"--numstat",
|
|
23523
|
+
"--format=",
|
|
23524
|
+
hash
|
|
23525
|
+
]);
|
|
23526
|
+
const changedFiles = (await runGit(worktreePath, [
|
|
23527
|
+
"show",
|
|
23528
|
+
"--name-only",
|
|
23529
|
+
"--format=",
|
|
23530
|
+
hash
|
|
23531
|
+
])).stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23532
|
+
entries.push({
|
|
23533
|
+
type: "commit",
|
|
23534
|
+
hash,
|
|
23535
|
+
title: title.trim() || hash.slice(0, 7),
|
|
23536
|
+
relatedChanges: parseRelatedChanges(changedFiles),
|
|
23537
|
+
diff: diffResult.ok ? parseNumStat(diffResult.stdout) : EMPTY_DIFF
|
|
23538
|
+
});
|
|
23539
|
+
}
|
|
23540
|
+
const trackedResult = await runGit(worktreePath, [
|
|
23541
|
+
"diff",
|
|
23542
|
+
"--numstat",
|
|
23543
|
+
"HEAD"
|
|
23544
|
+
]);
|
|
23545
|
+
const trackedFilesResult = await runGit(worktreePath, [
|
|
23546
|
+
"diff",
|
|
23547
|
+
"--name-only",
|
|
23548
|
+
"HEAD"
|
|
23549
|
+
]);
|
|
23550
|
+
const untrackedResult = await runGit(worktreePath, [
|
|
23551
|
+
"ls-files",
|
|
23552
|
+
"--others",
|
|
23553
|
+
"--exclude-standard"
|
|
23554
|
+
]);
|
|
23555
|
+
const trackedFiles = trackedFilesResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23556
|
+
const untrackedFiles = untrackedResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23557
|
+
const allUncommittedFiles = new Set([...trackedFiles, ...untrackedFiles]);
|
|
23558
|
+
const trackedDiff = trackedResult.ok ? parseNumStat(trackedResult.stdout) : EMPTY_DIFF;
|
|
23559
|
+
entries.push({
|
|
23560
|
+
type: "uncommitted",
|
|
23561
|
+
title: "Uncommitted",
|
|
23562
|
+
relatedChanges: parseRelatedChanges([...allUncommittedFiles]),
|
|
23563
|
+
diff: {
|
|
23564
|
+
files: allUncommittedFiles.size,
|
|
23565
|
+
insertions: trackedDiff.insertions,
|
|
23566
|
+
deletions: trackedDiff.deletions
|
|
23567
|
+
}
|
|
23568
|
+
});
|
|
23569
|
+
return entries;
|
|
23570
|
+
}
|
|
23571
|
+
async function collectWorktree(options) {
|
|
23572
|
+
const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries } = options;
|
|
23573
|
+
const worktreePath = resolve$1(worktree.path);
|
|
23574
|
+
const resolvedProjectDir = resolve$1(projectDir);
|
|
23575
|
+
const aheadBehindResult = await runGit(worktreePath, [
|
|
23576
|
+
"rev-list",
|
|
23577
|
+
"--left-right",
|
|
23578
|
+
"--count",
|
|
23579
|
+
`${defaultBranch}...HEAD`
|
|
23580
|
+
]);
|
|
23581
|
+
let ahead = 0;
|
|
23582
|
+
let behind = 0;
|
|
23583
|
+
if (aheadBehindResult.ok) {
|
|
23584
|
+
const [behindRaw, aheadRaw] = aheadBehindResult.stdout.trim().split(/\s+/);
|
|
23585
|
+
ahead = Number(aheadRaw) || 0;
|
|
23586
|
+
behind = Number(behindRaw) || 0;
|
|
23587
|
+
}
|
|
23588
|
+
const diffResult = await runGit(worktreePath, [
|
|
23589
|
+
"diff",
|
|
23590
|
+
"--shortstat",
|
|
23591
|
+
`${defaultBranch}...HEAD`
|
|
23592
|
+
]);
|
|
23593
|
+
const diff = diffResult.ok ? parseShortStat(diffResult.stdout) : EMPTY_DIFF;
|
|
23594
|
+
const entries = await collectCommitEntries({
|
|
23595
|
+
worktreePath,
|
|
23596
|
+
defaultBranch,
|
|
23597
|
+
maxCommitEntries,
|
|
23598
|
+
runGit
|
|
23599
|
+
});
|
|
23600
|
+
return {
|
|
23601
|
+
path: worktreePath,
|
|
23602
|
+
relativePath: relativePath(resolvedProjectDir, worktreePath),
|
|
23603
|
+
branchName: parseBranchName(worktree.branchRef, worktree.detached),
|
|
23604
|
+
isCurrent: resolvedProjectDir === worktreePath,
|
|
23605
|
+
ahead,
|
|
23606
|
+
behind,
|
|
23607
|
+
diff,
|
|
23608
|
+
entries
|
|
23609
|
+
};
|
|
23610
|
+
}
|
|
23611
|
+
async function buildDashboardGitSnapshot(options) {
|
|
23612
|
+
const runGit = options.runGit ?? defaultRunGit;
|
|
23613
|
+
const maxCommitEntries = options.maxCommitEntries ?? 8;
|
|
23614
|
+
const resolvedProjectDir = resolve$1(options.projectDir);
|
|
23615
|
+
const defaultBranch = await resolveDefaultBranch(resolvedProjectDir, runGit);
|
|
23616
|
+
const worktreeResult = await runGit(resolvedProjectDir, [
|
|
23617
|
+
"worktree",
|
|
23618
|
+
"list",
|
|
23619
|
+
"--porcelain"
|
|
23620
|
+
]);
|
|
23621
|
+
const parsed = worktreeResult.ok ? parseWorktreeList(worktreeResult.stdout) : [];
|
|
23622
|
+
const baseWorktrees = parsed.length > 0 ? parsed : [{
|
|
23623
|
+
path: resolvedProjectDir,
|
|
23624
|
+
branchRef: null,
|
|
23625
|
+
detached: false
|
|
23626
|
+
}];
|
|
23627
|
+
const worktrees = await Promise.all(baseWorktrees.map((worktree) => collectWorktree({
|
|
23628
|
+
projectDir: resolvedProjectDir,
|
|
23629
|
+
worktree,
|
|
23630
|
+
defaultBranch,
|
|
23631
|
+
runGit,
|
|
23632
|
+
maxCommitEntries
|
|
23633
|
+
})));
|
|
23634
|
+
worktrees.sort((a, b) => {
|
|
23635
|
+
if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1;
|
|
23636
|
+
return a.branchName.localeCompare(b.branchName);
|
|
23637
|
+
});
|
|
23638
|
+
return {
|
|
23639
|
+
defaultBranch,
|
|
23640
|
+
worktrees
|
|
23641
|
+
};
|
|
23642
|
+
}
|
|
23643
|
+
|
|
23644
|
+
//#endregion
|
|
23645
|
+
//#region ../server/src/dashboard-time-trends.ts
|
|
23646
|
+
const MIN_TREND_POINT_LIMIT = 20;
|
|
23647
|
+
const MAX_TREND_POINT_LIMIT = 500;
|
|
23648
|
+
const DEFAULT_TREND_POINT_LIMIT = 100;
|
|
23649
|
+
const TARGET_TREND_BARS = 20;
|
|
23650
|
+
const DAY_MS = 1440 * 60 * 1e3;
|
|
23651
|
+
function clampPointLimit(pointLimit) {
|
|
23652
|
+
if (!Number.isFinite(pointLimit)) return DEFAULT_TREND_POINT_LIMIT;
|
|
23653
|
+
return Math.max(MIN_TREND_POINT_LIMIT, Math.min(MAX_TREND_POINT_LIMIT, Math.trunc(pointLimit)));
|
|
23654
|
+
}
|
|
23655
|
+
function createEmptyTrendSeries() {
|
|
23656
|
+
return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
|
|
23657
|
+
}
|
|
23658
|
+
function normalizeEvents(events, pointLimit) {
|
|
23659
|
+
return events.filter((event) => Number.isFinite(event.ts) && event.ts > 0 && Number.isFinite(event.value)).sort((a, b) => a.ts - b.ts).slice(-pointLimit);
|
|
23660
|
+
}
|
|
23661
|
+
function buildTimeWindow(options) {
|
|
23662
|
+
const { probeEvents, targetBars, rightEdgeTs } = options;
|
|
23663
|
+
if (probeEvents.length === 0) return null;
|
|
23664
|
+
const probeEnd = probeEvents[probeEvents.length - 1].ts;
|
|
23665
|
+
const end = typeof rightEdgeTs === "number" && Number.isFinite(rightEdgeTs) && rightEdgeTs > 0 ? Math.max(probeEnd, rightEdgeTs) : probeEnd;
|
|
23666
|
+
const probeStart = probeEvents[0].ts;
|
|
23667
|
+
const rangeMs = Math.max(1, end - probeStart);
|
|
23668
|
+
const bucketMs = rangeMs >= DAY_MS ? Math.max(DAY_MS, Math.ceil(rangeMs / targetBars / DAY_MS) * DAY_MS) : Math.max(1, Math.ceil(rangeMs / targetBars));
|
|
23669
|
+
const windowStart = end - bucketMs * targetBars;
|
|
23670
|
+
return {
|
|
23671
|
+
windowStart,
|
|
23672
|
+
bucketMs,
|
|
23673
|
+
bucketEnds: Array.from({ length: targetBars }, (_, index) => windowStart + bucketMs * (index + 1))
|
|
23674
|
+
};
|
|
23675
|
+
}
|
|
23676
|
+
function bucketizeTrend(events, reducer, rightEdgeTs) {
|
|
23677
|
+
if (events.length === 0) return [];
|
|
23678
|
+
const timeWindow = buildTimeWindow({
|
|
23679
|
+
probeEvents: events,
|
|
23680
|
+
targetBars: TARGET_TREND_BARS,
|
|
23681
|
+
rightEdgeTs
|
|
23682
|
+
});
|
|
23683
|
+
if (!timeWindow) return [];
|
|
23684
|
+
const { windowStart, bucketMs, bucketEnds } = timeWindow;
|
|
23685
|
+
const sums = Array.from({ length: bucketEnds.length }, () => 0);
|
|
23686
|
+
const counts = Array.from({ length: bucketEnds.length }, () => 0);
|
|
23687
|
+
let baseline = 0;
|
|
23688
|
+
for (const event of events) {
|
|
23689
|
+
if (event.ts <= windowStart) {
|
|
23690
|
+
if (reducer === "sum-cumulative") baseline += event.value;
|
|
23691
|
+
continue;
|
|
23692
|
+
}
|
|
23693
|
+
const offset = event.ts - windowStart;
|
|
23694
|
+
const index = Math.max(0, Math.min(bucketEnds.length - 1, Math.ceil(offset / bucketMs) - 1));
|
|
23695
|
+
sums[index] += event.value;
|
|
23696
|
+
counts[index] += 1;
|
|
23697
|
+
}
|
|
23698
|
+
let cumulative = baseline;
|
|
23699
|
+
let carry = baseline !== 0 ? baseline : events[0].value;
|
|
23700
|
+
return bucketEnds.map((ts, index) => {
|
|
23701
|
+
if (reducer === "sum") return {
|
|
23702
|
+
ts,
|
|
23703
|
+
value: sums[index]
|
|
23704
|
+
};
|
|
23705
|
+
if (reducer === "sum-cumulative") {
|
|
23706
|
+
cumulative += sums[index];
|
|
23707
|
+
return {
|
|
23708
|
+
ts,
|
|
23709
|
+
value: cumulative
|
|
23710
|
+
};
|
|
23711
|
+
}
|
|
23712
|
+
if (counts[index] > 0) carry = sums[index] / counts[index];
|
|
23713
|
+
return {
|
|
23714
|
+
ts,
|
|
23715
|
+
value: carry
|
|
23716
|
+
};
|
|
23717
|
+
});
|
|
23718
|
+
}
|
|
23719
|
+
function buildDashboardTimeTrends(options) {
|
|
23720
|
+
const pointLimit = clampPointLimit(options.pointLimit);
|
|
23721
|
+
const trends = createEmptyTrendSeries();
|
|
23722
|
+
for (const metric of DASHBOARD_METRIC_KEYS) {
|
|
23723
|
+
if (options.availability[metric].state !== "ok") continue;
|
|
23724
|
+
trends[metric] = bucketizeTrend(normalizeEvents(options.events[metric], pointLimit), options.reducers?.[metric] ?? "sum", options.rightEdgeTs);
|
|
23725
|
+
}
|
|
23726
|
+
return {
|
|
23727
|
+
trends,
|
|
23728
|
+
trendMeta: {
|
|
23729
|
+
pointLimit,
|
|
23730
|
+
lastUpdatedAt: options.timestamp
|
|
23731
|
+
}
|
|
23732
|
+
};
|
|
23733
|
+
}
|
|
23734
|
+
|
|
23280
23735
|
//#endregion
|
|
23281
23736
|
//#region ../server/src/reactive-kv.ts
|
|
23282
23737
|
/**
|
|
@@ -23397,6 +23852,76 @@ function createReactiveSubscriptionWithInput(task) {
|
|
|
23397
23852
|
const t = initTRPC.context().create();
|
|
23398
23853
|
const router = t.router;
|
|
23399
23854
|
const publicProcedure = t.procedure;
|
|
23855
|
+
const execFileAsync = promisify$1(execFile);
|
|
23856
|
+
const dashboardGitTaskStatusEmitter = new EventEmitter$1();
|
|
23857
|
+
dashboardGitTaskStatusEmitter.setMaxListeners(200);
|
|
23858
|
+
const dashboardGitTaskStatus = {
|
|
23859
|
+
running: false,
|
|
23860
|
+
inFlight: 0,
|
|
23861
|
+
lastStartedAt: null,
|
|
23862
|
+
lastFinishedAt: null,
|
|
23863
|
+
lastReason: null,
|
|
23864
|
+
lastError: null
|
|
23865
|
+
};
|
|
23866
|
+
function getDashboardGitTaskStatus() {
|
|
23867
|
+
return { ...dashboardGitTaskStatus };
|
|
23868
|
+
}
|
|
23869
|
+
function emitDashboardGitTaskStatus() {
|
|
23870
|
+
dashboardGitTaskStatusEmitter.emit("change", getDashboardGitTaskStatus());
|
|
23871
|
+
}
|
|
23872
|
+
function beginDashboardGitTask(reason) {
|
|
23873
|
+
dashboardGitTaskStatus.inFlight += 1;
|
|
23874
|
+
dashboardGitTaskStatus.running = true;
|
|
23875
|
+
dashboardGitTaskStatus.lastStartedAt = Date.now();
|
|
23876
|
+
dashboardGitTaskStatus.lastReason = reason;
|
|
23877
|
+
dashboardGitTaskStatus.lastError = null;
|
|
23878
|
+
emitDashboardGitTaskStatus();
|
|
23879
|
+
}
|
|
23880
|
+
function endDashboardGitTask(error) {
|
|
23881
|
+
dashboardGitTaskStatus.inFlight = Math.max(0, dashboardGitTaskStatus.inFlight - 1);
|
|
23882
|
+
dashboardGitTaskStatus.running = dashboardGitTaskStatus.inFlight > 0;
|
|
23883
|
+
dashboardGitTaskStatus.lastFinishedAt = Date.now();
|
|
23884
|
+
if (error) dashboardGitTaskStatus.lastError = error instanceof Error ? error.message : String(error);
|
|
23885
|
+
emitDashboardGitTaskStatus();
|
|
23886
|
+
}
|
|
23887
|
+
function parseGitDirFromDotGitFile(content) {
|
|
23888
|
+
const line = content.split(/\r?\n/).map((item) => item.trim()).find((item) => item.startsWith("gitdir:"));
|
|
23889
|
+
if (!line) return null;
|
|
23890
|
+
const rawPath = line.slice(7).trim();
|
|
23891
|
+
return rawPath.length > 0 ? rawPath : null;
|
|
23892
|
+
}
|
|
23893
|
+
function getDashboardGitRefreshStampPath(projectDir) {
|
|
23894
|
+
return join$1(projectDir, "openspec", ".openspecui-dashboard-git-refresh.stamp");
|
|
23895
|
+
}
|
|
23896
|
+
async function touchDashboardGitRefreshStamp(projectDir, reason) {
|
|
23897
|
+
const stampPath = getDashboardGitRefreshStampPath(projectDir);
|
|
23898
|
+
await mkdir$1(dirname$1(stampPath), { recursive: true });
|
|
23899
|
+
await writeFile$1(stampPath, `${Date.now()} ${reason}\n`, "utf8");
|
|
23900
|
+
}
|
|
23901
|
+
async function registerDashboardGitReactiveDeps(projectDir) {
|
|
23902
|
+
await reactiveReadDir(projectDir, {
|
|
23903
|
+
includeHidden: true,
|
|
23904
|
+
exclude: ["node_modules"]
|
|
23905
|
+
});
|
|
23906
|
+
await reactiveReadFile(getDashboardGitRefreshStampPath(projectDir));
|
|
23907
|
+
const dotGitPath = join$1(projectDir, ".git");
|
|
23908
|
+
if (!await reactiveExists(dotGitPath)) return;
|
|
23909
|
+
const dotGitFileContent = await reactiveReadFile(dotGitPath);
|
|
23910
|
+
if (dotGitFileContent !== null) {
|
|
23911
|
+
const gitDirRaw = parseGitDirFromDotGitFile(dotGitFileContent);
|
|
23912
|
+
if (!gitDirRaw) return;
|
|
23913
|
+
const gitDirPath = resolve$1(projectDir, gitDirRaw);
|
|
23914
|
+
await reactiveReadDir(gitDirPath, { includeHidden: true });
|
|
23915
|
+
await reactiveReadFile(join$1(gitDirPath, "HEAD"));
|
|
23916
|
+
await reactiveReadFile(join$1(gitDirPath, "index"));
|
|
23917
|
+
await reactiveReadFile(join$1(gitDirPath, "packed-refs"));
|
|
23918
|
+
return;
|
|
23919
|
+
}
|
|
23920
|
+
await reactiveReadDir(dotGitPath, { includeHidden: true });
|
|
23921
|
+
await reactiveReadFile(join$1(dotGitPath, "HEAD"));
|
|
23922
|
+
await reactiveReadFile(join$1(dotGitPath, "index"));
|
|
23923
|
+
await reactiveReadFile(join$1(dotGitPath, "packed-refs"));
|
|
23924
|
+
}
|
|
23400
23925
|
function requireChangeId(changeId) {
|
|
23401
23926
|
if (!changeId) throw new Error("change is required");
|
|
23402
23927
|
return changeId;
|
|
@@ -23468,6 +23993,210 @@ async function fetchOpsxTemplateContents(ctx, schema$6) {
|
|
|
23468
23993
|
await ctx.kernel.ensureTemplateContents(schema$6);
|
|
23469
23994
|
return ctx.kernel.getTemplateContents(schema$6);
|
|
23470
23995
|
}
|
|
23996
|
+
function buildSystemStatus(ctx) {
|
|
23997
|
+
const runtime = getWatcherRuntimeStatus();
|
|
23998
|
+
return {
|
|
23999
|
+
projectDir: ctx.projectDir,
|
|
24000
|
+
watcherEnabled: runtime?.initialized ?? false,
|
|
24001
|
+
watcherGeneration: runtime?.generation ?? 0,
|
|
24002
|
+
watcherReinitializeCount: runtime?.reinitializeCount ?? 0,
|
|
24003
|
+
watcherLastReinitializeReason: runtime?.lastReinitializeReason ?? null
|
|
24004
|
+
};
|
|
24005
|
+
}
|
|
24006
|
+
function resolveTrendTimestamp(primary, secondary) {
|
|
24007
|
+
if (typeof primary === "number" && Number.isFinite(primary) && primary > 0) return primary;
|
|
24008
|
+
if (typeof secondary === "number" && Number.isFinite(secondary) && secondary > 0) return secondary;
|
|
24009
|
+
return null;
|
|
24010
|
+
}
|
|
24011
|
+
function parseDatedIdTimestamp(id) {
|
|
24012
|
+
const match$1 = /^(\d{4})-(\d{2})-(\d{2})(?:-|$)/.exec(id);
|
|
24013
|
+
if (!match$1) return null;
|
|
24014
|
+
const year = Number(match$1[1]);
|
|
24015
|
+
const month = Number(match$1[2]);
|
|
24016
|
+
const day = Number(match$1[3]);
|
|
24017
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return null;
|
|
24018
|
+
if (month < 1 || month > 12) return null;
|
|
24019
|
+
if (day < 1 || day > 31) return null;
|
|
24020
|
+
const ts = Date.UTC(year, month - 1, day);
|
|
24021
|
+
return Number.isFinite(ts) ? ts : null;
|
|
24022
|
+
}
|
|
24023
|
+
function createEmptyTriColorTrends() {
|
|
24024
|
+
return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
|
|
24025
|
+
}
|
|
24026
|
+
async function readLatestCommitTimestamp(projectDir) {
|
|
24027
|
+
try {
|
|
24028
|
+
const { stdout } = await execFileAsync("git", [
|
|
24029
|
+
"log",
|
|
24030
|
+
"-1",
|
|
24031
|
+
"--format=%ct"
|
|
24032
|
+
], {
|
|
24033
|
+
cwd: projectDir,
|
|
24034
|
+
maxBuffer: 1024 * 1024,
|
|
24035
|
+
encoding: "utf8"
|
|
24036
|
+
});
|
|
24037
|
+
const seconds = Number(stdout.trim());
|
|
24038
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : null;
|
|
24039
|
+
} catch {
|
|
24040
|
+
return null;
|
|
24041
|
+
}
|
|
24042
|
+
}
|
|
24043
|
+
async function fetchDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
24044
|
+
if (contextStorage.getStore()) await registerDashboardGitReactiveDeps(ctx.projectDir);
|
|
24045
|
+
const now = Date.now();
|
|
24046
|
+
const [specMetas, changeMetas, archiveMetas] = await Promise.all([
|
|
24047
|
+
ctx.adapter.listSpecsWithMeta(),
|
|
24048
|
+
ctx.adapter.listChangesWithMeta(),
|
|
24049
|
+
ctx.adapter.listArchivedChangesWithMeta()
|
|
24050
|
+
]);
|
|
24051
|
+
const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
|
|
24052
|
+
const change = await ctx.adapter.readArchivedChange(meta.id);
|
|
24053
|
+
if (!change) return null;
|
|
24054
|
+
return {
|
|
24055
|
+
id: meta.id,
|
|
24056
|
+
createdAt: meta.createdAt,
|
|
24057
|
+
updatedAt: meta.updatedAt,
|
|
24058
|
+
tasksCompleted: change.tasks.filter((task) => task.completed).length
|
|
24059
|
+
};
|
|
24060
|
+
}))).filter((item) => item !== null);
|
|
24061
|
+
const specifications = (await Promise.all(specMetas.map(async (meta) => {
|
|
24062
|
+
const spec = await ctx.adapter.readSpec(meta.id);
|
|
24063
|
+
if (!spec) return null;
|
|
24064
|
+
return {
|
|
24065
|
+
id: meta.id,
|
|
24066
|
+
name: meta.name,
|
|
24067
|
+
requirements: spec.requirements.length,
|
|
24068
|
+
updatedAt: meta.updatedAt
|
|
24069
|
+
};
|
|
24070
|
+
}))).filter((item) => item !== null).sort((a, b) => b.requirements - a.requirements || b.updatedAt - a.updatedAt);
|
|
24071
|
+
const activeChanges = changeMetas.map((change) => ({
|
|
24072
|
+
id: change.id,
|
|
24073
|
+
name: change.name,
|
|
24074
|
+
progress: change.progress,
|
|
24075
|
+
updatedAt: change.updatedAt
|
|
24076
|
+
}));
|
|
24077
|
+
const requirements = specifications.reduce((sum, spec) => sum + spec.requirements, 0);
|
|
24078
|
+
const tasksTotal = activeChanges.reduce((sum, change) => sum + change.progress.total, 0);
|
|
24079
|
+
const tasksCompleted = activeChanges.reduce((sum, change) => sum + change.progress.completed, 0);
|
|
24080
|
+
const archivedTasksCompleted = archivedChanges.reduce((sum, change) => sum + change.tasksCompleted, 0);
|
|
24081
|
+
const taskCompletionPercent = tasksTotal > 0 ? Math.round(tasksCompleted / tasksTotal * 100) : null;
|
|
24082
|
+
const inProgressChanges = activeChanges.filter((change) => change.progress.total > 0 && change.progress.completed < change.progress.total).length;
|
|
24083
|
+
const specificationTrendEvents = specMetas.flatMap((spec) => {
|
|
24084
|
+
const ts = resolveTrendTimestamp(spec.createdAt, spec.updatedAt);
|
|
24085
|
+
return ts === null ? [] : [{
|
|
24086
|
+
ts,
|
|
24087
|
+
value: 1
|
|
24088
|
+
}];
|
|
24089
|
+
});
|
|
24090
|
+
const completedTrendEvents = archivedChanges.flatMap((archive) => {
|
|
24091
|
+
const ts = parseDatedIdTimestamp(archive.id) ?? resolveTrendTimestamp(archive.updatedAt, archive.createdAt);
|
|
24092
|
+
return ts === null ? [] : [{
|
|
24093
|
+
ts,
|
|
24094
|
+
value: archive.tasksCompleted
|
|
24095
|
+
}];
|
|
24096
|
+
});
|
|
24097
|
+
const specMetaById = new Map(specMetas.map((meta) => [meta.id, meta]));
|
|
24098
|
+
const requirementTrendEvents = specifications.flatMap((spec) => {
|
|
24099
|
+
const meta = specMetaById.get(spec.id);
|
|
24100
|
+
const ts = resolveTrendTimestamp(meta?.updatedAt, meta?.createdAt);
|
|
24101
|
+
return ts === null ? [] : [{
|
|
24102
|
+
ts,
|
|
24103
|
+
value: spec.requirements
|
|
24104
|
+
}];
|
|
24105
|
+
});
|
|
24106
|
+
const hasObjectiveSpecificationTrend = specificationTrendEvents.length > 0 || specifications.length === 0;
|
|
24107
|
+
const hasObjectiveRequirementTrend = requirementTrendEvents.length > 0 || requirements === 0;
|
|
24108
|
+
const hasObjectiveCompletedTrend = completedTrendEvents.length > 0 || archiveMetas.length === 0;
|
|
24109
|
+
const config = await ctx.configManager.readConfig();
|
|
24110
|
+
beginDashboardGitTask(reason);
|
|
24111
|
+
let latestCommitTs = null;
|
|
24112
|
+
let git;
|
|
24113
|
+
try {
|
|
24114
|
+
const gitSnapshotPromise = buildDashboardGitSnapshot({ projectDir: ctx.projectDir }).catch(() => ({
|
|
24115
|
+
defaultBranch: "main",
|
|
24116
|
+
worktrees: []
|
|
24117
|
+
}));
|
|
24118
|
+
latestCommitTs = await readLatestCommitTimestamp(ctx.projectDir);
|
|
24119
|
+
git = await gitSnapshotPromise;
|
|
24120
|
+
} catch (error) {
|
|
24121
|
+
endDashboardGitTask(error);
|
|
24122
|
+
throw error;
|
|
24123
|
+
}
|
|
24124
|
+
endDashboardGitTask(null);
|
|
24125
|
+
const cardAvailability = {
|
|
24126
|
+
specifications: hasObjectiveSpecificationTrend ? { state: "ok" } : {
|
|
24127
|
+
state: "invalid",
|
|
24128
|
+
reason: "objective-history-unavailable"
|
|
24129
|
+
},
|
|
24130
|
+
requirements: hasObjectiveRequirementTrend ? { state: "ok" } : {
|
|
24131
|
+
state: "invalid",
|
|
24132
|
+
reason: "objective-history-unavailable"
|
|
24133
|
+
},
|
|
24134
|
+
activeChanges: {
|
|
24135
|
+
state: "invalid",
|
|
24136
|
+
reason: "objective-history-unavailable"
|
|
24137
|
+
},
|
|
24138
|
+
inProgressChanges: {
|
|
24139
|
+
state: "invalid",
|
|
24140
|
+
reason: "objective-history-unavailable"
|
|
24141
|
+
},
|
|
24142
|
+
completedChanges: hasObjectiveCompletedTrend ? { state: "ok" } : {
|
|
24143
|
+
state: "invalid",
|
|
24144
|
+
reason: "objective-history-unavailable"
|
|
24145
|
+
},
|
|
24146
|
+
taskCompletionPercent: {
|
|
24147
|
+
state: "invalid",
|
|
24148
|
+
reason: taskCompletionPercent === null ? "semantic-uncomputable" : "objective-history-unavailable"
|
|
24149
|
+
}
|
|
24150
|
+
};
|
|
24151
|
+
const trendKinds = {
|
|
24152
|
+
specifications: "monotonic",
|
|
24153
|
+
requirements: "monotonic",
|
|
24154
|
+
activeChanges: "bidirectional",
|
|
24155
|
+
inProgressChanges: "bidirectional",
|
|
24156
|
+
completedChanges: "monotonic",
|
|
24157
|
+
taskCompletionPercent: "bidirectional"
|
|
24158
|
+
};
|
|
24159
|
+
const { trends: baselineTrends, trendMeta } = buildDashboardTimeTrends({
|
|
24160
|
+
pointLimit: config.dashboard.trendPointLimit,
|
|
24161
|
+
timestamp: now,
|
|
24162
|
+
rightEdgeTs: latestCommitTs,
|
|
24163
|
+
availability: cardAvailability,
|
|
24164
|
+
events: {
|
|
24165
|
+
specifications: specificationTrendEvents,
|
|
24166
|
+
requirements: requirementTrendEvents,
|
|
24167
|
+
activeChanges: [],
|
|
24168
|
+
inProgressChanges: [],
|
|
24169
|
+
completedChanges: completedTrendEvents,
|
|
24170
|
+
taskCompletionPercent: []
|
|
24171
|
+
},
|
|
24172
|
+
reducers: {
|
|
24173
|
+
specifications: "sum",
|
|
24174
|
+
requirements: "sum",
|
|
24175
|
+
completedChanges: "sum"
|
|
24176
|
+
}
|
|
24177
|
+
});
|
|
24178
|
+
return {
|
|
24179
|
+
summary: {
|
|
24180
|
+
specifications: specifications.length,
|
|
24181
|
+
requirements,
|
|
24182
|
+
activeChanges: activeChanges.length,
|
|
24183
|
+
inProgressChanges,
|
|
24184
|
+
completedChanges: archiveMetas.length,
|
|
24185
|
+
archivedTasksCompleted,
|
|
24186
|
+
tasksTotal,
|
|
24187
|
+
tasksCompleted,
|
|
24188
|
+
taskCompletionPercent
|
|
24189
|
+
},
|
|
24190
|
+
trends: baselineTrends,
|
|
24191
|
+
triColorTrends: createEmptyTriColorTrends(),
|
|
24192
|
+
trendKinds,
|
|
24193
|
+
cardAvailability,
|
|
24194
|
+
trendMeta,
|
|
24195
|
+
specifications,
|
|
24196
|
+
activeChanges,
|
|
24197
|
+
git
|
|
24198
|
+
};
|
|
24199
|
+
}
|
|
23471
24200
|
/**
|
|
23472
24201
|
* Spec router - spec CRUD operations
|
|
23473
24202
|
*/
|
|
@@ -23675,25 +24404,17 @@ const configRouter = router({
|
|
|
23675
24404
|
"dark",
|
|
23676
24405
|
"system"
|
|
23677
24406
|
]).optional(),
|
|
23678
|
-
terminal:
|
|
23679
|
-
|
|
23680
|
-
fontFamily: stringType().optional(),
|
|
23681
|
-
cursorBlink: booleanType().optional(),
|
|
23682
|
-
cursorStyle: enumType([
|
|
23683
|
-
"block",
|
|
23684
|
-
"underline",
|
|
23685
|
-
"bar"
|
|
23686
|
-
]).optional(),
|
|
23687
|
-
scrollback: numberType().min(0).max(1e5).optional()
|
|
23688
|
-
}).optional()
|
|
24407
|
+
terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
|
|
24408
|
+
dashboard: DashboardConfigSchema.partial().optional()
|
|
23689
24409
|
})).mutation(async ({ ctx, input }) => {
|
|
23690
24410
|
const hasCliCommand = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "command");
|
|
23691
24411
|
const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
|
|
23692
24412
|
if (hasCliCommand && !hasCliArgs) {
|
|
23693
24413
|
await ctx.configManager.setCliCommand(input.cli?.command ?? "");
|
|
23694
|
-
if (input.theme !== void 0 || input.terminal !== void 0) await ctx.configManager.writeConfig({
|
|
24414
|
+
if (input.theme !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0) await ctx.configManager.writeConfig({
|
|
23695
24415
|
theme: input.theme,
|
|
23696
|
-
terminal: input.terminal
|
|
24416
|
+
terminal: input.terminal,
|
|
24417
|
+
dashboard: input.dashboard
|
|
23697
24418
|
});
|
|
23698
24419
|
return { success: true };
|
|
23699
24420
|
}
|
|
@@ -24130,9 +24851,63 @@ const searchRouter = router({
|
|
|
24130
24851
|
})
|
|
24131
24852
|
});
|
|
24132
24853
|
/**
|
|
24854
|
+
* System router - runtime status and heartbeat-friendly subscription
|
|
24855
|
+
*/
|
|
24856
|
+
const systemRouter = router({
|
|
24857
|
+
status: publicProcedure.query(({ ctx }) => {
|
|
24858
|
+
return buildSystemStatus(ctx);
|
|
24859
|
+
}),
|
|
24860
|
+
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
24861
|
+
return observable((emit) => {
|
|
24862
|
+
emit.next(buildSystemStatus(ctx));
|
|
24863
|
+
const timer = setInterval(() => {
|
|
24864
|
+
emit.next(buildSystemStatus(ctx));
|
|
24865
|
+
}, 3e3);
|
|
24866
|
+
timer.unref();
|
|
24867
|
+
return () => {
|
|
24868
|
+
clearInterval(timer);
|
|
24869
|
+
};
|
|
24870
|
+
});
|
|
24871
|
+
})
|
|
24872
|
+
});
|
|
24873
|
+
/**
|
|
24874
|
+
* Dashboard router - objective project overview for UI
|
|
24875
|
+
*/
|
|
24876
|
+
const dashboardRouter = router({
|
|
24877
|
+
get: publicProcedure.query(async ({ ctx }) => {
|
|
24878
|
+
return fetchDashboardOverview(ctx, "dashboard.get");
|
|
24879
|
+
}),
|
|
24880
|
+
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
24881
|
+
return createReactiveSubscription(async () => {
|
|
24882
|
+
return fetchDashboardOverview(ctx, "dashboard.subscribe");
|
|
24883
|
+
});
|
|
24884
|
+
}),
|
|
24885
|
+
refreshGitSnapshot: publicProcedure.input(objectType({ reason: stringType().optional() }).optional()).mutation(async ({ ctx, input }) => {
|
|
24886
|
+
const reason = input?.reason?.trim() || "manual-refresh";
|
|
24887
|
+
await touchDashboardGitRefreshStamp(ctx.projectDir, reason);
|
|
24888
|
+
return { success: true };
|
|
24889
|
+
}),
|
|
24890
|
+
gitTaskStatus: publicProcedure.query(() => {
|
|
24891
|
+
return getDashboardGitTaskStatus();
|
|
24892
|
+
}),
|
|
24893
|
+
subscribeGitTaskStatus: publicProcedure.subscription(() => {
|
|
24894
|
+
return observable((emit) => {
|
|
24895
|
+
emit.next(getDashboardGitTaskStatus());
|
|
24896
|
+
const handler = (status) => {
|
|
24897
|
+
emit.next(status);
|
|
24898
|
+
};
|
|
24899
|
+
dashboardGitTaskStatusEmitter.on("change", handler);
|
|
24900
|
+
return () => {
|
|
24901
|
+
dashboardGitTaskStatusEmitter.off("change", handler);
|
|
24902
|
+
};
|
|
24903
|
+
});
|
|
24904
|
+
})
|
|
24905
|
+
});
|
|
24906
|
+
/**
|
|
24133
24907
|
* Main app router
|
|
24134
24908
|
*/
|
|
24135
24909
|
const appRouter = router({
|
|
24910
|
+
dashboard: dashboardRouter,
|
|
24136
24911
|
spec: specRouter,
|
|
24137
24912
|
change: changeRouter,
|
|
24138
24913
|
archive: archiveRouter,
|
|
@@ -24142,11 +24917,12 @@ const appRouter = router({
|
|
|
24142
24917
|
cli: cliRouter,
|
|
24143
24918
|
opsx: opsxRouter,
|
|
24144
24919
|
kv: kvRouter,
|
|
24145
|
-
search: searchRouter
|
|
24920
|
+
search: searchRouter,
|
|
24921
|
+
system: systemRouter
|
|
24146
24922
|
});
|
|
24147
24923
|
|
|
24148
24924
|
//#endregion
|
|
24149
|
-
//#region ../search/
|
|
24925
|
+
//#region ../search/src/node-worker-provider.ts
|
|
24150
24926
|
function requestId() {
|
|
24151
24927
|
return Math.random().toString(36).slice(2);
|
|
24152
24928
|
}
|
|
@@ -24448,7 +25224,12 @@ async function createWebSocketServer(server, httpServer, config) {
|
|
|
24448
25224
|
const handler = applyWSSHandler({
|
|
24449
25225
|
wss,
|
|
24450
25226
|
router: appRouter,
|
|
24451
|
-
createContext: server.createContext
|
|
25227
|
+
createContext: server.createContext,
|
|
25228
|
+
keepAlive: {
|
|
25229
|
+
enabled: true,
|
|
25230
|
+
pingMs: 3e4,
|
|
25231
|
+
pongWaitMs: 5e3
|
|
25232
|
+
}
|
|
24452
25233
|
});
|
|
24453
25234
|
const ptyManager = new PtyManager(config.projectDir);
|
|
24454
25235
|
const ptyWss = new import_websocket_server.default({ noServer: true });
|
|
@@ -24589,4 +25370,4 @@ async function startServer$1(options = {}) {
|
|
|
24589
25370
|
}
|
|
24590
25371
|
|
|
24591
25372
|
//#endregion
|
|
24592
|
-
export { SchemaInfoSchema as a, CliExecutor as c,
|
|
25373
|
+
export { SchemaInfoSchema as a, CliExecutor as c, OpenSpecAdapter as d, __commonJS$1 as f, SchemaDetailSchema as i, ConfigManager as l, createServer$2 as n, SchemaResolutionSchema as o, __toESM$1 as p, require_dist as r, TemplatesSchema as s, startServer$1 as t, DEFAULT_CONFIG as u };
|