toolroll 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/actor.d.ts +21 -0
- package/dist/actor.js +48 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-presentation.d.ts +13 -2
- package/dist/assignment-presentation.js +22 -9
- package/dist/assignment-ui.d.ts +10 -15
- package/dist/assignment-ui.js +19 -18
- package/dist/assignment.d.ts +9 -0
- package/dist/assignment.js +62 -13
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +29 -24
- package/dist/browser-workspace.d.ts +120 -12
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +33 -13
- package/dist/chat-acceptance.js +2 -2
- package/dist/chat-actions.js +2 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-quiet.js +30 -19
- package/dist/chat-task-actions.d.ts +13 -0
- package/dist/chat-task-actions.js +19 -1
- package/dist/check-levels-ui.d.ts +21 -0
- package/dist/check-levels-ui.js +70 -0
- package/dist/check-levels.d.ts +67 -0
- package/dist/check-levels.js +171 -0
- package/dist/claim.d.ts +22 -0
- package/dist/claim.js +43 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/demo-chat.d.ts +0 -5
- package/dist/demo-chat.js +14 -3
- package/dist/dispatch.js +7 -2
- package/dist/dispose.d.ts +8 -0
- package/dist/dispose.js +7 -1
- package/dist/export.js +3 -2
- package/dist/first-run.d.ts +34 -0
- package/dist/first-run.js +37 -1
- package/dist/flow-gallery-ui.d.ts +30 -0
- package/dist/flow-gallery-ui.js +85 -0
- package/dist/flow-gallery.d.ts +111 -0
- package/dist/flow-gallery.js +445 -0
- package/dist/flow-share.d.ts +102 -0
- package/dist/flow-share.js +614 -0
- package/dist/flow-starters.d.ts +2 -0
- package/dist/flow-starters.js +1 -1
- package/dist/flow-steps.js +7 -0
- package/dist/flow-triggers.js +2 -0
- package/dist/flows-cli.d.ts +56 -0
- package/dist/flows-cli.js +109 -3
- package/dist/flows-ui.d.ts +15 -1
- package/dist/flows-ui.js +31 -6
- package/dist/guides.js +27 -2
- package/dist/lead-context.js +6 -2
- package/dist/lead-status.d.ts +7 -0
- package/dist/lead-status.js +11 -2
- package/dist/mate-doors.d.ts +0 -15
- package/dist/mate-doors.js +5 -1
- package/dist/mate-tools.js +9 -4
- package/dist/models-ui.js +1 -1
- package/dist/operate.d.ts +4 -1
- package/dist/operate.js +601 -85
- package/dist/project-concurrency.d.ts +62 -0
- package/dist/project-concurrency.js +116 -0
- package/dist/project-delete-ui.d.ts +7 -0
- package/dist/project-delete-ui.js +19 -2
- package/dist/project-delete.js +4 -0
- package/dist/pull-request-flow.d.ts +9 -0
- package/dist/pull-request-flow.js +21 -3
- package/dist/result-follow-ups.d.ts +101 -0
- package/dist/result-follow-ups.js +333 -0
- package/dist/scope.js +11 -0
- package/dist/serve.d.ts +211 -2
- package/dist/serve.js +1070 -204
- package/dist/store.d.ts +76 -3
- package/dist/store.js +311 -22
- package/dist/surface.js +31 -8
- package/dist/tailnet.d.ts +11 -0
- package/dist/tailnet.js +47 -0
- package/dist/task-outcome-cli.js +2 -2
- package/dist/task-status.d.ts +186 -0
- package/dist/task-status.js +335 -0
- package/dist/team-cli.js +3 -0
- package/dist/telegram-progress.js +62 -100
- package/dist/telegram-status.js +21 -8
- package/dist/verification-evidence.js +21 -1
- package/dist/webhooks.js +5 -0
- package/dist/work-index.js +60 -11
- package/dist/work-summary.d.ts +0 -4
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +10 -0
- package/dist/workspace-ui.js +33 -2
- package/package.json +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flow gallery: ready-made flows grouped by what they're for (Ship code, Keep it healthy, Hear from users,
|
|
3
|
+
* Operations). Each says in one line what it promises, what it needs, and asks only what it must (a project,
|
|
4
|
+
* a label, a schedule, a branch, a test command). A preview says in plain words what it will do and never do;
|
|
5
|
+
* using one makes an ordinary flow (its zones), its triggers and any script it runs, together or not at all.
|
|
6
|
+
*
|
|
7
|
+
* A template is data — steps in the lead's vocabulary, trigger settings and scripts, with `{{ask.<key>}}`
|
|
8
|
+
* where an answer goes and `{{script.<name>}}` where its script's name goes — so it can be written out as a
|
|
9
|
+
* shared flow file later. Every one is built only from existing zone and trigger kinds, and none merges:
|
|
10
|
+
* a pull request it opens waits for a person.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { scanForSecrets } from "./evidence.js";
|
|
14
|
+
import { flowDefinitionOf } from "./flow-engine.js";
|
|
15
|
+
import { saveScript, validateScript } from "./flow-scripts.js";
|
|
16
|
+
import { isToolrollRepo, planeReviewSteps } from "./flow-starters.js";
|
|
17
|
+
import { addFlowTriggerTo, describeTrigger, githubRepoOf, scheduleFromWords, validateTriggerConfig } from "./flow-triggers.js";
|
|
18
|
+
import { clockTime, flowFromSteps, flowTerms, FLOW_TEMPLATES, ISSUE_LABEL } from "./flows.js";
|
|
19
|
+
import { publishingOf } from "./pull-request-flow.js";
|
|
20
|
+
export const GALLERY_GROUPS = [
|
|
21
|
+
{ id: "ship", label: "Ship code" },
|
|
22
|
+
{ id: "health", label: "Keep it healthy" },
|
|
23
|
+
{ id: "users", label: "Hear from users" },
|
|
24
|
+
{ id: "ops", label: "Operations" },
|
|
25
|
+
];
|
|
26
|
+
/** Weekly upkeep's package managers: what each runs. A script that fails (exits non-zero) when something is out of date. */
|
|
27
|
+
export const OUTDATED_COMMANDS = {
|
|
28
|
+
npm: { label: "npm (npm outdated)", command: "npm outdated" },
|
|
29
|
+
pip: { label: "pip (pip list --outdated)", command: "out=\"$(python3 -m pip list --outdated 2>/dev/null | tail -n +3)\"\nif [ -n \"$out\" ]; then echo \"$out\"; exit 1; fi" },
|
|
30
|
+
cargo: { label: "cargo (cargo outdated)", command: "cargo outdated --root-deps-only --exit-code 1" },
|
|
31
|
+
};
|
|
32
|
+
const NEVER_MERGES = "Never merges. Each pull request waits for you.";
|
|
33
|
+
const work = (text) => `${text}\n\nChanges asked for (if any): {{note}}`;
|
|
34
|
+
const research = (text) => `${text}\n\nFeedback to address (if any): {{note}}`;
|
|
35
|
+
const owner = (id, title) => ({ id, title, kind: "approval", decider: "owner" });
|
|
36
|
+
const pr = { id: "pull-request", title: "Pull request", kind: "pull-request" };
|
|
37
|
+
const drawn = (id) => structuredClone(FLOW_TEMPLATES.find(one => one.id === id).definition);
|
|
38
|
+
const about = (id) => FLOW_TEMPLATES.find(one => one.id === id).about;
|
|
39
|
+
const SORTS = "OpenRouter (Jev sorts)";
|
|
40
|
+
export const GALLERY = [
|
|
41
|
+
// ---------------------------------------------------------------- Ship code
|
|
42
|
+
{
|
|
43
|
+
id: "overnight-bug-bash", name: "Overnight bug bash", group: "ship",
|
|
44
|
+
promise: "Bugs filed during the day are fixed overnight; the results wait for you in the morning.",
|
|
45
|
+
needs: ["GitHub"], github: true,
|
|
46
|
+
asks: [{ key: "label", default: "bug", label: "Issue label" }, { key: "time", default: "22:00", label: "Start building at", hint: "Builds run from then until 06:00." }],
|
|
47
|
+
steps: [
|
|
48
|
+
{ id: "tonight", title: "Tonight", kind: "wait", waitFor: "hours", from: "{{ask.time}}", until: "06:00" },
|
|
49
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix this bug, with a regression check where one fits: {{card.title}}\n\n{{card.description}}") },
|
|
50
|
+
owner("morning", "Morning review"),
|
|
51
|
+
pr,
|
|
52
|
+
{ id: "comment", title: "Comment on the issue", kind: "update", message: "A fix is ready for review: {{stage.pull-request}}", close: false },
|
|
53
|
+
],
|
|
54
|
+
triggers: [{ kind: "github", watch: "issues", label: "{{ask.label}}" }],
|
|
55
|
+
does: ["Watches issues labelled “{{ask.label}}” in {{github}}, from people with write access.", "Holds each one until {{ask.time}}, then builds a fix as a task, under your usual approvals.",
|
|
56
|
+
"In the morning every result waits in Morning review for you.", "What you approve opens a pull request, and the issue gets a comment."],
|
|
57
|
+
never: NEVER_MERGES,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "dependency-babysitter", name: "Dependency PR babysitter", group: "ship",
|
|
61
|
+
promise: "Dependabot and Renovate pull requests with red CI get a fix.",
|
|
62
|
+
needs: ["GitHub", "CI", "Dependabot or Renovate"], github: true,
|
|
63
|
+
asks: [{ key: "label", default: "dependencies", label: "Pull request label", hint: "Dependabot adds “dependencies”." }],
|
|
64
|
+
steps: [
|
|
65
|
+
{ id: "why-red", title: "Why is CI red?", kind: "report", instructions: research("A dependency update pull request: {{card.title}}\n\n{{card.description}}\n\nLook at its checks with gh pr checks. If they pass, start the report with “CI is green” and stop. Otherwise say which check fails and what in the update breaks it.") },
|
|
66
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Make the project work with this dependency update so CI passes, starting from the update's changes. If the report starts with “CI is green”, change nothing and say so.\n\nThe report:\n{{stage.why-red}}") },
|
|
67
|
+
owner("review", "Review the fix"),
|
|
68
|
+
pr,
|
|
69
|
+
{ id: "comment", title: "Comment on the update", kind: "update", message: "A fix for this update is ready: {{stage.pull-request}}", close: false },
|
|
70
|
+
],
|
|
71
|
+
triggers: [{ kind: "github", watch: "pulls", label: "{{ask.label}}", from: "anyone" }],
|
|
72
|
+
does: ["Watches pull requests labelled “{{ask.label}}” in {{github}}, from anyone (bots included).", "Finds out why CI is red, then builds a fix as a task, under your usual approvals.",
|
|
73
|
+
"What you approve opens a pull request with the fix, and the update gets a comment linking it."],
|
|
74
|
+
never: "Never merges or pushes to the update's branch. You decide what ships.",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "release-notes", name: "Release notes writer", group: "ship",
|
|
78
|
+
promise: "Press a button for a release; get notes drafted from the merged pull requests.",
|
|
79
|
+
needs: ["GitHub"], github: true, asks: [],
|
|
80
|
+
steps: [
|
|
81
|
+
{ id: "draft", title: "Draft the notes", kind: "report", instructions: research("Write release notes for {{card.title}}.\n\n{{card.description}}\n\nList the pull requests merged since the last tag, or the tag named above, with gh pr list --state merged. Group them as New, Improved and Fixed, one plain line each with its number. End with a short changelog entry in this project's style.") },
|
|
82
|
+
owner("check", "Check the notes"),
|
|
83
|
+
{ id: "post", title: "Post them", kind: "notify", message: "Release notes for {{card.title}}:\n\n{{stage.draft}}" },
|
|
84
|
+
],
|
|
85
|
+
triggers: [{ kind: "button", label: "Write release notes", questions: ["Which version?", "Since which tag? (blank for the last one)"] }],
|
|
86
|
+
does: ["Adds a “Write release notes” button that asks for the version.", "Drafts the notes from the pull requests merged since the last tag.", "You check them, then they're posted to the project's chat."],
|
|
87
|
+
never: "Never tags, publishes or merges anything.",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "docs-follow-code", name: "Docs follow the code", group: "ship",
|
|
91
|
+
promise: "A pull request that changes a public API or CLI gets a docs update.",
|
|
92
|
+
needs: ["GitHub", SORTS], github: true, asks: [],
|
|
93
|
+
steps: [
|
|
94
|
+
{ id: "changes-docs", title: "Changes the docs?", kind: "sort", question: "Does this pull request change something people use directly: a public API, a CLI command or flag, a config option or documented behaviour?",
|
|
95
|
+
answers: [
|
|
96
|
+
{ answer: "Yes", means: "It adds, removes or changes something people call, type or configure", goesTo: "update-docs" },
|
|
97
|
+
{ answer: "No", means: "Only internal code, tests, refactors or fixes that don't change how it's used", goesTo: "Done" },
|
|
98
|
+
], ifNotSure: "update-docs" },
|
|
99
|
+
{ id: "update-docs", title: "Update the docs", kind: "task", instructions: work("This pull request changes how the project is used: {{card.title}}\n\n{{card.description}}\n\nRead it with gh pr diff. Update the README, docs, help text and examples to match. Change only documentation.") },
|
|
100
|
+
owner("review", "Review the docs"),
|
|
101
|
+
pr,
|
|
102
|
+
],
|
|
103
|
+
triggers: [{ kind: "github", watch: "pulls" }],
|
|
104
|
+
does: ["Watches new pull requests in {{github}}, from people with write access.", "Jev decides whether each changes a public API, CLI or config; when unsure, it treats it as yes.",
|
|
105
|
+
"Writes the docs update as a task, under your usual approvals.", "What you approve opens a docs pull request."],
|
|
106
|
+
never: NEVER_MERGES,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "pr-second-opinion", name: "PR second opinion", group: "ship",
|
|
110
|
+
promise: "Every pull request gets a careful review of risks and missing tests.",
|
|
111
|
+
needs: ["GitHub"], github: true, asks: [],
|
|
112
|
+
steps: [
|
|
113
|
+
{ id: "review", title: "Write a review", kind: "report", instructions: research("Review this pull request as a careful second reader: {{card.title}}\n\n{{card.description}}\n\nRead the diff with gh pr diff. Write a short review: risks, missing tests and anything unclear, most important first, citing files and lines. Say “Looks good” if there's nothing worth raising.") },
|
|
114
|
+
owner("check", "Post it?"),
|
|
115
|
+
{ id: "comment", title: "Comment on the pull request", kind: "update", message: "A second opinion:\n\n{{stage.review}}", close: false },
|
|
116
|
+
],
|
|
117
|
+
triggers: [{ kind: "github", watch: "pulls" }],
|
|
118
|
+
does: ["Watches pull requests opened by people with write access in {{github}}.", "Writes a review: risks, missing tests, anything unclear.", "You read it; what you approve is posted as a comment."],
|
|
119
|
+
never: "Never approves, changes or merges a pull request.",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "coding", name: "Coding flow", group: "ship", promise: about("coding"), needs: [], asks: [], definition: drawn("coding"), triggers: [],
|
|
123
|
+
does: ["Researches each request and writes a short triage.", "You decide whether to go ahead; then it's built as a task, under your usual approvals.", "You review the result, then the team hears it shipped."],
|
|
124
|
+
never: "Never ships anything you haven't reviewed.",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "issues-to-prs", name: "Issues to PRs", group: "ship", promise: "Labelled GitHub issues are built and opened as pull requests.", needs: ["GitHub", "CI"], github: true,
|
|
128
|
+
asks: [{ key: "label", default: ISSUE_LABEL, label: "Issue label" }], definition: drawn("issues-to-prs"),
|
|
129
|
+
triggers: [{ kind: "github", watch: "issues", label: "{{ask.label}}" }],
|
|
130
|
+
does: ["Watches issues labelled “{{ask.label}}” in {{github}}, from people with write access.", "Builds each one as a task, under your usual approvals.", "What you approve opens a pull request and waits for CI; then the issue gets a comment and is closed."],
|
|
131
|
+
never: NEVER_MERGES,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "effort-routing", name: "Effort routing", group: "ship", promise: "Small changes go straight to a build; big ones get a plan and a person first.", needs: [SORTS], asks: [], definition: drawn("effort-routing"), triggers: [],
|
|
135
|
+
does: [about("effort-routing")], never: "Never builds a big change before you approve its plan.",
|
|
136
|
+
},
|
|
137
|
+
// ---------------------------------------------------------- Keep it healthy
|
|
138
|
+
{
|
|
139
|
+
id: "fix-ci", name: "Fix failing CI", group: "health",
|
|
140
|
+
promise: "When CI fails on your main branch, a fix is built and opened for review.",
|
|
141
|
+
needs: ["GitHub", "CI"], github: true,
|
|
142
|
+
asks: [{ key: "branch", default: "main", label: "Branch" }],
|
|
143
|
+
steps: [
|
|
144
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("CI failed on {{ask.branch}}: {{card.title}}\n\n{{card.description}}\n\nFind the cause and make the failing check pass without weakening it.") },
|
|
145
|
+
owner("review", "Review the fix"),
|
|
146
|
+
pr,
|
|
147
|
+
],
|
|
148
|
+
triggers: [{ kind: "github", watch: "checks", branch: "{{ask.branch}}" }],
|
|
149
|
+
does: ["Watches checks on {{ask.branch}} in {{github}}.", "Builds a fix for each failure as a task, under your usual approvals.", "What you approve opens a pull request."],
|
|
150
|
+
never: "Never merges or pushes to your branch. You decide what ships.",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "flaky-test-hunter", name: "Flaky test hunter", group: "health",
|
|
154
|
+
promise: "A test that fails, then passes on rerun, gets found, explained and fixed or quarantined.",
|
|
155
|
+
needs: ["GitHub", "CI"], github: true,
|
|
156
|
+
asks: [{ key: "branch", default: "main", label: "Branch" }],
|
|
157
|
+
steps: [
|
|
158
|
+
{ id: "hunt", title: "Is it flaky?", kind: "report", instructions: research("A check failed on {{ask.branch}}: {{card.title}}\n\n{{card.description}}\n\nRerun the failing tests a few times. If one passes on rerun, name it and say why it's flaky (timing, order, shared state, network) and whether to fix or quarantine it. If it fails every time, start the report with “Not flaky” and say what broke.") },
|
|
159
|
+
{ id: "fix", title: "Fix or quarantine", kind: "task", instructions: work("Make this flaky test reliable. If the cause can't be fixed safely, quarantine it instead: skip it with a comment saying why. If the report starts with “Not flaky”, change nothing and say so.\n\nThe report:\n{{stage.hunt}}") },
|
|
160
|
+
owner("review", "Review the fix"),
|
|
161
|
+
pr,
|
|
162
|
+
],
|
|
163
|
+
triggers: [{ kind: "github", watch: "checks", branch: "{{ask.branch}}" }],
|
|
164
|
+
does: ["Watches failed checks on {{ask.branch}} in {{github}}.", "Reruns the failing tests to find the flaky one, and says why it's flaky.", "Fixes it or quarantines it as a task, under your usual approvals.", "What you approve opens a pull request."],
|
|
165
|
+
never: NEVER_MERGES,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "morning-plane-review", name: "Morning plane review", group: "health",
|
|
169
|
+
promise: "What went wrong in Toolroll yesterday becomes cards, researched and fixed.",
|
|
170
|
+
needs: [], asks: [{ key: "time", default: "07:30", label: "Review at" }],
|
|
171
|
+
steps: planeReviewSteps(false), ownSteps: planeReviewSteps(true),
|
|
172
|
+
triggers: [{ kind: "plane-review", at: "{{ask.time}}" }],
|
|
173
|
+
does: ["Every day at {{ask.time}}, looks at failed runs, waiting tasks, sign-ins, plan limits, chat, integrations and checks.", "One card per problem. A clean day adds nothing.",
|
|
174
|
+
"Finds the cause; one in this project gets a fix under your usual approvals and a pull request.", "Anything that fails waits in Needs a look."],
|
|
175
|
+
never: "Never merges or ships anything without you. Each fix waits for your approval.",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "nightly-journeys", name: "Nightly real-model journeys", group: "health",
|
|
179
|
+
promise: "Your end-to-end tests run every night; a failure becomes a researched fix.",
|
|
180
|
+
needs: ["A test command"],
|
|
181
|
+
asks: [{ key: "command", default: "npm run e2e", label: "Test command", hint: "Runs in a copy of the project. It fails when the command does." }, { key: "schedule", default: "daily 02:00", label: "When", hint: "Like “daily 02:00” or “weekdays 03:00”." }],
|
|
182
|
+
scripts: [{ name: "nightly-journeys", about: "Runs the project's end-to-end journeys.", body: "{{ask.command}}", timeoutMinutes: 60 }],
|
|
183
|
+
steps: [
|
|
184
|
+
{ id: "run", title: "Run the journeys", kind: "check", script: "{{script.nightly-journeys}}", next: "Done", ifFails: "find-cause" },
|
|
185
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("The nightly journeys failed. What the run printed:\n{{stage.run}}\n\nFind which journey failed and why: the code, the test or the model provider. Cite files and lines, and say what change fixes it.") },
|
|
186
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix the cause of the failed nightly journey without weakening the test.\n\nThe report:\n{{stage.find-cause}}") },
|
|
187
|
+
owner("review", "Review the fix"),
|
|
188
|
+
pr,
|
|
189
|
+
],
|
|
190
|
+
triggers: [{ kind: "schedule", schedule: "{{ask.schedule}}", title: "Nightly journeys" }],
|
|
191
|
+
does: ["Runs “{{ask.command}}” {{ask.schedule-words}}, with no AI.", "A pass adds nothing to review. A failure is researched, then fixed as a task, under your usual approvals.", "What you approve opens a pull request."],
|
|
192
|
+
never: NEVER_MERGES,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "weekly-upkeep", name: "Weekly upkeep", group: "health",
|
|
196
|
+
promise: "Outdated dependencies are updated each week, with major upgrades kept apart.",
|
|
197
|
+
needs: ["A script (npm, pip or cargo outdated)"],
|
|
198
|
+
asks: [{ key: "outdated", default: "npm", label: "Packages" }, { key: "schedule", default: "monday 09:00", label: "When", hint: "Like “monday 09:00”." }],
|
|
199
|
+
scripts: [{ name: "outdated", about: "Lists outdated dependencies; fails when there are any.", body: "{{ask.outdated-command}}" }],
|
|
200
|
+
steps: [
|
|
201
|
+
{ id: "outdated", title: "Anything outdated?", kind: "check", script: "{{script.outdated}}", next: "Done", ifFails: "update" },
|
|
202
|
+
{ id: "update", title: "Update them", kind: "task", instructions: work("Update the outdated dependencies below. Keep major upgrades in their own commits and note any breaking changes. Run the tests.\n\nWhat's outdated:\n{{stage.outdated}}") },
|
|
203
|
+
owner("review", "Review the updates"),
|
|
204
|
+
pr,
|
|
205
|
+
],
|
|
206
|
+
triggers: [{ kind: "schedule", schedule: "{{ask.schedule}}", title: "Weekly upkeep" }],
|
|
207
|
+
does: ["Checks for outdated {{ask.outdated}} packages {{ask.schedule-words}}, with no AI.", "When some are out of date, updates them as a task, under your usual approvals.", "What you approve opens a pull request."],
|
|
208
|
+
never: NEVER_MERGES,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "error-to-fix", name: "Error to fix", group: "health",
|
|
212
|
+
promise: "New errors from Sentry or any error tracker are sorted, researched and fixed.",
|
|
213
|
+
needs: ["A webhook from Sentry or another tracker", SORTS], asks: [],
|
|
214
|
+
steps: [
|
|
215
|
+
{ id: "worth-fixing", title: "Worth fixing?", kind: "sort", question: "Is this error in our own code, and new or frequent enough to fix now?",
|
|
216
|
+
answers: [
|
|
217
|
+
{ answer: "Fix it", means: "It comes from our code and is new, frequent, or hurts people", goesTo: "find-cause" },
|
|
218
|
+
{ answer: "Ignore", means: "Noise: bots, browser extensions, a third party's outage, or something known and expected", goesTo: "ignored" },
|
|
219
|
+
], ifNotSure: "by-hand" },
|
|
220
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("An error came in: {{card.title}}\n\n{{card.description}}\n\nFind the cause in this code: cite the files and lines, say why it happens and what change fixes it.") },
|
|
221
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix this error, with a regression check where one fits: {{card.title}}\n\nThe report:\n{{stage.find-cause}}") },
|
|
222
|
+
owner("review", "Review the fix"),
|
|
223
|
+
{ ...pr, next: "Done" },
|
|
224
|
+
{ id: "by-hand", title: "Check by hand", kind: "inbox", next: "find-cause" },
|
|
225
|
+
{ id: "done", title: "Done", kind: "done" },
|
|
226
|
+
{ id: "ignored", title: "Ignored", kind: "done" },
|
|
227
|
+
],
|
|
228
|
+
triggers: [{ kind: "webhook", title: "Error", titleField: "data.issue.title", bodyField: "data.issue.culprit" }],
|
|
229
|
+
does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Sentry or another error tracker.", "Jev keeps errors in your code that are new or frequent, and drops noise; ones it isn't sure about wait for you.",
|
|
230
|
+
"Each kept error is researched, then fixed as a task, under your usual approvals.", "What you approve opens a pull request."],
|
|
231
|
+
never: NEVER_MERGES,
|
|
232
|
+
},
|
|
233
|
+
// ---------------------------------------------------------- Hear from users
|
|
234
|
+
{
|
|
235
|
+
id: "feedback-to-feature", name: "Feedback to feature", group: "users",
|
|
236
|
+
promise: "Feedback is sorted into bugs, ideas and questions; ideas get a short spec for you to decide.",
|
|
237
|
+
needs: [SORTS, "A Slack or Discord channel (optional)"], asks: [],
|
|
238
|
+
steps: [
|
|
239
|
+
{ id: "sort", title: "Sort it", kind: "sort", question: "What kind of feedback is this?",
|
|
240
|
+
answers: [
|
|
241
|
+
{ answer: "Bug", means: "Something is broken or behaves wrongly", goesTo: "fix" },
|
|
242
|
+
{ answer: "Idea", means: "A request for something new, or to change how something works", goesTo: "spec" },
|
|
243
|
+
{ answer: "Question", means: "Someone asking how to do something", goesTo: "reply" },
|
|
244
|
+
], ifNotSure: "by-hand" },
|
|
245
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix this reported problem: {{card.title}}\n\n{{card.description}}") },
|
|
246
|
+
owner("review", "Review the fix"),
|
|
247
|
+
{ ...pr, next: "Done" },
|
|
248
|
+
{ id: "spec", title: "Write a short spec", kind: "report", instructions: research("Someone suggested this: {{card.title}}\n\n{{card.description}}\n\nWrite a short spec: the problem, who it helps, the smallest useful version, where it lands in the code, and a rough size.") },
|
|
249
|
+
owner("decide", "Build it?"),
|
|
250
|
+
{ id: "planned", title: "Planned", kind: "inbox", next: "Done" },
|
|
251
|
+
{ id: "reply", title: "Write a reply", kind: "draft", instructions: "Write a short, friendly answer to this question in plain words, saying what to do next if anything." },
|
|
252
|
+
{ ...owner("check-reply", "Check the reply"), ifFails: "reply" },
|
|
253
|
+
{ id: "answer", title: "Answer them", kind: "update", message: "{{stage.reply}}", close: false, next: "Done" },
|
|
254
|
+
{ id: "by-hand", title: "Sort by hand", kind: "inbox", next: "Done" },
|
|
255
|
+
],
|
|
256
|
+
triggers: [{ kind: "button", label: "Share feedback", questions: ["What's your feedback?", "Details"] }],
|
|
257
|
+
does: ["Adds a “Share feedback” button. You can also connect a Slack or Discord channel to it from that channel.", "Jev sorts each piece of feedback: bug, idea or question.",
|
|
258
|
+
"Bugs are fixed as tasks under your usual approvals; ideas get a short spec for you to decide; questions get a reply you check before it's sent."],
|
|
259
|
+
never: "Never builds an idea or sends a reply without you. " + NEVER_MERGES,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "triage", name: "Issue triage", group: "users", promise: "Jev sorts new issues into bugs, ideas and questions, and says how urgent each is.", needs: [SORTS], asks: [], definition: drawn("triage"), triggers: [],
|
|
263
|
+
does: [about("triage")], never: "Never closes an issue or replies without your approval.",
|
|
264
|
+
},
|
|
265
|
+
// ------------------------------------------------------------------ Operations
|
|
266
|
+
...[
|
|
267
|
+
["research", "Research a question, have a person check it, then share it.", [], "Never shares an answer you haven't checked."],
|
|
268
|
+
["spam-filter", "Jev screens what a public form or webhook brings in.", [SORTS], "Never deletes anything; filtered cards are kept."],
|
|
269
|
+
["lead-routing", "Jev sorts new enquiries by what they want and how ready they are.", [SORTS], "Never replies to anyone."],
|
|
270
|
+
["exception-routing", "Customer problems go to the team that owns them.", [SORTS], "Never replies or refunds."],
|
|
271
|
+
["email-replies", "Claude drafts a reply to each question; you approve it before it's emailed.", ["Email (Settings → Email)"], "Never sends an email you haven't approved."],
|
|
272
|
+
["follow-up", "A reply goes out, and a short nudge follows if nobody answers.", ["Email inbox (Settings → Email)"], "Never sends the first reply without your approval."],
|
|
273
|
+
["stalled-decisions", "Decisions that wait too long are passed to anyone who can approve.", [], "Never decides for you."],
|
|
274
|
+
].map(([id, promise, needs, never]) => ({
|
|
275
|
+
id, name: FLOW_TEMPLATES.find(one => one.id === id).label, group: "ops", promise, needs: [...needs], asks: [], definition: drawn(id), triggers: [], does: [about(id)], never,
|
|
276
|
+
})),
|
|
277
|
+
];
|
|
278
|
+
/** The blank flow, for starting from nothing. */
|
|
279
|
+
export const BLANK = { id: "blank", name: "Blank flow", group: "ops", promise: about("blank"), needs: [], asks: [], definition: drawn("blank"), triggers: [], does: [about("blank")], never: "Does nothing until you add zones." };
|
|
280
|
+
export const galleryTemplateOf = (id) => id === BLANK.id ? BLANK : GALLERY.find(one => one.id === id) ?? null;
|
|
281
|
+
/** Each question's default for a project: the branch pull requests go to, when that's set up. */
|
|
282
|
+
export function galleryDefaults(store, template, repo) {
|
|
283
|
+
const publishing = repo === null ? { on: false } : publishingOf(store, repo);
|
|
284
|
+
return Object.fromEntries(template.asks.map(ask => [ask.key, ask.key === "branch" && publishing.on ? publishing.base : ask.default]));
|
|
285
|
+
}
|
|
286
|
+
const oneLine = (value, cap, what) => {
|
|
287
|
+
const said = value.replace(/\s+/g, " ").trim();
|
|
288
|
+
if (said === "" || said.length > cap || /[\u0000-\u001f\u007f]/.test(said))
|
|
289
|
+
throw new Error(`Say the ${what} in one short line.`);
|
|
290
|
+
if (scanForSecrets(said).length > 0)
|
|
291
|
+
throw new Error(`That ${what} looks like it holds a key or password. Keep secrets out of it.`);
|
|
292
|
+
return said;
|
|
293
|
+
};
|
|
294
|
+
/** The answers, checked in plain words, with what they fill in. Throws. */
|
|
295
|
+
export function checkAnswers(template, given) {
|
|
296
|
+
const answers = {};
|
|
297
|
+
for (const ask of template.asks) {
|
|
298
|
+
const value = String(given[ask.key] ?? ask.default).trim();
|
|
299
|
+
if (ask.key === "label")
|
|
300
|
+
answers["label"] = oneLine(value, 50, "label");
|
|
301
|
+
else if (ask.key === "branch") {
|
|
302
|
+
if (!/^[A-Za-z0-9._/-]{1,100}$/.test(value))
|
|
303
|
+
throw new Error("That branch name isn't valid.");
|
|
304
|
+
answers["branch"] = value;
|
|
305
|
+
}
|
|
306
|
+
else if (ask.key === "time") {
|
|
307
|
+
const time = clockTime(value);
|
|
308
|
+
if (time === null)
|
|
309
|
+
throw new Error("Say the time as HH:MM, like 22:00.");
|
|
310
|
+
answers["time"] = time;
|
|
311
|
+
}
|
|
312
|
+
else if (ask.key === "schedule") {
|
|
313
|
+
const said = oneLine(value, 80, "schedule");
|
|
314
|
+
if (scheduleFromWords(said) === null)
|
|
315
|
+
throw new Error("Say when like “daily 02:00”, “weekdays 09:00” or “monday 09:00”.");
|
|
316
|
+
// Said without a time zone, it runs in this computer's, like the plane review.
|
|
317
|
+
const zoned = /@/.test(scheduleFromWords(said)) || /^every \d/i.test(said) ? null : scheduleFromWords(`${said} ${localTimeZone()}`);
|
|
318
|
+
answers["schedule"] = zoned === null ? said : `${said} ${localTimeZone()}`;
|
|
319
|
+
answers["schedule-words"] = scheduleWords(said);
|
|
320
|
+
}
|
|
321
|
+
else if (ask.key === "command")
|
|
322
|
+
answers["command"] = oneLine(value, 500, "test command");
|
|
323
|
+
else if (ask.key === "outdated") {
|
|
324
|
+
const manager = OUTDATED_COMMANDS[value];
|
|
325
|
+
if (manager === undefined)
|
|
326
|
+
throw new Error("Choose npm, pip or cargo.");
|
|
327
|
+
answers["outdated"] = value;
|
|
328
|
+
answers["outdated-command"] = manager.command;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return answers;
|
|
332
|
+
}
|
|
333
|
+
function localTimeZone() {
|
|
334
|
+
try {
|
|
335
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return "UTC";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/** "daily 02:00" as "every day at 02:00", the way a sentence says it. */
|
|
342
|
+
function scheduleWords(said) {
|
|
343
|
+
const daily = /^(?:every day|daily)(?: at)? (\d{1,2}:\d{2})/i.exec(said);
|
|
344
|
+
if (daily !== null)
|
|
345
|
+
return `every day at ${daily[1]}`;
|
|
346
|
+
const weekdays = /^(?:every )?weekdays?(?: at)? (\d{1,2}:\d{2})/i.exec(said);
|
|
347
|
+
if (weekdays !== null)
|
|
348
|
+
return `every weekday at ${weekdays[1]}`;
|
|
349
|
+
const weekly = /^(?:every |weekly on |weekly |on )?(sunday|monday|tuesday|wednesday|thursday|friday|saturday)s?(?: at)? (\d{1,2}:\d{2})/i.exec(said);
|
|
350
|
+
if (weekly !== null)
|
|
351
|
+
return `every ${weekly[1].charAt(0).toUpperCase()}${weekly[1].slice(1).toLowerCase()} at ${weekly[2]}`;
|
|
352
|
+
return said;
|
|
353
|
+
}
|
|
354
|
+
/** Fill `{{ask.<key>}}` and `{{script.<name>}}` through a template's data; the card's own fill-ins stay. */
|
|
355
|
+
function fill(value, answers, scripts, github) {
|
|
356
|
+
if (typeof value === "string")
|
|
357
|
+
return value.replace(/\{\{(ask|script)\.([a-z-]+)\}\}|\{\{github\}\}/g, (whole, kind, key) => kind === undefined ? github ?? "your GitHub repository" : (kind === "ask" ? answers[key] : scripts[key]) ?? whole);
|
|
358
|
+
if (Array.isArray(value))
|
|
359
|
+
return value.map(one => fill(one, answers, scripts, github));
|
|
360
|
+
if (value !== null && typeof value === "object")
|
|
361
|
+
return Object.fromEntries(Object.entries(value).map(([key, one]) => [key, fill(one, answers, scripts, github)]));
|
|
362
|
+
return value;
|
|
363
|
+
}
|
|
364
|
+
/** A template made concrete for a project and its answers: its zones, triggers and scripts, checked. Throws in plain words. */
|
|
365
|
+
export function buildFromGallery(store, template, repo, given) {
|
|
366
|
+
if (template.github === true && githubRepoOf(repo) === null)
|
|
367
|
+
throw new Error("This project isn't on GitHub, so this can't watch it.");
|
|
368
|
+
const answers = checkAnswers(template, given);
|
|
369
|
+
// A script keeps its name unless the project already has a different one by that name; then it gets a free one.
|
|
370
|
+
const names = {};
|
|
371
|
+
const scripts = (template.scripts ?? []).map(script => {
|
|
372
|
+
const body = fill(script.body, answers, {}, null);
|
|
373
|
+
const draft = validateScript({ name: script.name, about: script.about, body, timeoutMinutes: script.timeoutMinutes ?? 15 });
|
|
374
|
+
let name = draft.name, existing = false;
|
|
375
|
+
for (let n = 2;; n++) {
|
|
376
|
+
const there = store.flowScript(repo, name);
|
|
377
|
+
if (there === null)
|
|
378
|
+
break;
|
|
379
|
+
if (there.body === draft.body && there.language === "shell" && there.file === null) {
|
|
380
|
+
existing = true;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
name = `${draft.name}-${n}`;
|
|
384
|
+
}
|
|
385
|
+
names[script.name] = name;
|
|
386
|
+
return { name, about: draft.about, body: draft.body, timeoutMinutes: draft.timeoutMinutes, existing };
|
|
387
|
+
});
|
|
388
|
+
const github = githubRepoOf(repo);
|
|
389
|
+
const definition = template.definition !== undefined ? structuredClone(template.definition)
|
|
390
|
+
: flowFromSteps(fill(template.ownSteps !== undefined && isToolrollRepo(repo) ? template.ownSteps : template.steps, answers, names, github), null);
|
|
391
|
+
return { template, repo, answers, definition, triggers: fill(template.triggers, answers, names, github), scripts,
|
|
392
|
+
does: fill(template.does, answers, names, github), never: template.never };
|
|
393
|
+
}
|
|
394
|
+
/** A template's zones as its defaults draw them, for the gallery's small drawing; no project needed. */
|
|
395
|
+
export function galleryDiagram(template) {
|
|
396
|
+
if (template.definition !== undefined)
|
|
397
|
+
return template.definition;
|
|
398
|
+
const names = Object.fromEntries((template.scripts ?? []).map(one => [one.name, one.name]));
|
|
399
|
+
return flowFromSteps(fill(template.steps, checkAnswers(template, {}), names, null), null);
|
|
400
|
+
}
|
|
401
|
+
/** What using a template will do, in words, before anything is made: its triggers checked as the flow will have them. Throws. */
|
|
402
|
+
export function previewGallery(store, template, repo, given, actor, now) {
|
|
403
|
+
const built = buildFromGallery(store, template, repo, given);
|
|
404
|
+
const draft = { id: 0, repo, name: template.name, definitionJson: JSON.stringify(built.definition), revision: 1, state: "active", createdBy: actor, createdAt: now.toISOString(), updatedBy: actor, updatedAt: now.toISOString(), owner: actor };
|
|
405
|
+
const startsFrom = built.triggers.map(trigger => describeTrigger(validateTriggerConfig({ ...trigger, zone: trigger["zone"] ?? built.definition.start }, { store, flow: draft, definition: built.definition, actor }), store));
|
|
406
|
+
const steps = flowTerms(built.definition, null);
|
|
407
|
+
if (built.scripts.some(one => !one.existing))
|
|
408
|
+
steps.push(...built.scripts.filter(one => !one.existing).map(one => `Adds the ${one.name} script to this project: ${one.body.split("\n")[0]}${one.body.includes("\n") ? " …" : ""}`));
|
|
409
|
+
return { built, startsFrom, steps, digest: galleryDigest(template.id, repo, built.answers) };
|
|
410
|
+
}
|
|
411
|
+
/** What a preview showed: the template, the project and the answers. Creating checks the person saw these. */
|
|
412
|
+
export function galleryDigest(id, repo, answers) {
|
|
413
|
+
return createHash("sha256").update(JSON.stringify([id, repo, Object.entries(answers).sort(([a], [b]) => a.localeCompare(b))])).digest("hex").slice(0, 24);
|
|
414
|
+
}
|
|
415
|
+
/** Use a template in a project: its scripts, its flow and its triggers, together or not at all. */
|
|
416
|
+
export function useGalleryTemplate(store, template, repo, given, input) {
|
|
417
|
+
try {
|
|
418
|
+
const built = buildFromGallery(store, template, repo, given);
|
|
419
|
+
const name = (input.name ?? "").replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || template.name;
|
|
420
|
+
let hook = false;
|
|
421
|
+
const flow = store.transact(() => {
|
|
422
|
+
for (const script of built.scripts)
|
|
423
|
+
if (!script.existing) {
|
|
424
|
+
const saved = saveScript(store, repo, { name: script.name, about: script.about, body: script.body, timeoutMinutes: script.timeoutMinutes }, input.by, input.now);
|
|
425
|
+
if (!saved.ok)
|
|
426
|
+
throw new Error(saved.message);
|
|
427
|
+
}
|
|
428
|
+
const id = store.createFlow({ repo, name, definitionJson: JSON.stringify(built.definition), by: input.by }, input.now);
|
|
429
|
+
const made = store.getFlow(id);
|
|
430
|
+
for (const trigger of built.triggers) {
|
|
431
|
+
const added = addFlowTriggerTo(store, made, { ...trigger, zone: trigger["zone"] ?? flowDefinitionOf(made).start }, input.by, input.now, input.dir);
|
|
432
|
+
if (!added.ok)
|
|
433
|
+
throw new Error(added.message);
|
|
434
|
+
if (added.reveal !== null)
|
|
435
|
+
hook = true;
|
|
436
|
+
}
|
|
437
|
+
return id;
|
|
438
|
+
});
|
|
439
|
+
// A webhook's address is a secret shown once: it's made on the flow's Triggers panel, where it can be copied.
|
|
440
|
+
return { ok: true, flow, said: hook ? `${name} is ready. To get its webhook address, open Triggers and choose New address.` : `${name} is ready.` };
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
return { ok: false, said: `${template.name} couldn't be made: ${error instanceof Error ? error.message : "unknown"}` };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type ScriptDraft } from "./flow-scripts.js";
|
|
2
|
+
import { type FlowDefinition } from "./flows.js";
|
|
3
|
+
import type { FlowRow, Store } from "./store.js";
|
|
4
|
+
export declare const FLOW_FILE_FORMAT = "toolroll-flow";
|
|
5
|
+
export declare const FLOW_FILE_VERSION = 1;
|
|
6
|
+
/** The largest flow file Toolroll reads or fetches. */
|
|
7
|
+
export declare const FLOW_FILE_MAX_BYTES: number;
|
|
8
|
+
export declare const FLOW_FILE_SUFFIX = ".toolroll-flow.json";
|
|
9
|
+
/** Something the import asks for: `{{param.<id>}}` in a zone or trigger is replaced by its value. */
|
|
10
|
+
export type FlowFileParameter = {
|
|
11
|
+
id: string;
|
|
12
|
+
about: string;
|
|
13
|
+
default?: string;
|
|
14
|
+
optional?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type FlowFileScript = {
|
|
17
|
+
name: string;
|
|
18
|
+
about: string;
|
|
19
|
+
language: ScriptDraft["language"];
|
|
20
|
+
timeoutMinutes: number;
|
|
21
|
+
body?: string;
|
|
22
|
+
file?: string;
|
|
23
|
+
};
|
|
24
|
+
export type FlowFile = {
|
|
25
|
+
format: typeof FLOW_FILE_FORMAT;
|
|
26
|
+
version: typeof FLOW_FILE_VERSION;
|
|
27
|
+
name: string;
|
|
28
|
+
about: string;
|
|
29
|
+
needs: string[];
|
|
30
|
+
parameters: FlowFileParameter[];
|
|
31
|
+
/** The zones in the lead's step vocabulary, the start first. */
|
|
32
|
+
zones: Record<string, unknown>[];
|
|
33
|
+
/** Each trigger's settings, as `toolroll flows trigger add` takes them. */
|
|
34
|
+
triggers: Record<string, unknown>[];
|
|
35
|
+
scripts: FlowFileScript[];
|
|
36
|
+
};
|
|
37
|
+
export type FlowExported = {
|
|
38
|
+
file: FlowFile;
|
|
39
|
+
json: string;
|
|
40
|
+
fileName: string;
|
|
41
|
+
left: string[];
|
|
42
|
+
};
|
|
43
|
+
/** A flow as a file: its zones and paths, its triggers' settings and the scripts it runs, nothing of this installation's. */
|
|
44
|
+
export declare function exportFlow(store: Store, flow: FlowRow, dir: string | null): FlowExported;
|
|
45
|
+
export declare class FlowFileError extends Error {
|
|
46
|
+
}
|
|
47
|
+
/** Read a flow file: its size, its JSON, its format and version, and every zone, trigger, script and parameter. Throws FlowFileError in plain words. */
|
|
48
|
+
export declare function parseFlowFile(text: string): FlowFile;
|
|
49
|
+
/** --param k=v lines (or a form's fields) as values; refuses parameters the file doesn't ask for. */
|
|
50
|
+
export declare function parameterValues(file: FlowFile, given: Record<string, string>): Record<string, string>;
|
|
51
|
+
export declare function needWords(need: string): string;
|
|
52
|
+
export type FlowImportPlan = {
|
|
53
|
+
name: string;
|
|
54
|
+
repo: string;
|
|
55
|
+
definition: FlowDefinition;
|
|
56
|
+
triggers: Record<string, unknown>[];
|
|
57
|
+
/** Scripts to add (held until approved), and those the project already has exactly. */
|
|
58
|
+
scripts: {
|
|
59
|
+
add: FlowFileScript[];
|
|
60
|
+
kept: string[];
|
|
61
|
+
};
|
|
62
|
+
values: Record<string, string>;
|
|
63
|
+
title: string;
|
|
64
|
+
/** The whole preview, line by line (the CLI's); `parts` is the same, grouped for the console's page. */
|
|
65
|
+
terms: string[];
|
|
66
|
+
parts: {
|
|
67
|
+
about: string | null;
|
|
68
|
+
steps: string[];
|
|
69
|
+
notes: string[];
|
|
70
|
+
triggers: string[];
|
|
71
|
+
scripts: string[];
|
|
72
|
+
kept: string[];
|
|
73
|
+
filled: string | null;
|
|
74
|
+
needs: string | null;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
/** The import in plain words, checked against the project it lands in. Throws FlowFileError. */
|
|
78
|
+
export declare function planFlowImport(store: Store, repo: string, file: FlowFile, given: Record<string, string>, actor: string): FlowImportPlan;
|
|
79
|
+
/** Make the planned flow: scripts held, the flow, and its triggers switched off. One transaction. */
|
|
80
|
+
export declare function importFlow(store: Store, plan: FlowImportPlan, actor: string, now: Date, dir: string | null): {
|
|
81
|
+
id: number;
|
|
82
|
+
said: string;
|
|
83
|
+
};
|
|
84
|
+
/** What a flow's zones and paths are, without where they sit on the canvas: what a round trip keeps. */
|
|
85
|
+
export declare function flowShape(definition: FlowDefinition): string;
|
|
86
|
+
/** A gist or GitHub file page as the address of its raw text; null when it isn't one Toolroll fetches. */
|
|
87
|
+
export declare function rawFlowUrl(address: string): string | null;
|
|
88
|
+
export type FetchLike = (url: string, init: {
|
|
89
|
+
redirect: "follow";
|
|
90
|
+
signal: AbortSignal;
|
|
91
|
+
headers: Record<string, string>;
|
|
92
|
+
}) => Promise<{
|
|
93
|
+
ok: boolean;
|
|
94
|
+
status: number;
|
|
95
|
+
url?: string;
|
|
96
|
+
headers: {
|
|
97
|
+
get(name: string): string | null;
|
|
98
|
+
};
|
|
99
|
+
body: ReadableStream<Uint8Array> | null;
|
|
100
|
+
}>;
|
|
101
|
+
/** A flow file from a gist or GitHub, over HTTPS only, refused past the size cap. Throws FlowFileError in plain words. */
|
|
102
|
+
export declare function fetchFlowFile(address: string, fetcher?: FetchLike): Promise<string>;
|