@ifrankwang/openspec-agents 0.146.1 → 0.147.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/.mcp-server/cli.mjs +49 -163
- package/README.md +122 -59
- package/package.json +1 -1
- package/src/adapters/mcp-common/index.ts +3 -5
- package/src/adapters/opencode/index.ts +2 -2
- package/src/adapters/plugin-common/index.ts +0 -2
- package/src/core/index.ts +0 -3
- package/src/core/provider.ts +1 -3
- package/.mcp-server/dashboard/index.html +0 -295
- package/assets/dashboard/index.html +0 -295
- package/src/adapters/opencode/dashboard.ts +0 -64
- package/src/core/dashboard.ts +0 -101
- package/src/dashboard/page.ts +0 -19
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { createServer } from "node:http"
|
|
2
|
-
import type { Server } from "node:http"
|
|
3
|
-
import { readDashboardState } from "../../core/dashboard.ts"
|
|
4
|
-
import { getDashboardPage } from "../../dashboard/page.ts"
|
|
5
|
-
|
|
6
|
-
const BASE_PORT = 4519
|
|
7
|
-
const MAX_ATTEMPTS = 20
|
|
8
|
-
|
|
9
|
-
const servers = new Map<string, Server>()
|
|
10
|
-
|
|
11
|
-
export async function startDashboard(worktree: string): Promise<void> {
|
|
12
|
-
if (servers.has(worktree)) return
|
|
13
|
-
|
|
14
|
-
const pageHtml = getDashboardPage()
|
|
15
|
-
|
|
16
|
-
// 端口占用自动递增:EADDRINUSE 走 server error 事件(异步),经 once("error") + await listen
|
|
17
|
-
// 判定重试;其余启动期错误仅记录不中断。修复前 try/catch 同步捕获永不触发,换端口失效。
|
|
18
|
-
for (let port = BASE_PORT; port < BASE_PORT + MAX_ATTEMPTS; port++) {
|
|
19
|
-
try {
|
|
20
|
-
const server = createServer(async (req, res) => {
|
|
21
|
-
try {
|
|
22
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`)
|
|
23
|
-
if (url.pathname === "/api/state") {
|
|
24
|
-
try {
|
|
25
|
-
const data = await readDashboardState(worktree)
|
|
26
|
-
res.writeHead(200, { "content-type": "application/json;charset=utf-8" })
|
|
27
|
-
res.end(JSON.stringify(data ?? { active: false }))
|
|
28
|
-
} catch (err) {
|
|
29
|
-
res.writeHead(500, { "content-type": "application/json;charset=utf-8" })
|
|
30
|
-
res.end(JSON.stringify({ active: false, error: String(err) }))
|
|
31
|
-
}
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
res.writeHead(200, { "content-type": "text/html;charset=utf-8" })
|
|
35
|
-
res.end(pageHtml)
|
|
36
|
-
} catch (err) {
|
|
37
|
-
console.error("[dashboard]", (err as Error).message)
|
|
38
|
-
res.writeHead(500, { "content-type": "text/plain;charset=utf-8" })
|
|
39
|
-
res.end("internal error")
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
await new Promise<void>((resolve, reject) => {
|
|
43
|
-
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
44
|
-
if (err.code === "EADDRINUSE") {
|
|
45
|
-
reject(err)
|
|
46
|
-
} else {
|
|
47
|
-
console.error("[dashboard]", err.message)
|
|
48
|
-
resolve()
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
server.listen(port, "127.0.0.1", () => resolve())
|
|
52
|
-
})
|
|
53
|
-
servers.set(worktree, server)
|
|
54
|
-
console.log(`[dashboard] 编排进度看板 http://127.0.0.1:${port}`)
|
|
55
|
-
return
|
|
56
|
-
} catch {
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
console.error(
|
|
62
|
-
`[dashboard] 无法启动编排进度看板:端口 ${BASE_PORT}-${BASE_PORT + MAX_ATTEMPTS - 1} 均被占用`
|
|
63
|
-
)
|
|
64
|
-
}
|
package/src/core/dashboard.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import type { OrchestrateState } from "./types.ts"
|
|
2
|
-
import { readStateByWorktree, readStateByChangeId, getStateDir } from "./state.ts"
|
|
3
|
-
import { readdirSync, existsSync } from "node:fs"
|
|
4
|
-
import { readFile } from "node:fs/promises"
|
|
5
|
-
import path from "path"
|
|
6
|
-
import type { WorkItem, WorkItemType, WorkItemPhase, Verdict } from "./workflow/types.ts"
|
|
7
|
-
|
|
8
|
-
/** 看板卡片投影:WorkItem → 前端可渲染结构。 */
|
|
9
|
-
export interface WorkItemCard {
|
|
10
|
-
id: string
|
|
11
|
-
type: WorkItemType
|
|
12
|
-
title: string
|
|
13
|
-
description: string
|
|
14
|
-
phase: WorkItemPhase
|
|
15
|
-
suspended: boolean
|
|
16
|
-
suspendReason?: string
|
|
17
|
-
currentStep: string | null
|
|
18
|
-
labels: string[]
|
|
19
|
-
source: string
|
|
20
|
-
severity?: string
|
|
21
|
-
agentVerdicts: Array<{ stepId: string; agentKey: string; verdict: Verdict }>
|
|
22
|
-
children: Array<{ id: string; type: WorkItemType; title: string; phase: WorkItemPhase; severity?: string }>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** taskGroup → 看板投影(旧三面板视图已随单轨化移除)。 */
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* WorkItem → 看板卡片。
|
|
29
|
-
* tags 键为 `${stepId}:${agentKey}`,拆解为可渲染的 agent 裁决数组;
|
|
30
|
-
* metadata 仅暴露无下划线前缀的业务字段(suspend_reason),内部字段(_ 前缀)不泄露。
|
|
31
|
-
*/
|
|
32
|
-
function toWorkItemCard(item: WorkItem): WorkItemCard {
|
|
33
|
-
const agentVerdicts = Object.entries(item.tags).map(([key, verdict]) => {
|
|
34
|
-
const sep = key.indexOf(":")
|
|
35
|
-
return {
|
|
36
|
-
stepId: sep === -1 ? key : key.slice(0, sep),
|
|
37
|
-
agentKey: sep === -1 ? key : key.slice(sep + 1),
|
|
38
|
-
verdict,
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
const suspendReason =
|
|
42
|
-
typeof item.metadata["suspend_reason"] === "string" ? (item.metadata["suspend_reason"] as string) : undefined
|
|
43
|
-
return {
|
|
44
|
-
id: item.id,
|
|
45
|
-
type: item.type,
|
|
46
|
-
title: item.title,
|
|
47
|
-
description: item.description,
|
|
48
|
-
phase: item.phase,
|
|
49
|
-
suspended: item.suspended,
|
|
50
|
-
suspendReason,
|
|
51
|
-
currentStep: item.currentStep,
|
|
52
|
-
labels: item.labels,
|
|
53
|
-
source: item.source,
|
|
54
|
-
severity: item.severity,
|
|
55
|
-
agentVerdicts,
|
|
56
|
-
children: item.children.map((child) => ({
|
|
57
|
-
id: child.id,
|
|
58
|
-
type: child.type,
|
|
59
|
-
title: child.title,
|
|
60
|
-
phase: child.phase,
|
|
61
|
-
severity: child.severity,
|
|
62
|
-
})),
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** OrchestrateState → 看板投影(workItems 单轨 + workItemCards 5 列视图)。 */
|
|
67
|
-
function projectState(state: OrchestrateState) {
|
|
68
|
-
return {
|
|
69
|
-
active: true,
|
|
70
|
-
changeId: state.changeId,
|
|
71
|
-
currentTaskGroupId: state.taskGroupId,
|
|
72
|
-
baseBranch: state.baseBranch,
|
|
73
|
-
createdAt: state.createdAt,
|
|
74
|
-
updatedAt: state.updatedAt,
|
|
75
|
-
workItems: state.workItems,
|
|
76
|
-
workItemCards: state.workItems.map(toWorkItemCard),
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export async function readDashboardState(worktree: string, changeId?: string) {
|
|
81
|
-
if (changeId) {
|
|
82
|
-
const state = await readStateByWorktree(worktree, changeId)
|
|
83
|
-
if (!state) return null
|
|
84
|
-
return projectState(state)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const stateDir = getStateDir(worktree)
|
|
88
|
-
if (!existsSync(stateDir)) return null
|
|
89
|
-
const files = readdirSync(stateDir).filter(f => f.endsWith(".json"))
|
|
90
|
-
const results: ReturnType<typeof projectState>[] = []
|
|
91
|
-
for (const f of files) {
|
|
92
|
-
try {
|
|
93
|
-
const raw = await readFile(path.join(stateDir, f), "utf-8")
|
|
94
|
-
const state = JSON.parse(raw) as OrchestrateState
|
|
95
|
-
if (!state.changeId) continue
|
|
96
|
-
results.push(projectState(state))
|
|
97
|
-
} catch { /* skip unreadable */ }
|
|
98
|
-
}
|
|
99
|
-
if (results.length === 1) return results[0]
|
|
100
|
-
return results.length > 0 ? results : null
|
|
101
|
-
}
|
package/src/dashboard/page.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url"
|
|
2
|
-
import { readFileSync, existsSync } from "node:fs"
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
|
|
5
|
-
let _html: string | null = null
|
|
6
|
-
|
|
7
|
-
/** 页面资源定位:bundle 形态(ZCode 插件包)资源随 bundle 同目录放置;源码/包形态位于 ../../assets/dashboard/。 */
|
|
8
|
-
function resolveDashboardHtml(): string {
|
|
9
|
-
const __dirname = fileURLToPath(new URL(".", import.meta.url))
|
|
10
|
-
const bundled = join(__dirname, "dashboard", "index.html")
|
|
11
|
-
if (existsSync(bundled)) return bundled
|
|
12
|
-
return join(__dirname, "../../assets/dashboard/index.html")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function getDashboardPage(): string {
|
|
16
|
-
if (_html) return _html
|
|
17
|
-
_html = readFileSync(resolveDashboardHtml(), "utf-8")
|
|
18
|
-
return _html
|
|
19
|
-
}
|