openspecui 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{src-B5XQ-ERE.mjs → src-Nn_MJz41.mjs} +1424 -1294
- package/package.json +17 -13
- package/web/assets/{BufferResource-WPSrP4iZ.js → BufferResource-CSbEbiLP.js} +1 -1
- package/web/assets/{CanvasRenderer-yL0hHLLA.js → CanvasRenderer-CvpAMPzA.js} +1 -1
- package/web/assets/{Filter-MPAI5-Oj.js → Filter-BqwVCa6B.js} +1 -1
- package/web/assets/{RenderTargetSystem-D4qtRaUW.js → RenderTargetSystem-mD_Y2dUA.js} +1 -1
- package/web/assets/{WebGLRenderer-BHfrXEBc.js → WebGLRenderer-DsvDwXyL.js} +1 -1
- package/web/assets/{WebGPURenderer-DE5Q1Ilu.js → WebGPURenderer-YcBYXAol.js} +1 -1
- package/web/assets/{browserAll-qg0ACMg1.js → browserAll-MbWxSnea.js} +1 -1
- package/web/assets/{ghostty-web-D1cPLYOq.js → ghostty-web-cM8zjOdb.js} +1 -1
- package/web/assets/{index-BQWvXpvf.js → index-B0lABsCj.js} +1 -1
- package/web/assets/{index-D7oe-Hg-.js → index-B1J3yPM2.js} +1 -1
- package/web/assets/{index-BEpMDdcu.js → index-BCMJS8eq.js} +1 -1
- package/web/assets/{index-C8igR5_C.js → index-BMKX_bGe.js} +1 -1
- package/web/assets/{index-BTEmB46j.js → index-BY40EQxT.js} +1 -1
- package/web/assets/{index-BLmH2_gh.js → index-Cq4njHD4.js} +183 -182
- package/web/assets/{index-DlK0frtF.js → index-Cr4OFm9E.js} +1 -1
- package/web/assets/{index-DBI0U-_m.js → index-CskIUxS-.js} +1 -1
- package/web/assets/{index-B2AIdjev.js → index-D0sXMPhu.js} +1 -1
- package/web/assets/{index-D1IhiEyy.css → index-DHeR2UYq.css} +1 -1
- package/web/assets/{index-DfNI0fNY.js → index-DijRYB99.js} +1 -1
- package/web/assets/{index-Gknjn7E7.js → index-DuZDcW_P.js} +1 -1
- package/web/assets/{index-DYz8XhtF.js → index-DyZwNTgF.js} +1 -1
- package/web/assets/{index-BM_5Tg9c.js → index-JHrsE4PU.js} +1 -1
- package/web/assets/{index-CQRaC2Gn.js → index-KaodXPu0.js} +1 -1
- package/web/assets/{index-DMpt0Kcu.js → index-TGQYcfbH.js} +1 -1
- package/web/assets/{index-CSledjj-.js → index-kjHuAdn9.js} +1 -1
- package/web/assets/{webworkerAll-iHUuedbL.js → webworkerAll-BE_Ec3Rk.js} +1 -1
- package/web/index.html +2 -2
- package/LICENSE +0 -21
|
@@ -15,10 +15,10 @@ import { watch } from "fs";
|
|
|
15
15
|
import { exec, spawn } from "child_process";
|
|
16
16
|
import { promisify } from "util";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import * as pty from "@lydell/node-pty";
|
|
19
|
-
import { execFile } from "node:child_process";
|
|
20
18
|
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
19
|
+
import { execFile } from "node:child_process";
|
|
21
20
|
import { promisify as promisify$1 } from "node:util";
|
|
21
|
+
import * as pty from "@lydell/node-pty";
|
|
22
22
|
import { Worker as Worker$1 } from "node:worker_threads";
|
|
23
23
|
|
|
24
24
|
//#region rolldown:runtime
|
|
@@ -22886,1022 +22886,1390 @@ var import_websocket = /* @__PURE__ */ __toESM$1(require_websocket(), 1);
|
|
|
22886
22886
|
var import_websocket_server = /* @__PURE__ */ __toESM$1(require_websocket_server(), 1);
|
|
22887
22887
|
|
|
22888
22888
|
//#endregion
|
|
22889
|
-
//#region ../server/src/
|
|
22890
|
-
const
|
|
22891
|
-
|
|
22892
|
-
|
|
22893
|
-
|
|
22894
|
-
|
|
22895
|
-
|
|
22896
|
-
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
|
|
22902
|
-
|
|
22903
|
-
|
|
22904
|
-
command,
|
|
22905
|
-
args: opts.args ?? []
|
|
22906
|
-
};
|
|
22907
|
-
return {
|
|
22908
|
-
command: resolveDefaultShell(opts.platform, opts.env),
|
|
22909
|
-
args: []
|
|
22910
|
-
};
|
|
22911
|
-
}
|
|
22912
|
-
var PtySession = class extends EventEmitter {
|
|
22913
|
-
id;
|
|
22914
|
-
command;
|
|
22915
|
-
args;
|
|
22916
|
-
platform;
|
|
22917
|
-
closeTip;
|
|
22918
|
-
closeCallbackUrl;
|
|
22919
|
-
createdAt;
|
|
22920
|
-
process;
|
|
22921
|
-
titleInterval = null;
|
|
22922
|
-
lastTitle = "";
|
|
22923
|
-
buffer = [];
|
|
22924
|
-
bufferByteLength = 0;
|
|
22925
|
-
maxBufferLines;
|
|
22926
|
-
maxBufferBytes;
|
|
22927
|
-
isExited = false;
|
|
22928
|
-
exitCode = null;
|
|
22929
|
-
constructor(id, opts) {
|
|
22930
|
-
super();
|
|
22931
|
-
this.id = id;
|
|
22932
|
-
this.createdAt = Date.now();
|
|
22933
|
-
const resolvedCommand = resolvePtyCommand({
|
|
22934
|
-
platform: opts.platform,
|
|
22935
|
-
command: opts.command,
|
|
22936
|
-
args: opts.args,
|
|
22937
|
-
env: process.env
|
|
22938
|
-
});
|
|
22939
|
-
this.command = resolvedCommand.command;
|
|
22940
|
-
this.args = resolvedCommand.args;
|
|
22941
|
-
this.platform = opts.platform;
|
|
22942
|
-
this.closeTip = opts.closeTip;
|
|
22943
|
-
this.closeCallbackUrl = opts.closeCallbackUrl;
|
|
22944
|
-
this.maxBufferLines = opts.scrollback ?? DEFAULT_SCROLLBACK;
|
|
22945
|
-
this.maxBufferBytes = opts.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
22946
|
-
this.process = pty.spawn(this.command, this.args, {
|
|
22947
|
-
name: "xterm-256color",
|
|
22948
|
-
cols: opts.cols ?? 80,
|
|
22949
|
-
rows: opts.rows ?? 24,
|
|
22950
|
-
cwd: opts.cwd,
|
|
22951
|
-
env: {
|
|
22952
|
-
...process.env,
|
|
22953
|
-
TERM: "xterm-256color"
|
|
22954
|
-
}
|
|
22955
|
-
});
|
|
22956
|
-
this.process.onData((data) => {
|
|
22957
|
-
this.appendBuffer(data);
|
|
22958
|
-
this.emit("data", data);
|
|
22959
|
-
});
|
|
22960
|
-
this.process.onExit(({ exitCode }) => {
|
|
22961
|
-
if (this.titleInterval) {
|
|
22962
|
-
clearInterval(this.titleInterval);
|
|
22963
|
-
this.titleInterval = null;
|
|
22964
|
-
}
|
|
22965
|
-
this.isExited = true;
|
|
22966
|
-
this.exitCode = exitCode;
|
|
22967
|
-
this.emit("exit", exitCode);
|
|
22889
|
+
//#region ../server/src/dashboard-overview-service.ts
|
|
22890
|
+
const REBUILD_DEBOUNCE_MS$1 = 250;
|
|
22891
|
+
var DashboardOverviewService = class {
|
|
22892
|
+
current = null;
|
|
22893
|
+
initialized = false;
|
|
22894
|
+
initPromise = null;
|
|
22895
|
+
refreshPromise = null;
|
|
22896
|
+
refreshTimer = null;
|
|
22897
|
+
pendingRefreshReason = null;
|
|
22898
|
+
emitter = new EventEmitter$1();
|
|
22899
|
+
constructor(loadOverview, watcher) {
|
|
22900
|
+
this.loadOverview = loadOverview;
|
|
22901
|
+
this.emitter.setMaxListeners(200);
|
|
22902
|
+
watcher?.on("change", () => {
|
|
22903
|
+
this.scheduleRefresh("watcher-change");
|
|
22968
22904
|
});
|
|
22969
|
-
this.titleInterval = setInterval(() => {
|
|
22970
|
-
try {
|
|
22971
|
-
const title = this.process.process;
|
|
22972
|
-
if (title && title !== this.lastTitle) {
|
|
22973
|
-
this.lastTitle = title;
|
|
22974
|
-
this.emit("title", title);
|
|
22975
|
-
}
|
|
22976
|
-
} catch {}
|
|
22977
|
-
}, 1e3);
|
|
22978
|
-
}
|
|
22979
|
-
get title() {
|
|
22980
|
-
return this.lastTitle;
|
|
22981
|
-
}
|
|
22982
|
-
appendBuffer(data) {
|
|
22983
|
-
let chunk = data;
|
|
22984
|
-
if (chunk.length > this.maxBufferBytes) chunk = chunk.slice(-this.maxBufferBytes);
|
|
22985
|
-
this.buffer.push(chunk);
|
|
22986
|
-
this.bufferByteLength += chunk.length;
|
|
22987
|
-
while (this.bufferByteLength > this.maxBufferBytes && this.buffer.length > 0) {
|
|
22988
|
-
const removed = this.buffer.shift();
|
|
22989
|
-
this.bufferByteLength -= removed.length;
|
|
22990
|
-
}
|
|
22991
|
-
while (this.buffer.length > this.maxBufferLines) {
|
|
22992
|
-
const removed = this.buffer.shift();
|
|
22993
|
-
this.bufferByteLength -= removed.length;
|
|
22994
|
-
}
|
|
22995
|
-
}
|
|
22996
|
-
getBuffer() {
|
|
22997
|
-
return this.buffer.join("");
|
|
22998
|
-
}
|
|
22999
|
-
write(data) {
|
|
23000
|
-
if (!this.isExited) this.process.write(data);
|
|
23001
22905
|
}
|
|
23002
|
-
|
|
23003
|
-
if (
|
|
23004
|
-
|
|
23005
|
-
|
|
23006
|
-
if (this.titleInterval) {
|
|
23007
|
-
clearInterval(this.titleInterval);
|
|
23008
|
-
this.titleInterval = null;
|
|
23009
|
-
}
|
|
22906
|
+
async init() {
|
|
22907
|
+
if (this.initialized && this.current) return this.current;
|
|
22908
|
+
if (this.initPromise) return this.initPromise;
|
|
22909
|
+
this.initPromise = this.refresh("init");
|
|
23010
22910
|
try {
|
|
23011
|
-
this.
|
|
23012
|
-
}
|
|
23013
|
-
|
|
23014
|
-
|
|
23015
|
-
toInfo() {
|
|
23016
|
-
return {
|
|
23017
|
-
id: this.id,
|
|
23018
|
-
title: this.lastTitle,
|
|
23019
|
-
command: this.command,
|
|
23020
|
-
args: this.args,
|
|
23021
|
-
platform: this.platform,
|
|
23022
|
-
isExited: this.isExited,
|
|
23023
|
-
exitCode: this.exitCode,
|
|
23024
|
-
closeTip: this.closeTip,
|
|
23025
|
-
closeCallbackUrl: this.closeCallbackUrl,
|
|
23026
|
-
createdAt: this.createdAt
|
|
23027
|
-
};
|
|
22911
|
+
return await this.initPromise;
|
|
22912
|
+
} finally {
|
|
22913
|
+
this.initPromise = null;
|
|
22914
|
+
}
|
|
23028
22915
|
}
|
|
23029
|
-
|
|
23030
|
-
|
|
23031
|
-
|
|
23032
|
-
idCounter = 0;
|
|
23033
|
-
platform;
|
|
23034
|
-
constructor(defaultCwd) {
|
|
23035
|
-
this.defaultCwd = defaultCwd;
|
|
23036
|
-
this.platform = detectPtyPlatform();
|
|
22916
|
+
async getCurrent() {
|
|
22917
|
+
if (this.current) return this.current;
|
|
22918
|
+
return this.init();
|
|
23037
22919
|
}
|
|
23038
|
-
|
|
23039
|
-
|
|
23040
|
-
|
|
23041
|
-
|
|
23042
|
-
|
|
23043
|
-
command: opts.command,
|
|
23044
|
-
args: opts.args,
|
|
23045
|
-
closeTip: opts.closeTip,
|
|
23046
|
-
closeCallbackUrl: opts.closeCallbackUrl,
|
|
23047
|
-
cwd: this.defaultCwd,
|
|
23048
|
-
scrollback: opts.scrollback,
|
|
23049
|
-
maxBufferBytes: opts.maxBufferBytes,
|
|
23050
|
-
platform: this.platform
|
|
22920
|
+
subscribe(listener, options) {
|
|
22921
|
+
this.emitter.on("change", listener);
|
|
22922
|
+
if (options?.emitCurrent) if (this.current) listener(this.current);
|
|
22923
|
+
else this.init().catch((error) => {
|
|
22924
|
+
options?.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
23051
22925
|
});
|
|
23052
|
-
|
|
23053
|
-
|
|
23054
|
-
|
|
23055
|
-
get(id) {
|
|
23056
|
-
return this.sessions.get(id);
|
|
23057
|
-
}
|
|
23058
|
-
list() {
|
|
23059
|
-
const result = [];
|
|
23060
|
-
for (const session of this.sessions.values()) result.push(session.toInfo());
|
|
23061
|
-
return result;
|
|
23062
|
-
}
|
|
23063
|
-
write(id, data) {
|
|
23064
|
-
this.sessions.get(id)?.write(data);
|
|
23065
|
-
}
|
|
23066
|
-
resize(id, cols, rows) {
|
|
23067
|
-
this.sessions.get(id)?.resize(cols, rows);
|
|
22926
|
+
return () => {
|
|
22927
|
+
this.emitter.off("change", listener);
|
|
22928
|
+
};
|
|
23068
22929
|
}
|
|
23069
|
-
|
|
23070
|
-
|
|
23071
|
-
|
|
23072
|
-
|
|
23073
|
-
this.
|
|
22930
|
+
scheduleRefresh(reason = "scheduled-refresh") {
|
|
22931
|
+
this.cancelScheduledRefresh();
|
|
22932
|
+
this.refreshTimer = setTimeout(() => {
|
|
22933
|
+
this.refreshTimer = null;
|
|
22934
|
+
this.refresh(reason).catch(() => {});
|
|
22935
|
+
}, REBUILD_DEBOUNCE_MS$1);
|
|
22936
|
+
}
|
|
22937
|
+
async refresh(reason = "manual-refresh") {
|
|
22938
|
+
this.cancelScheduledRefresh();
|
|
22939
|
+
if (this.refreshPromise) {
|
|
22940
|
+
this.pendingRefreshReason = reason;
|
|
22941
|
+
return this.refreshPromise;
|
|
22942
|
+
}
|
|
22943
|
+
this.refreshPromise = (async () => {
|
|
22944
|
+
const next = await this.loadOverview(reason);
|
|
22945
|
+
this.current = next;
|
|
22946
|
+
this.initialized = true;
|
|
22947
|
+
this.emitter.emit("change", next);
|
|
22948
|
+
return next;
|
|
22949
|
+
})();
|
|
22950
|
+
try {
|
|
22951
|
+
return await this.refreshPromise;
|
|
22952
|
+
} finally {
|
|
22953
|
+
this.refreshPromise = null;
|
|
22954
|
+
if (this.pendingRefreshReason) {
|
|
22955
|
+
const pendingReason = this.pendingRefreshReason;
|
|
22956
|
+
this.pendingRefreshReason = null;
|
|
22957
|
+
this.refresh(pendingReason).catch(() => {});
|
|
22958
|
+
}
|
|
23074
22959
|
}
|
|
23075
22960
|
}
|
|
23076
|
-
|
|
23077
|
-
|
|
23078
|
-
this.
|
|
22961
|
+
dispose() {
|
|
22962
|
+
this.cancelScheduledRefresh();
|
|
22963
|
+
this.emitter.removeAllListeners();
|
|
22964
|
+
}
|
|
22965
|
+
cancelScheduledRefresh() {
|
|
22966
|
+
if (!this.refreshTimer) return;
|
|
22967
|
+
clearTimeout(this.refreshTimer);
|
|
22968
|
+
this.refreshTimer = null;
|
|
23079
22969
|
}
|
|
23080
22970
|
};
|
|
23081
22971
|
|
|
23082
22972
|
//#endregion
|
|
23083
|
-
//#region ../server/src/
|
|
23084
|
-
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
23088
|
-
|
|
22973
|
+
//#region ../server/src/dashboard-git-snapshot.ts
|
|
22974
|
+
const execFileAsync$1 = promisify$1(execFile);
|
|
22975
|
+
const EMPTY_DIFF = {
|
|
22976
|
+
files: 0,
|
|
22977
|
+
insertions: 0,
|
|
22978
|
+
deletions: 0
|
|
22979
|
+
};
|
|
22980
|
+
async function defaultRunGit(cwd, args) {
|
|
22981
|
+
try {
|
|
22982
|
+
const { stdout } = await execFileAsync$1("git", args, {
|
|
22983
|
+
cwd,
|
|
22984
|
+
encoding: "utf8",
|
|
22985
|
+
maxBuffer: 8 * 1024 * 1024
|
|
22986
|
+
});
|
|
22987
|
+
return {
|
|
22988
|
+
ok: true,
|
|
22989
|
+
stdout
|
|
23089
22990
|
};
|
|
23090
|
-
|
|
23091
|
-
|
|
23092
|
-
|
|
23093
|
-
|
|
23094
|
-
message,
|
|
23095
|
-
sessionId: opts?.sessionId
|
|
23096
|
-
});
|
|
22991
|
+
} catch {
|
|
22992
|
+
return {
|
|
22993
|
+
ok: false,
|
|
22994
|
+
stdout: ""
|
|
23097
22995
|
};
|
|
23098
|
-
|
|
23099
|
-
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23103
|
-
|
|
23104
|
-
|
|
23105
|
-
|
|
23106
|
-
|
|
23107
|
-
|
|
23108
|
-
};
|
|
23109
|
-
const onExit = (exitCode) => {
|
|
23110
|
-
send({
|
|
23111
|
-
type: "exit",
|
|
23112
|
-
sessionId,
|
|
23113
|
-
exitCode
|
|
23114
|
-
});
|
|
23115
|
-
};
|
|
23116
|
-
const onTitle = (title) => {
|
|
23117
|
-
send({
|
|
23118
|
-
type: "title",
|
|
23119
|
-
sessionId,
|
|
23120
|
-
title
|
|
23121
|
-
});
|
|
23122
|
-
};
|
|
23123
|
-
session.on("data", onData);
|
|
23124
|
-
session.on("exit", onExit);
|
|
23125
|
-
session.on("title", onTitle);
|
|
23126
|
-
cleanups.set(sessionId, () => {
|
|
23127
|
-
session.removeListener("data", onData);
|
|
23128
|
-
session.removeListener("exit", onExit);
|
|
23129
|
-
session.removeListener("title", onTitle);
|
|
23130
|
-
cleanups.delete(sessionId);
|
|
23131
|
-
});
|
|
23132
|
-
};
|
|
23133
|
-
ws.on("message", (raw$1) => {
|
|
23134
|
-
let parsed;
|
|
23135
|
-
try {
|
|
23136
|
-
parsed = JSON.parse(String(raw$1));
|
|
23137
|
-
} catch {
|
|
23138
|
-
sendError("INVALID_JSON", "Invalid JSON payload");
|
|
23139
|
-
return;
|
|
23140
|
-
}
|
|
23141
|
-
const parsedMessage = PtyClientMessageSchema.safeParse(parsed);
|
|
23142
|
-
if (!parsedMessage.success) {
|
|
23143
|
-
const firstIssue = parsedMessage.error.issues[0]?.message;
|
|
23144
|
-
sendError("INVALID_MESSAGE", firstIssue ?? "Invalid PTY message");
|
|
23145
|
-
return;
|
|
23146
|
-
}
|
|
23147
|
-
const msg = parsedMessage.data;
|
|
23148
|
-
switch (msg.type) {
|
|
23149
|
-
case "create":
|
|
23150
|
-
try {
|
|
23151
|
-
const createMessage = msg;
|
|
23152
|
-
const session = ptyManager.create({
|
|
23153
|
-
cols: msg.cols,
|
|
23154
|
-
rows: msg.rows,
|
|
23155
|
-
command: msg.command,
|
|
23156
|
-
args: msg.args,
|
|
23157
|
-
closeTip: createMessage.closeTip,
|
|
23158
|
-
closeCallbackUrl: createMessage.closeCallbackUrl
|
|
23159
|
-
});
|
|
23160
|
-
send({
|
|
23161
|
-
type: "created",
|
|
23162
|
-
requestId: msg.requestId,
|
|
23163
|
-
sessionId: session.id,
|
|
23164
|
-
platform: session.platform
|
|
23165
|
-
});
|
|
23166
|
-
attachToSession(session);
|
|
23167
|
-
} catch (err) {
|
|
23168
|
-
sendError("PTY_CREATE_FAILED", err instanceof Error ? err.message : String(err), { sessionId: msg.requestId });
|
|
23169
|
-
}
|
|
23170
|
-
break;
|
|
23171
|
-
case "attach": {
|
|
23172
|
-
const session = ptyManager.get(msg.sessionId);
|
|
23173
|
-
if (!session) {
|
|
23174
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23175
|
-
send({
|
|
23176
|
-
type: "exit",
|
|
23177
|
-
sessionId: msg.sessionId,
|
|
23178
|
-
exitCode: -1
|
|
23179
|
-
});
|
|
23180
|
-
break;
|
|
23181
|
-
}
|
|
23182
|
-
attachToSession(session, {
|
|
23183
|
-
cols: msg.cols,
|
|
23184
|
-
rows: msg.rows
|
|
23185
|
-
});
|
|
23186
|
-
const buffer = session.getBuffer();
|
|
23187
|
-
if (buffer) send({
|
|
23188
|
-
type: "buffer",
|
|
23189
|
-
sessionId: session.id,
|
|
23190
|
-
data: buffer
|
|
23191
|
-
});
|
|
23192
|
-
if (session.title) send({
|
|
23193
|
-
type: "title",
|
|
23194
|
-
sessionId: session.id,
|
|
23195
|
-
title: session.title
|
|
23196
|
-
});
|
|
23197
|
-
if (session.isExited) send({
|
|
23198
|
-
type: "exit",
|
|
23199
|
-
sessionId: session.id,
|
|
23200
|
-
exitCode: session.exitCode ?? -1
|
|
23201
|
-
});
|
|
23202
|
-
break;
|
|
23203
|
-
}
|
|
23204
|
-
case "list":
|
|
23205
|
-
send({
|
|
23206
|
-
type: "list",
|
|
23207
|
-
sessions: ptyManager.list().map((s) => ({
|
|
23208
|
-
id: s.id,
|
|
23209
|
-
title: s.title,
|
|
23210
|
-
command: s.command,
|
|
23211
|
-
args: s.args,
|
|
23212
|
-
platform: s.platform,
|
|
23213
|
-
isExited: s.isExited,
|
|
23214
|
-
exitCode: s.exitCode,
|
|
23215
|
-
closeTip: s.closeTip,
|
|
23216
|
-
closeCallbackUrl: s.closeCallbackUrl
|
|
23217
|
-
}))
|
|
23218
|
-
});
|
|
23219
|
-
break;
|
|
23220
|
-
case "input": {
|
|
23221
|
-
const session = ptyManager.get(msg.sessionId);
|
|
23222
|
-
if (!session) {
|
|
23223
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23224
|
-
break;
|
|
23225
|
-
}
|
|
23226
|
-
session.write(msg.data);
|
|
23227
|
-
break;
|
|
23228
|
-
}
|
|
23229
|
-
case "resize": {
|
|
23230
|
-
const session = ptyManager.get(msg.sessionId);
|
|
23231
|
-
if (!session) {
|
|
23232
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23233
|
-
break;
|
|
23234
|
-
}
|
|
23235
|
-
session.resize(msg.cols, msg.rows);
|
|
23236
|
-
break;
|
|
23237
|
-
}
|
|
23238
|
-
case "close": {
|
|
23239
|
-
const session = ptyManager.get(msg.sessionId);
|
|
23240
|
-
if (!session) {
|
|
23241
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23242
|
-
break;
|
|
23243
|
-
}
|
|
23244
|
-
cleanups.get(msg.sessionId)?.();
|
|
23245
|
-
ptyManager.close(session.id);
|
|
23246
|
-
break;
|
|
23247
|
-
}
|
|
23248
|
-
}
|
|
23249
|
-
});
|
|
23250
|
-
ws.on("close", () => {
|
|
23251
|
-
for (const cleanup of cleanups.values()) cleanup();
|
|
23252
|
-
cleanups.clear();
|
|
23253
|
-
});
|
|
22996
|
+
}
|
|
22997
|
+
}
|
|
22998
|
+
function parseShortStat(output) {
|
|
22999
|
+
const files = Number(/(\d+)\s+files? changed/.exec(output)?.[1] ?? 0);
|
|
23000
|
+
const insertions = Number(/(\d+)\s+insertions?\(\+\)/.exec(output)?.[1] ?? 0);
|
|
23001
|
+
const deletions = Number(/(\d+)\s+deletions?\(-\)/.exec(output)?.[1] ?? 0);
|
|
23002
|
+
return {
|
|
23003
|
+
files: Number.isFinite(files) ? files : 0,
|
|
23004
|
+
insertions: Number.isFinite(insertions) ? insertions : 0,
|
|
23005
|
+
deletions: Number.isFinite(deletions) ? deletions : 0
|
|
23254
23006
|
};
|
|
23255
23007
|
}
|
|
23256
|
-
|
|
23257
|
-
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
"
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
]);
|
|
23264
|
-
|
|
23265
|
-
|
|
23266
|
-
|
|
23267
|
-
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
|
|
23272
|
-
|
|
23273
|
-
|
|
23274
|
-
query: stringType(),
|
|
23275
|
-
limit: numberType().int().positive().optional()
|
|
23276
|
-
});
|
|
23277
|
-
const SearchHitSchema = objectType({
|
|
23278
|
-
documentId: stringType(),
|
|
23279
|
-
kind: SearchDocumentKindSchema,
|
|
23280
|
-
title: stringType(),
|
|
23281
|
-
href: stringType(),
|
|
23282
|
-
path: stringType(),
|
|
23283
|
-
score: numberType(),
|
|
23284
|
-
snippet: stringType(),
|
|
23285
|
-
updatedAt: numberType()
|
|
23286
|
-
});
|
|
23287
|
-
const SearchWorkerRequestSchema = discriminatedUnionType("type", [
|
|
23288
|
-
objectType({
|
|
23289
|
-
id: stringType(),
|
|
23290
|
-
type: literalType("init"),
|
|
23291
|
-
docs: arrayType(SearchDocumentSchema)
|
|
23292
|
-
}),
|
|
23293
|
-
objectType({
|
|
23294
|
-
id: stringType(),
|
|
23295
|
-
type: literalType("replaceAll"),
|
|
23296
|
-
docs: arrayType(SearchDocumentSchema)
|
|
23297
|
-
}),
|
|
23298
|
-
objectType({
|
|
23299
|
-
id: stringType(),
|
|
23300
|
-
type: literalType("search"),
|
|
23301
|
-
query: SearchQuerySchema
|
|
23302
|
-
}),
|
|
23303
|
-
objectType({
|
|
23304
|
-
id: stringType(),
|
|
23305
|
-
type: literalType("dispose")
|
|
23306
|
-
})
|
|
23307
|
-
]);
|
|
23308
|
-
const SearchWorkerResponseSchema = discriminatedUnionType("type", [
|
|
23309
|
-
objectType({
|
|
23310
|
-
id: stringType(),
|
|
23311
|
-
type: literalType("ok")
|
|
23312
|
-
}),
|
|
23313
|
-
objectType({
|
|
23314
|
-
id: stringType(),
|
|
23315
|
-
type: literalType("results"),
|
|
23316
|
-
hits: arrayType(SearchHitSchema)
|
|
23317
|
-
}),
|
|
23318
|
-
objectType({
|
|
23319
|
-
id: stringType(),
|
|
23320
|
-
type: literalType("error"),
|
|
23321
|
-
message: stringType()
|
|
23322
|
-
})
|
|
23323
|
-
]);
|
|
23324
|
-
|
|
23325
|
-
//#endregion
|
|
23326
|
-
//#region ../search/src/worker-source.ts
|
|
23327
|
-
const sharedRuntimeSource = String.raw`
|
|
23328
|
-
const DEFAULT_LIMIT = 50;
|
|
23329
|
-
const MAX_LIMIT = 200;
|
|
23330
|
-
const SNIPPET_SIZE = 180;
|
|
23331
|
-
|
|
23332
|
-
function normalizeText(input) {
|
|
23333
|
-
return String(input || '')
|
|
23334
|
-
.toLowerCase()
|
|
23335
|
-
.replace(/\s+/g, ' ')
|
|
23336
|
-
.trim();
|
|
23008
|
+
function parseNumStat(output) {
|
|
23009
|
+
let files = 0;
|
|
23010
|
+
let insertions = 0;
|
|
23011
|
+
let deletions = 0;
|
|
23012
|
+
for (const line of output.split("\n")) {
|
|
23013
|
+
const trimmed = line.trim();
|
|
23014
|
+
if (!trimmed) continue;
|
|
23015
|
+
const [addRaw, deleteRaw] = trimmed.split(" ");
|
|
23016
|
+
if (!addRaw || !deleteRaw) continue;
|
|
23017
|
+
files += 1;
|
|
23018
|
+
if (addRaw !== "-") insertions += Number(addRaw) || 0;
|
|
23019
|
+
if (deleteRaw !== "-") deletions += Number(deleteRaw) || 0;
|
|
23020
|
+
}
|
|
23021
|
+
return {
|
|
23022
|
+
files,
|
|
23023
|
+
insertions,
|
|
23024
|
+
deletions
|
|
23025
|
+
};
|
|
23337
23026
|
}
|
|
23338
|
-
|
|
23339
|
-
|
|
23340
|
-
return normalizeText(query)
|
|
23341
|
-
.split(' ')
|
|
23342
|
-
.map((term) => term.trim())
|
|
23343
|
-
.filter((term) => term.length > 0);
|
|
23027
|
+
function normalizeGitPath(path$1) {
|
|
23028
|
+
return path$1.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
23344
23029
|
}
|
|
23345
|
-
|
|
23346
|
-
|
|
23347
|
-
|
|
23348
|
-
|
|
23349
|
-
kind: doc.kind,
|
|
23350
|
-
title: doc.title,
|
|
23351
|
-
href: doc.href,
|
|
23352
|
-
path: doc.path,
|
|
23353
|
-
content: doc.content,
|
|
23354
|
-
updatedAt: doc.updatedAt,
|
|
23355
|
-
normalizedTitle: normalizeText(doc.title),
|
|
23356
|
-
normalizedPath: normalizeText(doc.path),
|
|
23357
|
-
normalizedContent: normalizeText(doc.content),
|
|
23358
|
-
};
|
|
23359
|
-
}
|
|
23360
|
-
|
|
23361
|
-
function buildSearchIndex(docs) {
|
|
23362
|
-
return {
|
|
23363
|
-
documents: Array.isArray(docs) ? docs.map(toSearchIndexDocument) : [],
|
|
23364
|
-
};
|
|
23030
|
+
function relativePath(fromDir, target) {
|
|
23031
|
+
const rel = relative$1(fromDir, target);
|
|
23032
|
+
if (!rel || rel.length === 0) return ".";
|
|
23033
|
+
return rel;
|
|
23365
23034
|
}
|
|
23366
|
-
|
|
23367
|
-
|
|
23368
|
-
|
|
23369
|
-
|
|
23035
|
+
function parseBranchName(branchRef, detached) {
|
|
23036
|
+
if (detached) return "(detached)";
|
|
23037
|
+
if (!branchRef) return "(unknown)";
|
|
23038
|
+
return branchRef.replace(/^refs\/heads\//, "");
|
|
23370
23039
|
}
|
|
23371
|
-
|
|
23372
|
-
|
|
23373
|
-
|
|
23374
|
-
|
|
23375
|
-
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23040
|
+
function parseWorktreeList(porcelain) {
|
|
23041
|
+
const entries = [];
|
|
23042
|
+
let current = null;
|
|
23043
|
+
const flush = () => {
|
|
23044
|
+
if (!current) return;
|
|
23045
|
+
entries.push(current);
|
|
23046
|
+
current = null;
|
|
23047
|
+
};
|
|
23048
|
+
for (const line of porcelain.split("\n")) {
|
|
23049
|
+
if (line.startsWith("worktree ")) {
|
|
23050
|
+
flush();
|
|
23051
|
+
current = {
|
|
23052
|
+
path: line.slice(9).trim(),
|
|
23053
|
+
branchRef: null,
|
|
23054
|
+
detached: false
|
|
23055
|
+
};
|
|
23056
|
+
continue;
|
|
23057
|
+
}
|
|
23058
|
+
if (!current) continue;
|
|
23059
|
+
if (line.startsWith("branch ")) {
|
|
23060
|
+
current.branchRef = line.slice(7).trim();
|
|
23061
|
+
continue;
|
|
23062
|
+
}
|
|
23063
|
+
if (line === "detached") {
|
|
23064
|
+
current.detached = true;
|
|
23065
|
+
continue;
|
|
23066
|
+
}
|
|
23067
|
+
}
|
|
23068
|
+
flush();
|
|
23069
|
+
return entries;
|
|
23379
23070
|
}
|
|
23380
|
-
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
|
|
23071
|
+
function parseRelatedChanges(paths) {
|
|
23072
|
+
const related = /* @__PURE__ */ new Set();
|
|
23073
|
+
for (const path$1 of paths) {
|
|
23074
|
+
const normalized = normalizeGitPath(path$1);
|
|
23075
|
+
const activeMatch = /^openspec\/changes\/([^/]+)\//.exec(normalized);
|
|
23076
|
+
if (activeMatch?.[1]) {
|
|
23077
|
+
related.add(activeMatch[1]);
|
|
23078
|
+
continue;
|
|
23079
|
+
}
|
|
23080
|
+
const archiveMatch = /^openspec\/changes\/archive\/([^/]+)\//.exec(normalized);
|
|
23081
|
+
if (archiveMatch?.[1]) {
|
|
23082
|
+
const fullName = archiveMatch[1];
|
|
23083
|
+
related.add(fullName.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
|
|
23084
|
+
}
|
|
23085
|
+
}
|
|
23086
|
+
return [...related].sort((a, b) => a.localeCompare(b));
|
|
23396
23087
|
}
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
23403
|
-
|
|
23404
|
-
|
|
23405
|
-
|
|
23406
|
-
|
|
23407
|
-
|
|
23408
|
-
|
|
23409
|
-
|
|
23410
|
-
|
|
23411
|
-
|
|
23412
|
-
|
|
23413
|
-
|
|
23414
|
-
}
|
|
23415
|
-
|
|
23416
|
-
const start = Math.max(0, matchIndex - Math.floor(SNIPPET_SIZE / 3));
|
|
23417
|
-
const end = Math.min(source.length, start + SNIPPET_SIZE);
|
|
23418
|
-
|
|
23419
|
-
const prefix = start > 0 ? '...' : '';
|
|
23420
|
-
const suffix = end < source.length ? '...' : '';
|
|
23421
|
-
return prefix + source.slice(start, end) + suffix;
|
|
23088
|
+
async function resolveDefaultBranch(projectDir, runGit) {
|
|
23089
|
+
const remoteHead = await runGit(projectDir, [
|
|
23090
|
+
"symbolic-ref",
|
|
23091
|
+
"--quiet",
|
|
23092
|
+
"--short",
|
|
23093
|
+
"refs/remotes/origin/HEAD"
|
|
23094
|
+
]);
|
|
23095
|
+
const remoteRef = remoteHead.stdout.trim();
|
|
23096
|
+
if (remoteHead.ok && remoteRef) return remoteRef;
|
|
23097
|
+
const localHead = await runGit(projectDir, [
|
|
23098
|
+
"rev-parse",
|
|
23099
|
+
"--abbrev-ref",
|
|
23100
|
+
"HEAD"
|
|
23101
|
+
]);
|
|
23102
|
+
const localRef = localHead.stdout.trim();
|
|
23103
|
+
if (localHead.ok && localRef && localRef !== "HEAD") return localRef;
|
|
23104
|
+
return "main";
|
|
23422
23105
|
}
|
|
23423
|
-
|
|
23424
|
-
|
|
23425
|
-
|
|
23426
|
-
|
|
23427
|
-
|
|
23428
|
-
|
|
23429
|
-
|
|
23430
|
-
|
|
23431
|
-
|
|
23432
|
-
|
|
23433
|
-
|
|
23434
|
-
|
|
23435
|
-
|
|
23436
|
-
|
|
23437
|
-
|
|
23438
|
-
|
|
23439
|
-
|
|
23440
|
-
|
|
23441
|
-
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
|
|
23106
|
+
async function collectCommitEntries(options) {
|
|
23107
|
+
const { worktreePath, defaultBranch, maxCommitEntries, runGit } = options;
|
|
23108
|
+
const entries = [];
|
|
23109
|
+
const commits = await runGit(worktreePath, [
|
|
23110
|
+
"log",
|
|
23111
|
+
"--format=%H%x1f%s",
|
|
23112
|
+
`-n${maxCommitEntries}`,
|
|
23113
|
+
`${defaultBranch}..HEAD`
|
|
23114
|
+
]);
|
|
23115
|
+
if (commits.ok) for (const line of commits.stdout.split("\n")) {
|
|
23116
|
+
if (!line.trim()) continue;
|
|
23117
|
+
const [hash, title = ""] = line.split("");
|
|
23118
|
+
if (!hash) continue;
|
|
23119
|
+
const diffResult = await runGit(worktreePath, [
|
|
23120
|
+
"show",
|
|
23121
|
+
"--numstat",
|
|
23122
|
+
"--format=",
|
|
23123
|
+
hash
|
|
23124
|
+
]);
|
|
23125
|
+
const changedFiles = (await runGit(worktreePath, [
|
|
23126
|
+
"show",
|
|
23127
|
+
"--name-only",
|
|
23128
|
+
"--format=",
|
|
23129
|
+
hash
|
|
23130
|
+
])).stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23131
|
+
entries.push({
|
|
23132
|
+
type: "commit",
|
|
23133
|
+
hash,
|
|
23134
|
+
title: title.trim() || hash.slice(0, 7),
|
|
23135
|
+
relatedChanges: parseRelatedChanges(changedFiles),
|
|
23136
|
+
diff: diffResult.ok ? parseNumStat(diffResult.stdout) : EMPTY_DIFF
|
|
23137
|
+
});
|
|
23138
|
+
}
|
|
23139
|
+
const trackedResult = await runGit(worktreePath, [
|
|
23140
|
+
"diff",
|
|
23141
|
+
"--numstat",
|
|
23142
|
+
"HEAD"
|
|
23143
|
+
]);
|
|
23144
|
+
const trackedFilesResult = await runGit(worktreePath, [
|
|
23145
|
+
"diff",
|
|
23146
|
+
"--name-only",
|
|
23147
|
+
"HEAD"
|
|
23148
|
+
]);
|
|
23149
|
+
const untrackedResult = await runGit(worktreePath, [
|
|
23150
|
+
"ls-files",
|
|
23151
|
+
"--others",
|
|
23152
|
+
"--exclude-standard"
|
|
23153
|
+
]);
|
|
23154
|
+
const trackedFiles = trackedFilesResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23155
|
+
const untrackedFiles = untrackedResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23156
|
+
const allUncommittedFiles = new Set([...trackedFiles, ...untrackedFiles]);
|
|
23157
|
+
const trackedDiff = trackedResult.ok ? parseNumStat(trackedResult.stdout) : EMPTY_DIFF;
|
|
23158
|
+
entries.push({
|
|
23159
|
+
type: "uncommitted",
|
|
23160
|
+
title: "Uncommitted",
|
|
23161
|
+
relatedChanges: parseRelatedChanges([...allUncommittedFiles]),
|
|
23162
|
+
diff: {
|
|
23163
|
+
files: allUncommittedFiles.size,
|
|
23164
|
+
insertions: trackedDiff.insertions,
|
|
23165
|
+
deletions: trackedDiff.deletions
|
|
23166
|
+
}
|
|
23167
|
+
});
|
|
23168
|
+
return entries;
|
|
23169
|
+
}
|
|
23170
|
+
async function collectWorktree(options) {
|
|
23171
|
+
const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries } = options;
|
|
23172
|
+
const worktreePath = resolve$1(worktree.path);
|
|
23173
|
+
const resolvedProjectDir = resolve$1(projectDir);
|
|
23174
|
+
const aheadBehindResult = await runGit(worktreePath, [
|
|
23175
|
+
"rev-list",
|
|
23176
|
+
"--left-right",
|
|
23177
|
+
"--count",
|
|
23178
|
+
`${defaultBranch}...HEAD`
|
|
23179
|
+
]);
|
|
23180
|
+
let ahead = 0;
|
|
23181
|
+
let behind = 0;
|
|
23182
|
+
if (aheadBehindResult.ok) {
|
|
23183
|
+
const [behindRaw, aheadRaw] = aheadBehindResult.stdout.trim().split(/\s+/);
|
|
23184
|
+
ahead = Number(aheadRaw) || 0;
|
|
23185
|
+
behind = Number(behindRaw) || 0;
|
|
23186
|
+
}
|
|
23187
|
+
const diffResult = await runGit(worktreePath, [
|
|
23188
|
+
"diff",
|
|
23189
|
+
"--shortstat",
|
|
23190
|
+
`${defaultBranch}...HEAD`
|
|
23191
|
+
]);
|
|
23192
|
+
const diff = diffResult.ok ? parseShortStat(diffResult.stdout) : EMPTY_DIFF;
|
|
23193
|
+
const entries = await collectCommitEntries({
|
|
23194
|
+
worktreePath,
|
|
23195
|
+
defaultBranch,
|
|
23196
|
+
maxCommitEntries,
|
|
23197
|
+
runGit
|
|
23198
|
+
});
|
|
23199
|
+
return {
|
|
23200
|
+
path: worktreePath,
|
|
23201
|
+
relativePath: relativePath(resolvedProjectDir, worktreePath),
|
|
23202
|
+
branchName: parseBranchName(worktree.branchRef, worktree.detached),
|
|
23203
|
+
detached: worktree.detached,
|
|
23204
|
+
isCurrent: resolvedProjectDir === worktreePath,
|
|
23205
|
+
ahead,
|
|
23206
|
+
behind,
|
|
23207
|
+
diff,
|
|
23208
|
+
entries
|
|
23209
|
+
};
|
|
23210
|
+
}
|
|
23211
|
+
async function removeDetachedDashboardGitWorktree(options) {
|
|
23212
|
+
const runGit = options.runGit ?? defaultRunGit;
|
|
23213
|
+
const resolvedProjectDir = resolve$1(options.projectDir);
|
|
23214
|
+
const resolvedTargetPath = resolve$1(options.targetPath);
|
|
23215
|
+
if (resolvedTargetPath === resolvedProjectDir) throw new Error("Cannot remove the current worktree.");
|
|
23216
|
+
const worktreeResult = await runGit(resolvedProjectDir, [
|
|
23217
|
+
"worktree",
|
|
23218
|
+
"list",
|
|
23219
|
+
"--porcelain"
|
|
23220
|
+
]);
|
|
23221
|
+
if (!worktreeResult.ok) throw new Error("Failed to inspect git worktrees.");
|
|
23222
|
+
const matched = parseWorktreeList(worktreeResult.stdout).find((worktree) => resolve$1(worktree.path) === resolvedTargetPath);
|
|
23223
|
+
if (!matched) throw new Error("Worktree not found.");
|
|
23224
|
+
if (!matched.detached) throw new Error("Only detached worktrees can be removed from Dashboard.");
|
|
23225
|
+
if (!(await runGit(resolvedProjectDir, [
|
|
23226
|
+
"worktree",
|
|
23227
|
+
"remove",
|
|
23228
|
+
"--force",
|
|
23229
|
+
resolvedTargetPath
|
|
23230
|
+
])).ok) throw new Error("Failed to remove detached worktree.");
|
|
23231
|
+
}
|
|
23232
|
+
async function buildDashboardGitSnapshot(options) {
|
|
23233
|
+
const runGit = options.runGit ?? defaultRunGit;
|
|
23234
|
+
const maxCommitEntries = options.maxCommitEntries ?? 8;
|
|
23235
|
+
const resolvedProjectDir = resolve$1(options.projectDir);
|
|
23236
|
+
const defaultBranch = await resolveDefaultBranch(resolvedProjectDir, runGit);
|
|
23237
|
+
const worktreeResult = await runGit(resolvedProjectDir, [
|
|
23238
|
+
"worktree",
|
|
23239
|
+
"list",
|
|
23240
|
+
"--porcelain"
|
|
23241
|
+
]);
|
|
23242
|
+
const parsed = worktreeResult.ok ? parseWorktreeList(worktreeResult.stdout) : [];
|
|
23243
|
+
const baseWorktrees = parsed.length > 0 ? parsed : [{
|
|
23244
|
+
path: resolvedProjectDir,
|
|
23245
|
+
branchRef: null,
|
|
23246
|
+
detached: false
|
|
23247
|
+
}];
|
|
23248
|
+
const worktrees = await Promise.all(baseWorktrees.map((worktree) => collectWorktree({
|
|
23249
|
+
projectDir: resolvedProjectDir,
|
|
23250
|
+
worktree,
|
|
23251
|
+
defaultBranch,
|
|
23252
|
+
runGit,
|
|
23253
|
+
maxCommitEntries
|
|
23254
|
+
})));
|
|
23255
|
+
worktrees.sort((a, b) => {
|
|
23256
|
+
if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1;
|
|
23257
|
+
return a.branchName.localeCompare(b.branchName);
|
|
23258
|
+
});
|
|
23259
|
+
return {
|
|
23260
|
+
defaultBranch,
|
|
23261
|
+
worktrees
|
|
23262
|
+
};
|
|
23451
23263
|
}
|
|
23452
23264
|
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23458
|
-
|
|
23459
|
-
|
|
23460
|
-
|
|
23461
|
-
|
|
23462
|
-
|
|
23463
|
-
return { id: payload.id, type: 'ok' };
|
|
23464
|
-
}
|
|
23465
|
-
|
|
23466
|
-
if (payload.type === 'search') {
|
|
23467
|
-
const hits = searchIndex(index, payload.query || { query: '' });
|
|
23468
|
-
return { id: payload.id, type: 'results', hits };
|
|
23469
|
-
}
|
|
23470
|
-
|
|
23471
|
-
if (payload.type === 'dispose') {
|
|
23472
|
-
index = buildSearchIndex([]);
|
|
23473
|
-
return { id: payload.id, type: 'ok' };
|
|
23474
|
-
}
|
|
23475
|
-
|
|
23476
|
-
throw new Error('Unsupported worker request type');
|
|
23477
|
-
} catch (error) {
|
|
23478
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
23479
|
-
return { id: payload?.id ?? 'unknown', type: 'error', message };
|
|
23480
|
-
}
|
|
23265
|
+
//#endregion
|
|
23266
|
+
//#region ../server/src/dashboard-time-trends.ts
|
|
23267
|
+
const MIN_TREND_POINT_LIMIT = 20;
|
|
23268
|
+
const MAX_TREND_POINT_LIMIT = 500;
|
|
23269
|
+
const DEFAULT_TREND_POINT_LIMIT = 100;
|
|
23270
|
+
const TARGET_TREND_BARS = 20;
|
|
23271
|
+
const DAY_MS = 1440 * 60 * 1e3;
|
|
23272
|
+
function clampPointLimit(pointLimit) {
|
|
23273
|
+
if (!Number.isFinite(pointLimit)) return DEFAULT_TREND_POINT_LIMIT;
|
|
23274
|
+
return Math.max(MIN_TREND_POINT_LIMIT, Math.min(MAX_TREND_POINT_LIMIT, Math.trunc(pointLimit)));
|
|
23481
23275
|
}
|
|
23482
|
-
|
|
23483
|
-
|
|
23484
|
-
|
|
23276
|
+
function createEmptyTrendSeries() {
|
|
23277
|
+
return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
|
|
23278
|
+
}
|
|
23279
|
+
function normalizeEvents(events, pointLimit) {
|
|
23280
|
+
return events.filter((event) => Number.isFinite(event.ts) && event.ts > 0 && Number.isFinite(event.value)).sort((a, b) => a.ts - b.ts).slice(-pointLimit);
|
|
23281
|
+
}
|
|
23282
|
+
function buildTimeWindow(options) {
|
|
23283
|
+
const { probeEvents, targetBars, rightEdgeTs } = options;
|
|
23284
|
+
if (probeEvents.length === 0) return null;
|
|
23285
|
+
const probeEnd = probeEvents[probeEvents.length - 1].ts;
|
|
23286
|
+
const end = typeof rightEdgeTs === "number" && Number.isFinite(rightEdgeTs) && rightEdgeTs > 0 ? Math.max(probeEnd, rightEdgeTs) : probeEnd;
|
|
23287
|
+
const probeStart = probeEvents[0].ts;
|
|
23288
|
+
const rangeMs = Math.max(1, end - probeStart);
|
|
23289
|
+
const bucketMs = rangeMs >= DAY_MS ? Math.max(DAY_MS, Math.ceil(rangeMs / targetBars / DAY_MS) * DAY_MS) : Math.max(1, Math.ceil(rangeMs / targetBars));
|
|
23290
|
+
const windowStart = end - bucketMs * targetBars;
|
|
23291
|
+
return {
|
|
23292
|
+
windowStart,
|
|
23293
|
+
bucketMs,
|
|
23294
|
+
bucketEnds: Array.from({ length: targetBars }, (_, index) => windowStart + bucketMs * (index + 1))
|
|
23295
|
+
};
|
|
23296
|
+
}
|
|
23297
|
+
function bucketizeTrend(events, reducer, rightEdgeTs) {
|
|
23298
|
+
if (events.length === 0) return [];
|
|
23299
|
+
const timeWindow = buildTimeWindow({
|
|
23300
|
+
probeEvents: events,
|
|
23301
|
+
targetBars: TARGET_TREND_BARS,
|
|
23302
|
+
rightEdgeTs
|
|
23303
|
+
});
|
|
23304
|
+
if (!timeWindow) return [];
|
|
23305
|
+
const { windowStart, bucketMs, bucketEnds } = timeWindow;
|
|
23306
|
+
const sums = Array.from({ length: bucketEnds.length }, () => 0);
|
|
23307
|
+
const counts = Array.from({ length: bucketEnds.length }, () => 0);
|
|
23308
|
+
let baseline = 0;
|
|
23309
|
+
for (const event of events) {
|
|
23310
|
+
if (event.ts <= windowStart) {
|
|
23311
|
+
if (reducer === "sum-cumulative") baseline += event.value;
|
|
23312
|
+
continue;
|
|
23313
|
+
}
|
|
23314
|
+
const offset = event.ts - windowStart;
|
|
23315
|
+
const index = Math.max(0, Math.min(bucketEnds.length - 1, Math.ceil(offset / bucketMs) - 1));
|
|
23316
|
+
sums[index] += event.value;
|
|
23317
|
+
counts[index] += 1;
|
|
23318
|
+
}
|
|
23319
|
+
let cumulative = baseline;
|
|
23320
|
+
let carry = baseline !== 0 ? baseline : events[0].value;
|
|
23321
|
+
return bucketEnds.map((ts, index) => {
|
|
23322
|
+
if (reducer === "sum") return {
|
|
23323
|
+
ts,
|
|
23324
|
+
value: sums[index]
|
|
23325
|
+
};
|
|
23326
|
+
if (reducer === "sum-cumulative") {
|
|
23327
|
+
cumulative += sums[index];
|
|
23328
|
+
return {
|
|
23329
|
+
ts,
|
|
23330
|
+
value: cumulative
|
|
23331
|
+
};
|
|
23332
|
+
}
|
|
23333
|
+
if (counts[index] > 0) carry = sums[index] / counts[index];
|
|
23334
|
+
return {
|
|
23335
|
+
ts,
|
|
23336
|
+
value: carry
|
|
23337
|
+
};
|
|
23338
|
+
});
|
|
23339
|
+
}
|
|
23340
|
+
function buildDashboardTimeTrends(options) {
|
|
23341
|
+
const pointLimit = clampPointLimit(options.pointLimit);
|
|
23342
|
+
const trends = createEmptyTrendSeries();
|
|
23343
|
+
for (const metric of DASHBOARD_METRIC_KEYS) {
|
|
23344
|
+
if (options.availability[metric].state !== "ok") continue;
|
|
23345
|
+
trends[metric] = bucketizeTrend(normalizeEvents(options.events[metric], pointLimit), options.reducers?.[metric] ?? "sum", options.rightEdgeTs);
|
|
23346
|
+
}
|
|
23347
|
+
return {
|
|
23348
|
+
trends,
|
|
23349
|
+
trendMeta: {
|
|
23350
|
+
pointLimit,
|
|
23351
|
+
lastUpdatedAt: options.timestamp
|
|
23352
|
+
}
|
|
23353
|
+
};
|
|
23485
23354
|
}
|
|
23486
23355
|
|
|
23487
23356
|
//#endregion
|
|
23488
|
-
//#region ../server/src/
|
|
23489
|
-
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
|
|
23493
|
-
|
|
23494
|
-
|
|
23495
|
-
|
|
23496
|
-
|
|
23497
|
-
|
|
23498
|
-
|
|
23499
|
-
|
|
23500
|
-
|
|
23501
|
-
|
|
23502
|
-
|
|
23503
|
-
|
|
23504
|
-
|
|
23505
|
-
|
|
23506
|
-
|
|
23507
|
-
|
|
23508
|
-
|
|
23509
|
-
|
|
23510
|
-
|
|
23511
|
-
|
|
23512
|
-
|
|
23513
|
-
|
|
23514
|
-
|
|
23357
|
+
//#region ../server/src/dashboard-overview.ts
|
|
23358
|
+
const execFileAsync = promisify$1(execFile);
|
|
23359
|
+
const DASHBOARD_GIT_REFRESH_STAMP_NAME = "openspecui-dashboard-git-refresh.stamp";
|
|
23360
|
+
const dashboardGitTaskStatusEmitter = new EventEmitter$1();
|
|
23361
|
+
dashboardGitTaskStatusEmitter.setMaxListeners(200);
|
|
23362
|
+
const dashboardGitTaskStatus = {
|
|
23363
|
+
running: false,
|
|
23364
|
+
inFlight: 0,
|
|
23365
|
+
lastStartedAt: null,
|
|
23366
|
+
lastFinishedAt: null,
|
|
23367
|
+
lastReason: null,
|
|
23368
|
+
lastError: null
|
|
23369
|
+
};
|
|
23370
|
+
function createEmptyTriColorTrends() {
|
|
23371
|
+
return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
|
|
23372
|
+
}
|
|
23373
|
+
function resolveTrendTimestamp(primary, secondary) {
|
|
23374
|
+
if (typeof primary === "number" && Number.isFinite(primary) && primary > 0) return primary;
|
|
23375
|
+
if (typeof secondary === "number" && Number.isFinite(secondary) && secondary > 0) return secondary;
|
|
23376
|
+
return null;
|
|
23377
|
+
}
|
|
23378
|
+
function parseDatedIdTimestamp(id) {
|
|
23379
|
+
const match$1 = /^(\d{4})-(\d{2})-(\d{2})(?:-|$)/.exec(id);
|
|
23380
|
+
if (!match$1) return null;
|
|
23381
|
+
const year = Number(match$1[1]);
|
|
23382
|
+
const month = Number(match$1[2]);
|
|
23383
|
+
const day = Number(match$1[3]);
|
|
23384
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return null;
|
|
23385
|
+
if (month < 1 || month > 12) return null;
|
|
23386
|
+
if (day < 1 || day > 31) return null;
|
|
23387
|
+
const ts = Date.UTC(year, month - 1, day);
|
|
23388
|
+
return Number.isFinite(ts) ? ts : null;
|
|
23389
|
+
}
|
|
23390
|
+
async function readLatestCommitTimestamp(projectDir) {
|
|
23391
|
+
try {
|
|
23392
|
+
const { stdout } = await execFileAsync("git", [
|
|
23393
|
+
"log",
|
|
23394
|
+
"-1",
|
|
23395
|
+
"--format=%ct"
|
|
23396
|
+
], {
|
|
23397
|
+
cwd: projectDir,
|
|
23398
|
+
maxBuffer: 1024 * 1024,
|
|
23399
|
+
encoding: "utf8"
|
|
23400
|
+
});
|
|
23401
|
+
const seconds = Number(stdout.trim());
|
|
23402
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : null;
|
|
23403
|
+
} catch {
|
|
23404
|
+
return null;
|
|
23405
|
+
}
|
|
23406
|
+
}
|
|
23407
|
+
function emitDashboardGitTaskStatus() {
|
|
23408
|
+
dashboardGitTaskStatusEmitter.emit("change", getDashboardGitTaskStatus());
|
|
23409
|
+
}
|
|
23410
|
+
function beginDashboardGitTask(reason) {
|
|
23411
|
+
dashboardGitTaskStatus.inFlight += 1;
|
|
23412
|
+
dashboardGitTaskStatus.running = true;
|
|
23413
|
+
dashboardGitTaskStatus.lastStartedAt = Date.now();
|
|
23414
|
+
dashboardGitTaskStatus.lastReason = reason;
|
|
23415
|
+
dashboardGitTaskStatus.lastError = null;
|
|
23416
|
+
emitDashboardGitTaskStatus();
|
|
23417
|
+
}
|
|
23418
|
+
function endDashboardGitTask(error) {
|
|
23419
|
+
dashboardGitTaskStatus.inFlight = Math.max(0, dashboardGitTaskStatus.inFlight - 1);
|
|
23420
|
+
dashboardGitTaskStatus.running = dashboardGitTaskStatus.inFlight > 0;
|
|
23421
|
+
dashboardGitTaskStatus.lastFinishedAt = Date.now();
|
|
23422
|
+
if (error) dashboardGitTaskStatus.lastError = error instanceof Error ? error.message : String(error);
|
|
23423
|
+
emitDashboardGitTaskStatus();
|
|
23424
|
+
}
|
|
23425
|
+
function getDashboardGitTaskStatus() {
|
|
23426
|
+
return { ...dashboardGitTaskStatus };
|
|
23427
|
+
}
|
|
23428
|
+
function subscribeDashboardGitTaskStatus(listener) {
|
|
23429
|
+
dashboardGitTaskStatusEmitter.on("change", listener);
|
|
23430
|
+
return () => {
|
|
23431
|
+
dashboardGitTaskStatusEmitter.off("change", listener);
|
|
23432
|
+
};
|
|
23433
|
+
}
|
|
23434
|
+
async function resolveGitMetadataDir(projectDir) {
|
|
23435
|
+
try {
|
|
23436
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-dir"], {
|
|
23437
|
+
cwd: projectDir,
|
|
23438
|
+
maxBuffer: 1024 * 1024,
|
|
23439
|
+
encoding: "utf8"
|
|
23440
|
+
});
|
|
23441
|
+
const gitDirRaw = stdout.trim();
|
|
23442
|
+
if (!gitDirRaw) return null;
|
|
23443
|
+
const gitDirPath = resolve$1(projectDir, gitDirRaw);
|
|
23444
|
+
if (!(await stat(gitDirPath)).isDirectory()) return null;
|
|
23445
|
+
return gitDirPath;
|
|
23446
|
+
} catch {
|
|
23447
|
+
return null;
|
|
23448
|
+
}
|
|
23449
|
+
}
|
|
23450
|
+
function getDashboardGitRefreshStampPath(gitMetadataDir) {
|
|
23451
|
+
return join$1(gitMetadataDir, DASHBOARD_GIT_REFRESH_STAMP_NAME);
|
|
23452
|
+
}
|
|
23453
|
+
async function touchDashboardGitRefreshStamp(projectDir, reason) {
|
|
23454
|
+
const gitMetadataDir = await resolveGitMetadataDir(projectDir);
|
|
23455
|
+
if (!gitMetadataDir) return { skipped: true };
|
|
23456
|
+
const stampPath = getDashboardGitRefreshStampPath(gitMetadataDir);
|
|
23457
|
+
await mkdir$1(dirname$1(stampPath), { recursive: true });
|
|
23458
|
+
await writeFile$1(stampPath, `${Date.now()} ${reason}\n`, "utf8");
|
|
23459
|
+
return { skipped: false };
|
|
23460
|
+
}
|
|
23461
|
+
async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
23462
|
+
const now = Date.now();
|
|
23463
|
+
const [specMetas, changeMetas, archiveMetas] = await Promise.all([
|
|
23464
|
+
ctx.adapter.listSpecsWithMeta(),
|
|
23465
|
+
ctx.adapter.listChangesWithMeta(),
|
|
23466
|
+
ctx.adapter.listArchivedChangesWithMeta()
|
|
23467
|
+
]);
|
|
23468
|
+
const activeChanges = changeMetas.map((changeMeta) => ({
|
|
23469
|
+
id: changeMeta.id,
|
|
23470
|
+
name: changeMeta.name ?? changeMeta.id,
|
|
23471
|
+
progress: changeMeta.progress,
|
|
23472
|
+
updatedAt: changeMeta.updatedAt
|
|
23473
|
+
})).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
23474
|
+
const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
|
|
23475
|
+
const change = await ctx.adapter.readArchivedChange(meta.id);
|
|
23476
|
+
if (!change) return null;
|
|
23477
|
+
return {
|
|
23478
|
+
id: meta.id,
|
|
23479
|
+
createdAt: meta.createdAt,
|
|
23480
|
+
updatedAt: meta.updatedAt,
|
|
23481
|
+
tasksCompleted: change.tasks.filter((task) => task.completed).length
|
|
23482
|
+
};
|
|
23483
|
+
}))).filter((item) => item !== null);
|
|
23484
|
+
const specifications = (await Promise.all(specMetas.map(async (meta) => {
|
|
23485
|
+
const spec = await ctx.adapter.readSpec(meta.id);
|
|
23486
|
+
if (!spec) return null;
|
|
23487
|
+
return {
|
|
23488
|
+
id: meta.id,
|
|
23489
|
+
name: meta.name,
|
|
23490
|
+
requirements: spec.requirements.length,
|
|
23491
|
+
updatedAt: meta.updatedAt
|
|
23492
|
+
};
|
|
23493
|
+
}))).filter((item) => item !== null).sort((a, b) => b.requirements - a.requirements || b.updatedAt - a.updatedAt);
|
|
23494
|
+
const requirements = specifications.reduce((sum, spec) => sum + spec.requirements, 0);
|
|
23495
|
+
const tasksTotal = activeChanges.reduce((sum, change) => sum + change.progress.total, 0);
|
|
23496
|
+
const tasksCompleted = activeChanges.reduce((sum, change) => sum + change.progress.completed, 0);
|
|
23497
|
+
const archivedTasksCompleted = archivedChanges.reduce((sum, change) => sum + change.tasksCompleted, 0);
|
|
23498
|
+
const taskCompletionPercent = tasksTotal > 0 ? Math.round(tasksCompleted / tasksTotal * 100) : null;
|
|
23499
|
+
const inProgressChanges = activeChanges.filter((change) => change.progress.total > 0 && change.progress.completed < change.progress.total).length;
|
|
23500
|
+
const specificationTrendEvents = specMetas.flatMap((spec) => {
|
|
23501
|
+
const ts = resolveTrendTimestamp(spec.createdAt, spec.updatedAt);
|
|
23502
|
+
return ts === null ? [] : [{
|
|
23503
|
+
ts,
|
|
23504
|
+
value: 1
|
|
23505
|
+
}];
|
|
23506
|
+
});
|
|
23507
|
+
const completedTrendEvents = archivedChanges.flatMap((archive) => {
|
|
23508
|
+
const ts = parseDatedIdTimestamp(archive.id) ?? resolveTrendTimestamp(archive.updatedAt, archive.createdAt);
|
|
23509
|
+
return ts === null ? [] : [{
|
|
23510
|
+
ts,
|
|
23511
|
+
value: archive.tasksCompleted
|
|
23512
|
+
}];
|
|
23513
|
+
});
|
|
23514
|
+
const specMetaById = new Map(specMetas.map((meta) => [meta.id, meta]));
|
|
23515
|
+
const requirementTrendEvents = specifications.flatMap((spec) => {
|
|
23516
|
+
const meta = specMetaById.get(spec.id);
|
|
23517
|
+
const ts = resolveTrendTimestamp(meta?.updatedAt, meta?.createdAt);
|
|
23518
|
+
return ts === null ? [] : [{
|
|
23519
|
+
ts,
|
|
23520
|
+
value: spec.requirements
|
|
23521
|
+
}];
|
|
23522
|
+
});
|
|
23523
|
+
const hasObjectiveSpecificationTrend = specificationTrendEvents.length > 0 || specifications.length === 0;
|
|
23524
|
+
const hasObjectiveRequirementTrend = requirementTrendEvents.length > 0 || requirements === 0;
|
|
23525
|
+
const hasObjectiveCompletedTrend = completedTrendEvents.length > 0 || archiveMetas.length === 0;
|
|
23526
|
+
const config = await ctx.configManager.readConfig();
|
|
23527
|
+
beginDashboardGitTask(reason);
|
|
23528
|
+
let latestCommitTs = null;
|
|
23529
|
+
let git;
|
|
23530
|
+
try {
|
|
23531
|
+
const gitSnapshotPromise = buildDashboardGitSnapshot({ projectDir: ctx.projectDir }).catch(() => ({
|
|
23532
|
+
defaultBranch: "main",
|
|
23533
|
+
worktrees: []
|
|
23534
|
+
}));
|
|
23535
|
+
latestCommitTs = await readLatestCommitTimestamp(ctx.projectDir);
|
|
23536
|
+
git = await gitSnapshotPromise;
|
|
23537
|
+
} catch (error) {
|
|
23538
|
+
endDashboardGitTask(error);
|
|
23539
|
+
throw error;
|
|
23540
|
+
}
|
|
23541
|
+
endDashboardGitTask(null);
|
|
23542
|
+
const cardAvailability = {
|
|
23543
|
+
specifications: hasObjectiveSpecificationTrend ? { state: "ok" } : {
|
|
23544
|
+
state: "invalid",
|
|
23545
|
+
reason: "objective-history-unavailable"
|
|
23546
|
+
},
|
|
23547
|
+
requirements: hasObjectiveRequirementTrend ? { state: "ok" } : {
|
|
23548
|
+
state: "invalid",
|
|
23549
|
+
reason: "objective-history-unavailable"
|
|
23550
|
+
},
|
|
23551
|
+
activeChanges: {
|
|
23552
|
+
state: "invalid",
|
|
23553
|
+
reason: "objective-history-unavailable"
|
|
23554
|
+
},
|
|
23555
|
+
inProgressChanges: {
|
|
23556
|
+
state: "invalid",
|
|
23557
|
+
reason: "objective-history-unavailable"
|
|
23558
|
+
},
|
|
23559
|
+
completedChanges: hasObjectiveCompletedTrend ? { state: "ok" } : {
|
|
23560
|
+
state: "invalid",
|
|
23561
|
+
reason: "objective-history-unavailable"
|
|
23562
|
+
},
|
|
23563
|
+
taskCompletionPercent: {
|
|
23564
|
+
state: "invalid",
|
|
23565
|
+
reason: taskCompletionPercent === null ? "semantic-uncomputable" : "objective-history-unavailable"
|
|
23566
|
+
}
|
|
23567
|
+
};
|
|
23568
|
+
const trendKinds = {
|
|
23569
|
+
specifications: "monotonic",
|
|
23570
|
+
requirements: "monotonic",
|
|
23571
|
+
activeChanges: "bidirectional",
|
|
23572
|
+
inProgressChanges: "bidirectional",
|
|
23573
|
+
completedChanges: "monotonic",
|
|
23574
|
+
taskCompletionPercent: "bidirectional"
|
|
23575
|
+
};
|
|
23576
|
+
const { trends: baselineTrends, trendMeta } = buildDashboardTimeTrends({
|
|
23577
|
+
pointLimit: config.dashboard.trendPointLimit,
|
|
23578
|
+
timestamp: now,
|
|
23579
|
+
rightEdgeTs: latestCommitTs,
|
|
23580
|
+
availability: cardAvailability,
|
|
23581
|
+
events: {
|
|
23582
|
+
specifications: specificationTrendEvents,
|
|
23583
|
+
requirements: requirementTrendEvents,
|
|
23584
|
+
activeChanges: [],
|
|
23585
|
+
inProgressChanges: [],
|
|
23586
|
+
completedChanges: completedTrendEvents,
|
|
23587
|
+
taskCompletionPercent: []
|
|
23588
|
+
},
|
|
23589
|
+
reducers: {
|
|
23590
|
+
specifications: "sum",
|
|
23591
|
+
requirements: "sum",
|
|
23592
|
+
completedChanges: "sum"
|
|
23593
|
+
}
|
|
23594
|
+
});
|
|
23595
|
+
return {
|
|
23596
|
+
summary: {
|
|
23597
|
+
specifications: specifications.length,
|
|
23598
|
+
requirements,
|
|
23599
|
+
activeChanges: activeChanges.length,
|
|
23600
|
+
inProgressChanges,
|
|
23601
|
+
completedChanges: archiveMetas.length,
|
|
23602
|
+
archivedTasksCompleted,
|
|
23603
|
+
tasksTotal,
|
|
23604
|
+
tasksCompleted,
|
|
23605
|
+
taskCompletionPercent
|
|
23606
|
+
},
|
|
23607
|
+
trends: baselineTrends,
|
|
23608
|
+
triColorTrends: createEmptyTriColorTrends(),
|
|
23609
|
+
trendKinds,
|
|
23610
|
+
cardAvailability,
|
|
23611
|
+
trendMeta,
|
|
23612
|
+
specifications,
|
|
23613
|
+
activeChanges,
|
|
23614
|
+
git
|
|
23615
|
+
};
|
|
23616
|
+
}
|
|
23617
|
+
|
|
23618
|
+
//#endregion
|
|
23619
|
+
//#region ../server/src/pty-manager.ts
|
|
23620
|
+
const DEFAULT_SCROLLBACK = 1e3;
|
|
23621
|
+
const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
23622
|
+
function detectPtyPlatform() {
|
|
23623
|
+
if (process.platform === "win32") return "windows";
|
|
23624
|
+
if (process.platform === "darwin") return "macos";
|
|
23625
|
+
return "common";
|
|
23626
|
+
}
|
|
23627
|
+
function resolveDefaultShell(platform, env) {
|
|
23628
|
+
if (platform === "windows") return env.ComSpec?.trim() || "cmd.exe";
|
|
23629
|
+
return env.SHELL?.trim() || "/bin/sh";
|
|
23630
|
+
}
|
|
23631
|
+
function resolvePtyCommand(opts) {
|
|
23632
|
+
const command = opts.command?.trim();
|
|
23633
|
+
if (command) return {
|
|
23634
|
+
command,
|
|
23635
|
+
args: opts.args ?? []
|
|
23636
|
+
};
|
|
23637
|
+
return {
|
|
23638
|
+
command: resolveDefaultShell(opts.platform, opts.env),
|
|
23639
|
+
args: []
|
|
23640
|
+
};
|
|
23641
|
+
}
|
|
23642
|
+
var PtySession = class extends EventEmitter {
|
|
23643
|
+
id;
|
|
23644
|
+
command;
|
|
23645
|
+
args;
|
|
23646
|
+
platform;
|
|
23647
|
+
closeTip;
|
|
23648
|
+
closeCallbackUrl;
|
|
23649
|
+
createdAt;
|
|
23650
|
+
process;
|
|
23651
|
+
titleInterval = null;
|
|
23652
|
+
lastTitle = "";
|
|
23653
|
+
buffer = [];
|
|
23654
|
+
bufferByteLength = 0;
|
|
23655
|
+
maxBufferLines;
|
|
23656
|
+
maxBufferBytes;
|
|
23657
|
+
isExited = false;
|
|
23658
|
+
exitCode = null;
|
|
23659
|
+
constructor(id, opts) {
|
|
23660
|
+
super();
|
|
23661
|
+
this.id = id;
|
|
23662
|
+
this.createdAt = Date.now();
|
|
23663
|
+
const resolvedCommand = resolvePtyCommand({
|
|
23664
|
+
platform: opts.platform,
|
|
23665
|
+
command: opts.command,
|
|
23666
|
+
args: opts.args,
|
|
23667
|
+
env: process.env
|
|
23668
|
+
});
|
|
23669
|
+
this.command = resolvedCommand.command;
|
|
23670
|
+
this.args = resolvedCommand.args;
|
|
23671
|
+
this.platform = opts.platform;
|
|
23672
|
+
this.closeTip = opts.closeTip;
|
|
23673
|
+
this.closeCallbackUrl = opts.closeCallbackUrl;
|
|
23674
|
+
this.maxBufferLines = opts.scrollback ?? DEFAULT_SCROLLBACK;
|
|
23675
|
+
this.maxBufferBytes = opts.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
23676
|
+
this.process = pty.spawn(this.command, this.args, {
|
|
23677
|
+
name: "xterm-256color",
|
|
23678
|
+
cols: opts.cols ?? 80,
|
|
23679
|
+
rows: opts.rows ?? 24,
|
|
23680
|
+
cwd: opts.cwd,
|
|
23681
|
+
env: {
|
|
23682
|
+
...process.env,
|
|
23683
|
+
TERM: "xterm-256color"
|
|
23684
|
+
}
|
|
23685
|
+
});
|
|
23686
|
+
this.process.onData((data) => {
|
|
23687
|
+
this.appendBuffer(data);
|
|
23688
|
+
this.emit("data", data);
|
|
23689
|
+
});
|
|
23690
|
+
this.process.onExit(({ exitCode }) => {
|
|
23691
|
+
if (this.titleInterval) {
|
|
23692
|
+
clearInterval(this.titleInterval);
|
|
23693
|
+
this.titleInterval = null;
|
|
23694
|
+
}
|
|
23695
|
+
this.isExited = true;
|
|
23696
|
+
this.exitCode = exitCode;
|
|
23697
|
+
this.emit("exit", exitCode);
|
|
23698
|
+
});
|
|
23699
|
+
this.titleInterval = setInterval(() => {
|
|
23700
|
+
try {
|
|
23701
|
+
const title = this.process.process;
|
|
23702
|
+
if (title && title !== this.lastTitle) {
|
|
23703
|
+
this.lastTitle = title;
|
|
23704
|
+
this.emit("title", title);
|
|
23705
|
+
}
|
|
23706
|
+
} catch {}
|
|
23707
|
+
}, 1e3);
|
|
23708
|
+
}
|
|
23709
|
+
get title() {
|
|
23710
|
+
return this.lastTitle;
|
|
23711
|
+
}
|
|
23712
|
+
appendBuffer(data) {
|
|
23713
|
+
let chunk = data;
|
|
23714
|
+
if (chunk.length > this.maxBufferBytes) chunk = chunk.slice(-this.maxBufferBytes);
|
|
23715
|
+
this.buffer.push(chunk);
|
|
23716
|
+
this.bufferByteLength += chunk.length;
|
|
23717
|
+
while (this.bufferByteLength > this.maxBufferBytes && this.buffer.length > 0) {
|
|
23718
|
+
const removed = this.buffer.shift();
|
|
23719
|
+
this.bufferByteLength -= removed.length;
|
|
23720
|
+
}
|
|
23721
|
+
while (this.buffer.length > this.maxBufferLines) {
|
|
23722
|
+
const removed = this.buffer.shift();
|
|
23723
|
+
this.bufferByteLength -= removed.length;
|
|
23724
|
+
}
|
|
23725
|
+
}
|
|
23726
|
+
getBuffer() {
|
|
23727
|
+
return this.buffer.join("");
|
|
23728
|
+
}
|
|
23729
|
+
write(data) {
|
|
23730
|
+
if (!this.isExited) this.process.write(data);
|
|
23731
|
+
}
|
|
23732
|
+
resize(cols, rows) {
|
|
23733
|
+
if (!this.isExited) this.process.resize(cols, rows);
|
|
23734
|
+
}
|
|
23735
|
+
close() {
|
|
23736
|
+
if (this.titleInterval) {
|
|
23737
|
+
clearInterval(this.titleInterval);
|
|
23738
|
+
this.titleInterval = null;
|
|
23739
|
+
}
|
|
23740
|
+
try {
|
|
23741
|
+
this.process.kill();
|
|
23742
|
+
} catch {}
|
|
23743
|
+
this.removeAllListeners();
|
|
23744
|
+
}
|
|
23745
|
+
toInfo() {
|
|
23746
|
+
return {
|
|
23747
|
+
id: this.id,
|
|
23748
|
+
title: this.lastTitle,
|
|
23749
|
+
command: this.command,
|
|
23750
|
+
args: this.args,
|
|
23751
|
+
platform: this.platform,
|
|
23752
|
+
isExited: this.isExited,
|
|
23753
|
+
exitCode: this.exitCode,
|
|
23754
|
+
closeTip: this.closeTip,
|
|
23755
|
+
closeCallbackUrl: this.closeCallbackUrl,
|
|
23756
|
+
createdAt: this.createdAt
|
|
23757
|
+
};
|
|
23758
|
+
}
|
|
23759
|
+
};
|
|
23760
|
+
var PtyManager = class {
|
|
23761
|
+
sessions = /* @__PURE__ */ new Map();
|
|
23762
|
+
idCounter = 0;
|
|
23763
|
+
platform;
|
|
23764
|
+
constructor(defaultCwd) {
|
|
23765
|
+
this.defaultCwd = defaultCwd;
|
|
23766
|
+
this.platform = detectPtyPlatform();
|
|
23767
|
+
}
|
|
23768
|
+
create(opts) {
|
|
23769
|
+
const id = `pty-${++this.idCounter}`;
|
|
23770
|
+
const session = new PtySession(id, {
|
|
23771
|
+
cols: opts.cols,
|
|
23772
|
+
rows: opts.rows,
|
|
23773
|
+
command: opts.command,
|
|
23774
|
+
args: opts.args,
|
|
23775
|
+
closeTip: opts.closeTip,
|
|
23776
|
+
closeCallbackUrl: opts.closeCallbackUrl,
|
|
23777
|
+
cwd: this.defaultCwd,
|
|
23778
|
+
scrollback: opts.scrollback,
|
|
23779
|
+
maxBufferBytes: opts.maxBufferBytes,
|
|
23780
|
+
platform: this.platform
|
|
23781
|
+
});
|
|
23782
|
+
this.sessions.set(id, session);
|
|
23783
|
+
return session;
|
|
23784
|
+
}
|
|
23785
|
+
get(id) {
|
|
23786
|
+
return this.sessions.get(id);
|
|
23787
|
+
}
|
|
23788
|
+
list() {
|
|
23789
|
+
const result = [];
|
|
23790
|
+
for (const session of this.sessions.values()) result.push(session.toInfo());
|
|
23791
|
+
return result;
|
|
23792
|
+
}
|
|
23793
|
+
write(id, data) {
|
|
23794
|
+
this.sessions.get(id)?.write(data);
|
|
23795
|
+
}
|
|
23796
|
+
resize(id, cols, rows) {
|
|
23797
|
+
this.sessions.get(id)?.resize(cols, rows);
|
|
23798
|
+
}
|
|
23799
|
+
close(id) {
|
|
23800
|
+
const session = this.sessions.get(id);
|
|
23801
|
+
if (session) {
|
|
23802
|
+
session.close();
|
|
23803
|
+
this.sessions.delete(id);
|
|
23804
|
+
}
|
|
23805
|
+
}
|
|
23806
|
+
closeAll() {
|
|
23807
|
+
for (const session of this.sessions.values()) session.close();
|
|
23808
|
+
this.sessions.clear();
|
|
23809
|
+
}
|
|
23810
|
+
};
|
|
23811
|
+
|
|
23812
|
+
//#endregion
|
|
23813
|
+
//#region ../server/src/pty-websocket.ts
|
|
23814
|
+
function createPtyWebSocketHandler(ptyManager) {
|
|
23815
|
+
return (ws) => {
|
|
23816
|
+
const cleanups = /* @__PURE__ */ new Map();
|
|
23817
|
+
const send = (msg) => {
|
|
23818
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
|
|
23819
|
+
};
|
|
23820
|
+
const sendError = (code, message, opts) => {
|
|
23821
|
+
send({
|
|
23822
|
+
type: "error",
|
|
23823
|
+
code,
|
|
23824
|
+
message,
|
|
23825
|
+
sessionId: opts?.sessionId
|
|
23826
|
+
});
|
|
23827
|
+
};
|
|
23828
|
+
const attachToSession = (session, opts) => {
|
|
23829
|
+
const sessionId = session.id;
|
|
23830
|
+
cleanups.get(sessionId)?.();
|
|
23831
|
+
if (opts?.cols && opts?.rows && !session.isExited) session.resize(opts.cols, opts.rows);
|
|
23832
|
+
const onData = (data) => {
|
|
23833
|
+
send({
|
|
23834
|
+
type: "output",
|
|
23835
|
+
sessionId,
|
|
23836
|
+
data
|
|
23837
|
+
});
|
|
23838
|
+
};
|
|
23839
|
+
const onExit = (exitCode) => {
|
|
23840
|
+
send({
|
|
23841
|
+
type: "exit",
|
|
23842
|
+
sessionId,
|
|
23843
|
+
exitCode
|
|
23844
|
+
});
|
|
23845
|
+
};
|
|
23846
|
+
const onTitle = (title) => {
|
|
23847
|
+
send({
|
|
23848
|
+
type: "title",
|
|
23849
|
+
sessionId,
|
|
23850
|
+
title
|
|
23851
|
+
});
|
|
23852
|
+
};
|
|
23853
|
+
session.on("data", onData);
|
|
23854
|
+
session.on("exit", onExit);
|
|
23855
|
+
session.on("title", onTitle);
|
|
23856
|
+
cleanups.set(sessionId, () => {
|
|
23857
|
+
session.removeListener("data", onData);
|
|
23858
|
+
session.removeListener("exit", onExit);
|
|
23859
|
+
session.removeListener("title", onTitle);
|
|
23860
|
+
cleanups.delete(sessionId);
|
|
23861
|
+
});
|
|
23862
|
+
};
|
|
23863
|
+
ws.on("message", (raw$1) => {
|
|
23864
|
+
let parsed;
|
|
23865
|
+
try {
|
|
23866
|
+
parsed = JSON.parse(String(raw$1));
|
|
23867
|
+
} catch {
|
|
23868
|
+
sendError("INVALID_JSON", "Invalid JSON payload");
|
|
23869
|
+
return;
|
|
23870
|
+
}
|
|
23871
|
+
const parsedMessage = PtyClientMessageSchema.safeParse(parsed);
|
|
23872
|
+
if (!parsedMessage.success) {
|
|
23873
|
+
const firstIssue = parsedMessage.error.issues[0]?.message;
|
|
23874
|
+
sendError("INVALID_MESSAGE", firstIssue ?? "Invalid PTY message");
|
|
23875
|
+
return;
|
|
23876
|
+
}
|
|
23877
|
+
const msg = parsedMessage.data;
|
|
23878
|
+
switch (msg.type) {
|
|
23879
|
+
case "create":
|
|
23880
|
+
try {
|
|
23881
|
+
const createMessage = msg;
|
|
23882
|
+
const session = ptyManager.create({
|
|
23883
|
+
cols: msg.cols,
|
|
23884
|
+
rows: msg.rows,
|
|
23885
|
+
command: msg.command,
|
|
23886
|
+
args: msg.args,
|
|
23887
|
+
closeTip: createMessage.closeTip,
|
|
23888
|
+
closeCallbackUrl: createMessage.closeCallbackUrl
|
|
23889
|
+
});
|
|
23890
|
+
send({
|
|
23891
|
+
type: "created",
|
|
23892
|
+
requestId: msg.requestId,
|
|
23893
|
+
sessionId: session.id,
|
|
23894
|
+
platform: session.platform
|
|
23895
|
+
});
|
|
23896
|
+
attachToSession(session);
|
|
23897
|
+
} catch (err) {
|
|
23898
|
+
sendError("PTY_CREATE_FAILED", err instanceof Error ? err.message : String(err), { sessionId: msg.requestId });
|
|
23899
|
+
}
|
|
23900
|
+
break;
|
|
23901
|
+
case "attach": {
|
|
23902
|
+
const session = ptyManager.get(msg.sessionId);
|
|
23903
|
+
if (!session) {
|
|
23904
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23905
|
+
send({
|
|
23906
|
+
type: "exit",
|
|
23907
|
+
sessionId: msg.sessionId,
|
|
23908
|
+
exitCode: -1
|
|
23909
|
+
});
|
|
23910
|
+
break;
|
|
23911
|
+
}
|
|
23912
|
+
attachToSession(session, {
|
|
23913
|
+
cols: msg.cols,
|
|
23914
|
+
rows: msg.rows
|
|
23915
|
+
});
|
|
23916
|
+
const buffer = session.getBuffer();
|
|
23917
|
+
if (buffer) send({
|
|
23918
|
+
type: "buffer",
|
|
23919
|
+
sessionId: session.id,
|
|
23920
|
+
data: buffer
|
|
23921
|
+
});
|
|
23922
|
+
if (session.title) send({
|
|
23923
|
+
type: "title",
|
|
23924
|
+
sessionId: session.id,
|
|
23925
|
+
title: session.title
|
|
23926
|
+
});
|
|
23927
|
+
if (session.isExited) send({
|
|
23928
|
+
type: "exit",
|
|
23929
|
+
sessionId: session.id,
|
|
23930
|
+
exitCode: session.exitCode ?? -1
|
|
23931
|
+
});
|
|
23932
|
+
break;
|
|
23933
|
+
}
|
|
23934
|
+
case "list":
|
|
23935
|
+
send({
|
|
23936
|
+
type: "list",
|
|
23937
|
+
sessions: ptyManager.list().map((s) => ({
|
|
23938
|
+
id: s.id,
|
|
23939
|
+
title: s.title,
|
|
23940
|
+
command: s.command,
|
|
23941
|
+
args: s.args,
|
|
23942
|
+
platform: s.platform,
|
|
23943
|
+
isExited: s.isExited,
|
|
23944
|
+
exitCode: s.exitCode,
|
|
23945
|
+
closeTip: s.closeTip,
|
|
23946
|
+
closeCallbackUrl: s.closeCallbackUrl
|
|
23947
|
+
}))
|
|
23948
|
+
});
|
|
23949
|
+
break;
|
|
23950
|
+
case "input": {
|
|
23951
|
+
const session = ptyManager.get(msg.sessionId);
|
|
23952
|
+
if (!session) {
|
|
23953
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23954
|
+
break;
|
|
23955
|
+
}
|
|
23956
|
+
session.write(msg.data);
|
|
23957
|
+
break;
|
|
23958
|
+
}
|
|
23959
|
+
case "resize": {
|
|
23960
|
+
const session = ptyManager.get(msg.sessionId);
|
|
23961
|
+
if (!session) {
|
|
23962
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23963
|
+
break;
|
|
23964
|
+
}
|
|
23965
|
+
session.resize(msg.cols, msg.rows);
|
|
23966
|
+
break;
|
|
23515
23967
|
}
|
|
23516
|
-
|
|
23517
|
-
|
|
23518
|
-
|
|
23519
|
-
|
|
23520
|
-
|
|
23521
|
-
|
|
23522
|
-
|
|
23968
|
+
case "close": {
|
|
23969
|
+
const session = ptyManager.get(msg.sessionId);
|
|
23970
|
+
if (!session) {
|
|
23971
|
+
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23972
|
+
break;
|
|
23973
|
+
}
|
|
23974
|
+
cleanups.get(msg.sessionId)?.();
|
|
23975
|
+
ptyManager.close(session.id);
|
|
23976
|
+
break;
|
|
23523
23977
|
}
|
|
23524
23978
|
}
|
|
23525
|
-
};
|
|
23526
|
-
startStream(safeEventHandler).then((cancelFn) => {
|
|
23527
|
-
cancel = cancelFn;
|
|
23528
|
-
}).catch((err) => {
|
|
23529
|
-
console.error("[CLI Stream] Error starting stream:", err);
|
|
23530
|
-
if (!completed) {
|
|
23531
|
-
completed = true;
|
|
23532
|
-
try {
|
|
23533
|
-
emit.error(err instanceof Error ? err : new Error(String(err)));
|
|
23534
|
-
} catch {}
|
|
23535
|
-
}
|
|
23536
23979
|
});
|
|
23537
|
-
|
|
23538
|
-
|
|
23539
|
-
|
|
23540
|
-
};
|
|
23541
|
-
});
|
|
23542
|
-
}
|
|
23543
|
-
|
|
23544
|
-
//#endregion
|
|
23545
|
-
//#region ../server/src/dashboard-git-snapshot.ts
|
|
23546
|
-
const execFileAsync$1 = promisify$1(execFile);
|
|
23547
|
-
const EMPTY_DIFF = {
|
|
23548
|
-
files: 0,
|
|
23549
|
-
insertions: 0,
|
|
23550
|
-
deletions: 0
|
|
23551
|
-
};
|
|
23552
|
-
async function defaultRunGit(cwd, args) {
|
|
23553
|
-
try {
|
|
23554
|
-
const { stdout } = await execFileAsync$1("git", args, {
|
|
23555
|
-
cwd,
|
|
23556
|
-
encoding: "utf8",
|
|
23557
|
-
maxBuffer: 8 * 1024 * 1024
|
|
23980
|
+
ws.on("close", () => {
|
|
23981
|
+
for (const cleanup of cleanups.values()) cleanup();
|
|
23982
|
+
cleanups.clear();
|
|
23558
23983
|
});
|
|
23559
|
-
return {
|
|
23560
|
-
ok: true,
|
|
23561
|
-
stdout
|
|
23562
|
-
};
|
|
23563
|
-
} catch {
|
|
23564
|
-
return {
|
|
23565
|
-
ok: false,
|
|
23566
|
-
stdout: ""
|
|
23567
|
-
};
|
|
23568
|
-
}
|
|
23569
|
-
}
|
|
23570
|
-
function parseShortStat(output) {
|
|
23571
|
-
const files = Number(/(\d+)\s+files? changed/.exec(output)?.[1] ?? 0);
|
|
23572
|
-
const insertions = Number(/(\d+)\s+insertions?\(\+\)/.exec(output)?.[1] ?? 0);
|
|
23573
|
-
const deletions = Number(/(\d+)\s+deletions?\(-\)/.exec(output)?.[1] ?? 0);
|
|
23574
|
-
return {
|
|
23575
|
-
files: Number.isFinite(files) ? files : 0,
|
|
23576
|
-
insertions: Number.isFinite(insertions) ? insertions : 0,
|
|
23577
|
-
deletions: Number.isFinite(deletions) ? deletions : 0
|
|
23578
|
-
};
|
|
23579
|
-
}
|
|
23580
|
-
function parseNumStat(output) {
|
|
23581
|
-
let files = 0;
|
|
23582
|
-
let insertions = 0;
|
|
23583
|
-
let deletions = 0;
|
|
23584
|
-
for (const line of output.split("\n")) {
|
|
23585
|
-
const trimmed = line.trim();
|
|
23586
|
-
if (!trimmed) continue;
|
|
23587
|
-
const [addRaw, deleteRaw] = trimmed.split(" ");
|
|
23588
|
-
if (!addRaw || !deleteRaw) continue;
|
|
23589
|
-
files += 1;
|
|
23590
|
-
if (addRaw !== "-") insertions += Number(addRaw) || 0;
|
|
23591
|
-
if (deleteRaw !== "-") deletions += Number(deleteRaw) || 0;
|
|
23592
|
-
}
|
|
23593
|
-
return {
|
|
23594
|
-
files,
|
|
23595
|
-
insertions,
|
|
23596
|
-
deletions
|
|
23597
23984
|
};
|
|
23598
23985
|
}
|
|
23599
|
-
|
|
23600
|
-
|
|
23986
|
+
|
|
23987
|
+
//#endregion
|
|
23988
|
+
//#region ../search/src/protocol.ts
|
|
23989
|
+
const SearchDocumentKindSchema = enumType([
|
|
23990
|
+
"spec",
|
|
23991
|
+
"change",
|
|
23992
|
+
"archive"
|
|
23993
|
+
]);
|
|
23994
|
+
const SearchDocumentSchema = objectType({
|
|
23995
|
+
id: stringType(),
|
|
23996
|
+
kind: SearchDocumentKindSchema,
|
|
23997
|
+
title: stringType(),
|
|
23998
|
+
href: stringType(),
|
|
23999
|
+
path: stringType(),
|
|
24000
|
+
content: stringType(),
|
|
24001
|
+
updatedAt: numberType()
|
|
24002
|
+
});
|
|
24003
|
+
const SearchQuerySchema = objectType({
|
|
24004
|
+
query: stringType(),
|
|
24005
|
+
limit: numberType().int().positive().optional()
|
|
24006
|
+
});
|
|
24007
|
+
const SearchHitSchema = objectType({
|
|
24008
|
+
documentId: stringType(),
|
|
24009
|
+
kind: SearchDocumentKindSchema,
|
|
24010
|
+
title: stringType(),
|
|
24011
|
+
href: stringType(),
|
|
24012
|
+
path: stringType(),
|
|
24013
|
+
score: numberType(),
|
|
24014
|
+
snippet: stringType(),
|
|
24015
|
+
updatedAt: numberType()
|
|
24016
|
+
});
|
|
24017
|
+
const SearchWorkerRequestSchema = discriminatedUnionType("type", [
|
|
24018
|
+
objectType({
|
|
24019
|
+
id: stringType(),
|
|
24020
|
+
type: literalType("init"),
|
|
24021
|
+
docs: arrayType(SearchDocumentSchema)
|
|
24022
|
+
}),
|
|
24023
|
+
objectType({
|
|
24024
|
+
id: stringType(),
|
|
24025
|
+
type: literalType("replaceAll"),
|
|
24026
|
+
docs: arrayType(SearchDocumentSchema)
|
|
24027
|
+
}),
|
|
24028
|
+
objectType({
|
|
24029
|
+
id: stringType(),
|
|
24030
|
+
type: literalType("search"),
|
|
24031
|
+
query: SearchQuerySchema
|
|
24032
|
+
}),
|
|
24033
|
+
objectType({
|
|
24034
|
+
id: stringType(),
|
|
24035
|
+
type: literalType("dispose")
|
|
24036
|
+
})
|
|
24037
|
+
]);
|
|
24038
|
+
const SearchWorkerResponseSchema = discriminatedUnionType("type", [
|
|
24039
|
+
objectType({
|
|
24040
|
+
id: stringType(),
|
|
24041
|
+
type: literalType("ok")
|
|
24042
|
+
}),
|
|
24043
|
+
objectType({
|
|
24044
|
+
id: stringType(),
|
|
24045
|
+
type: literalType("results"),
|
|
24046
|
+
hits: arrayType(SearchHitSchema)
|
|
24047
|
+
}),
|
|
24048
|
+
objectType({
|
|
24049
|
+
id: stringType(),
|
|
24050
|
+
type: literalType("error"),
|
|
24051
|
+
message: stringType()
|
|
24052
|
+
})
|
|
24053
|
+
]);
|
|
24054
|
+
|
|
24055
|
+
//#endregion
|
|
24056
|
+
//#region ../search/src/worker-source.ts
|
|
24057
|
+
const sharedRuntimeSource = String.raw`
|
|
24058
|
+
const DEFAULT_LIMIT = 50;
|
|
24059
|
+
const MAX_LIMIT = 200;
|
|
24060
|
+
const SNIPPET_SIZE = 180;
|
|
24061
|
+
|
|
24062
|
+
function normalizeText(input) {
|
|
24063
|
+
return String(input || '')
|
|
24064
|
+
.toLowerCase()
|
|
24065
|
+
.replace(/\s+/g, ' ')
|
|
24066
|
+
.trim();
|
|
23601
24067
|
}
|
|
23602
|
-
|
|
23603
|
-
|
|
23604
|
-
|
|
23605
|
-
|
|
24068
|
+
|
|
24069
|
+
function splitTerms(query) {
|
|
24070
|
+
return normalizeText(query)
|
|
24071
|
+
.split(' ')
|
|
24072
|
+
.map((term) => term.trim())
|
|
24073
|
+
.filter((term) => term.length > 0);
|
|
23606
24074
|
}
|
|
23607
|
-
|
|
23608
|
-
|
|
23609
|
-
|
|
23610
|
-
|
|
24075
|
+
|
|
24076
|
+
function toSearchIndexDocument(doc) {
|
|
24077
|
+
return {
|
|
24078
|
+
id: doc.id,
|
|
24079
|
+
kind: doc.kind,
|
|
24080
|
+
title: doc.title,
|
|
24081
|
+
href: doc.href,
|
|
24082
|
+
path: doc.path,
|
|
24083
|
+
content: doc.content,
|
|
24084
|
+
updatedAt: doc.updatedAt,
|
|
24085
|
+
normalizedTitle: normalizeText(doc.title),
|
|
24086
|
+
normalizedPath: normalizeText(doc.path),
|
|
24087
|
+
normalizedContent: normalizeText(doc.content),
|
|
24088
|
+
};
|
|
23611
24089
|
}
|
|
23612
|
-
|
|
23613
|
-
|
|
23614
|
-
|
|
23615
|
-
|
|
23616
|
-
|
|
23617
|
-
entries.push(current);
|
|
23618
|
-
current = null;
|
|
23619
|
-
};
|
|
23620
|
-
for (const line of porcelain.split("\n")) {
|
|
23621
|
-
if (line.startsWith("worktree ")) {
|
|
23622
|
-
flush();
|
|
23623
|
-
current = {
|
|
23624
|
-
path: line.slice(9).trim(),
|
|
23625
|
-
branchRef: null,
|
|
23626
|
-
detached: false
|
|
23627
|
-
};
|
|
23628
|
-
continue;
|
|
23629
|
-
}
|
|
23630
|
-
if (!current) continue;
|
|
23631
|
-
if (line.startsWith("branch ")) {
|
|
23632
|
-
current.branchRef = line.slice(7).trim();
|
|
23633
|
-
continue;
|
|
23634
|
-
}
|
|
23635
|
-
if (line === "detached") {
|
|
23636
|
-
current.detached = true;
|
|
23637
|
-
continue;
|
|
23638
|
-
}
|
|
23639
|
-
}
|
|
23640
|
-
flush();
|
|
23641
|
-
return entries;
|
|
24090
|
+
|
|
24091
|
+
function buildSearchIndex(docs) {
|
|
24092
|
+
return {
|
|
24093
|
+
documents: Array.isArray(docs) ? docs.map(toSearchIndexDocument) : [],
|
|
24094
|
+
};
|
|
23642
24095
|
}
|
|
23643
|
-
|
|
23644
|
-
|
|
23645
|
-
|
|
23646
|
-
|
|
23647
|
-
const activeMatch = /^openspec\/changes\/([^/]+)\//.exec(normalized);
|
|
23648
|
-
if (activeMatch?.[1]) {
|
|
23649
|
-
related.add(activeMatch[1]);
|
|
23650
|
-
continue;
|
|
23651
|
-
}
|
|
23652
|
-
const archiveMatch = /^openspec\/changes\/archive\/([^/]+)\//.exec(normalized);
|
|
23653
|
-
if (archiveMatch?.[1]) {
|
|
23654
|
-
const fullName = archiveMatch[1];
|
|
23655
|
-
related.add(fullName.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
|
|
23656
|
-
}
|
|
23657
|
-
}
|
|
23658
|
-
return [...related].sort((a, b) => a.localeCompare(b));
|
|
24096
|
+
|
|
24097
|
+
function resolveLimit(limit) {
|
|
24098
|
+
if (typeof limit !== 'number' || Number.isNaN(limit)) return DEFAULT_LIMIT;
|
|
24099
|
+
return Math.min(MAX_LIMIT, Math.max(1, Math.trunc(limit)));
|
|
23659
24100
|
}
|
|
23660
|
-
|
|
23661
|
-
|
|
23662
|
-
|
|
23663
|
-
|
|
23664
|
-
|
|
23665
|
-
|
|
23666
|
-
|
|
23667
|
-
|
|
23668
|
-
if (remoteHead.ok && remoteRef) return remoteRef;
|
|
23669
|
-
const localHead = await runGit(projectDir, [
|
|
23670
|
-
"rev-parse",
|
|
23671
|
-
"--abbrev-ref",
|
|
23672
|
-
"HEAD"
|
|
23673
|
-
]);
|
|
23674
|
-
const localRef = localHead.stdout.trim();
|
|
23675
|
-
if (localHead.ok && localRef && localRef !== "HEAD") return localRef;
|
|
23676
|
-
return "main";
|
|
24101
|
+
|
|
24102
|
+
function isDocumentMatch(doc, terms) {
|
|
24103
|
+
return terms.every(
|
|
24104
|
+
(term) =>
|
|
24105
|
+
doc.normalizedTitle.includes(term) ||
|
|
24106
|
+
doc.normalizedPath.includes(term) ||
|
|
24107
|
+
doc.normalizedContent.includes(term)
|
|
24108
|
+
);
|
|
23677
24109
|
}
|
|
23678
|
-
|
|
23679
|
-
|
|
23680
|
-
|
|
23681
|
-
|
|
23682
|
-
|
|
23683
|
-
|
|
23684
|
-
|
|
23685
|
-
|
|
23686
|
-
|
|
23687
|
-
|
|
23688
|
-
|
|
23689
|
-
|
|
23690
|
-
|
|
23691
|
-
|
|
23692
|
-
|
|
23693
|
-
|
|
23694
|
-
"--format=",
|
|
23695
|
-
hash
|
|
23696
|
-
]);
|
|
23697
|
-
const changedFiles = (await runGit(worktreePath, [
|
|
23698
|
-
"show",
|
|
23699
|
-
"--name-only",
|
|
23700
|
-
"--format=",
|
|
23701
|
-
hash
|
|
23702
|
-
])).stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23703
|
-
entries.push({
|
|
23704
|
-
type: "commit",
|
|
23705
|
-
hash,
|
|
23706
|
-
title: title.trim() || hash.slice(0, 7),
|
|
23707
|
-
relatedChanges: parseRelatedChanges(changedFiles),
|
|
23708
|
-
diff: diffResult.ok ? parseNumStat(diffResult.stdout) : EMPTY_DIFF
|
|
23709
|
-
});
|
|
23710
|
-
}
|
|
23711
|
-
const trackedResult = await runGit(worktreePath, [
|
|
23712
|
-
"diff",
|
|
23713
|
-
"--numstat",
|
|
23714
|
-
"HEAD"
|
|
23715
|
-
]);
|
|
23716
|
-
const trackedFilesResult = await runGit(worktreePath, [
|
|
23717
|
-
"diff",
|
|
23718
|
-
"--name-only",
|
|
23719
|
-
"HEAD"
|
|
23720
|
-
]);
|
|
23721
|
-
const untrackedResult = await runGit(worktreePath, [
|
|
23722
|
-
"ls-files",
|
|
23723
|
-
"--others",
|
|
23724
|
-
"--exclude-standard"
|
|
23725
|
-
]);
|
|
23726
|
-
const trackedFiles = trackedFilesResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23727
|
-
const untrackedFiles = untrackedResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23728
|
-
const allUncommittedFiles = new Set([...trackedFiles, ...untrackedFiles]);
|
|
23729
|
-
const trackedDiff = trackedResult.ok ? parseNumStat(trackedResult.stdout) : EMPTY_DIFF;
|
|
23730
|
-
entries.push({
|
|
23731
|
-
type: "uncommitted",
|
|
23732
|
-
title: "Uncommitted",
|
|
23733
|
-
relatedChanges: parseRelatedChanges([...allUncommittedFiles]),
|
|
23734
|
-
diff: {
|
|
23735
|
-
files: allUncommittedFiles.size,
|
|
23736
|
-
insertions: trackedDiff.insertions,
|
|
23737
|
-
deletions: trackedDiff.deletions
|
|
23738
|
-
}
|
|
23739
|
-
});
|
|
23740
|
-
return entries;
|
|
24110
|
+
|
|
24111
|
+
function scoreDocument(doc, terms) {
|
|
24112
|
+
let score = 0;
|
|
24113
|
+
|
|
24114
|
+
for (const term of terms) {
|
|
24115
|
+
if (doc.normalizedTitle.includes(term)) score += 30;
|
|
24116
|
+
if (doc.normalizedPath.includes(term)) score += 20;
|
|
24117
|
+
|
|
24118
|
+
const contentIdx = doc.normalizedContent.indexOf(term);
|
|
24119
|
+
if (contentIdx >= 0) {
|
|
24120
|
+
score += 8;
|
|
24121
|
+
if (contentIdx < 160) score += 4;
|
|
24122
|
+
}
|
|
24123
|
+
}
|
|
24124
|
+
|
|
24125
|
+
return score;
|
|
23741
24126
|
}
|
|
23742
|
-
|
|
23743
|
-
|
|
23744
|
-
|
|
23745
|
-
|
|
23746
|
-
|
|
23747
|
-
|
|
23748
|
-
|
|
23749
|
-
|
|
23750
|
-
|
|
23751
|
-
|
|
23752
|
-
|
|
23753
|
-
|
|
23754
|
-
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
|
|
23766
|
-
|
|
23767
|
-
defaultBranch,
|
|
23768
|
-
maxCommitEntries,
|
|
23769
|
-
runGit
|
|
23770
|
-
});
|
|
23771
|
-
return {
|
|
23772
|
-
path: worktreePath,
|
|
23773
|
-
relativePath: relativePath(resolvedProjectDir, worktreePath),
|
|
23774
|
-
branchName: parseBranchName(worktree.branchRef, worktree.detached),
|
|
23775
|
-
isCurrent: resolvedProjectDir === worktreePath,
|
|
23776
|
-
ahead,
|
|
23777
|
-
behind,
|
|
23778
|
-
diff,
|
|
23779
|
-
entries
|
|
23780
|
-
};
|
|
24127
|
+
|
|
24128
|
+
function createSnippet(content, terms) {
|
|
24129
|
+
const source = String(content || '').trim();
|
|
24130
|
+
if (!source) return '';
|
|
24131
|
+
|
|
24132
|
+
const normalizedSource = normalizeText(source);
|
|
24133
|
+
let matchIndex = -1;
|
|
24134
|
+
|
|
24135
|
+
for (const term of terms) {
|
|
24136
|
+
const idx = normalizedSource.indexOf(term);
|
|
24137
|
+
if (idx >= 0 && (matchIndex < 0 || idx < matchIndex)) {
|
|
24138
|
+
matchIndex = idx;
|
|
24139
|
+
}
|
|
24140
|
+
}
|
|
24141
|
+
|
|
24142
|
+
if (matchIndex < 0) {
|
|
24143
|
+
return source.slice(0, SNIPPET_SIZE);
|
|
24144
|
+
}
|
|
24145
|
+
|
|
24146
|
+
const start = Math.max(0, matchIndex - Math.floor(SNIPPET_SIZE / 3));
|
|
24147
|
+
const end = Math.min(source.length, start + SNIPPET_SIZE);
|
|
24148
|
+
|
|
24149
|
+
const prefix = start > 0 ? '...' : '';
|
|
24150
|
+
const suffix = end < source.length ? '...' : '';
|
|
24151
|
+
return prefix + source.slice(start, end) + suffix;
|
|
23781
24152
|
}
|
|
23782
|
-
|
|
23783
|
-
|
|
23784
|
-
|
|
23785
|
-
|
|
23786
|
-
|
|
23787
|
-
|
|
23788
|
-
|
|
23789
|
-
|
|
23790
|
-
|
|
23791
|
-
|
|
23792
|
-
|
|
23793
|
-
|
|
23794
|
-
|
|
23795
|
-
|
|
23796
|
-
|
|
23797
|
-
|
|
23798
|
-
|
|
23799
|
-
|
|
23800
|
-
|
|
23801
|
-
|
|
23802
|
-
|
|
23803
|
-
|
|
23804
|
-
|
|
23805
|
-
|
|
23806
|
-
|
|
23807
|
-
|
|
23808
|
-
|
|
23809
|
-
|
|
23810
|
-
|
|
23811
|
-
|
|
23812
|
-
|
|
24153
|
+
|
|
24154
|
+
function searchIndex(index, query) {
|
|
24155
|
+
const terms = splitTerms(query && query.query ? query.query : '');
|
|
24156
|
+
if (terms.length === 0) return [];
|
|
24157
|
+
|
|
24158
|
+
const hits = [];
|
|
24159
|
+
|
|
24160
|
+
for (const doc of index.documents) {
|
|
24161
|
+
if (!isDocumentMatch(doc, terms)) continue;
|
|
24162
|
+
|
|
24163
|
+
hits.push({
|
|
24164
|
+
documentId: doc.id,
|
|
24165
|
+
kind: doc.kind,
|
|
24166
|
+
title: doc.title,
|
|
24167
|
+
href: doc.href,
|
|
24168
|
+
path: doc.path,
|
|
24169
|
+
score: scoreDocument(doc, terms),
|
|
24170
|
+
snippet: createSnippet(doc.content, terms),
|
|
24171
|
+
updatedAt: doc.updatedAt,
|
|
24172
|
+
});
|
|
24173
|
+
}
|
|
24174
|
+
|
|
24175
|
+
hits.sort((a, b) => {
|
|
24176
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
24177
|
+
return b.updatedAt - a.updatedAt;
|
|
24178
|
+
});
|
|
24179
|
+
|
|
24180
|
+
return hits.slice(0, resolveLimit(query ? query.limit : undefined));
|
|
24181
|
+
}
|
|
24182
|
+
|
|
24183
|
+
let index = buildSearchIndex([]);
|
|
24184
|
+
|
|
24185
|
+
function handleMessage(payload) {
|
|
24186
|
+
try {
|
|
24187
|
+
if (!payload || typeof payload !== 'object') {
|
|
24188
|
+
throw new Error('Invalid worker request payload');
|
|
24189
|
+
}
|
|
24190
|
+
|
|
24191
|
+
if (payload.type === 'init' || payload.type === 'replaceAll') {
|
|
24192
|
+
index = buildSearchIndex(Array.isArray(payload.docs) ? payload.docs : []);
|
|
24193
|
+
return { id: payload.id, type: 'ok' };
|
|
24194
|
+
}
|
|
24195
|
+
|
|
24196
|
+
if (payload.type === 'search') {
|
|
24197
|
+
const hits = searchIndex(index, payload.query || { query: '' });
|
|
24198
|
+
return { id: payload.id, type: 'results', hits };
|
|
24199
|
+
}
|
|
24200
|
+
|
|
24201
|
+
if (payload.type === 'dispose') {
|
|
24202
|
+
index = buildSearchIndex([]);
|
|
24203
|
+
return { id: payload.id, type: 'ok' };
|
|
24204
|
+
}
|
|
24205
|
+
|
|
24206
|
+
throw new Error('Unsupported worker request type');
|
|
24207
|
+
} catch (error) {
|
|
24208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24209
|
+
return { id: payload?.id ?? 'unknown', type: 'error', message };
|
|
24210
|
+
}
|
|
24211
|
+
}
|
|
24212
|
+
`;
|
|
24213
|
+
function buildNodeWorkerSource() {
|
|
24214
|
+
return `${sharedRuntimeSource}\nconst { parentPort } = require('node:worker_threads');\nif (!parentPort) { throw new Error('Missing parentPort'); }\nparentPort.on('message', (payload) => { parentPort.postMessage(handleMessage(payload)); });`;
|
|
23813
24215
|
}
|
|
23814
24216
|
|
|
23815
24217
|
//#endregion
|
|
23816
|
-
//#region ../server/src/
|
|
23817
|
-
|
|
23818
|
-
|
|
23819
|
-
|
|
23820
|
-
|
|
23821
|
-
|
|
23822
|
-
|
|
23823
|
-
|
|
23824
|
-
|
|
23825
|
-
|
|
23826
|
-
|
|
23827
|
-
|
|
23828
|
-
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
23832
|
-
|
|
23833
|
-
|
|
23834
|
-
|
|
23835
|
-
|
|
23836
|
-
|
|
23837
|
-
|
|
23838
|
-
|
|
23839
|
-
|
|
23840
|
-
|
|
23841
|
-
|
|
23842
|
-
|
|
23843
|
-
|
|
23844
|
-
|
|
23845
|
-
|
|
23846
|
-
|
|
23847
|
-
|
|
23848
|
-
|
|
23849
|
-
|
|
23850
|
-
|
|
23851
|
-
|
|
23852
|
-
|
|
23853
|
-
});
|
|
23854
|
-
if (!timeWindow) return [];
|
|
23855
|
-
const { windowStart, bucketMs, bucketEnds } = timeWindow;
|
|
23856
|
-
const sums = Array.from({ length: bucketEnds.length }, () => 0);
|
|
23857
|
-
const counts = Array.from({ length: bucketEnds.length }, () => 0);
|
|
23858
|
-
let baseline = 0;
|
|
23859
|
-
for (const event of events) {
|
|
23860
|
-
if (event.ts <= windowStart) {
|
|
23861
|
-
if (reducer === "sum-cumulative") baseline += event.value;
|
|
23862
|
-
continue;
|
|
23863
|
-
}
|
|
23864
|
-
const offset = event.ts - windowStart;
|
|
23865
|
-
const index = Math.max(0, Math.min(bucketEnds.length - 1, Math.ceil(offset / bucketMs) - 1));
|
|
23866
|
-
sums[index] += event.value;
|
|
23867
|
-
counts[index] += 1;
|
|
23868
|
-
}
|
|
23869
|
-
let cumulative = baseline;
|
|
23870
|
-
let carry = baseline !== 0 ? baseline : events[0].value;
|
|
23871
|
-
return bucketEnds.map((ts, index) => {
|
|
23872
|
-
if (reducer === "sum") return {
|
|
23873
|
-
ts,
|
|
23874
|
-
value: sums[index]
|
|
24218
|
+
//#region ../server/src/cli-stream-observable.ts
|
|
24219
|
+
/**
|
|
24220
|
+
* 创建安全的 CLI 流式 observable
|
|
24221
|
+
*
|
|
24222
|
+
* 解决的问题:
|
|
24223
|
+
* 1. 防止在 emit.complete() 之后调用 emit.next()(会导致 "Controller is already closed" 错误)
|
|
24224
|
+
* 2. 统一的错误处理,防止未捕获的异常导致服务器崩溃
|
|
24225
|
+
* 3. 确保取消时正确清理资源
|
|
24226
|
+
*
|
|
24227
|
+
* @param startStream 启动流的函数,接收 onEvent 回调,返回取消函数的 Promise
|
|
24228
|
+
*/
|
|
24229
|
+
function createCliStreamObservable(startStream) {
|
|
24230
|
+
return observable((emit) => {
|
|
24231
|
+
let cancel;
|
|
24232
|
+
let completed = false;
|
|
24233
|
+
/**
|
|
24234
|
+
* 安全的事件处理器
|
|
24235
|
+
* - 检查是否已完成,防止重复调用
|
|
24236
|
+
* - 使用 try-catch 防止异常导致服务器崩溃
|
|
24237
|
+
*/
|
|
24238
|
+
const safeEventHandler = (event) => {
|
|
24239
|
+
if (completed) return;
|
|
24240
|
+
try {
|
|
24241
|
+
emit.next(event);
|
|
24242
|
+
if (event.type === "exit") {
|
|
24243
|
+
completed = true;
|
|
24244
|
+
emit.complete();
|
|
24245
|
+
}
|
|
24246
|
+
} catch (err) {
|
|
24247
|
+
console.error("[CLI Stream] Error emitting event:", err);
|
|
24248
|
+
if (!completed) {
|
|
24249
|
+
completed = true;
|
|
24250
|
+
try {
|
|
24251
|
+
emit.error(err instanceof Error ? err : new Error(String(err)));
|
|
24252
|
+
} catch {}
|
|
24253
|
+
}
|
|
24254
|
+
}
|
|
23875
24255
|
};
|
|
23876
|
-
|
|
23877
|
-
|
|
23878
|
-
|
|
23879
|
-
|
|
23880
|
-
|
|
23881
|
-
|
|
23882
|
-
|
|
23883
|
-
|
|
23884
|
-
|
|
23885
|
-
|
|
23886
|
-
|
|
24256
|
+
startStream(safeEventHandler).then((cancelFn) => {
|
|
24257
|
+
cancel = cancelFn;
|
|
24258
|
+
}).catch((err) => {
|
|
24259
|
+
console.error("[CLI Stream] Error starting stream:", err);
|
|
24260
|
+
if (!completed) {
|
|
24261
|
+
completed = true;
|
|
24262
|
+
try {
|
|
24263
|
+
emit.error(err instanceof Error ? err : new Error(String(err)));
|
|
24264
|
+
} catch {}
|
|
24265
|
+
}
|
|
24266
|
+
});
|
|
24267
|
+
return () => {
|
|
24268
|
+
completed = true;
|
|
24269
|
+
cancel?.();
|
|
23887
24270
|
};
|
|
23888
24271
|
});
|
|
23889
24272
|
}
|
|
23890
|
-
function buildDashboardTimeTrends(options) {
|
|
23891
|
-
const pointLimit = clampPointLimit(options.pointLimit);
|
|
23892
|
-
const trends = createEmptyTrendSeries();
|
|
23893
|
-
for (const metric of DASHBOARD_METRIC_KEYS) {
|
|
23894
|
-
if (options.availability[metric].state !== "ok") continue;
|
|
23895
|
-
trends[metric] = bucketizeTrend(normalizeEvents(options.events[metric], pointLimit), options.reducers?.[metric] ?? "sum", options.rightEdgeTs);
|
|
23896
|
-
}
|
|
23897
|
-
return {
|
|
23898
|
-
trends,
|
|
23899
|
-
trendMeta: {
|
|
23900
|
-
pointLimit,
|
|
23901
|
-
lastUpdatedAt: options.timestamp
|
|
23902
|
-
}
|
|
23903
|
-
};
|
|
23904
|
-
}
|
|
23905
24273
|
|
|
23906
24274
|
//#endregion
|
|
23907
24275
|
//#region ../server/src/reactive-kv.ts
|
|
@@ -23980,133 +24348,55 @@ const reactiveKV = new ReactiveKV();
|
|
|
23980
24348
|
*/
|
|
23981
24349
|
function createReactiveSubscription(task) {
|
|
23982
24350
|
return observable((emit) => {
|
|
23983
|
-
const context = new ReactiveContext();
|
|
23984
|
-
const controller = new AbortController();
|
|
23985
|
-
(async () => {
|
|
23986
|
-
try {
|
|
23987
|
-
for await (const data of context.stream(task, controller.signal)) emit.next(data);
|
|
23988
|
-
} catch (err) {
|
|
23989
|
-
if (!controller.signal.aborted) emit.error(err);
|
|
23990
|
-
}
|
|
23991
|
-
})();
|
|
23992
|
-
return () => {
|
|
23993
|
-
controller.abort();
|
|
23994
|
-
};
|
|
23995
|
-
});
|
|
23996
|
-
}
|
|
23997
|
-
/**
|
|
23998
|
-
* 创建带输入参数的响应式订阅
|
|
23999
|
-
*
|
|
24000
|
-
* @param task 接收输入参数的异步任务
|
|
24001
|
-
* @returns 返回一个函数,接收输入参数并返回 tRPC observable
|
|
24002
|
-
*
|
|
24003
|
-
* @example
|
|
24004
|
-
* ```typescript
|
|
24005
|
-
* // 在 router 中使用
|
|
24006
|
-
* subscribeOne: publicProcedure
|
|
24007
|
-
* .input(z.object({ id: z.string() }))
|
|
24008
|
-
* .subscription(({ ctx, input }) => {
|
|
24009
|
-
* return createReactiveSubscriptionWithInput(
|
|
24010
|
-
* (id: string) => ctx.adapter.readSpec(id)
|
|
24011
|
-
* )(input.id)
|
|
24012
|
-
* })
|
|
24013
|
-
* ```
|
|
24014
|
-
*/
|
|
24015
|
-
function createReactiveSubscriptionWithInput(task) {
|
|
24016
|
-
return (input) => {
|
|
24017
|
-
return createReactiveSubscription(() => task(input));
|
|
24018
|
-
};
|
|
24019
|
-
}
|
|
24020
|
-
|
|
24021
|
-
//#endregion
|
|
24022
|
-
//#region ../server/src/router.ts
|
|
24023
|
-
const t = initTRPC.context().create();
|
|
24024
|
-
const router = t.router;
|
|
24025
|
-
const publicProcedure = t.procedure;
|
|
24026
|
-
const execFileAsync = promisify$1(execFile);
|
|
24027
|
-
const OPSX_CORE_PROFILE_WORKFLOWS = [
|
|
24028
|
-
"propose",
|
|
24029
|
-
"explore",
|
|
24030
|
-
"apply",
|
|
24031
|
-
"archive"
|
|
24032
|
-
];
|
|
24033
|
-
const dashboardGitTaskStatusEmitter = new EventEmitter$1();
|
|
24034
|
-
dashboardGitTaskStatusEmitter.setMaxListeners(200);
|
|
24035
|
-
const dashboardGitTaskStatus = {
|
|
24036
|
-
running: false,
|
|
24037
|
-
inFlight: 0,
|
|
24038
|
-
lastStartedAt: null,
|
|
24039
|
-
lastFinishedAt: null,
|
|
24040
|
-
lastReason: null,
|
|
24041
|
-
lastError: null
|
|
24042
|
-
};
|
|
24043
|
-
function getDashboardGitTaskStatus() {
|
|
24044
|
-
return { ...dashboardGitTaskStatus };
|
|
24045
|
-
}
|
|
24046
|
-
function emitDashboardGitTaskStatus() {
|
|
24047
|
-
dashboardGitTaskStatusEmitter.emit("change", getDashboardGitTaskStatus());
|
|
24048
|
-
}
|
|
24049
|
-
function beginDashboardGitTask(reason) {
|
|
24050
|
-
dashboardGitTaskStatus.inFlight += 1;
|
|
24051
|
-
dashboardGitTaskStatus.running = true;
|
|
24052
|
-
dashboardGitTaskStatus.lastStartedAt = Date.now();
|
|
24053
|
-
dashboardGitTaskStatus.lastReason = reason;
|
|
24054
|
-
dashboardGitTaskStatus.lastError = null;
|
|
24055
|
-
emitDashboardGitTaskStatus();
|
|
24056
|
-
}
|
|
24057
|
-
function endDashboardGitTask(error) {
|
|
24058
|
-
dashboardGitTaskStatus.inFlight = Math.max(0, dashboardGitTaskStatus.inFlight - 1);
|
|
24059
|
-
dashboardGitTaskStatus.running = dashboardGitTaskStatus.inFlight > 0;
|
|
24060
|
-
dashboardGitTaskStatus.lastFinishedAt = Date.now();
|
|
24061
|
-
if (error) dashboardGitTaskStatus.lastError = error instanceof Error ? error.message : String(error);
|
|
24062
|
-
emitDashboardGitTaskStatus();
|
|
24063
|
-
}
|
|
24064
|
-
const DASHBOARD_GIT_REFRESH_STAMP_NAME = "openspecui-dashboard-git-refresh.stamp";
|
|
24065
|
-
async function resolveGitMetadataDir(projectDir) {
|
|
24066
|
-
try {
|
|
24067
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-dir"], {
|
|
24068
|
-
cwd: projectDir,
|
|
24069
|
-
maxBuffer: 1024 * 1024,
|
|
24070
|
-
encoding: "utf8"
|
|
24071
|
-
});
|
|
24072
|
-
const gitDirRaw = stdout.trim();
|
|
24073
|
-
if (!gitDirRaw) return null;
|
|
24074
|
-
const gitDirPath = resolve$1(projectDir, gitDirRaw);
|
|
24075
|
-
if (!(await stat(gitDirPath)).isDirectory()) return null;
|
|
24076
|
-
return gitDirPath;
|
|
24077
|
-
} catch {
|
|
24078
|
-
return null;
|
|
24079
|
-
}
|
|
24080
|
-
}
|
|
24081
|
-
async function resolveGitMetadataDirReactive(projectDir) {
|
|
24082
|
-
const gitMetadataDir = await resolveGitMetadataDir(projectDir);
|
|
24083
|
-
if (!gitMetadataDir) return null;
|
|
24084
|
-
await reactiveReadDir(gitMetadataDir, { includeHidden: true });
|
|
24085
|
-
return gitMetadataDir;
|
|
24086
|
-
}
|
|
24087
|
-
function getDashboardGitRefreshStampPath(gitMetadataDir) {
|
|
24088
|
-
return join$1(gitMetadataDir, DASHBOARD_GIT_REFRESH_STAMP_NAME);
|
|
24089
|
-
}
|
|
24090
|
-
async function touchDashboardGitRefreshStamp(projectDir, reason) {
|
|
24091
|
-
const gitMetadataDir = await resolveGitMetadataDir(projectDir);
|
|
24092
|
-
if (!gitMetadataDir) return { skipped: true };
|
|
24093
|
-
const stampPath = getDashboardGitRefreshStampPath(gitMetadataDir);
|
|
24094
|
-
await mkdir$1(dirname$1(stampPath), { recursive: true });
|
|
24095
|
-
await writeFile$1(stampPath, `${Date.now()} ${reason}\n`, "utf8");
|
|
24096
|
-
return { skipped: false };
|
|
24097
|
-
}
|
|
24098
|
-
async function registerDashboardGitReactiveDeps(projectDir) {
|
|
24099
|
-
await reactiveReadDir(projectDir, {
|
|
24100
|
-
includeHidden: true,
|
|
24101
|
-
exclude: ["node_modules"]
|
|
24351
|
+
const context = new ReactiveContext();
|
|
24352
|
+
const controller = new AbortController();
|
|
24353
|
+
(async () => {
|
|
24354
|
+
try {
|
|
24355
|
+
for await (const data of context.stream(task, controller.signal)) emit.next(data);
|
|
24356
|
+
} catch (err) {
|
|
24357
|
+
if (!controller.signal.aborted) emit.error(err);
|
|
24358
|
+
}
|
|
24359
|
+
})();
|
|
24360
|
+
return () => {
|
|
24361
|
+
controller.abort();
|
|
24362
|
+
};
|
|
24102
24363
|
});
|
|
24103
|
-
const gitMetadataDir = await resolveGitMetadataDirReactive(projectDir);
|
|
24104
|
-
if (!gitMetadataDir) return;
|
|
24105
|
-
await reactiveReadFile(getDashboardGitRefreshStampPath(gitMetadataDir));
|
|
24106
|
-
await reactiveReadFile(join$1(gitMetadataDir, "HEAD"));
|
|
24107
|
-
await reactiveReadFile(join$1(gitMetadataDir, "index"));
|
|
24108
|
-
await reactiveReadFile(join$1(gitMetadataDir, "packed-refs"));
|
|
24109
24364
|
}
|
|
24365
|
+
/**
|
|
24366
|
+
* 创建带输入参数的响应式订阅
|
|
24367
|
+
*
|
|
24368
|
+
* @param task 接收输入参数的异步任务
|
|
24369
|
+
* @returns 返回一个函数,接收输入参数并返回 tRPC observable
|
|
24370
|
+
*
|
|
24371
|
+
* @example
|
|
24372
|
+
* ```typescript
|
|
24373
|
+
* // 在 router 中使用
|
|
24374
|
+
* subscribeOne: publicProcedure
|
|
24375
|
+
* .input(z.object({ id: z.string() }))
|
|
24376
|
+
* .subscription(({ ctx, input }) => {
|
|
24377
|
+
* return createReactiveSubscriptionWithInput(
|
|
24378
|
+
* (id: string) => ctx.adapter.readSpec(id)
|
|
24379
|
+
* )(input.id)
|
|
24380
|
+
* })
|
|
24381
|
+
* ```
|
|
24382
|
+
*/
|
|
24383
|
+
function createReactiveSubscriptionWithInput(task) {
|
|
24384
|
+
return (input) => {
|
|
24385
|
+
return createReactiveSubscription(() => task(input));
|
|
24386
|
+
};
|
|
24387
|
+
}
|
|
24388
|
+
|
|
24389
|
+
//#endregion
|
|
24390
|
+
//#region ../server/src/router.ts
|
|
24391
|
+
const t = initTRPC.context().create();
|
|
24392
|
+
const router = t.router;
|
|
24393
|
+
const publicProcedure = t.procedure;
|
|
24394
|
+
const OPSX_CORE_PROFILE_WORKFLOWS = [
|
|
24395
|
+
"propose",
|
|
24396
|
+
"explore",
|
|
24397
|
+
"apply",
|
|
24398
|
+
"archive"
|
|
24399
|
+
];
|
|
24110
24400
|
function requireChangeId(changeId) {
|
|
24111
24401
|
if (!changeId) throw new Error("change is required");
|
|
24112
24402
|
return changeId;
|
|
@@ -24276,200 +24566,6 @@ function buildSystemStatus(ctx) {
|
|
|
24276
24566
|
watcherLastReinitializeReason: runtime?.lastReinitializeReason ?? null
|
|
24277
24567
|
};
|
|
24278
24568
|
}
|
|
24279
|
-
function resolveTrendTimestamp(primary, secondary) {
|
|
24280
|
-
if (typeof primary === "number" && Number.isFinite(primary) && primary > 0) return primary;
|
|
24281
|
-
if (typeof secondary === "number" && Number.isFinite(secondary) && secondary > 0) return secondary;
|
|
24282
|
-
return null;
|
|
24283
|
-
}
|
|
24284
|
-
function parseDatedIdTimestamp(id) {
|
|
24285
|
-
const match$1 = /^(\d{4})-(\d{2})-(\d{2})(?:-|$)/.exec(id);
|
|
24286
|
-
if (!match$1) return null;
|
|
24287
|
-
const year = Number(match$1[1]);
|
|
24288
|
-
const month = Number(match$1[2]);
|
|
24289
|
-
const day = Number(match$1[3]);
|
|
24290
|
-
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return null;
|
|
24291
|
-
if (month < 1 || month > 12) return null;
|
|
24292
|
-
if (day < 1 || day > 31) return null;
|
|
24293
|
-
const ts = Date.UTC(year, month - 1, day);
|
|
24294
|
-
return Number.isFinite(ts) ? ts : null;
|
|
24295
|
-
}
|
|
24296
|
-
function createEmptyTriColorTrends() {
|
|
24297
|
-
return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
|
|
24298
|
-
}
|
|
24299
|
-
async function readLatestCommitTimestamp(projectDir) {
|
|
24300
|
-
try {
|
|
24301
|
-
const { stdout } = await execFileAsync("git", [
|
|
24302
|
-
"log",
|
|
24303
|
-
"-1",
|
|
24304
|
-
"--format=%ct"
|
|
24305
|
-
], {
|
|
24306
|
-
cwd: projectDir,
|
|
24307
|
-
maxBuffer: 1024 * 1024,
|
|
24308
|
-
encoding: "utf8"
|
|
24309
|
-
});
|
|
24310
|
-
const seconds = Number(stdout.trim());
|
|
24311
|
-
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : null;
|
|
24312
|
-
} catch {
|
|
24313
|
-
return null;
|
|
24314
|
-
}
|
|
24315
|
-
}
|
|
24316
|
-
async function fetchDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
24317
|
-
if (contextStorage.getStore()) await registerDashboardGitReactiveDeps(ctx.projectDir);
|
|
24318
|
-
const now = Date.now();
|
|
24319
|
-
const [specMetas, changeMetas, archiveMetas] = await Promise.all([
|
|
24320
|
-
ctx.adapter.listSpecsWithMeta(),
|
|
24321
|
-
ctx.adapter.listChangesWithMeta(),
|
|
24322
|
-
ctx.adapter.listArchivedChangesWithMeta()
|
|
24323
|
-
]);
|
|
24324
|
-
const activeChanges = changeMetas.map((changeMeta) => ({
|
|
24325
|
-
id: changeMeta.id,
|
|
24326
|
-
name: changeMeta.name ?? changeMeta.id,
|
|
24327
|
-
progress: changeMeta.progress,
|
|
24328
|
-
updatedAt: changeMeta.updatedAt
|
|
24329
|
-
})).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
24330
|
-
const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
|
|
24331
|
-
const change = await ctx.adapter.readArchivedChange(meta.id);
|
|
24332
|
-
if (!change) return null;
|
|
24333
|
-
return {
|
|
24334
|
-
id: meta.id,
|
|
24335
|
-
createdAt: meta.createdAt,
|
|
24336
|
-
updatedAt: meta.updatedAt,
|
|
24337
|
-
tasksCompleted: change.tasks.filter((task) => task.completed).length
|
|
24338
|
-
};
|
|
24339
|
-
}))).filter((item) => item !== null);
|
|
24340
|
-
const specifications = (await Promise.all(specMetas.map(async (meta) => {
|
|
24341
|
-
const spec = await ctx.adapter.readSpec(meta.id);
|
|
24342
|
-
if (!spec) return null;
|
|
24343
|
-
return {
|
|
24344
|
-
id: meta.id,
|
|
24345
|
-
name: meta.name,
|
|
24346
|
-
requirements: spec.requirements.length,
|
|
24347
|
-
updatedAt: meta.updatedAt
|
|
24348
|
-
};
|
|
24349
|
-
}))).filter((item) => item !== null).sort((a, b) => b.requirements - a.requirements || b.updatedAt - a.updatedAt);
|
|
24350
|
-
const requirements = specifications.reduce((sum, spec) => sum + spec.requirements, 0);
|
|
24351
|
-
const tasksTotal = activeChanges.reduce((sum, change) => sum + change.progress.total, 0);
|
|
24352
|
-
const tasksCompleted = activeChanges.reduce((sum, change) => sum + change.progress.completed, 0);
|
|
24353
|
-
const archivedTasksCompleted = archivedChanges.reduce((sum, change) => sum + change.tasksCompleted, 0);
|
|
24354
|
-
const taskCompletionPercent = tasksTotal > 0 ? Math.round(tasksCompleted / tasksTotal * 100) : null;
|
|
24355
|
-
const inProgressChanges = activeChanges.filter((change) => change.progress.total > 0 && change.progress.completed < change.progress.total).length;
|
|
24356
|
-
const specificationTrendEvents = specMetas.flatMap((spec) => {
|
|
24357
|
-
const ts = resolveTrendTimestamp(spec.createdAt, spec.updatedAt);
|
|
24358
|
-
return ts === null ? [] : [{
|
|
24359
|
-
ts,
|
|
24360
|
-
value: 1
|
|
24361
|
-
}];
|
|
24362
|
-
});
|
|
24363
|
-
const completedTrendEvents = archivedChanges.flatMap((archive) => {
|
|
24364
|
-
const ts = parseDatedIdTimestamp(archive.id) ?? resolveTrendTimestamp(archive.updatedAt, archive.createdAt);
|
|
24365
|
-
return ts === null ? [] : [{
|
|
24366
|
-
ts,
|
|
24367
|
-
value: archive.tasksCompleted
|
|
24368
|
-
}];
|
|
24369
|
-
});
|
|
24370
|
-
const specMetaById = new Map(specMetas.map((meta) => [meta.id, meta]));
|
|
24371
|
-
const requirementTrendEvents = specifications.flatMap((spec) => {
|
|
24372
|
-
const meta = specMetaById.get(spec.id);
|
|
24373
|
-
const ts = resolveTrendTimestamp(meta?.updatedAt, meta?.createdAt);
|
|
24374
|
-
return ts === null ? [] : [{
|
|
24375
|
-
ts,
|
|
24376
|
-
value: spec.requirements
|
|
24377
|
-
}];
|
|
24378
|
-
});
|
|
24379
|
-
const hasObjectiveSpecificationTrend = specificationTrendEvents.length > 0 || specifications.length === 0;
|
|
24380
|
-
const hasObjectiveRequirementTrend = requirementTrendEvents.length > 0 || requirements === 0;
|
|
24381
|
-
const hasObjectiveCompletedTrend = completedTrendEvents.length > 0 || archiveMetas.length === 0;
|
|
24382
|
-
const config = await ctx.configManager.readConfig();
|
|
24383
|
-
beginDashboardGitTask(reason);
|
|
24384
|
-
let latestCommitTs = null;
|
|
24385
|
-
let git;
|
|
24386
|
-
try {
|
|
24387
|
-
const gitSnapshotPromise = buildDashboardGitSnapshot({ projectDir: ctx.projectDir }).catch(() => ({
|
|
24388
|
-
defaultBranch: "main",
|
|
24389
|
-
worktrees: []
|
|
24390
|
-
}));
|
|
24391
|
-
latestCommitTs = await readLatestCommitTimestamp(ctx.projectDir);
|
|
24392
|
-
git = await gitSnapshotPromise;
|
|
24393
|
-
} catch (error) {
|
|
24394
|
-
endDashboardGitTask(error);
|
|
24395
|
-
throw error;
|
|
24396
|
-
}
|
|
24397
|
-
endDashboardGitTask(null);
|
|
24398
|
-
const cardAvailability = {
|
|
24399
|
-
specifications: hasObjectiveSpecificationTrend ? { state: "ok" } : {
|
|
24400
|
-
state: "invalid",
|
|
24401
|
-
reason: "objective-history-unavailable"
|
|
24402
|
-
},
|
|
24403
|
-
requirements: hasObjectiveRequirementTrend ? { state: "ok" } : {
|
|
24404
|
-
state: "invalid",
|
|
24405
|
-
reason: "objective-history-unavailable"
|
|
24406
|
-
},
|
|
24407
|
-
activeChanges: {
|
|
24408
|
-
state: "invalid",
|
|
24409
|
-
reason: "objective-history-unavailable"
|
|
24410
|
-
},
|
|
24411
|
-
inProgressChanges: {
|
|
24412
|
-
state: "invalid",
|
|
24413
|
-
reason: "objective-history-unavailable"
|
|
24414
|
-
},
|
|
24415
|
-
completedChanges: hasObjectiveCompletedTrend ? { state: "ok" } : {
|
|
24416
|
-
state: "invalid",
|
|
24417
|
-
reason: "objective-history-unavailable"
|
|
24418
|
-
},
|
|
24419
|
-
taskCompletionPercent: {
|
|
24420
|
-
state: "invalid",
|
|
24421
|
-
reason: taskCompletionPercent === null ? "semantic-uncomputable" : "objective-history-unavailable"
|
|
24422
|
-
}
|
|
24423
|
-
};
|
|
24424
|
-
const trendKinds = {
|
|
24425
|
-
specifications: "monotonic",
|
|
24426
|
-
requirements: "monotonic",
|
|
24427
|
-
activeChanges: "bidirectional",
|
|
24428
|
-
inProgressChanges: "bidirectional",
|
|
24429
|
-
completedChanges: "monotonic",
|
|
24430
|
-
taskCompletionPercent: "bidirectional"
|
|
24431
|
-
};
|
|
24432
|
-
const { trends: baselineTrends, trendMeta } = buildDashboardTimeTrends({
|
|
24433
|
-
pointLimit: config.dashboard.trendPointLimit,
|
|
24434
|
-
timestamp: now,
|
|
24435
|
-
rightEdgeTs: latestCommitTs,
|
|
24436
|
-
availability: cardAvailability,
|
|
24437
|
-
events: {
|
|
24438
|
-
specifications: specificationTrendEvents,
|
|
24439
|
-
requirements: requirementTrendEvents,
|
|
24440
|
-
activeChanges: [],
|
|
24441
|
-
inProgressChanges: [],
|
|
24442
|
-
completedChanges: completedTrendEvents,
|
|
24443
|
-
taskCompletionPercent: []
|
|
24444
|
-
},
|
|
24445
|
-
reducers: {
|
|
24446
|
-
specifications: "sum",
|
|
24447
|
-
requirements: "sum",
|
|
24448
|
-
completedChanges: "sum"
|
|
24449
|
-
}
|
|
24450
|
-
});
|
|
24451
|
-
return {
|
|
24452
|
-
summary: {
|
|
24453
|
-
specifications: specifications.length,
|
|
24454
|
-
requirements,
|
|
24455
|
-
activeChanges: activeChanges.length,
|
|
24456
|
-
inProgressChanges,
|
|
24457
|
-
completedChanges: archiveMetas.length,
|
|
24458
|
-
archivedTasksCompleted,
|
|
24459
|
-
tasksTotal,
|
|
24460
|
-
tasksCompleted,
|
|
24461
|
-
taskCompletionPercent
|
|
24462
|
-
},
|
|
24463
|
-
trends: baselineTrends,
|
|
24464
|
-
triColorTrends: createEmptyTriColorTrends(),
|
|
24465
|
-
trendKinds,
|
|
24466
|
-
cardAvailability,
|
|
24467
|
-
trendMeta,
|
|
24468
|
-
specifications,
|
|
24469
|
-
activeChanges,
|
|
24470
|
-
git
|
|
24471
|
-
};
|
|
24472
|
-
}
|
|
24473
24569
|
/**
|
|
24474
24570
|
* Spec router - spec CRUD operations
|
|
24475
24571
|
*/
|
|
@@ -25171,30 +25267,52 @@ const systemRouter = router({
|
|
|
25171
25267
|
*/
|
|
25172
25268
|
const dashboardRouter = router({
|
|
25173
25269
|
get: publicProcedure.query(async ({ ctx }) => {
|
|
25174
|
-
return
|
|
25270
|
+
return ctx.dashboardOverviewService.getCurrent();
|
|
25175
25271
|
}),
|
|
25176
25272
|
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
25177
|
-
return
|
|
25178
|
-
|
|
25273
|
+
return observable((emit) => {
|
|
25274
|
+
const unsubscribe = ctx.dashboardOverviewService.subscribe((overview) => {
|
|
25275
|
+
emit.next(overview);
|
|
25276
|
+
}, {
|
|
25277
|
+
emitCurrent: true,
|
|
25278
|
+
onError: (error) => {
|
|
25279
|
+
emit.error(error);
|
|
25280
|
+
}
|
|
25281
|
+
});
|
|
25282
|
+
return () => {
|
|
25283
|
+
unsubscribe();
|
|
25284
|
+
};
|
|
25179
25285
|
});
|
|
25180
25286
|
}),
|
|
25181
25287
|
refreshGitSnapshot: publicProcedure.input(objectType({ reason: stringType().optional() }).optional()).mutation(async ({ ctx, input }) => {
|
|
25182
25288
|
const reason = input?.reason?.trim() || "manual-refresh";
|
|
25183
25289
|
await touchDashboardGitRefreshStamp(ctx.projectDir, reason);
|
|
25290
|
+
await ctx.dashboardOverviewService.refresh(reason);
|
|
25291
|
+
return { success: true };
|
|
25292
|
+
}),
|
|
25293
|
+
removeDetachedWorktree: publicProcedure.input(objectType({ path: stringType().min(1) })).mutation(async ({ ctx, input }) => {
|
|
25294
|
+
await ctx.dashboardOverviewService.getCurrent();
|
|
25295
|
+
await removeDetachedDashboardGitWorktree({
|
|
25296
|
+
projectDir: ctx.projectDir,
|
|
25297
|
+
targetPath: input.path
|
|
25298
|
+
});
|
|
25299
|
+
await touchDashboardGitRefreshStamp(ctx.projectDir, "remove-detached-worktree");
|
|
25300
|
+
await ctx.dashboardOverviewService.refresh("remove-detached-worktree");
|
|
25184
25301
|
return { success: true };
|
|
25185
25302
|
}),
|
|
25186
|
-
gitTaskStatus: publicProcedure.query(() => {
|
|
25303
|
+
gitTaskStatus: publicProcedure.query(async ({ ctx }) => {
|
|
25304
|
+
await ctx.dashboardOverviewService.getCurrent();
|
|
25187
25305
|
return getDashboardGitTaskStatus();
|
|
25188
25306
|
}),
|
|
25189
|
-
subscribeGitTaskStatus: publicProcedure.subscription(() => {
|
|
25307
|
+
subscribeGitTaskStatus: publicProcedure.subscription(({ ctx }) => {
|
|
25190
25308
|
return observable((emit) => {
|
|
25309
|
+
ctx.dashboardOverviewService.getCurrent().catch(() => {});
|
|
25191
25310
|
emit.next(getDashboardGitTaskStatus());
|
|
25192
|
-
const
|
|
25311
|
+
const unsubscribe = subscribeDashboardGitTaskStatus((status) => {
|
|
25193
25312
|
emit.next(status);
|
|
25194
|
-
};
|
|
25195
|
-
dashboardGitTaskStatusEmitter.on("change", handler);
|
|
25313
|
+
});
|
|
25196
25314
|
return () => {
|
|
25197
|
-
|
|
25315
|
+
unsubscribe();
|
|
25198
25316
|
};
|
|
25199
25317
|
});
|
|
25200
25318
|
})
|
|
@@ -25472,6 +25590,11 @@ function createServer$2(config) {
|
|
|
25472
25590
|
const kernel = config.kernel;
|
|
25473
25591
|
const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
|
|
25474
25592
|
const searchService = new SearchService(adapter, watcher);
|
|
25593
|
+
const dashboardOverviewService = new DashboardOverviewService((reason) => loadDashboardOverview({
|
|
25594
|
+
adapter,
|
|
25595
|
+
configManager,
|
|
25596
|
+
projectDir: config.projectDir
|
|
25597
|
+
}, reason), watcher);
|
|
25475
25598
|
const app = new Hono();
|
|
25476
25599
|
const corsOrigins = config.corsOrigins ?? ["http://localhost:5173", "http://localhost:3000"];
|
|
25477
25600
|
app.use("*", cors({
|
|
@@ -25498,6 +25621,7 @@ function createServer$2(config) {
|
|
|
25498
25621
|
cliExecutor,
|
|
25499
25622
|
kernel,
|
|
25500
25623
|
searchService,
|
|
25624
|
+
dashboardOverviewService,
|
|
25501
25625
|
watcher,
|
|
25502
25626
|
projectDir: config.projectDir
|
|
25503
25627
|
})
|
|
@@ -25509,6 +25633,7 @@ function createServer$2(config) {
|
|
|
25509
25633
|
cliExecutor,
|
|
25510
25634
|
kernel,
|
|
25511
25635
|
searchService,
|
|
25636
|
+
dashboardOverviewService,
|
|
25512
25637
|
watcher,
|
|
25513
25638
|
projectDir: config.projectDir
|
|
25514
25639
|
});
|
|
@@ -25519,6 +25644,7 @@ function createServer$2(config) {
|
|
|
25519
25644
|
cliExecutor,
|
|
25520
25645
|
kernel,
|
|
25521
25646
|
searchService,
|
|
25647
|
+
dashboardOverviewService,
|
|
25522
25648
|
watcher,
|
|
25523
25649
|
createContext,
|
|
25524
25650
|
port: config.port ?? 3100
|
|
@@ -25566,6 +25692,7 @@ async function createWebSocketServer(server, httpServer, config) {
|
|
|
25566
25692
|
wss.close();
|
|
25567
25693
|
server.watcher?.stop();
|
|
25568
25694
|
server.searchService.dispose().catch(() => {});
|
|
25695
|
+
server.dashboardOverviewService.dispose();
|
|
25569
25696
|
}
|
|
25570
25697
|
};
|
|
25571
25698
|
}
|
|
@@ -25601,6 +25728,9 @@ async function startServer(config, setupApp) {
|
|
|
25601
25728
|
server.searchService.init().catch((err) => {
|
|
25602
25729
|
console.error("Search service warmup failed:", err);
|
|
25603
25730
|
});
|
|
25731
|
+
server.dashboardOverviewService.init().catch((err) => {
|
|
25732
|
+
console.error("Dashboard overview warmup failed:", err);
|
|
25733
|
+
});
|
|
25604
25734
|
return {
|
|
25605
25735
|
url,
|
|
25606
25736
|
port,
|