@demon-utils/playwright 0.1.3 → 0.1.6

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/index.js.map CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/commentary.ts"],
3
+ "sources": ["node:path", "../src/commentary.ts", "../src/review.ts", "../src/git-context.ts", "../src/html-generator.ts", "../src/recorder.ts"],
4
4
  "sourcesContent": [
5
- "import type { Page } from \"@playwright/test\";\n\nexport interface ShowCommentaryOptions {\n selector: string;\n text: string;\n}\n\nconst TOOLTIP_ID = \"demon-commentary-tooltip\";\n\nexport async function showCommentary(\n page: Page,\n options: ShowCommentaryOptions,\n): Promise<void> {\n await page.evaluate(\n ({ selector, text, tooltipId }) => {\n const target = document.querySelector(selector);\n if (!target) {\n throw new Error(\n `demon commentary: element not found for selector \"${selector}\"`,\n );\n }\n\n // Remove any existing tooltip\n document.getElementById(tooltipId)?.remove();\n\n const rect = target.getBoundingClientRect();\n\n const tooltip = document.createElement(\"div\");\n tooltip.id = tooltipId;\n tooltip.textContent = text;\n\n const style = document.createElement(\"style\");\n style.setAttribute(\"data-demon-commentary\", \"\");\n style.textContent = `\n @keyframes demon-commentary-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n @keyframes demon-commentary-out {\n from {\n opacity: 1;\n transform: translateY(0);\n }\n to {\n opacity: 0;\n transform: translateY(8px);\n }\n }\n #${tooltipId} {\n position: fixed;\n z-index: 2147483647;\n background: #1a1a2e;\n color: #eee;\n padding: 8px 14px;\n border-radius: 6px;\n font: 14px/1.4 system-ui, sans-serif;\n max-width: 320px;\n box-shadow: 0 4px 12px rgba(0,0,0,0.3);\n pointer-events: none;\n animation: demon-commentary-in 0.3s ease-out forwards;\n }\n #${tooltipId}.demon-commentary-hiding {\n animation: demon-commentary-out 0.25s ease-in forwards;\n }\n `;\n\n // Remove previous style if any\n document.querySelector(\"style[data-demon-commentary]\")?.remove();\n document.head.appendChild(style);\n\n // Position below the target element, horizontally centered\n const top = rect.bottom + 10;\n const left = rect.left + rect.width / 2;\n tooltip.style.top = `${top}px`;\n tooltip.style.left = `${left}px`;\n tooltip.style.transform = `translateX(-50%)`;\n\n document.body.appendChild(tooltip);\n },\n { selector: options.selector, text: options.text, tooltipId: TOOLTIP_ID },\n );\n}\n\nexport async function hideCommentary(page: Page): Promise<void> {\n await page.evaluate((tooltipId) => {\n const tooltip = document.getElementById(tooltipId);\n if (!tooltip) return;\n\n tooltip.classList.add(\"demon-commentary-hiding\");\n\n tooltip.addEventListener(\n \"animationend\",\n () => {\n tooltip.remove();\n document.querySelector(\"style[data-demon-commentary]\")?.remove();\n },\n { once: true },\n );\n }, TOOLTIP_ID);\n\n // Wait for the animate-out to complete\n await page.waitForTimeout(300);\n}\n"
5
+ "function assertPath(path){if(typeof path!==\"string\")throw TypeError(\"Path must be a string. Received \"+JSON.stringify(path))}function normalizeStringPosix(path,allowAboveRoot){var res=\"\",lastSegmentLength=0,lastSlash=-1,dots=0,code;for(var i=0;i<=path.length;++i){if(i<path.length)code=path.charCodeAt(i);else if(code===47)break;else code=47;if(code===47){if(lastSlash===i-1||dots===1);else if(lastSlash!==i-1&&dots===2){if(res.length<2||lastSegmentLength!==2||res.charCodeAt(res.length-1)!==46||res.charCodeAt(res.length-2)!==46){if(res.length>2){var lastSlashIndex=res.lastIndexOf(\"/\");if(lastSlashIndex!==res.length-1){if(lastSlashIndex===-1)res=\"\",lastSegmentLength=0;else res=res.slice(0,lastSlashIndex),lastSegmentLength=res.length-1-res.lastIndexOf(\"/\");lastSlash=i,dots=0;continue}}else if(res.length===2||res.length===1){res=\"\",lastSegmentLength=0,lastSlash=i,dots=0;continue}}if(allowAboveRoot){if(res.length>0)res+=\"/..\";else res=\"..\";lastSegmentLength=2}}else{if(res.length>0)res+=\"/\"+path.slice(lastSlash+1,i);else res=path.slice(lastSlash+1,i);lastSegmentLength=i-lastSlash-1}lastSlash=i,dots=0}else if(code===46&&dots!==-1)++dots;else dots=-1}return res}function _format(sep,pathObject){var dir=pathObject.dir||pathObject.root,base=pathObject.base||(pathObject.name||\"\")+(pathObject.ext||\"\");if(!dir)return base;if(dir===pathObject.root)return dir+base;return dir+sep+base}function resolve(){var resolvedPath=\"\",resolvedAbsolute=!1,cwd;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path;if(i>=0)path=arguments[i];else{if(cwd===void 0)cwd=process.cwd();path=cwd}if(assertPath(path),path.length===0)continue;resolvedPath=path+\"/\"+resolvedPath,resolvedAbsolute=path.charCodeAt(0)===47}if(resolvedPath=normalizeStringPosix(resolvedPath,!resolvedAbsolute),resolvedAbsolute)if(resolvedPath.length>0)return\"/\"+resolvedPath;else return\"/\";else if(resolvedPath.length>0)return resolvedPath;else return\".\"}function normalize(path){if(assertPath(path),path.length===0)return\".\";var isAbsolute=path.charCodeAt(0)===47,trailingSeparator=path.charCodeAt(path.length-1)===47;if(path=normalizeStringPosix(path,!isAbsolute),path.length===0&&!isAbsolute)path=\".\";if(path.length>0&&trailingSeparator)path+=\"/\";if(isAbsolute)return\"/\"+path;return path}function isAbsolute(path){return assertPath(path),path.length>0&&path.charCodeAt(0)===47}function join(){if(arguments.length===0)return\".\";var joined;for(var i=0;i<arguments.length;++i){var arg=arguments[i];if(assertPath(arg),arg.length>0)if(joined===void 0)joined=arg;else joined+=\"/\"+arg}if(joined===void 0)return\".\";return normalize(joined)}function relative(from,to){if(assertPath(from),assertPath(to),from===to)return\"\";if(from=resolve(from),to=resolve(to),from===to)return\"\";var fromStart=1;for(;fromStart<from.length;++fromStart)if(from.charCodeAt(fromStart)!==47)break;var fromEnd=from.length,fromLen=fromEnd-fromStart,toStart=1;for(;toStart<to.length;++toStart)if(to.charCodeAt(toStart)!==47)break;var toEnd=to.length,toLen=toEnd-toStart,length=fromLen<toLen?fromLen:toLen,lastCommonSep=-1,i=0;for(;i<=length;++i){if(i===length){if(toLen>length){if(to.charCodeAt(toStart+i)===47)return to.slice(toStart+i+1);else if(i===0)return to.slice(toStart+i)}else if(fromLen>length){if(from.charCodeAt(fromStart+i)===47)lastCommonSep=i;else if(i===0)lastCommonSep=0}break}var fromCode=from.charCodeAt(fromStart+i),toCode=to.charCodeAt(toStart+i);if(fromCode!==toCode)break;else if(fromCode===47)lastCommonSep=i}var out=\"\";for(i=fromStart+lastCommonSep+1;i<=fromEnd;++i)if(i===fromEnd||from.charCodeAt(i)===47)if(out.length===0)out+=\"..\";else out+=\"/..\";if(out.length>0)return out+to.slice(toStart+lastCommonSep);else{if(toStart+=lastCommonSep,to.charCodeAt(toStart)===47)++toStart;return to.slice(toStart)}}function _makeLong(path){return path}function dirname(path){if(assertPath(path),path.length===0)return\".\";var code=path.charCodeAt(0),hasRoot=code===47,end=-1,matchedSlash=!0;for(var i=path.length-1;i>=1;--i)if(code=path.charCodeAt(i),code===47){if(!matchedSlash){end=i;break}}else matchedSlash=!1;if(end===-1)return hasRoot?\"/\":\".\";if(hasRoot&&end===1)return\"//\";return path.slice(0,end)}function basename(path,ext){if(ext!==void 0&&typeof ext!==\"string\")throw TypeError('\"ext\" argument must be a string');assertPath(path);var start=0,end=-1,matchedSlash=!0,i;if(ext!==void 0&&ext.length>0&&ext.length<=path.length){if(ext.length===path.length&&ext===path)return\"\";var extIdx=ext.length-1,firstNonSlashEnd=-1;for(i=path.length-1;i>=0;--i){var code=path.charCodeAt(i);if(code===47){if(!matchedSlash){start=i+1;break}}else{if(firstNonSlashEnd===-1)matchedSlash=!1,firstNonSlashEnd=i+1;if(extIdx>=0)if(code===ext.charCodeAt(extIdx)){if(--extIdx===-1)end=i}else extIdx=-1,end=firstNonSlashEnd}}if(start===end)end=firstNonSlashEnd;else if(end===-1)end=path.length;return path.slice(start,end)}else{for(i=path.length-1;i>=0;--i)if(path.charCodeAt(i)===47){if(!matchedSlash){start=i+1;break}}else if(end===-1)matchedSlash=!1,end=i+1;if(end===-1)return\"\";return path.slice(start,end)}}function extname(path){assertPath(path);var startDot=-1,startPart=0,end=-1,matchedSlash=!0,preDotState=0;for(var i=path.length-1;i>=0;--i){var code=path.charCodeAt(i);if(code===47){if(!matchedSlash){startPart=i+1;break}continue}if(end===-1)matchedSlash=!1,end=i+1;if(code===46){if(startDot===-1)startDot=i;else if(preDotState!==1)preDotState=1}else if(startDot!==-1)preDotState=-1}if(startDot===-1||end===-1||preDotState===0||preDotState===1&&startDot===end-1&&startDot===startPart+1)return\"\";return path.slice(startDot,end)}function format(pathObject){if(pathObject===null||typeof pathObject!==\"object\")throw TypeError('The \"pathObject\" argument must be of type Object. Received type '+typeof pathObject);return _format(\"/\",pathObject)}function parse(path){assertPath(path);var ret={root:\"\",dir:\"\",base:\"\",ext:\"\",name:\"\"};if(path.length===0)return ret;var code=path.charCodeAt(0),isAbsolute2=code===47,start;if(isAbsolute2)ret.root=\"/\",start=1;else start=0;var startDot=-1,startPart=0,end=-1,matchedSlash=!0,i=path.length-1,preDotState=0;for(;i>=start;--i){if(code=path.charCodeAt(i),code===47){if(!matchedSlash){startPart=i+1;break}continue}if(end===-1)matchedSlash=!1,end=i+1;if(code===46){if(startDot===-1)startDot=i;else if(preDotState!==1)preDotState=1}else if(startDot!==-1)preDotState=-1}if(startDot===-1||end===-1||preDotState===0||preDotState===1&&startDot===end-1&&startDot===startPart+1){if(end!==-1)if(startPart===0&&isAbsolute2)ret.base=ret.name=path.slice(1,end);else ret.base=ret.name=path.slice(startPart,end)}else{if(startPart===0&&isAbsolute2)ret.name=path.slice(1,startDot),ret.base=path.slice(1,end);else ret.name=path.slice(startPart,startDot),ret.base=path.slice(startPart,end);ret.ext=path.slice(startDot,end)}if(startPart>0)ret.dir=path.slice(0,startPart-1);else if(isAbsolute2)ret.dir=\"/\";return ret}var sep=\"/\",delimiter=\":\",posix=((p)=>(p.posix=p,p))({resolve,normalize,isAbsolute,join,relative,_makeLong,dirname,basename,extname,format,parse,sep,delimiter,win32:null,posix:null});var path_default=posix;export{sep,resolve,relative,posix,parse,normalize,join,isAbsolute,format,extname,dirname,delimiter,path_default as default,basename,_makeLong};",
6
+ "import type { Page } from \"@playwright/test\";\n\nexport interface ShowCommentaryOptions {\n selector: string;\n text: string;\n}\n\nconst TOOLTIP_ID = \"demon-commentary-tooltip\";\n\nexport async function showCommentary(\n page: Page,\n options: ShowCommentaryOptions,\n): Promise<void> {\n await page.evaluate(\n ({ selector, text, tooltipId }) => {\n const target = document.querySelector(selector);\n if (!target) {\n throw new Error(\n `demon commentary: element not found for selector \"${selector}\"`,\n );\n }\n\n // Remove any existing tooltip\n document.getElementById(tooltipId)?.remove();\n\n const rect = target.getBoundingClientRect();\n\n const tooltip = document.createElement(\"div\");\n tooltip.id = tooltipId;\n tooltip.textContent = text;\n\n const style = document.createElement(\"style\");\n style.setAttribute(\"data-demon-commentary\", \"\");\n style.textContent = `\n @keyframes demon-commentary-in {\n from {\n opacity: 0;\n transform: translateY(var(--demon-slide-y, 8px));\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n @keyframes demon-commentary-out {\n from {\n opacity: 1;\n transform: translateY(0);\n }\n to {\n opacity: 0;\n transform: translateY(var(--demon-slide-y, 8px));\n }\n }\n #${tooltipId} {\n --demon-slide-y: 8px;\n position: fixed;\n z-index: 2147483647;\n background: #1a1a2e;\n color: #eee;\n padding: 8px 14px;\n border-radius: 6px;\n font: 14px/1.4 system-ui, sans-serif;\n max-width: 320px;\n box-shadow: 0 4px 12px rgba(0,0,0,0.3);\n pointer-events: none;\n animation: demon-commentary-in 0.3s ease-out forwards;\n }\n #${tooltipId}.demon-commentary-hiding {\n animation: demon-commentary-out 0.25s ease-in forwards;\n }\n `;\n\n // Remove previous style if any\n document.querySelector(\"style[data-demon-commentary]\")?.remove();\n document.head.appendChild(style);\n\n // Append hidden to measure real dimensions\n tooltip.style.visibility = \"hidden\";\n document.body.appendChild(tooltip);\n const tooltipRect = tooltip.getBoundingClientRect();\n const tooltipWidth = tooltipRect.width;\n const tooltipHeight = tooltipRect.height;\n const viewportWidth = window.innerWidth;\n\n // Vertical: default below target, flip above if overflowing bottom\n let top = rect.bottom + 10;\n if (\n top + tooltipHeight > window.innerHeight &&\n rect.top - 10 - tooltipHeight >= 0\n ) {\n top = rect.top - 10 - tooltipHeight;\n tooltip.style.setProperty(\"--demon-slide-y\", \"-8px\");\n }\n\n // Horizontal: centered, clamped to viewport\n const left = Math.max(\n 4,\n Math.min(\n rect.left + rect.width / 2 - tooltipWidth / 2,\n viewportWidth - 4 - tooltipWidth,\n ),\n );\n\n tooltip.style.top = `${top}px`;\n tooltip.style.left = `${left}px`;\n tooltip.style.visibility = \"\";\n },\n { selector: options.selector, text: options.text, tooltipId: TOOLTIP_ID },\n );\n}\n\nexport async function hideCommentary(page: Page): Promise<void> {\n await page.evaluate((tooltipId) => {\n const tooltip = document.getElementById(tooltipId);\n if (!tooltip) return;\n\n tooltip.classList.add(\"demon-commentary-hiding\");\n\n tooltip.addEventListener(\n \"animationend\",\n () => {\n tooltip.remove();\n document.querySelector(\"style[data-demon-commentary]\")?.remove();\n },\n { once: true },\n );\n }, TOOLTIP_ID);\n\n // Wait for the animate-out to complete\n await page.waitForTimeout(300);\n}\n",
7
+ "export type SpawnFn = (\n cmd: string[],\n) => { exitCode: Promise<number>; stdout: ReadableStream<Uint8Array> };\n\nexport interface InvokeClaudeOptions {\n agent?: string;\n spawn?: SpawnFn;\n}\n\nconst GIT_DIFF_MAX_CHARS = 50_000;\n\nexport interface BuildReviewPromptOptions {\n filenames: string[];\n stepsMap: Record<string, Array<{ text: string; timestampSeconds: number }>>;\n gitDiff?: string;\n guidelines?: string[];\n}\n\nexport function buildReviewPrompt(options: BuildReviewPromptOptions): string {\n const { filenames, stepsMap, gitDiff, guidelines } = options;\n\n const demoEntries = filenames.map((f) => {\n const steps = stepsMap[f] ?? [];\n const stepLines = steps\n .map((s) => `- [${s.timestampSeconds}s] ${s.text}`)\n .join(\"\\n\");\n return `Video: ${f}\\nRecorded steps:\\n${stepLines || \"(no steps recorded)\"}`;\n });\n\n const sections: string[] = [];\n\n if (guidelines && guidelines.length > 0) {\n sections.push(`## Coding Guidelines\\n\\n${guidelines.join(\"\\n\\n\")}`);\n }\n\n if (gitDiff) {\n let diff = gitDiff;\n if (diff.length > GIT_DIFF_MAX_CHARS) {\n diff = diff.slice(0, GIT_DIFF_MAX_CHARS) + \"\\n\\n... (diff truncated at 50k characters)\";\n }\n sections.push(`## Git Diff\\n\\n\\`\\`\\`diff\\n${diff}\\n\\`\\`\\``);\n }\n\n sections.push(`## Demo Recordings\\n\\n${demoEntries.join(\"\\n\\n\")}`);\n\n return `You are a code reviewer. You are given a git diff, coding guidelines, and demo recordings that show the feature in action.\n\n${sections.join(\"\\n\\n\")}\n\n## Task\n\nReview the code changes and demo recordings. Generate a JSON object matching this exact schema:\n\n{\n \"demos\": [\n {\n \"file\": \"<filename>\",\n \"summary\": \"<a meaningful sentence describing what this demo showcases based on the steps>\"\n }\n ],\n \"review\": {\n \"summary\": \"<2-3 sentence overview of the changes>\",\n \"highlights\": [\"<positive aspect 1>\", \"<positive aspect 2>\"],\n \"verdict\": \"approve\" | \"request_changes\",\n \"verdictReason\": \"<one sentence justifying the verdict>\",\n \"issues\": [\n {\n \"severity\": \"major\" | \"minor\" | \"nit\",\n \"description\": \"<what the issue is and how to fix it>\"\n }\n ]\n }\n}\n\nRules:\n- Return ONLY the JSON object, no markdown fences or extra text.\n- Include one entry in \"demos\" for each filename, in the same order.\n- \"file\" must exactly match the provided filename.\n- \"verdict\" must be exactly \"approve\" or \"request_changes\".\n- Use \"request_changes\" if there are any \"major\" issues.\n- \"severity\" must be exactly \"major\", \"minor\", or \"nit\".\n- \"major\": bugs, security issues, broken functionality, guideline violations.\n- \"minor\": code quality, readability, missing edge cases.\n- \"nit\": style, naming, trivial improvements.\n- \"highlights\" must have at least one entry.\n- \"issues\" can be an empty array if there are no issues.\n- Verify that demo steps demonstrate the acceptance criteria being met.`;\n}\n\nexport async function invokeClaude(\n prompt: string,\n options?: InvokeClaudeOptions,\n): Promise<string> {\n const spawnFn = options?.spawn ?? defaultSpawn;\n const agent = options?.agent ?? \"claude\";\n const proc = spawnFn([agent, \"-p\", prompt]);\n\n const reader = proc.stdout.getReader();\n const chunks: Uint8Array[] = [];\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n const exitCode = await proc.exitCode;\n const output = new TextDecoder().decode(\n concatUint8Arrays(chunks),\n );\n\n if (exitCode !== 0) {\n throw new Error(\n `claude process exited with code ${exitCode}: ${output.trim()}`,\n );\n }\n\n return output.trim();\n}\n\nimport type { IssueSeverity, ReviewVerdict } from \"./review-types.ts\";\n\nexport interface LlmReviewResponse {\n demos: Array<{ file: string; summary: string }>;\n review: {\n summary: string;\n highlights: string[];\n verdict: ReviewVerdict;\n verdictReason: string;\n issues: Array<{ severity: IssueSeverity; description: string }>;\n };\n}\n\nconst VALID_VERDICTS: ReadonlySet<string> = new Set([\"approve\", \"request_changes\"]);\nconst VALID_SEVERITIES: ReadonlySet<string> = new Set([\"major\", \"minor\", \"nit\"]);\n\nexport function extractJson(raw: string): string {\n // Try raw string first\n try {\n JSON.parse(raw);\n return raw;\n } catch {\n // look for first { and last }\n }\n\n const start = raw.indexOf(\"{\");\n const end = raw.lastIndexOf(\"}\");\n if (start === -1 || end === -1 || end <= start) {\n throw new Error(`No JSON object found in LLM response: ${raw.slice(0, 200)}`);\n }\n\n return raw.slice(start, end + 1);\n}\n\nexport function parseLlmResponse(raw: string): LlmReviewResponse {\n const jsonStr = extractJson(raw);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(jsonStr);\n } catch {\n throw new Error(`Invalid JSON from LLM: ${raw.slice(0, 200)}`);\n }\n\n if (typeof parsed !== \"object\" || parsed === null || !(\"demos\" in parsed)) {\n throw new Error(\"Missing 'demos' array in review metadata\");\n }\n\n const obj = parsed as Record<string, unknown>;\n if (!Array.isArray(obj[\"demos\"])) {\n throw new Error(\"'demos' must be an array\");\n }\n\n for (const demo of obj[\"demos\"] as unknown[]) {\n if (typeof demo !== \"object\" || demo === null) {\n throw new Error(\"Each demo must be an object\");\n }\n const d = demo as Record<string, unknown>;\n\n if (typeof d[\"file\"] !== \"string\") {\n throw new Error(\"Each demo must have a 'file' string\");\n }\n if (typeof d[\"summary\"] !== \"string\") {\n throw new Error(\"Each demo must have a 'summary' string\");\n }\n }\n\n if (typeof obj[\"review\"] !== \"object\" || obj[\"review\"] === null) {\n throw new Error(\"Missing 'review' object in response\");\n }\n\n const review = obj[\"review\"] as Record<string, unknown>;\n\n if (typeof review[\"summary\"] !== \"string\") {\n throw new Error(\"review.summary must be a string\");\n }\n\n if (!Array.isArray(review[\"highlights\"])) {\n throw new Error(\"review.highlights must be an array\");\n }\n if (review[\"highlights\"].length === 0) {\n throw new Error(\"review.highlights must not be empty\");\n }\n for (const h of review[\"highlights\"]) {\n if (typeof h !== \"string\") {\n throw new Error(\"Each highlight must be a string\");\n }\n }\n\n if (typeof review[\"verdict\"] !== \"string\" || !VALID_VERDICTS.has(review[\"verdict\"])) {\n throw new Error(\"review.verdict must be 'approve' or 'request_changes'\");\n }\n\n if (typeof review[\"verdictReason\"] !== \"string\") {\n throw new Error(\"review.verdictReason must be a string\");\n }\n\n if (!Array.isArray(review[\"issues\"])) {\n throw new Error(\"review.issues must be an array\");\n }\n\n for (const issue of review[\"issues\"] as unknown[]) {\n if (typeof issue !== \"object\" || issue === null) {\n throw new Error(\"Each issue must be an object\");\n }\n const i = issue as Record<string, unknown>;\n if (typeof i[\"severity\"] !== \"string\" || !VALID_SEVERITIES.has(i[\"severity\"])) {\n throw new Error(\"Each issue severity must be 'major', 'minor', or 'nit'\");\n }\n if (typeof i[\"description\"] !== \"string\") {\n throw new Error(\"Each issue must have a 'description' string\");\n }\n }\n\n return parsed as LlmReviewResponse;\n}\n\nfunction defaultSpawn(\n cmd: string[],\n): { exitCode: Promise<number>; stdout: ReadableStream<Uint8Array> } {\n const [command, ...args] = cmd;\n const proc = Bun.spawn([command!, ...args], {\n stdout: \"pipe\",\n stderr: \"pipe\",\n });\n return {\n exitCode: proc.exited,\n stdout: proc.stdout as unknown as ReadableStream<Uint8Array>,\n };\n}\n\nfunction concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const a of arrays) {\n result.set(a, offset);\n offset += a.length;\n }\n return result;\n}\n",
8
+ "import { readFileSync } from \"node:fs\";\n\nexport type ExecFn = (cmd: string[], cwd: string) => Promise<string>;\nexport type ReadFileFn = (path: string) => string;\n\nexport interface RepoContext {\n gitDiff: string;\n guidelines: string[];\n}\n\nexport interface GetRepoContextOptions {\n exec?: ExecFn;\n readFile?: ReadFileFn;\n}\n\nconst defaultExec: ExecFn = async (cmd, cwd) => {\n const proc = Bun.spawnSync(cmd, { cwd });\n if (proc.exitCode !== 0) {\n const stderr = proc.stderr.toString().trim();\n throw new Error(`Command failed (exit ${proc.exitCode}): ${cmd.join(\" \")}${stderr ? `: ${stderr}` : \"\"}`);\n }\n return proc.stdout.toString();\n};\n\nconst defaultReadFile: ReadFileFn = (path) => {\n return readFileSync(path, \"utf-8\");\n};\n\nexport async function getRepoContext(\n demosDir: string,\n options?: GetRepoContextOptions,\n): Promise<RepoContext> {\n const exec = options?.exec ?? defaultExec;\n const readFile = options?.readFile ?? defaultReadFile;\n\n const gitRoot = (await exec([\"git\", \"rev-parse\", \"--show-toplevel\"], demosDir)).trim();\n\n let gitDiff: string;\n const workingDiff = (await exec([\"git\", \"diff\", \"HEAD\"], gitRoot)).trim();\n if (workingDiff.length > 0) {\n gitDiff = workingDiff;\n } else {\n gitDiff = (await exec([\"git\", \"diff\", \"HEAD~1..HEAD\"], gitRoot)).trim();\n }\n\n const lsOutput = (await exec([\"git\", \"ls-files\"], gitRoot)).trim();\n const files = lsOutput.split(\"\\n\").filter((f) => f.length > 0);\n\n const guidelinePatterns = [\"CLAUDE.md\", \"SKILL.md\"];\n const guidelines: string[] = [];\n\n for (const file of files) {\n const basename = file.split(\"/\").pop() ?? \"\";\n if (guidelinePatterns.includes(basename)) {\n const fullPath = `${gitRoot}/${file}`;\n const content = readFile(fullPath);\n guidelines.push(`# ${file}\\n${content}`);\n }\n }\n\n return { gitDiff, guidelines };\n}\n",
9
+ "import type { ReviewMetadata, CodeReview } from \"./review-types.ts\";\n\nexport interface GenerateReviewHtmlOptions {\n metadata: ReviewMetadata;\n title?: string;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\nfunction escapeAttr(s: string): string {\n return escapeHtml(s);\n}\n\nfunction renderReviewSection(review: CodeReview): string {\n const bannerClass = review.verdict === \"approve\" ? \"approve\" : \"request-changes\";\n const verdictLabel = review.verdict === \"approve\" ? \"Approved\" : \"Changes Requested\";\n\n const highlightsHtml = review.highlights\n .map((h) => `<li>${escapeHtml(h)}</li>`)\n .join(\"\\n \");\n\n const issuesHtml = review.issues.length > 0\n ? review.issues\n .map((issue) => {\n const badgeLabel = issue.severity.toUpperCase();\n return `<div class=\"issue ${issue.severity}\"><span class=\"severity-badge\">${badgeLabel}</span> <span class=\"issue-text\">${escapeHtml(issue.description)}</span><button class=\"feedback-add-issue\" data-issue=\"${escapeAttr(issue.description)}\">+</button></div>`;\n })\n .join(\"\\n \")\n : '<p class=\"no-issues\">No issues found.</p>';\n\n return `<section class=\"review-section\">\n <div class=\"verdict-banner ${bannerClass}\">\n <strong>${verdictLabel}</strong>: ${escapeHtml(review.verdictReason)}\n </div>\n <div class=\"review-body\">\n <h2>Summary</h2>\n <p>${escapeHtml(review.summary)}</p>\n <h2>Highlights</h2>\n <ul class=\"highlights-list\">\n ${highlightsHtml}\n </ul>\n <h2>Issues</h2>\n ${issuesHtml}\n </div>\n </section>`;\n}\n\nexport function generateReviewHtml(options: GenerateReviewHtmlOptions): string {\n const { metadata, title = \"Demo Review\" } = options;\n\n if (metadata.demos.length === 0) {\n throw new Error(\"metadata.demos must not be empty\");\n }\n\n const firstDemo = metadata.demos[0];\n\n const demoButtons = metadata.demos\n .map((demo, i) => {\n const activeClass = i === 0 ? ' class=\"active\"' : \"\";\n return `<li><button data-index=\"${i}\"${activeClass}>${escapeHtml(demo.file)}</button></li>`;\n })\n .join(\"\\n \");\n\n const metadataJson = JSON.stringify(metadata).replace(/<\\//g, \"<\\\\/\");\n\n const reviewHtml = metadata.review ? renderReviewSection(metadata.review) : \"\";\n const hasReview = !!metadata.review;\n const defaultTab = hasReview ? \"summary\" : \"demos\";\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${escapeHtml(title)}</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }\n header { padding: 1rem 2rem; background: #16213e; border-bottom: 1px solid #0f3460; }\n header h1 { font-size: 1.4rem; color: #e94560; }\n .tab-bar { display: flex; gap: 0; background: #16213e; border-bottom: 2px solid #0f3460; padding: 0 2rem; }\n .tab-btn { padding: 0.7rem 1.5rem; background: none; border: none; border-bottom: 3px solid transparent; color: #999; font-size: 0.95rem; cursor: pointer; font-family: inherit; transition: all 0.15s; margin-bottom: -2px; }\n .tab-btn:hover { color: #e0e0e0; }\n .tab-btn.active { color: #e94560; border-bottom-color: #e94560; }\n .tab-panel { display: none; }\n .tab-panel.active { display: block; }\n .review-section { padding: 1.5rem 2rem; }\n .verdict-banner { padding: 1rem 1.5rem; border-radius: 6px; font-size: 1rem; margin-bottom: 1.5rem; }\n .verdict-banner.approve { background: #1b4332; border: 1px solid #2d6a4f; color: #95d5b2; }\n .verdict-banner.request-changes { background: #4a1520; border: 1px solid #842029; color: #f5c6cb; }\n .review-body { max-width: 900px; }\n .review-body h2 { font-size: 1.1rem; color: #e94560; margin: 1.2rem 0 0.5rem; }\n .review-body p { font-size: 0.95rem; line-height: 1.6; color: #ccc; }\n .highlights-list { list-style: disc; padding-left: 1.5rem; margin-bottom: 0.5rem; }\n .highlights-list li { font-size: 0.95rem; line-height: 1.5; color: #95d5b2; margin-bottom: 0.3rem; }\n .issue { padding: 0.6rem 0.8rem; margin-bottom: 0.5rem; border-radius: 4px; font-size: 0.9rem; line-height: 1.4; }\n .issue.major { background: rgba(132, 32, 41, 0.3); border-left: 4px solid #dc3545; }\n .issue.minor { background: rgba(255, 193, 7, 0.1); border-left: 4px solid #ffc107; }\n .issue.nit { background: rgba(108, 117, 125, 0.2); border-left: 4px solid #6c757d; }\n .severity-badge { display: inline-block; font-size: 0.7rem; font-weight: bold; padding: 0.15rem 0.4rem; border-radius: 3px; margin-right: 0.5rem; vertical-align: middle; }\n .issue.major .severity-badge { background: #dc3545; color: #fff; }\n .issue.minor .severity-badge { background: #ffc107; color: #000; }\n .issue.nit .severity-badge { background: #6c757d; color: #fff; }\n .no-issues { color: #95d5b2; font-style: italic; }\n .demos-section { padding: 1rem 0; }\n .review-layout { display: flex; height: 600px; }\n .video-panel { flex: 4; padding: 1rem; display: flex; align-items: center; justify-content: center; background: #0f0f23; }\n .video-wrapper { position: relative; width: 100%; max-height: 100%; display: flex; flex-direction: column; }\n .video-wrapper video { width: 100%; max-height: calc(100% - 36px); border-radius: 4px 4px 0 0; display: block; cursor: pointer; }\n .video-controls { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #16213e; border-radius: 0 0 4px 4px; }\n .video-controls button { background: none; border: none; color: #e0e0e0; cursor: pointer; font-size: 1rem; padding: 0; width: 20px; display: flex; align-items: center; justify-content: center; }\n .video-controls button:hover { color: #e94560; }\n .video-controls input[type=\"range\"] { flex: 1; height: 4px; accent-color: #e94560; cursor: pointer; }\n .video-controls .vc-time { font-size: 0.75rem; color: #999; white-space: nowrap; font-variant-numeric: tabular-nums; }\n .side-panel { flex: 1; min-width: 260px; max-width: 360px; padding: 1rem; overflow-y: auto; background: #16213e; border-left: 1px solid #0f3460; }\n .side-panel h2 { font-size: 1rem; margin-bottom: 0.5rem; color: #e94560; }\n .side-panel section { margin-bottom: 1.5rem; }\n #demo-list { list-style: none; }\n #demo-list li { margin-bottom: 0.25rem; }\n #demo-list button { width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }\n #demo-list button:hover { background: #0f3460; }\n #demo-list button.active { background: #e94560; color: #fff; border-color: #e94560; }\n #summary-text { font-size: 0.9rem; line-height: 1.5; color: #ccc; }\n #steps-list { list-style: none; }\n #steps-list li { margin-bottom: 0.3rem; }\n #steps-list button { width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: transparent; color: #53a8b6; border: none; border-left: 3px solid transparent; cursor: pointer; font-size: 0.85rem; transition: all 0.2s; }\n #steps-list button:hover { color: #e94560; }\n #steps-list button.step-active { background: rgba(233, 69, 96, 0.15); color: #e94560; border-left-color: #e94560; }\n .timestamp { font-weight: bold; margin-right: 0.4rem; color: #e94560; }\n .issue { position: relative; }\n .feedback-add-issue { position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%); background: none; border: 1px solid #53a8b6; color: #53a8b6; border-radius: 4px; cursor: pointer; font-size: 0.85rem; padding: 0.1rem 0.45rem; line-height: 1; }\n .feedback-add-issue:hover { background: #53a8b6; color: #1a1a2e; }\n #feedback-selection-btn { display: none; position: absolute; z-index: 1000; padding: 0.35rem 0.7rem; background: #e94560; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 0.8rem; white-space: nowrap; }\n .feedback-layout { display: flex; gap: 1.5rem; padding: 1.5rem 2rem; }\n .feedback-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }\n .feedback-right { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; }\n #feedback-list { list-style: none; padding: 0; }\n #feedback-list li { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.6rem; background: #16213e; border: 1px solid #0f3460; border-radius: 4px; margin-bottom: 0.4rem; font-size: 0.9rem; }\n #feedback-list li span { flex: 1; }\n .feedback-remove { background: none; border: none; color: #dc3545; cursor: pointer; font-size: 0.9rem; padding: 0 0.3rem; }\n .feedback-remove:hover { color: #ff6b7a; }\n #feedback-general { width: 100%; min-height: 100px; background: #16213e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 4px; padding: 0.6rem; font-family: inherit; font-size: 0.9rem; resize: vertical; }\n #feedback-preview { background: #0f0f23; color: #ccc; border: 1px solid #0f3460; border-radius: 4px; padding: 1rem; white-space: pre-wrap; font-size: 0.85rem; line-height: 1.5; flex: 1; min-height: 200px; overflow-y: auto; }\n #feedback-copy { align-self: flex-end; padding: 0.5rem 1rem; background: none; border: 1px solid #53a8b6; color: #53a8b6; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }\n #feedback-copy:hover { background: #53a8b6; color: #1a1a2e; }\n </style>\n</head>\n<body>\n <header>\n <h1>${escapeHtml(title)}</h1>\n </header>\n <nav class=\"tab-bar\">\n ${hasReview ? `<button class=\"tab-btn${defaultTab === \"summary\" ? \" active\" : \"\"}\" data-tab=\"summary\">Summary</button>` : \"\"}\n <button class=\"tab-btn${defaultTab === \"demos\" ? \" active\" : \"\"}\" data-tab=\"demos\">Demos</button>\n ${hasReview ? `<button class=\"tab-btn\" data-tab=\"feedback\">Feedback</button>` : \"\"}\n </nav>\n <main>\n ${hasReview ? `<div id=\"tab-summary\" class=\"tab-panel${defaultTab === \"summary\" ? \" active\" : \"\"}\">\n ${reviewHtml}\n </div>` : \"\"}\n <div id=\"tab-demos\" class=\"tab-panel${defaultTab === \"demos\" ? \" active\" : \"\"}\">\n <section class=\"demos-section\">\n <div class=\"review-layout\">\n <div class=\"video-panel\">\n <div class=\"video-wrapper\">\n <video id=\"review-video\" src=\"${escapeAttr(firstDemo.file)}\"></video>\n <div class=\"video-controls\">\n <button id=\"vc-play\" aria-label=\"Play\">&#9654;</button>\n <input id=\"vc-seek\" type=\"range\" min=\"0\" max=\"100\" value=\"0\" step=\"0.1\">\n <span class=\"vc-time\" id=\"vc-time\">0:00 / 0:00</span>\n </div>\n </div>\n </div>\n <div class=\"side-panel\">\n <section>\n <h2>Demos</h2>\n <ul id=\"demo-list\">\n ${demoButtons}\n </ul>\n </section>\n <section>\n <h2>Summary</h2>\n <p id=\"summary-text\"></p>\n </section>\n <section id=\"steps-section\">\n <h2>Steps</h2>\n <ul id=\"steps-list\"></ul>\n </section>\n </div>\n </div>\n </section>\n </div>\n ${hasReview ? `<div id=\"tab-feedback\" class=\"tab-panel\">\n <div class=\"feedback-layout\">\n <div class=\"feedback-left\">\n <h2>Feedback Items</h2>\n <ul id=\"feedback-list\"></ul>\n <h2>General Feedback</h2>\n <textarea id=\"feedback-general\" placeholder=\"Add general feedback here...\"></textarea>\n </div>\n <div class=\"feedback-right\">\n <h2>Preview</h2>\n <pre id=\"feedback-preview\"></pre>\n <button id=\"feedback-copy\">Copy to clipboard</button>\n </div>\n </div>\n </div>` : \"\"}\n </main>\n ${hasReview ? `<button id=\"feedback-selection-btn\">Add to feedback</button>` : \"\"}\n <script>\n (function() {\n // Tab switching\n var tabBtns = document.querySelectorAll(\".tab-btn\");\n var tabPanels = document.querySelectorAll(\".tab-panel\");\n tabBtns.forEach(function(btn) {\n btn.addEventListener(\"click\", function() {\n var target = btn.getAttribute(\"data-tab\");\n tabBtns.forEach(function(b) { b.classList.toggle(\"active\", b === btn); });\n tabPanels.forEach(function(p) { p.classList.toggle(\"active\", p.id === \"tab-\" + target); });\n });\n });\n\n var metadata = ${metadataJson};\n var video = document.getElementById(\"review-video\");\n var summaryText = document.getElementById(\"summary-text\");\n var stepsList = document.getElementById(\"steps-list\");\n var demoButtons = document.querySelectorAll(\"#demo-list button\");\n\n function esc(s) {\n var d = document.createElement(\"div\");\n d.appendChild(document.createTextNode(s));\n return d.innerHTML;\n }\n\n function formatTime(seconds) {\n var m = Math.floor(seconds / 60);\n var s = Math.floor(seconds % 60);\n return m + \":\" + (s < 10 ? \"0\" : \"\") + s;\n }\n\n function selectDemo(index) {\n var demo = metadata.demos[index];\n video.src = demo.file;\n video.load();\n summaryText.textContent = demo.summary;\n\n demoButtons.forEach(function(btn, i) {\n btn.classList.toggle(\"active\", i === index);\n });\n\n var stepsHtml = \"\";\n demo.steps.forEach(function(step) {\n stepsHtml += '<li><button data-time=\"' + step.timestampSeconds + '\">' +\n '<span class=\"timestamp\">' + esc(formatTime(step.timestampSeconds)) + '</span>' +\n esc(step.text) + '</button></li>';\n });\n stepsList.innerHTML = stepsHtml;\n }\n\n demoButtons.forEach(function(btn) {\n btn.addEventListener(\"click\", function() {\n selectDemo(parseInt(btn.getAttribute(\"data-index\"), 10));\n });\n });\n\n stepsList.addEventListener(\"click\", function(e) {\n var btn = e.target.closest(\"button[data-time]\");\n if (btn) {\n video.currentTime = parseFloat(btn.getAttribute(\"data-time\"));\n video.play();\n }\n });\n\n video.addEventListener(\"timeupdate\", function() {\n var buttons = document.querySelectorAll(\"#steps-list button[data-time]\");\n var ct = video.currentTime;\n var activeIdx = -1;\n buttons.forEach(function(btn, i) {\n if (parseFloat(btn.getAttribute(\"data-time\")) <= ct) activeIdx = i;\n btn.classList.remove(\"step-active\");\n });\n if (activeIdx >= 0) buttons[activeIdx].classList.add(\"step-active\");\n });\n\n selectDemo(0);\n\n // Custom video controls\n var playBtn = document.getElementById(\"vc-play\");\n var seekBar = document.getElementById(\"vc-seek\");\n var timeDisplay = document.getElementById(\"vc-time\");\n var seeking = false;\n\n function fmtTime(sec) {\n var m = Math.floor(sec / 60);\n var s = Math.floor(sec % 60);\n return m + \":\" + (s < 10 ? \"0\" : \"\") + s;\n }\n\n function updateTime() {\n var cur = video.currentTime || 0;\n var dur = video.duration || 0;\n timeDisplay.textContent = fmtTime(cur) + \" / \" + fmtTime(dur);\n if (!seeking && dur) seekBar.value = (cur / dur) * 100;\n }\n\n function updatePlayBtn() {\n playBtn.innerHTML = video.paused ? \"&#9654;\" : \"&#9646;&#9646;\";\n }\n\n playBtn.addEventListener(\"click\", function() {\n video.paused ? video.play() : video.pause();\n });\n video.addEventListener(\"click\", function() {\n video.paused ? video.play() : video.pause();\n });\n video.addEventListener(\"play\", updatePlayBtn);\n video.addEventListener(\"pause\", updatePlayBtn);\n video.addEventListener(\"ended\", updatePlayBtn);\n video.addEventListener(\"timeupdate\", updateTime);\n video.addEventListener(\"loadedmetadata\", updateTime);\n\n seekBar.addEventListener(\"input\", function() {\n seeking = true;\n if (video.duration) {\n video.currentTime = (seekBar.value / 100) * video.duration;\n }\n });\n seekBar.addEventListener(\"change\", function() { seeking = false; });\n\n // Feedback tab logic\n if (document.getElementById(\"tab-feedback\")) {\n var feedbackItems = [];\n var feedbackList = document.getElementById(\"feedback-list\");\n var feedbackGeneral = document.getElementById(\"feedback-general\");\n var feedbackPreview = document.getElementById(\"feedback-preview\");\n var feedbackCopy = document.getElementById(\"feedback-copy\");\n var selectionBtn = document.getElementById(\"feedback-selection-btn\");\n\n function addFeedbackItem(text) {\n var trimmed = text.trim();\n if (!trimmed) return;\n for (var i = 0; i < feedbackItems.length; i++) {\n if (feedbackItems[i] === trimmed) return;\n }\n feedbackItems.push(trimmed);\n renderFeedback();\n }\n\n function removeFeedbackItem(index) {\n feedbackItems.splice(index, 1);\n renderFeedback();\n }\n\n function renderFeedback() {\n var html = \"\";\n feedbackItems.forEach(function(item, i) {\n html += '<li><span>' + esc(item) + '</span><button class=\"feedback-remove\" data-index=\"' + i + '\">X</button></li>';\n });\n feedbackList.innerHTML = html;\n updatePreview();\n }\n\n function updatePreview() {\n var lines = \"\";\n feedbackItems.forEach(function(item, i) {\n lines += (i + 1) + \". Address: \" + item + \"\\\\n\";\n });\n var general = feedbackGeneral.value.trim();\n if (general) {\n lines += \"\\\\nGeneral feedback:\\\\n\" + general;\n }\n feedbackPreview.textContent = lines;\n }\n\n // Issue \"+\" buttons\n var summaryTab = document.getElementById(\"tab-summary\");\n if (summaryTab) {\n summaryTab.addEventListener(\"click\", function(e) {\n var btn = e.target.closest(\".feedback-add-issue\");\n if (btn) {\n addFeedbackItem(btn.getAttribute(\"data-issue\"));\n }\n });\n }\n\n // Text selection floating button\n var selectionTimeout;\n document.addEventListener(\"mouseup\", function(e) {\n clearTimeout(selectionTimeout);\n selectionTimeout = setTimeout(function() {\n var sel = window.getSelection();\n var text = sel ? sel.toString().trim() : \"\";\n if (!text) return;\n var anchor = sel.anchorNode;\n var inSummary = false;\n var node = anchor;\n while (node) {\n if (node.id === \"tab-summary\") { inSummary = true; break; }\n node = node.parentNode;\n }\n if (!inSummary) return;\n selectionBtn.style.display = \"block\";\n selectionBtn.style.left = e.pageX + \"px\";\n selectionBtn.style.top = (e.pageY - 35) + \"px\";\n selectionBtn._selectedText = text;\n }, 100);\n });\n\n selectionBtn.addEventListener(\"click\", function() {\n if (selectionBtn._selectedText) {\n addFeedbackItem(selectionBtn._selectedText);\n }\n selectionBtn.style.display = \"none\";\n window.getSelection().removeAllRanges();\n });\n\n document.addEventListener(\"mousedown\", function(e) {\n if (e.target !== selectionBtn) {\n selectionBtn.style.display = \"none\";\n }\n });\n\n // Remove buttons\n feedbackList.addEventListener(\"click\", function(e) {\n var btn = e.target.closest(\".feedback-remove\");\n if (btn) {\n removeFeedbackItem(parseInt(btn.getAttribute(\"data-index\"), 10));\n }\n });\n\n // Textarea input\n feedbackGeneral.addEventListener(\"input\", updatePreview);\n\n // Copy button\n feedbackCopy.addEventListener(\"click\", function() {\n var text = feedbackPreview.textContent;\n function onCopied() {\n feedbackCopy.textContent = \"Copied!\";\n setTimeout(function() { feedbackCopy.textContent = \"Copy to clipboard\"; }, 1500);\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(onCopied, onCopied);\n } else {\n onCopied();\n }\n });\n\n renderFeedback();\n }\n })();\n </script>\n</body>\n</html>`;\n}\n",
10
+ "import type { Page } from \"@playwright/test\";\n\nimport type { ShowCommentaryOptions } from \"./commentary.ts\";\nimport { showCommentary as defaultShowCommentary } from \"./commentary.ts\";\n\nexport interface DemoStep {\n text: string;\n timestampSeconds: number;\n}\n\ntype ShowCommentaryFn = (\n page: Page,\n options: ShowCommentaryOptions,\n) => Promise<void>;\n\ntype TestStepFn = (\n title: string,\n body: () => Promise<void>,\n) => Promise<void>;\n\nexport interface DemoRecorderOptions {\n showCommentary?: ShowCommentaryFn;\n testStep?: TestStepFn;\n}\n\nexport class DemoRecorder {\n private steps: DemoStep[] = [];\n private startTime: number;\n private showCommentaryFn: ShowCommentaryFn;\n private testStepFn: TestStepFn | undefined;\n\n constructor(options?: DemoRecorderOptions) {\n this.startTime = Date.now();\n this.showCommentaryFn = options?.showCommentary ?? defaultShowCommentary;\n this.testStepFn = options?.testStep;\n }\n\n async step(\n page: Page,\n text: string,\n options: { selector: string },\n ): Promise<void> {\n const body = async () => {\n await this.showCommentaryFn(page, {\n selector: options.selector,\n text,\n });\n\n const timestampSeconds =\n Math.round((Date.now() - this.startTime) / 100) / 10;\n\n this.steps.push({ text, timestampSeconds });\n };\n\n if (this.testStepFn) {\n await this.testStepFn(text, body);\n } else {\n await body();\n }\n }\n\n getSteps(): DemoStep[] {\n return [...this.steps];\n }\n\n async save(outputDir: string): Promise<void> {\n const { join } = await import(\"node:path\");\n const { mkdirSync, writeFileSync } = await import(\"node:fs\");\n\n mkdirSync(outputDir, { recursive: true });\n const filePath = join(outputDir, \"demo-steps.json\");\n writeFileSync(filePath, JSON.stringify(this.steps, null, 2) + \"\\n\");\n }\n}\n"
6
11
  ],
7
- "mappings": ";AAOA,IAAM,aAAa;AAEnB,eAAsB,cAAc,CAClC,MACA,SACe;AAAA,EACf,MAAM,KAAK,SACT,GAAG,UAAU,MAAM,gBAAgB;AAAA,IACjC,MAAM,SAAS,SAAS,cAAc,QAAQ;AAAA,IAC9C,IAAI,CAAC,QAAQ;AAAA,MACX,MAAM,IAAI,MACR,qDAAqD,WACvD;AAAA,IACF;AAAA,IAGA,SAAS,eAAe,SAAS,GAAG,OAAO;AAAA,IAE3C,MAAM,OAAO,OAAO,sBAAsB;AAAA,IAE1C,MAAM,UAAU,SAAS,cAAc,KAAK;AAAA,IAC5C,QAAQ,KAAK;AAAA,IACb,QAAQ,cAAc;AAAA,IAEtB,MAAM,QAAQ,SAAS,cAAc,OAAO;AAAA,IAC5C,MAAM,aAAa,yBAAyB,EAAE;AAAA,IAC9C,MAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAqBf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAaA;AAAA;AAAA;AAAA;AAAA,IAML,SAAS,cAAc,8BAA8B,GAAG,OAAO;AAAA,IAC/D,SAAS,KAAK,YAAY,KAAK;AAAA,IAG/B,MAAM,MAAM,KAAK,SAAS;AAAA,IAC1B,MAAM,OAAO,KAAK,OAAO,KAAK,QAAQ;AAAA,IACtC,QAAQ,MAAM,MAAM,GAAG;AAAA,IACvB,QAAQ,MAAM,OAAO,GAAG;AAAA,IACxB,QAAQ,MAAM,YAAY;AAAA,IAE1B,SAAS,KAAK,YAAY,OAAO;AAAA,KAEnC,EAAE,UAAU,QAAQ,UAAU,MAAM,QAAQ,MAAM,WAAW,WAAW,CAC1E;AAAA;AAGF,eAAsB,cAAc,CAAC,MAA2B;AAAA,EAC9D,MAAM,KAAK,SAAS,CAAC,cAAc;AAAA,IACjC,MAAM,UAAU,SAAS,eAAe,SAAS;AAAA,IACjD,IAAI,CAAC;AAAA,MAAS;AAAA,IAEd,QAAQ,UAAU,IAAI,yBAAyB;AAAA,IAE/C,QAAQ,iBACN,gBACA,MAAM;AAAA,MACJ,QAAQ,OAAO;AAAA,MACf,SAAS,cAAc,8BAA8B,GAAG,OAAO;AAAA,OAEjE,EAAE,MAAM,KAAK,CACf;AAAA,KACC,UAAU;AAAA,EAGb,MAAM,KAAK,eAAe,GAAG;AAAA;",
8
- "debugId": "308A425302523F0964756E2164756E21",
12
+ "mappings": "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",
13
+ "debugId": "160BDBF1FB742F2264756E2164756E21",
9
14
  "names": []
10
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@demon-utils/playwright",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "module": "dist/index.js",
6
6
  "types": "src/index.ts",
@@ -14,6 +14,9 @@
14
14
  "dist",
15
15
  "src"
16
16
  ],
17
+ "bin": {
18
+ "demon-demo-review": "dist/bin/demon-demo-review.js"
19
+ },
17
20
  "type": "module",
18
21
  "peerDependencies": {
19
22
  "@playwright/test": ">=1.40.0"
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, statSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { resolve, join, basename, dirname } from "node:path";
4
+
5
+ import {
6
+ buildReviewPrompt,
7
+ invokeClaude,
8
+ parseLlmResponse,
9
+ } from "../review.ts";
10
+ import { generateReviewHtml } from "../html-generator.ts";
11
+ import { getRepoContext } from "../git-context.ts";
12
+ import type { ReviewMetadata } from "../review-types.ts";
13
+
14
+ let dir: string | undefined;
15
+ let agent: string | undefined;
16
+
17
+ const args = process.argv.slice(2);
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i] === "--agent") {
20
+ agent = args[++i];
21
+ } else if (!dir) {
22
+ dir = args[i];
23
+ }
24
+ }
25
+
26
+ if (!dir) {
27
+ console.error("Usage: demon-demo-review [--agent <path>] <directory>");
28
+ console.error(" Discovers .webm video files in the given directory.");
29
+ process.exit(1);
30
+ }
31
+
32
+ const resolved = resolve(dir);
33
+
34
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
35
+ console.error(`Error: "${resolved}" is not a valid directory.`);
36
+ process.exit(1);
37
+ }
38
+
39
+ // Discover .webm files — search top-level first, then one level deep
40
+ // (Playwright creates per-test subdirectories under outputDir)
41
+ let webmFiles = readdirSync(resolved)
42
+ .filter((f) => f.endsWith(".webm"))
43
+ .map((f) => join(resolved, f));
44
+
45
+ if (webmFiles.length === 0) {
46
+ for (const entry of readdirSync(resolved, { withFileTypes: true })) {
47
+ if (!entry.isDirectory()) continue;
48
+ const subdir = join(resolved, entry.name);
49
+ for (const f of readdirSync(subdir)) {
50
+ if (f.endsWith(".webm")) {
51
+ webmFiles.push(join(subdir, f));
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ webmFiles.sort();
58
+
59
+ if (webmFiles.length === 0) {
60
+ console.error(`Error: No .webm files found in "${resolved}" or its subdirectories.`);
61
+ process.exit(1);
62
+ }
63
+
64
+ for (const file of webmFiles) {
65
+ console.log(file);
66
+ }
67
+
68
+ // Collect demo-steps.json from the directory of each .webm file
69
+ const stepsMap: Record<string, Array<{ text: string; timestampSeconds: number }>> = {};
70
+ for (const webmFile of webmFiles) {
71
+ const stepsPath = join(dirname(webmFile), "demo-steps.json");
72
+ if (!existsSync(stepsPath)) continue;
73
+ try {
74
+ const raw = readFileSync(stepsPath, "utf-8");
75
+ const parsed = JSON.parse(raw);
76
+ if (Array.isArray(parsed)) {
77
+ stepsMap[basename(webmFile)] = parsed;
78
+ }
79
+ } catch {
80
+ // skip malformed steps files
81
+ }
82
+ }
83
+
84
+ if (Object.keys(stepsMap).length === 0) {
85
+ console.error("Error: No demo-steps.json found alongside any .webm files.");
86
+ console.error("Use DemoRecorder in your demo tests to generate step data.");
87
+ process.exit(1);
88
+ }
89
+
90
+ // Gather repo context (git diff + guidelines)
91
+ let gitDiff: string | undefined;
92
+ let guidelines: string[] | undefined;
93
+ try {
94
+ const repoContext = await getRepoContext(resolved);
95
+ gitDiff = repoContext.gitDiff;
96
+ guidelines = repoContext.guidelines;
97
+ } catch (err) {
98
+ console.warn(
99
+ "Warning: Could not gather repo context:",
100
+ err instanceof Error ? err.message : err,
101
+ );
102
+ }
103
+
104
+ try {
105
+ const basenames = webmFiles.map((f) => basename(f));
106
+
107
+ const prompt = buildReviewPrompt({ filenames: basenames, stepsMap, gitDiff, guidelines });
108
+
109
+ console.log("Invoking claude to generate review metadata...");
110
+ const rawOutput = await invokeClaude(prompt, { agent });
111
+
112
+ const llmResponse = parseLlmResponse(rawOutput);
113
+
114
+ // Construct final metadata by merging LLM summaries with steps
115
+ const metadata: ReviewMetadata = {
116
+ demos: llmResponse.demos.map((demo) => ({
117
+ file: demo.file,
118
+ summary: demo.summary,
119
+ steps: stepsMap[demo.file] ?? [],
120
+ })),
121
+ review: llmResponse.review,
122
+ };
123
+
124
+ const outputPath = join(resolved, "review-metadata.json");
125
+ writeFileSync(outputPath, JSON.stringify(metadata, null, 2) + "\n");
126
+ console.log(`Review metadata written to ${outputPath}`);
127
+
128
+ const html = generateReviewHtml({ metadata });
129
+ const htmlPath = join(resolved, "review.html");
130
+ writeFileSync(htmlPath, html);
131
+ console.log(resolve(htmlPath));
132
+ } catch (err) {
133
+ console.error(
134
+ "Error generating review metadata:",
135
+ err instanceof Error ? err.message : err,
136
+ );
137
+ process.exit(1);
138
+ }
@@ -69,3 +69,99 @@ test("showCommentary throws for missing selector", async ({ page }) => {
69
69
  }),
70
70
  ).rejects.toThrow();
71
71
  });
72
+
73
+ test("tooltip flips above when target is near bottom edge", async ({
74
+ page,
75
+ }) => {
76
+ await page.goto(`data:text/html,
77
+ <html><body>
78
+ <button id="btn" style="position:fixed;bottom:20px;left:50%;transform:translateX(-50%);">Bottom</button>
79
+ </body></html>`);
80
+
81
+ await showCommentary(page, { selector: "#btn", text: "Flipped above" });
82
+
83
+ const tooltip = page.locator(`#${TOOLTIP_ID}`);
84
+ const btn = page.locator("#btn");
85
+ const tooltipBox = await tooltip.boundingBox();
86
+ const btnBox = await btn.boundingBox();
87
+
88
+ expect(tooltipBox).toBeTruthy();
89
+ expect(btnBox).toBeTruthy();
90
+ // Tooltip bottom should be above or at the target top
91
+ expect(tooltipBox!.y + tooltipBox!.height).toBeLessThanOrEqual(btnBox!.y);
92
+ });
93
+
94
+ test("tooltip doesn't overflow right edge", async ({ page }) => {
95
+ await page.goto(`data:text/html,
96
+ <html><body>
97
+ <button id="btn" style="position:fixed;top:100px;right:10px;">Right</button>
98
+ </body></html>`);
99
+
100
+ await showCommentary(page, { selector: "#btn", text: "Clamped right" });
101
+
102
+ const tooltipBox = await page.locator(`#${TOOLTIP_ID}`).boundingBox();
103
+ expect(tooltipBox).toBeTruthy();
104
+ expect(tooltipBox!.x + tooltipBox!.width).toBeLessThanOrEqual(1280);
105
+ });
106
+
107
+ test("tooltip doesn't overflow left edge", async ({ page }) => {
108
+ await page.goto(`data:text/html,
109
+ <html><body>
110
+ <button id="btn" style="position:fixed;top:100px;left:10px;">Left</button>
111
+ </body></html>`);
112
+
113
+ await showCommentary(page, { selector: "#btn", text: "Clamped left" });
114
+
115
+ const tooltipBox = await page.locator(`#${TOOLTIP_ID}`).boundingBox();
116
+ expect(tooltipBox).toBeTruthy();
117
+ expect(tooltipBox!.x).toBeGreaterThanOrEqual(0);
118
+ });
119
+
120
+ test("tooltip handles bottom-right corner target", async ({ page }) => {
121
+ await page.goto(`data:text/html,
122
+ <html><body>
123
+ <button id="btn" style="position:fixed;bottom:20px;right:10px;">Corner</button>
124
+ </body></html>`);
125
+
126
+ await showCommentary(page, {
127
+ selector: "#btn",
128
+ text: "Bottom-right corner",
129
+ });
130
+
131
+ const tooltip = page.locator(`#${TOOLTIP_ID}`);
132
+ const btn = page.locator("#btn");
133
+ const tooltipBox = await tooltip.boundingBox();
134
+ const btnBox = await btn.boundingBox();
135
+
136
+ expect(tooltipBox).toBeTruthy();
137
+ expect(btnBox).toBeTruthy();
138
+ // Flipped above
139
+ expect(tooltipBox!.y + tooltipBox!.height).toBeLessThanOrEqual(btnBox!.y);
140
+ // Right-clamped
141
+ expect(tooltipBox!.x + tooltipBox!.width).toBeLessThanOrEqual(1280);
142
+ });
143
+
144
+ test("default position: tooltip below target, horizontally centered", async ({
145
+ page,
146
+ }) => {
147
+ await page.goto(INLINE_HTML);
148
+
149
+ await showCommentary(page, {
150
+ selector: "#submit-btn",
151
+ text: "Default position",
152
+ });
153
+
154
+ const tooltip = page.locator(`#${TOOLTIP_ID}`);
155
+ const btn = page.locator("#submit-btn");
156
+ const tooltipBox = await tooltip.boundingBox();
157
+ const btnBox = await btn.boundingBox();
158
+
159
+ expect(tooltipBox).toBeTruthy();
160
+ expect(btnBox).toBeTruthy();
161
+ // Below target
162
+ expect(tooltipBox!.y).toBeGreaterThanOrEqual(btnBox!.y + btnBox!.height);
163
+ // Horizontally: tooltip center ≈ button center (within 2px tolerance)
164
+ const tooltipCenter = tooltipBox!.x + tooltipBox!.width / 2;
165
+ const btnCenter = btnBox!.x + btnBox!.width / 2;
166
+ expect(Math.abs(tooltipCenter - btnCenter)).toBeLessThanOrEqual(2);
167
+ });
package/src/commentary.ts CHANGED
@@ -35,7 +35,7 @@ export async function showCommentary(
35
35
  @keyframes demon-commentary-in {
36
36
  from {
37
37
  opacity: 0;
38
- transform: translateY(8px);
38
+ transform: translateY(var(--demon-slide-y, 8px));
39
39
  }
40
40
  to {
41
41
  opacity: 1;
@@ -49,10 +49,11 @@ export async function showCommentary(
49
49
  }
50
50
  to {
51
51
  opacity: 0;
52
- transform: translateY(8px);
52
+ transform: translateY(var(--demon-slide-y, 8px));
53
53
  }
54
54
  }
55
55
  #${tooltipId} {
56
+ --demon-slide-y: 8px;
56
57
  position: fixed;
57
58
  z-index: 2147483647;
58
59
  background: #1a1a2e;
@@ -74,14 +75,36 @@ export async function showCommentary(
74
75
  document.querySelector("style[data-demon-commentary]")?.remove();
75
76
  document.head.appendChild(style);
76
77
 
77
- // Position below the target element, horizontally centered
78
- const top = rect.bottom + 10;
79
- const left = rect.left + rect.width / 2;
78
+ // Append hidden to measure real dimensions
79
+ tooltip.style.visibility = "hidden";
80
+ document.body.appendChild(tooltip);
81
+ const tooltipRect = tooltip.getBoundingClientRect();
82
+ const tooltipWidth = tooltipRect.width;
83
+ const tooltipHeight = tooltipRect.height;
84
+ const viewportWidth = window.innerWidth;
85
+
86
+ // Vertical: default below target, flip above if overflowing bottom
87
+ let top = rect.bottom + 10;
88
+ if (
89
+ top + tooltipHeight > window.innerHeight &&
90
+ rect.top - 10 - tooltipHeight >= 0
91
+ ) {
92
+ top = rect.top - 10 - tooltipHeight;
93
+ tooltip.style.setProperty("--demon-slide-y", "-8px");
94
+ }
95
+
96
+ // Horizontal: centered, clamped to viewport
97
+ const left = Math.max(
98
+ 4,
99
+ Math.min(
100
+ rect.left + rect.width / 2 - tooltipWidth / 2,
101
+ viewportWidth - 4 - tooltipWidth,
102
+ ),
103
+ );
104
+
80
105
  tooltip.style.top = `${top}px`;
81
106
  tooltip.style.left = `${left}px`;
82
- tooltip.style.transform = `translateX(-50%)`;
83
-
84
- document.body.appendChild(tooltip);
107
+ tooltip.style.visibility = "";
85
108
  },
86
109
  { selector: options.selector, text: options.text, tooltipId: TOOLTIP_ID },
87
110
  );
@@ -0,0 +1,90 @@
1
+ import { describe, test, expect } from "bun:test";
2
+
3
+ import type { ExecFn, ReadFileFn } from "./git-context.ts";
4
+ import { getRepoContext } from "./git-context.ts";
5
+
6
+ function mockExec(responses: Record<string, string>): ExecFn {
7
+ return async (cmd: string[], _cwd: string) => {
8
+ const key = cmd.join(" ");
9
+ if (key in responses) {
10
+ return responses[key]!;
11
+ }
12
+ throw new Error(`Unexpected command: ${key}`);
13
+ };
14
+ }
15
+
16
+ function mockReadFile(files: Record<string, string>): ReadFileFn {
17
+ return (path: string) => {
18
+ if (path in files) {
19
+ return files[path]!;
20
+ }
21
+ throw new Error(`File not found: ${path}`);
22
+ };
23
+ }
24
+
25
+ describe("getRepoContext", () => {
26
+ test("returns diff from working tree when dirty", async () => {
27
+ const exec = mockExec({
28
+ "git rev-parse --show-toplevel": "/repo\n",
29
+ "git diff HEAD": "diff --git a/file.ts\n+added line\n",
30
+ "git ls-files": "src/index.ts\n",
31
+ });
32
+ const readFile = mockReadFile({});
33
+
34
+ const ctx = await getRepoContext("/repo/demos", { exec, readFile });
35
+ expect(ctx.gitDiff).toBe("diff --git a/file.ts\n+added line");
36
+ });
37
+
38
+ test("falls back to HEAD~1..HEAD when working tree is clean", async () => {
39
+ const exec = mockExec({
40
+ "git rev-parse --show-toplevel": "/repo\n",
41
+ "git diff HEAD": "",
42
+ "git diff HEAD~1..HEAD": "diff --git a/committed.ts\n+committed line\n",
43
+ "git ls-files": "",
44
+ });
45
+ const readFile = mockReadFile({});
46
+
47
+ const ctx = await getRepoContext("/repo/demos", { exec, readFile });
48
+ expect(ctx.gitDiff).toBe("diff --git a/committed.ts\n+committed line");
49
+ });
50
+
51
+ test("discovers CLAUDE.md and SKILL.md files", async () => {
52
+ const exec = mockExec({
53
+ "git rev-parse --show-toplevel": "/repo\n",
54
+ "git diff HEAD": "some diff\n",
55
+ "git ls-files": "CLAUDE.md\nplugins/demo/SKILL.md\nsrc/index.ts\n",
56
+ });
57
+ const readFile = mockReadFile({
58
+ "/repo/CLAUDE.md": "root guidelines",
59
+ "/repo/plugins/demo/SKILL.md": "skill guidelines",
60
+ });
61
+
62
+ const ctx = await getRepoContext("/repo/demos", { exec, readFile });
63
+ expect(ctx.guidelines).toHaveLength(2);
64
+ expect(ctx.guidelines[0]).toBe("# CLAUDE.md\nroot guidelines");
65
+ expect(ctx.guidelines[1]).toBe("# plugins/demo/SKILL.md\nskill guidelines");
66
+ });
67
+
68
+ test("returns empty guidelines when no CLAUDE.md or SKILL.md exist", async () => {
69
+ const exec = mockExec({
70
+ "git rev-parse --show-toplevel": "/repo\n",
71
+ "git diff HEAD": "some diff\n",
72
+ "git ls-files": "src/index.ts\npackage.json\n",
73
+ });
74
+ const readFile = mockReadFile({});
75
+
76
+ const ctx = await getRepoContext("/repo/demos", { exec, readFile });
77
+ expect(ctx.guidelines).toEqual([]);
78
+ });
79
+
80
+ test("throws when git rev-parse fails", async () => {
81
+ const exec: ExecFn = async () => {
82
+ throw new Error("not a git repository");
83
+ };
84
+ const readFile = mockReadFile({});
85
+
86
+ await expect(getRepoContext("/not-a-repo", { exec, readFile })).rejects.toThrow(
87
+ "not a git repository",
88
+ );
89
+ });
90
+ });