openspecui 1.4.0 → 1.5.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/LICENSE +21 -0
- package/dist/cli.mjs +44 -25
- package/dist/index.mjs +1 -1
- package/dist/{open-BVmQScxd.mjs → open-DDagk2eo.mjs} +2 -2
- package/dist/{src-kxKAXq88.mjs → src-16GA3our.mjs} +826 -141
- package/package.json +3 -3
- package/web/assets/{BufferResource-C85AhGv8.js → BufferResource-Bn1UWy0D.js} +1 -1
- package/web/assets/{CanvasRenderer-CgtmVcg4.js → CanvasRenderer-D8NiU8la.js} +1 -1
- package/web/assets/{Filter-Cb47-EZS.js → Filter-CRwq487x.js} +1 -1
- package/web/assets/{RenderTargetSystem-Cx8qJk_q.js → RenderTargetSystem-CtoB_qTm.js} +1 -1
- package/web/assets/{WebGLRenderer-pIi2Bx_Q.js → WebGLRenderer-BgKO8R0a.js} +1 -1
- package/web/assets/{WebGPURenderer-C3CBA4dG.js → WebGPURenderer-CQeL2efC.js} +1 -1
- package/web/assets/{browserAll-C_q9nnWk.js → browserAll-DP6sOYev.js} +1 -1
- package/web/assets/{ghostty-web-D6mGnCnU.js → ghostty-web-evxujSxm.js} +1 -1
- package/web/assets/{index-C79ew42C.js → index-4MAU81Qk.js} +1 -1
- package/web/assets/{index-BwQ_9hzT.js → index-B0IbsqHi.js} +1 -1
- package/web/assets/{index-ur_rMFp9.js → index-B147AOgf.js} +1 -1
- package/web/assets/{index-TCkbFaCm.js → index-BMashGQn.js} +1 -1
- package/web/assets/{index-D45XwNhE.js → index-BPZ3nG0r.js} +1 -1
- package/web/assets/{index-BBPTFxy1.js → index-BejnsZfY.js} +1 -1
- package/web/assets/{index-CGrYIgSe.js → index-BnT52DZ8.js} +1 -1
- package/web/assets/{index-BSqKqaGj.js → index-CBCPR3Qb.js} +1 -1
- package/web/assets/{index-LWU4Mw81.js → index-D2Tp4F9B.js} +1 -1
- package/web/assets/{index-CGweB5Ib.js → index-D6ardy54.js} +1 -1
- package/web/assets/{index-knJhpHqo.js → index-DJqmTRAR.js} +1 -1
- package/web/assets/{index-B5M3Dg-Q.js → index-DTeOcXKn.js} +1 -1
- package/web/assets/{index-C6pjde1Q.js → index-DcXyAs0z.js} +1 -1
- package/web/assets/{index-3eymcnUu.js → index-T8xoxmUb.js} +218 -215
- package/web/assets/index-Ys2MTD3W.css +1 -0
- package/web/assets/{index-CyqwjCgH.js → index-dSf1u0YV.js} +1 -1
- package/web/assets/{index-D8KhKUsi.js → index-f0QdJSzm.js} +1 -1
- package/web/assets/{webworkerAll-Cq063Dqj.js → webworkerAll-DA2HufNb.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-BImvtc4B.css +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createServer } from "
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { createServer as createServer$1 } from "http";
|
|
3
4
|
import { Http2ServerRequest } from "http2";
|
|
4
5
|
import { Readable } from "stream";
|
|
5
6
|
import crypto from "crypto";
|
|
@@ -9,13 +10,14 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
9
10
|
import { mkdir as mkdir$1, readFile as readFile$1, readdir, rm, stat, writeFile as writeFile$1 } from "node:fs/promises";
|
|
10
11
|
import { dirname as dirname$1, join as join$1, matchesGlob, relative as relative$1, resolve as resolve$1, sep } from "node:path";
|
|
11
12
|
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
12
|
-
import { watch } from "fs";
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
|
+
import { watch } from "fs";
|
|
14
15
|
import { exec, spawn } from "child_process";
|
|
15
16
|
import { promisify } from "util";
|
|
16
|
-
import { createServer as createServer$1 } from "node:net";
|
|
17
17
|
import * as pty from "@lydell/node-pty";
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
18
19
|
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
20
|
+
import { promisify as promisify$1 } from "node:util";
|
|
19
21
|
import { Worker as Worker$1 } from "node:worker_threads";
|
|
20
22
|
import { fileURLToPath } from "node:url";
|
|
21
23
|
|
|
@@ -49,6 +51,41 @@ var __toESM$1 = (mod, isNodeMode, target) => (target = mod != null ? __create$1(
|
|
|
49
51
|
}) : target, mod));
|
|
50
52
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
51
53
|
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region ../server/src/port-utils.ts
|
|
56
|
+
/**
|
|
57
|
+
* Check if a port is available by trying to listen on it.
|
|
58
|
+
* Uses default binding (both IPv4 and IPv6) to detect conflicts.
|
|
59
|
+
*/
|
|
60
|
+
function isPortAvailable(port) {
|
|
61
|
+
return new Promise((resolve$2) => {
|
|
62
|
+
const server = createServer();
|
|
63
|
+
server.once("error", () => {
|
|
64
|
+
resolve$2(false);
|
|
65
|
+
});
|
|
66
|
+
server.once("listening", () => {
|
|
67
|
+
server.close(() => resolve$2(true));
|
|
68
|
+
});
|
|
69
|
+
server.listen(port);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Find an available port starting from the given port.
|
|
74
|
+
* Will try up to maxAttempts ports sequentially.
|
|
75
|
+
*
|
|
76
|
+
* @param startPort - The preferred port to start checking from
|
|
77
|
+
* @param maxAttempts - Maximum number of ports to try (default: 10)
|
|
78
|
+
* @returns The first available port found
|
|
79
|
+
* @throws Error if no available port is found in the range
|
|
80
|
+
*/
|
|
81
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
82
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
83
|
+
const port = startPort + i;
|
|
84
|
+
if (await isPortAvailable(port)) return port;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
52
89
|
//#endregion
|
|
53
90
|
//#region ../../node_modules/.pnpm/@hono+node-server@1.19.6_hono@4.10.6/node_modules/@hono/node-server/dist/index.mjs
|
|
54
91
|
var RequestError = class extends Error {
|
|
@@ -476,7 +513,7 @@ var createAdaptorServer = (options) => {
|
|
|
476
513
|
overrideGlobalObjects: options.overrideGlobalObjects,
|
|
477
514
|
autoCleanupIncoming: options.autoCleanupIncoming
|
|
478
515
|
});
|
|
479
|
-
return (options.createServer || createServer)(options.serverOptions || {}, requestListener);
|
|
516
|
+
return (options.createServer || createServer$1)(options.serverOptions || {}, requestListener);
|
|
480
517
|
};
|
|
481
518
|
var serve = (options, listeningListener) => {
|
|
482
519
|
const server = createAdaptorServer(options);
|
|
@@ -697,14 +734,14 @@ var MarkdownParser = class {
|
|
|
697
734
|
if (currentOperation === "RENAMED") {
|
|
698
735
|
const fromMatch = line.match(/FROM:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
|
|
699
736
|
const toMatch = line.match(/TO:\s*`?###\s*Requirement:\s*(.+?)`?$/i);
|
|
700
|
-
if (fromMatch)
|
|
701
|
-
|
|
702
|
-
from
|
|
703
|
-
}
|
|
704
|
-
if (toMatch)
|
|
705
|
-
|
|
706
|
-
to
|
|
707
|
-
}
|
|
737
|
+
if (fromMatch) {
|
|
738
|
+
if (!renameBuffer) renameBuffer = {};
|
|
739
|
+
renameBuffer.from = fromMatch[1].trim();
|
|
740
|
+
}
|
|
741
|
+
if (toMatch) {
|
|
742
|
+
if (!renameBuffer) renameBuffer = {};
|
|
743
|
+
renameBuffer.to = toMatch[1].trim();
|
|
744
|
+
}
|
|
708
745
|
if (renameBuffer?.from && renameBuffer?.to) {
|
|
709
746
|
deltas.push({
|
|
710
747
|
spec: deltaSpec.specId,
|
|
@@ -789,86 +826,6 @@ var MarkdownParser = class {
|
|
|
789
826
|
}
|
|
790
827
|
};
|
|
791
828
|
|
|
792
|
-
//#endregion
|
|
793
|
-
//#region ../core/src/validator.ts
|
|
794
|
-
/**
|
|
795
|
-
* Validator for OpenSpec documents
|
|
796
|
-
*/
|
|
797
|
-
var Validator = class {
|
|
798
|
-
/**
|
|
799
|
-
* Validate a spec document
|
|
800
|
-
*/
|
|
801
|
-
validateSpec(spec) {
|
|
802
|
-
const issues = [];
|
|
803
|
-
if (!spec.overview || spec.overview.trim().length === 0) issues.push({
|
|
804
|
-
severity: "ERROR",
|
|
805
|
-
message: "Spec must have a Purpose/Overview section",
|
|
806
|
-
path: "overview"
|
|
807
|
-
});
|
|
808
|
-
if (spec.requirements.length === 0) issues.push({
|
|
809
|
-
severity: "ERROR",
|
|
810
|
-
message: "Spec must have at least one requirement",
|
|
811
|
-
path: "requirements"
|
|
812
|
-
});
|
|
813
|
-
for (const req of spec.requirements) {
|
|
814
|
-
if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
|
|
815
|
-
severity: "WARNING",
|
|
816
|
-
message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
|
|
817
|
-
path: `requirements.${req.id}`
|
|
818
|
-
});
|
|
819
|
-
if (req.scenarios.length === 0) issues.push({
|
|
820
|
-
severity: "WARNING",
|
|
821
|
-
message: `Requirement should have at least one scenario: ${req.id}`,
|
|
822
|
-
path: `requirements.${req.id}.scenarios`
|
|
823
|
-
});
|
|
824
|
-
if (req.text.length > 1e3) issues.push({
|
|
825
|
-
severity: "WARNING",
|
|
826
|
-
message: `Requirement text is too long (max 1000 chars): ${req.id}`,
|
|
827
|
-
path: `requirements.${req.id}.text`
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
return {
|
|
831
|
-
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
832
|
-
issues
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
/**
|
|
836
|
-
* Validate a change proposal
|
|
837
|
-
*/
|
|
838
|
-
validateChange(change) {
|
|
839
|
-
const issues = [];
|
|
840
|
-
if (!change.why || change.why.length < 50) issues.push({
|
|
841
|
-
severity: "ERROR",
|
|
842
|
-
message: "Change \"Why\" section must be at least 50 characters",
|
|
843
|
-
path: "why"
|
|
844
|
-
});
|
|
845
|
-
if (change.why && change.why.length > 500) issues.push({
|
|
846
|
-
severity: "WARNING",
|
|
847
|
-
message: "Change \"Why\" section should be under 500 characters",
|
|
848
|
-
path: "why"
|
|
849
|
-
});
|
|
850
|
-
if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
|
|
851
|
-
severity: "ERROR",
|
|
852
|
-
message: "Change must have a \"What Changes\" section",
|
|
853
|
-
path: "whatChanges"
|
|
854
|
-
});
|
|
855
|
-
if (change.deltas.length === 0) issues.push({
|
|
856
|
-
severity: "WARNING",
|
|
857
|
-
message: "Change should have at least one delta",
|
|
858
|
-
path: "deltas"
|
|
859
|
-
});
|
|
860
|
-
if (change.deltas.length > 50) issues.push({
|
|
861
|
-
severity: "WARNING",
|
|
862
|
-
message: "Change has too many deltas (max 50)",
|
|
863
|
-
path: "deltas"
|
|
864
|
-
});
|
|
865
|
-
return {
|
|
866
|
-
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
867
|
-
issues
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
|
|
872
829
|
//#endregion
|
|
873
830
|
//#region ../core/src/reactive-fs/reactive-state.ts
|
|
874
831
|
/**
|
|
@@ -1713,6 +1670,86 @@ async function reactiveStat(path$1) {
|
|
|
1713
1670
|
return state.get();
|
|
1714
1671
|
}
|
|
1715
1672
|
|
|
1673
|
+
//#endregion
|
|
1674
|
+
//#region ../core/src/validator.ts
|
|
1675
|
+
/**
|
|
1676
|
+
* Validator for OpenSpec documents
|
|
1677
|
+
*/
|
|
1678
|
+
var Validator = class {
|
|
1679
|
+
/**
|
|
1680
|
+
* Validate a spec document
|
|
1681
|
+
*/
|
|
1682
|
+
validateSpec(spec) {
|
|
1683
|
+
const issues = [];
|
|
1684
|
+
if (!spec.overview || spec.overview.trim().length === 0) issues.push({
|
|
1685
|
+
severity: "ERROR",
|
|
1686
|
+
message: "Spec must have a Purpose/Overview section",
|
|
1687
|
+
path: "overview"
|
|
1688
|
+
});
|
|
1689
|
+
if (spec.requirements.length === 0) issues.push({
|
|
1690
|
+
severity: "ERROR",
|
|
1691
|
+
message: "Spec must have at least one requirement",
|
|
1692
|
+
path: "requirements"
|
|
1693
|
+
});
|
|
1694
|
+
for (const req of spec.requirements) {
|
|
1695
|
+
if (!req.text.includes("SHALL") && !req.text.includes("MUST")) issues.push({
|
|
1696
|
+
severity: "WARNING",
|
|
1697
|
+
message: `Requirement should contain "SHALL" or "MUST": ${req.id}`,
|
|
1698
|
+
path: `requirements.${req.id}`
|
|
1699
|
+
});
|
|
1700
|
+
if (req.scenarios.length === 0) issues.push({
|
|
1701
|
+
severity: "WARNING",
|
|
1702
|
+
message: `Requirement should have at least one scenario: ${req.id}`,
|
|
1703
|
+
path: `requirements.${req.id}.scenarios`
|
|
1704
|
+
});
|
|
1705
|
+
if (req.text.length > 1e3) issues.push({
|
|
1706
|
+
severity: "WARNING",
|
|
1707
|
+
message: `Requirement text is too long (max 1000 chars): ${req.id}`,
|
|
1708
|
+
path: `requirements.${req.id}.text`
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
1713
|
+
issues
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Validate a change proposal
|
|
1718
|
+
*/
|
|
1719
|
+
validateChange(change) {
|
|
1720
|
+
const issues = [];
|
|
1721
|
+
if (!change.why || change.why.length < 50) issues.push({
|
|
1722
|
+
severity: "ERROR",
|
|
1723
|
+
message: "Change \"Why\" section must be at least 50 characters",
|
|
1724
|
+
path: "why"
|
|
1725
|
+
});
|
|
1726
|
+
if (change.why && change.why.length > 500) issues.push({
|
|
1727
|
+
severity: "WARNING",
|
|
1728
|
+
message: "Change \"Why\" section should be under 500 characters",
|
|
1729
|
+
path: "why"
|
|
1730
|
+
});
|
|
1731
|
+
if (!change.whatChanges || change.whatChanges.trim().length === 0) issues.push({
|
|
1732
|
+
severity: "ERROR",
|
|
1733
|
+
message: "Change must have a \"What Changes\" section",
|
|
1734
|
+
path: "whatChanges"
|
|
1735
|
+
});
|
|
1736
|
+
if (change.deltas.length === 0) issues.push({
|
|
1737
|
+
severity: "WARNING",
|
|
1738
|
+
message: "Change should have at least one delta",
|
|
1739
|
+
path: "deltas"
|
|
1740
|
+
});
|
|
1741
|
+
if (change.deltas.length > 50) issues.push({
|
|
1742
|
+
severity: "WARNING",
|
|
1743
|
+
message: "Change has too many deltas (max 50)",
|
|
1744
|
+
path: "deltas"
|
|
1745
|
+
});
|
|
1746
|
+
return {
|
|
1747
|
+
valid: issues.filter((i) => i.severity === "ERROR").length === 0,
|
|
1748
|
+
issues
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1716
1753
|
//#endregion
|
|
1717
1754
|
//#region ../core/src/adapter.ts
|
|
1718
1755
|
/**
|
|
@@ -1892,17 +1929,17 @@ var OpenSpecAdapter = class {
|
|
|
1892
1929
|
const fullPath = join(dir, name);
|
|
1893
1930
|
const statInfo = await reactiveStat(fullPath);
|
|
1894
1931
|
if (!statInfo) continue;
|
|
1895
|
-
const relativePath = fullPath.slice(root.length + 1);
|
|
1932
|
+
const relativePath$1 = fullPath.slice(root.length + 1);
|
|
1896
1933
|
if (statInfo.isDirectory) {
|
|
1897
1934
|
files.push({
|
|
1898
|
-
path: relativePath,
|
|
1935
|
+
path: relativePath$1,
|
|
1899
1936
|
type: "directory"
|
|
1900
1937
|
});
|
|
1901
1938
|
files.push(...await this.collectChangeFiles(root, fullPath));
|
|
1902
1939
|
} else {
|
|
1903
1940
|
const content = await reactiveReadFile(fullPath);
|
|
1904
1941
|
files.push({
|
|
1905
|
-
path: relativePath,
|
|
1942
|
+
path: relativePath$1,
|
|
1906
1943
|
type: "file",
|
|
1907
1944
|
content: content ?? void 0
|
|
1908
1945
|
});
|
|
@@ -6244,6 +6281,7 @@ const TerminalConfigSchema = objectType({
|
|
|
6244
6281
|
scrollback: numberType().min(0).max(1e5).default(1e3),
|
|
6245
6282
|
rendererEngine: stringType().default("xterm")
|
|
6246
6283
|
});
|
|
6284
|
+
const DashboardConfigSchema = objectType({ trendPointLimit: numberType().int().min(20).max(500).default(100) });
|
|
6247
6285
|
/**
|
|
6248
6286
|
* OpenSpecUI 配置 Schema
|
|
6249
6287
|
*
|
|
@@ -6255,13 +6293,15 @@ const OpenSpecUIConfigSchema = objectType({
|
|
|
6255
6293
|
args: arrayType(stringType()).optional()
|
|
6256
6294
|
}).default({}),
|
|
6257
6295
|
theme: enumType(THEME_VALUES).default("system"),
|
|
6258
|
-
terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({}))
|
|
6296
|
+
terminal: TerminalConfigSchema.default(TerminalConfigSchema.parse({})),
|
|
6297
|
+
dashboard: DashboardConfigSchema.default(DashboardConfigSchema.parse({}))
|
|
6259
6298
|
});
|
|
6260
6299
|
/** 默认配置(静态,用于测试和类型) */
|
|
6261
6300
|
const DEFAULT_CONFIG = {
|
|
6262
6301
|
cli: {},
|
|
6263
6302
|
theme: "system",
|
|
6264
|
-
terminal: TerminalConfigSchema.parse({})
|
|
6303
|
+
terminal: TerminalConfigSchema.parse({}),
|
|
6304
|
+
dashboard: DashboardConfigSchema.parse({})
|
|
6265
6305
|
};
|
|
6266
6306
|
/**
|
|
6267
6307
|
* 配置管理器
|
|
@@ -6327,6 +6367,10 @@ var ConfigManager = class {
|
|
|
6327
6367
|
terminal: {
|
|
6328
6368
|
...current.terminal,
|
|
6329
6369
|
...config.terminal
|
|
6370
|
+
},
|
|
6371
|
+
dashboard: {
|
|
6372
|
+
...current.dashboard,
|
|
6373
|
+
...config.dashboard
|
|
6330
6374
|
}
|
|
6331
6375
|
};
|
|
6332
6376
|
const serialized = JSON.stringify(merged, null, 2);
|
|
@@ -7039,6 +7083,17 @@ async function getConfiguredTools(projectDir) {
|
|
|
7039
7083
|
return state.get();
|
|
7040
7084
|
}
|
|
7041
7085
|
|
|
7086
|
+
//#endregion
|
|
7087
|
+
//#region ../core/src/dashboard-types.ts
|
|
7088
|
+
const DASHBOARD_METRIC_KEYS = [
|
|
7089
|
+
"specifications",
|
|
7090
|
+
"requirements",
|
|
7091
|
+
"activeChanges",
|
|
7092
|
+
"inProgressChanges",
|
|
7093
|
+
"completedChanges",
|
|
7094
|
+
"taskCompletionPercent"
|
|
7095
|
+
];
|
|
7096
|
+
|
|
7042
7097
|
//#endregion
|
|
7043
7098
|
//#region ../core/src/opsx-types.ts
|
|
7044
7099
|
const ArtifactStatusSchema = objectType({
|
|
@@ -13838,10 +13893,10 @@ async function readEntriesUnderRoot(root) {
|
|
|
13838
13893
|
const fullPath = join$1(dir, name);
|
|
13839
13894
|
const statInfo = await reactiveStat(fullPath);
|
|
13840
13895
|
if (!statInfo) continue;
|
|
13841
|
-
const relativePath = toRelativePath(root, fullPath);
|
|
13896
|
+
const relativePath$1 = toRelativePath(root, fullPath);
|
|
13842
13897
|
if (statInfo.isDirectory) {
|
|
13843
13898
|
entries.push({
|
|
13844
|
-
path: relativePath,
|
|
13899
|
+
path: relativePath$1,
|
|
13845
13900
|
type: "directory"
|
|
13846
13901
|
});
|
|
13847
13902
|
entries.push(...await collectEntries(fullPath));
|
|
@@ -13849,7 +13904,7 @@ async function readEntriesUnderRoot(root) {
|
|
|
13849
13904
|
const content = await reactiveReadFile(fullPath);
|
|
13850
13905
|
const size = content ? Buffer.byteLength(content, "utf-8") : void 0;
|
|
13851
13906
|
entries.push({
|
|
13852
|
-
path: relativePath,
|
|
13907
|
+
path: relativePath$1,
|
|
13853
13908
|
type: "file",
|
|
13854
13909
|
content: content ?? void 0,
|
|
13855
13910
|
size
|
|
@@ -22659,41 +22714,6 @@ var import_sender = /* @__PURE__ */ __toESM$1(require_sender(), 1);
|
|
|
22659
22714
|
var import_websocket = /* @__PURE__ */ __toESM$1(require_websocket(), 1);
|
|
22660
22715
|
var import_websocket_server = /* @__PURE__ */ __toESM$1(require_websocket_server(), 1);
|
|
22661
22716
|
|
|
22662
|
-
//#endregion
|
|
22663
|
-
//#region ../server/src/port-utils.ts
|
|
22664
|
-
/**
|
|
22665
|
-
* Check if a port is available by trying to listen on it.
|
|
22666
|
-
* Uses default binding (both IPv4 and IPv6) to detect conflicts.
|
|
22667
|
-
*/
|
|
22668
|
-
function isPortAvailable(port) {
|
|
22669
|
-
return new Promise((resolve$2) => {
|
|
22670
|
-
const server = createServer$1();
|
|
22671
|
-
server.once("error", () => {
|
|
22672
|
-
resolve$2(false);
|
|
22673
|
-
});
|
|
22674
|
-
server.once("listening", () => {
|
|
22675
|
-
server.close(() => resolve$2(true));
|
|
22676
|
-
});
|
|
22677
|
-
server.listen(port);
|
|
22678
|
-
});
|
|
22679
|
-
}
|
|
22680
|
-
/**
|
|
22681
|
-
* Find an available port starting from the given port.
|
|
22682
|
-
* Will try up to maxAttempts ports sequentially.
|
|
22683
|
-
*
|
|
22684
|
-
* @param startPort - The preferred port to start checking from
|
|
22685
|
-
* @param maxAttempts - Maximum number of ports to try (default: 10)
|
|
22686
|
-
* @returns The first available port found
|
|
22687
|
-
* @throws Error if no available port is found in the range
|
|
22688
|
-
*/
|
|
22689
|
-
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
22690
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
22691
|
-
const port = startPort + i;
|
|
22692
|
-
if (await isPortAvailable(port)) return port;
|
|
22693
|
-
}
|
|
22694
|
-
throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
22695
|
-
}
|
|
22696
|
-
|
|
22697
22717
|
//#endregion
|
|
22698
22718
|
//#region ../server/src/pty-manager.ts
|
|
22699
22719
|
const DEFAULT_SCROLLBACK = 1e3;
|
|
@@ -23064,7 +23084,7 @@ function createPtyWebSocketHandler(ptyManager) {
|
|
|
23064
23084
|
}
|
|
23065
23085
|
|
|
23066
23086
|
//#endregion
|
|
23067
|
-
//#region ../search/
|
|
23087
|
+
//#region ../search/src/protocol.ts
|
|
23068
23088
|
const SearchDocumentKindSchema = enumType([
|
|
23069
23089
|
"spec",
|
|
23070
23090
|
"change",
|
|
@@ -23130,6 +23150,9 @@ const SearchWorkerResponseSchema = discriminatedUnionType("type", [
|
|
|
23130
23150
|
message: stringType()
|
|
23131
23151
|
})
|
|
23132
23152
|
]);
|
|
23153
|
+
|
|
23154
|
+
//#endregion
|
|
23155
|
+
//#region ../search/src/worker-source.ts
|
|
23133
23156
|
const sharedRuntimeSource = String.raw`
|
|
23134
23157
|
const DEFAULT_LIMIT = 50;
|
|
23135
23158
|
const MAX_LIMIT = 200;
|
|
@@ -23347,6 +23370,368 @@ function createCliStreamObservable(startStream) {
|
|
|
23347
23370
|
});
|
|
23348
23371
|
}
|
|
23349
23372
|
|
|
23373
|
+
//#endregion
|
|
23374
|
+
//#region ../server/src/dashboard-git-snapshot.ts
|
|
23375
|
+
const execFileAsync$1 = promisify$1(execFile);
|
|
23376
|
+
const EMPTY_DIFF = {
|
|
23377
|
+
files: 0,
|
|
23378
|
+
insertions: 0,
|
|
23379
|
+
deletions: 0
|
|
23380
|
+
};
|
|
23381
|
+
async function defaultRunGit(cwd, args) {
|
|
23382
|
+
try {
|
|
23383
|
+
const { stdout } = await execFileAsync$1("git", args, {
|
|
23384
|
+
cwd,
|
|
23385
|
+
encoding: "utf8",
|
|
23386
|
+
maxBuffer: 8 * 1024 * 1024
|
|
23387
|
+
});
|
|
23388
|
+
return {
|
|
23389
|
+
ok: true,
|
|
23390
|
+
stdout
|
|
23391
|
+
};
|
|
23392
|
+
} catch {
|
|
23393
|
+
return {
|
|
23394
|
+
ok: false,
|
|
23395
|
+
stdout: ""
|
|
23396
|
+
};
|
|
23397
|
+
}
|
|
23398
|
+
}
|
|
23399
|
+
function parseShortStat(output) {
|
|
23400
|
+
const files = Number(/(\d+)\s+files? changed/.exec(output)?.[1] ?? 0);
|
|
23401
|
+
const insertions = Number(/(\d+)\s+insertions?\(\+\)/.exec(output)?.[1] ?? 0);
|
|
23402
|
+
const deletions = Number(/(\d+)\s+deletions?\(-\)/.exec(output)?.[1] ?? 0);
|
|
23403
|
+
return {
|
|
23404
|
+
files: Number.isFinite(files) ? files : 0,
|
|
23405
|
+
insertions: Number.isFinite(insertions) ? insertions : 0,
|
|
23406
|
+
deletions: Number.isFinite(deletions) ? deletions : 0
|
|
23407
|
+
};
|
|
23408
|
+
}
|
|
23409
|
+
function parseNumStat(output) {
|
|
23410
|
+
let files = 0;
|
|
23411
|
+
let insertions = 0;
|
|
23412
|
+
let deletions = 0;
|
|
23413
|
+
for (const line of output.split("\n")) {
|
|
23414
|
+
const trimmed = line.trim();
|
|
23415
|
+
if (!trimmed) continue;
|
|
23416
|
+
const [addRaw, deleteRaw] = trimmed.split(" ");
|
|
23417
|
+
if (!addRaw || !deleteRaw) continue;
|
|
23418
|
+
files += 1;
|
|
23419
|
+
if (addRaw !== "-") insertions += Number(addRaw) || 0;
|
|
23420
|
+
if (deleteRaw !== "-") deletions += Number(deleteRaw) || 0;
|
|
23421
|
+
}
|
|
23422
|
+
return {
|
|
23423
|
+
files,
|
|
23424
|
+
insertions,
|
|
23425
|
+
deletions
|
|
23426
|
+
};
|
|
23427
|
+
}
|
|
23428
|
+
function normalizeGitPath(path$1) {
|
|
23429
|
+
return path$1.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
23430
|
+
}
|
|
23431
|
+
function relativePath(fromDir, target) {
|
|
23432
|
+
const rel = relative$1(fromDir, target);
|
|
23433
|
+
if (!rel || rel.length === 0) return ".";
|
|
23434
|
+
return rel;
|
|
23435
|
+
}
|
|
23436
|
+
function parseBranchName(branchRef, detached) {
|
|
23437
|
+
if (detached) return "(detached)";
|
|
23438
|
+
if (!branchRef) return "(unknown)";
|
|
23439
|
+
return branchRef.replace(/^refs\/heads\//, "");
|
|
23440
|
+
}
|
|
23441
|
+
function parseWorktreeList(porcelain) {
|
|
23442
|
+
const entries = [];
|
|
23443
|
+
let current = null;
|
|
23444
|
+
const flush = () => {
|
|
23445
|
+
if (!current) return;
|
|
23446
|
+
entries.push(current);
|
|
23447
|
+
current = null;
|
|
23448
|
+
};
|
|
23449
|
+
for (const line of porcelain.split("\n")) {
|
|
23450
|
+
if (line.startsWith("worktree ")) {
|
|
23451
|
+
flush();
|
|
23452
|
+
current = {
|
|
23453
|
+
path: line.slice(9).trim(),
|
|
23454
|
+
branchRef: null,
|
|
23455
|
+
detached: false
|
|
23456
|
+
};
|
|
23457
|
+
continue;
|
|
23458
|
+
}
|
|
23459
|
+
if (!current) continue;
|
|
23460
|
+
if (line.startsWith("branch ")) {
|
|
23461
|
+
current.branchRef = line.slice(7).trim();
|
|
23462
|
+
continue;
|
|
23463
|
+
}
|
|
23464
|
+
if (line === "detached") {
|
|
23465
|
+
current.detached = true;
|
|
23466
|
+
continue;
|
|
23467
|
+
}
|
|
23468
|
+
}
|
|
23469
|
+
flush();
|
|
23470
|
+
return entries;
|
|
23471
|
+
}
|
|
23472
|
+
function parseRelatedChanges(paths) {
|
|
23473
|
+
const related = /* @__PURE__ */ new Set();
|
|
23474
|
+
for (const path$1 of paths) {
|
|
23475
|
+
const normalized = normalizeGitPath(path$1);
|
|
23476
|
+
const activeMatch = /^openspec\/changes\/([^/]+)\//.exec(normalized);
|
|
23477
|
+
if (activeMatch?.[1]) {
|
|
23478
|
+
related.add(activeMatch[1]);
|
|
23479
|
+
continue;
|
|
23480
|
+
}
|
|
23481
|
+
const archiveMatch = /^openspec\/changes\/archive\/([^/]+)\//.exec(normalized);
|
|
23482
|
+
if (archiveMatch?.[1]) {
|
|
23483
|
+
const fullName = archiveMatch[1];
|
|
23484
|
+
related.add(fullName.replace(/^\d{4}-\d{2}-\d{2}-/, ""));
|
|
23485
|
+
}
|
|
23486
|
+
}
|
|
23487
|
+
return [...related].sort((a, b) => a.localeCompare(b));
|
|
23488
|
+
}
|
|
23489
|
+
async function resolveDefaultBranch(projectDir, runGit) {
|
|
23490
|
+
const remoteHead = await runGit(projectDir, [
|
|
23491
|
+
"symbolic-ref",
|
|
23492
|
+
"--quiet",
|
|
23493
|
+
"--short",
|
|
23494
|
+
"refs/remotes/origin/HEAD"
|
|
23495
|
+
]);
|
|
23496
|
+
const remoteRef = remoteHead.stdout.trim();
|
|
23497
|
+
if (remoteHead.ok && remoteRef) return remoteRef;
|
|
23498
|
+
const localHead = await runGit(projectDir, [
|
|
23499
|
+
"rev-parse",
|
|
23500
|
+
"--abbrev-ref",
|
|
23501
|
+
"HEAD"
|
|
23502
|
+
]);
|
|
23503
|
+
const localRef = localHead.stdout.trim();
|
|
23504
|
+
if (localHead.ok && localRef && localRef !== "HEAD") return localRef;
|
|
23505
|
+
return "main";
|
|
23506
|
+
}
|
|
23507
|
+
async function collectCommitEntries(options) {
|
|
23508
|
+
const { worktreePath, defaultBranch, maxCommitEntries, runGit } = options;
|
|
23509
|
+
const entries = [];
|
|
23510
|
+
const commits = await runGit(worktreePath, [
|
|
23511
|
+
"log",
|
|
23512
|
+
"--format=%H%x1f%s",
|
|
23513
|
+
`-n${maxCommitEntries}`,
|
|
23514
|
+
`${defaultBranch}..HEAD`
|
|
23515
|
+
]);
|
|
23516
|
+
if (commits.ok) for (const line of commits.stdout.split("\n")) {
|
|
23517
|
+
if (!line.trim()) continue;
|
|
23518
|
+
const [hash, title = ""] = line.split("");
|
|
23519
|
+
if (!hash) continue;
|
|
23520
|
+
const diffResult = await runGit(worktreePath, [
|
|
23521
|
+
"show",
|
|
23522
|
+
"--numstat",
|
|
23523
|
+
"--format=",
|
|
23524
|
+
hash
|
|
23525
|
+
]);
|
|
23526
|
+
const changedFiles = (await runGit(worktreePath, [
|
|
23527
|
+
"show",
|
|
23528
|
+
"--name-only",
|
|
23529
|
+
"--format=",
|
|
23530
|
+
hash
|
|
23531
|
+
])).stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23532
|
+
entries.push({
|
|
23533
|
+
type: "commit",
|
|
23534
|
+
hash,
|
|
23535
|
+
title: title.trim() || hash.slice(0, 7),
|
|
23536
|
+
relatedChanges: parseRelatedChanges(changedFiles),
|
|
23537
|
+
diff: diffResult.ok ? parseNumStat(diffResult.stdout) : EMPTY_DIFF
|
|
23538
|
+
});
|
|
23539
|
+
}
|
|
23540
|
+
const trackedResult = await runGit(worktreePath, [
|
|
23541
|
+
"diff",
|
|
23542
|
+
"--numstat",
|
|
23543
|
+
"HEAD"
|
|
23544
|
+
]);
|
|
23545
|
+
const trackedFilesResult = await runGit(worktreePath, [
|
|
23546
|
+
"diff",
|
|
23547
|
+
"--name-only",
|
|
23548
|
+
"HEAD"
|
|
23549
|
+
]);
|
|
23550
|
+
const untrackedResult = await runGit(worktreePath, [
|
|
23551
|
+
"ls-files",
|
|
23552
|
+
"--others",
|
|
23553
|
+
"--exclude-standard"
|
|
23554
|
+
]);
|
|
23555
|
+
const trackedFiles = trackedFilesResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23556
|
+
const untrackedFiles = untrackedResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
23557
|
+
const allUncommittedFiles = new Set([...trackedFiles, ...untrackedFiles]);
|
|
23558
|
+
const trackedDiff = trackedResult.ok ? parseNumStat(trackedResult.stdout) : EMPTY_DIFF;
|
|
23559
|
+
entries.push({
|
|
23560
|
+
type: "uncommitted",
|
|
23561
|
+
title: "Uncommitted",
|
|
23562
|
+
relatedChanges: parseRelatedChanges([...allUncommittedFiles]),
|
|
23563
|
+
diff: {
|
|
23564
|
+
files: allUncommittedFiles.size,
|
|
23565
|
+
insertions: trackedDiff.insertions,
|
|
23566
|
+
deletions: trackedDiff.deletions
|
|
23567
|
+
}
|
|
23568
|
+
});
|
|
23569
|
+
return entries;
|
|
23570
|
+
}
|
|
23571
|
+
async function collectWorktree(options) {
|
|
23572
|
+
const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries } = options;
|
|
23573
|
+
const worktreePath = resolve$1(worktree.path);
|
|
23574
|
+
const resolvedProjectDir = resolve$1(projectDir);
|
|
23575
|
+
const aheadBehindResult = await runGit(worktreePath, [
|
|
23576
|
+
"rev-list",
|
|
23577
|
+
"--left-right",
|
|
23578
|
+
"--count",
|
|
23579
|
+
`${defaultBranch}...HEAD`
|
|
23580
|
+
]);
|
|
23581
|
+
let ahead = 0;
|
|
23582
|
+
let behind = 0;
|
|
23583
|
+
if (aheadBehindResult.ok) {
|
|
23584
|
+
const [behindRaw, aheadRaw] = aheadBehindResult.stdout.trim().split(/\s+/);
|
|
23585
|
+
ahead = Number(aheadRaw) || 0;
|
|
23586
|
+
behind = Number(behindRaw) || 0;
|
|
23587
|
+
}
|
|
23588
|
+
const diffResult = await runGit(worktreePath, [
|
|
23589
|
+
"diff",
|
|
23590
|
+
"--shortstat",
|
|
23591
|
+
`${defaultBranch}...HEAD`
|
|
23592
|
+
]);
|
|
23593
|
+
const diff = diffResult.ok ? parseShortStat(diffResult.stdout) : EMPTY_DIFF;
|
|
23594
|
+
const entries = await collectCommitEntries({
|
|
23595
|
+
worktreePath,
|
|
23596
|
+
defaultBranch,
|
|
23597
|
+
maxCommitEntries,
|
|
23598
|
+
runGit
|
|
23599
|
+
});
|
|
23600
|
+
return {
|
|
23601
|
+
path: worktreePath,
|
|
23602
|
+
relativePath: relativePath(resolvedProjectDir, worktreePath),
|
|
23603
|
+
branchName: parseBranchName(worktree.branchRef, worktree.detached),
|
|
23604
|
+
isCurrent: resolvedProjectDir === worktreePath,
|
|
23605
|
+
ahead,
|
|
23606
|
+
behind,
|
|
23607
|
+
diff,
|
|
23608
|
+
entries
|
|
23609
|
+
};
|
|
23610
|
+
}
|
|
23611
|
+
async function buildDashboardGitSnapshot(options) {
|
|
23612
|
+
const runGit = options.runGit ?? defaultRunGit;
|
|
23613
|
+
const maxCommitEntries = options.maxCommitEntries ?? 8;
|
|
23614
|
+
const resolvedProjectDir = resolve$1(options.projectDir);
|
|
23615
|
+
const defaultBranch = await resolveDefaultBranch(resolvedProjectDir, runGit);
|
|
23616
|
+
const worktreeResult = await runGit(resolvedProjectDir, [
|
|
23617
|
+
"worktree",
|
|
23618
|
+
"list",
|
|
23619
|
+
"--porcelain"
|
|
23620
|
+
]);
|
|
23621
|
+
const parsed = worktreeResult.ok ? parseWorktreeList(worktreeResult.stdout) : [];
|
|
23622
|
+
const baseWorktrees = parsed.length > 0 ? parsed : [{
|
|
23623
|
+
path: resolvedProjectDir,
|
|
23624
|
+
branchRef: null,
|
|
23625
|
+
detached: false
|
|
23626
|
+
}];
|
|
23627
|
+
const worktrees = await Promise.all(baseWorktrees.map((worktree) => collectWorktree({
|
|
23628
|
+
projectDir: resolvedProjectDir,
|
|
23629
|
+
worktree,
|
|
23630
|
+
defaultBranch,
|
|
23631
|
+
runGit,
|
|
23632
|
+
maxCommitEntries
|
|
23633
|
+
})));
|
|
23634
|
+
worktrees.sort((a, b) => {
|
|
23635
|
+
if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1;
|
|
23636
|
+
return a.branchName.localeCompare(b.branchName);
|
|
23637
|
+
});
|
|
23638
|
+
return {
|
|
23639
|
+
defaultBranch,
|
|
23640
|
+
worktrees
|
|
23641
|
+
};
|
|
23642
|
+
}
|
|
23643
|
+
|
|
23644
|
+
//#endregion
|
|
23645
|
+
//#region ../server/src/dashboard-time-trends.ts
|
|
23646
|
+
const MIN_TREND_POINT_LIMIT = 20;
|
|
23647
|
+
const MAX_TREND_POINT_LIMIT = 500;
|
|
23648
|
+
const DEFAULT_TREND_POINT_LIMIT = 100;
|
|
23649
|
+
const TARGET_TREND_BARS = 20;
|
|
23650
|
+
const DAY_MS = 1440 * 60 * 1e3;
|
|
23651
|
+
function clampPointLimit(pointLimit) {
|
|
23652
|
+
if (!Number.isFinite(pointLimit)) return DEFAULT_TREND_POINT_LIMIT;
|
|
23653
|
+
return Math.max(MIN_TREND_POINT_LIMIT, Math.min(MAX_TREND_POINT_LIMIT, Math.trunc(pointLimit)));
|
|
23654
|
+
}
|
|
23655
|
+
function createEmptyTrendSeries() {
|
|
23656
|
+
return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
|
|
23657
|
+
}
|
|
23658
|
+
function normalizeEvents(events, pointLimit) {
|
|
23659
|
+
return events.filter((event) => Number.isFinite(event.ts) && event.ts > 0 && Number.isFinite(event.value)).sort((a, b) => a.ts - b.ts).slice(-pointLimit);
|
|
23660
|
+
}
|
|
23661
|
+
function buildTimeWindow(options) {
|
|
23662
|
+
const { probeEvents, targetBars, rightEdgeTs } = options;
|
|
23663
|
+
if (probeEvents.length === 0) return null;
|
|
23664
|
+
const probeEnd = probeEvents[probeEvents.length - 1].ts;
|
|
23665
|
+
const end = typeof rightEdgeTs === "number" && Number.isFinite(rightEdgeTs) && rightEdgeTs > 0 ? Math.max(probeEnd, rightEdgeTs) : probeEnd;
|
|
23666
|
+
const probeStart = probeEvents[0].ts;
|
|
23667
|
+
const rangeMs = Math.max(1, end - probeStart);
|
|
23668
|
+
const bucketMs = rangeMs >= DAY_MS ? Math.max(DAY_MS, Math.ceil(rangeMs / targetBars / DAY_MS) * DAY_MS) : Math.max(1, Math.ceil(rangeMs / targetBars));
|
|
23669
|
+
const windowStart = end - bucketMs * targetBars;
|
|
23670
|
+
return {
|
|
23671
|
+
windowStart,
|
|
23672
|
+
bucketMs,
|
|
23673
|
+
bucketEnds: Array.from({ length: targetBars }, (_, index) => windowStart + bucketMs * (index + 1))
|
|
23674
|
+
};
|
|
23675
|
+
}
|
|
23676
|
+
function bucketizeTrend(events, reducer, rightEdgeTs) {
|
|
23677
|
+
if (events.length === 0) return [];
|
|
23678
|
+
const timeWindow = buildTimeWindow({
|
|
23679
|
+
probeEvents: events,
|
|
23680
|
+
targetBars: TARGET_TREND_BARS,
|
|
23681
|
+
rightEdgeTs
|
|
23682
|
+
});
|
|
23683
|
+
if (!timeWindow) return [];
|
|
23684
|
+
const { windowStart, bucketMs, bucketEnds } = timeWindow;
|
|
23685
|
+
const sums = Array.from({ length: bucketEnds.length }, () => 0);
|
|
23686
|
+
const counts = Array.from({ length: bucketEnds.length }, () => 0);
|
|
23687
|
+
let baseline = 0;
|
|
23688
|
+
for (const event of events) {
|
|
23689
|
+
if (event.ts <= windowStart) {
|
|
23690
|
+
if (reducer === "sum-cumulative") baseline += event.value;
|
|
23691
|
+
continue;
|
|
23692
|
+
}
|
|
23693
|
+
const offset = event.ts - windowStart;
|
|
23694
|
+
const index = Math.max(0, Math.min(bucketEnds.length - 1, Math.ceil(offset / bucketMs) - 1));
|
|
23695
|
+
sums[index] += event.value;
|
|
23696
|
+
counts[index] += 1;
|
|
23697
|
+
}
|
|
23698
|
+
let cumulative = baseline;
|
|
23699
|
+
let carry = baseline !== 0 ? baseline : events[0].value;
|
|
23700
|
+
return bucketEnds.map((ts, index) => {
|
|
23701
|
+
if (reducer === "sum") return {
|
|
23702
|
+
ts,
|
|
23703
|
+
value: sums[index]
|
|
23704
|
+
};
|
|
23705
|
+
if (reducer === "sum-cumulative") {
|
|
23706
|
+
cumulative += sums[index];
|
|
23707
|
+
return {
|
|
23708
|
+
ts,
|
|
23709
|
+
value: cumulative
|
|
23710
|
+
};
|
|
23711
|
+
}
|
|
23712
|
+
if (counts[index] > 0) carry = sums[index] / counts[index];
|
|
23713
|
+
return {
|
|
23714
|
+
ts,
|
|
23715
|
+
value: carry
|
|
23716
|
+
};
|
|
23717
|
+
});
|
|
23718
|
+
}
|
|
23719
|
+
function buildDashboardTimeTrends(options) {
|
|
23720
|
+
const pointLimit = clampPointLimit(options.pointLimit);
|
|
23721
|
+
const trends = createEmptyTrendSeries();
|
|
23722
|
+
for (const metric of DASHBOARD_METRIC_KEYS) {
|
|
23723
|
+
if (options.availability[metric].state !== "ok") continue;
|
|
23724
|
+
trends[metric] = bucketizeTrend(normalizeEvents(options.events[metric], pointLimit), options.reducers?.[metric] ?? "sum", options.rightEdgeTs);
|
|
23725
|
+
}
|
|
23726
|
+
return {
|
|
23727
|
+
trends,
|
|
23728
|
+
trendMeta: {
|
|
23729
|
+
pointLimit,
|
|
23730
|
+
lastUpdatedAt: options.timestamp
|
|
23731
|
+
}
|
|
23732
|
+
};
|
|
23733
|
+
}
|
|
23734
|
+
|
|
23350
23735
|
//#endregion
|
|
23351
23736
|
//#region ../server/src/reactive-kv.ts
|
|
23352
23737
|
/**
|
|
@@ -23467,6 +23852,76 @@ function createReactiveSubscriptionWithInput(task) {
|
|
|
23467
23852
|
const t = initTRPC.context().create();
|
|
23468
23853
|
const router = t.router;
|
|
23469
23854
|
const publicProcedure = t.procedure;
|
|
23855
|
+
const execFileAsync = promisify$1(execFile);
|
|
23856
|
+
const dashboardGitTaskStatusEmitter = new EventEmitter$1();
|
|
23857
|
+
dashboardGitTaskStatusEmitter.setMaxListeners(200);
|
|
23858
|
+
const dashboardGitTaskStatus = {
|
|
23859
|
+
running: false,
|
|
23860
|
+
inFlight: 0,
|
|
23861
|
+
lastStartedAt: null,
|
|
23862
|
+
lastFinishedAt: null,
|
|
23863
|
+
lastReason: null,
|
|
23864
|
+
lastError: null
|
|
23865
|
+
};
|
|
23866
|
+
function getDashboardGitTaskStatus() {
|
|
23867
|
+
return { ...dashboardGitTaskStatus };
|
|
23868
|
+
}
|
|
23869
|
+
function emitDashboardGitTaskStatus() {
|
|
23870
|
+
dashboardGitTaskStatusEmitter.emit("change", getDashboardGitTaskStatus());
|
|
23871
|
+
}
|
|
23872
|
+
function beginDashboardGitTask(reason) {
|
|
23873
|
+
dashboardGitTaskStatus.inFlight += 1;
|
|
23874
|
+
dashboardGitTaskStatus.running = true;
|
|
23875
|
+
dashboardGitTaskStatus.lastStartedAt = Date.now();
|
|
23876
|
+
dashboardGitTaskStatus.lastReason = reason;
|
|
23877
|
+
dashboardGitTaskStatus.lastError = null;
|
|
23878
|
+
emitDashboardGitTaskStatus();
|
|
23879
|
+
}
|
|
23880
|
+
function endDashboardGitTask(error) {
|
|
23881
|
+
dashboardGitTaskStatus.inFlight = Math.max(0, dashboardGitTaskStatus.inFlight - 1);
|
|
23882
|
+
dashboardGitTaskStatus.running = dashboardGitTaskStatus.inFlight > 0;
|
|
23883
|
+
dashboardGitTaskStatus.lastFinishedAt = Date.now();
|
|
23884
|
+
if (error) dashboardGitTaskStatus.lastError = error instanceof Error ? error.message : String(error);
|
|
23885
|
+
emitDashboardGitTaskStatus();
|
|
23886
|
+
}
|
|
23887
|
+
function parseGitDirFromDotGitFile(content) {
|
|
23888
|
+
const line = content.split(/\r?\n/).map((item) => item.trim()).find((item) => item.startsWith("gitdir:"));
|
|
23889
|
+
if (!line) return null;
|
|
23890
|
+
const rawPath = line.slice(7).trim();
|
|
23891
|
+
return rawPath.length > 0 ? rawPath : null;
|
|
23892
|
+
}
|
|
23893
|
+
function getDashboardGitRefreshStampPath(projectDir) {
|
|
23894
|
+
return join$1(projectDir, "openspec", ".openspecui-dashboard-git-refresh.stamp");
|
|
23895
|
+
}
|
|
23896
|
+
async function touchDashboardGitRefreshStamp(projectDir, reason) {
|
|
23897
|
+
const stampPath = getDashboardGitRefreshStampPath(projectDir);
|
|
23898
|
+
await mkdir$1(dirname$1(stampPath), { recursive: true });
|
|
23899
|
+
await writeFile$1(stampPath, `${Date.now()} ${reason}\n`, "utf8");
|
|
23900
|
+
}
|
|
23901
|
+
async function registerDashboardGitReactiveDeps(projectDir) {
|
|
23902
|
+
await reactiveReadDir(projectDir, {
|
|
23903
|
+
includeHidden: true,
|
|
23904
|
+
exclude: ["node_modules"]
|
|
23905
|
+
});
|
|
23906
|
+
await reactiveReadFile(getDashboardGitRefreshStampPath(projectDir));
|
|
23907
|
+
const dotGitPath = join$1(projectDir, ".git");
|
|
23908
|
+
if (!await reactiveExists(dotGitPath)) return;
|
|
23909
|
+
const dotGitFileContent = await reactiveReadFile(dotGitPath);
|
|
23910
|
+
if (dotGitFileContent !== null) {
|
|
23911
|
+
const gitDirRaw = parseGitDirFromDotGitFile(dotGitFileContent);
|
|
23912
|
+
if (!gitDirRaw) return;
|
|
23913
|
+
const gitDirPath = resolve$1(projectDir, gitDirRaw);
|
|
23914
|
+
await reactiveReadDir(gitDirPath, { includeHidden: true });
|
|
23915
|
+
await reactiveReadFile(join$1(gitDirPath, "HEAD"));
|
|
23916
|
+
await reactiveReadFile(join$1(gitDirPath, "index"));
|
|
23917
|
+
await reactiveReadFile(join$1(gitDirPath, "packed-refs"));
|
|
23918
|
+
return;
|
|
23919
|
+
}
|
|
23920
|
+
await reactiveReadDir(dotGitPath, { includeHidden: true });
|
|
23921
|
+
await reactiveReadFile(join$1(dotGitPath, "HEAD"));
|
|
23922
|
+
await reactiveReadFile(join$1(dotGitPath, "index"));
|
|
23923
|
+
await reactiveReadFile(join$1(dotGitPath, "packed-refs"));
|
|
23924
|
+
}
|
|
23470
23925
|
function requireChangeId(changeId) {
|
|
23471
23926
|
if (!changeId) throw new Error("change is required");
|
|
23472
23927
|
return changeId;
|
|
@@ -23548,6 +24003,200 @@ function buildSystemStatus(ctx) {
|
|
|
23548
24003
|
watcherLastReinitializeReason: runtime?.lastReinitializeReason ?? null
|
|
23549
24004
|
};
|
|
23550
24005
|
}
|
|
24006
|
+
function resolveTrendTimestamp(primary, secondary) {
|
|
24007
|
+
if (typeof primary === "number" && Number.isFinite(primary) && primary > 0) return primary;
|
|
24008
|
+
if (typeof secondary === "number" && Number.isFinite(secondary) && secondary > 0) return secondary;
|
|
24009
|
+
return null;
|
|
24010
|
+
}
|
|
24011
|
+
function parseDatedIdTimestamp(id) {
|
|
24012
|
+
const match$1 = /^(\d{4})-(\d{2})-(\d{2})(?:-|$)/.exec(id);
|
|
24013
|
+
if (!match$1) return null;
|
|
24014
|
+
const year = Number(match$1[1]);
|
|
24015
|
+
const month = Number(match$1[2]);
|
|
24016
|
+
const day = Number(match$1[3]);
|
|
24017
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return null;
|
|
24018
|
+
if (month < 1 || month > 12) return null;
|
|
24019
|
+
if (day < 1 || day > 31) return null;
|
|
24020
|
+
const ts = Date.UTC(year, month - 1, day);
|
|
24021
|
+
return Number.isFinite(ts) ? ts : null;
|
|
24022
|
+
}
|
|
24023
|
+
function createEmptyTriColorTrends() {
|
|
24024
|
+
return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
|
|
24025
|
+
}
|
|
24026
|
+
async function readLatestCommitTimestamp(projectDir) {
|
|
24027
|
+
try {
|
|
24028
|
+
const { stdout } = await execFileAsync("git", [
|
|
24029
|
+
"log",
|
|
24030
|
+
"-1",
|
|
24031
|
+
"--format=%ct"
|
|
24032
|
+
], {
|
|
24033
|
+
cwd: projectDir,
|
|
24034
|
+
maxBuffer: 1024 * 1024,
|
|
24035
|
+
encoding: "utf8"
|
|
24036
|
+
});
|
|
24037
|
+
const seconds = Number(stdout.trim());
|
|
24038
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : null;
|
|
24039
|
+
} catch {
|
|
24040
|
+
return null;
|
|
24041
|
+
}
|
|
24042
|
+
}
|
|
24043
|
+
async function fetchDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
24044
|
+
if (contextStorage.getStore()) await registerDashboardGitReactiveDeps(ctx.projectDir);
|
|
24045
|
+
const now = Date.now();
|
|
24046
|
+
const [specMetas, changeMetas, archiveMetas] = await Promise.all([
|
|
24047
|
+
ctx.adapter.listSpecsWithMeta(),
|
|
24048
|
+
ctx.adapter.listChangesWithMeta(),
|
|
24049
|
+
ctx.adapter.listArchivedChangesWithMeta()
|
|
24050
|
+
]);
|
|
24051
|
+
const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
|
|
24052
|
+
const change = await ctx.adapter.readArchivedChange(meta.id);
|
|
24053
|
+
if (!change) return null;
|
|
24054
|
+
return {
|
|
24055
|
+
id: meta.id,
|
|
24056
|
+
createdAt: meta.createdAt,
|
|
24057
|
+
updatedAt: meta.updatedAt,
|
|
24058
|
+
tasksCompleted: change.tasks.filter((task) => task.completed).length
|
|
24059
|
+
};
|
|
24060
|
+
}))).filter((item) => item !== null);
|
|
24061
|
+
const specifications = (await Promise.all(specMetas.map(async (meta) => {
|
|
24062
|
+
const spec = await ctx.adapter.readSpec(meta.id);
|
|
24063
|
+
if (!spec) return null;
|
|
24064
|
+
return {
|
|
24065
|
+
id: meta.id,
|
|
24066
|
+
name: meta.name,
|
|
24067
|
+
requirements: spec.requirements.length,
|
|
24068
|
+
updatedAt: meta.updatedAt
|
|
24069
|
+
};
|
|
24070
|
+
}))).filter((item) => item !== null).sort((a, b) => b.requirements - a.requirements || b.updatedAt - a.updatedAt);
|
|
24071
|
+
const activeChanges = changeMetas.map((change) => ({
|
|
24072
|
+
id: change.id,
|
|
24073
|
+
name: change.name,
|
|
24074
|
+
progress: change.progress,
|
|
24075
|
+
updatedAt: change.updatedAt
|
|
24076
|
+
}));
|
|
24077
|
+
const requirements = specifications.reduce((sum, spec) => sum + spec.requirements, 0);
|
|
24078
|
+
const tasksTotal = activeChanges.reduce((sum, change) => sum + change.progress.total, 0);
|
|
24079
|
+
const tasksCompleted = activeChanges.reduce((sum, change) => sum + change.progress.completed, 0);
|
|
24080
|
+
const archivedTasksCompleted = archivedChanges.reduce((sum, change) => sum + change.tasksCompleted, 0);
|
|
24081
|
+
const taskCompletionPercent = tasksTotal > 0 ? Math.round(tasksCompleted / tasksTotal * 100) : null;
|
|
24082
|
+
const inProgressChanges = activeChanges.filter((change) => change.progress.total > 0 && change.progress.completed < change.progress.total).length;
|
|
24083
|
+
const specificationTrendEvents = specMetas.flatMap((spec) => {
|
|
24084
|
+
const ts = resolveTrendTimestamp(spec.createdAt, spec.updatedAt);
|
|
24085
|
+
return ts === null ? [] : [{
|
|
24086
|
+
ts,
|
|
24087
|
+
value: 1
|
|
24088
|
+
}];
|
|
24089
|
+
});
|
|
24090
|
+
const completedTrendEvents = archivedChanges.flatMap((archive) => {
|
|
24091
|
+
const ts = parseDatedIdTimestamp(archive.id) ?? resolveTrendTimestamp(archive.updatedAt, archive.createdAt);
|
|
24092
|
+
return ts === null ? [] : [{
|
|
24093
|
+
ts,
|
|
24094
|
+
value: archive.tasksCompleted
|
|
24095
|
+
}];
|
|
24096
|
+
});
|
|
24097
|
+
const specMetaById = new Map(specMetas.map((meta) => [meta.id, meta]));
|
|
24098
|
+
const requirementTrendEvents = specifications.flatMap((spec) => {
|
|
24099
|
+
const meta = specMetaById.get(spec.id);
|
|
24100
|
+
const ts = resolveTrendTimestamp(meta?.updatedAt, meta?.createdAt);
|
|
24101
|
+
return ts === null ? [] : [{
|
|
24102
|
+
ts,
|
|
24103
|
+
value: spec.requirements
|
|
24104
|
+
}];
|
|
24105
|
+
});
|
|
24106
|
+
const hasObjectiveSpecificationTrend = specificationTrendEvents.length > 0 || specifications.length === 0;
|
|
24107
|
+
const hasObjectiveRequirementTrend = requirementTrendEvents.length > 0 || requirements === 0;
|
|
24108
|
+
const hasObjectiveCompletedTrend = completedTrendEvents.length > 0 || archiveMetas.length === 0;
|
|
24109
|
+
const config = await ctx.configManager.readConfig();
|
|
24110
|
+
beginDashboardGitTask(reason);
|
|
24111
|
+
let latestCommitTs = null;
|
|
24112
|
+
let git;
|
|
24113
|
+
try {
|
|
24114
|
+
const gitSnapshotPromise = buildDashboardGitSnapshot({ projectDir: ctx.projectDir }).catch(() => ({
|
|
24115
|
+
defaultBranch: "main",
|
|
24116
|
+
worktrees: []
|
|
24117
|
+
}));
|
|
24118
|
+
latestCommitTs = await readLatestCommitTimestamp(ctx.projectDir);
|
|
24119
|
+
git = await gitSnapshotPromise;
|
|
24120
|
+
} catch (error) {
|
|
24121
|
+
endDashboardGitTask(error);
|
|
24122
|
+
throw error;
|
|
24123
|
+
}
|
|
24124
|
+
endDashboardGitTask(null);
|
|
24125
|
+
const cardAvailability = {
|
|
24126
|
+
specifications: hasObjectiveSpecificationTrend ? { state: "ok" } : {
|
|
24127
|
+
state: "invalid",
|
|
24128
|
+
reason: "objective-history-unavailable"
|
|
24129
|
+
},
|
|
24130
|
+
requirements: hasObjectiveRequirementTrend ? { state: "ok" } : {
|
|
24131
|
+
state: "invalid",
|
|
24132
|
+
reason: "objective-history-unavailable"
|
|
24133
|
+
},
|
|
24134
|
+
activeChanges: {
|
|
24135
|
+
state: "invalid",
|
|
24136
|
+
reason: "objective-history-unavailable"
|
|
24137
|
+
},
|
|
24138
|
+
inProgressChanges: {
|
|
24139
|
+
state: "invalid",
|
|
24140
|
+
reason: "objective-history-unavailable"
|
|
24141
|
+
},
|
|
24142
|
+
completedChanges: hasObjectiveCompletedTrend ? { state: "ok" } : {
|
|
24143
|
+
state: "invalid",
|
|
24144
|
+
reason: "objective-history-unavailable"
|
|
24145
|
+
},
|
|
24146
|
+
taskCompletionPercent: {
|
|
24147
|
+
state: "invalid",
|
|
24148
|
+
reason: taskCompletionPercent === null ? "semantic-uncomputable" : "objective-history-unavailable"
|
|
24149
|
+
}
|
|
24150
|
+
};
|
|
24151
|
+
const trendKinds = {
|
|
24152
|
+
specifications: "monotonic",
|
|
24153
|
+
requirements: "monotonic",
|
|
24154
|
+
activeChanges: "bidirectional",
|
|
24155
|
+
inProgressChanges: "bidirectional",
|
|
24156
|
+
completedChanges: "monotonic",
|
|
24157
|
+
taskCompletionPercent: "bidirectional"
|
|
24158
|
+
};
|
|
24159
|
+
const { trends: baselineTrends, trendMeta } = buildDashboardTimeTrends({
|
|
24160
|
+
pointLimit: config.dashboard.trendPointLimit,
|
|
24161
|
+
timestamp: now,
|
|
24162
|
+
rightEdgeTs: latestCommitTs,
|
|
24163
|
+
availability: cardAvailability,
|
|
24164
|
+
events: {
|
|
24165
|
+
specifications: specificationTrendEvents,
|
|
24166
|
+
requirements: requirementTrendEvents,
|
|
24167
|
+
activeChanges: [],
|
|
24168
|
+
inProgressChanges: [],
|
|
24169
|
+
completedChanges: completedTrendEvents,
|
|
24170
|
+
taskCompletionPercent: []
|
|
24171
|
+
},
|
|
24172
|
+
reducers: {
|
|
24173
|
+
specifications: "sum",
|
|
24174
|
+
requirements: "sum",
|
|
24175
|
+
completedChanges: "sum"
|
|
24176
|
+
}
|
|
24177
|
+
});
|
|
24178
|
+
return {
|
|
24179
|
+
summary: {
|
|
24180
|
+
specifications: specifications.length,
|
|
24181
|
+
requirements,
|
|
24182
|
+
activeChanges: activeChanges.length,
|
|
24183
|
+
inProgressChanges,
|
|
24184
|
+
completedChanges: archiveMetas.length,
|
|
24185
|
+
archivedTasksCompleted,
|
|
24186
|
+
tasksTotal,
|
|
24187
|
+
tasksCompleted,
|
|
24188
|
+
taskCompletionPercent
|
|
24189
|
+
},
|
|
24190
|
+
trends: baselineTrends,
|
|
24191
|
+
triColorTrends: createEmptyTriColorTrends(),
|
|
24192
|
+
trendKinds,
|
|
24193
|
+
cardAvailability,
|
|
24194
|
+
trendMeta,
|
|
24195
|
+
specifications,
|
|
24196
|
+
activeChanges,
|
|
24197
|
+
git
|
|
24198
|
+
};
|
|
24199
|
+
}
|
|
23551
24200
|
/**
|
|
23552
24201
|
* Spec router - spec CRUD operations
|
|
23553
24202
|
*/
|
|
@@ -23755,15 +24404,17 @@ const configRouter = router({
|
|
|
23755
24404
|
"dark",
|
|
23756
24405
|
"system"
|
|
23757
24406
|
]).optional(),
|
|
23758
|
-
terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional()
|
|
24407
|
+
terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
|
|
24408
|
+
dashboard: DashboardConfigSchema.partial().optional()
|
|
23759
24409
|
})).mutation(async ({ ctx, input }) => {
|
|
23760
24410
|
const hasCliCommand = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "command");
|
|
23761
24411
|
const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
|
|
23762
24412
|
if (hasCliCommand && !hasCliArgs) {
|
|
23763
24413
|
await ctx.configManager.setCliCommand(input.cli?.command ?? "");
|
|
23764
|
-
if (input.theme !== void 0 || input.terminal !== void 0) await ctx.configManager.writeConfig({
|
|
24414
|
+
if (input.theme !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0) await ctx.configManager.writeConfig({
|
|
23765
24415
|
theme: input.theme,
|
|
23766
|
-
terminal: input.terminal
|
|
24416
|
+
terminal: input.terminal,
|
|
24417
|
+
dashboard: input.dashboard
|
|
23767
24418
|
});
|
|
23768
24419
|
return { success: true };
|
|
23769
24420
|
}
|
|
@@ -24220,9 +24871,43 @@ const systemRouter = router({
|
|
|
24220
24871
|
})
|
|
24221
24872
|
});
|
|
24222
24873
|
/**
|
|
24874
|
+
* Dashboard router - objective project overview for UI
|
|
24875
|
+
*/
|
|
24876
|
+
const dashboardRouter = router({
|
|
24877
|
+
get: publicProcedure.query(async ({ ctx }) => {
|
|
24878
|
+
return fetchDashboardOverview(ctx, "dashboard.get");
|
|
24879
|
+
}),
|
|
24880
|
+
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
24881
|
+
return createReactiveSubscription(async () => {
|
|
24882
|
+
return fetchDashboardOverview(ctx, "dashboard.subscribe");
|
|
24883
|
+
});
|
|
24884
|
+
}),
|
|
24885
|
+
refreshGitSnapshot: publicProcedure.input(objectType({ reason: stringType().optional() }).optional()).mutation(async ({ ctx, input }) => {
|
|
24886
|
+
const reason = input?.reason?.trim() || "manual-refresh";
|
|
24887
|
+
await touchDashboardGitRefreshStamp(ctx.projectDir, reason);
|
|
24888
|
+
return { success: true };
|
|
24889
|
+
}),
|
|
24890
|
+
gitTaskStatus: publicProcedure.query(() => {
|
|
24891
|
+
return getDashboardGitTaskStatus();
|
|
24892
|
+
}),
|
|
24893
|
+
subscribeGitTaskStatus: publicProcedure.subscription(() => {
|
|
24894
|
+
return observable((emit) => {
|
|
24895
|
+
emit.next(getDashboardGitTaskStatus());
|
|
24896
|
+
const handler = (status) => {
|
|
24897
|
+
emit.next(status);
|
|
24898
|
+
};
|
|
24899
|
+
dashboardGitTaskStatusEmitter.on("change", handler);
|
|
24900
|
+
return () => {
|
|
24901
|
+
dashboardGitTaskStatusEmitter.off("change", handler);
|
|
24902
|
+
};
|
|
24903
|
+
});
|
|
24904
|
+
})
|
|
24905
|
+
});
|
|
24906
|
+
/**
|
|
24223
24907
|
* Main app router
|
|
24224
24908
|
*/
|
|
24225
24909
|
const appRouter = router({
|
|
24910
|
+
dashboard: dashboardRouter,
|
|
24226
24911
|
spec: specRouter,
|
|
24227
24912
|
change: changeRouter,
|
|
24228
24913
|
archive: archiveRouter,
|
|
@@ -24237,7 +24922,7 @@ const appRouter = router({
|
|
|
24237
24922
|
});
|
|
24238
24923
|
|
|
24239
24924
|
//#endregion
|
|
24240
|
-
//#region ../search/
|
|
24925
|
+
//#region ../search/src/node-worker-provider.ts
|
|
24241
24926
|
function requestId() {
|
|
24242
24927
|
return Math.random().toString(36).slice(2);
|
|
24243
24928
|
}
|
|
@@ -24685,4 +25370,4 @@ async function startServer$1(options = {}) {
|
|
|
24685
25370
|
}
|
|
24686
25371
|
|
|
24687
25372
|
//#endregion
|
|
24688
|
-
export { SchemaInfoSchema as a, CliExecutor as c,
|
|
25373
|
+
export { SchemaInfoSchema as a, CliExecutor as c, OpenSpecAdapter as d, __commonJS$1 as f, SchemaDetailSchema as i, ConfigManager as l, createServer$2 as n, SchemaResolutionSchema as o, __toESM$1 as p, require_dist as r, TemplatesSchema as s, startServer$1 as t, DEFAULT_CONFIG as u };
|