@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.
- package/dist/bin/demon-demo-init.js +56 -0
- package/dist/bin/demon-demo-init.js.map +10 -0
- package/dist/bin/demon-demo-review.js +187 -523
- package/dist/bin/demon-demo-review.js.map +7 -7
- package/dist/bin/demoon.js +1445 -0
- package/dist/bin/demoon.js.map +22 -0
- package/dist/bin/review-template.html +62 -0
- package/dist/github-issue.js +749 -0
- package/dist/github-issue.js.map +16 -0
- package/dist/index.js +1320 -867
- package/dist/index.js.map +16 -8
- package/dist/orchestrator.js +1421 -0
- package/dist/orchestrator.js.map +20 -0
- package/dist/review-generator.js +424 -0
- package/dist/review-generator.js.map +12 -0
- package/dist/review-template.html +62 -0
- package/package.json +11 -2
- package/src/bin/demon-demo-init.ts +59 -0
- package/src/bin/demon-demo-review.ts +19 -97
- package/src/bin/demoon.ts +140 -0
- package/src/feedback-server.ts +138 -0
- package/src/git-context.test.ts +68 -2
- package/src/git-context.ts +48 -9
- package/src/github-issue.test.ts +188 -0
- package/src/github-issue.ts +139 -0
- package/src/html-generator.e2e.test.ts +361 -80
- package/src/index.ts +9 -3
- package/src/orchestrator.test.ts +183 -0
- package/src/orchestrator.ts +341 -0
- package/src/review-generator.ts +221 -0
- package/src/review-types.ts +3 -0
- package/src/review.ts +13 -7
- package/src/html-generator.test.ts +0 -561
- package/src/html-generator.ts +0 -461
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
var __create = Object.create;
|
|
2
3
|
var __getProtoOf = Object.getPrototypeOf;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
@@ -14,451 +15,16 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
14
15
|
});
|
|
15
16
|
return to;
|
|
16
17
|
};
|
|
17
|
-
var
|
|
18
|
-
for (var name in all)
|
|
19
|
-
__defProp(target, name, {
|
|
20
|
-
get: all[name],
|
|
21
|
-
enumerable: true,
|
|
22
|
-
configurable: true,
|
|
23
|
-
set: (newValue) => all[name] = () => newValue
|
|
24
|
-
});
|
|
25
|
-
};
|
|
26
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
27
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
28
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
29
|
-
}) : x)(function(x) {
|
|
30
|
-
if (typeof require !== "undefined")
|
|
31
|
-
return require.apply(this, arguments);
|
|
32
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
33
|
-
});
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
34
19
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
resolve: () => resolve,
|
|
40
|
-
relative: () => relative,
|
|
41
|
-
posix: () => posix,
|
|
42
|
-
parse: () => parse,
|
|
43
|
-
normalize: () => normalize,
|
|
44
|
-
join: () => join,
|
|
45
|
-
isAbsolute: () => isAbsolute,
|
|
46
|
-
format: () => format,
|
|
47
|
-
extname: () => extname,
|
|
48
|
-
dirname: () => dirname,
|
|
49
|
-
delimiter: () => delimiter,
|
|
50
|
-
default: () => path_default,
|
|
51
|
-
basename: () => basename,
|
|
52
|
-
_makeLong: () => _makeLong
|
|
53
|
-
});
|
|
54
|
-
function assertPath(path) {
|
|
55
|
-
if (typeof path !== "string")
|
|
56
|
-
throw TypeError("Path must be a string. Received " + JSON.stringify(path));
|
|
57
|
-
}
|
|
58
|
-
function normalizeStringPosix(path, allowAboveRoot) {
|
|
59
|
-
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
|
|
60
|
-
for (var i = 0;i <= path.length; ++i) {
|
|
61
|
-
if (i < path.length)
|
|
62
|
-
code = path.charCodeAt(i);
|
|
63
|
-
else if (code === 47)
|
|
64
|
-
break;
|
|
65
|
-
else
|
|
66
|
-
code = 47;
|
|
67
|
-
if (code === 47) {
|
|
68
|
-
if (lastSlash === i - 1 || dots === 1)
|
|
69
|
-
;
|
|
70
|
-
else if (lastSlash !== i - 1 && dots === 2) {
|
|
71
|
-
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
|
|
72
|
-
if (res.length > 2) {
|
|
73
|
-
var lastSlashIndex = res.lastIndexOf("/");
|
|
74
|
-
if (lastSlashIndex !== res.length - 1) {
|
|
75
|
-
if (lastSlashIndex === -1)
|
|
76
|
-
res = "", lastSegmentLength = 0;
|
|
77
|
-
else
|
|
78
|
-
res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
|
|
79
|
-
lastSlash = i, dots = 0;
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
} else if (res.length === 2 || res.length === 1) {
|
|
83
|
-
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (allowAboveRoot) {
|
|
88
|
-
if (res.length > 0)
|
|
89
|
-
res += "/..";
|
|
90
|
-
else
|
|
91
|
-
res = "..";
|
|
92
|
-
lastSegmentLength = 2;
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
if (res.length > 0)
|
|
96
|
-
res += "/" + path.slice(lastSlash + 1, i);
|
|
97
|
-
else
|
|
98
|
-
res = path.slice(lastSlash + 1, i);
|
|
99
|
-
lastSegmentLength = i - lastSlash - 1;
|
|
100
|
-
}
|
|
101
|
-
lastSlash = i, dots = 0;
|
|
102
|
-
} else if (code === 46 && dots !== -1)
|
|
103
|
-
++dots;
|
|
104
|
-
else
|
|
105
|
-
dots = -1;
|
|
106
|
-
}
|
|
107
|
-
return res;
|
|
108
|
-
}
|
|
109
|
-
function _format(sep, pathObject) {
|
|
110
|
-
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
|
|
111
|
-
if (!dir)
|
|
112
|
-
return base;
|
|
113
|
-
if (dir === pathObject.root)
|
|
114
|
-
return dir + base;
|
|
115
|
-
return dir + sep + base;
|
|
116
|
-
}
|
|
117
|
-
function resolve() {
|
|
118
|
-
var resolvedPath = "", resolvedAbsolute = false, cwd;
|
|
119
|
-
for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
|
|
120
|
-
var path;
|
|
121
|
-
if (i >= 0)
|
|
122
|
-
path = arguments[i];
|
|
123
|
-
else {
|
|
124
|
-
if (cwd === undefined)
|
|
125
|
-
cwd = process.cwd();
|
|
126
|
-
path = cwd;
|
|
127
|
-
}
|
|
128
|
-
if (assertPath(path), path.length === 0)
|
|
129
|
-
continue;
|
|
130
|
-
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
|
|
131
|
-
}
|
|
132
|
-
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
|
|
133
|
-
if (resolvedPath.length > 0)
|
|
134
|
-
return "/" + resolvedPath;
|
|
135
|
-
else
|
|
136
|
-
return "/";
|
|
137
|
-
else if (resolvedPath.length > 0)
|
|
138
|
-
return resolvedPath;
|
|
139
|
-
else
|
|
140
|
-
return ".";
|
|
141
|
-
}
|
|
142
|
-
function normalize(path) {
|
|
143
|
-
if (assertPath(path), path.length === 0)
|
|
144
|
-
return ".";
|
|
145
|
-
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
|
|
146
|
-
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
|
|
147
|
-
path = ".";
|
|
148
|
-
if (path.length > 0 && trailingSeparator)
|
|
149
|
-
path += "/";
|
|
150
|
-
if (isAbsolute)
|
|
151
|
-
return "/" + path;
|
|
152
|
-
return path;
|
|
153
|
-
}
|
|
154
|
-
function isAbsolute(path) {
|
|
155
|
-
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
|
|
156
|
-
}
|
|
157
|
-
function join() {
|
|
158
|
-
if (arguments.length === 0)
|
|
159
|
-
return ".";
|
|
160
|
-
var joined;
|
|
161
|
-
for (var i = 0;i < arguments.length; ++i) {
|
|
162
|
-
var arg = arguments[i];
|
|
163
|
-
if (assertPath(arg), arg.length > 0)
|
|
164
|
-
if (joined === undefined)
|
|
165
|
-
joined = arg;
|
|
166
|
-
else
|
|
167
|
-
joined += "/" + arg;
|
|
168
|
-
}
|
|
169
|
-
if (joined === undefined)
|
|
170
|
-
return ".";
|
|
171
|
-
return normalize(joined);
|
|
172
|
-
}
|
|
173
|
-
function relative(from, to) {
|
|
174
|
-
if (assertPath(from), assertPath(to), from === to)
|
|
175
|
-
return "";
|
|
176
|
-
if (from = resolve(from), to = resolve(to), from === to)
|
|
177
|
-
return "";
|
|
178
|
-
var fromStart = 1;
|
|
179
|
-
for (;fromStart < from.length; ++fromStart)
|
|
180
|
-
if (from.charCodeAt(fromStart) !== 47)
|
|
181
|
-
break;
|
|
182
|
-
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
|
|
183
|
-
for (;toStart < to.length; ++toStart)
|
|
184
|
-
if (to.charCodeAt(toStart) !== 47)
|
|
185
|
-
break;
|
|
186
|
-
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
|
|
187
|
-
for (;i <= length; ++i) {
|
|
188
|
-
if (i === length) {
|
|
189
|
-
if (toLen > length) {
|
|
190
|
-
if (to.charCodeAt(toStart + i) === 47)
|
|
191
|
-
return to.slice(toStart + i + 1);
|
|
192
|
-
else if (i === 0)
|
|
193
|
-
return to.slice(toStart + i);
|
|
194
|
-
} else if (fromLen > length) {
|
|
195
|
-
if (from.charCodeAt(fromStart + i) === 47)
|
|
196
|
-
lastCommonSep = i;
|
|
197
|
-
else if (i === 0)
|
|
198
|
-
lastCommonSep = 0;
|
|
199
|
-
}
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
|
|
203
|
-
if (fromCode !== toCode)
|
|
204
|
-
break;
|
|
205
|
-
else if (fromCode === 47)
|
|
206
|
-
lastCommonSep = i;
|
|
207
|
-
}
|
|
208
|
-
var out = "";
|
|
209
|
-
for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
|
|
210
|
-
if (i === fromEnd || from.charCodeAt(i) === 47)
|
|
211
|
-
if (out.length === 0)
|
|
212
|
-
out += "..";
|
|
213
|
-
else
|
|
214
|
-
out += "/..";
|
|
215
|
-
if (out.length > 0)
|
|
216
|
-
return out + to.slice(toStart + lastCommonSep);
|
|
217
|
-
else {
|
|
218
|
-
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
|
|
219
|
-
++toStart;
|
|
220
|
-
return to.slice(toStart);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
function _makeLong(path) {
|
|
224
|
-
return path;
|
|
225
|
-
}
|
|
226
|
-
function dirname(path) {
|
|
227
|
-
if (assertPath(path), path.length === 0)
|
|
228
|
-
return ".";
|
|
229
|
-
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
|
|
230
|
-
for (var i = path.length - 1;i >= 1; --i)
|
|
231
|
-
if (code = path.charCodeAt(i), code === 47) {
|
|
232
|
-
if (!matchedSlash) {
|
|
233
|
-
end = i;
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
} else
|
|
237
|
-
matchedSlash = false;
|
|
238
|
-
if (end === -1)
|
|
239
|
-
return hasRoot ? "/" : ".";
|
|
240
|
-
if (hasRoot && end === 1)
|
|
241
|
-
return "//";
|
|
242
|
-
return path.slice(0, end);
|
|
243
|
-
}
|
|
244
|
-
function basename(path, ext) {
|
|
245
|
-
if (ext !== undefined && typeof ext !== "string")
|
|
246
|
-
throw TypeError('"ext" argument must be a string');
|
|
247
|
-
assertPath(path);
|
|
248
|
-
var start = 0, end = -1, matchedSlash = true, i;
|
|
249
|
-
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
|
|
250
|
-
if (ext.length === path.length && ext === path)
|
|
251
|
-
return "";
|
|
252
|
-
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
|
|
253
|
-
for (i = path.length - 1;i >= 0; --i) {
|
|
254
|
-
var code = path.charCodeAt(i);
|
|
255
|
-
if (code === 47) {
|
|
256
|
-
if (!matchedSlash) {
|
|
257
|
-
start = i + 1;
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
if (firstNonSlashEnd === -1)
|
|
262
|
-
matchedSlash = false, firstNonSlashEnd = i + 1;
|
|
263
|
-
if (extIdx >= 0)
|
|
264
|
-
if (code === ext.charCodeAt(extIdx)) {
|
|
265
|
-
if (--extIdx === -1)
|
|
266
|
-
end = i;
|
|
267
|
-
} else
|
|
268
|
-
extIdx = -1, end = firstNonSlashEnd;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (start === end)
|
|
272
|
-
end = firstNonSlashEnd;
|
|
273
|
-
else if (end === -1)
|
|
274
|
-
end = path.length;
|
|
275
|
-
return path.slice(start, end);
|
|
276
|
-
} else {
|
|
277
|
-
for (i = path.length - 1;i >= 0; --i)
|
|
278
|
-
if (path.charCodeAt(i) === 47) {
|
|
279
|
-
if (!matchedSlash) {
|
|
280
|
-
start = i + 1;
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
} else if (end === -1)
|
|
284
|
-
matchedSlash = false, end = i + 1;
|
|
285
|
-
if (end === -1)
|
|
286
|
-
return "";
|
|
287
|
-
return path.slice(start, end);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function extname(path) {
|
|
291
|
-
assertPath(path);
|
|
292
|
-
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
|
|
293
|
-
for (var i = path.length - 1;i >= 0; --i) {
|
|
294
|
-
var code = path.charCodeAt(i);
|
|
295
|
-
if (code === 47) {
|
|
296
|
-
if (!matchedSlash) {
|
|
297
|
-
startPart = i + 1;
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (end === -1)
|
|
303
|
-
matchedSlash = false, end = i + 1;
|
|
304
|
-
if (code === 46) {
|
|
305
|
-
if (startDot === -1)
|
|
306
|
-
startDot = i;
|
|
307
|
-
else if (preDotState !== 1)
|
|
308
|
-
preDotState = 1;
|
|
309
|
-
} else if (startDot !== -1)
|
|
310
|
-
preDotState = -1;
|
|
311
|
-
}
|
|
312
|
-
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
|
313
|
-
return "";
|
|
314
|
-
return path.slice(startDot, end);
|
|
315
|
-
}
|
|
316
|
-
function format(pathObject) {
|
|
317
|
-
if (pathObject === null || typeof pathObject !== "object")
|
|
318
|
-
throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
|
|
319
|
-
return _format("/", pathObject);
|
|
320
|
-
}
|
|
321
|
-
function parse(path) {
|
|
322
|
-
assertPath(path);
|
|
323
|
-
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
|
|
324
|
-
if (path.length === 0)
|
|
325
|
-
return ret;
|
|
326
|
-
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
|
|
327
|
-
if (isAbsolute2)
|
|
328
|
-
ret.root = "/", start = 1;
|
|
329
|
-
else
|
|
330
|
-
start = 0;
|
|
331
|
-
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
|
|
332
|
-
for (;i >= start; --i) {
|
|
333
|
-
if (code = path.charCodeAt(i), code === 47) {
|
|
334
|
-
if (!matchedSlash) {
|
|
335
|
-
startPart = i + 1;
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
if (end === -1)
|
|
341
|
-
matchedSlash = false, end = i + 1;
|
|
342
|
-
if (code === 46) {
|
|
343
|
-
if (startDot === -1)
|
|
344
|
-
startDot = i;
|
|
345
|
-
else if (preDotState !== 1)
|
|
346
|
-
preDotState = 1;
|
|
347
|
-
} else if (startDot !== -1)
|
|
348
|
-
preDotState = -1;
|
|
349
|
-
}
|
|
350
|
-
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
|
|
351
|
-
if (end !== -1)
|
|
352
|
-
if (startPart === 0 && isAbsolute2)
|
|
353
|
-
ret.base = ret.name = path.slice(1, end);
|
|
354
|
-
else
|
|
355
|
-
ret.base = ret.name = path.slice(startPart, end);
|
|
356
|
-
} else {
|
|
357
|
-
if (startPart === 0 && isAbsolute2)
|
|
358
|
-
ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
|
|
359
|
-
else
|
|
360
|
-
ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
|
|
361
|
-
ret.ext = path.slice(startDot, end);
|
|
362
|
-
}
|
|
363
|
-
if (startPart > 0)
|
|
364
|
-
ret.dir = path.slice(0, startPart - 1);
|
|
365
|
-
else if (isAbsolute2)
|
|
366
|
-
ret.dir = "/";
|
|
367
|
-
return ret;
|
|
368
|
-
}
|
|
369
|
-
var sep = "/", delimiter = ":", posix, path_default;
|
|
370
|
-
var init_path = __esm(() => {
|
|
371
|
-
posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
|
|
372
|
-
path_default = posix;
|
|
373
|
-
});
|
|
20
|
+
// src/review-generator.ts
|
|
21
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
22
|
+
import { join, dirname, relative } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
374
24
|
|
|
375
|
-
// src/commentary.ts
|
|
376
|
-
var TOOLTIP_ID = "demon-commentary-tooltip";
|
|
377
|
-
async function showCommentary(page, options) {
|
|
378
|
-
await page.evaluate(({ selector, text, tooltipId }) => {
|
|
379
|
-
const target = document.querySelector(selector);
|
|
380
|
-
if (!target) {
|
|
381
|
-
throw new Error(`demon commentary: element not found for selector "${selector}"`);
|
|
382
|
-
}
|
|
383
|
-
document.getElementById(tooltipId)?.remove();
|
|
384
|
-
const rect = target.getBoundingClientRect();
|
|
385
|
-
const tooltip = document.createElement("div");
|
|
386
|
-
tooltip.id = tooltipId;
|
|
387
|
-
tooltip.textContent = text;
|
|
388
|
-
const style = document.createElement("style");
|
|
389
|
-
style.setAttribute("data-demon-commentary", "");
|
|
390
|
-
style.textContent = `
|
|
391
|
-
@keyframes demon-commentary-in {
|
|
392
|
-
from {
|
|
393
|
-
opacity: 0;
|
|
394
|
-
transform: translateY(var(--demon-slide-y, 8px));
|
|
395
|
-
}
|
|
396
|
-
to {
|
|
397
|
-
opacity: 1;
|
|
398
|
-
transform: translateY(0);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
@keyframes demon-commentary-out {
|
|
402
|
-
from {
|
|
403
|
-
opacity: 1;
|
|
404
|
-
transform: translateY(0);
|
|
405
|
-
}
|
|
406
|
-
to {
|
|
407
|
-
opacity: 0;
|
|
408
|
-
transform: translateY(var(--demon-slide-y, 8px));
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
#${tooltipId} {
|
|
412
|
-
--demon-slide-y: 8px;
|
|
413
|
-
position: fixed;
|
|
414
|
-
z-index: 2147483647;
|
|
415
|
-
background: #1a1a2e;
|
|
416
|
-
color: #eee;
|
|
417
|
-
padding: 8px 14px;
|
|
418
|
-
border-radius: 6px;
|
|
419
|
-
font: 14px/1.4 system-ui, sans-serif;
|
|
420
|
-
max-width: 320px;
|
|
421
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
422
|
-
pointer-events: none;
|
|
423
|
-
animation: demon-commentary-in 0.3s ease-out forwards;
|
|
424
|
-
}
|
|
425
|
-
#${tooltipId}.demon-commentary-hiding {
|
|
426
|
-
animation: demon-commentary-out 0.25s ease-in forwards;
|
|
427
|
-
}
|
|
428
|
-
`;
|
|
429
|
-
document.querySelector("style[data-demon-commentary]")?.remove();
|
|
430
|
-
document.head.appendChild(style);
|
|
431
|
-
tooltip.style.visibility = "hidden";
|
|
432
|
-
document.body.appendChild(tooltip);
|
|
433
|
-
const tooltipRect = tooltip.getBoundingClientRect();
|
|
434
|
-
const tooltipWidth = tooltipRect.width;
|
|
435
|
-
const tooltipHeight = tooltipRect.height;
|
|
436
|
-
const viewportWidth = window.innerWidth;
|
|
437
|
-
let top = rect.bottom + 10;
|
|
438
|
-
if (top + tooltipHeight > window.innerHeight && rect.top - 10 - tooltipHeight >= 0) {
|
|
439
|
-
top = rect.top - 10 - tooltipHeight;
|
|
440
|
-
tooltip.style.setProperty("--demon-slide-y", "-8px");
|
|
441
|
-
}
|
|
442
|
-
const left = Math.max(4, Math.min(rect.left + rect.width / 2 - tooltipWidth / 2, viewportWidth - 4 - tooltipWidth));
|
|
443
|
-
tooltip.style.top = `${top}px`;
|
|
444
|
-
tooltip.style.left = `${left}px`;
|
|
445
|
-
tooltip.style.visibility = "";
|
|
446
|
-
}, { selector: options.selector, text: options.text, tooltipId: TOOLTIP_ID });
|
|
447
|
-
}
|
|
448
|
-
async function hideCommentary(page) {
|
|
449
|
-
await page.evaluate((tooltipId) => {
|
|
450
|
-
const tooltip = document.getElementById(tooltipId);
|
|
451
|
-
if (!tooltip)
|
|
452
|
-
return;
|
|
453
|
-
tooltip.classList.add("demon-commentary-hiding");
|
|
454
|
-
tooltip.addEventListener("animationend", () => {
|
|
455
|
-
tooltip.remove();
|
|
456
|
-
document.querySelector("style[data-demon-commentary]")?.remove();
|
|
457
|
-
}, { once: true });
|
|
458
|
-
}, TOOLTIP_ID);
|
|
459
|
-
await page.waitForTimeout(300);
|
|
460
|
-
}
|
|
461
25
|
// src/review.ts
|
|
26
|
+
import { spawn } from "node:child_process";
|
|
27
|
+
import { Readable } from "node:stream";
|
|
462
28
|
var GIT_DIFF_MAX_CHARS = 50000;
|
|
463
29
|
function buildReviewPrompt(options) {
|
|
464
30
|
const { filenames, stepsMap, gitDiff, guidelines } = options;
|
|
@@ -644,14 +210,14 @@ function parseLlmResponse(raw) {
|
|
|
644
210
|
}
|
|
645
211
|
function defaultSpawn(cmd) {
|
|
646
212
|
const [command, ...args] = cmd;
|
|
647
|
-
const proc =
|
|
648
|
-
|
|
649
|
-
stderr: "pipe"
|
|
213
|
+
const proc = spawn(command, args, {
|
|
214
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
650
215
|
});
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
216
|
+
const exitCode = new Promise((resolve) => {
|
|
217
|
+
proc.on("close", (code) => resolve(code ?? 1));
|
|
218
|
+
});
|
|
219
|
+
const stdout = Readable.toWeb(proc.stdout);
|
|
220
|
+
return { exitCode, stdout };
|
|
655
221
|
}
|
|
656
222
|
function concatUint8Arrays(arrays) {
|
|
657
223
|
const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
@@ -663,15 +229,36 @@ function concatUint8Arrays(arrays) {
|
|
|
663
229
|
}
|
|
664
230
|
return result;
|
|
665
231
|
}
|
|
232
|
+
|
|
666
233
|
// src/git-context.ts
|
|
667
|
-
|
|
234
|
+
import { readFileSync } from "node:fs";
|
|
235
|
+
import { spawnSync } from "node:child_process";
|
|
236
|
+
async function detectDefaultBase(exec, gitRoot) {
|
|
237
|
+
let currentBranch;
|
|
238
|
+
try {
|
|
239
|
+
currentBranch = (await exec(["git", "rev-parse", "--abbrev-ref", "HEAD"], gitRoot)).trim();
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (currentBranch === "main" || currentBranch === "master") {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
for (const candidate of ["main", "master"]) {
|
|
247
|
+
try {
|
|
248
|
+
await exec(["git", "rev-parse", "--verify", candidate], gitRoot);
|
|
249
|
+
return candidate;
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
668
254
|
var defaultExec = async (cmd, cwd) => {
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
255
|
+
const [command, ...args] = cmd;
|
|
256
|
+
const proc = spawnSync(command, args, { cwd, encoding: "utf-8" });
|
|
257
|
+
if (proc.status !== 0) {
|
|
258
|
+
const stderr = (proc.stderr ?? "").trim();
|
|
259
|
+
throw new Error(`Command failed (exit ${proc.status}): ${cmd.join(" ")}${stderr ? `: ${stderr}` : ""}`);
|
|
673
260
|
}
|
|
674
|
-
return proc.stdout
|
|
261
|
+
return proc.stdout ?? "";
|
|
675
262
|
};
|
|
676
263
|
var defaultReadFile = (path) => {
|
|
677
264
|
return readFileSync(path, "utf-8");
|
|
@@ -680,12 +267,17 @@ async function getRepoContext(demosDir, options) {
|
|
|
680
267
|
const exec = options?.exec ?? defaultExec;
|
|
681
268
|
const readFile = options?.readFile ?? defaultReadFile;
|
|
682
269
|
const gitRoot = (await exec(["git", "rev-parse", "--show-toplevel"], demosDir)).trim();
|
|
270
|
+
const diffBase = options?.diffBase ?? await detectDefaultBase(exec, gitRoot);
|
|
683
271
|
let gitDiff;
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
gitDiff = workingDiff;
|
|
272
|
+
if (diffBase) {
|
|
273
|
+
gitDiff = (await exec(["git", "diff", `${diffBase}...HEAD`], gitRoot)).trim();
|
|
687
274
|
} else {
|
|
688
|
-
|
|
275
|
+
const workingDiff = (await exec(["git", "diff", "HEAD"], gitRoot)).trim();
|
|
276
|
+
if (workingDiff.length > 0) {
|
|
277
|
+
gitDiff = workingDiff;
|
|
278
|
+
} else {
|
|
279
|
+
gitDiff = (await exec(["git", "diff", "HEAD~1..HEAD"], gitRoot)).trim();
|
|
280
|
+
}
|
|
689
281
|
}
|
|
690
282
|
const lsOutput = (await exec(["git", "ls-files"], gitRoot)).trim();
|
|
691
283
|
const files = lsOutput.split(`
|
|
@@ -703,438 +295,1209 @@ ${content}`);
|
|
|
703
295
|
}
|
|
704
296
|
return { gitDiff, guidelines };
|
|
705
297
|
}
|
|
706
|
-
|
|
298
|
+
|
|
299
|
+
// src/review-generator.ts
|
|
300
|
+
function getReviewTemplate() {
|
|
301
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
302
|
+
const distDir = dirname(currentFile);
|
|
303
|
+
const templatePath = join(distDir, "review-template.html");
|
|
304
|
+
if (!existsSync(templatePath)) {
|
|
305
|
+
throw new Error(`Review template not found at ${templatePath}. ` + `Make sure to build the review-app package first.`);
|
|
306
|
+
}
|
|
307
|
+
return readFileSync2(templatePath, "utf-8");
|
|
308
|
+
}
|
|
707
309
|
function escapeHtml(s) {
|
|
708
310
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
709
311
|
}
|
|
710
|
-
function
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const bannerClass = review.verdict === "approve" ? "approve" : "request-changes";
|
|
715
|
-
const verdictLabel = review.verdict === "approve" ? "Approved" : "Changes Requested";
|
|
716
|
-
const highlightsHtml = review.highlights.map((h) => `<li>${escapeHtml(h)}</li>`).join(`
|
|
717
|
-
`);
|
|
718
|
-
const issuesHtml = review.issues.length > 0 ? review.issues.map((issue) => {
|
|
719
|
-
const badgeLabel = issue.severity.toUpperCase();
|
|
720
|
-
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>`;
|
|
721
|
-
}).join(`
|
|
722
|
-
`) : '<p class="no-issues">No issues found.</p>';
|
|
723
|
-
return `<section class="review-section">
|
|
724
|
-
<div class="verdict-banner ${bannerClass}">
|
|
725
|
-
<strong>${verdictLabel}</strong>: ${escapeHtml(review.verdictReason)}
|
|
726
|
-
</div>
|
|
727
|
-
<div class="review-body">
|
|
728
|
-
<h2>Summary</h2>
|
|
729
|
-
<p>${escapeHtml(review.summary)}</p>
|
|
730
|
-
<h2>Highlights</h2>
|
|
731
|
-
<ul class="highlights-list">
|
|
732
|
-
${highlightsHtml}
|
|
733
|
-
</ul>
|
|
734
|
-
<h2>Issues</h2>
|
|
735
|
-
${issuesHtml}
|
|
736
|
-
</div>
|
|
737
|
-
</section>`;
|
|
312
|
+
function generateReviewHtml(appData) {
|
|
313
|
+
const template = getReviewTemplate();
|
|
314
|
+
const jsonData = JSON.stringify(appData);
|
|
315
|
+
return template.replace("<title>Demo Review</title>", `<title>${escapeHtml(appData.title)}</title>`).replace('"{{__INJECT_REVIEW_DATA__}}"', jsonData);
|
|
738
316
|
}
|
|
739
|
-
function
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
<title>${escapeHtml(title)}</title>
|
|
760
|
-
<style>
|
|
761
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
762
|
-
body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
|
|
763
|
-
header { padding: 1rem 2rem; background: #16213e; border-bottom: 1px solid #0f3460; }
|
|
764
|
-
header h1 { font-size: 1.4rem; color: #e94560; }
|
|
765
|
-
.tab-bar { display: flex; gap: 0; background: #16213e; border-bottom: 2px solid #0f3460; padding: 0 2rem; }
|
|
766
|
-
.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; }
|
|
767
|
-
.tab-btn:hover { color: #e0e0e0; }
|
|
768
|
-
.tab-btn.active { color: #e94560; border-bottom-color: #e94560; }
|
|
769
|
-
.tab-panel { display: none; }
|
|
770
|
-
.tab-panel.active { display: block; }
|
|
771
|
-
.review-section { padding: 1.5rem 2rem; }
|
|
772
|
-
.verdict-banner { padding: 1rem 1.5rem; border-radius: 6px; font-size: 1rem; margin-bottom: 1.5rem; }
|
|
773
|
-
.verdict-banner.approve { background: #1b4332; border: 1px solid #2d6a4f; color: #95d5b2; }
|
|
774
|
-
.verdict-banner.request-changes { background: #4a1520; border: 1px solid #842029; color: #f5c6cb; }
|
|
775
|
-
.review-body { max-width: 900px; }
|
|
776
|
-
.review-body h2 { font-size: 1.1rem; color: #e94560; margin: 1.2rem 0 0.5rem; }
|
|
777
|
-
.review-body p { font-size: 0.95rem; line-height: 1.6; color: #ccc; }
|
|
778
|
-
.highlights-list { list-style: disc; padding-left: 1.5rem; margin-bottom: 0.5rem; }
|
|
779
|
-
.highlights-list li { font-size: 0.95rem; line-height: 1.5; color: #95d5b2; margin-bottom: 0.3rem; }
|
|
780
|
-
.issue { padding: 0.6rem 0.8rem; margin-bottom: 0.5rem; border-radius: 4px; font-size: 0.9rem; line-height: 1.4; }
|
|
781
|
-
.issue.major { background: rgba(132, 32, 41, 0.3); border-left: 4px solid #dc3545; }
|
|
782
|
-
.issue.minor { background: rgba(255, 193, 7, 0.1); border-left: 4px solid #ffc107; }
|
|
783
|
-
.issue.nit { background: rgba(108, 117, 125, 0.2); border-left: 4px solid #6c757d; }
|
|
784
|
-
.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; }
|
|
785
|
-
.issue.major .severity-badge { background: #dc3545; color: #fff; }
|
|
786
|
-
.issue.minor .severity-badge { background: #ffc107; color: #000; }
|
|
787
|
-
.issue.nit .severity-badge { background: #6c757d; color: #fff; }
|
|
788
|
-
.no-issues { color: #95d5b2; font-style: italic; }
|
|
789
|
-
.demos-section { padding: 1rem 0; }
|
|
790
|
-
.review-layout { display: flex; height: 600px; }
|
|
791
|
-
.video-panel { flex: 4; padding: 1rem; display: flex; align-items: center; justify-content: center; background: #0f0f23; }
|
|
792
|
-
.video-wrapper { position: relative; width: 100%; max-height: 100%; display: flex; flex-direction: column; }
|
|
793
|
-
.video-wrapper video { width: 100%; max-height: calc(100% - 36px); border-radius: 4px 4px 0 0; display: block; cursor: pointer; }
|
|
794
|
-
.video-controls { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #16213e; border-radius: 0 0 4px 4px; }
|
|
795
|
-
.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; }
|
|
796
|
-
.video-controls button:hover { color: #e94560; }
|
|
797
|
-
.video-controls input[type="range"] { flex: 1; height: 4px; accent-color: #e94560; cursor: pointer; }
|
|
798
|
-
.video-controls .vc-time { font-size: 0.75rem; color: #999; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
799
|
-
.side-panel { flex: 1; min-width: 260px; max-width: 360px; padding: 1rem; overflow-y: auto; background: #16213e; border-left: 1px solid #0f3460; }
|
|
800
|
-
.side-panel h2 { font-size: 1rem; margin-bottom: 0.5rem; color: #e94560; }
|
|
801
|
-
.side-panel section { margin-bottom: 1.5rem; }
|
|
802
|
-
#demo-list { list-style: none; }
|
|
803
|
-
#demo-list li { margin-bottom: 0.25rem; }
|
|
804
|
-
#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; }
|
|
805
|
-
#demo-list button:hover { background: #0f3460; }
|
|
806
|
-
#demo-list button.active { background: #e94560; color: #fff; border-color: #e94560; }
|
|
807
|
-
#summary-text { font-size: 0.9rem; line-height: 1.5; color: #ccc; }
|
|
808
|
-
#steps-list { list-style: none; }
|
|
809
|
-
#steps-list li { margin-bottom: 0.3rem; }
|
|
810
|
-
#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; }
|
|
811
|
-
#steps-list button:hover { color: #e94560; }
|
|
812
|
-
#steps-list button.step-active { background: rgba(233, 69, 96, 0.15); color: #e94560; border-left-color: #e94560; }
|
|
813
|
-
.timestamp { font-weight: bold; margin-right: 0.4rem; color: #e94560; }
|
|
814
|
-
.issue { position: relative; }
|
|
815
|
-
.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; }
|
|
816
|
-
.feedback-add-issue:hover { background: #53a8b6; color: #1a1a2e; }
|
|
817
|
-
#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; }
|
|
818
|
-
.feedback-layout { display: flex; gap: 1.5rem; padding: 1.5rem 2rem; }
|
|
819
|
-
.feedback-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
|
|
820
|
-
.feedback-right { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
821
|
-
#feedback-list { list-style: none; padding: 0; }
|
|
822
|
-
#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; }
|
|
823
|
-
#feedback-list li span { flex: 1; }
|
|
824
|
-
.feedback-remove { background: none; border: none; color: #dc3545; cursor: pointer; font-size: 0.9rem; padding: 0 0.3rem; }
|
|
825
|
-
.feedback-remove:hover { color: #ff6b7a; }
|
|
826
|
-
#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; }
|
|
827
|
-
#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; }
|
|
828
|
-
#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; }
|
|
829
|
-
#feedback-copy:hover { background: #53a8b6; color: #1a1a2e; }
|
|
830
|
-
</style>
|
|
831
|
-
</head>
|
|
832
|
-
<body>
|
|
833
|
-
<header>
|
|
834
|
-
<h1>${escapeHtml(title)}</h1>
|
|
835
|
-
</header>
|
|
836
|
-
<nav class="tab-bar">
|
|
837
|
-
${hasReview ? `<button class="tab-btn${defaultTab === "summary" ? " active" : ""}" data-tab="summary">Summary</button>` : ""}
|
|
838
|
-
<button class="tab-btn${defaultTab === "demos" ? " active" : ""}" data-tab="demos">Demos</button>
|
|
839
|
-
${hasReview ? `<button class="tab-btn" data-tab="feedback">Feedback</button>` : ""}
|
|
840
|
-
</nav>
|
|
841
|
-
<main>
|
|
842
|
-
${hasReview ? `<div id="tab-summary" class="tab-panel${defaultTab === "summary" ? " active" : ""}">
|
|
843
|
-
${reviewHtml}
|
|
844
|
-
</div>` : ""}
|
|
845
|
-
<div id="tab-demos" class="tab-panel${defaultTab === "demos" ? " active" : ""}">
|
|
846
|
-
<section class="demos-section">
|
|
847
|
-
<div class="review-layout">
|
|
848
|
-
<div class="video-panel">
|
|
849
|
-
<div class="video-wrapper">
|
|
850
|
-
<video id="review-video" src="${escapeAttr(firstDemo.file)}"></video>
|
|
851
|
-
<div class="video-controls">
|
|
852
|
-
<button id="vc-play" aria-label="Play">▶</button>
|
|
853
|
-
<input id="vc-seek" type="range" min="0" max="100" value="0" step="0.1">
|
|
854
|
-
<span class="vc-time" id="vc-time">0:00 / 0:00</span>
|
|
855
|
-
</div>
|
|
856
|
-
</div>
|
|
857
|
-
</div>
|
|
858
|
-
<div class="side-panel">
|
|
859
|
-
<section>
|
|
860
|
-
<h2>Demos</h2>
|
|
861
|
-
<ul id="demo-list">
|
|
862
|
-
${demoButtons}
|
|
863
|
-
</ul>
|
|
864
|
-
</section>
|
|
865
|
-
<section>
|
|
866
|
-
<h2>Summary</h2>
|
|
867
|
-
<p id="summary-text"></p>
|
|
868
|
-
</section>
|
|
869
|
-
<section id="steps-section">
|
|
870
|
-
<h2>Steps</h2>
|
|
871
|
-
<ul id="steps-list"></ul>
|
|
872
|
-
</section>
|
|
873
|
-
</div>
|
|
874
|
-
</div>
|
|
875
|
-
</section>
|
|
876
|
-
</div>
|
|
877
|
-
${hasReview ? `<div id="tab-feedback" class="tab-panel">
|
|
878
|
-
<div class="feedback-layout">
|
|
879
|
-
<div class="feedback-left">
|
|
880
|
-
<h2>Feedback Items</h2>
|
|
881
|
-
<ul id="feedback-list"></ul>
|
|
882
|
-
<h2>General Feedback</h2>
|
|
883
|
-
<textarea id="feedback-general" placeholder="Add general feedback here..."></textarea>
|
|
884
|
-
</div>
|
|
885
|
-
<div class="feedback-right">
|
|
886
|
-
<h2>Preview</h2>
|
|
887
|
-
<pre id="feedback-preview"></pre>
|
|
888
|
-
<button id="feedback-copy">Copy to clipboard</button>
|
|
889
|
-
</div>
|
|
890
|
-
</div>
|
|
891
|
-
</div>` : ""}
|
|
892
|
-
</main>
|
|
893
|
-
${hasReview ? `<button id="feedback-selection-btn">Add to feedback</button>` : ""}
|
|
894
|
-
<script>
|
|
895
|
-
(function() {
|
|
896
|
-
// Tab switching
|
|
897
|
-
var tabBtns = document.querySelectorAll(".tab-btn");
|
|
898
|
-
var tabPanels = document.querySelectorAll(".tab-panel");
|
|
899
|
-
tabBtns.forEach(function(btn) {
|
|
900
|
-
btn.addEventListener("click", function() {
|
|
901
|
-
var target = btn.getAttribute("data-tab");
|
|
902
|
-
tabBtns.forEach(function(b) { b.classList.toggle("active", b === btn); });
|
|
903
|
-
tabPanels.forEach(function(p) { p.classList.toggle("active", p.id === "tab-" + target); });
|
|
904
|
-
});
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
var metadata = ${metadataJson};
|
|
908
|
-
var video = document.getElementById("review-video");
|
|
909
|
-
var summaryText = document.getElementById("summary-text");
|
|
910
|
-
var stepsList = document.getElementById("steps-list");
|
|
911
|
-
var demoButtons = document.querySelectorAll("#demo-list button");
|
|
912
|
-
|
|
913
|
-
function esc(s) {
|
|
914
|
-
var d = document.createElement("div");
|
|
915
|
-
d.appendChild(document.createTextNode(s));
|
|
916
|
-
return d.innerHTML;
|
|
317
|
+
function discoverDemoFiles(directory) {
|
|
318
|
+
const files = [];
|
|
319
|
+
const processFile = (filePath, filename) => {
|
|
320
|
+
const relativePath = relative(directory, filePath);
|
|
321
|
+
if (filename.endsWith(".webm")) {
|
|
322
|
+
files.push({ path: filePath, filename, relativePath, type: "web-ux" });
|
|
323
|
+
} else if (filename.endsWith(".jsonl")) {
|
|
324
|
+
files.push({ path: filePath, filename, relativePath, type: "log-based" });
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
for (const f of readdirSync(directory)) {
|
|
328
|
+
processFile(join(directory, f), f);
|
|
329
|
+
}
|
|
330
|
+
if (files.length === 0) {
|
|
331
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
332
|
+
if (!entry.isDirectory())
|
|
333
|
+
continue;
|
|
334
|
+
const subdir = join(directory, entry.name);
|
|
335
|
+
for (const f of readdirSync(subdir)) {
|
|
336
|
+
processFile(join(subdir, f), f);
|
|
917
337
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return files.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
341
|
+
}
|
|
342
|
+
async function generateReview(options) {
|
|
343
|
+
const { directory, agent, feedbackEndpoint, title = "Demo Review", diffBase } = options;
|
|
344
|
+
const demoFiles = discoverDemoFiles(directory);
|
|
345
|
+
const webUxDemos = demoFiles.filter((d) => d.type === "web-ux");
|
|
346
|
+
const logBasedDemos = demoFiles.filter((d) => d.type === "log-based");
|
|
347
|
+
if (demoFiles.length === 0) {
|
|
348
|
+
throw new Error(`No .webm or .jsonl files found in "${directory}" or its subdirectories.`);
|
|
349
|
+
}
|
|
350
|
+
const filenameToRelativePath = new Map(demoFiles.map((d) => [d.filename, d.relativePath]));
|
|
351
|
+
const stepsMapByFilename = {};
|
|
352
|
+
const stepsMapByRelativePath = {};
|
|
353
|
+
for (const demo of webUxDemos) {
|
|
354
|
+
const stepsPath = join(dirname(demo.path), "demo-steps.json");
|
|
355
|
+
if (!existsSync(stepsPath))
|
|
356
|
+
continue;
|
|
357
|
+
try {
|
|
358
|
+
const raw = readFileSync2(stepsPath, "utf-8");
|
|
359
|
+
const parsed = JSON.parse(raw);
|
|
360
|
+
if (Array.isArray(parsed)) {
|
|
361
|
+
stepsMapByFilename[demo.filename] = parsed;
|
|
362
|
+
stepsMapByRelativePath[demo.relativePath] = parsed;
|
|
923
363
|
}
|
|
364
|
+
} catch {}
|
|
365
|
+
}
|
|
366
|
+
const logsMap = {};
|
|
367
|
+
for (const demo of logBasedDemos) {
|
|
368
|
+
logsMap[demo.relativePath] = readFileSync2(demo.path, "utf-8");
|
|
369
|
+
}
|
|
370
|
+
const hasWebUxDemos = webUxDemos.length > 0;
|
|
371
|
+
const hasLogDemos = logBasedDemos.length > 0;
|
|
372
|
+
if (hasWebUxDemos && Object.keys(stepsMapByFilename).length === 0) {
|
|
373
|
+
throw new Error("No demo-steps.json found alongside any .webm files. " + "Use DemoRecorder in your demo tests to generate step data.");
|
|
374
|
+
}
|
|
375
|
+
if (!hasWebUxDemos && !hasLogDemos) {
|
|
376
|
+
throw new Error("No demo files found.");
|
|
377
|
+
}
|
|
378
|
+
let gitDiff;
|
|
379
|
+
let guidelines;
|
|
380
|
+
try {
|
|
381
|
+
const repoContext = await getRepoContext(directory, { diffBase });
|
|
382
|
+
gitDiff = repoContext.gitDiff;
|
|
383
|
+
guidelines = repoContext.guidelines;
|
|
384
|
+
} catch {}
|
|
385
|
+
const allFilenames = demoFiles.map((d) => d.filename);
|
|
386
|
+
const prompt = buildReviewPrompt({ filenames: allFilenames, stepsMap: stepsMapByFilename, gitDiff, guidelines });
|
|
387
|
+
const rawOutput = await invokeClaude(prompt, { agent });
|
|
388
|
+
const llmResponse = parseLlmResponse(rawOutput);
|
|
389
|
+
const typeMap = new Map(demoFiles.map((d) => [d.filename, d.type]));
|
|
390
|
+
const metadata = {
|
|
391
|
+
demos: llmResponse.demos.map((demo) => {
|
|
392
|
+
const relativePath = filenameToRelativePath.get(demo.file) ?? demo.file;
|
|
393
|
+
return {
|
|
394
|
+
file: relativePath,
|
|
395
|
+
type: typeMap.get(demo.file) ?? "web-ux",
|
|
396
|
+
summary: demo.summary,
|
|
397
|
+
steps: stepsMapByRelativePath[relativePath] ?? []
|
|
398
|
+
};
|
|
399
|
+
}),
|
|
400
|
+
review: llmResponse.review
|
|
401
|
+
};
|
|
402
|
+
const metadataPath = join(directory, "review-metadata.json");
|
|
403
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + `
|
|
404
|
+
`);
|
|
405
|
+
const appData = {
|
|
406
|
+
metadata,
|
|
407
|
+
title,
|
|
408
|
+
videos: {},
|
|
409
|
+
logs: Object.keys(logsMap).length > 0 ? logsMap : undefined,
|
|
410
|
+
feedbackEndpoint
|
|
411
|
+
};
|
|
412
|
+
const html = generateReviewHtml(appData);
|
|
413
|
+
const htmlPath = join(directory, "review.html");
|
|
414
|
+
writeFileSync(htmlPath, html);
|
|
415
|
+
return { htmlPath, metadataPath, metadata };
|
|
416
|
+
}
|
|
924
417
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
418
|
+
// ../../node_modules/universal-user-agent/index.js
|
|
419
|
+
function getUserAgent() {
|
|
420
|
+
if (typeof navigator === "object" && "userAgent" in navigator) {
|
|
421
|
+
return navigator.userAgent;
|
|
422
|
+
}
|
|
423
|
+
if (typeof process === "object" && process.version !== undefined) {
|
|
424
|
+
return `Node.js/${process.version.substr(1)} (${process.platform}; ${process.arch})`;
|
|
425
|
+
}
|
|
426
|
+
return "<environment undetectable>";
|
|
427
|
+
}
|
|
934
428
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
429
|
+
// ../../node_modules/@octokit/endpoint/dist-bundle/index.js
|
|
430
|
+
var VERSION = "0.0.0-development";
|
|
431
|
+
var userAgent = `octokit-endpoint.js/${VERSION} ${getUserAgent()}`;
|
|
432
|
+
var DEFAULTS = {
|
|
433
|
+
method: "GET",
|
|
434
|
+
baseUrl: "https://api.github.com",
|
|
435
|
+
headers: {
|
|
436
|
+
accept: "application/vnd.github.v3+json",
|
|
437
|
+
"user-agent": userAgent
|
|
438
|
+
},
|
|
439
|
+
mediaType: {
|
|
440
|
+
format: ""
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
function lowercaseKeys(object) {
|
|
444
|
+
if (!object) {
|
|
445
|
+
return {};
|
|
446
|
+
}
|
|
447
|
+
return Object.keys(object).reduce((newObj, key) => {
|
|
448
|
+
newObj[key.toLowerCase()] = object[key];
|
|
449
|
+
return newObj;
|
|
450
|
+
}, {});
|
|
451
|
+
}
|
|
452
|
+
function isPlainObject(value) {
|
|
453
|
+
if (typeof value !== "object" || value === null)
|
|
454
|
+
return false;
|
|
455
|
+
if (Object.prototype.toString.call(value) !== "[object Object]")
|
|
456
|
+
return false;
|
|
457
|
+
const proto = Object.getPrototypeOf(value);
|
|
458
|
+
if (proto === null)
|
|
459
|
+
return true;
|
|
460
|
+
const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
|
|
461
|
+
return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
|
|
462
|
+
}
|
|
463
|
+
function mergeDeep(defaults, options) {
|
|
464
|
+
const result = Object.assign({}, defaults);
|
|
465
|
+
Object.keys(options).forEach((key) => {
|
|
466
|
+
if (isPlainObject(options[key])) {
|
|
467
|
+
if (!(key in defaults))
|
|
468
|
+
Object.assign(result, { [key]: options[key] });
|
|
469
|
+
else
|
|
470
|
+
result[key] = mergeDeep(defaults[key], options[key]);
|
|
471
|
+
} else {
|
|
472
|
+
Object.assign(result, { [key]: options[key] });
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
function removeUndefinedProperties(obj) {
|
|
478
|
+
for (const key in obj) {
|
|
479
|
+
if (obj[key] === undefined) {
|
|
480
|
+
delete obj[key];
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return obj;
|
|
484
|
+
}
|
|
485
|
+
function merge(defaults, route, options) {
|
|
486
|
+
if (typeof route === "string") {
|
|
487
|
+
let [method, url] = route.split(" ");
|
|
488
|
+
options = Object.assign(url ? { method, url } : { url: method }, options);
|
|
489
|
+
} else {
|
|
490
|
+
options = Object.assign({}, route);
|
|
491
|
+
}
|
|
492
|
+
options.headers = lowercaseKeys(options.headers);
|
|
493
|
+
removeUndefinedProperties(options);
|
|
494
|
+
removeUndefinedProperties(options.headers);
|
|
495
|
+
const mergedOptions = mergeDeep(defaults || {}, options);
|
|
496
|
+
if (options.url === "/graphql") {
|
|
497
|
+
if (defaults && defaults.mediaType.previews?.length) {
|
|
498
|
+
mergedOptions.mediaType.previews = defaults.mediaType.previews.filter((preview) => !mergedOptions.mediaType.previews.includes(preview)).concat(mergedOptions.mediaType.previews);
|
|
499
|
+
}
|
|
500
|
+
mergedOptions.mediaType.previews = (mergedOptions.mediaType.previews || []).map((preview) => preview.replace(/-preview/, ""));
|
|
501
|
+
}
|
|
502
|
+
return mergedOptions;
|
|
503
|
+
}
|
|
504
|
+
function addQueryParameters(url, parameters) {
|
|
505
|
+
const separator = /\?/.test(url) ? "&" : "?";
|
|
506
|
+
const names = Object.keys(parameters);
|
|
507
|
+
if (names.length === 0) {
|
|
508
|
+
return url;
|
|
509
|
+
}
|
|
510
|
+
return url + separator + names.map((name) => {
|
|
511
|
+
if (name === "q") {
|
|
512
|
+
return "q=" + parameters.q.split("+").map(encodeURIComponent).join("+");
|
|
513
|
+
}
|
|
514
|
+
return `${name}=${encodeURIComponent(parameters[name])}`;
|
|
515
|
+
}).join("&");
|
|
516
|
+
}
|
|
517
|
+
var urlVariableRegex = /\{[^{}}]+\}/g;
|
|
518
|
+
function removeNonChars(variableName) {
|
|
519
|
+
return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
|
|
520
|
+
}
|
|
521
|
+
function extractUrlVariableNames(url) {
|
|
522
|
+
const matches = url.match(urlVariableRegex);
|
|
523
|
+
if (!matches) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
return matches.map(removeNonChars).reduce((a, b) => a.concat(b), []);
|
|
527
|
+
}
|
|
528
|
+
function omit(object, keysToOmit) {
|
|
529
|
+
const result = { __proto__: null };
|
|
530
|
+
for (const key of Object.keys(object)) {
|
|
531
|
+
if (keysToOmit.indexOf(key) === -1) {
|
|
532
|
+
result[key] = object[key];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
function encodeReserved(str) {
|
|
538
|
+
return str.split(/(%[0-9A-Fa-f]{2})/g).map(function(part) {
|
|
539
|
+
if (!/%[0-9A-Fa-f]/.test(part)) {
|
|
540
|
+
part = encodeURI(part).replace(/%5B/g, "[").replace(/%5D/g, "]");
|
|
541
|
+
}
|
|
542
|
+
return part;
|
|
543
|
+
}).join("");
|
|
544
|
+
}
|
|
545
|
+
function encodeUnreserved(str) {
|
|
546
|
+
return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
|
|
547
|
+
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
function encodeValue(operator, value, key) {
|
|
551
|
+
value = operator === "+" || operator === "#" ? encodeReserved(value) : encodeUnreserved(value);
|
|
552
|
+
if (key) {
|
|
553
|
+
return encodeUnreserved(key) + "=" + value;
|
|
554
|
+
} else {
|
|
555
|
+
return value;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function isDefined(value) {
|
|
559
|
+
return value !== undefined && value !== null;
|
|
560
|
+
}
|
|
561
|
+
function isKeyOperator(operator) {
|
|
562
|
+
return operator === ";" || operator === "&" || operator === "?";
|
|
563
|
+
}
|
|
564
|
+
function getValues(context, operator, key, modifier) {
|
|
565
|
+
var value = context[key], result = [];
|
|
566
|
+
if (isDefined(value) && value !== "") {
|
|
567
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
568
|
+
value = value.toString();
|
|
569
|
+
if (modifier && modifier !== "*") {
|
|
570
|
+
value = value.substring(0, parseInt(modifier, 10));
|
|
942
571
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
572
|
+
result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : ""));
|
|
573
|
+
} else {
|
|
574
|
+
if (modifier === "*") {
|
|
575
|
+
if (Array.isArray(value)) {
|
|
576
|
+
value.filter(isDefined).forEach(function(value2) {
|
|
577
|
+
result.push(encodeValue(operator, value2, isKeyOperator(operator) ? key : ""));
|
|
578
|
+
});
|
|
579
|
+
} else {
|
|
580
|
+
Object.keys(value).forEach(function(k) {
|
|
581
|
+
if (isDefined(value[k])) {
|
|
582
|
+
result.push(encodeValue(operator, value[k], k));
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
const tmp = [];
|
|
588
|
+
if (Array.isArray(value)) {
|
|
589
|
+
value.filter(isDefined).forEach(function(value2) {
|
|
590
|
+
tmp.push(encodeValue(operator, value2));
|
|
591
|
+
});
|
|
592
|
+
} else {
|
|
593
|
+
Object.keys(value).forEach(function(k) {
|
|
594
|
+
if (isDefined(value[k])) {
|
|
595
|
+
tmp.push(encodeUnreserved(k));
|
|
596
|
+
tmp.push(encodeValue(operator, value[k].toString()));
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (isKeyOperator(operator)) {
|
|
601
|
+
result.push(encodeUnreserved(key) + "=" + tmp.join(","));
|
|
602
|
+
} else if (tmp.length !== 0) {
|
|
603
|
+
result.push(tmp.join(","));
|
|
955
604
|
}
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
video.addEventListener("timeupdate", function() {
|
|
959
|
-
var buttons = document.querySelectorAll("#steps-list button[data-time]");
|
|
960
|
-
var ct = video.currentTime;
|
|
961
|
-
var activeIdx = -1;
|
|
962
|
-
buttons.forEach(function(btn, i) {
|
|
963
|
-
if (parseFloat(btn.getAttribute("data-time")) <= ct) activeIdx = i;
|
|
964
|
-
btn.classList.remove("step-active");
|
|
965
|
-
});
|
|
966
|
-
if (activeIdx >= 0) buttons[activeIdx].classList.add("step-active");
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
selectDemo(0);
|
|
970
|
-
|
|
971
|
-
// Custom video controls
|
|
972
|
-
var playBtn = document.getElementById("vc-play");
|
|
973
|
-
var seekBar = document.getElementById("vc-seek");
|
|
974
|
-
var timeDisplay = document.getElementById("vc-time");
|
|
975
|
-
var seeking = false;
|
|
976
|
-
|
|
977
|
-
function fmtTime(sec) {
|
|
978
|
-
var m = Math.floor(sec / 60);
|
|
979
|
-
var s = Math.floor(sec % 60);
|
|
980
|
-
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
function updateTime() {
|
|
984
|
-
var cur = video.currentTime || 0;
|
|
985
|
-
var dur = video.duration || 0;
|
|
986
|
-
timeDisplay.textContent = fmtTime(cur) + " / " + fmtTime(dur);
|
|
987
|
-
if (!seeking && dur) seekBar.value = (cur / dur) * 100;
|
|
988
605
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
if (operator === ";") {
|
|
609
|
+
if (isDefined(value)) {
|
|
610
|
+
result.push(encodeUnreserved(key));
|
|
992
611
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
612
|
+
} else if (value === "" && (operator === "&" || operator === "?")) {
|
|
613
|
+
result.push(encodeUnreserved(key) + "=");
|
|
614
|
+
} else if (value === "") {
|
|
615
|
+
result.push("");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
function parseUrl(template) {
|
|
621
|
+
return {
|
|
622
|
+
expand: expand.bind(null, template)
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function expand(template, context) {
|
|
626
|
+
var operators = ["+", "#", ".", "/", ";", "?", "&"];
|
|
627
|
+
template = template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function(_, expression, literal) {
|
|
628
|
+
if (expression) {
|
|
629
|
+
let operator = "";
|
|
630
|
+
const values = [];
|
|
631
|
+
if (operators.indexOf(expression.charAt(0)) !== -1) {
|
|
632
|
+
operator = expression.charAt(0);
|
|
633
|
+
expression = expression.substr(1);
|
|
634
|
+
}
|
|
635
|
+
expression.split(/,/g).forEach(function(variable) {
|
|
636
|
+
var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
|
|
637
|
+
values.push(getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
|
|
1011
638
|
});
|
|
1012
|
-
|
|
639
|
+
if (operator && operator !== "+") {
|
|
640
|
+
var separator = ",";
|
|
641
|
+
if (operator === "?") {
|
|
642
|
+
separator = "&";
|
|
643
|
+
} else if (operator !== "#") {
|
|
644
|
+
separator = operator;
|
|
645
|
+
}
|
|
646
|
+
return (values.length !== 0 ? operator : "") + values.join(separator);
|
|
647
|
+
} else {
|
|
648
|
+
return values.join(",");
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
return encodeReserved(literal);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
if (template === "/") {
|
|
655
|
+
return template;
|
|
656
|
+
} else {
|
|
657
|
+
return template.replace(/\/$/, "");
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function parse(options) {
|
|
661
|
+
let method = options.method.toUpperCase();
|
|
662
|
+
let url = (options.url || "/").replace(/:([a-z]\w+)/g, "{$1}");
|
|
663
|
+
let headers = Object.assign({}, options.headers);
|
|
664
|
+
let body;
|
|
665
|
+
let parameters = omit(options, [
|
|
666
|
+
"method",
|
|
667
|
+
"baseUrl",
|
|
668
|
+
"url",
|
|
669
|
+
"headers",
|
|
670
|
+
"request",
|
|
671
|
+
"mediaType"
|
|
672
|
+
]);
|
|
673
|
+
const urlVariableNames = extractUrlVariableNames(url);
|
|
674
|
+
url = parseUrl(url).expand(parameters);
|
|
675
|
+
if (!/^http/.test(url)) {
|
|
676
|
+
url = options.baseUrl + url;
|
|
677
|
+
}
|
|
678
|
+
const omittedParameters = Object.keys(options).filter((option) => urlVariableNames.includes(option)).concat("baseUrl");
|
|
679
|
+
const remainingParameters = omit(parameters, omittedParameters);
|
|
680
|
+
const isBinaryRequest = /application\/octet-stream/i.test(headers.accept);
|
|
681
|
+
if (!isBinaryRequest) {
|
|
682
|
+
if (options.mediaType.format) {
|
|
683
|
+
headers.accept = headers.accept.split(/,/).map((format) => format.replace(/application\/vnd(\.\w+)(\.v3)?(\.\w+)?(\+json)?$/, `application/vnd$1$2.${options.mediaType.format}`)).join(",");
|
|
684
|
+
}
|
|
685
|
+
if (url.endsWith("/graphql")) {
|
|
686
|
+
if (options.mediaType.previews?.length) {
|
|
687
|
+
const previewsFromAcceptHeader = headers.accept.match(/(?<![\w-])[\w-]+(?=-preview)/g) || [];
|
|
688
|
+
headers.accept = previewsFromAcceptHeader.concat(options.mediaType.previews).map((preview) => {
|
|
689
|
+
const format = options.mediaType.format ? `.${options.mediaType.format}` : "+json";
|
|
690
|
+
return `application/vnd.github.${preview}-preview${format}`;
|
|
691
|
+
}).join(",");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (["GET", "HEAD"].includes(method)) {
|
|
696
|
+
url = addQueryParameters(url, remainingParameters);
|
|
697
|
+
} else {
|
|
698
|
+
if ("data" in remainingParameters) {
|
|
699
|
+
body = remainingParameters.data;
|
|
700
|
+
} else {
|
|
701
|
+
if (Object.keys(remainingParameters).length) {
|
|
702
|
+
body = remainingParameters;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (!headers["content-type"] && typeof body !== "undefined") {
|
|
707
|
+
headers["content-type"] = "application/json; charset=utf-8";
|
|
708
|
+
}
|
|
709
|
+
if (["PATCH", "PUT"].includes(method) && typeof body === "undefined") {
|
|
710
|
+
body = "";
|
|
711
|
+
}
|
|
712
|
+
return Object.assign({ method, url, headers }, typeof body !== "undefined" ? { body } : null, options.request ? { request: options.request } : null);
|
|
713
|
+
}
|
|
714
|
+
function endpointWithDefaults(defaults, route, options) {
|
|
715
|
+
return parse(merge(defaults, route, options));
|
|
716
|
+
}
|
|
717
|
+
function withDefaults(oldDefaults, newDefaults) {
|
|
718
|
+
const DEFAULTS2 = merge(oldDefaults, newDefaults);
|
|
719
|
+
const endpoint2 = endpointWithDefaults.bind(null, DEFAULTS2);
|
|
720
|
+
return Object.assign(endpoint2, {
|
|
721
|
+
DEFAULTS: DEFAULTS2,
|
|
722
|
+
defaults: withDefaults.bind(null, DEFAULTS2),
|
|
723
|
+
merge: merge.bind(null, DEFAULTS2),
|
|
724
|
+
parse
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
var endpoint = withDefaults(null, DEFAULTS);
|
|
1013
728
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
729
|
+
// ../../node_modules/fast-content-type-parse/index.js
|
|
730
|
+
var NullObject = function NullObject2() {};
|
|
731
|
+
NullObject.prototype = Object.create(null);
|
|
732
|
+
var paramRE = /; *([!#$%&'*+.^\w`|~-]+)=("(?:[\v\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\v\u0020-\u00ff])*"|[!#$%&'*+.^\w`|~-]+) */gu;
|
|
733
|
+
var quotedPairRE = /\\([\v\u0020-\u00ff])/gu;
|
|
734
|
+
var mediaTypeRE = /^[!#$%&'*+.^\w|~-]+\/[!#$%&'*+.^\w|~-]+$/u;
|
|
735
|
+
var defaultContentType = { type: "", parameters: new NullObject };
|
|
736
|
+
Object.freeze(defaultContentType.parameters);
|
|
737
|
+
Object.freeze(defaultContentType);
|
|
738
|
+
function safeParse(header) {
|
|
739
|
+
if (typeof header !== "string") {
|
|
740
|
+
return defaultContentType;
|
|
741
|
+
}
|
|
742
|
+
let index = header.indexOf(";");
|
|
743
|
+
const type = index !== -1 ? header.slice(0, index).trim() : header.trim();
|
|
744
|
+
if (mediaTypeRE.test(type) === false) {
|
|
745
|
+
return defaultContentType;
|
|
746
|
+
}
|
|
747
|
+
const result = {
|
|
748
|
+
type: type.toLowerCase(),
|
|
749
|
+
parameters: new NullObject
|
|
750
|
+
};
|
|
751
|
+
if (index === -1) {
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
let key;
|
|
755
|
+
let match;
|
|
756
|
+
let value;
|
|
757
|
+
paramRE.lastIndex = index;
|
|
758
|
+
while (match = paramRE.exec(header)) {
|
|
759
|
+
if (match.index !== index) {
|
|
760
|
+
return defaultContentType;
|
|
761
|
+
}
|
|
762
|
+
index += match[0].length;
|
|
763
|
+
key = match[1].toLowerCase();
|
|
764
|
+
value = match[2];
|
|
765
|
+
if (value[0] === '"') {
|
|
766
|
+
value = value.slice(1, value.length - 1);
|
|
767
|
+
quotedPairRE.test(value) && (value = value.replace(quotedPairRE, "$1"));
|
|
768
|
+
}
|
|
769
|
+
result.parameters[key] = value;
|
|
770
|
+
}
|
|
771
|
+
if (index !== header.length) {
|
|
772
|
+
return defaultContentType;
|
|
773
|
+
}
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
776
|
+
var $safeParse = safeParse;
|
|
1022
777
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
778
|
+
// ../../node_modules/@octokit/request-error/dist-src/index.js
|
|
779
|
+
class RequestError extends Error {
|
|
780
|
+
name;
|
|
781
|
+
status;
|
|
782
|
+
request;
|
|
783
|
+
response;
|
|
784
|
+
constructor(message, statusCode, options) {
|
|
785
|
+
super(message);
|
|
786
|
+
this.name = "HttpError";
|
|
787
|
+
this.status = Number.parseInt(statusCode);
|
|
788
|
+
if (Number.isNaN(this.status)) {
|
|
789
|
+
this.status = 0;
|
|
790
|
+
}
|
|
791
|
+
if ("response" in options) {
|
|
792
|
+
this.response = options.response;
|
|
793
|
+
}
|
|
794
|
+
const requestCopy = Object.assign({}, options.request);
|
|
795
|
+
if (options.request.headers.authorization) {
|
|
796
|
+
requestCopy.headers = Object.assign({}, options.request.headers, {
|
|
797
|
+
authorization: options.request.headers.authorization.replace(/(?<! ) .*$/, " [REDACTED]")
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
|
|
801
|
+
this.request = requestCopy;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
1032
804
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
805
|
+
// ../../node_modules/@octokit/request/dist-bundle/index.js
|
|
806
|
+
var VERSION2 = "9.2.4";
|
|
807
|
+
var defaults_default = {
|
|
808
|
+
headers: {
|
|
809
|
+
"user-agent": `octokit-request.js/${VERSION2} ${getUserAgent()}`
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
function isPlainObject2(value) {
|
|
813
|
+
if (typeof value !== "object" || value === null)
|
|
814
|
+
return false;
|
|
815
|
+
if (Object.prototype.toString.call(value) !== "[object Object]")
|
|
816
|
+
return false;
|
|
817
|
+
const proto = Object.getPrototypeOf(value);
|
|
818
|
+
if (proto === null)
|
|
819
|
+
return true;
|
|
820
|
+
const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
|
|
821
|
+
return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
|
|
822
|
+
}
|
|
823
|
+
async function fetchWrapper(requestOptions) {
|
|
824
|
+
const fetch = requestOptions.request?.fetch || globalThis.fetch;
|
|
825
|
+
if (!fetch) {
|
|
826
|
+
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");
|
|
827
|
+
}
|
|
828
|
+
const log = requestOptions.request?.log || console;
|
|
829
|
+
const parseSuccessResponseBody = requestOptions.request?.parseSuccessResponseBody !== false;
|
|
830
|
+
const body = isPlainObject2(requestOptions.body) || Array.isArray(requestOptions.body) ? JSON.stringify(requestOptions.body) : requestOptions.body;
|
|
831
|
+
const requestHeaders = Object.fromEntries(Object.entries(requestOptions.headers).map(([name, value]) => [
|
|
832
|
+
name,
|
|
833
|
+
String(value)
|
|
834
|
+
]));
|
|
835
|
+
let fetchResponse;
|
|
836
|
+
try {
|
|
837
|
+
fetchResponse = await fetch(requestOptions.url, {
|
|
838
|
+
method: requestOptions.method,
|
|
839
|
+
body,
|
|
840
|
+
redirect: requestOptions.request?.redirect,
|
|
841
|
+
headers: requestHeaders,
|
|
842
|
+
signal: requestOptions.request?.signal,
|
|
843
|
+
...requestOptions.body && { duplex: "half" }
|
|
844
|
+
});
|
|
845
|
+
} catch (error) {
|
|
846
|
+
let message = "Unknown Error";
|
|
847
|
+
if (error instanceof Error) {
|
|
848
|
+
if (error.name === "AbortError") {
|
|
849
|
+
error.status = 500;
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
message = error.message;
|
|
853
|
+
if (error.name === "TypeError" && "cause" in error) {
|
|
854
|
+
if (error.cause instanceof Error) {
|
|
855
|
+
message = error.cause.message;
|
|
856
|
+
} else if (typeof error.cause === "string") {
|
|
857
|
+
message = error.cause;
|
|
1036
858
|
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const requestError = new RequestError(message, 500, {
|
|
862
|
+
request: requestOptions
|
|
863
|
+
});
|
|
864
|
+
requestError.cause = error;
|
|
865
|
+
throw requestError;
|
|
866
|
+
}
|
|
867
|
+
const status = fetchResponse.status;
|
|
868
|
+
const url = fetchResponse.url;
|
|
869
|
+
const responseHeaders = {};
|
|
870
|
+
for (const [key, value] of fetchResponse.headers) {
|
|
871
|
+
responseHeaders[key] = value;
|
|
872
|
+
}
|
|
873
|
+
const octokitResponse = {
|
|
874
|
+
url,
|
|
875
|
+
status,
|
|
876
|
+
headers: responseHeaders,
|
|
877
|
+
data: ""
|
|
878
|
+
};
|
|
879
|
+
if ("deprecation" in responseHeaders) {
|
|
880
|
+
const matches = responseHeaders.link && responseHeaders.link.match(/<([^<>]+)>; rel="deprecation"/);
|
|
881
|
+
const deprecationLink = matches && matches.pop();
|
|
882
|
+
log.warn(`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${responseHeaders.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`);
|
|
883
|
+
}
|
|
884
|
+
if (status === 204 || status === 205) {
|
|
885
|
+
return octokitResponse;
|
|
886
|
+
}
|
|
887
|
+
if (requestOptions.method === "HEAD") {
|
|
888
|
+
if (status < 400) {
|
|
889
|
+
return octokitResponse;
|
|
890
|
+
}
|
|
891
|
+
throw new RequestError(fetchResponse.statusText, status, {
|
|
892
|
+
response: octokitResponse,
|
|
893
|
+
request: requestOptions
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
if (status === 304) {
|
|
897
|
+
octokitResponse.data = await getResponseData(fetchResponse);
|
|
898
|
+
throw new RequestError("Not modified", status, {
|
|
899
|
+
response: octokitResponse,
|
|
900
|
+
request: requestOptions
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
if (status >= 400) {
|
|
904
|
+
octokitResponse.data = await getResponseData(fetchResponse);
|
|
905
|
+
throw new RequestError(toErrorMessage(octokitResponse.data), status, {
|
|
906
|
+
response: octokitResponse,
|
|
907
|
+
request: requestOptions
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
octokitResponse.data = parseSuccessResponseBody ? await getResponseData(fetchResponse) : fetchResponse.body;
|
|
911
|
+
return octokitResponse;
|
|
912
|
+
}
|
|
913
|
+
async function getResponseData(response) {
|
|
914
|
+
const contentType = response.headers.get("content-type");
|
|
915
|
+
if (!contentType) {
|
|
916
|
+
return response.text().catch(() => "");
|
|
917
|
+
}
|
|
918
|
+
const mimetype = $safeParse(contentType);
|
|
919
|
+
if (isJSONResponse(mimetype)) {
|
|
920
|
+
let text = "";
|
|
921
|
+
try {
|
|
922
|
+
text = await response.text();
|
|
923
|
+
return JSON.parse(text);
|
|
924
|
+
} catch (err) {
|
|
925
|
+
return text;
|
|
926
|
+
}
|
|
927
|
+
} else if (mimetype.type.startsWith("text/") || mimetype.parameters.charset?.toLowerCase() === "utf-8") {
|
|
928
|
+
return response.text().catch(() => "");
|
|
929
|
+
} else {
|
|
930
|
+
return response.arrayBuffer().catch(() => new ArrayBuffer(0));
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function isJSONResponse(mimetype) {
|
|
934
|
+
return mimetype.type === "application/json" || mimetype.type === "application/scim+json";
|
|
935
|
+
}
|
|
936
|
+
function toErrorMessage(data) {
|
|
937
|
+
if (typeof data === "string") {
|
|
938
|
+
return data;
|
|
939
|
+
}
|
|
940
|
+
if (data instanceof ArrayBuffer) {
|
|
941
|
+
return "Unknown error";
|
|
942
|
+
}
|
|
943
|
+
if ("message" in data) {
|
|
944
|
+
const suffix = "documentation_url" in data ? ` - ${data.documentation_url}` : "";
|
|
945
|
+
return Array.isArray(data.errors) ? `${data.message}: ${data.errors.map((v) => JSON.stringify(v)).join(", ")}${suffix}` : `${data.message}${suffix}`;
|
|
946
|
+
}
|
|
947
|
+
return `Unknown error: ${JSON.stringify(data)}`;
|
|
948
|
+
}
|
|
949
|
+
function withDefaults2(oldEndpoint, newDefaults) {
|
|
950
|
+
const endpoint2 = oldEndpoint.defaults(newDefaults);
|
|
951
|
+
const newApi = function(route, parameters) {
|
|
952
|
+
const endpointOptions = endpoint2.merge(route, parameters);
|
|
953
|
+
if (!endpointOptions.request || !endpointOptions.request.hook) {
|
|
954
|
+
return fetchWrapper(endpoint2.parse(endpointOptions));
|
|
955
|
+
}
|
|
956
|
+
const request2 = (route2, parameters2) => {
|
|
957
|
+
return fetchWrapper(endpoint2.parse(endpoint2.merge(route2, parameters2)));
|
|
958
|
+
};
|
|
959
|
+
Object.assign(request2, {
|
|
960
|
+
endpoint: endpoint2,
|
|
961
|
+
defaults: withDefaults2.bind(null, endpoint2)
|
|
962
|
+
});
|
|
963
|
+
return endpointOptions.request.hook(request2, endpointOptions);
|
|
964
|
+
};
|
|
965
|
+
return Object.assign(newApi, {
|
|
966
|
+
endpoint: endpoint2,
|
|
967
|
+
defaults: withDefaults2.bind(null, endpoint2)
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
var request = withDefaults2(endpoint, defaults_default);
|
|
1037
971
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
972
|
+
// ../../node_modules/@octokit/graphql/dist-bundle/index.js
|
|
973
|
+
var VERSION3 = "0.0.0-development";
|
|
974
|
+
function _buildMessageForResponseErrors(data) {
|
|
975
|
+
return `Request failed due to following response errors:
|
|
976
|
+
` + data.errors.map((e) => ` - ${e.message}`).join(`
|
|
977
|
+
`);
|
|
978
|
+
}
|
|
979
|
+
var GraphqlResponseError = class extends Error {
|
|
980
|
+
constructor(request2, headers, response) {
|
|
981
|
+
super(_buildMessageForResponseErrors(response));
|
|
982
|
+
this.request = request2;
|
|
983
|
+
this.headers = headers;
|
|
984
|
+
this.response = response;
|
|
985
|
+
this.errors = response.errors;
|
|
986
|
+
this.data = response.data;
|
|
987
|
+
if (Error.captureStackTrace) {
|
|
988
|
+
Error.captureStackTrace(this, this.constructor);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
name = "GraphqlResponseError";
|
|
992
|
+
errors;
|
|
993
|
+
data;
|
|
994
|
+
};
|
|
995
|
+
var NON_VARIABLE_OPTIONS = [
|
|
996
|
+
"method",
|
|
997
|
+
"baseUrl",
|
|
998
|
+
"url",
|
|
999
|
+
"headers",
|
|
1000
|
+
"request",
|
|
1001
|
+
"query",
|
|
1002
|
+
"mediaType",
|
|
1003
|
+
"operationName"
|
|
1004
|
+
];
|
|
1005
|
+
var FORBIDDEN_VARIABLE_OPTIONS = ["query", "method", "url"];
|
|
1006
|
+
var GHES_V3_SUFFIX_REGEX = /\/api\/v3\/?$/;
|
|
1007
|
+
function graphql(request2, query, options) {
|
|
1008
|
+
if (options) {
|
|
1009
|
+
if (typeof query === "string" && "query" in options) {
|
|
1010
|
+
return Promise.reject(new Error(`[@octokit/graphql] "query" cannot be used as variable name`));
|
|
1011
|
+
}
|
|
1012
|
+
for (const key in options) {
|
|
1013
|
+
if (!FORBIDDEN_VARIABLE_OPTIONS.includes(key))
|
|
1014
|
+
continue;
|
|
1015
|
+
return Promise.reject(new Error(`[@octokit/graphql] "${key}" cannot be used as variable name`));
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const parsedOptions = typeof query === "string" ? Object.assign({ query }, options) : query;
|
|
1019
|
+
const requestOptions = Object.keys(parsedOptions).reduce((result, key) => {
|
|
1020
|
+
if (NON_VARIABLE_OPTIONS.includes(key)) {
|
|
1021
|
+
result[key] = parsedOptions[key];
|
|
1022
|
+
return result;
|
|
1023
|
+
}
|
|
1024
|
+
if (!result.variables) {
|
|
1025
|
+
result.variables = {};
|
|
1026
|
+
}
|
|
1027
|
+
result.variables[key] = parsedOptions[key];
|
|
1028
|
+
return result;
|
|
1029
|
+
}, {});
|
|
1030
|
+
const baseUrl = parsedOptions.baseUrl || request2.endpoint.DEFAULTS.baseUrl;
|
|
1031
|
+
if (GHES_V3_SUFFIX_REGEX.test(baseUrl)) {
|
|
1032
|
+
requestOptions.url = baseUrl.replace(GHES_V3_SUFFIX_REGEX, "/api/graphql");
|
|
1033
|
+
}
|
|
1034
|
+
return request2(requestOptions).then((response) => {
|
|
1035
|
+
if (response.data.errors) {
|
|
1036
|
+
const headers = {};
|
|
1037
|
+
for (const key of Object.keys(response.headers)) {
|
|
1038
|
+
headers[key] = response.headers[key];
|
|
1039
|
+
}
|
|
1040
|
+
throw new GraphqlResponseError(requestOptions, headers, response.data);
|
|
1041
|
+
}
|
|
1042
|
+
return response.data.data;
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
function withDefaults3(request2, newDefaults) {
|
|
1046
|
+
const newRequest = request2.defaults(newDefaults);
|
|
1047
|
+
const newApi = (query, options) => {
|
|
1048
|
+
return graphql(newRequest, query, options);
|
|
1049
|
+
};
|
|
1050
|
+
return Object.assign(newApi, {
|
|
1051
|
+
defaults: withDefaults3.bind(null, newRequest),
|
|
1052
|
+
endpoint: newRequest.endpoint
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
var graphql2 = withDefaults3(request, {
|
|
1056
|
+
headers: {
|
|
1057
|
+
"user-agent": `octokit-graphql.js/${VERSION3} ${getUserAgent()}`
|
|
1058
|
+
},
|
|
1059
|
+
method: "POST",
|
|
1060
|
+
url: "/graphql"
|
|
1061
|
+
});
|
|
1046
1062
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1063
|
+
// src/github-issue.ts
|
|
1064
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1065
|
+
var ISSUE_QUERY = `
|
|
1066
|
+
query($owner: String!, $repo: String!, $number: Int!) {
|
|
1067
|
+
repository(owner: $owner, name: $repo) {
|
|
1068
|
+
issue(number: $number) {
|
|
1069
|
+
number
|
|
1070
|
+
title
|
|
1071
|
+
body
|
|
1072
|
+
state
|
|
1073
|
+
labels(first: 20) {
|
|
1074
|
+
nodes {
|
|
1075
|
+
name
|
|
1055
1076
|
}
|
|
1056
|
-
feedbackPreview.textContent = lines;
|
|
1057
1077
|
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
`;
|
|
1082
|
+
function getRepoInfoFromGit() {
|
|
1083
|
+
try {
|
|
1084
|
+
const proc = spawnSync2("git", ["remote", "get-url", "origin"], { encoding: "utf-8" });
|
|
1085
|
+
if (proc.status !== 0 || !proc.stdout) {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
const url = proc.stdout.trim();
|
|
1089
|
+
const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
|
|
1090
|
+
if (sshMatch) {
|
|
1091
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
1092
|
+
}
|
|
1093
|
+
const httpsMatch = url.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
1094
|
+
if (httpsMatch) {
|
|
1095
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
1096
|
+
}
|
|
1097
|
+
return null;
|
|
1098
|
+
} catch {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function getTokenFromEnvironment() {
|
|
1103
|
+
return process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"] ?? null;
|
|
1104
|
+
}
|
|
1105
|
+
async function fetchGitHubIssue(issueId, options) {
|
|
1106
|
+
const issueNumber = typeof issueId === "string" ? parseInt(issueId, 10) : issueId;
|
|
1107
|
+
if (isNaN(issueNumber)) {
|
|
1108
|
+
throw new Error(`Invalid issue ID: ${issueId}`);
|
|
1109
|
+
}
|
|
1110
|
+
let owner = options?.owner;
|
|
1111
|
+
let repo = options?.repo;
|
|
1112
|
+
if (!owner || !repo) {
|
|
1113
|
+
const detected = getRepoInfoFromGit();
|
|
1114
|
+
if (!detected) {
|
|
1115
|
+
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.");
|
|
1116
|
+
}
|
|
1117
|
+
owner = owner ?? detected.owner;
|
|
1118
|
+
repo = repo ?? detected.repo;
|
|
1119
|
+
}
|
|
1120
|
+
const token = options?.token ?? getTokenFromEnvironment();
|
|
1121
|
+
if (!token) {
|
|
1122
|
+
throw new Error("No GitHub token found. Please set GITHUB_TOKEN or GH_TOKEN environment variable, " + "or provide token in options.");
|
|
1123
|
+
}
|
|
1124
|
+
const graphqlFn = options?.graphql ?? graphql2.defaults({
|
|
1125
|
+
headers: {
|
|
1126
|
+
authorization: `token ${token}`
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
const response = await graphqlFn(ISSUE_QUERY, {
|
|
1130
|
+
owner,
|
|
1131
|
+
repo,
|
|
1132
|
+
number: issueNumber
|
|
1133
|
+
});
|
|
1134
|
+
const issue = response.repository.issue;
|
|
1135
|
+
return {
|
|
1136
|
+
number: issue.number,
|
|
1137
|
+
title: issue.title,
|
|
1138
|
+
body: issue.body ?? "",
|
|
1139
|
+
labels: issue.labels.nodes.map((l) => l.name),
|
|
1140
|
+
state: issue.state.toLowerCase()
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1058
1143
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1144
|
+
// src/orchestrator.ts
|
|
1145
|
+
import { mkdirSync, existsSync as existsSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1146
|
+
import { join as join2, dirname as pathDirname } from "node:path";
|
|
1147
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1148
|
+
var GIT_DIFF_MAX_CHARS2 = 50000;
|
|
1149
|
+
var defaultExec2 = async (cmd, cwd) => {
|
|
1150
|
+
const [command, ...args] = cmd;
|
|
1151
|
+
const proc = spawnSync3(command, args, { cwd, encoding: "utf-8" });
|
|
1152
|
+
if (proc.status !== 0) {
|
|
1153
|
+
const stderr = (proc.stderr ?? "").trim();
|
|
1154
|
+
throw new Error(`Command failed (exit ${proc.status}): ${cmd.join(" ")}${stderr ? `: ${stderr}` : ""}`);
|
|
1155
|
+
}
|
|
1156
|
+
return proc.stdout ?? "";
|
|
1157
|
+
};
|
|
1158
|
+
async function getGitRoot(exec, cwd) {
|
|
1159
|
+
return (await exec(["git", "rev-parse", "--show-toplevel"], cwd)).trim();
|
|
1160
|
+
}
|
|
1161
|
+
async function getCurrentBranch(exec, cwd) {
|
|
1162
|
+
const branch = (await exec(["git", "branch", "--show-current"], cwd)).trim();
|
|
1163
|
+
return branch.replace(/\//g, "-") || "unknown";
|
|
1164
|
+
}
|
|
1165
|
+
function buildPresenterPrompt(options) {
|
|
1166
|
+
const { issue, gitDiff, guidelines, reviewFolder, assetsFolder, testsFolder } = options;
|
|
1167
|
+
let diff = gitDiff;
|
|
1168
|
+
if (diff.length > GIT_DIFF_MAX_CHARS2) {
|
|
1169
|
+
diff = diff.slice(0, GIT_DIFF_MAX_CHARS2) + `
|
|
1069
1170
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
selectionTimeout = setTimeout(function() {
|
|
1075
|
-
var sel = window.getSelection();
|
|
1076
|
-
var text = sel ? sel.toString().trim() : "";
|
|
1077
|
-
if (!text) return;
|
|
1078
|
-
var anchor = sel.anchorNode;
|
|
1079
|
-
var inSummary = false;
|
|
1080
|
-
var node = anchor;
|
|
1081
|
-
while (node) {
|
|
1082
|
-
if (node.id === "tab-summary") { inSummary = true; break; }
|
|
1083
|
-
node = node.parentNode;
|
|
1084
|
-
}
|
|
1085
|
-
if (!inSummary) return;
|
|
1086
|
-
selectionBtn.style.display = "block";
|
|
1087
|
-
selectionBtn.style.left = e.pageX + "px";
|
|
1088
|
-
selectionBtn.style.top = (e.pageY - 35) + "px";
|
|
1089
|
-
selectionBtn._selectedText = text;
|
|
1090
|
-
}, 100);
|
|
1091
|
-
});
|
|
1171
|
+
... (diff truncated at 50k characters)`;
|
|
1172
|
+
}
|
|
1173
|
+
const sections = [];
|
|
1174
|
+
sections.push(`## GitHub Issue #${issue.number}: ${issue.title}
|
|
1092
1175
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
}
|
|
1097
|
-
selectionBtn.style.display = "none";
|
|
1098
|
-
window.getSelection().removeAllRanges();
|
|
1099
|
-
});
|
|
1176
|
+
${issue.body}`);
|
|
1177
|
+
if (guidelines.length > 0) {
|
|
1178
|
+
sections.push(`## Coding Guidelines
|
|
1100
1179
|
|
|
1101
|
-
|
|
1102
|
-
if (e.target !== selectionBtn) {
|
|
1103
|
-
selectionBtn.style.display = "none";
|
|
1104
|
-
}
|
|
1105
|
-
});
|
|
1180
|
+
${guidelines.join(`
|
|
1106
1181
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
if (btn) {
|
|
1111
|
-
removeFeedbackItem(parseInt(btn.getAttribute("data-index"), 10));
|
|
1112
|
-
}
|
|
1113
|
-
});
|
|
1182
|
+
`)}`);
|
|
1183
|
+
}
|
|
1184
|
+
sections.push(`## Git Diff
|
|
1114
1185
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1186
|
+
\`\`\`diff
|
|
1187
|
+
${diff}
|
|
1188
|
+
\`\`\``);
|
|
1189
|
+
return `You are a demo presenter. You must create demo recordings that showcase the feature described in the GitHub issue.
|
|
1117
1190
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1191
|
+
${sections.join(`
|
|
1192
|
+
|
|
1193
|
+
`)}
|
|
1194
|
+
|
|
1195
|
+
## Configuration
|
|
1196
|
+
|
|
1197
|
+
- **Review Folder:** ${reviewFolder}
|
|
1198
|
+
- **Assets Directory:** ${assetsFolder}
|
|
1199
|
+
- **Tests Directory:** ${testsFolder}
|
|
1200
|
+
|
|
1201
|
+
## Task
|
|
1202
|
+
|
|
1203
|
+
Based on the GitHub issue and git diff above, create demo recordings that demonstrate each acceptance criterion is met.
|
|
1204
|
+
|
|
1205
|
+
For web-ux demos:
|
|
1206
|
+
- Create Playwright test files in the Tests Directory
|
|
1207
|
+
- Use DemoRecorder to capture steps
|
|
1208
|
+
- Save recordings to the Assets Directory
|
|
1209
|
+
|
|
1210
|
+
For log-based demos:
|
|
1211
|
+
- Create .jsonl files directly in the Assets Directory
|
|
1212
|
+
- Use demon__highlight annotations for key lines
|
|
1213
|
+
|
|
1214
|
+
After creating demos, report:
|
|
1215
|
+
1. List of demo test files created (paths to .demo.ts files)
|
|
1216
|
+
2. List of artifact files generated (.webm for web-ux, .jsonl for log-based)
|
|
1217
|
+
3. Any errors encountered`;
|
|
1218
|
+
}
|
|
1219
|
+
function buildReviewerPrompt(options) {
|
|
1220
|
+
const { issue, gitDiff, guidelines, demoFiles, stepsMap, logsMap } = options;
|
|
1221
|
+
let diff = gitDiff;
|
|
1222
|
+
if (diff.length > GIT_DIFF_MAX_CHARS2) {
|
|
1223
|
+
diff = diff.slice(0, GIT_DIFF_MAX_CHARS2) + `
|
|
1224
|
+
|
|
1225
|
+
... (diff truncated at 50k characters)`;
|
|
1226
|
+
}
|
|
1227
|
+
const sections = [];
|
|
1228
|
+
sections.push(`## GitHub Issue #${issue.number}: ${issue.title}
|
|
1229
|
+
|
|
1230
|
+
${issue.body}`);
|
|
1231
|
+
if (guidelines.length > 0) {
|
|
1232
|
+
sections.push(`## Coding Guidelines
|
|
1131
1233
|
|
|
1132
|
-
|
|
1234
|
+
${guidelines.join(`
|
|
1235
|
+
|
|
1236
|
+
`)}`);
|
|
1237
|
+
}
|
|
1238
|
+
sections.push(`## Git Diff
|
|
1239
|
+
|
|
1240
|
+
\`\`\`diff
|
|
1241
|
+
${diff}
|
|
1242
|
+
\`\`\``);
|
|
1243
|
+
const demoEntries = demoFiles.map((f) => {
|
|
1244
|
+
if (f.type === "web-ux") {
|
|
1245
|
+
const steps = stepsMap[f.filename] ?? stepsMap[f.relativePath] ?? [];
|
|
1246
|
+
const stepLines = steps.map((s) => `- [${s.timestampSeconds}s] ${s.text}`).join(`
|
|
1247
|
+
`);
|
|
1248
|
+
return `Video: ${f.relativePath}
|
|
1249
|
+
Recorded steps:
|
|
1250
|
+
${stepLines || "(no steps recorded)"}`;
|
|
1251
|
+
} else {
|
|
1252
|
+
const logContent = logsMap[f.relativePath] ?? "";
|
|
1253
|
+
const preview = logContent.split(`
|
|
1254
|
+
`).slice(0, 20).join(`
|
|
1255
|
+
`);
|
|
1256
|
+
return `Log: ${f.relativePath}
|
|
1257
|
+
Content preview:
|
|
1258
|
+
${preview}`;
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
sections.push(`## Demo Recordings
|
|
1262
|
+
|
|
1263
|
+
${demoEntries.join(`
|
|
1264
|
+
|
|
1265
|
+
`)}`);
|
|
1266
|
+
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.
|
|
1267
|
+
|
|
1268
|
+
${sections.join(`
|
|
1269
|
+
|
|
1270
|
+
`)}
|
|
1271
|
+
|
|
1272
|
+
## Task
|
|
1273
|
+
|
|
1274
|
+
Review the code changes and demo recordings against the GitHub issue's acceptance criteria. Generate a JSON object matching this exact schema:
|
|
1275
|
+
|
|
1276
|
+
{
|
|
1277
|
+
"demos": [
|
|
1278
|
+
{
|
|
1279
|
+
"file": "<filename>",
|
|
1280
|
+
"summary": "<a meaningful sentence describing what this demo showcases based on the steps>"
|
|
1281
|
+
}
|
|
1282
|
+
],
|
|
1283
|
+
"review": {
|
|
1284
|
+
"summary": "<2-3 sentence overview of the changes>",
|
|
1285
|
+
"highlights": ["<positive aspect 1>", "<positive aspect 2>"],
|
|
1286
|
+
"verdict": "approve" | "request_changes",
|
|
1287
|
+
"verdictReason": "<one sentence justifying the verdict>",
|
|
1288
|
+
"issues": [
|
|
1289
|
+
{
|
|
1290
|
+
"severity": "major" | "minor" | "nit",
|
|
1291
|
+
"description": "<what the issue is and how to fix it>"
|
|
1133
1292
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1293
|
+
]
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
Rules:
|
|
1298
|
+
- Return ONLY the JSON object, no markdown fences or extra text.
|
|
1299
|
+
- Include one entry in "demos" for each demo file, in the same order.
|
|
1300
|
+
- "file" must exactly match the provided relative path.
|
|
1301
|
+
- "verdict" must be exactly "approve" or "request_changes".
|
|
1302
|
+
- Use "request_changes" if acceptance criteria from the issue are not demonstrated.
|
|
1303
|
+
- "severity" must be exactly "major", "minor", or "nit".
|
|
1304
|
+
- "major": bugs, security issues, broken functionality, missing acceptance criteria.
|
|
1305
|
+
- "minor": code quality, readability, missing edge cases.
|
|
1306
|
+
- "nit": style, naming, trivial improvements.
|
|
1307
|
+
- "highlights" must have at least one entry.
|
|
1308
|
+
- "issues" can be an empty array if there are no issues.
|
|
1309
|
+
- Verify that demo steps demonstrate ALL acceptance criteria from the issue.`;
|
|
1310
|
+
}
|
|
1311
|
+
function collectDemoData(demoFiles) {
|
|
1312
|
+
const stepsMapByFilename = {};
|
|
1313
|
+
const stepsMapByRelativePath = {};
|
|
1314
|
+
const logsMap = {};
|
|
1315
|
+
for (const demo of demoFiles) {
|
|
1316
|
+
if (demo.type === "web-ux") {
|
|
1317
|
+
const stepsPath = join2(pathDirname(demo.path), "demo-steps.json");
|
|
1318
|
+
if (existsSync2(stepsPath)) {
|
|
1319
|
+
try {
|
|
1320
|
+
const raw = readFileSync3(stepsPath, "utf-8");
|
|
1321
|
+
const parsed = JSON.parse(raw);
|
|
1322
|
+
if (Array.isArray(parsed)) {
|
|
1323
|
+
stepsMapByFilename[demo.filename] = parsed;
|
|
1324
|
+
stepsMapByRelativePath[demo.relativePath] = parsed;
|
|
1325
|
+
}
|
|
1326
|
+
} catch {}
|
|
1327
|
+
}
|
|
1328
|
+
} else {
|
|
1329
|
+
logsMap[demo.relativePath] = readFileSync3(demo.path, "utf-8");
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return { stepsMapByFilename, stepsMapByRelativePath, logsMap };
|
|
1333
|
+
}
|
|
1334
|
+
async function runReviewOrchestration(options) {
|
|
1335
|
+
const exec = options.exec ?? defaultExec2;
|
|
1336
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1337
|
+
const issue = options.issue ?? await fetchGitHubIssue(options.issueId, options.github);
|
|
1338
|
+
const gitRoot = await getGitRoot(exec, cwd);
|
|
1339
|
+
const branchName = await getCurrentBranch(exec, cwd);
|
|
1340
|
+
const repoContext = await getRepoContext(gitRoot, {
|
|
1341
|
+
exec,
|
|
1342
|
+
diffBase: options.diffBase
|
|
1343
|
+
});
|
|
1344
|
+
const reviewFolder = join2(gitRoot, ".demoon", "reviews", branchName);
|
|
1345
|
+
const assetsFolder = join2(reviewFolder, "assets");
|
|
1346
|
+
const testsFolder = join2(reviewFolder, "tests");
|
|
1347
|
+
if (!existsSync2(reviewFolder)) {
|
|
1348
|
+
mkdirSync(reviewFolder, { recursive: true });
|
|
1349
|
+
}
|
|
1350
|
+
if (!existsSync2(assetsFolder)) {
|
|
1351
|
+
mkdirSync(assetsFolder, { recursive: true });
|
|
1352
|
+
}
|
|
1353
|
+
if (!existsSync2(testsFolder)) {
|
|
1354
|
+
mkdirSync(testsFolder, { recursive: true });
|
|
1355
|
+
}
|
|
1356
|
+
const presenterPrompt = buildPresenterPrompt({
|
|
1357
|
+
issue,
|
|
1358
|
+
gitDiff: repoContext.gitDiff,
|
|
1359
|
+
guidelines: repoContext.guidelines,
|
|
1360
|
+
reviewFolder,
|
|
1361
|
+
assetsFolder,
|
|
1362
|
+
testsFolder
|
|
1363
|
+
});
|
|
1364
|
+
await invokeClaude(presenterPrompt, { agent: options.agent, spawn: options.spawn });
|
|
1365
|
+
const demoFiles = discoverDemoFiles(assetsFolder);
|
|
1366
|
+
if (demoFiles.length === 0) {
|
|
1367
|
+
throw new Error(`No demo files (.webm or .jsonl) found in ${assetsFolder} after Presenter phase`);
|
|
1368
|
+
}
|
|
1369
|
+
const { stepsMapByFilename, stepsMapByRelativePath, logsMap } = collectDemoData(demoFiles);
|
|
1370
|
+
const reviewerPrompt = buildReviewerPrompt({
|
|
1371
|
+
issue,
|
|
1372
|
+
gitDiff: repoContext.gitDiff,
|
|
1373
|
+
guidelines: repoContext.guidelines,
|
|
1374
|
+
demoFiles,
|
|
1375
|
+
stepsMap: stepsMapByFilename,
|
|
1376
|
+
logsMap
|
|
1377
|
+
});
|
|
1378
|
+
const rawOutput = await invokeClaude(reviewerPrompt, { agent: options.agent, spawn: options.spawn });
|
|
1379
|
+
const llmResponse = parseLlmResponse(rawOutput);
|
|
1380
|
+
const filenameToRelativePath = new Map(demoFiles.map((d) => [d.filename, d.relativePath]));
|
|
1381
|
+
const typeMap = new Map(demoFiles.map((d) => [d.filename, d.type]));
|
|
1382
|
+
const metadata = {
|
|
1383
|
+
demos: llmResponse.demos.map((demo) => {
|
|
1384
|
+
const relativePath = filenameToRelativePath.get(demo.file) ?? demo.file;
|
|
1385
|
+
return {
|
|
1386
|
+
file: relativePath,
|
|
1387
|
+
type: typeMap.get(demo.file) ?? "web-ux",
|
|
1388
|
+
summary: demo.summary,
|
|
1389
|
+
steps: stepsMapByRelativePath[relativePath] ?? []
|
|
1390
|
+
};
|
|
1391
|
+
}),
|
|
1392
|
+
review: llmResponse.review
|
|
1393
|
+
};
|
|
1394
|
+
const metadataPath = join2(assetsFolder, "review-metadata.json");
|
|
1395
|
+
writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2) + `
|
|
1396
|
+
`);
|
|
1397
|
+
const appData = {
|
|
1398
|
+
metadata,
|
|
1399
|
+
title: `Review: Issue #${issue.number} - ${issue.title}`,
|
|
1400
|
+
videos: {},
|
|
1401
|
+
logs: Object.keys(logsMap).length > 0 ? logsMap : undefined,
|
|
1402
|
+
feedbackEndpoint: options.feedbackEndpoint
|
|
1403
|
+
};
|
|
1404
|
+
const html = generateReviewHtml(appData);
|
|
1405
|
+
const htmlPath = join2(assetsFolder, "review.html");
|
|
1406
|
+
writeFileSync2(htmlPath, html);
|
|
1407
|
+
return {
|
|
1408
|
+
reviewFolder,
|
|
1409
|
+
htmlPath,
|
|
1410
|
+
metadataPath,
|
|
1411
|
+
metadata,
|
|
1412
|
+
issue
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/commentary.ts
|
|
1417
|
+
var TOOLTIP_ID = "demon-commentary-tooltip";
|
|
1418
|
+
async function showCommentary(page, options) {
|
|
1419
|
+
await page.evaluate(({ selector, text, tooltipId }) => {
|
|
1420
|
+
const target = document.querySelector(selector);
|
|
1421
|
+
if (!target) {
|
|
1422
|
+
throw new Error(`demon commentary: element not found for selector "${selector}"`);
|
|
1423
|
+
}
|
|
1424
|
+
document.getElementById(tooltipId)?.remove();
|
|
1425
|
+
const rect = target.getBoundingClientRect();
|
|
1426
|
+
const tooltip = document.createElement("div");
|
|
1427
|
+
tooltip.id = tooltipId;
|
|
1428
|
+
tooltip.textContent = text;
|
|
1429
|
+
const style = document.createElement("style");
|
|
1430
|
+
style.setAttribute("data-demon-commentary", "");
|
|
1431
|
+
style.textContent = `
|
|
1432
|
+
@keyframes demon-commentary-in {
|
|
1433
|
+
from {
|
|
1434
|
+
opacity: 0;
|
|
1435
|
+
transform: translateY(var(--demon-slide-y, 8px));
|
|
1436
|
+
}
|
|
1437
|
+
to {
|
|
1438
|
+
opacity: 1;
|
|
1439
|
+
transform: translateY(0);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
@keyframes demon-commentary-out {
|
|
1443
|
+
from {
|
|
1444
|
+
opacity: 1;
|
|
1445
|
+
transform: translateY(0);
|
|
1446
|
+
}
|
|
1447
|
+
to {
|
|
1448
|
+
opacity: 0;
|
|
1449
|
+
transform: translateY(var(--demon-slide-y, 8px));
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
#${tooltipId} {
|
|
1453
|
+
--demon-slide-y: 8px;
|
|
1454
|
+
position: fixed;
|
|
1455
|
+
z-index: 2147483647;
|
|
1456
|
+
background: #1a1a2e;
|
|
1457
|
+
color: #eee;
|
|
1458
|
+
padding: 8px 14px;
|
|
1459
|
+
border-radius: 6px;
|
|
1460
|
+
font: 14px/1.4 system-ui, sans-serif;
|
|
1461
|
+
max-width: 320px;
|
|
1462
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
1463
|
+
pointer-events: none;
|
|
1464
|
+
animation: demon-commentary-in 0.3s ease-out forwards;
|
|
1465
|
+
}
|
|
1466
|
+
#${tooltipId}.demon-commentary-hiding {
|
|
1467
|
+
animation: demon-commentary-out 0.25s ease-in forwards;
|
|
1468
|
+
}
|
|
1469
|
+
`;
|
|
1470
|
+
document.querySelector("style[data-demon-commentary]")?.remove();
|
|
1471
|
+
document.head.appendChild(style);
|
|
1472
|
+
tooltip.style.visibility = "hidden";
|
|
1473
|
+
document.body.appendChild(tooltip);
|
|
1474
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
1475
|
+
const tooltipWidth = tooltipRect.width;
|
|
1476
|
+
const tooltipHeight = tooltipRect.height;
|
|
1477
|
+
const viewportWidth = window.innerWidth;
|
|
1478
|
+
let top = rect.bottom + 10;
|
|
1479
|
+
if (top + tooltipHeight > window.innerHeight && rect.top - 10 - tooltipHeight >= 0) {
|
|
1480
|
+
top = rect.top - 10 - tooltipHeight;
|
|
1481
|
+
tooltip.style.setProperty("--demon-slide-y", "-8px");
|
|
1482
|
+
}
|
|
1483
|
+
const left = Math.max(4, Math.min(rect.left + rect.width / 2 - tooltipWidth / 2, viewportWidth - 4 - tooltipWidth));
|
|
1484
|
+
tooltip.style.top = `${top}px`;
|
|
1485
|
+
tooltip.style.left = `${left}px`;
|
|
1486
|
+
tooltip.style.visibility = "";
|
|
1487
|
+
}, { selector: options.selector, text: options.text, tooltipId: TOOLTIP_ID });
|
|
1488
|
+
}
|
|
1489
|
+
async function hideCommentary(page) {
|
|
1490
|
+
await page.evaluate((tooltipId) => {
|
|
1491
|
+
const tooltip = document.getElementById(tooltipId);
|
|
1492
|
+
if (!tooltip)
|
|
1493
|
+
return;
|
|
1494
|
+
tooltip.classList.add("demon-commentary-hiding");
|
|
1495
|
+
tooltip.addEventListener("animationend", () => {
|
|
1496
|
+
tooltip.remove();
|
|
1497
|
+
document.querySelector("style[data-demon-commentary]")?.remove();
|
|
1498
|
+
}, { once: true });
|
|
1499
|
+
}, TOOLTIP_ID);
|
|
1500
|
+
await page.waitForTimeout(300);
|
|
1138
1501
|
}
|
|
1139
1502
|
// src/recorder.ts
|
|
1140
1503
|
class DemoRecorder {
|
|
@@ -1166,24 +1529,114 @@ class DemoRecorder {
|
|
|
1166
1529
|
return [...this.steps];
|
|
1167
1530
|
}
|
|
1168
1531
|
async save(outputDir) {
|
|
1169
|
-
const { join:
|
|
1170
|
-
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
1171
|
-
|
|
1172
|
-
const filePath =
|
|
1173
|
-
|
|
1532
|
+
const { join: join3 } = await import("node:path");
|
|
1533
|
+
const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync3 } = await import("node:fs");
|
|
1534
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
1535
|
+
const filePath = join3(outputDir, "demo-steps.json");
|
|
1536
|
+
writeFileSync3(filePath, JSON.stringify(this.steps, null, 2) + `
|
|
1174
1537
|
`);
|
|
1175
1538
|
}
|
|
1176
1539
|
}
|
|
1540
|
+
// src/feedback-server.ts
|
|
1541
|
+
import { randomUUID } from "node:crypto";
|
|
1542
|
+
var pendingReviews = new Map;
|
|
1543
|
+
var CORS_HEADERS = {
|
|
1544
|
+
"Access-Control-Allow-Origin": "*",
|
|
1545
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
1546
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
1547
|
+
};
|
|
1548
|
+
function isValidPayload(body) {
|
|
1549
|
+
if (typeof body !== "object" || body === null)
|
|
1550
|
+
return false;
|
|
1551
|
+
const obj = body;
|
|
1552
|
+
if (obj["verdict"] !== "approve" && obj["verdict"] !== "request_changes")
|
|
1553
|
+
return false;
|
|
1554
|
+
if (obj["feedback"] !== undefined && typeof obj["feedback"] !== "string")
|
|
1555
|
+
return false;
|
|
1556
|
+
return true;
|
|
1557
|
+
}
|
|
1558
|
+
function startFeedbackServer(preferredPort = 0) {
|
|
1559
|
+
const reviewId = randomUUID();
|
|
1560
|
+
const server = Bun.serve({
|
|
1561
|
+
port: preferredPort,
|
|
1562
|
+
async fetch(req) {
|
|
1563
|
+
const url = new URL(req.url);
|
|
1564
|
+
if (req.method === "OPTIONS") {
|
|
1565
|
+
return new Response(null, {
|
|
1566
|
+
status: 204,
|
|
1567
|
+
headers: CORS_HEADERS
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
if (url.pathname === "/feedback" && req.method === "POST") {
|
|
1571
|
+
const requestReviewId = url.searchParams.get("reviewId");
|
|
1572
|
+
const headers = {
|
|
1573
|
+
"Content-Type": "application/json",
|
|
1574
|
+
...CORS_HEADERS
|
|
1575
|
+
};
|
|
1576
|
+
if (!requestReviewId) {
|
|
1577
|
+
return new Response(JSON.stringify({ error: "Missing reviewId query parameter" }), { status: 400, headers });
|
|
1578
|
+
}
|
|
1579
|
+
const pending = pendingReviews.get(requestReviewId);
|
|
1580
|
+
if (!pending) {
|
|
1581
|
+
return new Response(JSON.stringify({ error: "Review not found or already completed" }), { status: 404, headers });
|
|
1582
|
+
}
|
|
1583
|
+
let body;
|
|
1584
|
+
try {
|
|
1585
|
+
body = await req.json();
|
|
1586
|
+
} catch {
|
|
1587
|
+
return new Response(JSON.stringify({ error: "Invalid JSON" }), { status: 400, headers });
|
|
1588
|
+
}
|
|
1589
|
+
if (!isValidPayload(body)) {
|
|
1590
|
+
return new Response(JSON.stringify({ error: "Invalid payload" }), { status: 400, headers });
|
|
1591
|
+
}
|
|
1592
|
+
pending.resolve(body);
|
|
1593
|
+
pendingReviews.delete(requestReviewId);
|
|
1594
|
+
return new Response(JSON.stringify({ success: true, verdict: body.verdict }), { status: 200, headers });
|
|
1595
|
+
}
|
|
1596
|
+
if (url.pathname === "/health") {
|
|
1597
|
+
return new Response(JSON.stringify({ status: "ok" }), {
|
|
1598
|
+
headers: { "Content-Type": "application/json" }
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
return new Response("Not Found", { status: 404 });
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
const port = server.port ?? 3000;
|
|
1605
|
+
const feedbackEndpoint = `http://localhost:${port}/feedback?reviewId=${reviewId}`;
|
|
1606
|
+
const feedbackPromise = new Promise((resolve, reject) => {
|
|
1607
|
+
pendingReviews.set(reviewId, { resolve, reject });
|
|
1608
|
+
});
|
|
1609
|
+
return {
|
|
1610
|
+
server,
|
|
1611
|
+
port,
|
|
1612
|
+
reviewId,
|
|
1613
|
+
feedbackEndpoint,
|
|
1614
|
+
waitForFeedback: () => feedbackPromise,
|
|
1615
|
+
stop: () => {
|
|
1616
|
+
const pending = pendingReviews.get(reviewId);
|
|
1617
|
+
if (pending) {
|
|
1618
|
+
pending.reject(new Error("Server stopped"));
|
|
1619
|
+
pendingReviews.delete(reviewId);
|
|
1620
|
+
}
|
|
1621
|
+
server.stop();
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1177
1625
|
export {
|
|
1626
|
+
startFeedbackServer,
|
|
1178
1627
|
showCommentary,
|
|
1628
|
+
runReviewOrchestration,
|
|
1179
1629
|
parseLlmResponse,
|
|
1180
1630
|
invokeClaude,
|
|
1181
1631
|
hideCommentary,
|
|
1632
|
+
getReviewTemplate,
|
|
1182
1633
|
getRepoContext,
|
|
1183
1634
|
generateReviewHtml,
|
|
1635
|
+
generateReview,
|
|
1184
1636
|
extractJson,
|
|
1637
|
+
discoverDemoFiles,
|
|
1185
1638
|
buildReviewPrompt,
|
|
1186
1639
|
DemoRecorder
|
|
1187
1640
|
};
|
|
1188
1641
|
|
|
1189
|
-
//# debugId=
|
|
1642
|
+
//# debugId=D9F27F14A139E97164756E2164756E21
|