toolroll 0.9.85 → 0.9.87
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/CHANGELOG.md +83 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.js +4 -3
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -68
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +363 -177
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.js +1 -0
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +4 -1
- package/dist/contracts/console-api.js +4 -1
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +21 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/coordinator.js +1 -0
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/deploy-gate.js +4 -0
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +54 -5
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +152 -32
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -6
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-flow.js +35 -4
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +21 -0
- package/dist/release.js +40 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/result-follow-ups.js +4 -0
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +8 -0
- package/dist/scope.js +13 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +20 -18
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +31 -2
- package/dist/server/flows.js +79 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +35 -190
- package/dist/server/render-pages.js +154 -563
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +43 -5
- package/dist/server/render-tasks.js +131 -91
- package/dist/server/route-table.js +31 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +90 -2
- package/dist/server/tasks.d.ts +18 -0
- package/dist/server/tasks.js +148 -41
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +182 -5
- package/dist/store.js +512 -38
- package/dist/surface.js +23 -3
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.d.ts +24 -0
- package/dist/work-index.js +56 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +8 -4
- package/package.json +1 -1
package/dist/flow-triggers.js
CHANGED
|
@@ -49,6 +49,7 @@ import { readFlowSecrets } from "./flow-secrets.js";
|
|
|
49
49
|
import { prepareUnattendedCandidates, grantSignsScriptOf, scriptProvenanceOf, scriptRanDigestOf } from "./flow-unattended.js";
|
|
50
50
|
import { tmpdir } from "node:os";
|
|
51
51
|
import { envValue } from "./names.js";
|
|
52
|
+
import { recordBranchCheckRun, unprovenNoticeRuns } from "./tidy.js";
|
|
52
53
|
import { linearLabelNames, readFormSubmission, readGithubEvent, readGithubIssue, readGithubIssueEventList, readGithubIssueList, readGithubRun, readGithubRunList, readInboundMail, readLinearAnswer, readLinearEvent, readLinearIssue, readWebhookPayload } from "./contracts/trigger-payloads.js";
|
|
53
54
|
import { isRecord } from "./is-record.js";
|
|
54
55
|
export { FLOW_TRIGGER_KINDS };
|
|
@@ -925,6 +926,9 @@ async function checkTrigger(store, trigger, config, now, io) {
|
|
|
925
926
|
store.updateFlowTrigger(trigger.id, { failures, lastAt: now.toISOString(), lastOutcome: fetched.problem, nextAt: new Date(now.getTime() + BACKOFF_MS[Math.min(failures, BACKOFF_MS.length) - 1]).toISOString() }, now);
|
|
926
927
|
return { ok: false, said: fetched.problem, added: 0 };
|
|
927
928
|
}
|
|
929
|
+
// Tidy's CI evidence (task-tidy.md): the watched branch's completed runs, green and red, beside the failures it carded.
|
|
930
|
+
if (config.kind === "github" && config.watch === "checks")
|
|
931
|
+
await observeBranchRuns(store, trigger, config, io.gh, now);
|
|
928
932
|
const by = config.kind === "github" ? "GitHub" : config.kind === "linear" ? "Linear" : "Email";
|
|
929
933
|
// Known items are left out before the batch is taken (v126): a page of things already received can't keep the unseen
|
|
930
934
|
// ones behind it waiting.
|
|
@@ -1206,6 +1210,14 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
1206
1210
|
: config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body.pull_request, "pull")
|
|
1207
1211
|
: config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body.workflow_run) : null;
|
|
1208
1212
|
items = found === null ? [] : [found];
|
|
1213
|
+
// A signed workflow run on the watched branch is tidy's CI evidence too (task-tidy.md), green or red: recorded only
|
|
1214
|
+
// inside `written`, which re-proves the trigger is still active with the same config, credential and address.
|
|
1215
|
+
if (config.watch === "checks" && config.branch !== null && event === "workflow_run" && action === "completed") {
|
|
1216
|
+
const branch = config.branch;
|
|
1217
|
+
const recorded = written(() => { recordBranchCheckRun(store, { repo: config.repo, branch }, readGithubRun(body.workflow_run), now); return { status: 200, said: "Recorded." }; });
|
|
1218
|
+
if (recorded.status !== 200)
|
|
1219
|
+
return recorded;
|
|
1220
|
+
}
|
|
1209
1221
|
}
|
|
1210
1222
|
else if (config.kind === "linear") {
|
|
1211
1223
|
const body = readLinearEvent(payload), sent = Number(body.webhookTimestamp);
|
|
@@ -1648,3 +1660,46 @@ export function receiveFlowForm(store, token, fields, now) {
|
|
|
1648
1660
|
store.updateFlowTrigger(trigger.id, { lastAt: now.toISOString(), lastOutcome: "Someone sent the form." }, now);
|
|
1649
1661
|
return { status: 200, html: thanks };
|
|
1650
1662
|
}
|
|
1663
|
+
/** Tidy's CI evidence (docs/plans/task-tidy.md): the watched branch's recent completed workflow runs, read through the
|
|
1664
|
+
* person's own gh login. Recorded only while the trigger still watches exactly that repository and branch once GitHub
|
|
1665
|
+
* answered; any failure records nothing (tidy then keeps the notices). */
|
|
1666
|
+
async function observeBranchRuns(store, trigger, config, gh, now) {
|
|
1667
|
+
if (config.branch === null)
|
|
1668
|
+
return;
|
|
1669
|
+
const branch = config.branch;
|
|
1670
|
+
let result;
|
|
1671
|
+
try {
|
|
1672
|
+
result = await gh("gh", ["api", "-H", "Accept: application/vnd.github+json", `repos/${config.repo}/actions/runs?branch=${encodeURIComponent(branch)}&status=completed&per_page=50`], { timeoutMs: 20_000 });
|
|
1673
|
+
}
|
|
1674
|
+
catch {
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (result.code !== 0)
|
|
1678
|
+
return;
|
|
1679
|
+
let body;
|
|
1680
|
+
try {
|
|
1681
|
+
body = JSON.parse(result.stdout);
|
|
1682
|
+
}
|
|
1683
|
+
catch {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const runs = [...readGithubRunList(body).workflow_runs ?? []];
|
|
1687
|
+
// A few older failure cards' own runs, so tidy can tell whether each was a push to this branch (task-tidy.md).
|
|
1688
|
+
for (const id of unprovenNoticeRuns(store, trigger.id, config.repo)) {
|
|
1689
|
+
try {
|
|
1690
|
+
const one = await gh("gh", ["api", "-H", "Accept: application/vnd.github+json", `repos/${config.repo}/actions/runs/${id}`], { timeoutMs: 20_000 });
|
|
1691
|
+
if (one.code === 0)
|
|
1692
|
+
runs.push(readGithubRun(JSON.parse(one.stdout)));
|
|
1693
|
+
}
|
|
1694
|
+
catch { /* read again next time */ }
|
|
1695
|
+
}
|
|
1696
|
+
// Re-proved and recorded in one transaction: a trigger paused or changed meanwhile records nothing.
|
|
1697
|
+
store.transact(() => {
|
|
1698
|
+
const current = store.getFlowTrigger(trigger.id);
|
|
1699
|
+
const still = current === null ? null : triggerConfigOf(current);
|
|
1700
|
+
if (current === null || current.state !== "active" || current.configJson !== trigger.configJson || still === null || still.kind !== "github" || still.watch !== "checks" || still.repo !== config.repo || still.branch !== branch)
|
|
1701
|
+
return;
|
|
1702
|
+
for (const run of runs)
|
|
1703
|
+
recordBranchCheckRun(store, { repo: config.repo, branch }, run, now);
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
/** The words a "runs on its own" grant signs (flow-unattended.ts), kept free of server imports so the flow page's
|
|
2
2
|
* signing form says each limit exactly as the signature will. */
|
|
3
3
|
export declare const dollarWords: (micro: number) => string;
|
|
4
|
+
/** The most runs a day a grant may allow. */
|
|
5
|
+
export declare const UNATTENDED_MAX_RUNS = 50;
|
|
6
|
+
/** The largest per-attempt budget a grant may name: $100. */
|
|
7
|
+
export declare const UNATTENDED_MAX_BUDGET_MICROUSD = 100000000;
|
|
8
|
+
/** A per-attempt budget as typed ("5", "$7.50", "" for no cap), the one way the server and the flow page read it. */
|
|
9
|
+
export declare function unattendedBudgetOf(typed: string): {
|
|
10
|
+
ok: true;
|
|
11
|
+
value: number | null;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
/** Runs a day as typed ("3", "" for no limit), the one way the server and the flow page read it. */
|
|
17
|
+
export declare function unattendedRunsOf(typed: string): {
|
|
18
|
+
ok: true;
|
|
19
|
+
value: number | null;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
4
24
|
/** "Oct 19": the end's day, where space is short (the exact time goes beside it, on hover). */
|
|
5
25
|
export declare const untilDayWords: (iso: string) => string;
|
|
6
26
|
/** "Oct 19, 14:05 UTC": the expiry as every surface says it. */
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
/** The words a "runs on its own" grant signs (flow-unattended.ts), kept free of server imports so the flow page's
|
|
2
2
|
* signing form says each limit exactly as the signature will. */
|
|
3
3
|
export const dollarWords = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
|
|
4
|
+
/** The most runs a day a grant may allow. */
|
|
5
|
+
export const UNATTENDED_MAX_RUNS = 50;
|
|
6
|
+
/** The largest per-attempt budget a grant may name: $100. */
|
|
7
|
+
export const UNATTENDED_MAX_BUDGET_MICROUSD = 100_000_000;
|
|
8
|
+
/** A per-attempt budget as typed ("5", "$7.50", "" for no cap), the one way the server and the flow page read it. */
|
|
9
|
+
export function unattendedBudgetOf(typed) {
|
|
10
|
+
const text = typed.trim().replace(/^\$/, "");
|
|
11
|
+
if (text === "")
|
|
12
|
+
return { ok: true, value: null };
|
|
13
|
+
const value = Number(text);
|
|
14
|
+
if (!/^\d+(\.\d{1,2})?$/.test(text) || value <= 0 || Math.round(value * 1_000_000) > UNATTENDED_MAX_BUDGET_MICROUSD)
|
|
15
|
+
return { ok: false, message: `Give the budget in dollars, up to ${dollarWords(UNATTENDED_MAX_BUDGET_MICROUSD)}, or leave it empty.` };
|
|
16
|
+
return { ok: true, value: Math.round(value * 1_000_000) };
|
|
17
|
+
}
|
|
18
|
+
/** Runs a day as typed ("3", "" for no limit), the one way the server and the flow page read it. */
|
|
19
|
+
export function unattendedRunsOf(typed) {
|
|
20
|
+
const text = typed.trim();
|
|
21
|
+
if (text === "")
|
|
22
|
+
return { ok: true, value: null };
|
|
23
|
+
const value = Number(text);
|
|
24
|
+
if (!/^\d+$/.test(text) || !Number.isInteger(value) || value < 1 || value > UNATTENDED_MAX_RUNS)
|
|
25
|
+
return { ok: false, message: `Give runs a day as 1 to ${UNATTENDED_MAX_RUNS}, or leave it empty.` };
|
|
26
|
+
return { ok: true, value };
|
|
27
|
+
}
|
|
4
28
|
/** "Oct 19": the end's day, where space is short (the exact time goes beside it, on hover). */
|
|
5
29
|
export const untilDayWords = (iso) => new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" }).format(new Date(iso));
|
|
6
30
|
/** "Oct 19, 14:05 UTC": the expiry as every surface says it. */
|
|
@@ -8,10 +8,8 @@ export declare const FLOW_ACCEPTANCE: {
|
|
|
8
8
|
report: AcceptanceCriterion[];
|
|
9
9
|
task: AcceptanceCriterion[];
|
|
10
10
|
};
|
|
11
|
-
/** The
|
|
12
|
-
export
|
|
13
|
-
/** The largest per-attempt budget a grant may name: $100. */
|
|
14
|
-
export declare const UNATTENDED_MAX_BUDGET_MICROUSD = 100000000;
|
|
11
|
+
/** The limits a grant may name, and their one parser, live with its words (the flow page validates with them too). */
|
|
12
|
+
export { UNATTENDED_MAX_BUDGET_MICROUSD, UNATTENDED_MAX_RUNS } from "./flow-unattended-words.js";
|
|
15
13
|
/** The code a script schedule runs, as signed: the script's version and content digest, and for one that runs a project
|
|
16
14
|
* file, that file's path and, sorted by path, the digest of its bytes and of every project file it statically imports. */
|
|
17
15
|
export type SignedScript = {
|
|
@@ -77,7 +75,7 @@ export type UnattendedGrant = {
|
|
|
77
75
|
* revision signed, and neither project protected. Shared by every dispatch belt. */
|
|
78
76
|
export declare const grantStandsSql: (digest: string, now: string, repo: string) => string;
|
|
79
77
|
/** Why a grant ended, as its row and ledger line say it. A renewal's ledger line is the new signature's. */
|
|
80
|
-
export type UnattendedEnd = "expired" | "revoked" | "renewed" | "schedule changed" | "script changed" | "project protected" | "access changed";
|
|
78
|
+
export type UnattendedEnd = "expired" | "revoked" | "renewed" | "flow changed" | "schedule changed" | "script changed" | "project protected" | "access changed";
|
|
81
79
|
/**
|
|
82
80
|
* The one road for what ends grants from outside this module (store.ts calls it): a flow's schedule added, paused,
|
|
83
81
|
* resumed or removed; a project's approval rules protecting work; an account's access or standing changing. Every live
|
|
@@ -222,6 +220,12 @@ export type UnattendedView = {
|
|
|
222
220
|
optional: string | null;
|
|
223
221
|
}[];
|
|
224
222
|
schedulesDigest: string;
|
|
223
|
+
/** No live grant, and the latest one was ended by a change (the flow, a script, a schedule, the project's rules or
|
|
224
|
+
* the signer's access) before it would have run out: why, and when, so the page says it no longer runs on its own. */
|
|
225
|
+
ended: {
|
|
226
|
+
why: string;
|
|
227
|
+
at: string;
|
|
228
|
+
} | null;
|
|
225
229
|
};
|
|
226
230
|
export declare function unattendedViewOf(store: Store, flow: FlowRow, viewer: {
|
|
227
231
|
name: string;
|
package/dist/flow-unattended.js
CHANGED
|
@@ -31,7 +31,7 @@ import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
|
31
31
|
import { createRequire } from "node:module";
|
|
32
32
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
33
33
|
import { isProtectedWork } from "./approval-policy.js";
|
|
34
|
-
import { dollarWords as dollars, scheduleWords, UNATTENDED_RULE_WORDS, unattendedCoverWords, unattendedEndWords, unattendedHeadWords, unattendedLimitWords, untilWords } from "./flow-unattended-words.js";
|
|
34
|
+
import { dollarWords as dollars, scheduleWords, unattendedBudgetOf, unattendedRunsOf, UNATTENDED_RULE_WORDS, unattendedCoverWords, unattendedEndWords, unattendedHeadWords, unattendedLimitWords, untilWords } from "./flow-unattended-words.js";
|
|
35
35
|
import { MODE_MAX_DAYS } from "./modes.js";
|
|
36
36
|
import { accountReachesSql } from "./project-access.js";
|
|
37
37
|
import { canonicalJson } from "./canonical-json.js";
|
|
@@ -70,10 +70,8 @@ export const FLOW_ACCEPTANCE = {
|
|
|
70
70
|
report: [{ id: "c1", statement: "The report answers the request, citing where in the repository each point comes from", how: null, evidence: ["manual-review"] }],
|
|
71
71
|
task: [{ id: "c1", statement: "The requested change is made and the project's existing checks still pass", how: null, evidence: ["manual-review"] }],
|
|
72
72
|
};
|
|
73
|
-
/** The
|
|
74
|
-
export
|
|
75
|
-
/** The largest per-attempt budget a grant may name: $100. */
|
|
76
|
-
export const UNATTENDED_MAX_BUDGET_MICROUSD = 100_000_000;
|
|
73
|
+
/** The limits a grant may name, and their one parser, live with its words (the flow page validates with them too). */
|
|
74
|
+
export { UNATTENDED_MAX_BUDGET_MICROUSD, UNATTENDED_MAX_RUNS } from "./flow-unattended-words.js";
|
|
77
75
|
export const unattendedTermsJson = (terms) => canonicalJson(terms);
|
|
78
76
|
/** sha256 over a domain-separated canonical encoding, 32 hex like the mode digest. */
|
|
79
77
|
export function unattendedDigestOf(terms) {
|
|
@@ -213,10 +211,11 @@ function closeGrant(store, row, by, reason, now) {
|
|
|
213
211
|
export function endUnattendedGrants(store, filter, by, reason, now) {
|
|
214
212
|
const rows = "flow" in filter ? store.handle.prepare("SELECT * FROM flow_unattended_grant WHERE flow = ? AND revoked_at IS NULL").all(filter.flow)
|
|
215
213
|
: "signer" in filter ? store.handle.prepare("SELECT * FROM flow_unattended_grant WHERE signed_by = ? AND revoked_at IS NULL").all(filter.signer)
|
|
216
|
-
// A script saved or removed: the grants in its project that signed a schedule running it
|
|
214
|
+
// A script saved or removed: the grants in its project that signed a schedule running it, or whose flow has a step
|
|
215
|
+
// that runs it (pre-authorized work would run the new code unseen); an unreadable one too, to be safe.
|
|
217
216
|
: "script" in filter ? store.handle.prepare("SELECT * FROM flow_unattended_grant WHERE repo = ? AND revoked_at IS NULL").all(filter.script.repo).filter(row => {
|
|
218
217
|
const terms = unattendedTermsFromJson(String(row["terms_json"]));
|
|
219
|
-
return terms === null || terms.triggers.some(one => one.script?.name === filter.script.name);
|
|
218
|
+
return terms === null || terms.triggers.some(one => one.script?.name === filter.script.name) || flowStepRunsScript(store, Number(row["flow"]), filter.script.name);
|
|
220
219
|
})
|
|
221
220
|
// A project's rules reach the flows in it and the flows whose build zones work in it.
|
|
222
221
|
: store.handle.prepare(`SELECT ug.* FROM flow_unattended_grant ug JOIN flow f ON f.id = ug.flow WHERE ug.revoked_at IS NULL
|
|
@@ -605,22 +604,13 @@ export function parseUnattendedAsk(input) {
|
|
|
605
604
|
const days = Number(text(input.days));
|
|
606
605
|
if (!Number.isInteger(days) || days < 1 || days > MODE_MAX_DAYS)
|
|
607
606
|
return { ok: false, message: `Choose 1 to ${MODE_MAX_DAYS} days: it always ends.` };
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
const runsText = text(input.runs);
|
|
617
|
-
let dailyRunCap = null;
|
|
618
|
-
if (runsText !== "") {
|
|
619
|
-
const value = Number(runsText);
|
|
620
|
-
if (!Number.isInteger(value) || value < 1 || value > UNATTENDED_MAX_RUNS)
|
|
621
|
-
return { ok: false, message: `Give runs a day as 1 to ${UNATTENDED_MAX_RUNS}, or leave it empty.` };
|
|
622
|
-
dailyRunCap = value;
|
|
623
|
-
}
|
|
607
|
+
const budget = unattendedBudgetOf(text(input.budget));
|
|
608
|
+
if (!budget.ok)
|
|
609
|
+
return budget;
|
|
610
|
+
const runs = unattendedRunsOf(text(input.runs));
|
|
611
|
+
if (!runs.ok)
|
|
612
|
+
return runs;
|
|
613
|
+
const budgetMicrousd = budget.value, dailyRunCap = runs.value;
|
|
624
614
|
const includeText = Array.isArray(input.include) ? input.include.map(text).join(",") : text(input.include);
|
|
625
615
|
const include = includeText === "" ? [] : includeText.split(/[\s,]+/).filter(one => one !== "").map(Number);
|
|
626
616
|
if (include.some(id => !Number.isSafeInteger(id) || id < 1))
|
|
@@ -772,5 +762,37 @@ export function unattendedViewOf(store, flow, viewer, now) {
|
|
|
772
762
|
terms: { head: unattendedHeadWords(viewer.name, FLOW_ACCEPTANCE), rules: [...UNATTENDED_RULE_WORDS] },
|
|
773
763
|
schedules: candidates.map(one => ({ id: one.id, words: one.words, optional: one.optional })),
|
|
774
764
|
schedulesDigest: candidatesDigestOf(candidates),
|
|
765
|
+
ended: live === null ? unattendedEndedOf(store, flow.id, now) : null,
|
|
775
766
|
};
|
|
776
767
|
}
|
|
768
|
+
/** Why the flow's latest grant ended early, in words, when a change ended it while it would still have run. */
|
|
769
|
+
const ENDED_WORDS = {
|
|
770
|
+
"flow changed": "the flow changed", "script changed": "a script it runs changed", "schedule changed": "a schedule changed",
|
|
771
|
+
"project protected": "the project's approval rules changed", "access changed": "its signer's access changed",
|
|
772
|
+
};
|
|
773
|
+
function unattendedEndedOf(store, flowId, now) {
|
|
774
|
+
const row = store.handle.prepare("SELECT revoked_at, revoke_reason, absolute_expiry FROM flow_unattended_grant WHERE flow = ? ORDER BY id DESC LIMIT 1").get(flowId);
|
|
775
|
+
if (row === undefined || row["revoked_at"] == null)
|
|
776
|
+
return null;
|
|
777
|
+
const why = ENDED_WORDS[String(row["revoke_reason"])];
|
|
778
|
+
return why === undefined || Date.parse(String(row["absolute_expiry"])) <= now.getTime() ? null : { why, at: String(row["revoked_at"]) };
|
|
779
|
+
}
|
|
780
|
+
/** Whether one of the flow's "Run a script" steps runs this script. A drawing that isn't readable and well formed counts,
|
|
781
|
+
* to be safe. */
|
|
782
|
+
function flowStepRunsScript(store, flowId, name) {
|
|
783
|
+
const flow = store.getFlow(flowId);
|
|
784
|
+
if (flow === null)
|
|
785
|
+
return false;
|
|
786
|
+
let drawn;
|
|
787
|
+
try {
|
|
788
|
+
drawn = JSON.parse(flow.definitionJson);
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
// Anything but a well-formed drawing (an object whose stages are a list of objects) fails closed: the grant ends.
|
|
794
|
+
const stages = drawn !== null && typeof drawn === "object" && !Array.isArray(drawn) ? drawn.stages : undefined;
|
|
795
|
+
if (!Array.isArray(stages) || !stages.every(stage => stage !== null && typeof stage === "object" && !Array.isArray(stage)))
|
|
796
|
+
return true;
|
|
797
|
+
return stages.some(stage => stage.kind === "check" && stage.script === name);
|
|
798
|
+
}
|
package/dist/flows-cli.d.ts
CHANGED
|
@@ -296,6 +296,25 @@ export declare const FLOWS_DESCRIPTORS: readonly [{
|
|
|
296
296
|
readonly takesValue: true;
|
|
297
297
|
readonly meaning: 'also cover these script or plane-review schedules (trigger ids, like 12,14); plain schedules are covered already';
|
|
298
298
|
}];
|
|
299
|
+
}, {
|
|
300
|
+
readonly action: 'browser';
|
|
301
|
+
readonly synopsis: "let a flow's script zones marked browser: true start Chromium in their sandbox (on: a person approves with their own password, previews until --yes; off: any approver revokes)";
|
|
302
|
+
readonly mutation: 'identity-idempotent';
|
|
303
|
+
readonly audience: 'operator';
|
|
304
|
+
readonly positionals: readonly ['flow', 'on|off'];
|
|
305
|
+
readonly flags: readonly [...{
|
|
306
|
+
name: string;
|
|
307
|
+
takesValue: boolean;
|
|
308
|
+
meaning: string;
|
|
309
|
+
}[], {
|
|
310
|
+
name: string;
|
|
311
|
+
takesValue: boolean;
|
|
312
|
+
meaning: string;
|
|
313
|
+
}, {
|
|
314
|
+
readonly name: 'digest';
|
|
315
|
+
readonly takesValue: true;
|
|
316
|
+
readonly meaning: "required with --yes: the approval digest the preview showed; refuses if the flow or its scripts changed since";
|
|
317
|
+
}];
|
|
299
318
|
}, {
|
|
300
319
|
readonly action: 'archive';
|
|
301
320
|
readonly synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes';
|
package/dist/flows-cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { STARTER_FLOWS, starterFlowOf, starterOf, startersFor, starterTerms, swi
|
|
|
19
19
|
import { addFlowTriggerTo, checkFlowTriggerNow, describeTrigger, removeFlowTrigger, setFlowTriggerOn, triggerConfigOf, validateTriggerConfig, rawTakesDeliveries } from './flow-triggers.js';
|
|
20
20
|
import { deciderOf, FLOW_KIND_WORDS, FLOW_TEMPLATES, flowDigest, flowFromSteps, flowTerms, stepsFor } from './flows.js';
|
|
21
21
|
import { prepareUnattendedCandidates, activeUnattendedGrant, parseUnattendedAsk, revokeUnattended, signUnattended, unattendedViewOf, unattendedWords, untilWords, UNATTENDED_MAX_RUNS } from './flow-unattended.js';
|
|
22
|
+
import { browserApprovalDigest, browserTerms, browserTermsOf, browserViewOf, revokeFlowBrowser, signFlowBrowser } from './flow-browser.js';
|
|
22
23
|
import { MODE_MAX_DAYS } from './modes.js';
|
|
23
24
|
import { verifyApproverByPassword } from './principal.js';
|
|
24
25
|
import { authenticateApprover, proveCredential } from './scope.js';
|
|
@@ -71,6 +72,8 @@ export const FLOWS_DESCRIPTORS = [
|
|
|
71
72
|
{ name: 'budget', takesValue: true, meaning: 'optional dollar cap per attempt, like 5 or 2.50' },
|
|
72
73
|
{ name: 'runs', takesValue: true, meaning: `optional limit on runs a day (UTC), 1 to ${UNATTENDED_MAX_RUNS}` },
|
|
73
74
|
{ name: 'include', takesValue: true, meaning: 'also cover these script or plane-review schedules (trigger ids, like 12,14); plain schedules are covered already' }] },
|
|
75
|
+
{ action: 'browser', synopsis: "let a flow's script zones marked browser: true start Chromium in their sandbox (on: a person approves with their own password, previews until --yes; off: any approver revokes)", mutation: 'identity-idempotent', audience: 'operator', positionals: ['flow', 'on|off'], flags: [...write, yes,
|
|
76
|
+
{ name: 'digest', takesValue: true, meaning: "required with --yes: the approval digest the preview showed; refuses if the flow or its scripts changed since" }] },
|
|
74
77
|
{ action: 'archive', synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes', mutation: 'identity-idempotent', positionals: ['flow'], flags: [...write, yes] },
|
|
75
78
|
];
|
|
76
79
|
/** `--file` is a value here (a path in the project), where elsewhere it is a switch; so are unattended's `--budget`, `--runs` and `--include`. */
|
|
@@ -160,10 +163,12 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
160
163
|
const refuse = (project, reason, message, code = EXIT.refused) => { record(project, 'refused', reason); return fail(reason, message, code); };
|
|
161
164
|
// A trigger from anyone widens what comes in (chat-management.md §0a.3): only the person's own proof counts, never a
|
|
162
165
|
// machine credential standing in for them (a remote run's API token, a lead's token), though both pass the check above.
|
|
163
|
-
|
|
166
|
+
// The same proof approves browser use in a flow's script zones (flow-browser.ts): the method is recorded with it.
|
|
167
|
+
const personMethod = (project) => {
|
|
164
168
|
const proved = authenticateApprover(store, acting.name, acting.token, project);
|
|
165
|
-
return proved.ok && personProof(proved.method) ?
|
|
169
|
+
return proved.ok && personProof(proved.method) ? proved.method : null;
|
|
166
170
|
};
|
|
171
|
+
const widening = (project) => personMethod(project) === null ? {} : { widening: 'proved' };
|
|
167
172
|
const settle = (project) => { try {
|
|
168
173
|
advanceFlows(store, project, context.clock(), { evidenceRoot: context.evidenceRoot });
|
|
169
174
|
}
|
|
@@ -359,8 +364,10 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
359
364
|
return refuse(project, 'unknown-script', `There's no script called ${name} in ${projectName(project)}.`);
|
|
360
365
|
if (script.held === null)
|
|
361
366
|
return ok({ applied: false, repo: project, said: `${script.name} already runs.` }, [`${script.name} already runs.`]);
|
|
362
|
-
|
|
363
|
-
|
|
367
|
+
// Bound to the version just read: a script swapped under the same name meanwhile is refused, never approved unseen.
|
|
368
|
+
if (store.approveFlowScript(project, script.name, { version: script.version, digest: script.digest }) !== 'approved')
|
|
369
|
+
return refuse(project, 'changed', `${script.name} changed while this ran. Read it again (toolroll flows show), then approve it. Nothing was approved.`);
|
|
370
|
+
record(project, 'accepted', `script ${script.name} v${script.version} approved`);
|
|
364
371
|
settle(project);
|
|
365
372
|
return ok({ applied: true, repo: project, said: `Approved ${script.name}. Zones that run it go on.` }, [`Approved ${script.name}. Zones that run it go on.`]);
|
|
366
373
|
}
|
|
@@ -396,6 +403,42 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
396
403
|
`Revoke: toolroll flows unattended ${flow.id} off`,
|
|
397
404
|
]);
|
|
398
405
|
}
|
|
406
|
+
if (action === 'browser') {
|
|
407
|
+
const flow = flowOf(args[0], reachable);
|
|
408
|
+
if (flow === null)
|
|
409
|
+
return refuse(null, 'unknown-flow', 'No such flow in your projects. toolroll flows list names them.');
|
|
410
|
+
const turn = args[1];
|
|
411
|
+
if (turn !== 'on' && turn !== 'off')
|
|
412
|
+
return fail('usage', `Use flows browser ${flow.id} on, or flows browser ${flow.id} off.`, EXIT.usage);
|
|
413
|
+
if (turn === 'off') {
|
|
414
|
+
// Lowering authority is one step for any approver on the project.
|
|
415
|
+
if (!revokeFlowBrowser(store, flow.id, who, now))
|
|
416
|
+
return refuse(flow.repo, 'not-on', `${flow.name} has no approved browser use. Nothing was changed.`);
|
|
417
|
+
record(flow.repo, 'accepted', `flow #${flow.id} browser off`);
|
|
418
|
+
return ok({ applied: true, browser: null }, [`${flow.name}'s scripts can no longer start a browser.`]);
|
|
419
|
+
}
|
|
420
|
+
if (flowDefinitionOf(flow) === null)
|
|
421
|
+
return refuse(flow.repo, 'unreadable', 'This flow\'s drawing can\'t be read. Save it again from the editor.');
|
|
422
|
+
const terms = browserTermsOf(store, flow);
|
|
423
|
+
if (terms === null || terms.zones.length === 0)
|
|
424
|
+
return refuse(flow.repo, 'refused', terms === null ? 'A zone that starts a browser has no script it can approve: missing, waiting for approval, or one that runs a project file (write the script on the zone instead). Nothing was changed.'
|
|
425
|
+
: `No zone in ${flow.name} says it starts a browser. Mark its script zone with browser: true (flows edit), then approve. Nothing was changed.`);
|
|
426
|
+
const digest = browserApprovalDigest(terms);
|
|
427
|
+
if (!flags.has('yes'))
|
|
428
|
+
return preview(flow.repo, `Approve browser use in the ${flow.name} flow`, [...browserTerms(terms.zones), `Approval digest ${digest}: approve exactly this with --yes --digest ${digest}.`], { flowId: flow.id, digest, zones: terms.zones });
|
|
429
|
+
// A person's own proof only: a lead's or remote run's token passes the approver check above but never approves this.
|
|
430
|
+
// The digest the person read is required: approving with --yes alone could approve something they never saw.
|
|
431
|
+
const given = text('digest');
|
|
432
|
+
if (given === null)
|
|
433
|
+
return fail('usage', `Approve exactly what you read: flows browser ${flow.id} on --yes --digest <the digest the preview showed>.`, EXIT.usage);
|
|
434
|
+
const signed = signFlowBrowser(store, { flow: flow.id, signer: who, proof: personMethod(flow.repo), sawDigest: given }, now);
|
|
435
|
+
if (!signed.ok)
|
|
436
|
+
return refuse(flow.repo, 'refused', `${signed.message} Nothing was changed.`);
|
|
437
|
+
record(flow.repo, 'accepted', `flow #${flow.id} browser on`);
|
|
438
|
+
return ok({ applied: true, browser: { digest: signed.grant.digest, zones: signed.grant.zones, signedBy: signed.grant.signedBy } }, [
|
|
439
|
+
`${flow.name} may start a browser. Approved by ${who}:`, ...browserTerms(signed.grant.zones).map(line => ` ${line}`), `Revoke: toolroll flows browser ${flow.id} off`,
|
|
440
|
+
]);
|
|
441
|
+
}
|
|
399
442
|
if (action === 'archive') {
|
|
400
443
|
const cards = store.flowCards(flow.id, false).length, triggers = store.flowTriggers(flow.id).filter(one => one.state === 'active').length;
|
|
401
444
|
if (!flags.has('yes'))
|
|
@@ -566,6 +609,7 @@ function zoneOf(stage, flow) {
|
|
|
566
609
|
...(stage.limit === undefined ? {} : { remindAfterMinutes: stage.limit.minutes, thenMoveTo: stage.limit.to }),
|
|
567
610
|
...(stage.kind === 'approval' ? { decider: deciderOf(stage, flow) } : {}),
|
|
568
611
|
...(stage.script === null ? {} : { script: stage.script }),
|
|
612
|
+
...(stage.browser === true ? { browser: true } : {}),
|
|
569
613
|
...(stage.merge === undefined ? {} : { merge: stage.merge }),
|
|
570
614
|
...(stage.subagent === undefined ? {} : { subagent: stage.subagent }),
|
|
571
615
|
...(stage.options === undefined ? {} : { options: stage.options.map(one => ({ label: one.label, to: one.to })) }),
|
|
@@ -589,6 +633,10 @@ function describeFlow(store, flow, now) {
|
|
|
589
633
|
}),
|
|
590
634
|
cards: store.flowCards(flow.id, true).slice(0, 10).map(card => cardOf(definition, card)),
|
|
591
635
|
unattended: unattendedJson(store, flow, now),
|
|
636
|
+
// The latest grant a change ended early (the flow, a script, a schedule): why and when, as the flow page says it.
|
|
637
|
+
...(() => { const ended = unattendedViewOf(store, flow, { name: flow.owner, approver: true }, now).ended; return ended === null ? {} : { unattendedEnded: ended }; })(),
|
|
638
|
+
// Browser use, only for a flow with a zone that starts one: the same status line the flow page shows.
|
|
639
|
+
...(() => { const view = browserViewOf(store, flow); return view === null ? {} : { browser: { status: view.status, words: view.words, signedBy: view.signedBy, signedAt: view.signedAt } }; })(),
|
|
592
640
|
};
|
|
593
641
|
}
|
|
594
642
|
/** Whether a flow runs on its own, and until when: null when it doesn't. */
|
|
@@ -603,9 +651,10 @@ function showLines(flow) {
|
|
|
603
651
|
return [
|
|
604
652
|
`#${flow.id} ${flow.name} · ${flow.project} · owner ${flow.owner}`,
|
|
605
653
|
flow.unattended === null
|
|
606
|
-
? `Runs on its own: no · turn on: toolroll flows unattended ${flow.id} on --days N`
|
|
654
|
+
? `Runs on its own: no${'unattendedEnded' in flow && flow.unattendedEnded !== undefined ? ` (it ended ${untilWords(flow.unattendedEnded.at)}: ${flow.unattendedEnded.why})` : ''} · turn on: toolroll flows unattended ${flow.id} on --days N`
|
|
607
655
|
: `Runs on its own until ${untilWords(flow.unattended.until)} · revoke: toolroll flows unattended ${flow.id} off`,
|
|
608
656
|
...(flow.unattended === null ? [] : [` Covers: ${flow.unattended.covers.join('; ')}`]),
|
|
657
|
+
...(flow.browser === undefined ? [] : [`${flow.browser.words} · ${flow.browser.status === 'allowed' ? `turn off: toolroll flows browser ${flow.id} off` : `allow: toolroll flows browser ${flow.id} on`}`]),
|
|
609
658
|
'', 'Zones',
|
|
610
659
|
...flow.zones.flatMap((zone, index) => [
|
|
611
660
|
` ${index + 1}. ${zone.title} (${zone.id}) — ${zone.does}${zone.id === flow.start ? ' · start' : ''}`,
|
package/dist/flows-ui.d.ts
CHANGED
|
@@ -2,12 +2,18 @@ import type { BrowserFlowView } from "./browser-workspace.js";
|
|
|
2
2
|
import type { FlowRow, Store } from "./store.js";
|
|
3
3
|
import { type FlowFile, type FlowImportPlan } from "./flow-share.js";
|
|
4
4
|
import { type Html } from "./html.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/** The flow page's fallback rules, plus the Flows list and the import preview (kit pieces; only layout here). */
|
|
6
|
+
export declare const FLOWS_CSS: string;
|
|
7
|
+
/**
|
|
8
|
+
* Flows: the top bar (a project filter, More, New flow), then each flow as a quiet row under Needs you, Running,
|
|
9
|
+
* Turned on and Paused. Words come only from what the store knows now: cards waiting on a decision, cards in
|
|
10
|
+
* progress, and the triggers (on, paused, when they last looked and what they found).
|
|
11
|
+
*/
|
|
12
|
+
export declare function flowsListHtml(store: Store, flows: readonly FlowRow[], projects: readonly string[], canCreate: boolean, problem: string | null, filter?: string | null, now?: Date): Html;
|
|
13
|
+
/** Reads the chosen flow file into the form (the server reads it again, whole, before anything is made), and sends a
|
|
14
|
+
* filter select's form when it changes. */
|
|
15
|
+
export declare const FLOW_IMPORT_SCRIPT = "(function(){document.querySelectorAll('select[data-k-autosubmit]').forEach(function(s){s.addEventListener('change',function(){if(s.form)s.form.requestSubmit();});});var form=document.querySelector('[data-flow-import]');if(!form)return;var input=form.querySelector('[data-flow-file]'),text=form.querySelector('[data-flow-document]'),note=form.querySelector('[data-flow-file-note]'),named=form.querySelector('[data-flow-file-name]'),address=form.querySelector('[name=url]');function say(words){note.textContent=words;note.hidden=words==='';}var reading=null;input.addEventListener('change',function(){text.value='';say('');var file=input.files&&input.files[0];if(named)named.textContent=file?file.name:'No file chosen';if(!file){reading=null;return;}if(file.size>262144){say('That file is too big: a flow file is at most 256 KB.');input.value='';if(named)named.textContent='No file chosen';reading=null;return;}reading=file.text().then(function(value){text.value=value;});});form.addEventListener('submit',function(event){if(reading!==null&&text.value===''){event.preventDefault();reading.then(function(){form.requestSubmit();});return;}if(text.value===''&&address.value.trim()===''){event.preventDefault();say('Choose a flow file or give its address.');}});})();";
|
|
16
|
+
/** An import's preview, a reading page: the plan in plain words, what it asks for, and one Import button after the terms. */
|
|
11
17
|
export declare function flowImportHtml(input: {
|
|
12
18
|
plan: FlowImportPlan | null;
|
|
13
19
|
file: FlowFile;
|