openspecui 2.0.2 → 2.1.1
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 +272 -23
- package/dist/index.mjs +1 -1
- package/dist/{open-DDagk2eo.mjs → open-DfwCb8mL.mjs} +1 -1
- package/dist/{src-Cx7GJTGT.mjs → src-Nn_MJz41.mjs} +1460 -1313
- package/package.json +19 -13
- package/web/assets/{BufferResource-Cipj3hQ5.js → BufferResource-CSbEbiLP.js} +1 -1
- package/web/assets/{CanvasRenderer-C151fKcy.js → CanvasRenderer-CvpAMPzA.js} +1 -1
- package/web/assets/{Filter-BpxymkGv.js → Filter-BqwVCa6B.js} +1 -1
- package/web/assets/{RenderTargetSystem-C0zk81Mh.js → RenderTargetSystem-mD_Y2dUA.js} +1 -1
- package/web/assets/{WebGLRenderer-De3GrxdN.js → WebGLRenderer-DsvDwXyL.js} +1 -1
- package/web/assets/{WebGPURenderer-BkpejvST.js → WebGPURenderer-YcBYXAol.js} +1 -1
- package/web/assets/{browserAll-B8KqF-Xa.js → browserAll-MbWxSnea.js} +1 -1
- package/web/assets/{ghostty-web-BO5ww0eG.js → ghostty-web-cM8zjOdb.js} +1 -1
- package/web/assets/{index-Cj_nv8ue.js → index-B0lABsCj.js} +1 -1
- package/web/assets/{index-CwA_uPvf.js → index-B1J3yPM2.js} +1 -1
- package/web/assets/{index-CDX9fioY.js → index-BCMJS8eq.js} +1 -1
- package/web/assets/{index-CxDip8xJ.js → index-BMKX_bGe.js} +1 -1
- package/web/assets/{index-a8osabOh.js → index-BY40EQxT.js} +1 -1
- package/web/assets/{index-B-vvVL83.js → index-Cq4njHD4.js} +198 -193
- package/web/assets/{index-Bxm-n0do.js → index-Cr4OFm9E.js} +1 -1
- package/web/assets/{index-Cv-LON1K.js → index-CskIUxS-.js} +1 -1
- package/web/assets/{index-Nl7iD8Yi.js → index-D0sXMPhu.js} +1 -1
- package/web/assets/index-DHeR2UYq.css +1 -0
- package/web/assets/{index-DtTSWBHJ.js → index-DijRYB99.js} +1 -1
- package/web/assets/{index-DzK6gT7C.js → index-DuZDcW_P.js} +1 -1
- package/web/assets/{index-iROJdLzk.js → index-DyZwNTgF.js} +1 -1
- package/web/assets/{index-Bp2dpDFJ.js → index-JHrsE4PU.js} +1 -1
- package/web/assets/{index-DL08rl2O.js → index-KaodXPu0.js} +1 -1
- package/web/assets/{index-CFAzjIVm.js → index-TGQYcfbH.js} +1 -1
- package/web/assets/{index-2DXouwnE.js → index-kjHuAdn9.js} +1 -1
- package/web/assets/{webworkerAll-DPcI1cDK.js → webworkerAll-BE_Ec3Rk.js} +1 -1
- package/web/index.html +2 -2
- package/LICENSE +0 -21
- package/web/assets/index-aWxXp1oO.css +0 -1
|
@@ -8,18 +8,18 @@ import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
|
8
8
|
import { dirname, join } from "path";
|
|
9
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
10
|
import { mkdir as mkdir$1, readFile as readFile$1, readdir, rm, stat, writeFile as writeFile$1 } from "node:fs/promises";
|
|
11
|
-
import { dirname as dirname$1, join as join$1, matchesGlob, relative as relative$1, resolve as resolve$1, sep } from "node:path";
|
|
11
|
+
import { basename as basename$1, dirname as dirname$1, join as join$1, matchesGlob, relative as relative$1, resolve as resolve$1, sep } from "node:path";
|
|
12
12
|
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
14
|
import { watch } from "fs";
|
|
15
15
|
import { exec, spawn } from "child_process";
|
|
16
16
|
import { promisify } from "util";
|
|
17
|
-
import
|
|
18
|
-
import { execFile } from "node:child_process";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
19
18
|
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
19
|
+
import { execFile } from "node:child_process";
|
|
20
20
|
import { promisify as promisify$1 } from "node:util";
|
|
21
|
+
import * as pty from "@lydell/node-pty";
|
|
21
22
|
import { Worker as Worker$1 } from "node:worker_threads";
|
|
22
|
-
import { fileURLToPath } from "node:url";
|
|
23
23
|
|
|
24
24
|
//#region rolldown:runtime
|
|
25
25
|
var __create$1 = Object.create;
|
|
@@ -6308,6 +6308,7 @@ const OpenSpecUIConfigSchema = objectType({
|
|
|
6308
6308
|
}).default({}),
|
|
6309
6309
|
theme: enumType(THEME_VALUES).default("system"),
|
|
6310
6310
|
codeEditor: CodeEditorConfigSchema.default(CodeEditorConfigSchema.parse({})),
|
|
6311
|
+
appBaseUrl: stringType().default(""),
|
|
6311
6312
|
terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({})),
|
|
6312
6313
|
dashboard: DashboardConfigSchema.default(DashboardConfigSchema.parse({}))
|
|
6313
6314
|
});
|
|
@@ -6316,6 +6317,7 @@ const DEFAULT_CONFIG = {
|
|
|
6316
6317
|
cli: {},
|
|
6317
6318
|
theme: "system",
|
|
6318
6319
|
codeEditor: CodeEditorConfigSchema.parse({}),
|
|
6320
|
+
appBaseUrl: "",
|
|
6319
6321
|
terminal: TerminalConfigSchema.parse({}),
|
|
6320
6322
|
dashboard: DashboardConfigSchema.parse({})
|
|
6321
6323
|
};
|
|
@@ -6384,6 +6386,7 @@ var ConfigManager = class {
|
|
|
6384
6386
|
...current.codeEditor,
|
|
6385
6387
|
...config.codeEditor
|
|
6386
6388
|
},
|
|
6389
|
+
appBaseUrl: config.appBaseUrl ?? current.appBaseUrl,
|
|
6387
6390
|
terminal: {
|
|
6388
6391
|
...current.terminal,
|
|
6389
6392
|
...config.terminal
|
|
@@ -7134,6 +7137,37 @@ const DASHBOARD_METRIC_KEYS = [
|
|
|
7134
7137
|
"taskCompletionPercent"
|
|
7135
7138
|
];
|
|
7136
7139
|
|
|
7140
|
+
//#endregion
|
|
7141
|
+
//#region ../core/src/hosted-app.ts
|
|
7142
|
+
const OFFICIAL_APP_BASE_URL = "https://app.openspecui.com";
|
|
7143
|
+
function withHttpsProtocol(value) {
|
|
7144
|
+
if (/^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(value)) return value;
|
|
7145
|
+
return `https://${value}`;
|
|
7146
|
+
}
|
|
7147
|
+
function normalizeHostedAppBaseUrl(input) {
|
|
7148
|
+
const trimmed = input.trim();
|
|
7149
|
+
if (!trimmed) throw new Error("Hosted app base URL must not be empty");
|
|
7150
|
+
let parsed;
|
|
7151
|
+
try {
|
|
7152
|
+
parsed = new URL(withHttpsProtocol(trimmed));
|
|
7153
|
+
} catch (error) {
|
|
7154
|
+
throw new Error(`Invalid hosted app base URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
7155
|
+
}
|
|
7156
|
+
parsed.hash = "";
|
|
7157
|
+
parsed.search = "";
|
|
7158
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
7159
|
+
parsed.pathname = pathname.length > 0 ? pathname : "/";
|
|
7160
|
+
return parsed.toString().replace(/\/$/, parsed.pathname === "/" ? "" : "");
|
|
7161
|
+
}
|
|
7162
|
+
function resolveHostedAppBaseUrl(options) {
|
|
7163
|
+
return normalizeHostedAppBaseUrl(options.override?.trim() || options.configured?.trim() || OFFICIAL_APP_BASE_URL);
|
|
7164
|
+
}
|
|
7165
|
+
function buildHostedLaunchUrl(options) {
|
|
7166
|
+
const url = new URL(normalizeHostedAppBaseUrl(options.baseUrl));
|
|
7167
|
+
url.searchParams.set("api", options.apiBaseUrl);
|
|
7168
|
+
return url.toString();
|
|
7169
|
+
}
|
|
7170
|
+
|
|
7137
7171
|
//#endregion
|
|
7138
7172
|
//#region ../core/src/opsx-display-path.ts
|
|
7139
7173
|
const VIRTUAL_PROJECT_DIRNAME = "project";
|
|
@@ -22852,1022 +22886,1390 @@ var import_websocket = /* @__PURE__ */ __toESM$1(require_websocket(), 1);
|
|
|
22852
22886
|
var import_websocket_server = /* @__PURE__ */ __toESM$1(require_websocket_server(), 1);
|
|
22853
22887
|
|
|
22854
22888
|
//#endregion
|
|
22855
|
-
//#region ../server/src/
|
|
22856
|
-
const
|
|
22857
|
-
|
|
22858
|
-
|
|
22859
|
-
|
|
22860
|
-
|
|
22861
|
-
|
|
22862
|
-
|
|
22863
|
-
|
|
22864
|
-
|
|
22865
|
-
|
|
22866
|
-
|
|
22867
|
-
|
|
22868
|
-
|
|
22869
|
-
|
|
22870
|
-
command,
|
|
22871
|
-
args: opts.args ?? []
|
|
22872
|
-
};
|
|
22873
|
-
return {
|
|
22874
|
-
command: resolveDefaultShell(opts.platform, opts.env),
|
|
22875
|
-
args: []
|
|
22876
|
-
};
|
|
22877
|
-
}
|
|
22878
|
-
var PtySession = class extends EventEmitter {
|
|
22879
|
-
id;
|
|
22880
|
-
command;
|
|
22881
|
-
args;
|
|
22882
|
-
platform;
|
|
22883
|
-
closeTip;
|
|
22884
|
-
closeCallbackUrl;
|
|
22885
|
-
createdAt;
|
|
22886
|
-
process;
|
|
22887
|
-
titleInterval = null;
|
|
22888
|
-
lastTitle = "";
|
|
22889
|
-
buffer = [];
|
|
22890
|
-
bufferByteLength = 0;
|
|
22891
|
-
maxBufferLines;
|
|
22892
|
-
maxBufferBytes;
|
|
22893
|
-
isExited = false;
|
|
22894
|
-
exitCode = null;
|
|
22895
|
-
constructor(id, opts) {
|
|
22896
|
-
super();
|
|
22897
|
-
this.id = id;
|
|
22898
|
-
this.createdAt = Date.now();
|
|
22899
|
-
const resolvedCommand = resolvePtyCommand({
|
|
22900
|
-
platform: opts.platform,
|
|
22901
|
-
command: opts.command,
|
|
22902
|
-
args: opts.args,
|
|
22903
|
-
env: process.env
|
|
22904
|
-
});
|
|
22905
|
-
this.command = resolvedCommand.command;
|
|
22906
|
-
this.args = resolvedCommand.args;
|
|
22907
|
-
this.platform = opts.platform;
|
|
22908
|
-
this.closeTip = opts.closeTip;
|
|
22909
|
-
this.closeCallbackUrl = opts.closeCallbackUrl;
|
|
22910
|
-
this.maxBufferLines = opts.scrollback ?? DEFAULT_SCROLLBACK;
|
|
22911
|
-
this.maxBufferBytes = opts.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
22912
|
-
this.process = pty.spawn(this.command, this.args, {
|
|
22913
|
-
name: "xterm-256color",
|
|
22914
|
-
cols: opts.cols ?? 80,
|
|
22915
|
-
rows: opts.rows ?? 24,
|
|
22916
|
-
cwd: opts.cwd,
|
|
22917
|
-
env: {
|
|
22918
|
-
...process.env,
|
|
22919
|
-
TERM: "xterm-256color"
|
|
22920
|
-
}
|
|
22921
|
-
});
|
|
22922
|
-
this.process.onData((data) => {
|
|
22923
|
-
this.appendBuffer(data);
|
|
22924
|
-
this.emit("data", data);
|
|
22925
|
-
});
|
|
22926
|
-
this.process.onExit(({ exitCode }) => {
|
|
22927
|
-
if (this.titleInterval) {
|
|
22928
|
-
clearInterval(this.titleInterval);
|
|
22929
|
-
this.titleInterval = null;
|
|
22930
|
-
}
|
|
22931
|
-
this.isExited = true;
|
|
22932
|
-
this.exitCode = exitCode;
|
|
22933
|
-
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");
|
|
22934
22904
|
});
|
|
22935
|
-
this.titleInterval = setInterval(() => {
|
|
22936
|
-
try {
|
|
22937
|
-
const title = this.process.process;
|
|
22938
|
-
if (title && title !== this.lastTitle) {
|
|
22939
|
-
this.lastTitle = title;
|
|
22940
|
-
this.emit("title", title);
|
|
22941
|
-
}
|
|
22942
|
-
} catch {}
|
|
22943
|
-
}, 1e3);
|
|
22944
|
-
}
|
|
22945
|
-
get title() {
|
|
22946
|
-
return this.lastTitle;
|
|
22947
|
-
}
|
|
22948
|
-
appendBuffer(data) {
|
|
22949
|
-
let chunk = data;
|
|
22950
|
-
if (chunk.length > this.maxBufferBytes) chunk = chunk.slice(-this.maxBufferBytes);
|
|
22951
|
-
this.buffer.push(chunk);
|
|
22952
|
-
this.bufferByteLength += chunk.length;
|
|
22953
|
-
while (this.bufferByteLength > this.maxBufferBytes && this.buffer.length > 0) {
|
|
22954
|
-
const removed = this.buffer.shift();
|
|
22955
|
-
this.bufferByteLength -= removed.length;
|
|
22956
|
-
}
|
|
22957
|
-
while (this.buffer.length > this.maxBufferLines) {
|
|
22958
|
-
const removed = this.buffer.shift();
|
|
22959
|
-
this.bufferByteLength -= removed.length;
|
|
22960
|
-
}
|
|
22961
|
-
}
|
|
22962
|
-
getBuffer() {
|
|
22963
|
-
return this.buffer.join("");
|
|
22964
|
-
}
|
|
22965
|
-
write(data) {
|
|
22966
|
-
if (!this.isExited) this.process.write(data);
|
|
22967
22905
|
}
|
|
22968
|
-
|
|
22969
|
-
if (
|
|
22970
|
-
|
|
22971
|
-
|
|
22972
|
-
if (this.titleInterval) {
|
|
22973
|
-
clearInterval(this.titleInterval);
|
|
22974
|
-
this.titleInterval = null;
|
|
22975
|
-
}
|
|
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");
|
|
22976
22910
|
try {
|
|
22977
|
-
this.
|
|
22978
|
-
}
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
toInfo() {
|
|
22982
|
-
return {
|
|
22983
|
-
id: this.id,
|
|
22984
|
-
title: this.lastTitle,
|
|
22985
|
-
command: this.command,
|
|
22986
|
-
args: this.args,
|
|
22987
|
-
platform: this.platform,
|
|
22988
|
-
isExited: this.isExited,
|
|
22989
|
-
exitCode: this.exitCode,
|
|
22990
|
-
closeTip: this.closeTip,
|
|
22991
|
-
closeCallbackUrl: this.closeCallbackUrl,
|
|
22992
|
-
createdAt: this.createdAt
|
|
22993
|
-
};
|
|
22911
|
+
return await this.initPromise;
|
|
22912
|
+
} finally {
|
|
22913
|
+
this.initPromise = null;
|
|
22914
|
+
}
|
|
22994
22915
|
}
|
|
22995
|
-
|
|
22996
|
-
|
|
22997
|
-
|
|
22998
|
-
idCounter = 0;
|
|
22999
|
-
platform;
|
|
23000
|
-
constructor(defaultCwd) {
|
|
23001
|
-
this.defaultCwd = defaultCwd;
|
|
23002
|
-
this.platform = detectPtyPlatform();
|
|
22916
|
+
async getCurrent() {
|
|
22917
|
+
if (this.current) return this.current;
|
|
22918
|
+
return this.init();
|
|
23003
22919
|
}
|
|
23004
|
-
|
|
23005
|
-
|
|
23006
|
-
|
|
23007
|
-
|
|
23008
|
-
|
|
23009
|
-
command: opts.command,
|
|
23010
|
-
args: opts.args,
|
|
23011
|
-
closeTip: opts.closeTip,
|
|
23012
|
-
closeCallbackUrl: opts.closeCallbackUrl,
|
|
23013
|
-
cwd: this.defaultCwd,
|
|
23014
|
-
scrollback: opts.scrollback,
|
|
23015
|
-
maxBufferBytes: opts.maxBufferBytes,
|
|
23016
|
-
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)));
|
|
23017
22925
|
});
|
|
23018
|
-
|
|
23019
|
-
|
|
23020
|
-
|
|
23021
|
-
get(id) {
|
|
23022
|
-
return this.sessions.get(id);
|
|
23023
|
-
}
|
|
23024
|
-
list() {
|
|
23025
|
-
const result = [];
|
|
23026
|
-
for (const session of this.sessions.values()) result.push(session.toInfo());
|
|
23027
|
-
return result;
|
|
23028
|
-
}
|
|
23029
|
-
write(id, data) {
|
|
23030
|
-
this.sessions.get(id)?.write(data);
|
|
23031
|
-
}
|
|
23032
|
-
resize(id, cols, rows) {
|
|
23033
|
-
this.sessions.get(id)?.resize(cols, rows);
|
|
22926
|
+
return () => {
|
|
22927
|
+
this.emitter.off("change", listener);
|
|
22928
|
+
};
|
|
23034
22929
|
}
|
|
23035
|
-
|
|
23036
|
-
|
|
23037
|
-
|
|
23038
|
-
|
|
23039
|
-
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
|
+
}
|
|
23040
22959
|
}
|
|
23041
22960
|
}
|
|
23042
|
-
|
|
23043
|
-
|
|
23044
|
-
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;
|
|
23045
22969
|
}
|
|
23046
22970
|
};
|
|
23047
22971
|
|
|
23048
22972
|
//#endregion
|
|
23049
|
-
//#region ../server/src/
|
|
23050
|
-
|
|
23051
|
-
|
|
23052
|
-
|
|
23053
|
-
|
|
23054
|
-
|
|
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
|
|
23055
22990
|
};
|
|
23056
|
-
|
|
23057
|
-
|
|
23058
|
-
|
|
23059
|
-
|
|
23060
|
-
message,
|
|
23061
|
-
sessionId: opts?.sessionId
|
|
23062
|
-
});
|
|
22991
|
+
} catch {
|
|
22992
|
+
return {
|
|
22993
|
+
ok: false,
|
|
22994
|
+
stdout: ""
|
|
23063
22995
|
};
|
|
23064
|
-
|
|
23065
|
-
|
|
23066
|
-
|
|
23067
|
-
|
|
23068
|
-
|
|
23069
|
-
|
|
23070
|
-
|
|
23071
|
-
|
|
23072
|
-
|
|
23073
|
-
|
|
23074
|
-
|
|
23075
|
-
|
|
23076
|
-
|
|
23077
|
-
|
|
23078
|
-
|
|
23079
|
-
|
|
23080
|
-
|
|
23081
|
-
|
|
23082
|
-
|
|
23083
|
-
|
|
23084
|
-
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
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
|
|
23006
|
+
};
|
|
23007
|
+
}
|
|
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
|
+
};
|
|
23026
|
+
}
|
|
23027
|
+
function normalizeGitPath(path$1) {
|
|
23028
|
+
return path$1.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
23029
|
+
}
|
|
23030
|
+
function relativePath(fromDir, target) {
|
|
23031
|
+
const rel = relative$1(fromDir, target);
|
|
23032
|
+
if (!rel || rel.length === 0) return ".";
|
|
23033
|
+
return rel;
|
|
23034
|
+
}
|
|
23035
|
+
function parseBranchName(branchRef, detached) {
|
|
23036
|
+
if (detached) return "(detached)";
|
|
23037
|
+
if (!branchRef) return "(unknown)";
|
|
23038
|
+
return branchRef.replace(/^refs\/heads\//, "");
|
|
23039
|
+
}
|
|
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
|
|
23088
23055
|
};
|
|
23089
|
-
|
|
23090
|
-
|
|
23091
|
-
|
|
23092
|
-
|
|
23093
|
-
|
|
23094
|
-
|
|
23095
|
-
|
|
23096
|
-
|
|
23097
|
-
|
|
23098
|
-
|
|
23099
|
-
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23103
|
-
|
|
23104
|
-
|
|
23105
|
-
|
|
23106
|
-
|
|
23107
|
-
|
|
23108
|
-
|
|
23109
|
-
|
|
23110
|
-
|
|
23111
|
-
|
|
23112
|
-
|
|
23113
|
-
|
|
23114
|
-
|
|
23115
|
-
|
|
23116
|
-
|
|
23117
|
-
|
|
23118
|
-
|
|
23119
|
-
|
|
23120
|
-
|
|
23121
|
-
|
|
23122
|
-
|
|
23123
|
-
|
|
23124
|
-
|
|
23125
|
-
|
|
23126
|
-
|
|
23127
|
-
|
|
23128
|
-
|
|
23129
|
-
|
|
23130
|
-
|
|
23131
|
-
|
|
23132
|
-
|
|
23133
|
-
|
|
23134
|
-
|
|
23135
|
-
|
|
23136
|
-
|
|
23137
|
-
|
|
23138
|
-
|
|
23139
|
-
|
|
23140
|
-
|
|
23141
|
-
|
|
23142
|
-
|
|
23143
|
-
|
|
23144
|
-
|
|
23145
|
-
|
|
23146
|
-
|
|
23147
|
-
|
|
23148
|
-
|
|
23149
|
-
|
|
23150
|
-
|
|
23151
|
-
|
|
23152
|
-
|
|
23153
|
-
|
|
23154
|
-
|
|
23155
|
-
|
|
23156
|
-
|
|
23157
|
-
|
|
23158
|
-
|
|
23159
|
-
|
|
23160
|
-
|
|
23161
|
-
|
|
23162
|
-
|
|
23163
|
-
|
|
23164
|
-
|
|
23165
|
-
|
|
23166
|
-
|
|
23167
|
-
|
|
23168
|
-
|
|
23169
|
-
|
|
23170
|
-
case "list":
|
|
23171
|
-
send({
|
|
23172
|
-
type: "list",
|
|
23173
|
-
sessions: ptyManager.list().map((s) => ({
|
|
23174
|
-
id: s.id,
|
|
23175
|
-
title: s.title,
|
|
23176
|
-
command: s.command,
|
|
23177
|
-
args: s.args,
|
|
23178
|
-
platform: s.platform,
|
|
23179
|
-
isExited: s.isExited,
|
|
23180
|
-
exitCode: s.exitCode,
|
|
23181
|
-
closeTip: s.closeTip,
|
|
23182
|
-
closeCallbackUrl: s.closeCallbackUrl
|
|
23183
|
-
}))
|
|
23184
|
-
});
|
|
23185
|
-
break;
|
|
23186
|
-
case "input": {
|
|
23187
|
-
const session = ptyManager.get(msg.sessionId);
|
|
23188
|
-
if (!session) {
|
|
23189
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23190
|
-
break;
|
|
23191
|
-
}
|
|
23192
|
-
session.write(msg.data);
|
|
23193
|
-
break;
|
|
23194
|
-
}
|
|
23195
|
-
case "resize": {
|
|
23196
|
-
const session = ptyManager.get(msg.sessionId);
|
|
23197
|
-
if (!session) {
|
|
23198
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23199
|
-
break;
|
|
23200
|
-
}
|
|
23201
|
-
session.resize(msg.cols, msg.rows);
|
|
23202
|
-
break;
|
|
23203
|
-
}
|
|
23204
|
-
case "close": {
|
|
23205
|
-
const session = ptyManager.get(msg.sessionId);
|
|
23206
|
-
if (!session) {
|
|
23207
|
-
sendError("SESSION_NOT_FOUND", `Session not found: ${msg.sessionId}`, { sessionId: msg.sessionId });
|
|
23208
|
-
break;
|
|
23209
|
-
}
|
|
23210
|
-
cleanups.get(msg.sessionId)?.();
|
|
23211
|
-
ptyManager.close(session.id);
|
|
23212
|
-
break;
|
|
23213
|
-
}
|
|
23214
|
-
}
|
|
23215
|
-
});
|
|
23216
|
-
ws.on("close", () => {
|
|
23217
|
-
for (const cleanup of cleanups.values()) cleanup();
|
|
23218
|
-
cleanups.clear();
|
|
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;
|
|
23070
|
+
}
|
|
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));
|
|
23087
|
+
}
|
|
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";
|
|
23105
|
+
}
|
|
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
|
|
23219
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
|
|
23220
23262
|
};
|
|
23221
23263
|
}
|
|
23222
23264
|
|
|
23223
23265
|
//#endregion
|
|
23224
|
-
//#region ../
|
|
23225
|
-
const
|
|
23226
|
-
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
|
|
23230
|
-
|
|
23231
|
-
|
|
23232
|
-
|
|
23233
|
-
|
|
23234
|
-
|
|
23235
|
-
|
|
23236
|
-
|
|
23237
|
-
|
|
23238
|
-
|
|
23239
|
-
|
|
23240
|
-
|
|
23241
|
-
|
|
23242
|
-
|
|
23243
|
-
const
|
|
23244
|
-
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
|
|
23251
|
-
|
|
23252
|
-
})
|
|
23253
|
-
|
|
23254
|
-
|
|
23255
|
-
|
|
23256
|
-
|
|
23257
|
-
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
}
|
|
23264
|
-
|
|
23265
|
-
|
|
23266
|
-
|
|
23267
|
-
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
|
|
23272
|
-
|
|
23273
|
-
|
|
23274
|
-
|
|
23275
|
-
|
|
23276
|
-
|
|
23277
|
-
|
|
23278
|
-
|
|
23279
|
-
|
|
23280
|
-
|
|
23281
|
-
|
|
23282
|
-
|
|
23283
|
-
|
|
23284
|
-
|
|
23285
|
-
|
|
23286
|
-
|
|
23287
|
-
|
|
23288
|
-
|
|
23289
|
-
|
|
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)));
|
|
23275
|
+
}
|
|
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
|
+
};
|
|
23354
|
+
}
|
|
23290
23355
|
|
|
23291
23356
|
//#endregion
|
|
23292
|
-
//#region ../
|
|
23293
|
-
const
|
|
23294
|
-
const
|
|
23295
|
-
const
|
|
23296
|
-
|
|
23297
|
-
|
|
23298
|
-
|
|
23299
|
-
|
|
23300
|
-
|
|
23301
|
-
|
|
23302
|
-
|
|
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, []]));
|
|
23303
23372
|
}
|
|
23304
|
-
|
|
23305
|
-
|
|
23306
|
-
|
|
23307
|
-
|
|
23308
|
-
.map((term) => term.trim())
|
|
23309
|
-
.filter((term) => term.length > 0);
|
|
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;
|
|
23310
23377
|
}
|
|
23311
|
-
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
|
|
23318
|
-
|
|
23319
|
-
|
|
23320
|
-
|
|
23321
|
-
|
|
23322
|
-
normalizedPath: normalizeText(doc.path),
|
|
23323
|
-
normalizedContent: normalizeText(doc.content),
|
|
23324
|
-
};
|
|
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;
|
|
23325
23389
|
}
|
|
23326
|
-
|
|
23327
|
-
|
|
23328
|
-
|
|
23329
|
-
|
|
23330
|
-
|
|
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
|
+
}
|
|
23331
23406
|
}
|
|
23332
|
-
|
|
23333
|
-
|
|
23334
|
-
if (typeof limit !== 'number' || Number.isNaN(limit)) return DEFAULT_LIMIT;
|
|
23335
|
-
return Math.min(MAX_LIMIT, Math.max(1, Math.trunc(limit)));
|
|
23407
|
+
function emitDashboardGitTaskStatus() {
|
|
23408
|
+
dashboardGitTaskStatusEmitter.emit("change", getDashboardGitTaskStatus());
|
|
23336
23409
|
}
|
|
23337
|
-
|
|
23338
|
-
|
|
23339
|
-
|
|
23340
|
-
|
|
23341
|
-
|
|
23342
|
-
|
|
23343
|
-
|
|
23344
|
-
);
|
|
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();
|
|
23345
23417
|
}
|
|
23346
|
-
|
|
23347
|
-
|
|
23348
|
-
|
|
23349
|
-
|
|
23350
|
-
|
|
23351
|
-
|
|
23352
|
-
if (doc.normalizedPath.includes(term)) score += 20;
|
|
23353
|
-
|
|
23354
|
-
const contentIdx = doc.normalizedContent.indexOf(term);
|
|
23355
|
-
if (contentIdx >= 0) {
|
|
23356
|
-
score += 8;
|
|
23357
|
-
if (contentIdx < 160) score += 4;
|
|
23358
|
-
}
|
|
23359
|
-
}
|
|
23360
|
-
|
|
23361
|
-
return score;
|
|
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();
|
|
23362
23424
|
}
|
|
23363
|
-
|
|
23364
|
-
|
|
23365
|
-
|
|
23366
|
-
|
|
23367
|
-
|
|
23368
|
-
|
|
23369
|
-
|
|
23370
|
-
|
|
23371
|
-
|
|
23372
|
-
|
|
23373
|
-
|
|
23374
|
-
|
|
23375
|
-
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23379
|
-
|
|
23380
|
-
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
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
|
+
};
|
|
23388
23616
|
}
|
|
23389
23617
|
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
hits.push({
|
|
23400
|
-
documentId: doc.id,
|
|
23401
|
-
kind: doc.kind,
|
|
23402
|
-
title: doc.title,
|
|
23403
|
-
href: doc.href,
|
|
23404
|
-
path: doc.path,
|
|
23405
|
-
score: scoreDocument(doc, terms),
|
|
23406
|
-
snippet: createSnippet(doc.content, terms),
|
|
23407
|
-
updatedAt: doc.updatedAt,
|
|
23408
|
-
});
|
|
23409
|
-
}
|
|
23410
|
-
|
|
23411
|
-
hits.sort((a, b) => {
|
|
23412
|
-
if (b.score !== a.score) return b.score - a.score;
|
|
23413
|
-
return b.updatedAt - a.updatedAt;
|
|
23414
|
-
});
|
|
23415
|
-
|
|
23416
|
-
return hits.slice(0, resolveLimit(query ? query.limit : undefined));
|
|
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";
|
|
23417
23626
|
}
|
|
23418
|
-
|
|
23419
|
-
|
|
23420
|
-
|
|
23421
|
-
function handleMessage(payload) {
|
|
23422
|
-
try {
|
|
23423
|
-
if (!payload || typeof payload !== 'object') {
|
|
23424
|
-
throw new Error('Invalid worker request payload');
|
|
23425
|
-
}
|
|
23426
|
-
|
|
23427
|
-
if (payload.type === 'init' || payload.type === 'replaceAll') {
|
|
23428
|
-
index = buildSearchIndex(Array.isArray(payload.docs) ? payload.docs : []);
|
|
23429
|
-
return { id: payload.id, type: 'ok' };
|
|
23430
|
-
}
|
|
23431
|
-
|
|
23432
|
-
if (payload.type === 'search') {
|
|
23433
|
-
const hits = searchIndex(index, payload.query || { query: '' });
|
|
23434
|
-
return { id: payload.id, type: 'results', hits };
|
|
23435
|
-
}
|
|
23436
|
-
|
|
23437
|
-
if (payload.type === 'dispose') {
|
|
23438
|
-
index = buildSearchIndex([]);
|
|
23439
|
-
return { id: payload.id, type: 'ok' };
|
|
23440
|
-
}
|
|
23441
|
-
|
|
23442
|
-
throw new Error('Unsupported worker request type');
|
|
23443
|
-
} catch (error) {
|
|
23444
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
23445
|
-
return { id: payload?.id ?? 'unknown', type: 'error', message };
|
|
23446
|
-
}
|
|
23627
|
+
function resolveDefaultShell(platform, env) {
|
|
23628
|
+
if (platform === "windows") return env.ComSpec?.trim() || "cmd.exe";
|
|
23629
|
+
return env.SHELL?.trim() || "/bin/sh";
|
|
23447
23630
|
}
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
|
|
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
|
+
};
|
|
23451
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
|
+
};
|
|
23452
23811
|
|
|
23453
23812
|
//#endregion
|
|
23454
|
-
//#region ../server/src/
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23458
|
-
|
|
23459
|
-
|
|
23460
|
-
|
|
23461
|
-
|
|
23462
|
-
|
|
23463
|
-
|
|
23464
|
-
|
|
23465
|
-
|
|
23466
|
-
|
|
23467
|
-
|
|
23468
|
-
|
|
23469
|
-
|
|
23470
|
-
|
|
23471
|
-
|
|
23472
|
-
|
|
23473
|
-
|
|
23474
|
-
|
|
23475
|
-
|
|
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;
|
|
23476
23865
|
try {
|
|
23477
|
-
|
|
23478
|
-
|
|
23479
|
-
|
|
23480
|
-
|
|
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;
|
|
23481
23958
|
}
|
|
23482
|
-
|
|
23483
|
-
|
|
23484
|
-
|
|
23485
|
-
|
|
23486
|
-
|
|
23487
|
-
|
|
23488
|
-
|
|
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;
|
|
23967
|
+
}
|
|
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;
|
|
23489
23977
|
}
|
|
23490
|
-
}
|
|
23491
|
-
};
|
|
23492
|
-
startStream(safeEventHandler).then((cancelFn) => {
|
|
23493
|
-
cancel = cancelFn;
|
|
23494
|
-
}).catch((err) => {
|
|
23495
|
-
console.error("[CLI Stream] Error starting stream:", err);
|
|
23496
|
-
if (!completed) {
|
|
23497
|
-
completed = true;
|
|
23498
|
-
try {
|
|
23499
|
-
emit.error(err instanceof Error ? err : new Error(String(err)));
|
|
23500
|
-
} catch {}
|
|
23501
23978
|
}
|
|
23502
23979
|
});
|
|
23503
|
-
|
|
23504
|
-
|
|
23505
|
-
|
|
23506
|
-
};
|
|
23507
|
-
});
|
|
23508
|
-
}
|
|
23509
|
-
|
|
23510
|
-
//#endregion
|
|
23511
|
-
//#region ../server/src/dashboard-git-snapshot.ts
|
|
23512
|
-
const execFileAsync$1 = promisify$1(execFile);
|
|
23513
|
-
const EMPTY_DIFF = {
|
|
23514
|
-
files: 0,
|
|
23515
|
-
insertions: 0,
|
|
23516
|
-
deletions: 0
|
|
23517
|
-
};
|
|
23518
|
-
async function defaultRunGit(cwd, args) {
|
|
23519
|
-
try {
|
|
23520
|
-
const { stdout } = await execFileAsync$1("git", args, {
|
|
23521
|
-
cwd,
|
|
23522
|
-
encoding: "utf8",
|
|
23523
|
-
maxBuffer: 8 * 1024 * 1024
|
|
23980
|
+
ws.on("close", () => {
|
|
23981
|
+
for (const cleanup of cleanups.values()) cleanup();
|
|
23982
|
+
cleanups.clear();
|
|
23524
23983
|
});
|
|
23525
|
-
return {
|
|
23526
|
-
ok: true,
|
|
23527
|
-
stdout
|
|
23528
|
-
};
|
|
23529
|
-
} catch {
|
|
23530
|
-
return {
|
|
23531
|
-
ok: false,
|
|
23532
|
-
stdout: ""
|
|
23533
|
-
};
|
|
23534
|
-
}
|
|
23535
|
-
}
|
|
23536
|
-
function parseShortStat(output) {
|
|
23537
|
-
const files = Number(/(\d+)\s+files? changed/.exec(output)?.[1] ?? 0);
|
|
23538
|
-
const insertions = Number(/(\d+)\s+insertions?\(\+\)/.exec(output)?.[1] ?? 0);
|
|
23539
|
-
const deletions = Number(/(\d+)\s+deletions?\(-\)/.exec(output)?.[1] ?? 0);
|
|
23540
|
-
return {
|
|
23541
|
-
files: Number.isFinite(files) ? files : 0,
|
|
23542
|
-
insertions: Number.isFinite(insertions) ? insertions : 0,
|
|
23543
|
-
deletions: Number.isFinite(deletions) ? deletions : 0
|
|
23544
|
-
};
|
|
23545
|
-
}
|
|
23546
|
-
function parseNumStat(output) {
|
|
23547
|
-
let files = 0;
|
|
23548
|
-
let insertions = 0;
|
|
23549
|
-
let deletions = 0;
|
|
23550
|
-
for (const line of output.split("\n")) {
|
|
23551
|
-
const trimmed = line.trim();
|
|
23552
|
-
if (!trimmed) continue;
|
|
23553
|
-
const [addRaw, deleteRaw] = trimmed.split(" ");
|
|
23554
|
-
if (!addRaw || !deleteRaw) continue;
|
|
23555
|
-
files += 1;
|
|
23556
|
-
if (addRaw !== "-") insertions += Number(addRaw) || 0;
|
|
23557
|
-
if (deleteRaw !== "-") deletions += Number(deleteRaw) || 0;
|
|
23558
|
-
}
|
|
23559
|
-
return {
|
|
23560
|
-
files,
|
|
23561
|
-
insertions,
|
|
23562
|
-
deletions
|
|
23563
23984
|
};
|
|
23564
23985
|
}
|
|
23565
|
-
|
|
23566
|
-
|
|
23567
|
-
|
|
23568
|
-
|
|
23569
|
-
|
|
23570
|
-
|
|
23571
|
-
|
|
23572
|
-
|
|
23573
|
-
|
|
23574
|
-
|
|
23575
|
-
|
|
23576
|
-
|
|
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();
|
|
23577
24067
|
}
|
|
23578
|
-
|
|
23579
|
-
|
|
23580
|
-
|
|
23581
|
-
|
|
23582
|
-
|
|
23583
|
-
|
|
23584
|
-
current = null;
|
|
23585
|
-
};
|
|
23586
|
-
for (const line of porcelain.split("\n")) {
|
|
23587
|
-
if (line.startsWith("worktree ")) {
|
|
23588
|
-
flush();
|
|
23589
|
-
current = {
|
|
23590
|
-
path: line.slice(9).trim(),
|
|
23591
|
-
branchRef: null,
|
|
23592
|
-
detached: false
|
|
23593
|
-
};
|
|
23594
|
-
continue;
|
|
23595
|
-
}
|
|
23596
|
-
if (!current) continue;
|
|
23597
|
-
if (line.startsWith("branch ")) {
|
|
23598
|
-
current.branchRef = line.slice(7).trim();
|
|
23599
|
-
continue;
|
|
23600
|
-
}
|
|
23601
|
-
if (line === "detached") {
|
|
23602
|
-
current.detached = true;
|
|
23603
|
-
continue;
|
|
23604
|
-
}
|
|
23605
|
-
}
|
|
23606
|
-
flush();
|
|
23607
|
-
return entries;
|
|
24068
|
+
|
|
24069
|
+
function splitTerms(query) {
|
|
24070
|
+
return normalizeText(query)
|
|
24071
|
+
.split(' ')
|
|
24072
|
+
.map((term) => term.trim())
|
|
24073
|
+
.filter((term) => term.length > 0);
|
|
23608
24074
|
}
|
|
23609
|
-
|
|
23610
|
-
|
|
23611
|
-
|
|
23612
|
-
|
|
23613
|
-
|
|
23614
|
-
|
|
23615
|
-
|
|
23616
|
-
|
|
23617
|
-
|
|
23618
|
-
|
|
23619
|
-
|
|
23620
|
-
|
|
23621
|
-
|
|
23622
|
-
|
|
23623
|
-
}
|
|
23624
|
-
return [...related].sort((a, b) => a.localeCompare(b));
|
|
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
|
+
};
|
|
23625
24089
|
}
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
23631
|
-
"refs/remotes/origin/HEAD"
|
|
23632
|
-
]);
|
|
23633
|
-
const remoteRef = remoteHead.stdout.trim();
|
|
23634
|
-
if (remoteHead.ok && remoteRef) return remoteRef;
|
|
23635
|
-
const localHead = await runGit(projectDir, [
|
|
23636
|
-
"rev-parse",
|
|
23637
|
-
"--abbrev-ref",
|
|
23638
|
-
"HEAD"
|
|
23639
|
-
]);
|
|
23640
|
-
const localRef = localHead.stdout.trim();
|
|
23641
|
-
if (localHead.ok && localRef && localRef !== "HEAD") return localRef;
|
|
23642
|
-
return "main";
|
|
24090
|
+
|
|
24091
|
+
function buildSearchIndex(docs) {
|
|
24092
|
+
return {
|
|
24093
|
+
documents: Array.isArray(docs) ? docs.map(toSearchIndexDocument) : [],
|
|
24094
|
+
};
|
|
23643
24095
|
}
|
|
23644
|
-
|
|
23645
|
-
|
|
23646
|
-
|
|
23647
|
-
|
|
23648
|
-
"log",
|
|
23649
|
-
"--format=%H%x1f%s",
|
|
23650
|
-
`-n${maxCommitEntries}`,
|
|
23651
|
-
`${defaultBranch}..HEAD`
|
|
23652
|
-
]);
|
|
23653
|
-
if (commits.ok) for (const line of commits.stdout.split("\n")) {
|
|
23654
|
-
if (!line.trim()) continue;
|
|
23655
|
-
const [hash, title = ""] = line.split("");
|
|
23656
|
-
if (!hash) continue;
|
|
23657
|
-
const diffResult = await runGit(worktreePath, [
|
|
23658
|
-
"show",
|
|
23659
|
-
"--numstat",
|
|
23660
|
-
"--format=",
|
|
23661
|
-
hash
|
|
23662
|
-
]);
|
|
23663
|
-
const changedFiles = (await runGit(worktreePath, [
|
|
23664
|
-
"show",
|
|
23665
|
-
"--name-only",
|
|
23666
|
-
"--format=",
|
|
23667
|
-
hash
|
|
23668
|
-
])).stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23669
|
-
entries.push({
|
|
23670
|
-
type: "commit",
|
|
23671
|
-
hash,
|
|
23672
|
-
title: title.trim() || hash.slice(0, 7),
|
|
23673
|
-
relatedChanges: parseRelatedChanges(changedFiles),
|
|
23674
|
-
diff: diffResult.ok ? parseNumStat(diffResult.stdout) : EMPTY_DIFF
|
|
23675
|
-
});
|
|
23676
|
-
}
|
|
23677
|
-
const trackedResult = await runGit(worktreePath, [
|
|
23678
|
-
"diff",
|
|
23679
|
-
"--numstat",
|
|
23680
|
-
"HEAD"
|
|
23681
|
-
]);
|
|
23682
|
-
const trackedFilesResult = await runGit(worktreePath, [
|
|
23683
|
-
"diff",
|
|
23684
|
-
"--name-only",
|
|
23685
|
-
"HEAD"
|
|
23686
|
-
]);
|
|
23687
|
-
const untrackedResult = await runGit(worktreePath, [
|
|
23688
|
-
"ls-files",
|
|
23689
|
-
"--others",
|
|
23690
|
-
"--exclude-standard"
|
|
23691
|
-
]);
|
|
23692
|
-
const trackedFiles = trackedFilesResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23693
|
-
const untrackedFiles = untrackedResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23694
|
-
const allUncommittedFiles = new Set([...trackedFiles, ...untrackedFiles]);
|
|
23695
|
-
const trackedDiff = trackedResult.ok ? parseNumStat(trackedResult.stdout) : EMPTY_DIFF;
|
|
23696
|
-
entries.push({
|
|
23697
|
-
type: "uncommitted",
|
|
23698
|
-
title: "Uncommitted",
|
|
23699
|
-
relatedChanges: parseRelatedChanges([...allUncommittedFiles]),
|
|
23700
|
-
diff: {
|
|
23701
|
-
files: allUncommittedFiles.size,
|
|
23702
|
-
insertions: trackedDiff.insertions,
|
|
23703
|
-
deletions: trackedDiff.deletions
|
|
23704
|
-
}
|
|
23705
|
-
});
|
|
23706
|
-
return entries;
|
|
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)));
|
|
23707
24100
|
}
|
|
23708
|
-
|
|
23709
|
-
|
|
23710
|
-
|
|
23711
|
-
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
|
|
23715
|
-
|
|
23716
|
-
`${defaultBranch}...HEAD`
|
|
23717
|
-
]);
|
|
23718
|
-
let ahead = 0;
|
|
23719
|
-
let behind = 0;
|
|
23720
|
-
if (aheadBehindResult.ok) {
|
|
23721
|
-
const [behindRaw, aheadRaw] = aheadBehindResult.stdout.trim().split(/\s+/);
|
|
23722
|
-
ahead = Number(aheadRaw) || 0;
|
|
23723
|
-
behind = Number(behindRaw) || 0;
|
|
23724
|
-
}
|
|
23725
|
-
const diffResult = await runGit(worktreePath, [
|
|
23726
|
-
"diff",
|
|
23727
|
-
"--shortstat",
|
|
23728
|
-
`${defaultBranch}...HEAD`
|
|
23729
|
-
]);
|
|
23730
|
-
const diff = diffResult.ok ? parseShortStat(diffResult.stdout) : EMPTY_DIFF;
|
|
23731
|
-
const entries = await collectCommitEntries({
|
|
23732
|
-
worktreePath,
|
|
23733
|
-
defaultBranch,
|
|
23734
|
-
maxCommitEntries,
|
|
23735
|
-
runGit
|
|
23736
|
-
});
|
|
23737
|
-
return {
|
|
23738
|
-
path: worktreePath,
|
|
23739
|
-
relativePath: relativePath(resolvedProjectDir, worktreePath),
|
|
23740
|
-
branchName: parseBranchName(worktree.branchRef, worktree.detached),
|
|
23741
|
-
isCurrent: resolvedProjectDir === worktreePath,
|
|
23742
|
-
ahead,
|
|
23743
|
-
behind,
|
|
23744
|
-
diff,
|
|
23745
|
-
entries
|
|
23746
|
-
};
|
|
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
|
+
);
|
|
23747
24109
|
}
|
|
23748
|
-
|
|
23749
|
-
|
|
23750
|
-
|
|
23751
|
-
|
|
23752
|
-
|
|
23753
|
-
|
|
23754
|
-
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
const worktrees = await Promise.all(baseWorktrees.map((worktree) => collectWorktree({
|
|
23765
|
-
projectDir: resolvedProjectDir,
|
|
23766
|
-
worktree,
|
|
23767
|
-
defaultBranch,
|
|
23768
|
-
runGit,
|
|
23769
|
-
maxCommitEntries
|
|
23770
|
-
})));
|
|
23771
|
-
worktrees.sort((a, b) => {
|
|
23772
|
-
if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1;
|
|
23773
|
-
return a.branchName.localeCompare(b.branchName);
|
|
23774
|
-
});
|
|
23775
|
-
return {
|
|
23776
|
-
defaultBranch,
|
|
23777
|
-
worktrees
|
|
23778
|
-
};
|
|
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;
|
|
23779
24126
|
}
|
|
23780
24127
|
|
|
23781
|
-
|
|
23782
|
-
|
|
23783
|
-
|
|
23784
|
-
|
|
23785
|
-
const
|
|
23786
|
-
|
|
23787
|
-
|
|
23788
|
-
|
|
23789
|
-
|
|
23790
|
-
|
|
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;
|
|
23791
24152
|
}
|
|
23792
|
-
|
|
23793
|
-
|
|
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));
|
|
23794
24181
|
}
|
|
23795
|
-
|
|
23796
|
-
|
|
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
|
+
}
|
|
23797
24211
|
}
|
|
23798
|
-
|
|
23799
|
-
|
|
23800
|
-
|
|
23801
|
-
const probeEnd = probeEvents[probeEvents.length - 1].ts;
|
|
23802
|
-
const end = typeof rightEdgeTs === "number" && Number.isFinite(rightEdgeTs) && rightEdgeTs > 0 ? Math.max(probeEnd, rightEdgeTs) : probeEnd;
|
|
23803
|
-
const probeStart = probeEvents[0].ts;
|
|
23804
|
-
const rangeMs = Math.max(1, end - probeStart);
|
|
23805
|
-
const bucketMs = rangeMs >= DAY_MS ? Math.max(DAY_MS, Math.ceil(rangeMs / targetBars / DAY_MS) * DAY_MS) : Math.max(1, Math.ceil(rangeMs / targetBars));
|
|
23806
|
-
const windowStart = end - bucketMs * targetBars;
|
|
23807
|
-
return {
|
|
23808
|
-
windowStart,
|
|
23809
|
-
bucketMs,
|
|
23810
|
-
bucketEnds: Array.from({ length: targetBars }, (_, index) => windowStart + bucketMs * (index + 1))
|
|
23811
|
-
};
|
|
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)); });`;
|
|
23812
24215
|
}
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
|
|
23816
|
-
|
|
23817
|
-
|
|
23818
|
-
|
|
23819
|
-
|
|
23820
|
-
|
|
23821
|
-
|
|
23822
|
-
|
|
23823
|
-
|
|
23824
|
-
|
|
23825
|
-
|
|
23826
|
-
|
|
23827
|
-
|
|
23828
|
-
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
23832
|
-
|
|
23833
|
-
|
|
23834
|
-
|
|
23835
|
-
|
|
23836
|
-
|
|
23837
|
-
|
|
23838
|
-
|
|
23839
|
-
|
|
23840
|
-
|
|
24216
|
+
|
|
24217
|
+
//#endregion
|
|
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
|
+
}
|
|
23841
24255
|
};
|
|
23842
|
-
|
|
23843
|
-
|
|
23844
|
-
|
|
23845
|
-
|
|
23846
|
-
|
|
23847
|
-
|
|
23848
|
-
|
|
23849
|
-
|
|
23850
|
-
|
|
23851
|
-
|
|
23852
|
-
|
|
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?.();
|
|
23853
24270
|
};
|
|
23854
24271
|
});
|
|
23855
24272
|
}
|
|
23856
|
-
function buildDashboardTimeTrends(options) {
|
|
23857
|
-
const pointLimit = clampPointLimit(options.pointLimit);
|
|
23858
|
-
const trends = createEmptyTrendSeries();
|
|
23859
|
-
for (const metric of DASHBOARD_METRIC_KEYS) {
|
|
23860
|
-
if (options.availability[metric].state !== "ok") continue;
|
|
23861
|
-
trends[metric] = bucketizeTrend(normalizeEvents(options.events[metric], pointLimit), options.reducers?.[metric] ?? "sum", options.rightEdgeTs);
|
|
23862
|
-
}
|
|
23863
|
-
return {
|
|
23864
|
-
trends,
|
|
23865
|
-
trendMeta: {
|
|
23866
|
-
pointLimit,
|
|
23867
|
-
lastUpdatedAt: options.timestamp
|
|
23868
|
-
}
|
|
23869
|
-
};
|
|
23870
|
-
}
|
|
23871
24273
|
|
|
23872
24274
|
//#endregion
|
|
23873
24275
|
//#region ../server/src/reactive-kv.ts
|
|
@@ -23960,119 +24362,41 @@ function createReactiveSubscription(task) {
|
|
|
23960
24362
|
};
|
|
23961
24363
|
});
|
|
23962
24364
|
}
|
|
23963
|
-
/**
|
|
23964
|
-
* 创建带输入参数的响应式订阅
|
|
23965
|
-
*
|
|
23966
|
-
* @param task 接收输入参数的异步任务
|
|
23967
|
-
* @returns 返回一个函数,接收输入参数并返回 tRPC observable
|
|
23968
|
-
*
|
|
23969
|
-
* @example
|
|
23970
|
-
* ```typescript
|
|
23971
|
-
* // 在 router 中使用
|
|
23972
|
-
* subscribeOne: publicProcedure
|
|
23973
|
-
* .input(z.object({ id: z.string() }))
|
|
23974
|
-
* .subscription(({ ctx, input }) => {
|
|
23975
|
-
* return createReactiveSubscriptionWithInput(
|
|
23976
|
-
* (id: string) => ctx.adapter.readSpec(id)
|
|
23977
|
-
* )(input.id)
|
|
23978
|
-
* })
|
|
23979
|
-
* ```
|
|
23980
|
-
*/
|
|
23981
|
-
function createReactiveSubscriptionWithInput(task) {
|
|
23982
|
-
return (input) => {
|
|
23983
|
-
return createReactiveSubscription(() => task(input));
|
|
23984
|
-
};
|
|
23985
|
-
}
|
|
23986
|
-
|
|
23987
|
-
//#endregion
|
|
23988
|
-
//#region ../server/src/router.ts
|
|
23989
|
-
const t = initTRPC.context().create();
|
|
23990
|
-
const router = t.router;
|
|
23991
|
-
const publicProcedure = t.procedure;
|
|
23992
|
-
const
|
|
23993
|
-
|
|
23994
|
-
"
|
|
23995
|
-
"
|
|
23996
|
-
"
|
|
23997
|
-
|
|
23998
|
-
];
|
|
23999
|
-
const dashboardGitTaskStatusEmitter = new EventEmitter$1();
|
|
24000
|
-
dashboardGitTaskStatusEmitter.setMaxListeners(200);
|
|
24001
|
-
const dashboardGitTaskStatus = {
|
|
24002
|
-
running: false,
|
|
24003
|
-
inFlight: 0,
|
|
24004
|
-
lastStartedAt: null,
|
|
24005
|
-
lastFinishedAt: null,
|
|
24006
|
-
lastReason: null,
|
|
24007
|
-
lastError: null
|
|
24008
|
-
};
|
|
24009
|
-
function getDashboardGitTaskStatus() {
|
|
24010
|
-
return { ...dashboardGitTaskStatus };
|
|
24011
|
-
}
|
|
24012
|
-
function emitDashboardGitTaskStatus() {
|
|
24013
|
-
dashboardGitTaskStatusEmitter.emit("change", getDashboardGitTaskStatus());
|
|
24014
|
-
}
|
|
24015
|
-
function beginDashboardGitTask(reason) {
|
|
24016
|
-
dashboardGitTaskStatus.inFlight += 1;
|
|
24017
|
-
dashboardGitTaskStatus.running = true;
|
|
24018
|
-
dashboardGitTaskStatus.lastStartedAt = Date.now();
|
|
24019
|
-
dashboardGitTaskStatus.lastReason = reason;
|
|
24020
|
-
dashboardGitTaskStatus.lastError = null;
|
|
24021
|
-
emitDashboardGitTaskStatus();
|
|
24022
|
-
}
|
|
24023
|
-
function endDashboardGitTask(error) {
|
|
24024
|
-
dashboardGitTaskStatus.inFlight = Math.max(0, dashboardGitTaskStatus.inFlight - 1);
|
|
24025
|
-
dashboardGitTaskStatus.running = dashboardGitTaskStatus.inFlight > 0;
|
|
24026
|
-
dashboardGitTaskStatus.lastFinishedAt = Date.now();
|
|
24027
|
-
if (error) dashboardGitTaskStatus.lastError = error instanceof Error ? error.message : String(error);
|
|
24028
|
-
emitDashboardGitTaskStatus();
|
|
24029
|
-
}
|
|
24030
|
-
const DASHBOARD_GIT_REFRESH_STAMP_NAME = "openspecui-dashboard-git-refresh.stamp";
|
|
24031
|
-
async function resolveGitMetadataDir(projectDir) {
|
|
24032
|
-
try {
|
|
24033
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-dir"], {
|
|
24034
|
-
cwd: projectDir,
|
|
24035
|
-
maxBuffer: 1024 * 1024,
|
|
24036
|
-
encoding: "utf8"
|
|
24037
|
-
});
|
|
24038
|
-
const gitDirRaw = stdout.trim();
|
|
24039
|
-
if (!gitDirRaw) return null;
|
|
24040
|
-
const gitDirPath = resolve$1(projectDir, gitDirRaw);
|
|
24041
|
-
if (!(await stat(gitDirPath)).isDirectory()) return null;
|
|
24042
|
-
return gitDirPath;
|
|
24043
|
-
} catch {
|
|
24044
|
-
return null;
|
|
24045
|
-
}
|
|
24046
|
-
}
|
|
24047
|
-
async function resolveGitMetadataDirReactive(projectDir) {
|
|
24048
|
-
const gitMetadataDir = await resolveGitMetadataDir(projectDir);
|
|
24049
|
-
if (!gitMetadataDir) return null;
|
|
24050
|
-
await reactiveReadDir(gitMetadataDir, { includeHidden: true });
|
|
24051
|
-
return gitMetadataDir;
|
|
24052
|
-
}
|
|
24053
|
-
function getDashboardGitRefreshStampPath(gitMetadataDir) {
|
|
24054
|
-
return join$1(gitMetadataDir, DASHBOARD_GIT_REFRESH_STAMP_NAME);
|
|
24055
|
-
}
|
|
24056
|
-
async function touchDashboardGitRefreshStamp(projectDir, reason) {
|
|
24057
|
-
const gitMetadataDir = await resolveGitMetadataDir(projectDir);
|
|
24058
|
-
if (!gitMetadataDir) return { skipped: true };
|
|
24059
|
-
const stampPath = getDashboardGitRefreshStampPath(gitMetadataDir);
|
|
24060
|
-
await mkdir$1(dirname$1(stampPath), { recursive: true });
|
|
24061
|
-
await writeFile$1(stampPath, `${Date.now()} ${reason}\n`, "utf8");
|
|
24062
|
-
return { skipped: false };
|
|
24063
|
-
}
|
|
24064
|
-
async function registerDashboardGitReactiveDeps(projectDir) {
|
|
24065
|
-
await reactiveReadDir(projectDir, {
|
|
24066
|
-
includeHidden: true,
|
|
24067
|
-
exclude: ["node_modules"]
|
|
24068
|
-
});
|
|
24069
|
-
const gitMetadataDir = await resolveGitMetadataDirReactive(projectDir);
|
|
24070
|
-
if (!gitMetadataDir) return;
|
|
24071
|
-
await reactiveReadFile(getDashboardGitRefreshStampPath(gitMetadataDir));
|
|
24072
|
-
await reactiveReadFile(join$1(gitMetadataDir, "HEAD"));
|
|
24073
|
-
await reactiveReadFile(join$1(gitMetadataDir, "index"));
|
|
24074
|
-
await reactiveReadFile(join$1(gitMetadataDir, "packed-refs"));
|
|
24075
|
-
}
|
|
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
|
+
];
|
|
24076
24400
|
function requireChangeId(changeId) {
|
|
24077
24401
|
if (!changeId) throw new Error("change is required");
|
|
24078
24402
|
return changeId;
|
|
@@ -24242,223 +24566,6 @@ function buildSystemStatus(ctx) {
|
|
|
24242
24566
|
watcherLastReinitializeReason: runtime?.lastReinitializeReason ?? null
|
|
24243
24567
|
};
|
|
24244
24568
|
}
|
|
24245
|
-
function resolveTrendTimestamp(primary, secondary) {
|
|
24246
|
-
if (typeof primary === "number" && Number.isFinite(primary) && primary > 0) return primary;
|
|
24247
|
-
if (typeof secondary === "number" && Number.isFinite(secondary) && secondary > 0) return secondary;
|
|
24248
|
-
return null;
|
|
24249
|
-
}
|
|
24250
|
-
function parseDatedIdTimestamp(id) {
|
|
24251
|
-
const match$1 = /^(\d{4})-(\d{2})-(\d{2})(?:-|$)/.exec(id);
|
|
24252
|
-
if (!match$1) return null;
|
|
24253
|
-
const year = Number(match$1[1]);
|
|
24254
|
-
const month = Number(match$1[2]);
|
|
24255
|
-
const day = Number(match$1[3]);
|
|
24256
|
-
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return null;
|
|
24257
|
-
if (month < 1 || month > 12) return null;
|
|
24258
|
-
if (day < 1 || day > 31) return null;
|
|
24259
|
-
const ts = Date.UTC(year, month - 1, day);
|
|
24260
|
-
return Number.isFinite(ts) ? ts : null;
|
|
24261
|
-
}
|
|
24262
|
-
function createEmptyTriColorTrends() {
|
|
24263
|
-
return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
|
|
24264
|
-
}
|
|
24265
|
-
async function readLatestCommitTimestamp(projectDir) {
|
|
24266
|
-
try {
|
|
24267
|
-
const { stdout } = await execFileAsync("git", [
|
|
24268
|
-
"log",
|
|
24269
|
-
"-1",
|
|
24270
|
-
"--format=%ct"
|
|
24271
|
-
], {
|
|
24272
|
-
cwd: projectDir,
|
|
24273
|
-
maxBuffer: 1024 * 1024,
|
|
24274
|
-
encoding: "utf8"
|
|
24275
|
-
});
|
|
24276
|
-
const seconds = Number(stdout.trim());
|
|
24277
|
-
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : null;
|
|
24278
|
-
} catch {
|
|
24279
|
-
return null;
|
|
24280
|
-
}
|
|
24281
|
-
}
|
|
24282
|
-
async function fetchDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
24283
|
-
if (contextStorage.getStore()) await registerDashboardGitReactiveDeps(ctx.projectDir);
|
|
24284
|
-
const now = Date.now();
|
|
24285
|
-
const [specMetas, changeMetas, archiveMetas] = await Promise.all([
|
|
24286
|
-
ctx.adapter.listSpecsWithMeta(),
|
|
24287
|
-
ctx.adapter.listChangesWithMeta(),
|
|
24288
|
-
ctx.adapter.listArchivedChangesWithMeta()
|
|
24289
|
-
]);
|
|
24290
|
-
await ctx.kernel.waitForWarmup();
|
|
24291
|
-
await ctx.kernel.ensureStatusList();
|
|
24292
|
-
const statusList = ctx.kernel.getStatusList();
|
|
24293
|
-
const changeMetaMap = new Map(changeMetas.map((change) => [change.id, change]));
|
|
24294
|
-
const activeChangeIds = new Set([...changeMetas.map((change) => change.id), ...statusList.map((status) => status.changeName)]);
|
|
24295
|
-
const statusByChange = new Map(statusList.map((status) => [status.changeName, status]));
|
|
24296
|
-
const activeChanges = (await Promise.all([...activeChangeIds].map(async (changeId) => {
|
|
24297
|
-
const status = statusByChange.get(changeId);
|
|
24298
|
-
const changeMeta = changeMetaMap.get(changeId);
|
|
24299
|
-
const statInfo = await reactiveStat(join$1(ctx.projectDir, "openspec", "changes", changeId));
|
|
24300
|
-
let progress = changeMeta?.progress ?? {
|
|
24301
|
-
total: 0,
|
|
24302
|
-
completed: 0
|
|
24303
|
-
};
|
|
24304
|
-
if (status) try {
|
|
24305
|
-
await ctx.kernel.ensureApplyInstructions(changeId, status.schemaName);
|
|
24306
|
-
const apply = ctx.kernel.getApplyInstructions(changeId, status.schemaName);
|
|
24307
|
-
progress = {
|
|
24308
|
-
total: apply.progress.total,
|
|
24309
|
-
completed: apply.progress.complete
|
|
24310
|
-
};
|
|
24311
|
-
} catch {}
|
|
24312
|
-
return {
|
|
24313
|
-
id: changeId,
|
|
24314
|
-
name: changeMeta?.name ?? changeId,
|
|
24315
|
-
progress,
|
|
24316
|
-
updatedAt: changeMeta?.updatedAt ?? statInfo?.mtime ?? 0
|
|
24317
|
-
};
|
|
24318
|
-
}))).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
24319
|
-
const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
|
|
24320
|
-
const change = await ctx.adapter.readArchivedChange(meta.id);
|
|
24321
|
-
if (!change) return null;
|
|
24322
|
-
return {
|
|
24323
|
-
id: meta.id,
|
|
24324
|
-
createdAt: meta.createdAt,
|
|
24325
|
-
updatedAt: meta.updatedAt,
|
|
24326
|
-
tasksCompleted: change.tasks.filter((task) => task.completed).length
|
|
24327
|
-
};
|
|
24328
|
-
}))).filter((item) => item !== null);
|
|
24329
|
-
const specifications = (await Promise.all(specMetas.map(async (meta) => {
|
|
24330
|
-
const spec = await ctx.adapter.readSpec(meta.id);
|
|
24331
|
-
if (!spec) return null;
|
|
24332
|
-
return {
|
|
24333
|
-
id: meta.id,
|
|
24334
|
-
name: meta.name,
|
|
24335
|
-
requirements: spec.requirements.length,
|
|
24336
|
-
updatedAt: meta.updatedAt
|
|
24337
|
-
};
|
|
24338
|
-
}))).filter((item) => item !== null).sort((a, b) => b.requirements - a.requirements || b.updatedAt - a.updatedAt);
|
|
24339
|
-
const requirements = specifications.reduce((sum, spec) => sum + spec.requirements, 0);
|
|
24340
|
-
const tasksTotal = activeChanges.reduce((sum, change) => sum + change.progress.total, 0);
|
|
24341
|
-
const tasksCompleted = activeChanges.reduce((sum, change) => sum + change.progress.completed, 0);
|
|
24342
|
-
const archivedTasksCompleted = archivedChanges.reduce((sum, change) => sum + change.tasksCompleted, 0);
|
|
24343
|
-
const taskCompletionPercent = tasksTotal > 0 ? Math.round(tasksCompleted / tasksTotal * 100) : null;
|
|
24344
|
-
const inProgressChanges = activeChanges.filter((change) => change.progress.total > 0 && change.progress.completed < change.progress.total).length;
|
|
24345
|
-
const specificationTrendEvents = specMetas.flatMap((spec) => {
|
|
24346
|
-
const ts = resolveTrendTimestamp(spec.createdAt, spec.updatedAt);
|
|
24347
|
-
return ts === null ? [] : [{
|
|
24348
|
-
ts,
|
|
24349
|
-
value: 1
|
|
24350
|
-
}];
|
|
24351
|
-
});
|
|
24352
|
-
const completedTrendEvents = archivedChanges.flatMap((archive) => {
|
|
24353
|
-
const ts = parseDatedIdTimestamp(archive.id) ?? resolveTrendTimestamp(archive.updatedAt, archive.createdAt);
|
|
24354
|
-
return ts === null ? [] : [{
|
|
24355
|
-
ts,
|
|
24356
|
-
value: archive.tasksCompleted
|
|
24357
|
-
}];
|
|
24358
|
-
});
|
|
24359
|
-
const specMetaById = new Map(specMetas.map((meta) => [meta.id, meta]));
|
|
24360
|
-
const requirementTrendEvents = specifications.flatMap((spec) => {
|
|
24361
|
-
const meta = specMetaById.get(spec.id);
|
|
24362
|
-
const ts = resolveTrendTimestamp(meta?.updatedAt, meta?.createdAt);
|
|
24363
|
-
return ts === null ? [] : [{
|
|
24364
|
-
ts,
|
|
24365
|
-
value: spec.requirements
|
|
24366
|
-
}];
|
|
24367
|
-
});
|
|
24368
|
-
const hasObjectiveSpecificationTrend = specificationTrendEvents.length > 0 || specifications.length === 0;
|
|
24369
|
-
const hasObjectiveRequirementTrend = requirementTrendEvents.length > 0 || requirements === 0;
|
|
24370
|
-
const hasObjectiveCompletedTrend = completedTrendEvents.length > 0 || archiveMetas.length === 0;
|
|
24371
|
-
const config = await ctx.configManager.readConfig();
|
|
24372
|
-
beginDashboardGitTask(reason);
|
|
24373
|
-
let latestCommitTs = null;
|
|
24374
|
-
let git;
|
|
24375
|
-
try {
|
|
24376
|
-
const gitSnapshotPromise = buildDashboardGitSnapshot({ projectDir: ctx.projectDir }).catch(() => ({
|
|
24377
|
-
defaultBranch: "main",
|
|
24378
|
-
worktrees: []
|
|
24379
|
-
}));
|
|
24380
|
-
latestCommitTs = await readLatestCommitTimestamp(ctx.projectDir);
|
|
24381
|
-
git = await gitSnapshotPromise;
|
|
24382
|
-
} catch (error) {
|
|
24383
|
-
endDashboardGitTask(error);
|
|
24384
|
-
throw error;
|
|
24385
|
-
}
|
|
24386
|
-
endDashboardGitTask(null);
|
|
24387
|
-
const cardAvailability = {
|
|
24388
|
-
specifications: hasObjectiveSpecificationTrend ? { state: "ok" } : {
|
|
24389
|
-
state: "invalid",
|
|
24390
|
-
reason: "objective-history-unavailable"
|
|
24391
|
-
},
|
|
24392
|
-
requirements: hasObjectiveRequirementTrend ? { state: "ok" } : {
|
|
24393
|
-
state: "invalid",
|
|
24394
|
-
reason: "objective-history-unavailable"
|
|
24395
|
-
},
|
|
24396
|
-
activeChanges: {
|
|
24397
|
-
state: "invalid",
|
|
24398
|
-
reason: "objective-history-unavailable"
|
|
24399
|
-
},
|
|
24400
|
-
inProgressChanges: {
|
|
24401
|
-
state: "invalid",
|
|
24402
|
-
reason: "objective-history-unavailable"
|
|
24403
|
-
},
|
|
24404
|
-
completedChanges: hasObjectiveCompletedTrend ? { state: "ok" } : {
|
|
24405
|
-
state: "invalid",
|
|
24406
|
-
reason: "objective-history-unavailable"
|
|
24407
|
-
},
|
|
24408
|
-
taskCompletionPercent: {
|
|
24409
|
-
state: "invalid",
|
|
24410
|
-
reason: taskCompletionPercent === null ? "semantic-uncomputable" : "objective-history-unavailable"
|
|
24411
|
-
}
|
|
24412
|
-
};
|
|
24413
|
-
const trendKinds = {
|
|
24414
|
-
specifications: "monotonic",
|
|
24415
|
-
requirements: "monotonic",
|
|
24416
|
-
activeChanges: "bidirectional",
|
|
24417
|
-
inProgressChanges: "bidirectional",
|
|
24418
|
-
completedChanges: "monotonic",
|
|
24419
|
-
taskCompletionPercent: "bidirectional"
|
|
24420
|
-
};
|
|
24421
|
-
const { trends: baselineTrends, trendMeta } = buildDashboardTimeTrends({
|
|
24422
|
-
pointLimit: config.dashboard.trendPointLimit,
|
|
24423
|
-
timestamp: now,
|
|
24424
|
-
rightEdgeTs: latestCommitTs,
|
|
24425
|
-
availability: cardAvailability,
|
|
24426
|
-
events: {
|
|
24427
|
-
specifications: specificationTrendEvents,
|
|
24428
|
-
requirements: requirementTrendEvents,
|
|
24429
|
-
activeChanges: [],
|
|
24430
|
-
inProgressChanges: [],
|
|
24431
|
-
completedChanges: completedTrendEvents,
|
|
24432
|
-
taskCompletionPercent: []
|
|
24433
|
-
},
|
|
24434
|
-
reducers: {
|
|
24435
|
-
specifications: "sum",
|
|
24436
|
-
requirements: "sum",
|
|
24437
|
-
completedChanges: "sum"
|
|
24438
|
-
}
|
|
24439
|
-
});
|
|
24440
|
-
return {
|
|
24441
|
-
summary: {
|
|
24442
|
-
specifications: specifications.length,
|
|
24443
|
-
requirements,
|
|
24444
|
-
activeChanges: activeChanges.length,
|
|
24445
|
-
inProgressChanges,
|
|
24446
|
-
completedChanges: archiveMetas.length,
|
|
24447
|
-
archivedTasksCompleted,
|
|
24448
|
-
tasksTotal,
|
|
24449
|
-
tasksCompleted,
|
|
24450
|
-
taskCompletionPercent
|
|
24451
|
-
},
|
|
24452
|
-
trends: baselineTrends,
|
|
24453
|
-
triColorTrends: createEmptyTriColorTrends(),
|
|
24454
|
-
trendKinds,
|
|
24455
|
-
cardAvailability,
|
|
24456
|
-
trendMeta,
|
|
24457
|
-
specifications,
|
|
24458
|
-
activeChanges,
|
|
24459
|
-
git
|
|
24460
|
-
};
|
|
24461
|
-
}
|
|
24462
24569
|
/**
|
|
24463
24570
|
* Spec router - spec CRUD operations
|
|
24464
24571
|
*/
|
|
@@ -24667,6 +24774,7 @@ const configRouter = router({
|
|
|
24667
24774
|
"system"
|
|
24668
24775
|
]).optional(),
|
|
24669
24776
|
codeEditor: objectType({ theme: CodeEditorThemeSchema.optional() }).optional(),
|
|
24777
|
+
appBaseUrl: stringType().optional(),
|
|
24670
24778
|
terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
|
|
24671
24779
|
dashboard: DashboardConfigSchema.partial().optional()
|
|
24672
24780
|
})).mutation(async ({ ctx, input }) => {
|
|
@@ -24674,9 +24782,10 @@ const configRouter = router({
|
|
|
24674
24782
|
const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
|
|
24675
24783
|
if (hasCliCommand && !hasCliArgs) {
|
|
24676
24784
|
await ctx.configManager.setCliCommand(input.cli?.command ?? "");
|
|
24677
|
-
if (input.theme !== void 0 || input.codeEditor !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0) await ctx.configManager.writeConfig({
|
|
24785
|
+
if (input.theme !== void 0 || input.codeEditor !== void 0 || input.appBaseUrl !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0) await ctx.configManager.writeConfig({
|
|
24678
24786
|
theme: input.theme,
|
|
24679
24787
|
codeEditor: input.codeEditor,
|
|
24788
|
+
appBaseUrl: input.appBaseUrl,
|
|
24680
24789
|
terminal: input.terminal,
|
|
24681
24790
|
dashboard: input.dashboard
|
|
24682
24791
|
});
|
|
@@ -25158,30 +25267,52 @@ const systemRouter = router({
|
|
|
25158
25267
|
*/
|
|
25159
25268
|
const dashboardRouter = router({
|
|
25160
25269
|
get: publicProcedure.query(async ({ ctx }) => {
|
|
25161
|
-
return
|
|
25270
|
+
return ctx.dashboardOverviewService.getCurrent();
|
|
25162
25271
|
}),
|
|
25163
25272
|
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
25164
|
-
return
|
|
25165
|
-
|
|
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
|
+
};
|
|
25166
25285
|
});
|
|
25167
25286
|
}),
|
|
25168
25287
|
refreshGitSnapshot: publicProcedure.input(objectType({ reason: stringType().optional() }).optional()).mutation(async ({ ctx, input }) => {
|
|
25169
25288
|
const reason = input?.reason?.trim() || "manual-refresh";
|
|
25170
25289
|
await touchDashboardGitRefreshStamp(ctx.projectDir, reason);
|
|
25290
|
+
await ctx.dashboardOverviewService.refresh(reason);
|
|
25171
25291
|
return { success: true };
|
|
25172
25292
|
}),
|
|
25173
|
-
|
|
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");
|
|
25301
|
+
return { success: true };
|
|
25302
|
+
}),
|
|
25303
|
+
gitTaskStatus: publicProcedure.query(async ({ ctx }) => {
|
|
25304
|
+
await ctx.dashboardOverviewService.getCurrent();
|
|
25174
25305
|
return getDashboardGitTaskStatus();
|
|
25175
25306
|
}),
|
|
25176
|
-
subscribeGitTaskStatus: publicProcedure.subscription(() => {
|
|
25307
|
+
subscribeGitTaskStatus: publicProcedure.subscription(({ ctx }) => {
|
|
25177
25308
|
return observable((emit) => {
|
|
25309
|
+
ctx.dashboardOverviewService.getCurrent().catch(() => {});
|
|
25178
25310
|
emit.next(getDashboardGitTaskStatus());
|
|
25179
|
-
const
|
|
25311
|
+
const unsubscribe = subscribeDashboardGitTaskStatus((status) => {
|
|
25180
25312
|
emit.next(status);
|
|
25181
|
-
};
|
|
25182
|
-
dashboardGitTaskStatusEmitter.on("change", handler);
|
|
25313
|
+
});
|
|
25183
25314
|
return () => {
|
|
25184
|
-
|
|
25315
|
+
unsubscribe();
|
|
25185
25316
|
};
|
|
25186
25317
|
});
|
|
25187
25318
|
})
|
|
@@ -25438,6 +25569,17 @@ var SearchService = class {
|
|
|
25438
25569
|
*
|
|
25439
25570
|
* @module server
|
|
25440
25571
|
*/
|
|
25572
|
+
const __dirname$1 = dirname$1(fileURLToPath(import.meta.url));
|
|
25573
|
+
function getServerPackageVersion() {
|
|
25574
|
+
try {
|
|
25575
|
+
const packageJsonPath = join$1(__dirname$1, "..", "package.json");
|
|
25576
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
25577
|
+
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
25578
|
+
} catch {
|
|
25579
|
+
return "0.0.0";
|
|
25580
|
+
}
|
|
25581
|
+
}
|
|
25582
|
+
const SERVER_PACKAGE_VERSION = getServerPackageVersion();
|
|
25441
25583
|
/**
|
|
25442
25584
|
* Create an OpenSpecUI HTTP server with optional WebSocket support
|
|
25443
25585
|
*/
|
|
@@ -25448,6 +25590,11 @@ function createServer$2(config) {
|
|
|
25448
25590
|
const kernel = config.kernel;
|
|
25449
25591
|
const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
|
|
25450
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);
|
|
25451
25598
|
const app = new Hono();
|
|
25452
25599
|
const corsOrigins = config.corsOrigins ?? ["http://localhost:5173", "http://localhost:3000"];
|
|
25453
25600
|
app.use("*", cors({
|
|
@@ -25458,7 +25605,9 @@ function createServer$2(config) {
|
|
|
25458
25605
|
return c.json({
|
|
25459
25606
|
status: "ok",
|
|
25460
25607
|
projectDir: config.projectDir,
|
|
25461
|
-
|
|
25608
|
+
projectName: basename$1(config.projectDir) || config.projectDir,
|
|
25609
|
+
watcherEnabled: !!watcher,
|
|
25610
|
+
openspecuiVersion: SERVER_PACKAGE_VERSION
|
|
25462
25611
|
});
|
|
25463
25612
|
});
|
|
25464
25613
|
app.use("/trpc/*", async (c) => {
|
|
@@ -25472,6 +25621,7 @@ function createServer$2(config) {
|
|
|
25472
25621
|
cliExecutor,
|
|
25473
25622
|
kernel,
|
|
25474
25623
|
searchService,
|
|
25624
|
+
dashboardOverviewService,
|
|
25475
25625
|
watcher,
|
|
25476
25626
|
projectDir: config.projectDir
|
|
25477
25627
|
})
|
|
@@ -25483,6 +25633,7 @@ function createServer$2(config) {
|
|
|
25483
25633
|
cliExecutor,
|
|
25484
25634
|
kernel,
|
|
25485
25635
|
searchService,
|
|
25636
|
+
dashboardOverviewService,
|
|
25486
25637
|
watcher,
|
|
25487
25638
|
projectDir: config.projectDir
|
|
25488
25639
|
});
|
|
@@ -25493,6 +25644,7 @@ function createServer$2(config) {
|
|
|
25493
25644
|
cliExecutor,
|
|
25494
25645
|
kernel,
|
|
25495
25646
|
searchService,
|
|
25647
|
+
dashboardOverviewService,
|
|
25496
25648
|
watcher,
|
|
25497
25649
|
createContext,
|
|
25498
25650
|
port: config.port ?? 3100
|
|
@@ -25540,6 +25692,7 @@ async function createWebSocketServer(server, httpServer, config) {
|
|
|
25540
25692
|
wss.close();
|
|
25541
25693
|
server.watcher?.stop();
|
|
25542
25694
|
server.searchService.dispose().catch(() => {});
|
|
25695
|
+
server.dashboardOverviewService.dispose();
|
|
25543
25696
|
}
|
|
25544
25697
|
};
|
|
25545
25698
|
}
|
|
@@ -25575,6 +25728,9 @@ async function startServer(config, setupApp) {
|
|
|
25575
25728
|
server.searchService.init().catch((err) => {
|
|
25576
25729
|
console.error("Search service warmup failed:", err);
|
|
25577
25730
|
});
|
|
25731
|
+
server.dashboardOverviewService.init().catch((err) => {
|
|
25732
|
+
console.error("Dashboard overview warmup failed:", err);
|
|
25733
|
+
});
|
|
25578
25734
|
return {
|
|
25579
25735
|
url,
|
|
25580
25736
|
port,
|
|
@@ -25590,9 +25746,6 @@ async function startServer(config, setupApp) {
|
|
|
25590
25746
|
//#endregion
|
|
25591
25747
|
//#region src/index.ts
|
|
25592
25748
|
const __dirname = dirname$1(fileURLToPath(import.meta.url));
|
|
25593
|
-
/**
|
|
25594
|
-
* Get the path to the web assets directory
|
|
25595
|
-
*/
|
|
25596
25749
|
function getWebAssetsDir() {
|
|
25597
25750
|
const devPath = join$1(__dirname, "..", "..", "web", "dist");
|
|
25598
25751
|
const prodPath = join$1(__dirname, "..", "web");
|
|
@@ -25600,9 +25753,6 @@ function getWebAssetsDir() {
|
|
|
25600
25753
|
if (existsSync(devPath)) return devPath;
|
|
25601
25754
|
throw new Error("Web assets not found. Make sure to build the web package first.");
|
|
25602
25755
|
}
|
|
25603
|
-
/**
|
|
25604
|
-
* Setup static file serving middleware for the Hono app
|
|
25605
|
-
*/
|
|
25606
25756
|
function setupStaticFiles(app) {
|
|
25607
25757
|
const webDir = getWebAssetsDir();
|
|
25608
25758
|
const mimeTypes = {
|
|
@@ -25639,18 +25789,15 @@ function setupStaticFiles(app) {
|
|
|
25639
25789
|
return c.notFound();
|
|
25640
25790
|
});
|
|
25641
25791
|
}
|
|
25642
|
-
/**
|
|
25643
|
-
* Start the OpenSpec UI server with WebSocket support for realtime updates.
|
|
25644
|
-
* Includes static file serving for the web UI.
|
|
25645
|
-
*/
|
|
25646
25792
|
async function startServer$1(options = {}) {
|
|
25647
|
-
const { projectDir = process.cwd(), port = 3100, enableWatcher = true } = options;
|
|
25793
|
+
const { projectDir = process.cwd(), port = 3100, enableWatcher = true, corsOrigins } = options;
|
|
25648
25794
|
return await startServer({
|
|
25649
25795
|
projectDir,
|
|
25650
25796
|
port,
|
|
25651
|
-
enableWatcher
|
|
25797
|
+
enableWatcher,
|
|
25798
|
+
corsOrigins
|
|
25652
25799
|
}, setupStaticFiles);
|
|
25653
25800
|
}
|
|
25654
25801
|
|
|
25655
25802
|
//#endregion
|
|
25656
|
-
export { SchemaInfoSchema as a, toOpsxDisplayPath as c,
|
|
25803
|
+
export { SchemaInfoSchema as a, toOpsxDisplayPath as c, CliExecutor as d, ConfigManager as f, __toESM$1 as g, __commonJS$1 as h, SchemaDetailSchema as i, buildHostedLaunchUrl as l, OpenSpecAdapter as m, createServer$2 as n, SchemaResolutionSchema as o, DEFAULT_CONFIG as p, require_dist as r, TemplatesSchema as s, startServer$1 as t, resolveHostedAppBaseUrl as u };
|