@iceinvein/agent-skills 0.1.24 → 0.1.26
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/README.md +1 -0
- package/package.json +1 -1
- package/skills/magpie/README.md +16 -1
- package/skills/magpie/bin/magpie.ts +60 -2
- package/skills/magpie/fixtures/example-pr/diff.patch +86 -0
- package/skills/magpie/fixtures/example-pr/findings.final.json +137 -0
- package/skills/magpie/fixtures/example-pr/post-status.json +5 -0
- package/skills/magpie/fixtures/example-pr/pr.json +21 -0
- package/skills/magpie/fixtures/fake-gh.sh +10 -1
- package/skills/magpie/package.json +1 -1
- package/skills/magpie/scripts/__tests__/cli.test.ts +119 -0
- package/skills/magpie/scripts/__tests__/diff-utils.test.ts +106 -0
- package/skills/magpie/scripts/__tests__/file-tree.test.ts +97 -0
- package/skills/magpie/scripts/__tests__/gh.test.ts +8 -0
- package/skills/magpie/scripts/__tests__/helper.test.ts +63 -0
- package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +10 -0
- package/skills/magpie/scripts/__tests__/post-cmd.test.ts +90 -1
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +210 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +2 -2
- package/skills/magpie/scripts/__tests__/render-action-bar.test.ts +66 -0
- package/skills/magpie/scripts/__tests__/render-annotation.test.ts +67 -0
- package/skills/magpie/scripts/__tests__/render-cmd.test.ts +46 -0
- package/skills/magpie/scripts/__tests__/render-diff.test.ts +84 -0
- package/skills/magpie/scripts/__tests__/render-file-tree.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +104 -182
- package/skills/magpie/scripts/__tests__/render-issues-list.test.ts +67 -0
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +86 -0
- package/skills/magpie/scripts/__tests__/server.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/types.test.ts +48 -3
- package/skills/magpie/scripts/diff-utils.ts +129 -0
- package/skills/magpie/scripts/file-tree.ts +74 -0
- package/skills/magpie/scripts/gh.ts +1 -0
- package/skills/magpie/scripts/helper.js +402 -192
- package/skills/magpie/scripts/post-cmd.ts +202 -0
- package/skills/magpie/scripts/preview-cmd.ts +300 -0
- package/skills/magpie/scripts/refresh.ts +16 -1
- package/skills/magpie/scripts/render-action-bar.ts +51 -0
- package/skills/magpie/scripts/render-annotation.ts +96 -0
- package/skills/magpie/scripts/render-cmd.ts +24 -1
- package/skills/magpie/scripts/render-diff.ts +125 -0
- package/skills/magpie/scripts/render-file-tree.ts +86 -0
- package/skills/magpie/scripts/render-findings.ts +165 -196
- package/skills/magpie/scripts/render-issues-list.ts +78 -0
- package/skills/magpie/scripts/render-progress.ts +74 -32
- package/skills/magpie/scripts/server.ts +43 -0
- package/skills/magpie/scripts/types.ts +13 -0
- package/skills/magpie/skill.json +4 -2
- package/skills/magpie/templates/styles.css +1288 -629
|
@@ -99,7 +99,30 @@ export async function runRender(runDir: string, page: 'progress' | 'findings'):
|
|
|
99
99
|
parseFinding(raw),
|
|
100
100
|
)
|
|
101
101
|
const postStatus = await readJson<PostStatusMap>(join(runDir, 'post-status.json'), {})
|
|
102
|
+
const prJson = await readJson<Record<string, unknown>>(join(runDir, 'pr.json'), {})
|
|
103
|
+
const prNumber = Number(prJson.number ?? 0)
|
|
104
|
+
const pr =
|
|
105
|
+
prNumber > 0
|
|
106
|
+
? {
|
|
107
|
+
number: prNumber,
|
|
108
|
+
branch: String(prJson.headRefName ?? '?'),
|
|
109
|
+
headSha: String(prJson.headRefOid ?? '?'),
|
|
110
|
+
}
|
|
111
|
+
: undefined
|
|
112
|
+
const filesArray = Array.isArray(prJson.files) ? (prJson.files as unknown[]) : []
|
|
113
|
+
const files = filesArray.map((f) => {
|
|
114
|
+
const entry = f as Record<string, unknown>
|
|
115
|
+
return {
|
|
116
|
+
path: String(entry.path ?? ''),
|
|
117
|
+
additions: Number(entry.additions ?? 0),
|
|
118
|
+
deletions: Number(entry.deletions ?? 0),
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
const diff = await readFile(join(runDir, 'diff.patch'), 'utf8').catch(() => '')
|
|
102
122
|
const outPath = await nextVersionedPath(screenDir, 'findings')
|
|
103
|
-
await renderFindingsToDisk(
|
|
123
|
+
await renderFindingsToDisk(
|
|
124
|
+
{ findings, postStatus, runId: basename(runDir), pr, files, diff },
|
|
125
|
+
outPath,
|
|
126
|
+
)
|
|
104
127
|
return 0
|
|
105
128
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { buildPairedLines, type DiffHunk, type DiffLine, type PairedLine } from './diff-utils.ts'
|
|
2
|
+
import { renderAnnotation } from './render-annotation.ts'
|
|
3
|
+
import type { PostStatusEntry, PostStatusMap, ReviewFinding } from './types.ts'
|
|
4
|
+
|
|
5
|
+
function esc(s: string): string {
|
|
6
|
+
return s.replace(/[&<>"']/g, (c) => {
|
|
7
|
+
const m: Record<string, string> = {
|
|
8
|
+
'&': '&',
|
|
9
|
+
'<': '<',
|
|
10
|
+
'>': '>',
|
|
11
|
+
'"': '"',
|
|
12
|
+
"'": ''',
|
|
13
|
+
}
|
|
14
|
+
return m[c] ?? c
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type RenderDiffInput = {
|
|
19
|
+
hunks: DiffHunk[]
|
|
20
|
+
findings: ReviewFinding[]
|
|
21
|
+
postStatus: PostStatusMap
|
|
22
|
+
selectedIds: Set<string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPosted(status: PostStatusEntry | undefined): boolean {
|
|
26
|
+
return status === 'posted'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function failedFrom(status: PostStatusEntry | undefined): { message: string } | undefined {
|
|
30
|
+
if (status && typeof status === 'object' && status.status === 'failed') {
|
|
31
|
+
return { message: status.message }
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function lineRow(line: DiffLine): string {
|
|
37
|
+
const cls = line.type === 'added' ? 'added' : line.type === 'removed' ? 'removed' : 'context'
|
|
38
|
+
const lineNo = line.type === 'removed' ? line.oldLineNo : line.newLineNo
|
|
39
|
+
const sign = line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' '
|
|
40
|
+
return `<div class="diff-row ${cls}"><span class="gutter">${lineNo ?? ''}</span><span class="sign">${sign}</span><span class="content">${esc(line.content)}</span></div>`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findingsByLine(findings: ReviewFinding[]): Map<number, ReviewFinding[]> {
|
|
44
|
+
const m = new Map<number, ReviewFinding[]>()
|
|
45
|
+
for (const f of findings) {
|
|
46
|
+
if (f.line == null) continue
|
|
47
|
+
const arr = m.get(f.line) ?? []
|
|
48
|
+
arr.push(f)
|
|
49
|
+
m.set(f.line, arr)
|
|
50
|
+
}
|
|
51
|
+
return m
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function renderUnifiedDiff(input: RenderDiffInput): string {
|
|
55
|
+
const { hunks, findings, postStatus, selectedIds } = input
|
|
56
|
+
if (hunks.length === 0) return `<div class="diff-empty">No changes</div>`
|
|
57
|
+
const byLine = findingsByLine(findings)
|
|
58
|
+
const parts: string[] = []
|
|
59
|
+
hunks.forEach((h, hi) => {
|
|
60
|
+
if (hi > 0) parts.push(`<div class="diff-hunk-sep">⋯</div>`)
|
|
61
|
+
for (const line of h.lines) {
|
|
62
|
+
parts.push(lineRow(line))
|
|
63
|
+
const targetLine = line.newLineNo ?? null
|
|
64
|
+
if (targetLine != null) {
|
|
65
|
+
const fs = byLine.get(targetLine)
|
|
66
|
+
if (fs) {
|
|
67
|
+
for (const f of fs) {
|
|
68
|
+
parts.push(
|
|
69
|
+
renderAnnotation(f, {
|
|
70
|
+
checked: selectedIds.has(f.id),
|
|
71
|
+
posted: isPosted(postStatus[f.id]),
|
|
72
|
+
failed: failedFrom(postStatus[f.id]),
|
|
73
|
+
asCard: false,
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
return parts.join('\n')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function pairedRow(pair: PairedLine): string {
|
|
85
|
+
const cellL =
|
|
86
|
+
pair.left == null
|
|
87
|
+
? `<div class="diff-cell empty"></div>`
|
|
88
|
+
: `<div class="diff-cell ${pair.left.type}"><span class="gutter">${pair.left.oldLineNo ?? pair.left.newLineNo ?? ''}</span><span class="sign">${pair.left.type === 'removed' ? '-' : ' '}</span><span class="content">${esc(pair.left.content)}</span></div>`
|
|
89
|
+
const cellR =
|
|
90
|
+
pair.right == null
|
|
91
|
+
? `<div class="diff-cell empty"></div>`
|
|
92
|
+
: `<div class="diff-cell ${pair.right.type}"><span class="gutter">${pair.right.newLineNo ?? pair.right.oldLineNo ?? ''}</span><span class="sign">${pair.right.type === 'added' ? '+' : ' '}</span><span class="content">${esc(pair.right.content)}</span></div>`
|
|
93
|
+
return `<div class="diff-row split">${cellL}${cellR}</div>`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function renderSplitDiff(input: RenderDiffInput): string {
|
|
97
|
+
const { hunks, findings, postStatus, selectedIds } = input
|
|
98
|
+
if (hunks.length === 0) return `<div class="diff-empty">No changes</div>`
|
|
99
|
+
const byLine = findingsByLine(findings)
|
|
100
|
+
const parts: string[] = []
|
|
101
|
+
hunks.forEach((h, hi) => {
|
|
102
|
+
if (hi > 0) parts.push(`<div class="diff-hunk-sep">⋯</div>`)
|
|
103
|
+
const pairs = buildPairedLines([h])
|
|
104
|
+
for (const pair of pairs) {
|
|
105
|
+
parts.push(pairedRow(pair))
|
|
106
|
+
const target = pair.right?.newLineNo ?? null
|
|
107
|
+
if (target != null) {
|
|
108
|
+
const fs = byLine.get(target)
|
|
109
|
+
if (fs) {
|
|
110
|
+
for (const f of fs) {
|
|
111
|
+
parts.push(
|
|
112
|
+
renderAnnotation(f, {
|
|
113
|
+
checked: selectedIds.has(f.id),
|
|
114
|
+
posted: isPosted(postStatus[f.id]),
|
|
115
|
+
failed: failedFrom(postStatus[f.id]),
|
|
116
|
+
asCard: false,
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
return parts.join('\n')
|
|
125
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildTree,
|
|
3
|
+
collapseTree,
|
|
4
|
+
countDirFindings,
|
|
5
|
+
type DirNode,
|
|
6
|
+
findingCountsBySeverity,
|
|
7
|
+
} from './file-tree.ts'
|
|
8
|
+
import type { PrFileEntry, ReviewFinding } from './types.ts'
|
|
9
|
+
|
|
10
|
+
function esc(s: string): string {
|
|
11
|
+
return s.replace(/[&<>"']/g, (c) => {
|
|
12
|
+
const m: Record<string, string> = {
|
|
13
|
+
'&': '&',
|
|
14
|
+
'<': '<',
|
|
15
|
+
'>': '>',
|
|
16
|
+
'"': '"',
|
|
17
|
+
"'": ''',
|
|
18
|
+
}
|
|
19
|
+
return m[c] ?? c
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pipsHtml(findings: ReviewFinding[], path: string): string {
|
|
24
|
+
const counts = findingCountsBySeverity(findings, path)
|
|
25
|
+
if (counts.length === 0) return ''
|
|
26
|
+
return counts
|
|
27
|
+
.map(
|
|
28
|
+
(c) =>
|
|
29
|
+
`<span class="sev-pip ${c.severity}" aria-label="${c.count} ${c.severity}">${c.count}</span>`,
|
|
30
|
+
)
|
|
31
|
+
.join('')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deltaHtml(f: PrFileEntry): string {
|
|
35
|
+
return `<span class="delta" aria-label="${f.additions} additions, ${f.deletions} deletions"><span class="add">+${f.additions}</span> <span class="del">-${f.deletions}</span></span>`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderFiles(files: PrFileEntry[], findings: ReviewFinding[], depth: number): string {
|
|
39
|
+
return [...files]
|
|
40
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
41
|
+
.map((file) => {
|
|
42
|
+
const fileName = file.path.split('/').pop() ?? file.path
|
|
43
|
+
return `<button type="button" class="tree-file" data-action="select-file" data-file="${esc(file.path)}" style="padding-left:${depth * 12 + 12}px">
|
|
44
|
+
<span class="name">${esc(fileName)}</span>
|
|
45
|
+
${pipsHtml(findings, file.path)}
|
|
46
|
+
${deltaHtml(file)}
|
|
47
|
+
</button>`
|
|
48
|
+
})
|
|
49
|
+
.join('')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderDirs(dirs: Map<string, DirNode>, findings: ReviewFinding[], depth: number): string {
|
|
53
|
+
return [...dirs.values()]
|
|
54
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
55
|
+
.map((dir) => {
|
|
56
|
+
const totalCount = countDirFindings(findings, dir)
|
|
57
|
+
const countBadge = totalCount > 0 ? `<span class="dir-count">${totalCount}</span>` : ''
|
|
58
|
+
return `<details class="tree-dir" open data-dir="${esc(dir.fullPath)}">
|
|
59
|
+
<summary style="padding-left:${depth * 12 + 6}px"><span class="chev"></span><span class="name">${esc(dir.name)}</span>${countBadge}</summary>
|
|
60
|
+
${renderDirs(dir.dirs, findings, depth + 1)}
|
|
61
|
+
${renderFiles(dir.files, findings, depth + 1)}
|
|
62
|
+
</details>`
|
|
63
|
+
})
|
|
64
|
+
.join('')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type RenderFileTreeInput = {
|
|
68
|
+
files: PrFileEntry[]
|
|
69
|
+
findings: ReviewFinding[]
|
|
70
|
+
selectedFile?: string | null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function renderFileTree(input: RenderFileTreeInput): string {
|
|
74
|
+
const tree = collapseTree(buildTree(input.files))
|
|
75
|
+
const generalCount = input.findings.filter((f) => !f.file).length
|
|
76
|
+
const overviewBadge = generalCount > 0 ? `<span class="dir-count">${generalCount}</span>` : ''
|
|
77
|
+
return `<aside class="file-rail" data-role="file-tree">
|
|
78
|
+
<button type="button" class="tree-overview" data-action="select-file" data-file="">
|
|
79
|
+
<span class="name">Overview</span>${overviewBadge}
|
|
80
|
+
</button>
|
|
81
|
+
<div class="tree-body">
|
|
82
|
+
${renderDirs(tree.dirs, input.findings, 0)}
|
|
83
|
+
${renderFiles(tree.files, input.findings, 0)}
|
|
84
|
+
</div>
|
|
85
|
+
</aside>`
|
|
86
|
+
}
|