@lchase/kiln 0.10.0 → 0.11.0
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/assets/serve.css +190 -480
- package/assets/serve.js +72 -3
- package/dist/board-view.js +688 -0
- package/dist/board.js +0 -97
- package/dist/serve-page.js +384 -324
- package/dist/serve.js +183 -11
- package/package.json +3 -2
package/dist/serve.js
CHANGED
|
@@ -7,13 +7,14 @@ import { AUTH_PREFLIGHT_PREFIX } from "./auth.js";
|
|
|
7
7
|
import { loadKilnConfig } from "./config.js";
|
|
8
8
|
import { answerJob, readStatus, retryJob, reviseJob, runTick, startupRecovery } from "./driver.js";
|
|
9
9
|
import { browse } from "./fs-browse.js";
|
|
10
|
-
import {
|
|
10
|
+
import { branchOf, buildBoard, dayReport, jobDetail, parseSeenQuery, parseStatsWindow, readBranchCommits, readDiffStat, statsFor, unseenChanges, } from "./board-view.js";
|
|
11
|
+
import { appendEvent, eventsForJob, readEvents } from "./events.js";
|
|
11
12
|
import { INVESTIGATION_WORKFLOW } from "./findings.js";
|
|
12
13
|
import { defaultHost } from "./host.js";
|
|
13
14
|
import { intakeJobs } from "./intake-http.js";
|
|
14
|
-
import { packageRoot } from "./paths.js";
|
|
15
|
+
import { jobBasename, packageRoot } from "./paths.js";
|
|
15
16
|
import { SANDBOX_PREFLIGHT_PREFIX } from "./sandbox.js";
|
|
16
|
-
import {
|
|
17
|
+
import { renderApp, renderMessagePage, renderTextPage } from "./serve-page.js";
|
|
17
18
|
import { isPaused, pauseFactory, resumeFactory, spendFor } from "./rails.js";
|
|
18
19
|
import { readSettings, saveSettings } from "./settings.js";
|
|
19
20
|
import { listWorkflowNames } from "./workflow.js";
|
|
@@ -201,14 +202,31 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
201
202
|
sendJson(res, 405, { error: "Method not allowed." }, secrets);
|
|
202
203
|
return;
|
|
203
204
|
}
|
|
204
|
-
if (path === "/" || path === "" || path === "/board") {
|
|
205
|
-
sendHtml(res, 200, renderConsole(css, readStatus(config).attention, pageOptions(state, csrf)), secrets);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
205
|
if (path === "/api/jobs" || path === "/api/status") {
|
|
209
206
|
sendJson(res, 200, statusDocument(state), secrets);
|
|
210
207
|
return;
|
|
211
208
|
}
|
|
209
|
+
if (path === "/api/board") {
|
|
210
|
+
sendJson(res, 200, boardPayload(state), secrets);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (path === "/api/stats") {
|
|
214
|
+
const window = parseStatsWindow(url.searchParams.get("window"));
|
|
215
|
+
if (!window) {
|
|
216
|
+
sendJson(res, 400, { error: "window must be today, 7d, or 30d." }, secrets);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
sendJson(res, 200, statsDocument(state, window), secrets);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (path === "/api/report") {
|
|
223
|
+
sendJson(res, 200, reportDocument(state), secrets);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (path === "/api/unseen") {
|
|
227
|
+
sendJson(res, 200, unseenDocument(state, url.searchParams.getAll("seen")), secrets);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
212
230
|
if (path === "/api/events") {
|
|
213
231
|
const jobId = url.searchParams.get("job_id");
|
|
214
232
|
if (!jobId || !JOB_ID.test(jobId)) {
|
|
@@ -218,6 +236,17 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
218
236
|
sendJson(res, 200, { job_id: jobId, events: eventsForJob(config.kilnDir, jobId) }, secrets);
|
|
219
237
|
return;
|
|
220
238
|
}
|
|
239
|
+
const apiDetail = /^\/api\/jobs\/([^/]+)\/detail$/.exec(path);
|
|
240
|
+
if (apiDetail?.[1]) {
|
|
241
|
+
const jobId = parseJobId(apiDetail[1]);
|
|
242
|
+
const job = jobId ? findJob(config, jobId) : undefined;
|
|
243
|
+
if (!job) {
|
|
244
|
+
sendJson(res, 404, { error: jobId ? `No job ${jobId}.` : "Not found." }, secrets);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
sendJson(res, 200, detailDocument(state, job), secrets);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
221
250
|
const apiJob = /^\/api\/jobs\/([^/]+)$/.exec(path);
|
|
222
251
|
if (apiJob?.[1]) {
|
|
223
252
|
const jobId = parseJobId(apiJob[1]);
|
|
@@ -229,6 +258,54 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
229
258
|
sendJson(res, 200, job, secrets);
|
|
230
259
|
return;
|
|
231
260
|
}
|
|
261
|
+
const listed = listSection(path);
|
|
262
|
+
if (listed) {
|
|
263
|
+
if (listed === "stats") {
|
|
264
|
+
const window = parseStatsWindow(url.searchParams.get("window"));
|
|
265
|
+
if (!window) {
|
|
266
|
+
sendHtml(res, 400, renderMessagePage(css, "Stats", "window must be today, 7d, or 30d."), secrets);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
sendHtml(res, 200, renderShell(state, css, csrf, "stats", { window }), secrets);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
sendHtml(res, 200, renderShell(state, css, csrf, listed), secrets);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const repoPage = /^\/repos\/([^/]+)$/.exec(path);
|
|
276
|
+
if (repoPage?.[1]) {
|
|
277
|
+
let name;
|
|
278
|
+
try {
|
|
279
|
+
name = decodeURIComponent(repoPage[1]);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", "Not found."), secrets);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!state.config.repos[name]) {
|
|
286
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", `No repo ${name}.`), secrets);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
sendHtml(res, 200, renderShell(state, css, csrf, "repos", { repo: name }), secrets);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const jobPage = /^\/jobs\/([^/]+)$/.exec(path);
|
|
293
|
+
if (jobPage?.[1]) {
|
|
294
|
+
const jobId = parseJobId(jobPage[1]);
|
|
295
|
+
const job = jobId ? findJob(config, jobId) : undefined;
|
|
296
|
+
if (!job) {
|
|
297
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", jobId ? `No job ${jobId}.` : "Not found."), secrets);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const tabRaw = url.searchParams.get("tab");
|
|
301
|
+
if (tabRaw && tabRaw !== "overview" && tabRaw !== "changes" && tabRaw !== "timeline") {
|
|
302
|
+
sendHtml(res, 400, renderMessagePage(css, "Job", "tab must be overview, changes, or timeline."), secrets);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const tab = tabRaw === "changes" || tabRaw === "timeline" ? tabRaw : "overview";
|
|
306
|
+
sendHtml(res, 200, renderShell(state, css, csrf, "job", { detail: detailDocument(state, job), tab }), secrets);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
232
309
|
const page = /^\/jobs\/([^/]+)\/(spec|log|findings)$/.exec(path);
|
|
233
310
|
if (page?.[1] && (page[2] === "spec" || page[2] === "log" || page[2] === "findings")) {
|
|
234
311
|
const jobId = parseJobId(page[1]);
|
|
@@ -402,13 +479,13 @@ function redirectTarget(req) {
|
|
|
402
479
|
return "/";
|
|
403
480
|
try {
|
|
404
481
|
const url = new URL(referer);
|
|
405
|
-
if (url.pathname
|
|
406
|
-
return "/
|
|
482
|
+
if (!url.pathname.startsWith("/") || url.pathname.startsWith("//"))
|
|
483
|
+
return "/";
|
|
484
|
+
return `${url.pathname}${url.search}`;
|
|
407
485
|
}
|
|
408
486
|
catch {
|
|
409
487
|
return "/";
|
|
410
488
|
}
|
|
411
|
-
return "/";
|
|
412
489
|
}
|
|
413
490
|
function wantsJson(req) {
|
|
414
491
|
const accept = headerValue(req.headers.accept);
|
|
@@ -639,9 +716,104 @@ function statusDocument(state) {
|
|
|
639
716
|
repos: repoList(state.config),
|
|
640
717
|
configError: state.configError ?? null,
|
|
641
718
|
paused: isPaused(state.config.kilnDir),
|
|
642
|
-
spend: { day: spend.day, usd: spend.usd, cap: spend.cap },
|
|
719
|
+
spend: { day: spend.day, usd: spend.usd, cap: spend.cap, blocked: spend.blocked },
|
|
643
720
|
};
|
|
644
721
|
}
|
|
722
|
+
function listSection(path) {
|
|
723
|
+
if (path === "/" || path === "" || path === "/board" || path === "/inbox")
|
|
724
|
+
return "inbox";
|
|
725
|
+
if (path === "/hand")
|
|
726
|
+
return "hand";
|
|
727
|
+
if (path === "/repos")
|
|
728
|
+
return "repos";
|
|
729
|
+
if (path === "/history")
|
|
730
|
+
return "history";
|
|
731
|
+
if (path === "/stats")
|
|
732
|
+
return "stats";
|
|
733
|
+
if (path === "/report")
|
|
734
|
+
return "report";
|
|
735
|
+
if (path === "/settings")
|
|
736
|
+
return "settings";
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
function renderShell(state, css, csrf, section, extra = {}) {
|
|
740
|
+
const status = readStatus(state.config);
|
|
741
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
742
|
+
const board = buildBoard(status.jobs, events);
|
|
743
|
+
const page = pageOptions(state, csrf);
|
|
744
|
+
return renderApp({
|
|
745
|
+
css,
|
|
746
|
+
section,
|
|
747
|
+
board,
|
|
748
|
+
attention: status.attention,
|
|
749
|
+
page: extra.repo ? { ...page, repo: extra.repo } : page,
|
|
750
|
+
...(extra.repo ? { repo: extra.repo } : {}),
|
|
751
|
+
...(section === "stats" && extra.window ? { stats: statsDocument(state, extra.window) } : {}),
|
|
752
|
+
...(section === "report" ? { report: reportDocument(state) } : {}),
|
|
753
|
+
...(extra.detail ? { detail: extra.detail, tab: extra.tab ?? "overview" } : {}),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
function boardPayload(state) {
|
|
757
|
+
const status = readStatus(state.config);
|
|
758
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
759
|
+
const spend = spendFor(state.config.kilnDir, state.config.dailyDollarCap);
|
|
760
|
+
return {
|
|
761
|
+
...buildBoard(status.jobs, events),
|
|
762
|
+
paused: isPaused(state.config.kilnDir),
|
|
763
|
+
spend,
|
|
764
|
+
configError: state.configError ?? null,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function statsDocument(state, window) {
|
|
768
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
769
|
+
return statsFor(events, {
|
|
770
|
+
window,
|
|
771
|
+
...(state.config.dailyDollarCap !== undefined ? { cap: state.config.dailyDollarCap } : {}),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
function reportDocument(state) {
|
|
775
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
776
|
+
return dayReport(events, state.config.dailyDollarCap !== undefined ? { cap: state.config.dailyDollarCap } : {});
|
|
777
|
+
}
|
|
778
|
+
function unseenDocument(state, seenValues) {
|
|
779
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
780
|
+
const board = buildBoard(readStatus(state.config).jobs, events);
|
|
781
|
+
return unseenChanges(events, parseSeenQuery(seenValues), new Set(board.inbox.map((job) => job.jobId)));
|
|
782
|
+
}
|
|
783
|
+
function detailDocument(state, job) {
|
|
784
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
785
|
+
const repo = state.config.repos[job.repo];
|
|
786
|
+
const branch = branchOf(job, events);
|
|
787
|
+
const base = prBase(events, job.jobId);
|
|
788
|
+
const commits = repo && branch ? readBranchCommits(repo.path, branch) : [];
|
|
789
|
+
const diff = repo && branch && base ? readDiffStat(repo.path, base, branch) : undefined;
|
|
790
|
+
const issue = jobIssue(state.config, job.jobId);
|
|
791
|
+
return jobDetail({
|
|
792
|
+
job,
|
|
793
|
+
events,
|
|
794
|
+
maxFixRounds: repo?.maxFixRounds ?? state.config.maxFixRounds,
|
|
795
|
+
commits,
|
|
796
|
+
...(issue ? { issue } : {}),
|
|
797
|
+
...(diff ? { diffStat: diff } : {}),
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
function prBase(events, jobId) {
|
|
801
|
+
let base;
|
|
802
|
+
for (const event of events) {
|
|
803
|
+
if (event.job_id === jobId && event.event === "pr.opened")
|
|
804
|
+
base = event.properties.base;
|
|
805
|
+
}
|
|
806
|
+
return base;
|
|
807
|
+
}
|
|
808
|
+
function jobIssue(config, jobId) {
|
|
809
|
+
try {
|
|
810
|
+
const raw = JSON.parse(readFileSync(join(config.jobsDir, `${jobBasename(jobId)}.json`), "utf8"));
|
|
811
|
+
return typeof raw.issue === "string" ? raw.issue : undefined;
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
645
817
|
function statusCode(error) {
|
|
646
818
|
if (typeof error === "object" && error !== null && "statusCode" in error) {
|
|
647
819
|
const code = error.statusCode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lchase/kiln",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Run a workflow on any registered repo. A markdown spec is the input.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
],
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/lchase/kiln.git"
|
|
19
|
+
"url": "git+https://github.com/lchase/kiln.git",
|
|
20
|
+
"directory": "packages/kiln"
|
|
20
21
|
},
|
|
21
22
|
"publishConfig": {
|
|
22
23
|
"access": "public"
|