@demon-utils/playwright 0.1.6 → 0.1.7

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.
@@ -0,0 +1,1445 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/bin/demoon.ts
5
+ import { resolve } from "path";
6
+ import { readFileSync as readFileSync4 } from "fs";
7
+
8
+ // src/orchestrator.ts
9
+ import { mkdirSync, existsSync as existsSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "node:fs";
10
+ import { join as join2, dirname as pathDirname } from "node:path";
11
+ import { spawnSync as spawnSync3 } from "node:child_process";
12
+
13
+ // src/review.ts
14
+ import { spawn } from "node:child_process";
15
+ import { Readable } from "node:stream";
16
+ async function invokeClaude(prompt, options) {
17
+ const spawnFn = options?.spawn ?? defaultSpawn;
18
+ const agent = options?.agent ?? "claude";
19
+ const proc = spawnFn([agent, "-p", prompt]);
20
+ const reader = proc.stdout.getReader();
21
+ const chunks = [];
22
+ for (;; ) {
23
+ const { done, value } = await reader.read();
24
+ if (done)
25
+ break;
26
+ chunks.push(value);
27
+ }
28
+ const exitCode = await proc.exitCode;
29
+ const output = new TextDecoder().decode(concatUint8Arrays(chunks));
30
+ if (exitCode !== 0) {
31
+ throw new Error(`claude process exited with code ${exitCode}: ${output.trim()}`);
32
+ }
33
+ return output.trim();
34
+ }
35
+ var VALID_VERDICTS = new Set(["approve", "request_changes"]);
36
+ var VALID_SEVERITIES = new Set(["major", "minor", "nit"]);
37
+ function extractJson(raw) {
38
+ try {
39
+ JSON.parse(raw);
40
+ return raw;
41
+ } catch {}
42
+ const start = raw.indexOf("{");
43
+ const end = raw.lastIndexOf("}");
44
+ if (start === -1 || end === -1 || end <= start) {
45
+ throw new Error(`No JSON object found in LLM response: ${raw.slice(0, 200)}`);
46
+ }
47
+ return raw.slice(start, end + 1);
48
+ }
49
+ function parseLlmResponse(raw) {
50
+ const jsonStr = extractJson(raw);
51
+ let parsed;
52
+ try {
53
+ parsed = JSON.parse(jsonStr);
54
+ } catch {
55
+ throw new Error(`Invalid JSON from LLM: ${raw.slice(0, 200)}`);
56
+ }
57
+ if (typeof parsed !== "object" || parsed === null || !("demos" in parsed)) {
58
+ throw new Error("Missing 'demos' array in review metadata");
59
+ }
60
+ const obj = parsed;
61
+ if (!Array.isArray(obj["demos"])) {
62
+ throw new Error("'demos' must be an array");
63
+ }
64
+ for (const demo of obj["demos"]) {
65
+ if (typeof demo !== "object" || demo === null) {
66
+ throw new Error("Each demo must be an object");
67
+ }
68
+ const d = demo;
69
+ if (typeof d["file"] !== "string") {
70
+ throw new Error("Each demo must have a 'file' string");
71
+ }
72
+ if (typeof d["summary"] !== "string") {
73
+ throw new Error("Each demo must have a 'summary' string");
74
+ }
75
+ }
76
+ if (typeof obj["review"] !== "object" || obj["review"] === null) {
77
+ throw new Error("Missing 'review' object in response");
78
+ }
79
+ const review = obj["review"];
80
+ if (typeof review["summary"] !== "string") {
81
+ throw new Error("review.summary must be a string");
82
+ }
83
+ if (!Array.isArray(review["highlights"])) {
84
+ throw new Error("review.highlights must be an array");
85
+ }
86
+ if (review["highlights"].length === 0) {
87
+ throw new Error("review.highlights must not be empty");
88
+ }
89
+ for (const h of review["highlights"]) {
90
+ if (typeof h !== "string") {
91
+ throw new Error("Each highlight must be a string");
92
+ }
93
+ }
94
+ if (typeof review["verdict"] !== "string" || !VALID_VERDICTS.has(review["verdict"])) {
95
+ throw new Error("review.verdict must be 'approve' or 'request_changes'");
96
+ }
97
+ if (typeof review["verdictReason"] !== "string") {
98
+ throw new Error("review.verdictReason must be a string");
99
+ }
100
+ if (!Array.isArray(review["issues"])) {
101
+ throw new Error("review.issues must be an array");
102
+ }
103
+ for (const issue of review["issues"]) {
104
+ if (typeof issue !== "object" || issue === null) {
105
+ throw new Error("Each issue must be an object");
106
+ }
107
+ const i = issue;
108
+ if (typeof i["severity"] !== "string" || !VALID_SEVERITIES.has(i["severity"])) {
109
+ throw new Error("Each issue severity must be 'major', 'minor', or 'nit'");
110
+ }
111
+ if (typeof i["description"] !== "string") {
112
+ throw new Error("Each issue must have a 'description' string");
113
+ }
114
+ }
115
+ return parsed;
116
+ }
117
+ function defaultSpawn(cmd) {
118
+ const [command, ...args] = cmd;
119
+ const proc = spawn(command, args, {
120
+ stdio: ["ignore", "pipe", "pipe"]
121
+ });
122
+ const exitCode = new Promise((resolve) => {
123
+ proc.on("close", (code) => resolve(code ?? 1));
124
+ });
125
+ const stdout = Readable.toWeb(proc.stdout);
126
+ return { exitCode, stdout };
127
+ }
128
+ function concatUint8Arrays(arrays) {
129
+ const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
130
+ const result = new Uint8Array(totalLength);
131
+ let offset = 0;
132
+ for (const a of arrays) {
133
+ result.set(a, offset);
134
+ offset += a.length;
135
+ }
136
+ return result;
137
+ }
138
+
139
+ // src/git-context.ts
140
+ import { readFileSync } from "node:fs";
141
+ import { spawnSync } from "node:child_process";
142
+ async function detectDefaultBase(exec, gitRoot) {
143
+ let currentBranch;
144
+ try {
145
+ currentBranch = (await exec(["git", "rev-parse", "--abbrev-ref", "HEAD"], gitRoot)).trim();
146
+ } catch {
147
+ return null;
148
+ }
149
+ if (currentBranch === "main" || currentBranch === "master") {
150
+ return null;
151
+ }
152
+ for (const candidate of ["main", "master"]) {
153
+ try {
154
+ await exec(["git", "rev-parse", "--verify", candidate], gitRoot);
155
+ return candidate;
156
+ } catch {}
157
+ }
158
+ return null;
159
+ }
160
+ var defaultExec = async (cmd, cwd) => {
161
+ const [command, ...args] = cmd;
162
+ const proc = spawnSync(command, args, { cwd, encoding: "utf-8" });
163
+ if (proc.status !== 0) {
164
+ const stderr = (proc.stderr ?? "").trim();
165
+ throw new Error(`Command failed (exit ${proc.status}): ${cmd.join(" ")}${stderr ? `: ${stderr}` : ""}`);
166
+ }
167
+ return proc.stdout ?? "";
168
+ };
169
+ var defaultReadFile = (path) => {
170
+ return readFileSync(path, "utf-8");
171
+ };
172
+ async function getRepoContext(demosDir, options) {
173
+ const exec = options?.exec ?? defaultExec;
174
+ const readFile = options?.readFile ?? defaultReadFile;
175
+ const gitRoot = (await exec(["git", "rev-parse", "--show-toplevel"], demosDir)).trim();
176
+ const diffBase = options?.diffBase ?? await detectDefaultBase(exec, gitRoot);
177
+ let gitDiff;
178
+ if (diffBase) {
179
+ gitDiff = (await exec(["git", "diff", `${diffBase}...HEAD`], gitRoot)).trim();
180
+ } else {
181
+ const workingDiff = (await exec(["git", "diff", "HEAD"], gitRoot)).trim();
182
+ if (workingDiff.length > 0) {
183
+ gitDiff = workingDiff;
184
+ } else {
185
+ gitDiff = (await exec(["git", "diff", "HEAD~1..HEAD"], gitRoot)).trim();
186
+ }
187
+ }
188
+ const lsOutput = (await exec(["git", "ls-files"], gitRoot)).trim();
189
+ const files = lsOutput.split(`
190
+ `).filter((f) => f.length > 0);
191
+ const guidelinePatterns = ["CLAUDE.md", "SKILL.md"];
192
+ const guidelines = [];
193
+ for (const file of files) {
194
+ const basename = file.split("/").pop() ?? "";
195
+ if (guidelinePatterns.includes(basename)) {
196
+ const fullPath = `${gitRoot}/${file}`;
197
+ const content = readFile(fullPath);
198
+ guidelines.push(`# ${file}
199
+ ${content}`);
200
+ }
201
+ }
202
+ return { gitDiff, guidelines };
203
+ }
204
+
205
+ // ../../node_modules/universal-user-agent/index.js
206
+ function getUserAgent() {
207
+ if (typeof navigator === "object" && "userAgent" in navigator) {
208
+ return navigator.userAgent;
209
+ }
210
+ if (typeof process === "object" && process.version !== undefined) {
211
+ return `Node.js/${process.version.substr(1)} (${process.platform}; ${process.arch})`;
212
+ }
213
+ return "<environment undetectable>";
214
+ }
215
+
216
+ // ../../node_modules/@octokit/endpoint/dist-bundle/index.js
217
+ var VERSION = "0.0.0-development";
218
+ var userAgent = `octokit-endpoint.js/${VERSION} ${getUserAgent()}`;
219
+ var DEFAULTS = {
220
+ method: "GET",
221
+ baseUrl: "https://api.github.com",
222
+ headers: {
223
+ accept: "application/vnd.github.v3+json",
224
+ "user-agent": userAgent
225
+ },
226
+ mediaType: {
227
+ format: ""
228
+ }
229
+ };
230
+ function lowercaseKeys(object) {
231
+ if (!object) {
232
+ return {};
233
+ }
234
+ return Object.keys(object).reduce((newObj, key) => {
235
+ newObj[key.toLowerCase()] = object[key];
236
+ return newObj;
237
+ }, {});
238
+ }
239
+ function isPlainObject(value) {
240
+ if (typeof value !== "object" || value === null)
241
+ return false;
242
+ if (Object.prototype.toString.call(value) !== "[object Object]")
243
+ return false;
244
+ const proto = Object.getPrototypeOf(value);
245
+ if (proto === null)
246
+ return true;
247
+ const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
248
+ return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
249
+ }
250
+ function mergeDeep(defaults, options) {
251
+ const result = Object.assign({}, defaults);
252
+ Object.keys(options).forEach((key) => {
253
+ if (isPlainObject(options[key])) {
254
+ if (!(key in defaults))
255
+ Object.assign(result, { [key]: options[key] });
256
+ else
257
+ result[key] = mergeDeep(defaults[key], options[key]);
258
+ } else {
259
+ Object.assign(result, { [key]: options[key] });
260
+ }
261
+ });
262
+ return result;
263
+ }
264
+ function removeUndefinedProperties(obj) {
265
+ for (const key in obj) {
266
+ if (obj[key] === undefined) {
267
+ delete obj[key];
268
+ }
269
+ }
270
+ return obj;
271
+ }
272
+ function merge(defaults, route, options) {
273
+ if (typeof route === "string") {
274
+ let [method, url] = route.split(" ");
275
+ options = Object.assign(url ? { method, url } : { url: method }, options);
276
+ } else {
277
+ options = Object.assign({}, route);
278
+ }
279
+ options.headers = lowercaseKeys(options.headers);
280
+ removeUndefinedProperties(options);
281
+ removeUndefinedProperties(options.headers);
282
+ const mergedOptions = mergeDeep(defaults || {}, options);
283
+ if (options.url === "/graphql") {
284
+ if (defaults && defaults.mediaType.previews?.length) {
285
+ mergedOptions.mediaType.previews = defaults.mediaType.previews.filter((preview) => !mergedOptions.mediaType.previews.includes(preview)).concat(mergedOptions.mediaType.previews);
286
+ }
287
+ mergedOptions.mediaType.previews = (mergedOptions.mediaType.previews || []).map((preview) => preview.replace(/-preview/, ""));
288
+ }
289
+ return mergedOptions;
290
+ }
291
+ function addQueryParameters(url, parameters) {
292
+ const separator = /\?/.test(url) ? "&" : "?";
293
+ const names = Object.keys(parameters);
294
+ if (names.length === 0) {
295
+ return url;
296
+ }
297
+ return url + separator + names.map((name) => {
298
+ if (name === "q") {
299
+ return "q=" + parameters.q.split("+").map(encodeURIComponent).join("+");
300
+ }
301
+ return `${name}=${encodeURIComponent(parameters[name])}`;
302
+ }).join("&");
303
+ }
304
+ var urlVariableRegex = /\{[^{}}]+\}/g;
305
+ function removeNonChars(variableName) {
306
+ return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
307
+ }
308
+ function extractUrlVariableNames(url) {
309
+ const matches = url.match(urlVariableRegex);
310
+ if (!matches) {
311
+ return [];
312
+ }
313
+ return matches.map(removeNonChars).reduce((a, b) => a.concat(b), []);
314
+ }
315
+ function omit(object, keysToOmit) {
316
+ const result = { __proto__: null };
317
+ for (const key of Object.keys(object)) {
318
+ if (keysToOmit.indexOf(key) === -1) {
319
+ result[key] = object[key];
320
+ }
321
+ }
322
+ return result;
323
+ }
324
+ function encodeReserved(str) {
325
+ return str.split(/(%[0-9A-Fa-f]{2})/g).map(function(part) {
326
+ if (!/%[0-9A-Fa-f]/.test(part)) {
327
+ part = encodeURI(part).replace(/%5B/g, "[").replace(/%5D/g, "]");
328
+ }
329
+ return part;
330
+ }).join("");
331
+ }
332
+ function encodeUnreserved(str) {
333
+ return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
334
+ return "%" + c.charCodeAt(0).toString(16).toUpperCase();
335
+ });
336
+ }
337
+ function encodeValue(operator, value, key) {
338
+ value = operator === "+" || operator === "#" ? encodeReserved(value) : encodeUnreserved(value);
339
+ if (key) {
340
+ return encodeUnreserved(key) + "=" + value;
341
+ } else {
342
+ return value;
343
+ }
344
+ }
345
+ function isDefined(value) {
346
+ return value !== undefined && value !== null;
347
+ }
348
+ function isKeyOperator(operator) {
349
+ return operator === ";" || operator === "&" || operator === "?";
350
+ }
351
+ function getValues(context, operator, key, modifier) {
352
+ var value = context[key], result = [];
353
+ if (isDefined(value) && value !== "") {
354
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
355
+ value = value.toString();
356
+ if (modifier && modifier !== "*") {
357
+ value = value.substring(0, parseInt(modifier, 10));
358
+ }
359
+ result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : ""));
360
+ } else {
361
+ if (modifier === "*") {
362
+ if (Array.isArray(value)) {
363
+ value.filter(isDefined).forEach(function(value2) {
364
+ result.push(encodeValue(operator, value2, isKeyOperator(operator) ? key : ""));
365
+ });
366
+ } else {
367
+ Object.keys(value).forEach(function(k) {
368
+ if (isDefined(value[k])) {
369
+ result.push(encodeValue(operator, value[k], k));
370
+ }
371
+ });
372
+ }
373
+ } else {
374
+ const tmp = [];
375
+ if (Array.isArray(value)) {
376
+ value.filter(isDefined).forEach(function(value2) {
377
+ tmp.push(encodeValue(operator, value2));
378
+ });
379
+ } else {
380
+ Object.keys(value).forEach(function(k) {
381
+ if (isDefined(value[k])) {
382
+ tmp.push(encodeUnreserved(k));
383
+ tmp.push(encodeValue(operator, value[k].toString()));
384
+ }
385
+ });
386
+ }
387
+ if (isKeyOperator(operator)) {
388
+ result.push(encodeUnreserved(key) + "=" + tmp.join(","));
389
+ } else if (tmp.length !== 0) {
390
+ result.push(tmp.join(","));
391
+ }
392
+ }
393
+ }
394
+ } else {
395
+ if (operator === ";") {
396
+ if (isDefined(value)) {
397
+ result.push(encodeUnreserved(key));
398
+ }
399
+ } else if (value === "" && (operator === "&" || operator === "?")) {
400
+ result.push(encodeUnreserved(key) + "=");
401
+ } else if (value === "") {
402
+ result.push("");
403
+ }
404
+ }
405
+ return result;
406
+ }
407
+ function parseUrl(template) {
408
+ return {
409
+ expand: expand.bind(null, template)
410
+ };
411
+ }
412
+ function expand(template, context) {
413
+ var operators = ["+", "#", ".", "/", ";", "?", "&"];
414
+ template = template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function(_, expression, literal) {
415
+ if (expression) {
416
+ let operator = "";
417
+ const values = [];
418
+ if (operators.indexOf(expression.charAt(0)) !== -1) {
419
+ operator = expression.charAt(0);
420
+ expression = expression.substr(1);
421
+ }
422
+ expression.split(/,/g).forEach(function(variable) {
423
+ var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
424
+ values.push(getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
425
+ });
426
+ if (operator && operator !== "+") {
427
+ var separator = ",";
428
+ if (operator === "?") {
429
+ separator = "&";
430
+ } else if (operator !== "#") {
431
+ separator = operator;
432
+ }
433
+ return (values.length !== 0 ? operator : "") + values.join(separator);
434
+ } else {
435
+ return values.join(",");
436
+ }
437
+ } else {
438
+ return encodeReserved(literal);
439
+ }
440
+ });
441
+ if (template === "/") {
442
+ return template;
443
+ } else {
444
+ return template.replace(/\/$/, "");
445
+ }
446
+ }
447
+ function parse(options) {
448
+ let method = options.method.toUpperCase();
449
+ let url = (options.url || "/").replace(/:([a-z]\w+)/g, "{$1}");
450
+ let headers = Object.assign({}, options.headers);
451
+ let body;
452
+ let parameters = omit(options, [
453
+ "method",
454
+ "baseUrl",
455
+ "url",
456
+ "headers",
457
+ "request",
458
+ "mediaType"
459
+ ]);
460
+ const urlVariableNames = extractUrlVariableNames(url);
461
+ url = parseUrl(url).expand(parameters);
462
+ if (!/^http/.test(url)) {
463
+ url = options.baseUrl + url;
464
+ }
465
+ const omittedParameters = Object.keys(options).filter((option) => urlVariableNames.includes(option)).concat("baseUrl");
466
+ const remainingParameters = omit(parameters, omittedParameters);
467
+ const isBinaryRequest = /application\/octet-stream/i.test(headers.accept);
468
+ if (!isBinaryRequest) {
469
+ if (options.mediaType.format) {
470
+ headers.accept = headers.accept.split(/,/).map((format) => format.replace(/application\/vnd(\.\w+)(\.v3)?(\.\w+)?(\+json)?$/, `application/vnd$1$2.${options.mediaType.format}`)).join(",");
471
+ }
472
+ if (url.endsWith("/graphql")) {
473
+ if (options.mediaType.previews?.length) {
474
+ const previewsFromAcceptHeader = headers.accept.match(/(?<![\w-])[\w-]+(?=-preview)/g) || [];
475
+ headers.accept = previewsFromAcceptHeader.concat(options.mediaType.previews).map((preview) => {
476
+ const format = options.mediaType.format ? `.${options.mediaType.format}` : "+json";
477
+ return `application/vnd.github.${preview}-preview${format}`;
478
+ }).join(",");
479
+ }
480
+ }
481
+ }
482
+ if (["GET", "HEAD"].includes(method)) {
483
+ url = addQueryParameters(url, remainingParameters);
484
+ } else {
485
+ if ("data" in remainingParameters) {
486
+ body = remainingParameters.data;
487
+ } else {
488
+ if (Object.keys(remainingParameters).length) {
489
+ body = remainingParameters;
490
+ }
491
+ }
492
+ }
493
+ if (!headers["content-type"] && typeof body !== "undefined") {
494
+ headers["content-type"] = "application/json; charset=utf-8";
495
+ }
496
+ if (["PATCH", "PUT"].includes(method) && typeof body === "undefined") {
497
+ body = "";
498
+ }
499
+ return Object.assign({ method, url, headers }, typeof body !== "undefined" ? { body } : null, options.request ? { request: options.request } : null);
500
+ }
501
+ function endpointWithDefaults(defaults, route, options) {
502
+ return parse(merge(defaults, route, options));
503
+ }
504
+ function withDefaults(oldDefaults, newDefaults) {
505
+ const DEFAULTS2 = merge(oldDefaults, newDefaults);
506
+ const endpoint2 = endpointWithDefaults.bind(null, DEFAULTS2);
507
+ return Object.assign(endpoint2, {
508
+ DEFAULTS: DEFAULTS2,
509
+ defaults: withDefaults.bind(null, DEFAULTS2),
510
+ merge: merge.bind(null, DEFAULTS2),
511
+ parse
512
+ });
513
+ }
514
+ var endpoint = withDefaults(null, DEFAULTS);
515
+
516
+ // ../../node_modules/fast-content-type-parse/index.js
517
+ var NullObject = function NullObject2() {};
518
+ NullObject.prototype = Object.create(null);
519
+ var paramRE = /; *([!#$%&'*+.^\w`|~-]+)=("(?:[\v\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\v\u0020-\u00ff])*"|[!#$%&'*+.^\w`|~-]+) */gu;
520
+ var quotedPairRE = /\\([\v\u0020-\u00ff])/gu;
521
+ var mediaTypeRE = /^[!#$%&'*+.^\w|~-]+\/[!#$%&'*+.^\w|~-]+$/u;
522
+ var defaultContentType = { type: "", parameters: new NullObject };
523
+ Object.freeze(defaultContentType.parameters);
524
+ Object.freeze(defaultContentType);
525
+ function safeParse(header) {
526
+ if (typeof header !== "string") {
527
+ return defaultContentType;
528
+ }
529
+ let index = header.indexOf(";");
530
+ const type = index !== -1 ? header.slice(0, index).trim() : header.trim();
531
+ if (mediaTypeRE.test(type) === false) {
532
+ return defaultContentType;
533
+ }
534
+ const result = {
535
+ type: type.toLowerCase(),
536
+ parameters: new NullObject
537
+ };
538
+ if (index === -1) {
539
+ return result;
540
+ }
541
+ let key;
542
+ let match;
543
+ let value;
544
+ paramRE.lastIndex = index;
545
+ while (match = paramRE.exec(header)) {
546
+ if (match.index !== index) {
547
+ return defaultContentType;
548
+ }
549
+ index += match[0].length;
550
+ key = match[1].toLowerCase();
551
+ value = match[2];
552
+ if (value[0] === '"') {
553
+ value = value.slice(1, value.length - 1);
554
+ quotedPairRE.test(value) && (value = value.replace(quotedPairRE, "$1"));
555
+ }
556
+ result.parameters[key] = value;
557
+ }
558
+ if (index !== header.length) {
559
+ return defaultContentType;
560
+ }
561
+ return result;
562
+ }
563
+ var $safeParse = safeParse;
564
+
565
+ // ../../node_modules/@octokit/request-error/dist-src/index.js
566
+ class RequestError extends Error {
567
+ name;
568
+ status;
569
+ request;
570
+ response;
571
+ constructor(message, statusCode, options) {
572
+ super(message);
573
+ this.name = "HttpError";
574
+ this.status = Number.parseInt(statusCode);
575
+ if (Number.isNaN(this.status)) {
576
+ this.status = 0;
577
+ }
578
+ if ("response" in options) {
579
+ this.response = options.response;
580
+ }
581
+ const requestCopy = Object.assign({}, options.request);
582
+ if (options.request.headers.authorization) {
583
+ requestCopy.headers = Object.assign({}, options.request.headers, {
584
+ authorization: options.request.headers.authorization.replace(/(?<! ) .*$/, " [REDACTED]")
585
+ });
586
+ }
587
+ requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
588
+ this.request = requestCopy;
589
+ }
590
+ }
591
+
592
+ // ../../node_modules/@octokit/request/dist-bundle/index.js
593
+ var VERSION2 = "9.2.4";
594
+ var defaults_default = {
595
+ headers: {
596
+ "user-agent": `octokit-request.js/${VERSION2} ${getUserAgent()}`
597
+ }
598
+ };
599
+ function isPlainObject2(value) {
600
+ if (typeof value !== "object" || value === null)
601
+ return false;
602
+ if (Object.prototype.toString.call(value) !== "[object Object]")
603
+ return false;
604
+ const proto = Object.getPrototypeOf(value);
605
+ if (proto === null)
606
+ return true;
607
+ const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
608
+ return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
609
+ }
610
+ async function fetchWrapper(requestOptions) {
611
+ const fetch = requestOptions.request?.fetch || globalThis.fetch;
612
+ if (!fetch) {
613
+ throw new Error("fetch is not set. Please pass a fetch implementation as new Octokit({ request: { fetch }}). Learn more at https://github.com/octokit/octokit.js/#fetch-missing");
614
+ }
615
+ const log = requestOptions.request?.log || console;
616
+ const parseSuccessResponseBody = requestOptions.request?.parseSuccessResponseBody !== false;
617
+ const body = isPlainObject2(requestOptions.body) || Array.isArray(requestOptions.body) ? JSON.stringify(requestOptions.body) : requestOptions.body;
618
+ const requestHeaders = Object.fromEntries(Object.entries(requestOptions.headers).map(([name, value]) => [
619
+ name,
620
+ String(value)
621
+ ]));
622
+ let fetchResponse;
623
+ try {
624
+ fetchResponse = await fetch(requestOptions.url, {
625
+ method: requestOptions.method,
626
+ body,
627
+ redirect: requestOptions.request?.redirect,
628
+ headers: requestHeaders,
629
+ signal: requestOptions.request?.signal,
630
+ ...requestOptions.body && { duplex: "half" }
631
+ });
632
+ } catch (error) {
633
+ let message = "Unknown Error";
634
+ if (error instanceof Error) {
635
+ if (error.name === "AbortError") {
636
+ error.status = 500;
637
+ throw error;
638
+ }
639
+ message = error.message;
640
+ if (error.name === "TypeError" && "cause" in error) {
641
+ if (error.cause instanceof Error) {
642
+ message = error.cause.message;
643
+ } else if (typeof error.cause === "string") {
644
+ message = error.cause;
645
+ }
646
+ }
647
+ }
648
+ const requestError = new RequestError(message, 500, {
649
+ request: requestOptions
650
+ });
651
+ requestError.cause = error;
652
+ throw requestError;
653
+ }
654
+ const status = fetchResponse.status;
655
+ const url = fetchResponse.url;
656
+ const responseHeaders = {};
657
+ for (const [key, value] of fetchResponse.headers) {
658
+ responseHeaders[key] = value;
659
+ }
660
+ const octokitResponse = {
661
+ url,
662
+ status,
663
+ headers: responseHeaders,
664
+ data: ""
665
+ };
666
+ if ("deprecation" in responseHeaders) {
667
+ const matches = responseHeaders.link && responseHeaders.link.match(/<([^<>]+)>; rel="deprecation"/);
668
+ const deprecationLink = matches && matches.pop();
669
+ log.warn(`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${responseHeaders.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`);
670
+ }
671
+ if (status === 204 || status === 205) {
672
+ return octokitResponse;
673
+ }
674
+ if (requestOptions.method === "HEAD") {
675
+ if (status < 400) {
676
+ return octokitResponse;
677
+ }
678
+ throw new RequestError(fetchResponse.statusText, status, {
679
+ response: octokitResponse,
680
+ request: requestOptions
681
+ });
682
+ }
683
+ if (status === 304) {
684
+ octokitResponse.data = await getResponseData(fetchResponse);
685
+ throw new RequestError("Not modified", status, {
686
+ response: octokitResponse,
687
+ request: requestOptions
688
+ });
689
+ }
690
+ if (status >= 400) {
691
+ octokitResponse.data = await getResponseData(fetchResponse);
692
+ throw new RequestError(toErrorMessage(octokitResponse.data), status, {
693
+ response: octokitResponse,
694
+ request: requestOptions
695
+ });
696
+ }
697
+ octokitResponse.data = parseSuccessResponseBody ? await getResponseData(fetchResponse) : fetchResponse.body;
698
+ return octokitResponse;
699
+ }
700
+ async function getResponseData(response) {
701
+ const contentType = response.headers.get("content-type");
702
+ if (!contentType) {
703
+ return response.text().catch(() => "");
704
+ }
705
+ const mimetype = $safeParse(contentType);
706
+ if (isJSONResponse(mimetype)) {
707
+ let text = "";
708
+ try {
709
+ text = await response.text();
710
+ return JSON.parse(text);
711
+ } catch (err) {
712
+ return text;
713
+ }
714
+ } else if (mimetype.type.startsWith("text/") || mimetype.parameters.charset?.toLowerCase() === "utf-8") {
715
+ return response.text().catch(() => "");
716
+ } else {
717
+ return response.arrayBuffer().catch(() => new ArrayBuffer(0));
718
+ }
719
+ }
720
+ function isJSONResponse(mimetype) {
721
+ return mimetype.type === "application/json" || mimetype.type === "application/scim+json";
722
+ }
723
+ function toErrorMessage(data) {
724
+ if (typeof data === "string") {
725
+ return data;
726
+ }
727
+ if (data instanceof ArrayBuffer) {
728
+ return "Unknown error";
729
+ }
730
+ if ("message" in data) {
731
+ const suffix = "documentation_url" in data ? ` - ${data.documentation_url}` : "";
732
+ return Array.isArray(data.errors) ? `${data.message}: ${data.errors.map((v) => JSON.stringify(v)).join(", ")}${suffix}` : `${data.message}${suffix}`;
733
+ }
734
+ return `Unknown error: ${JSON.stringify(data)}`;
735
+ }
736
+ function withDefaults2(oldEndpoint, newDefaults) {
737
+ const endpoint2 = oldEndpoint.defaults(newDefaults);
738
+ const newApi = function(route, parameters) {
739
+ const endpointOptions = endpoint2.merge(route, parameters);
740
+ if (!endpointOptions.request || !endpointOptions.request.hook) {
741
+ return fetchWrapper(endpoint2.parse(endpointOptions));
742
+ }
743
+ const request2 = (route2, parameters2) => {
744
+ return fetchWrapper(endpoint2.parse(endpoint2.merge(route2, parameters2)));
745
+ };
746
+ Object.assign(request2, {
747
+ endpoint: endpoint2,
748
+ defaults: withDefaults2.bind(null, endpoint2)
749
+ });
750
+ return endpointOptions.request.hook(request2, endpointOptions);
751
+ };
752
+ return Object.assign(newApi, {
753
+ endpoint: endpoint2,
754
+ defaults: withDefaults2.bind(null, endpoint2)
755
+ });
756
+ }
757
+ var request = withDefaults2(endpoint, defaults_default);
758
+
759
+ // ../../node_modules/@octokit/graphql/dist-bundle/index.js
760
+ var VERSION3 = "0.0.0-development";
761
+ function _buildMessageForResponseErrors(data) {
762
+ return `Request failed due to following response errors:
763
+ ` + data.errors.map((e) => ` - ${e.message}`).join(`
764
+ `);
765
+ }
766
+ var GraphqlResponseError = class extends Error {
767
+ constructor(request2, headers, response) {
768
+ super(_buildMessageForResponseErrors(response));
769
+ this.request = request2;
770
+ this.headers = headers;
771
+ this.response = response;
772
+ this.errors = response.errors;
773
+ this.data = response.data;
774
+ if (Error.captureStackTrace) {
775
+ Error.captureStackTrace(this, this.constructor);
776
+ }
777
+ }
778
+ name = "GraphqlResponseError";
779
+ errors;
780
+ data;
781
+ };
782
+ var NON_VARIABLE_OPTIONS = [
783
+ "method",
784
+ "baseUrl",
785
+ "url",
786
+ "headers",
787
+ "request",
788
+ "query",
789
+ "mediaType",
790
+ "operationName"
791
+ ];
792
+ var FORBIDDEN_VARIABLE_OPTIONS = ["query", "method", "url"];
793
+ var GHES_V3_SUFFIX_REGEX = /\/api\/v3\/?$/;
794
+ function graphql(request2, query, options) {
795
+ if (options) {
796
+ if (typeof query === "string" && "query" in options) {
797
+ return Promise.reject(new Error(`[@octokit/graphql] "query" cannot be used as variable name`));
798
+ }
799
+ for (const key in options) {
800
+ if (!FORBIDDEN_VARIABLE_OPTIONS.includes(key))
801
+ continue;
802
+ return Promise.reject(new Error(`[@octokit/graphql] "${key}" cannot be used as variable name`));
803
+ }
804
+ }
805
+ const parsedOptions = typeof query === "string" ? Object.assign({ query }, options) : query;
806
+ const requestOptions = Object.keys(parsedOptions).reduce((result, key) => {
807
+ if (NON_VARIABLE_OPTIONS.includes(key)) {
808
+ result[key] = parsedOptions[key];
809
+ return result;
810
+ }
811
+ if (!result.variables) {
812
+ result.variables = {};
813
+ }
814
+ result.variables[key] = parsedOptions[key];
815
+ return result;
816
+ }, {});
817
+ const baseUrl = parsedOptions.baseUrl || request2.endpoint.DEFAULTS.baseUrl;
818
+ if (GHES_V3_SUFFIX_REGEX.test(baseUrl)) {
819
+ requestOptions.url = baseUrl.replace(GHES_V3_SUFFIX_REGEX, "/api/graphql");
820
+ }
821
+ return request2(requestOptions).then((response) => {
822
+ if (response.data.errors) {
823
+ const headers = {};
824
+ for (const key of Object.keys(response.headers)) {
825
+ headers[key] = response.headers[key];
826
+ }
827
+ throw new GraphqlResponseError(requestOptions, headers, response.data);
828
+ }
829
+ return response.data.data;
830
+ });
831
+ }
832
+ function withDefaults3(request2, newDefaults) {
833
+ const newRequest = request2.defaults(newDefaults);
834
+ const newApi = (query, options) => {
835
+ return graphql(newRequest, query, options);
836
+ };
837
+ return Object.assign(newApi, {
838
+ defaults: withDefaults3.bind(null, newRequest),
839
+ endpoint: newRequest.endpoint
840
+ });
841
+ }
842
+ var graphql2 = withDefaults3(request, {
843
+ headers: {
844
+ "user-agent": `octokit-graphql.js/${VERSION3} ${getUserAgent()}`
845
+ },
846
+ method: "POST",
847
+ url: "/graphql"
848
+ });
849
+
850
+ // src/github-issue.ts
851
+ import { spawnSync as spawnSync2 } from "node:child_process";
852
+ var ISSUE_QUERY = `
853
+ query($owner: String!, $repo: String!, $number: Int!) {
854
+ repository(owner: $owner, name: $repo) {
855
+ issue(number: $number) {
856
+ number
857
+ title
858
+ body
859
+ state
860
+ labels(first: 20) {
861
+ nodes {
862
+ name
863
+ }
864
+ }
865
+ }
866
+ }
867
+ }
868
+ `;
869
+ function getRepoInfoFromGit() {
870
+ try {
871
+ const proc = spawnSync2("git", ["remote", "get-url", "origin"], { encoding: "utf-8" });
872
+ if (proc.status !== 0 || !proc.stdout) {
873
+ return null;
874
+ }
875
+ const url = proc.stdout.trim();
876
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
877
+ if (sshMatch) {
878
+ return { owner: sshMatch[1], repo: sshMatch[2] };
879
+ }
880
+ const httpsMatch = url.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
881
+ if (httpsMatch) {
882
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
883
+ }
884
+ return null;
885
+ } catch {
886
+ return null;
887
+ }
888
+ }
889
+ function getTokenFromEnvironment() {
890
+ return process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"] ?? null;
891
+ }
892
+ async function fetchGitHubIssue(issueId, options) {
893
+ const issueNumber = typeof issueId === "string" ? parseInt(issueId, 10) : issueId;
894
+ if (isNaN(issueNumber)) {
895
+ throw new Error(`Invalid issue ID: ${issueId}`);
896
+ }
897
+ let owner = options?.owner;
898
+ let repo = options?.repo;
899
+ if (!owner || !repo) {
900
+ const detected = getRepoInfoFromGit();
901
+ if (!detected) {
902
+ throw new Error("Could not detect repository owner/name from git remote. " + "Please provide owner and repo options, or run from a git repository with a GitHub origin.");
903
+ }
904
+ owner = owner ?? detected.owner;
905
+ repo = repo ?? detected.repo;
906
+ }
907
+ const token = options?.token ?? getTokenFromEnvironment();
908
+ if (!token) {
909
+ throw new Error("No GitHub token found. Please set GITHUB_TOKEN or GH_TOKEN environment variable, " + "or provide token in options.");
910
+ }
911
+ const graphqlFn = options?.graphql ?? graphql2.defaults({
912
+ headers: {
913
+ authorization: `token ${token}`
914
+ }
915
+ });
916
+ const response = await graphqlFn(ISSUE_QUERY, {
917
+ owner,
918
+ repo,
919
+ number: issueNumber
920
+ });
921
+ const issue = response.repository.issue;
922
+ return {
923
+ number: issue.number,
924
+ title: issue.title,
925
+ body: issue.body ?? "",
926
+ labels: issue.labels.nodes.map((l) => l.name),
927
+ state: issue.state.toLowerCase()
928
+ };
929
+ }
930
+
931
+ // src/review-generator.ts
932
+ import { existsSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
933
+ import { join, dirname, relative } from "node:path";
934
+ import { fileURLToPath } from "node:url";
935
+ function getReviewTemplate() {
936
+ const currentFile = fileURLToPath(import.meta.url);
937
+ const distDir = dirname(currentFile);
938
+ const templatePath = join(distDir, "review-template.html");
939
+ if (!existsSync(templatePath)) {
940
+ throw new Error(`Review template not found at ${templatePath}. ` + `Make sure to build the review-app package first.`);
941
+ }
942
+ return readFileSync2(templatePath, "utf-8");
943
+ }
944
+ function escapeHtml(s) {
945
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
946
+ }
947
+ function generateReviewHtml(appData) {
948
+ const template = getReviewTemplate();
949
+ const jsonData = JSON.stringify(appData);
950
+ return template.replace("<title>Demo Review</title>", `<title>${escapeHtml(appData.title)}</title>`).replace('"{{__INJECT_REVIEW_DATA__}}"', jsonData);
951
+ }
952
+ function discoverDemoFiles(directory) {
953
+ const files = [];
954
+ const processFile = (filePath, filename) => {
955
+ const relativePath = relative(directory, filePath);
956
+ if (filename.endsWith(".webm")) {
957
+ files.push({ path: filePath, filename, relativePath, type: "web-ux" });
958
+ } else if (filename.endsWith(".jsonl")) {
959
+ files.push({ path: filePath, filename, relativePath, type: "log-based" });
960
+ }
961
+ };
962
+ for (const f of readdirSync(directory)) {
963
+ processFile(join(directory, f), f);
964
+ }
965
+ if (files.length === 0) {
966
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
967
+ if (!entry.isDirectory())
968
+ continue;
969
+ const subdir = join(directory, entry.name);
970
+ for (const f of readdirSync(subdir)) {
971
+ processFile(join(subdir, f), f);
972
+ }
973
+ }
974
+ }
975
+ return files.sort((a, b) => a.filename.localeCompare(b.filename));
976
+ }
977
+
978
+ // src/orchestrator.ts
979
+ var GIT_DIFF_MAX_CHARS = 50000;
980
+ var defaultExec2 = async (cmd, cwd) => {
981
+ const [command, ...args] = cmd;
982
+ const proc = spawnSync3(command, args, { cwd, encoding: "utf-8" });
983
+ if (proc.status !== 0) {
984
+ const stderr = (proc.stderr ?? "").trim();
985
+ throw new Error(`Command failed (exit ${proc.status}): ${cmd.join(" ")}${stderr ? `: ${stderr}` : ""}`);
986
+ }
987
+ return proc.stdout ?? "";
988
+ };
989
+ async function getGitRoot(exec, cwd) {
990
+ return (await exec(["git", "rev-parse", "--show-toplevel"], cwd)).trim();
991
+ }
992
+ async function getCurrentBranch(exec, cwd) {
993
+ const branch = (await exec(["git", "branch", "--show-current"], cwd)).trim();
994
+ return branch.replace(/\//g, "-") || "unknown";
995
+ }
996
+ function buildPresenterPrompt(options) {
997
+ const { issue, gitDiff, guidelines, reviewFolder, assetsFolder, testsFolder } = options;
998
+ let diff = gitDiff;
999
+ if (diff.length > GIT_DIFF_MAX_CHARS) {
1000
+ diff = diff.slice(0, GIT_DIFF_MAX_CHARS) + `
1001
+
1002
+ ... (diff truncated at 50k characters)`;
1003
+ }
1004
+ const sections = [];
1005
+ sections.push(`## GitHub Issue #${issue.number}: ${issue.title}
1006
+
1007
+ ${issue.body}`);
1008
+ if (guidelines.length > 0) {
1009
+ sections.push(`## Coding Guidelines
1010
+
1011
+ ${guidelines.join(`
1012
+
1013
+ `)}`);
1014
+ }
1015
+ sections.push(`## Git Diff
1016
+
1017
+ \`\`\`diff
1018
+ ${diff}
1019
+ \`\`\``);
1020
+ return `You are a demo presenter. You must create demo recordings that showcase the feature described in the GitHub issue.
1021
+
1022
+ ${sections.join(`
1023
+
1024
+ `)}
1025
+
1026
+ ## Configuration
1027
+
1028
+ - **Review Folder:** ${reviewFolder}
1029
+ - **Assets Directory:** ${assetsFolder}
1030
+ - **Tests Directory:** ${testsFolder}
1031
+
1032
+ ## Task
1033
+
1034
+ Based on the GitHub issue and git diff above, create demo recordings that demonstrate each acceptance criterion is met.
1035
+
1036
+ For web-ux demos:
1037
+ - Create Playwright test files in the Tests Directory
1038
+ - Use DemoRecorder to capture steps
1039
+ - Save recordings to the Assets Directory
1040
+
1041
+ For log-based demos:
1042
+ - Create .jsonl files directly in the Assets Directory
1043
+ - Use demon__highlight annotations for key lines
1044
+
1045
+ After creating demos, report:
1046
+ 1. List of demo test files created (paths to .demo.ts files)
1047
+ 2. List of artifact files generated (.webm for web-ux, .jsonl for log-based)
1048
+ 3. Any errors encountered`;
1049
+ }
1050
+ function buildReviewerPrompt(options) {
1051
+ const { issue, gitDiff, guidelines, demoFiles, stepsMap, logsMap } = options;
1052
+ let diff = gitDiff;
1053
+ if (diff.length > GIT_DIFF_MAX_CHARS) {
1054
+ diff = diff.slice(0, GIT_DIFF_MAX_CHARS) + `
1055
+
1056
+ ... (diff truncated at 50k characters)`;
1057
+ }
1058
+ const sections = [];
1059
+ sections.push(`## GitHub Issue #${issue.number}: ${issue.title}
1060
+
1061
+ ${issue.body}`);
1062
+ if (guidelines.length > 0) {
1063
+ sections.push(`## Coding Guidelines
1064
+
1065
+ ${guidelines.join(`
1066
+
1067
+ `)}`);
1068
+ }
1069
+ sections.push(`## Git Diff
1070
+
1071
+ \`\`\`diff
1072
+ ${diff}
1073
+ \`\`\``);
1074
+ const demoEntries = demoFiles.map((f) => {
1075
+ if (f.type === "web-ux") {
1076
+ const steps = stepsMap[f.filename] ?? stepsMap[f.relativePath] ?? [];
1077
+ const stepLines = steps.map((s) => `- [${s.timestampSeconds}s] ${s.text}`).join(`
1078
+ `);
1079
+ return `Video: ${f.relativePath}
1080
+ Recorded steps:
1081
+ ${stepLines || "(no steps recorded)"}`;
1082
+ } else {
1083
+ const logContent = logsMap[f.relativePath] ?? "";
1084
+ const preview = logContent.split(`
1085
+ `).slice(0, 20).join(`
1086
+ `);
1087
+ return `Log: ${f.relativePath}
1088
+ Content preview:
1089
+ ${preview}`;
1090
+ }
1091
+ });
1092
+ sections.push(`## Demo Recordings
1093
+
1094
+ ${demoEntries.join(`
1095
+
1096
+ `)}`);
1097
+ return `You are a code reviewer. You are given a GitHub issue, git diff, coding guidelines, and demo recordings that show the feature in action.
1098
+
1099
+ ${sections.join(`
1100
+
1101
+ `)}
1102
+
1103
+ ## Task
1104
+
1105
+ Review the code changes and demo recordings against the GitHub issue's acceptance criteria. Generate a JSON object matching this exact schema:
1106
+
1107
+ {
1108
+ "demos": [
1109
+ {
1110
+ "file": "<filename>",
1111
+ "summary": "<a meaningful sentence describing what this demo showcases based on the steps>"
1112
+ }
1113
+ ],
1114
+ "review": {
1115
+ "summary": "<2-3 sentence overview of the changes>",
1116
+ "highlights": ["<positive aspect 1>", "<positive aspect 2>"],
1117
+ "verdict": "approve" | "request_changes",
1118
+ "verdictReason": "<one sentence justifying the verdict>",
1119
+ "issues": [
1120
+ {
1121
+ "severity": "major" | "minor" | "nit",
1122
+ "description": "<what the issue is and how to fix it>"
1123
+ }
1124
+ ]
1125
+ }
1126
+ }
1127
+
1128
+ Rules:
1129
+ - Return ONLY the JSON object, no markdown fences or extra text.
1130
+ - Include one entry in "demos" for each demo file, in the same order.
1131
+ - "file" must exactly match the provided relative path.
1132
+ - "verdict" must be exactly "approve" or "request_changes".
1133
+ - Use "request_changes" if acceptance criteria from the issue are not demonstrated.
1134
+ - "severity" must be exactly "major", "minor", or "nit".
1135
+ - "major": bugs, security issues, broken functionality, missing acceptance criteria.
1136
+ - "minor": code quality, readability, missing edge cases.
1137
+ - "nit": style, naming, trivial improvements.
1138
+ - "highlights" must have at least one entry.
1139
+ - "issues" can be an empty array if there are no issues.
1140
+ - Verify that demo steps demonstrate ALL acceptance criteria from the issue.`;
1141
+ }
1142
+ function collectDemoData(demoFiles) {
1143
+ const stepsMapByFilename = {};
1144
+ const stepsMapByRelativePath = {};
1145
+ const logsMap = {};
1146
+ for (const demo of demoFiles) {
1147
+ if (demo.type === "web-ux") {
1148
+ const stepsPath = join2(pathDirname(demo.path), "demo-steps.json");
1149
+ if (existsSync2(stepsPath)) {
1150
+ try {
1151
+ const raw = readFileSync3(stepsPath, "utf-8");
1152
+ const parsed = JSON.parse(raw);
1153
+ if (Array.isArray(parsed)) {
1154
+ stepsMapByFilename[demo.filename] = parsed;
1155
+ stepsMapByRelativePath[demo.relativePath] = parsed;
1156
+ }
1157
+ } catch {}
1158
+ }
1159
+ } else {
1160
+ logsMap[demo.relativePath] = readFileSync3(demo.path, "utf-8");
1161
+ }
1162
+ }
1163
+ return { stepsMapByFilename, stepsMapByRelativePath, logsMap };
1164
+ }
1165
+ async function runReviewOrchestration(options) {
1166
+ const exec = options.exec ?? defaultExec2;
1167
+ const cwd = options.cwd ?? process.cwd();
1168
+ const issue = options.issue ?? await fetchGitHubIssue(options.issueId, options.github);
1169
+ const gitRoot = await getGitRoot(exec, cwd);
1170
+ const branchName = await getCurrentBranch(exec, cwd);
1171
+ const repoContext = await getRepoContext(gitRoot, {
1172
+ exec,
1173
+ diffBase: options.diffBase
1174
+ });
1175
+ const reviewFolder = join2(gitRoot, ".demoon", "reviews", branchName);
1176
+ const assetsFolder = join2(reviewFolder, "assets");
1177
+ const testsFolder = join2(reviewFolder, "tests");
1178
+ if (!existsSync2(reviewFolder)) {
1179
+ mkdirSync(reviewFolder, { recursive: true });
1180
+ }
1181
+ if (!existsSync2(assetsFolder)) {
1182
+ mkdirSync(assetsFolder, { recursive: true });
1183
+ }
1184
+ if (!existsSync2(testsFolder)) {
1185
+ mkdirSync(testsFolder, { recursive: true });
1186
+ }
1187
+ const presenterPrompt = buildPresenterPrompt({
1188
+ issue,
1189
+ gitDiff: repoContext.gitDiff,
1190
+ guidelines: repoContext.guidelines,
1191
+ reviewFolder,
1192
+ assetsFolder,
1193
+ testsFolder
1194
+ });
1195
+ await invokeClaude(presenterPrompt, { agent: options.agent, spawn: options.spawn });
1196
+ const demoFiles = discoverDemoFiles(assetsFolder);
1197
+ if (demoFiles.length === 0) {
1198
+ throw new Error(`No demo files (.webm or .jsonl) found in ${assetsFolder} after Presenter phase`);
1199
+ }
1200
+ const { stepsMapByFilename, stepsMapByRelativePath, logsMap } = collectDemoData(demoFiles);
1201
+ const reviewerPrompt = buildReviewerPrompt({
1202
+ issue,
1203
+ gitDiff: repoContext.gitDiff,
1204
+ guidelines: repoContext.guidelines,
1205
+ demoFiles,
1206
+ stepsMap: stepsMapByFilename,
1207
+ logsMap
1208
+ });
1209
+ const rawOutput = await invokeClaude(reviewerPrompt, { agent: options.agent, spawn: options.spawn });
1210
+ const llmResponse = parseLlmResponse(rawOutput);
1211
+ const filenameToRelativePath = new Map(demoFiles.map((d) => [d.filename, d.relativePath]));
1212
+ const typeMap = new Map(demoFiles.map((d) => [d.filename, d.type]));
1213
+ const metadata = {
1214
+ demos: llmResponse.demos.map((demo) => {
1215
+ const relativePath = filenameToRelativePath.get(demo.file) ?? demo.file;
1216
+ return {
1217
+ file: relativePath,
1218
+ type: typeMap.get(demo.file) ?? "web-ux",
1219
+ summary: demo.summary,
1220
+ steps: stepsMapByRelativePath[relativePath] ?? []
1221
+ };
1222
+ }),
1223
+ review: llmResponse.review
1224
+ };
1225
+ const metadataPath = join2(assetsFolder, "review-metadata.json");
1226
+ writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2) + `
1227
+ `);
1228
+ const appData = {
1229
+ metadata,
1230
+ title: `Review: Issue #${issue.number} - ${issue.title}`,
1231
+ videos: {},
1232
+ logs: Object.keys(logsMap).length > 0 ? logsMap : undefined,
1233
+ feedbackEndpoint: options.feedbackEndpoint
1234
+ };
1235
+ const html = generateReviewHtml(appData);
1236
+ const htmlPath = join2(assetsFolder, "review.html");
1237
+ writeFileSync2(htmlPath, html);
1238
+ return {
1239
+ reviewFolder,
1240
+ htmlPath,
1241
+ metadataPath,
1242
+ metadata,
1243
+ issue
1244
+ };
1245
+ }
1246
+
1247
+ // src/feedback-server.ts
1248
+ import { randomUUID } from "node:crypto";
1249
+ var pendingReviews = new Map;
1250
+ var CORS_HEADERS = {
1251
+ "Access-Control-Allow-Origin": "*",
1252
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
1253
+ "Access-Control-Allow-Headers": "Content-Type"
1254
+ };
1255
+ function isValidPayload(body) {
1256
+ if (typeof body !== "object" || body === null)
1257
+ return false;
1258
+ const obj = body;
1259
+ if (obj["verdict"] !== "approve" && obj["verdict"] !== "request_changes")
1260
+ return false;
1261
+ if (obj["feedback"] !== undefined && typeof obj["feedback"] !== "string")
1262
+ return false;
1263
+ return true;
1264
+ }
1265
+ function startFeedbackServer(preferredPort = 0) {
1266
+ const reviewId = randomUUID();
1267
+ const server = Bun.serve({
1268
+ port: preferredPort,
1269
+ async fetch(req) {
1270
+ const url = new URL(req.url);
1271
+ if (req.method === "OPTIONS") {
1272
+ return new Response(null, {
1273
+ status: 204,
1274
+ headers: CORS_HEADERS
1275
+ });
1276
+ }
1277
+ if (url.pathname === "/feedback" && req.method === "POST") {
1278
+ const requestReviewId = url.searchParams.get("reviewId");
1279
+ const headers = {
1280
+ "Content-Type": "application/json",
1281
+ ...CORS_HEADERS
1282
+ };
1283
+ if (!requestReviewId) {
1284
+ return new Response(JSON.stringify({ error: "Missing reviewId query parameter" }), { status: 400, headers });
1285
+ }
1286
+ const pending = pendingReviews.get(requestReviewId);
1287
+ if (!pending) {
1288
+ return new Response(JSON.stringify({ error: "Review not found or already completed" }), { status: 404, headers });
1289
+ }
1290
+ let body;
1291
+ try {
1292
+ body = await req.json();
1293
+ } catch {
1294
+ return new Response(JSON.stringify({ error: "Invalid JSON" }), { status: 400, headers });
1295
+ }
1296
+ if (!isValidPayload(body)) {
1297
+ return new Response(JSON.stringify({ error: "Invalid payload" }), { status: 400, headers });
1298
+ }
1299
+ pending.resolve(body);
1300
+ pendingReviews.delete(requestReviewId);
1301
+ return new Response(JSON.stringify({ success: true, verdict: body.verdict }), { status: 200, headers });
1302
+ }
1303
+ if (url.pathname === "/health") {
1304
+ return new Response(JSON.stringify({ status: "ok" }), {
1305
+ headers: { "Content-Type": "application/json" }
1306
+ });
1307
+ }
1308
+ return new Response("Not Found", { status: 404 });
1309
+ }
1310
+ });
1311
+ const port = server.port ?? 3000;
1312
+ const feedbackEndpoint = `http://localhost:${port}/feedback?reviewId=${reviewId}`;
1313
+ const feedbackPromise = new Promise((resolve, reject) => {
1314
+ pendingReviews.set(reviewId, { resolve, reject });
1315
+ });
1316
+ return {
1317
+ server,
1318
+ port,
1319
+ reviewId,
1320
+ feedbackEndpoint,
1321
+ waitForFeedback: () => feedbackPromise,
1322
+ stop: () => {
1323
+ const pending = pendingReviews.get(reviewId);
1324
+ if (pending) {
1325
+ pending.reject(new Error("Server stopped"));
1326
+ pendingReviews.delete(reviewId);
1327
+ }
1328
+ server.stop();
1329
+ }
1330
+ };
1331
+ }
1332
+
1333
+ // src/bin/demoon.ts
1334
+ function printUsage() {
1335
+ console.error("Usage: demoon <command> [options]");
1336
+ console.error("");
1337
+ console.error("Commands:");
1338
+ console.error(" review Generate a review from a GitHub issue");
1339
+ console.error("");
1340
+ console.error("Review options:");
1341
+ console.error(" --github-issue-id <id> GitHub issue number (required unless --issue-file is provided)");
1342
+ console.error(" --issue-file <path> Path to JSON file with issue data (for testing, skips GitHub API)");
1343
+ console.error(" --base <ref> Base commit/branch for diff (auto-detects main/master if on feature branch)");
1344
+ console.error(" --agent <path> Path to Claude agent binary");
1345
+ console.error(" --port <number> Port for feedback server (default: random available port)");
1346
+ console.error("");
1347
+ console.error("Environment variables:");
1348
+ console.error(" GITHUB_TOKEN or GH_TOKEN GitHub personal access token (required for API access)");
1349
+ }
1350
+ async function main() {
1351
+ const args = process.argv.slice(2);
1352
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
1353
+ printUsage();
1354
+ process.exit(args.length === 0 ? 1 : 0);
1355
+ }
1356
+ const command = args[0];
1357
+ if (command !== "review") {
1358
+ console.error(`Unknown command: ${command}`);
1359
+ console.error("");
1360
+ printUsage();
1361
+ process.exit(1);
1362
+ }
1363
+ let issueId;
1364
+ let issueFile;
1365
+ let diffBase;
1366
+ let agent;
1367
+ let port = 0;
1368
+ for (let i = 1;i < args.length; i++) {
1369
+ const arg = args[i];
1370
+ if (arg === "--github-issue-id" || arg === "--issue") {
1371
+ issueId = args[++i];
1372
+ } else if (arg === "--issue-file") {
1373
+ issueFile = args[++i];
1374
+ } else if (arg === "--base") {
1375
+ diffBase = args[++i];
1376
+ } else if (arg === "--agent") {
1377
+ agent = args[++i];
1378
+ } else if (arg === "--port") {
1379
+ port = parseInt(args[++i] ?? "0", 10);
1380
+ } else if (!arg?.startsWith("-")) {
1381
+ if (!issueId) {
1382
+ issueId = arg;
1383
+ }
1384
+ }
1385
+ }
1386
+ let issue;
1387
+ if (issueFile) {
1388
+ const content = readFileSync4(issueFile, "utf-8");
1389
+ issue = JSON.parse(content);
1390
+ issueId = String(issue.number);
1391
+ }
1392
+ if (!issueId && !issue) {
1393
+ console.error("Error: --github-issue-id or --issue-file is required");
1394
+ console.error("");
1395
+ printUsage();
1396
+ process.exit(1);
1397
+ }
1398
+ const feedbackServer = startFeedbackServer(port);
1399
+ try {
1400
+ if (issue) {
1401
+ console.log(`Using issue from file: #${issue.number} - ${issue.title}`);
1402
+ } else {
1403
+ console.log(`Fetching GitHub issue #${issueId}...`);
1404
+ }
1405
+ const result = await runReviewOrchestration({
1406
+ issueId,
1407
+ issue,
1408
+ diffBase,
1409
+ agent,
1410
+ feedbackEndpoint: feedbackServer.feedbackEndpoint
1411
+ });
1412
+ console.log("");
1413
+ console.log(`Review generated for issue #${result.issue.number}: ${result.issue.title}`);
1414
+ console.log("");
1415
+ console.log(`Verdict: ${result.metadata.review?.verdict ?? "unknown"}`);
1416
+ if (result.metadata.review?.verdictReason) {
1417
+ console.log(`Reason: ${result.metadata.review.verdictReason}`);
1418
+ }
1419
+ console.log("");
1420
+ console.log(`Review folder: ${result.reviewFolder}`);
1421
+ console.log(`Review HTML: ${resolve(result.htmlPath)}`);
1422
+ console.log("");
1423
+ console.log(`Feedback server running at: http://localhost:${feedbackServer.port}`);
1424
+ console.log(`Open the review HTML and approve/request changes to complete.`);
1425
+ console.log("");
1426
+ const feedback = await feedbackServer.waitForFeedback();
1427
+ console.log("");
1428
+ console.log("=".repeat(60));
1429
+ console.log(`User verdict: ${feedback.verdict}`);
1430
+ if (feedback.feedback) {
1431
+ console.log("");
1432
+ console.log("Feedback:");
1433
+ console.log(feedback.feedback);
1434
+ }
1435
+ console.log("=".repeat(60));
1436
+ process.exit(feedback.verdict === "approve" ? 0 : 1);
1437
+ } catch (err) {
1438
+ console.error("Error:", err instanceof Error ? err.message : err);
1439
+ feedbackServer.stop();
1440
+ process.exit(1);
1441
+ }
1442
+ }
1443
+ main();
1444
+
1445
+ //# debugId=ABC1559AB7B4086F64756E2164756E21