cloving 1.0.1 → 1.1.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/README.md +145 -14
- package/dist/src/authority/commands.js +10 -2
- package/dist/src/authority/reconciliation.d.ts +2 -0
- package/dist/src/authority/reconciliation.js +10 -1
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +2 -0
- package/dist/src/broker/actor/authority-session.d.ts +2 -0
- package/dist/src/broker/actor/authority-session.js +3 -4
- package/dist/src/chats/analysis-batches.d.ts +24 -0
- package/dist/src/chats/analysis-batches.js +83 -0
- package/dist/src/chats/analysis-contract.d.ts +80 -0
- package/dist/src/chats/analysis-contract.js +16 -0
- package/dist/src/chats/analysis-data.d.ts +3 -0
- package/dist/src/chats/analysis-data.js +85 -0
- package/dist/src/chats/analysis-input.d.ts +12 -0
- package/dist/src/chats/analysis-input.js +83 -0
- package/dist/src/chats/analysis-links.d.ts +2 -0
- package/dist/src/chats/analysis-links.js +25 -0
- package/dist/src/chats/analysis-prompt.d.ts +10 -0
- package/dist/src/chats/analysis-prompt.js +16 -0
- package/dist/src/chats/analysis-proposal.d.ts +2 -0
- package/dist/src/chats/analysis-proposal.js +32 -0
- package/dist/src/chats/analysis-provenance.d.ts +4 -0
- package/dist/src/chats/analysis-provenance.js +99 -0
- package/dist/src/chats/analysis-shape.d.ts +3 -0
- package/dist/src/chats/analysis-shape.js +228 -0
- package/dist/src/chats/analysis.d.ts +3 -0
- package/dist/src/chats/analysis.js +42 -0
- package/dist/src/chats/corpus-input.d.ts +5 -0
- package/dist/src/chats/corpus-input.js +288 -0
- package/dist/src/chats/destination.js +3 -1
- package/dist/src/chats/import.d.ts +8 -6
- package/dist/src/chats/import.js +195 -23
- package/dist/src/chats/input-file.d.ts +3 -0
- package/dist/src/chats/input-file.js +32 -0
- package/dist/src/chats/normalize.d.ts +11 -2
- package/dist/src/chats/normalize.js +18 -2
- package/dist/src/chats/previous-import.d.ts +5 -0
- package/dist/src/chats/previous-import.js +34 -0
- package/dist/src/chats/reader.d.ts +7 -1
- package/dist/src/chats/reader.js +46 -15
- package/dist/src/chats/redact-import.d.ts +14 -0
- package/dist/src/chats/redact-import.js +141 -0
- package/dist/src/chats/redaction.d.ts +41 -0
- package/dist/src/chats/redaction.js +161 -0
- package/dist/src/chats/revision-input.d.ts +3 -0
- package/dist/src/chats/revision-input.js +180 -0
- package/dist/src/chats/revision.d.ts +48 -0
- package/dist/src/chats/revision.js +94 -0
- package/dist/src/chats/selection.d.ts +25 -0
- package/dist/src/chats/selection.js +109 -0
- package/dist/src/chats/source.d.ts +23 -0
- package/dist/src/chats/source.js +135 -0
- package/dist/src/chats/structure.d.ts +57 -0
- package/dist/src/chats/structure.js +216 -0
- package/dist/src/cli/arguments.d.ts +8 -0
- package/dist/src/cli/arguments.js +94 -13
- package/dist/src/cli/client.d.ts +1 -0
- package/dist/src/cli/client.js +38 -1
- package/dist/src/cli/commands.d.ts +3 -0
- package/dist/src/cli/commands.js +29 -11
- package/dist/src/cli/main.d.ts +2 -1
- package/dist/src/cli/main.js +61 -14
- package/dist/src/cli/render.d.ts +2 -0
- package/dist/src/cli/render.js +70 -2
- package/dist/src/cli/usage-render.d.ts +3 -0
- package/dist/src/cli/usage-render.js +28 -0
- package/dist/src/cli/usage.d.ts +3 -2
- package/dist/src/cli/usage.js +38 -6
- package/dist/src/cli/watch-actions.d.ts +10 -0
- package/dist/src/cli/watch-actions.js +58 -0
- package/dist/src/cli/watch-board-view.d.ts +32 -0
- package/dist/src/cli/watch-board-view.js +88 -15
- package/dist/src/cli/watch-board.js +6 -1
- package/dist/src/cli/watch-card-client.d.ts +1 -0
- package/dist/src/cli/watch-card-client.js +44 -0
- package/dist/src/cli/watch-card-forms.d.ts +2 -0
- package/dist/src/cli/watch-card-forms.js +33 -0
- package/dist/src/cli/watch-card-reply.d.ts +5 -0
- package/dist/src/cli/watch-card-reply.js +34 -0
- package/dist/src/cli/watch-card-view.d.ts +40 -0
- package/dist/src/cli/watch-card-view.js +86 -0
- package/dist/src/cli/watch-client.d.ts +1 -0
- package/dist/src/cli/watch-client.js +118 -0
- package/dist/src/cli/watch-model.js +31 -14
- package/dist/src/cli/watch-page.d.ts +1 -1
- package/dist/src/cli/watch-page.js +71 -85
- package/dist/src/cli/watch-pr-board.d.ts +3 -0
- package/dist/src/cli/watch-pr-board.js +26 -0
- package/dist/src/cli/watch-pr-command.d.ts +15 -0
- package/dist/src/cli/watch-pr-command.js +37 -0
- package/dist/src/cli/watch-pr-identity.d.ts +20 -0
- package/dist/src/cli/watch-pr-identity.js +62 -0
- package/dist/src/cli/watch-pr-observer.d.ts +19 -0
- package/dist/src/cli/watch-pr-observer.js +108 -0
- package/dist/src/cli/watch-pr-settings.d.ts +8 -0
- package/dist/src/cli/watch-pr-settings.js +47 -0
- package/dist/src/cli/watch-reads.js +4 -0
- package/dist/src/cli/watch-render.d.ts +16 -2
- package/dist/src/cli/watch-render.js +10 -3
- package/dist/src/cli/watch-run-input.d.ts +14 -0
- package/dist/src/cli/watch-run-input.js +49 -0
- package/dist/src/cli/watch-server.d.ts +3 -0
- package/dist/src/cli/watch-server.js +44 -6
- package/dist/src/cli/watch-start-view.d.ts +18 -0
- package/dist/src/cli/watch-start-view.js +77 -0
- package/dist/src/cli/watch-turn.d.ts +1 -0
- package/dist/src/cli/watch-turn.js +7 -2
- package/dist/src/cli/watch-usage.d.ts +6 -0
- package/dist/src/cli/watch-usage.js +65 -0
- package/dist/src/cli/watch-view.d.ts +15 -0
- package/dist/src/cli/watch-view.js +61 -17
- package/dist/src/cli.js +48 -6
- package/dist/src/daemon/analysis-launch.d.ts +5 -0
- package/dist/src/daemon/analysis-launch.js +81 -0
- package/dist/src/daemon/analysis-token.d.ts +6 -0
- package/dist/src/daemon/analysis-token.js +88 -0
- package/dist/src/daemon/analysis-worker.d.ts +22 -0
- package/dist/src/daemon/analysis-worker.js +219 -0
- package/dist/src/daemon/chats-files.d.ts +15 -0
- package/dist/src/daemon/chats-files.js +113 -0
- package/dist/src/daemon/chats-worker.d.ts +12 -0
- package/dist/src/daemon/chats-worker.js +200 -0
- package/dist/src/daemon/claude-turn.d.ts +10 -0
- package/dist/src/daemon/claude-turn.js +96 -15
- package/dist/src/daemon/claude-worker.d.ts +4 -1
- package/dist/src/daemon/claude-worker.js +99 -47
- package/dist/src/daemon/codex-stream.js +16 -0
- package/dist/src/daemon/daemon.d.ts +3 -0
- package/dist/src/daemon/daemon.js +16 -7
- package/dist/src/daemon/github-methods.js +4 -0
- package/dist/src/daemon/github-ticket-status.d.ts +3 -0
- package/dist/src/daemon/github-ticket-status.js +268 -0
- package/dist/src/daemon/issue-queue.d.ts +1 -0
- package/dist/src/daemon/issue-queue.js +115 -24
- package/dist/src/daemon/operation-journal.d.ts +1 -0
- package/dist/src/daemon/operation-journal.js +4 -2
- package/dist/src/daemon/retrying-turn.d.ts +9 -0
- package/dist/src/daemon/retrying-turn.js +185 -0
- package/dist/src/host/world-reconciliation.d.ts +2 -0
- package/dist/src/host/world-reconciliation.js +6 -0
- package/dist/src/host/world-runs.js +9 -1
- package/dist/src/service/run-handlers.js +15 -23
- package/dist/src/service/worker-totals.d.ts +3 -0
- package/dist/src/service/worker-totals.js +28 -0
- package/dist/src/ticket-status.d.ts +8 -0
- package/dist/src/ticket-status.js +45 -0
- package/dist/src/token-prices.d.ts +19 -0
- package/dist/src/token-prices.js +76 -0
- package/dist/src/worker-usage.d.ts +22 -0
- package/dist/src/worker-usage.js +44 -0
- package/dist/src/workflow-candidate.d.ts +29 -0
- package/dist/src/workflow-candidate.js +59 -0
- package/dist/src/workflow-contracts.d.ts +2 -0
- package/dist/src/workflow-contracts.js +27 -5
- package/dist/src/workflow-declaration.d.ts +1 -1
- package/dist/src/workflow-declaration.js +3 -1
- package/dist/src/workflows.d.ts +1 -0
- package/dist/src/workflows.js +1 -0
- package/dist/tools/self-host-processes.js +7 -2
- package/dist/tools/self-host-workflows.d.ts +1 -1
- package/dist/tools/self-host-workflows.js +2 -2
- package/dist/tools/self-host.js +6 -6
- package/dist/tools/self-update-host.d.ts +7 -0
- package/dist/tools/self-update-host.js +48 -1
- package/dist/tools/self-update.d.ts +1 -0
- package/dist/tools/self-update.js +85 -13
- package/dist/workflows/analyze-chats.d.ts +10 -0
- package/dist/workflows/analyze-chats.declaration.json +9 -0
- package/dist/workflows/analyze-chats.js +73 -0
- package/dist/workflows/develop-ticket.d.ts +1 -7
- package/dist/workflows/develop-ticket.js +42 -141
- package/dist/workflows/issue-labels.d.ts +3 -1
- package/dist/workflows/issue-labels.js +34 -0
- package/dist/workflows/ticket-prompts.d.ts +35 -0
- package/dist/workflows/ticket-prompts.js +133 -0
- package/package.json +5 -3
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { isTicketStatus } from "../ticket-status.js";
|
|
2
|
+
import { priceFields, statusTurns } from "./watch-usage.js";
|
|
3
|
+
import { priceOf, usageOf } from "../worker-usage.js";
|
|
4
|
+
import { priceText } from "./usage-render.js";
|
|
1
5
|
import { boardsOf, currentStepOf, roundOf, startWorkflowOf, } from "./watch-board.js";
|
|
2
6
|
import { decisionOf, phasesOf } from "./watch-phases.js";
|
|
3
7
|
import { displayRun } from "./watch-paths.js";
|
|
@@ -59,7 +63,7 @@ function failed(part) {
|
|
|
59
63
|
return [{ kind: "error", text: shown }];
|
|
60
64
|
}
|
|
61
65
|
function ended(line) {
|
|
62
|
-
const cost =
|
|
66
|
+
const cost = priceOf(line["total_cost_usd"]);
|
|
63
67
|
return [
|
|
64
68
|
{
|
|
65
69
|
kind: "result",
|
|
@@ -79,13 +83,6 @@ export function activityOf(line) {
|
|
|
79
83
|
return parts(value).flatMap(failed);
|
|
80
84
|
return value["type"] === "result" ? ended(value) : [];
|
|
81
85
|
}
|
|
82
|
-
function costOf(lines) {
|
|
83
|
-
const costs = lines
|
|
84
|
-
.map((line) => record(parsed(line)))
|
|
85
|
-
.filter((value) => value["type"] === "result")
|
|
86
|
-
.map((value) => count(value["total_cost_usd"]));
|
|
87
|
-
return costs.at(-1) ?? null;
|
|
88
|
-
}
|
|
89
86
|
function concise(items) {
|
|
90
87
|
return items.map((item, index) => {
|
|
91
88
|
const before = items[index - 1];
|
|
@@ -103,8 +100,9 @@ function shortened(run) {
|
|
|
103
100
|
turns: run.turns.map((turn) => ({
|
|
104
101
|
...turn,
|
|
105
102
|
activity: concise(turn.activity.map((item) => {
|
|
106
|
-
const
|
|
107
|
-
|
|
103
|
+
const spent = item.kind === "result"
|
|
104
|
+
? ` (cost ${priceText(item.costUsd, turn.estimatedCostUsd)})`
|
|
105
|
+
: "";
|
|
108
106
|
return { kind: item.kind, text: `${cut(item.text)}${spent}` };
|
|
109
107
|
})).slice(-activityLimit),
|
|
110
108
|
})),
|
|
@@ -118,14 +116,26 @@ export function turnOf(records) {
|
|
|
118
116
|
const report = record(parsed(text(result["result"])));
|
|
119
117
|
return {
|
|
120
118
|
key: records.key,
|
|
119
|
+
operationId: text(started["operationId"]),
|
|
120
|
+
usage: usageOf(report["usage"]),
|
|
121
121
|
runId: text(operation[2]),
|
|
122
122
|
target: text(started["target"]),
|
|
123
123
|
branch: text(started["branch"]),
|
|
124
124
|
worktree: text(started["worktree"]),
|
|
125
125
|
startedAt: text(started["startedAt"]),
|
|
126
126
|
outcome: records.result === null ? "in work" : outcome,
|
|
127
|
-
costUsd:
|
|
128
|
-
|
|
127
|
+
costUsd: priceOf(report["costUsd"]),
|
|
128
|
+
estimatedCostUsd: priceOf(report["estimatedCostUsd"]),
|
|
129
|
+
activity: records.lines
|
|
130
|
+
.flatMap(activityOf)
|
|
131
|
+
.map((item) => item.kind === "result"
|
|
132
|
+
? {
|
|
133
|
+
kind: item.kind,
|
|
134
|
+
text: item.text,
|
|
135
|
+
...priceFields(report["costUsd"]),
|
|
136
|
+
}
|
|
137
|
+
: item)
|
|
138
|
+
.slice(-activityLimit - 1),
|
|
129
139
|
endedAt: stamp(result["endedAt"]),
|
|
130
140
|
activeAt: records.activeAt ?? null,
|
|
131
141
|
commits: texts(report["commits"]),
|
|
@@ -215,14 +225,21 @@ function runOf(inputs, value) {
|
|
|
215
225
|
const workflowId = text(status["workflowId"]);
|
|
216
226
|
const budget = record(status["budget"]);
|
|
217
227
|
const ticketId = text(status["ticketId"]);
|
|
228
|
+
const ticketStatus = record(status["outcome"])["ticketStatus"];
|
|
218
229
|
return {
|
|
219
230
|
runId,
|
|
220
231
|
workflowId,
|
|
221
232
|
ticketId: ticketId === "" ? null : ticketId,
|
|
222
233
|
title: stamp(status["title"]),
|
|
223
234
|
outcome: stamp(record(status["outcome"])["kind"]),
|
|
235
|
+
...(isTicketStatus(ticketStatus) ? { ticketStatus } : {}),
|
|
236
|
+
...(record(status["outcome"])["issueUpdate"] === "failed"
|
|
237
|
+
? { issueUpdate: "failed" }
|
|
238
|
+
: {}),
|
|
224
239
|
latestAt: stamp(status["latestAt"]),
|
|
225
|
-
claudeCostUsd:
|
|
240
|
+
claudeCostUsd: priceOf(status["claudeCostUsd"]),
|
|
241
|
+
estimatedCostUsd: priceOf(status["estimatedCostUsd"]),
|
|
242
|
+
usage: usageOf(status["usage"]),
|
|
226
243
|
currentStep: currentStepOf(list(record(value)["events"]), workflowId),
|
|
227
244
|
round: roundOf(list(record(value)["events"])),
|
|
228
245
|
state: text(status["state"]),
|
|
@@ -234,7 +251,7 @@ function runOf(inputs, value) {
|
|
|
234
251
|
.filter((decision) => record(decision)["runId"] === runId)
|
|
235
252
|
.map((decision) => decisionCommands(decision, inputs.client)),
|
|
236
253
|
events: list(record(value)["events"]).map(eventOf),
|
|
237
|
-
turns: inputs.turns
|
|
254
|
+
turns: statusTurns(inputs.turns, status),
|
|
238
255
|
phases: phasesOf(list(record(value)["events"]), workflowId),
|
|
239
256
|
decision: decisionOf(list(record(value)["events"])),
|
|
240
257
|
feedback: list(status["feedback"]).map(feedbackOf),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function pageDocument(): string;
|
|
1
|
+
export declare function pageDocument(actionToken?: string | null): string;
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cardReply } from "./watch-card-reply.js";
|
|
2
|
+
import { watchCardClient } from "./watch-card-client.js";
|
|
3
|
+
import { AddCard, CardInput, cardKey, cardSubmitKey, } from "./watch-card-view.js";
|
|
4
|
+
import { watchClient } from "./watch-client.js";
|
|
5
|
+
import { priceOf, tokenCount, usageOf } from "../worker-usage.js";
|
|
6
|
+
import { priceText, usageText, totalsSuffix } from "./usage-render.js";
|
|
7
|
+
import { startPrompts, StartCard, StartRun } from "./watch-start-view.js";
|
|
8
|
+
import { Board, boardSizing, Card, Column, columnCollapsed, CardReview, WorkflowList, cardReview, cardStep, completionMark, } from "./watch-board-view.js";
|
|
2
9
|
import { boardWorkflow, watchHash, watchLocation, watchTitle, } from "./watch-navigation.js";
|
|
3
10
|
import { clockTime, completedDetail, counted, elapsed, groupActivity, pageTitle, phaseTime, runName, runTone, selectedRun, toneOf, turnTime, waitingDetail, } from "./watch-render.js";
|
|
4
11
|
import { Command, Decision, Events, Group, Item, Turn, Work, } from "./watch-turn.js";
|
|
5
|
-
import { clove, Banner, BannerDetail, Header, Page, Phases, PullRequest, Review, Run, } from "./watch-view.js";
|
|
12
|
+
import { clove, Banner, BannerDetail, BoardPage, Header, Page, Phases, PullRequest, Review, Run, } from "./watch-view.js";
|
|
6
13
|
const functions = [
|
|
14
|
+
cardReply,
|
|
15
|
+
cardSubmitKey,
|
|
16
|
+
cardKey,
|
|
17
|
+
AddCard,
|
|
18
|
+
CardInput,
|
|
19
|
+
priceOf,
|
|
20
|
+
tokenCount,
|
|
21
|
+
usageOf,
|
|
22
|
+
priceText,
|
|
23
|
+
usageText,
|
|
24
|
+
totalsSuffix,
|
|
7
25
|
watchHash,
|
|
8
26
|
watchLocation,
|
|
9
27
|
boardWorkflow,
|
|
@@ -13,6 +31,8 @@ const functions = [
|
|
|
13
31
|
completionMark,
|
|
14
32
|
CardReview,
|
|
15
33
|
Card,
|
|
34
|
+
Column,
|
|
35
|
+
columnCollapsed,
|
|
16
36
|
WorkflowList,
|
|
17
37
|
Board,
|
|
18
38
|
counted,
|
|
@@ -35,6 +55,9 @@ const functions = [
|
|
|
35
55
|
Command,
|
|
36
56
|
Decision,
|
|
37
57
|
Events,
|
|
58
|
+
startPrompts,
|
|
59
|
+
StartCard,
|
|
60
|
+
StartRun,
|
|
38
61
|
BannerDetail,
|
|
39
62
|
Banner,
|
|
40
63
|
Phases,
|
|
@@ -42,6 +65,7 @@ const functions = [
|
|
|
42
65
|
Review,
|
|
43
66
|
Run,
|
|
44
67
|
Header,
|
|
68
|
+
BoardPage,
|
|
45
69
|
Page,
|
|
46
70
|
];
|
|
47
71
|
const light = `
|
|
@@ -83,28 +107,57 @@ const style = `
|
|
|
83
107
|
:root[data-theme="dark"] {${dark}}
|
|
84
108
|
* { box-sizing: border-box; }
|
|
85
109
|
body { margin: 0 auto; max-width: 1800px; padding: 0 20px 96px; }
|
|
110
|
+
body:has(.board-page) { padding-bottom: 0; overflow: hidden; }
|
|
111
|
+
.board-page { height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
|
|
112
|
+
.board-page > header, .board-page > .down { flex-shrink: 0; }
|
|
113
|
+
.start-run { flex-shrink: 0; max-height: 45dvh; overflow-y: auto; }
|
|
86
114
|
a { color: var(--accent-strong); }
|
|
87
115
|
header { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; padding: 22px 0 10px; color: var(--muted); font-size: 12px; }
|
|
88
116
|
.brand { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); font: 800 26px/1.2 var(--heading-font); }
|
|
89
117
|
.brand svg { width: 30px; height: 30px; color: var(--accent); }
|
|
90
|
-
a:focus-visible, button:focus-visible, summary:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; box-shadow: 0 0 0 6px var(--code-bg); }
|
|
118
|
+
a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; box-shadow: 0 0 0 6px var(--code-bg); }
|
|
91
119
|
h2, h3 { font-family: var(--heading-font); font-weight: 700; }
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.
|
|
120
|
+
.start-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: 16px; margin-top: 12px; }
|
|
121
|
+
.start-card { min-width: 0; padding: 14px; border-radius: 14px; background: var(--surface); box-shadow: inset 0 0 0 1px var(--line); }
|
|
122
|
+
.start-card h3 { margin: 0; }
|
|
123
|
+
.start-card p { margin: 6px 0 12px; }
|
|
124
|
+
.start-card .command { grid-template-columns: minmax(0, 1fr); margin: 0; }
|
|
125
|
+
.start-card button { justify-self: end; }
|
|
126
|
+
.workspace { display: flex; width: 100%; margin-top: 12px; flex: 1; min-height: 0; }
|
|
127
|
+
.workflows { display: flex; flex-direction: row; flex-shrink: 0; gap: 8px; min-width: 0; margin-top: 12px; padding: 6px; overflow-x: auto; overflow-y: hidden; }
|
|
128
|
+
.workflows a { display: grid; flex-shrink: 0; white-space: nowrap; gap: 2px; border-bottom: 3px solid transparent; padding: 10px; color: inherit; text-decoration: none; }
|
|
95
129
|
.workflows a[aria-current="page"] { border-bottom-color: var(--accent); background: var(--surface); border-radius: 8px 8px 0 0; }
|
|
96
130
|
.workflows span, .card small { color: var(--muted); font-size: 12px; }
|
|
97
|
-
.board { min-width: 0; }
|
|
98
|
-
.board h2 { margin: 0 0
|
|
99
|
-
.columns { display:
|
|
100
|
-
|
|
101
|
-
.
|
|
131
|
+
.board { width: 100%; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
|
132
|
+
.board h2 { margin: 0 0 8px; font-size: 24px; flex-shrink: 0; }
|
|
133
|
+
.columns { display: flex; align-items: flex-start; gap: ${String(boardSizing.gap)}px; flex: 1; min-height: 0; overflow-x: scroll; overflow-y: hidden; padding: 6px 6px 12px; scrollbar-color: var(--muted) var(--surface-2); }
|
|
134
|
+
@supports selector(::-webkit-scrollbar) { .columns { scrollbar-color: auto; } }
|
|
135
|
+
.columns::-webkit-scrollbar { height: 12px; }
|
|
136
|
+
.columns::-webkit-scrollbar-track { background: var(--surface-2); }
|
|
137
|
+
.columns::-webkit-scrollbar-thumb { background: var(--muted); border-radius: 6px; }
|
|
138
|
+
.column { flex: 1 0 ${String(boardSizing.minimumColumnWidth)}px; min-width: ${String(boardSizing.minimumColumnWidth)}px; max-height: 100%; display: flex; flex-direction: column; overflow: hidden; padding: 10px; border-radius: 14px; box-shadow: inset 0 0 0 1px var(--line); background: var(--surface-2); }
|
|
139
|
+
.column-name { display: flex; align-items: center; gap: 6px; flex-shrink: 0; margin: 0; padding: 4px 0 10px; color: var(--muted); font: 500 12px/1.5 var(--code-font); }
|
|
140
|
+
.column-name > span:first-child { flex: 1; }
|
|
141
|
+
.column-cards { min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; }
|
|
142
|
+
.column.collapsed { flex: 0 0 ${String(boardSizing.collapsedColumnWidth)}px; min-width: ${String(boardSizing.collapsedColumnWidth)}px; padding: 6px; }
|
|
143
|
+
.collapsed .column-name { writing-mode: vertical-rl; padding: 0; }
|
|
144
|
+
.column-name button, .start-toggle { color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); margin: 0; }
|
|
145
|
+
.column-name button:hover, .start-toggle:hover { background: var(--surface); }
|
|
146
|
+
.add-card, .add-card-form button { color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); margin: 4px 8px 4px 0; }
|
|
147
|
+
.add-card { width: 100%; text-align: left; }
|
|
148
|
+
.add-card-form { padding: 8px 0; overflow-wrap: anywhere; }
|
|
149
|
+
.add-card-form label { display: block; color: var(--ink); margin-bottom: 8px; }
|
|
150
|
+
.add-card-form input, .add-card-form textarea { display: block; width: 100%; min-width: 0; box-sizing: border-box; padding: 8px; border: 1px solid var(--line); border-radius: 6px; font: inherit; color: var(--ink); background: var(--surface); }
|
|
151
|
+
.add-card-form textarea { resize: vertical; }
|
|
152
|
+
.add-card-form button:disabled { opacity: 0.6; cursor: default; }
|
|
153
|
+
.add-card-form p { font-size: 12px; margin: 8px 0; }
|
|
102
154
|
.card { display: grid; gap: 6px; margin-bottom: 10px; padding: 12px; border-radius: 14px; box-shadow: inset 0 0 0 1px var(--line), var(--shadow); color: var(--ink); overflow-wrap: anywhere; background: var(--surface); }
|
|
103
155
|
.card:hover { outline: 1px solid var(--accent-strong); }
|
|
104
|
-
.card-run { display: grid; gap:
|
|
105
|
-
.card-run strong { font-family: var(--heading-font); font-weight: 700; }
|
|
156
|
+
.card-run { display: grid; gap: 4px; min-width: 0; color: inherit; text-decoration: none; }
|
|
157
|
+
.card-run strong { font-family: var(--heading-font); font-weight: 700; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
|
|
106
158
|
.completion-mark { font-size: 12px; }
|
|
107
159
|
.card-step, .card-review, .card-cost, .card small { font: 400 12px/1.6 var(--code-font); font-variant-numeric: tabular-nums; }
|
|
160
|
+
.card-step, .card-review { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
108
161
|
.card-step { color: var(--muted); }
|
|
109
162
|
.card-step.active { color: var(--accent-strong); }
|
|
110
163
|
.card-review { color: var(--muted); }
|
|
@@ -180,84 +233,17 @@ button:hover { background: var(--code-head); }
|
|
|
180
233
|
.events { margin-top: 18px; padding-bottom: 10px; }
|
|
181
234
|
.events > summary { color: var(--code-fg); }
|
|
182
235
|
.down { font-weight: 600; }
|
|
183
|
-
@media (max-width: 700px) { body { padding-inline: 12px; }
|
|
236
|
+
@media (max-width: 700px) { body { padding-inline: 12px; } }
|
|
184
237
|
`;
|
|
185
|
-
|
|
186
|
-
const state = { model: null, stale: false, copied: "", label: "", run: "", loading: false, again: false };
|
|
187
|
-
function moved(next, followed) {
|
|
188
|
-
const arrived = next === "waiting_gate" && next !== state.run;
|
|
189
|
-
state.run = next;
|
|
190
|
-
if (arrived) scrollTo(0, 0);
|
|
191
|
-
else if (followed && next === "running") scrollTo(0, document.body.scrollHeight);
|
|
192
|
-
}
|
|
193
|
-
function copy(command) {
|
|
194
|
-
const shown = (label) => {
|
|
195
|
-
state.copied = command;
|
|
196
|
-
state.label = label;
|
|
197
|
-
draw();
|
|
198
|
-
};
|
|
199
|
-
navigator.clipboard.writeText(command).then(
|
|
200
|
-
() => { shown("Copied"); },
|
|
201
|
-
() => { shown("Copy failed"); },
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
function draw() {
|
|
205
|
-
const { model, stale } = state;
|
|
206
|
-
const selected = location.hash.slice(1);
|
|
207
|
-
const followed = innerHeight + scrollY >= document.body.scrollHeight - 80;
|
|
208
|
-
const copying = { command: state.copied, label: state.label, onCopy: copy };
|
|
209
|
-
const now = new Date().toISOString();
|
|
210
|
-
const page = component(Page, { model, selected, now, stale, copying });
|
|
211
|
-
render(page, document.getElementById("app"));
|
|
212
|
-
if (model === null) return;
|
|
213
|
-
document.title = watchTitle(model, selected);
|
|
214
|
-
const { runId } = watchLocation(selected);
|
|
215
|
-
moved(model.runs.find((run) => run.runId === runId)?.state ?? "", followed);
|
|
216
|
-
}
|
|
217
|
-
async function withSelectedRun(model) {
|
|
218
|
-
const { runId } = watchLocation(location.hash);
|
|
219
|
-
if (runId === null || model.runs.some((run) => run.runId === runId)) return model;
|
|
220
|
-
const detail = await fetch("data.json?runId=" + encodeURIComponent(runId), { cache: "no-store" });
|
|
221
|
-
if (detail.status === 404) return model;
|
|
222
|
-
if (!detail.ok) throw new Error("watch-run-not-available");
|
|
223
|
-
const { run } = await detail.json();
|
|
224
|
-
if (run?.runId !== runId) throw new Error("watch-run-mismatch");
|
|
225
|
-
return { ...model, runs: [...model.runs, run] };
|
|
226
|
-
}
|
|
227
|
-
async function load() {
|
|
228
|
-
state.again = state.loading;
|
|
229
|
-
if (state.loading) return;
|
|
230
|
-
state.loading = true;
|
|
231
|
-
try {
|
|
232
|
-
const reply = await fetch("data.json", { cache: "no-store" });
|
|
233
|
-
if (!reply.ok) throw new Error("watch-not-available");
|
|
234
|
-
state.model = await withSelectedRun(await reply.json());
|
|
235
|
-
state.stale = false;
|
|
236
|
-
} catch {
|
|
237
|
-
state.stale = true;
|
|
238
|
-
}
|
|
239
|
-
state.loading = false;
|
|
240
|
-
draw();
|
|
241
|
-
if (state.again) load();
|
|
242
|
-
}
|
|
243
|
-
function listen() {
|
|
244
|
-
const source = new EventSource("events");
|
|
245
|
-
source.onmessage = () => { load(); };
|
|
246
|
-
source.onerror = () => { state.stale = true; draw(); };
|
|
247
|
-
}
|
|
248
|
-
addEventListener("hashchange", () => { state.run = ""; draw(); load(); });
|
|
249
|
-
draw();
|
|
250
|
-
listen();
|
|
251
|
-
setInterval(draw, 1000);
|
|
252
|
-
setInterval(load, 15000);
|
|
253
|
-
`;
|
|
254
|
-
export function pageDocument() {
|
|
238
|
+
export function pageDocument(actionToken = null) {
|
|
255
239
|
const icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${clove.viewBox}">${clove.paths.map((d) => `<path fill="#0BBF64" d="${d}"/>`).join("")}</svg>`;
|
|
256
240
|
const script = [
|
|
257
241
|
'import { h as element, h as component, render } from "./preact.mjs";',
|
|
258
242
|
`const clove = ${JSON.stringify(clove)};`,
|
|
243
|
+
`const actionToken = ${JSON.stringify(actionToken)};`,
|
|
259
244
|
...functions.map((item) => item.toString()),
|
|
260
|
-
|
|
245
|
+
watchCardClient,
|
|
246
|
+
watchClient,
|
|
261
247
|
].join("\n");
|
|
262
248
|
return [
|
|
263
249
|
"<!doctype html>",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function observedCard(context, card) {
|
|
2
|
+
if (card.completionMark?.text !== "Needs attention")
|
|
3
|
+
return card;
|
|
4
|
+
const run = context.model.runs.find((run) => run.runId === card.runId);
|
|
5
|
+
if (run?.workflowId !== "develop-issue" ||
|
|
6
|
+
run.state !== "completed" ||
|
|
7
|
+
run.outcome !== "blocked" ||
|
|
8
|
+
run.pullRequest === null)
|
|
9
|
+
return card;
|
|
10
|
+
return {
|
|
11
|
+
...card,
|
|
12
|
+
pullRequestObservation: context.observer.observe(run.pullRequest),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function observedBoard(model, observer) {
|
|
16
|
+
return {
|
|
17
|
+
...model,
|
|
18
|
+
workflows: model.workflows.map((workflow) => ({
|
|
19
|
+
...workflow,
|
|
20
|
+
columns: workflow.columns.map((column) => ({
|
|
21
|
+
...column,
|
|
22
|
+
cards: column.cards.map((card) => observedCard({ model, observer }, card)),
|
|
23
|
+
})),
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type PrIdentity, type PrObservation } from "./watch-pr-identity.ts";
|
|
2
|
+
export interface WatchCommandOutput {
|
|
3
|
+
readonly stdout: string;
|
|
4
|
+
readonly stderr: string;
|
|
5
|
+
readonly status: number | null;
|
|
6
|
+
readonly errorCode: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface WatchCommandRequest {
|
|
9
|
+
readonly command: readonly string[];
|
|
10
|
+
readonly directory: string;
|
|
11
|
+
readonly signal: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
export type WatchCommandRunner = (request: WatchCommandRequest) => Promise<WatchCommandOutput>;
|
|
14
|
+
export declare function prArguments(identity: PrIdentity): readonly string[];
|
|
15
|
+
export declare function prCommandResult(identity: PrIdentity, output: WatchCommandOutput): PrObservation;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { prResponse, } from "./watch-pr-identity.js";
|
|
2
|
+
export function prArguments(identity) {
|
|
3
|
+
return [
|
|
4
|
+
"api",
|
|
5
|
+
"--method",
|
|
6
|
+
"GET",
|
|
7
|
+
"--hostname",
|
|
8
|
+
identity.host,
|
|
9
|
+
`repos/${identity.owner}/${identity.repository}/pulls/${String(identity.number)}`,
|
|
10
|
+
"--include",
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
function commandFailure(output) {
|
|
14
|
+
const status = Number(/\(HTTP (\d{3})\)/.exec(output.stderr)?.[1] ??
|
|
15
|
+
/^HTTP\/[^ ]+ (\d{3})/m.exec(output.stdout)?.[1]);
|
|
16
|
+
if (status === 429 ||
|
|
17
|
+
/rate limit/i.test(output.stderr) ||
|
|
18
|
+
/x-ratelimit-remaining: 0\s*$/im.test(output.stdout))
|
|
19
|
+
return "rate-limit";
|
|
20
|
+
if (output.status === 4 || [401, 403, 404].includes(status))
|
|
21
|
+
return "access";
|
|
22
|
+
return "command";
|
|
23
|
+
}
|
|
24
|
+
export function prCommandResult(identity, output) {
|
|
25
|
+
if (output.status !== 0 || output.errorCode !== null)
|
|
26
|
+
return { status: "unavailable", failure: commandFailure(output) };
|
|
27
|
+
const envelope = /^HTTP\/[^ ]+ 200[^\n]*\r?\n[\s\S]*?\r?\n\r?\n([\s\S]*)$/.exec(output.stdout);
|
|
28
|
+
if (envelope?.[1] === undefined)
|
|
29
|
+
return { status: "unavailable", failure: "response" };
|
|
30
|
+
try {
|
|
31
|
+
const response = JSON.parse(envelope[1]);
|
|
32
|
+
return prResponse(identity, response);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { status: "unavailable", failure: "response" };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PrIdentity {
|
|
2
|
+
readonly host: string;
|
|
3
|
+
readonly owner: string;
|
|
4
|
+
readonly repository: string;
|
|
5
|
+
readonly number: number;
|
|
6
|
+
}
|
|
7
|
+
export type PrFailure = "configuration" | "identity" | "command" | "access" | "rate-limit" | "response" | "timeout";
|
|
8
|
+
export type PrObservation = {
|
|
9
|
+
readonly status: "merged" | "unmerged";
|
|
10
|
+
} | {
|
|
11
|
+
readonly status: "unavailable";
|
|
12
|
+
readonly failure: PrFailure;
|
|
13
|
+
};
|
|
14
|
+
export declare function prResponse(identity: PrIdentity, value: unknown): PrObservation;
|
|
15
|
+
export type PrRepository = Omit<PrIdentity, "number">;
|
|
16
|
+
export declare function prRepository(origin: string): PrRepository | null;
|
|
17
|
+
export declare function prIdentity(repository: PrRepository, pullRequest: {
|
|
18
|
+
readonly number: number;
|
|
19
|
+
readonly url: string;
|
|
20
|
+
}): PrIdentity | null;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { record } from "../protocol.js";
|
|
2
|
+
function responseNames(fields) {
|
|
3
|
+
const repository = record(record(fields["base"])?.["repo"]) ?? {};
|
|
4
|
+
return [
|
|
5
|
+
fields["html_url"],
|
|
6
|
+
repository["html_url"],
|
|
7
|
+
repository["full_name"],
|
|
8
|
+
repository["name"],
|
|
9
|
+
record(repository["owner"])?.["login"],
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
export function prResponse(identity, value) {
|
|
13
|
+
const fields = record(value) ?? {};
|
|
14
|
+
const expected = `https://${identity.host}/${identity.owner}/${identity.repository}`;
|
|
15
|
+
const strings = responseNames(fields);
|
|
16
|
+
if (typeof fields["number"] !== "number" ||
|
|
17
|
+
strings.some((part) => typeof part !== "string"))
|
|
18
|
+
return { status: "unavailable", failure: "response" };
|
|
19
|
+
const wanted = [
|
|
20
|
+
`${expected}/pull/${String(identity.number)}`,
|
|
21
|
+
expected,
|
|
22
|
+
`${identity.owner}/${identity.repository}`,
|
|
23
|
+
identity.repository,
|
|
24
|
+
identity.owner,
|
|
25
|
+
];
|
|
26
|
+
if (fields["number"] !== identity.number ||
|
|
27
|
+
strings.some((part, index) => String(part).toLowerCase() !== String(wanted[index]).toLowerCase()))
|
|
28
|
+
return { status: "unavailable", failure: "identity" };
|
|
29
|
+
const merged = fields["merged"];
|
|
30
|
+
if (typeof merged !== "boolean")
|
|
31
|
+
return { status: "unavailable", failure: "response" };
|
|
32
|
+
return { status: merged ? "merged" : "unmerged" };
|
|
33
|
+
}
|
|
34
|
+
export function prRepository(origin) {
|
|
35
|
+
const address = origin
|
|
36
|
+
.replace(/^git@([^:/]+):/, "ssh://git@$1/")
|
|
37
|
+
.replace(/\r?\n$/, "");
|
|
38
|
+
const matched = /^(?:https:\/\/|ssh:\/\/git@)([a-zA-Z0-9.-]+)\/([a-zA-Z0-9-]+)\/([a-zA-Z0-9_.-]+)$/.exec(address);
|
|
39
|
+
const [, host, owner, name] = matched ?? [];
|
|
40
|
+
if (host === undefined || owner === undefined || name === undefined)
|
|
41
|
+
return null;
|
|
42
|
+
if (host
|
|
43
|
+
.split(".")
|
|
44
|
+
.some((label) => !/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label)))
|
|
45
|
+
return null;
|
|
46
|
+
const repository = name.replace(/\.git$/, "");
|
|
47
|
+
if (![host, owner].every((part) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(part)) ||
|
|
48
|
+
["", ".", ".."].includes(repository))
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
host: host.toLowerCase(),
|
|
52
|
+
owner: owner.toLowerCase(),
|
|
53
|
+
repository: repository.toLowerCase(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function prIdentity(repository, pullRequest) {
|
|
57
|
+
const { number, url } = pullRequest;
|
|
58
|
+
if (!Number.isSafeInteger(number) || number <= 0)
|
|
59
|
+
return null;
|
|
60
|
+
const expected = `https://${repository.host}/${repository.owner}/${repository.repository}/pull/${String(number)}`;
|
|
61
|
+
return url.toLowerCase() === expected ? { ...repository, number } : null;
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WatchPullRequest } from "./watch-render.ts";
|
|
2
|
+
import { type WatchCommandRunner } from "./watch-pr-command.ts";
|
|
3
|
+
import { type PrObservation, type PrRepository } from "./watch-pr-identity.ts";
|
|
4
|
+
export interface PrObserver {
|
|
5
|
+
readonly observe: (pullRequest: WatchPullRequest) => PrObservation | null;
|
|
6
|
+
readonly close: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface PrClock {
|
|
9
|
+
readonly now: () => number;
|
|
10
|
+
readonly after: (expired: () => void, milliseconds: number) => () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface PrObserverSettings {
|
|
13
|
+
readonly clock?: PrClock;
|
|
14
|
+
readonly repository: PrRepository | null;
|
|
15
|
+
readonly directory: string;
|
|
16
|
+
readonly command: readonly string[] | null;
|
|
17
|
+
readonly execute: WatchCommandRunner;
|
|
18
|
+
}
|
|
19
|
+
export declare function prObserver(settings: PrObserverSettings): PrObserver;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { prArguments, prCommandResult, } from "./watch-pr-command.js";
|
|
2
|
+
import { prIdentity, } from "./watch-pr-identity.js";
|
|
3
|
+
const systemClock = {
|
|
4
|
+
now: Date.now,
|
|
5
|
+
after: (expired, milliseconds) => {
|
|
6
|
+
const timer = setTimeout(expired, milliseconds);
|
|
7
|
+
return () => {
|
|
8
|
+
clearTimeout(timer);
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
async function readPr(state, attempt) {
|
|
13
|
+
const { entry, controller } = attempt;
|
|
14
|
+
const deadline = { expired: false };
|
|
15
|
+
const timeout = { status: "unavailable", failure: "timeout" };
|
|
16
|
+
const cancel = state.clock.after(() => {
|
|
17
|
+
deadline.expired = true;
|
|
18
|
+
entry.result = timeout;
|
|
19
|
+
controller.abort();
|
|
20
|
+
}, 5000);
|
|
21
|
+
controller.signal.addEventListener("abort", cancel, { once: true });
|
|
22
|
+
try {
|
|
23
|
+
const output = await Promise.resolve().then(() => state.settings.execute({
|
|
24
|
+
command: [
|
|
25
|
+
...(state.settings.command ?? []),
|
|
26
|
+
...prArguments(entry.identity),
|
|
27
|
+
],
|
|
28
|
+
directory: state.settings.directory,
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
}));
|
|
31
|
+
return deadline.expired ? timeout : prCommandResult(entry.identity, output);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return deadline.expired
|
|
35
|
+
? timeout
|
|
36
|
+
: { status: "unavailable", failure: "command" };
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
cancel();
|
|
40
|
+
controller.signal.removeEventListener("abort", cancel);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function pump(state) {
|
|
44
|
+
if (state.closed || state.active !== null)
|
|
45
|
+
return;
|
|
46
|
+
const entry = state.queue.shift();
|
|
47
|
+
if (entry === undefined)
|
|
48
|
+
return;
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
state.active = controller;
|
|
51
|
+
entry.started = state.clock.now();
|
|
52
|
+
const completed = (result) => {
|
|
53
|
+
entry.result = result;
|
|
54
|
+
entry.pending = false;
|
|
55
|
+
state.active = null;
|
|
56
|
+
pump(state);
|
|
57
|
+
};
|
|
58
|
+
readPr(state, { entry, controller }).then(completed, () => {
|
|
59
|
+
completed({ status: "unavailable", failure: "command" });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function needsRead(entry, now) {
|
|
63
|
+
if (entry === undefined)
|
|
64
|
+
return true;
|
|
65
|
+
if (entry.pending || entry.result?.status === "merged")
|
|
66
|
+
return false;
|
|
67
|
+
return now - entry.started >= 60000;
|
|
68
|
+
}
|
|
69
|
+
export function prObserver(settings) {
|
|
70
|
+
const state = {
|
|
71
|
+
settings,
|
|
72
|
+
clock: settings.clock ?? systemClock,
|
|
73
|
+
cache: new Map(),
|
|
74
|
+
queue: [],
|
|
75
|
+
active: null,
|
|
76
|
+
closed: false,
|
|
77
|
+
};
|
|
78
|
+
const configured = settings.repository !== null && Boolean(settings.command?.[0]?.trim());
|
|
79
|
+
const observe = (pullRequest) => {
|
|
80
|
+
if (!configured)
|
|
81
|
+
return { status: "unavailable", failure: "configuration" };
|
|
82
|
+
const identity = prIdentity(settings.repository, pullRequest);
|
|
83
|
+
if (identity === null)
|
|
84
|
+
return { status: "unavailable", failure: "identity" };
|
|
85
|
+
const key = JSON.stringify(identity);
|
|
86
|
+
const known = state.cache.get(key);
|
|
87
|
+
if (state.closed || !needsRead(known, state.clock.now()))
|
|
88
|
+
return known?.result ?? null;
|
|
89
|
+
const entry = {
|
|
90
|
+
identity,
|
|
91
|
+
result: known?.result ?? null,
|
|
92
|
+
started: Infinity,
|
|
93
|
+
pending: true,
|
|
94
|
+
};
|
|
95
|
+
state.cache.set(key, entry);
|
|
96
|
+
state.queue.push(entry);
|
|
97
|
+
pump(state);
|
|
98
|
+
return entry.result;
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
observe,
|
|
102
|
+
close: () => {
|
|
103
|
+
state.closed = true;
|
|
104
|
+
state.queue.length = 0;
|
|
105
|
+
state.active?.abort();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WatchCommandRunner } from "./watch-pr-command.ts";
|
|
2
|
+
import { type PrObserver } from "./watch-pr-observer.ts";
|
|
3
|
+
export interface WatchPrSettings {
|
|
4
|
+
readonly repository: string | undefined;
|
|
5
|
+
readonly githubCommand: string | undefined;
|
|
6
|
+
readonly execute: WatchCommandRunner;
|
|
7
|
+
}
|
|
8
|
+
export declare function watchPrObserver(settings: WatchPrSettings): Promise<PrObserver>;
|