toolroll 0.9.53 → 0.9.55
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 +17 -0
- package/README.md +5 -4
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +15 -20
- package/dist/assignment-ui.d.ts +3 -2
- package/dist/assignment-ui.js +4 -11
- package/dist/backup-ui.d.ts +2 -1
- package/dist/backup-ui.js +16 -27
- package/dist/brand-mark.d.ts +3 -2
- package/dist/brand-mark.js +4 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-shell.d.ts +2 -1
- package/dist/browser-shell.js +9 -9
- package/dist/browser-workspace.d.ts +2 -3
- package/dist/browser-workspace.js +0 -3
- package/dist/chat-ask.js +8 -8
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +26 -21
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -2
- package/dist/chat-delivery.js +313 -194
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +5 -0
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +26 -53
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +9 -3
- package/dist/coding-shipping-ui.d.ts +2 -1
- package/dist/coding-shipping-ui.js +6 -6
- package/dist/coding-ui.d.ts +3 -1
- package/dist/coding-ui.js +30 -29
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +6 -5
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/health.d.ts +1 -3
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -5
- package/dist/contracts/store-json.js +1 -4
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/credentials-ui.d.ts +5 -4
- package/dist/credentials-ui.js +23 -24
- package/dist/demo-chat.d.ts +3 -3
- package/dist/demo-chat.js +29 -76
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +21 -55
- package/dist/discord.js +4 -14
- package/dist/envelope.js +10 -0
- package/dist/evidence-pack.d.ts +2 -1
- package/dist/evidence-pack.js +45 -49
- package/dist/export-ui.d.ts +7 -2
- package/dist/export-ui.js +4 -10
- package/dist/export.js +4 -4
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-triggers.d.ts +1 -0
- package/dist/flow-triggers.js +16 -14
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +19 -30
- package/dist/flows.js +2 -1
- package/dist/guides.js +108 -3
- package/dist/html.d.ts +76 -0
- package/dist/html.js +173 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +19 -23
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +18 -20
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/lead-context.d.ts +11 -1
- package/dist/lead-context.js +6 -8
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +2 -1
- package/dist/limits-ui.js +7 -7
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mate-tools.js +3 -2
- package/dist/mcp-oauth.js +35 -36
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/modes.d.ts +13 -1
- package/dist/modes.js +17 -1
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.js +16 -9
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/plane-review.js +5 -11
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +15 -27
- package/dist/project-delete.js +21 -26
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +17 -32
- package/dist/recipe-ui.d.ts +8 -7
- package/dist/recipe-ui.js +39 -32
- package/dist/release-adapters.d.ts +60 -0
- package/dist/release-adapters.js +435 -0
- package/dist/release-cli.d.ts +24 -0
- package/dist/release-cli.js +181 -0
- package/dist/release-gate.d.ts +80 -0
- package/dist/release-gate.js +150 -0
- package/dist/release-links.d.ts +29 -0
- package/dist/release-links.js +102 -0
- package/dist/release.d.ts +331 -0
- package/dist/release.js +550 -0
- package/dist/reply-shape.d.ts +2 -1
- package/dist/reply-shape.js +5 -5
- package/dist/repository-context-ui.d.ts +2 -1
- package/dist/repository-context-ui.js +2 -7
- package/dist/request-budget-ui.d.ts +2 -1
- package/dist/request-budget-ui.js +16 -16
- package/dist/result-review.d.ts +2 -3
- package/dist/result-review.js +3 -9
- package/dist/result-shots.js +2 -2
- package/dist/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/retention.js +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +69 -58
- package/dist/server/chat.d.ts +3 -6
- package/dist/server/chat.js +969 -1006
- package/dist/server/chrome.d.ts +291 -0
- package/dist/server/chrome.js +747 -0
- package/dist/server/console.css +2819 -0
- package/dist/server/flows.d.ts +2 -6
- package/dist/server/flows.js +840 -766
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +5 -2
- package/dist/server/handler-context.d.ts +1 -1
- package/dist/server/handler-context.js +1 -1
- package/dist/server/handler-registry.d.ts +8 -6
- package/dist/server/handler-registry.js +14 -2
- package/dist/server/http.d.ts +274 -0
- package/dist/server/http.js +239 -0
- package/dist/server/live.d.ts +34 -0
- package/dist/server/live.js +309 -0
- package/dist/server/pages.d.ts +2 -6
- package/dist/server/pages.js +808 -789
- package/dist/server/people-tokens.d.ts +1 -6
- package/dist/server/people-tokens.js +580 -572
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +37 -70
- package/dist/server/render-chat.d.ts +402 -0
- package/dist/server/render-chat.js +927 -0
- package/dist/server/render-pages.d.ts +535 -0
- package/dist/server/render-pages.js +1277 -0
- package/dist/server/render-people.d.ts +32 -0
- package/dist/server/render-people.js +121 -0
- package/dist/server/render-results.d.ts +744 -0
- package/dist/server/render-results.js +2237 -0
- package/dist/server/render-settings.d.ts +147 -0
- package/dist/server/render-settings.js +577 -0
- package/dist/server/render-tasks.d.ts +632 -0
- package/dist/server/render-tasks.js +2345 -0
- package/dist/server/request-context.d.ts +46 -0
- package/dist/server/request-context.js +17 -0
- package/dist/server/route-table.d.ts +25 -3
- package/dist/server/route-table.js +163 -75
- package/dist/server/runtime.d.ts +15 -7
- package/dist/server/runtime.js +7 -1
- package/dist/server/session.d.ts +138 -0
- package/dist/server/session.js +94 -0
- package/dist/server/settings.d.ts +1 -5
- package/dist/server/settings.js +2182 -2117
- package/dist/server/task-views.d.ts +11 -4
- package/dist/server/task-views.js +26 -22
- package/dist/server/tasks.d.ts +9 -9
- package/dist/server/tasks.js +1486 -1418
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/skills-ui.d.ts +4 -3
- package/dist/skills-ui.js +14 -17
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -1
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -4
- package/dist/slack-chat.js +29 -52
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +23 -57
- package/dist/slack.js +6 -16
- package/dist/spend-ui.d.ts +2 -1
- package/dist/spend-ui.js +9 -32
- package/dist/sso-ui.d.ts +3 -2
- package/dist/sso-ui.js +26 -27
- package/dist/storage-ui.d.ts +2 -1
- package/dist/storage-ui.js +14 -33
- package/dist/store.d.ts +97 -364
- package/dist/store.js +207 -1531
- package/dist/surface.js +14 -3
- package/dist/task-composer.d.ts +2 -1
- package/dist/task-composer.js +7 -7
- package/dist/task-status.d.ts +4 -3
- package/dist/task-status.js +15 -16
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teammate-question.js +12 -12
- package/dist/teammates-ui.d.ts +5 -4
- package/dist/teammates-ui.js +61 -90
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +22 -33
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +50 -3
- package/dist/tools-ui.d.ts +5 -2
- package/dist/tools-ui.js +31 -31
- package/dist/when-html.d.ts +11 -3
- package/dist/when-html.js +5 -6
- package/dist/workspace-revision.js +3 -1
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -1
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
package/dist/flow-gallery-ui.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { brandMarkHtml } from "./brand-mark.js";
|
|
8
8
|
import { BLANK, GALLERY, GALLERY_GROUPS, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
|
|
9
9
|
import { choiceTargets } from "./flows.js";
|
|
10
|
-
|
|
10
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
11
11
|
export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
|
|
12
12
|
`.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}` +
|
|
13
13
|
`.gallery-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}` +
|
|
@@ -53,30 +53,28 @@ export function mainPath(definition) {
|
|
|
53
53
|
const stepName = (stage) => stage.kind === "choose" ? "You choose" : stage.kind === "approval" ? "You approve" : stage.kind === "send" ? "Sent to you" : stage.title;
|
|
54
54
|
/** A flow's steps in order, joined by arrows; a person's decisions carry the accent. */
|
|
55
55
|
export function stepStrip(definition) {
|
|
56
|
-
return `<ol class="gallery-steps" aria-label="Steps">${mainPath(definition).map((one, i) => {
|
|
57
|
-
const name =
|
|
58
|
-
return `<li>${i === 0 ? "" : `<span aria-hidden="true">→</span>`}${one.kind === "choose" || one.kind === "approval" ? `<b data-person>${name}</b>` : name}</li>`;
|
|
59
|
-
})
|
|
56
|
+
return html `<ol class="gallery-steps" aria-label="Steps">${mainPath(definition).map((one, i) => {
|
|
57
|
+
const name = stepName(one);
|
|
58
|
+
return html `<li>${i === 0 ? "" : html `<span aria-hidden="true">→</span>`}${one.kind === "choose" || one.kind === "approval" ? html `<b data-person>${name}</b>` : name}</li>`;
|
|
59
|
+
})}</ol>`;
|
|
60
60
|
}
|
|
61
|
-
const needsList = (template) => template.needs.length === 0 ? (template.tools ?? []).length > 0 ? "" : `<ul class="gallery-needs"><li>Nothing to connect</li></ul>`
|
|
62
|
-
: `<ul class="gallery-needs" aria-label="Needs">${template.needs.map(one => `<li>${
|
|
61
|
+
const needsList = (template) => template.needs.length === 0 ? (template.tools ?? []).length > 0 ? "" : html `<ul class="gallery-needs"><li>Nothing to connect</li></ul>`
|
|
62
|
+
: html `<ul class="gallery-needs" aria-label="Needs">${template.needs.map(one => html `<li>${one}</li>`)}</ul>`;
|
|
63
63
|
/** A tool's state in words, as Settings → Integrations badges it; nothing without a project. */
|
|
64
64
|
const STATE = { connected: ["connected", "Connected"], open: ["not-set-up", "Not connected"], taken: ["not-set-up", "Added another way"] };
|
|
65
|
-
const toolState = (tool) => tool.state === null ? "" : `<span class="integration-state integration-state--${STATE[tool.state][0]}"><i aria-hidden="true"></i>${STATE[tool.state][1]}</span>`;
|
|
65
|
+
const toolState = (tool) => tool.state === null ? "" : html `<span class="integration-state integration-state--${STATE[tool.state][0]}"><i aria-hidden="true"></i>${STATE[tool.state][1]}</span>`;
|
|
66
66
|
const toolMark = (tool) => brandMarkHtml(tool.id.replace(/-desktop$/, ""), tool.label, tool.state === "connected");
|
|
67
67
|
const toolsList = (tools) => tools.length === 0 ? ""
|
|
68
|
-
: `<ul class="gallery-tools" aria-label="Tools">${tools.map(one => `<li data-tool="${
|
|
68
|
+
: html `<ul class="gallery-tools" aria-label="Tools">${tools.map(one => html `<li data-tool="${one.id}" data-state="${one.state ?? "unknown"}">${toolMark(one)}<strong>${one.label}</strong>${toolState(one)}</li>`)}</ul>`;
|
|
69
69
|
/** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
|
|
70
70
|
export function galleryHtml(input) {
|
|
71
71
|
const href = (id) => `/flows/new/${id}${input.repo === null ? "" : `?repo=${encodeURIComponent(input.repo)}`}`;
|
|
72
72
|
const card = (template) => {
|
|
73
73
|
const definition = galleryDiagram(template);
|
|
74
|
-
return `<article class="gallery-card" data-template="${
|
|
75
|
-
stepStrip(definition) + toolsList(galleryToolsOf(template, definition, input.repo === null ? null : input.connections ?? null)) + needsList(template) +
|
|
76
|
-
(input.canUse ? `<a class="button-link" href="${e(href(template.id))}" aria-label="Use ${e(template.name)}">Use this</a>` : "") + `</article>`;
|
|
74
|
+
return html `<article class="gallery-card" data-template="${template.id}"><h3>${template.name}</h3><p class="gallery-promise">${template.promise}</p>${stepStrip(definition)}${toolsList(galleryToolsOf(template, definition, input.repo === null ? null : input.connections ?? null))}${needsList(template)}${input.canUse && html `<a class="button-link" href="${href(template.id)}" aria-label="Use ${template.name}">Use this</a>`}</article>`;
|
|
77
75
|
};
|
|
78
|
-
const groups = GALLERY_GROUPS.map(group => `<h2 id="gallery-${group.id}">${
|
|
79
|
-
return `<section class="gallery">${groups}${input.canUse ? `<p class="gallery-blank meta"><a href="${
|
|
76
|
+
const groups = GALLERY_GROUPS.map(group => html `<h2 id="gallery-${group.id}">${group.label}</h2><div class="gallery-grid" data-group="${group.id}">${GALLERY.filter(one => one.group === group.id).map(card)}</div>`);
|
|
77
|
+
return html `<section class="gallery">${groups}${input.canUse ? html `<p class="gallery-blank meta"><a href="${href(BLANK.id)}">Start from a blank flow</a></p>` : html `<p class="meta">An approver creates flows.</p>`}</section>`;
|
|
80
78
|
}
|
|
81
79
|
/** A template's page: its questions, then what it will do and never do, then Create. */
|
|
82
80
|
export function galleryUseHtml(input) {
|
|
@@ -86,43 +84,30 @@ export function galleryUseHtml(input) {
|
|
|
86
84
|
const missing = tools.filter(one => one.state !== "connected" && one.state !== "taken");
|
|
87
85
|
// Each tool with its state; one the project lacks offers its own Connect (the Tools page's, with the same password), and comes back here.
|
|
88
86
|
const connect = (tool) => {
|
|
89
|
-
const head = `<p class="gallery-tool-head">${toolMark(tool)} <strong>${
|
|
87
|
+
const head = html `<p class="gallery-tool-head">${toolMark(tool)} <strong>${tool.label}</strong> ${toolState(tool)}</p>`;
|
|
90
88
|
// Added another way, it is there: nothing to connect.
|
|
91
89
|
if (tool.state === "connected" || tool.state === "taken" || input.csrf === "")
|
|
92
|
-
return `<div class="gallery-connect" data-tool="${
|
|
90
|
+
return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="${String(tool.state)}">${head}</div>`;
|
|
93
91
|
const how = tool.id === "figma-desktop" ? "Open the Figma desktop app and turn on its Dev Mode MCP server in Preferences." : `You sign in on ${tool.label}, then come back here.`;
|
|
94
|
-
return `<div class="gallery-connect" data-tool="${
|
|
95
|
-
`<input type="hidden" name="csrf" value="${e(input.csrf)}"><input type="hidden" name="repo" value="${e(input.repo)}"><input type="hidden" name="shown" value="${e(input.repo)}"><input type="hidden" name="template" value="${e(template.id)}">` +
|
|
96
|
-
`<p class="meta">${e(how)}</p><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>` +
|
|
97
|
-
`<button name="service" value="${e(tool.id)}">Connect ${e(tool.label)}</button></form></div>`;
|
|
92
|
+
return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="open">${head}${postForm("/settings/tools/connect", html `<p class="meta">${how}</p><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button name="service" value="${tool.id}">Connect ${tool.label}</button>`, { hidden: { repo: input.repo, shown: input.repo, template: template.id } })}</div>`;
|
|
98
93
|
};
|
|
99
|
-
const toolsBlock = tools.length === 0 ? "" : `<section class="gallery-tools-use" aria-label="Tools">${tools.map(connect)
|
|
94
|
+
const toolsBlock = tools.length === 0 ? "" : html `<section class="gallery-tools-use" aria-label="Tools">${tools.map(connect)}</section>`;
|
|
100
95
|
// Made without a tool, the preview names the zone that needs it.
|
|
101
|
-
const zones = (tool) => tool.zones.map(zone => `“${
|
|
102
|
-
const needing = missing.filter(one => one.zones.length > 0).map(one => `<p class="meta" data-needs-tool="${
|
|
103
|
-
tools.filter(one => one.state === "taken" && one.zones.length > 0).map(one => `<p class="meta" data-tool-taken="${e(one.id)}">${zones(one)} use${one.zones.length === 1 ? "s" : ""} ${e(one.label)}, added another way.</p>`).join("");
|
|
96
|
+
const zones = (tool) => joinHtml(tool.zones.map(zone => html `“${zone}”`), " and ");
|
|
97
|
+
const needing = html `${missing.filter(one => one.zones.length > 0).map(one => html `<p class="meta" data-needs-tool="${one.id}">${zones(one)} need${one.zones.length === 1 ? "s" : ""} ${one.label}, which isn't connected.</p>`)}${tools.filter(one => one.state === "taken" && one.zones.length > 0).map(one => html `<p class="meta" data-tool-taken="${one.id}">${zones(one)} use${one.zones.length === 1 ? "s" : ""} ${one.label}, added another way.</p>`)}`;
|
|
104
98
|
const field = (ask) => {
|
|
105
99
|
const value = input.answers[ask.key] ?? ask.default;
|
|
106
|
-
const hint = ask.hint === undefined ? "" : ` <small>${
|
|
100
|
+
const hint = ask.hint === undefined ? "" : html ` <small>${ask.hint}</small>`;
|
|
107
101
|
if (ask.key === "outdated")
|
|
108
|
-
return `<label>${
|
|
109
|
-
return `<label>${
|
|
102
|
+
return html `<label>${ask.label}<select name="outdated">${Object.entries(OUTDATED_COMMANDS).map(([id, one]) => html `<option value="${id}"${id === value && html ` selected`}>${one.label}</option>`)}</select></label>`;
|
|
103
|
+
return html `<label>${ask.label}${hint}<input name="${ask.key}" value="${value}" required maxlength="${ask.key === "command" ? 500 : ask.key === "team" ? 12 : 100}"${ask.key === "command" ? html ` spellcheck="false" autocapitalize="off"` : ask.key === "team" ? html ` spellcheck="false" autocapitalize="characters"` : ""}></label>`;
|
|
110
104
|
};
|
|
111
105
|
const project = input.projects.length > 1
|
|
112
|
-
? `<label>Project<select name="repo">${input.projects.map(one => `<option value="${
|
|
113
|
-
: `<input type="hidden" name="repo" value="${
|
|
114
|
-
const preview = input.preview === null ? "" : `<section class="gallery-preview" aria-labelledby="gallery-will">`
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return `<section class="gallery-use" data-gallery-template="${e(template.id)}">` +
|
|
120
|
-
(input.problem === null ? "" : `<p class="problem" role="alert">${e(input.problem)}</p>`) + (input.said ? `<p role="status">${e(input.said)}</p>` : "") +
|
|
121
|
-
`<p>${e(template.promise)}</p>${input.diagram === null ? "" : stepStrip(input.diagram)}${needsList(template)}${toolsBlock}` +
|
|
122
|
-
`<form method="post" action="/flows/new/${e(template.id)}" data-gallery-use><input type="hidden" name="csrf" value="${e(input.csrf)}">` +
|
|
123
|
-
`<input type="hidden" name="previewed" value="${e(input.preview?.digest ?? "")}">${project}${template.asks.map(field).join("")}` +
|
|
124
|
-
`<label>Name<input name="name" value="${e(input.name)}" maxlength="80"></label>` +
|
|
125
|
-
(sendsAlready(template) ? "" : `<label class="gallery-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" ? " checked" : ""} data-send-result><span>${e(SEND_RESULT.title)}<small>When a card finishes, what was done comes to you in your chat apps.</small></span></label>`) + preview +
|
|
126
|
-
`<p class="gallery-actions">${input.preview === null ? "" : `<button type="submit" name="intent" value="create"${missing.length > 0 ? ' class="secondary" data-without-tools' : ""}>Create flow</button>`}<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button></p>` +
|
|
127
|
-
`</form><p class="meta"><a href="/flows/new">All templates</a></p></section>`;
|
|
106
|
+
? html `<label>Project<select name="repo">${input.projects.map(one => html `<option value="${one.path}"${one.path === input.repo && html ` selected`}>${one.name}</option>`)}</select></label>`
|
|
107
|
+
: html `<input type="hidden" name="repo" value="${input.repo}">`;
|
|
108
|
+
const preview = input.preview === null ? "" : html `<section class="gallery-preview" aria-labelledby="gallery-will"><h2 id="gallery-will">What it will do</h2><ul>${input.preview.built.does.map(one => html `<li>${one}</li>`)}</ul><p class="gallery-never">${input.preview.built.never}</p>${needing}${input.preview.startsFrom.length === 0 ? html `<p class="meta">Cards start when you add them.</p>` : input.preview.startsFrom.map(one => html `<p class="meta">Starts from: ${one}</p>`)}<details><summary>Every step</summary><ol>${input.preview.steps.map(one => html `<li>${one.replace(/^\d+\.\s/, "")}</li>`)}</ol></details></section>`;
|
|
109
|
+
const sendResult = !sendsAlready(template) && html `<label class="gallery-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" && html ` checked`} data-send-result><span>${SEND_RESULT.title}<small>When a card finishes, what was done comes to you in your chat apps.</small></span></label>`;
|
|
110
|
+
const actions = html `<p class="gallery-actions">${input.preview !== null && html `<button type="submit" name="intent" value="create"${missing.length > 0 && html ` class="secondary" data-without-tools`}>Create flow</button>`}<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button></p>`;
|
|
111
|
+
const form = postForm(`/flows/new/${template.id}`, html `${project}${template.asks.map(field)}<label>Name<input name="name" value="${input.name}" maxlength="80"></label>${sendResult}${preview}${actions}`, { attrs: { "data-gallery-use": true }, hidden: { previewed: input.preview?.digest ?? "" } });
|
|
112
|
+
return html `<section class="gallery-use" data-gallery-template="${template.id}">${input.problem !== null && html `<p class="problem" role="alert">${input.problem}</p>`}${input.said ? html `<p role="status">${input.said}</p>` : ""}<p>${template.promise}</p>${input.diagram !== null && stepStrip(input.diagram)}${needsList(template)}${toolsBlock}${form}<p class="meta"><a href="/flows/new">All templates</a></p></section>`;
|
|
128
113
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* that's on links to its flow; one that can't run here says why.
|
|
5
5
|
*/
|
|
6
6
|
import type { StarterView } from "./flow-starters.js";
|
|
7
|
+
import { type Html } from "./html.js";
|
|
7
8
|
export declare const STARTERS_CSS: string;
|
|
8
9
|
export declare function startersHtml(input: {
|
|
9
10
|
repo: string;
|
|
@@ -17,4 +18,4 @@ export declare function startersHtml(input: {
|
|
|
17
18
|
suggested: string | null;
|
|
18
19
|
said: string | null;
|
|
19
20
|
problem: string | null;
|
|
20
|
-
}):
|
|
21
|
+
}): Html;
|
package/dist/flow-starters-ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { html, postForm } from "./html.js";
|
|
2
2
|
export const STARTERS_CSS = `.starters{max-width:760px;min-width:0}.starters h2{margin:24px 0 4px;font-size:.9375rem}` +
|
|
3
3
|
`.starter{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 16px;align-items:start;padding:14px 0;border-top:1px solid var(--so-line);min-width:0}` +
|
|
4
4
|
`.starter:first-of-type{border-top:0}.starter>:not(.starter-action){grid-column:1}.starter h3{margin:0;font-size:.9375rem;display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center}` +
|
|
@@ -13,23 +13,20 @@ export const STARTERS_CSS = `.starters{max-width:760px;min-width:0}.starters h2{
|
|
|
13
13
|
`@media (max-width:560px){.starter{grid-template-columns:minmax(0,1fr)}.starter .starter-action{grid-column:1;grid-row:auto;margin-top:10px}` +
|
|
14
14
|
`.starter .starter-action button,.starter .starter-action .button-link{width:100%;justify-content:center}.starters select{font-size:16px}}`;
|
|
15
15
|
export function startersHtml(input) {
|
|
16
|
-
const note = input.problem ? `<p class="problem" role="alert">${
|
|
16
|
+
const note = input.problem ? html `<p class="problem" role="alert">${input.problem}</p>` : input.said ? html `<p role="status">${input.said}</p>` : "";
|
|
17
17
|
const selector = input.projects.length > 1
|
|
18
|
-
? `<form class="starters-project" method="get" action="/settings/flows"><label>Project<select name="repo">${input.projects.map(one => `<option value="${
|
|
18
|
+
? html `<form class="starters-project" method="get" action="/settings/flows"><label>Project<select name="repo">${input.projects.map(one => html `<option value="${one.path}"${one.path === input.repo && html ` selected`}>${one.name}</option>`)}</select></label><button>Show</button></form>` : "";
|
|
19
19
|
// The one asked about ("Do this every time…") comes first, marked.
|
|
20
20
|
const ordered = [...input.starters].sort((a, b) => Number(b.id === input.suggested) - Number(a.id === input.suggested));
|
|
21
21
|
const rows = ordered.map(one => {
|
|
22
|
-
const action = one.on !== null ? `<p class="starter-action"><a class="button-link starter-open" href="/flows/${one.on.flow}">Open flow</a></p>`
|
|
22
|
+
const action = one.on !== null ? html `<p class="starter-action"><a class="button-link starter-open" href="/flows/${one.on.flow}">Open flow</a></p>`
|
|
23
23
|
: one.blocked !== null || !input.canSwitch ? ""
|
|
24
|
-
:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}).join("");
|
|
33
|
-
const nobody = !input.canSwitch && input.starters.some(one => one.on === null) ? `<p class="meta">An approver switches these on.</p>` : "";
|
|
34
|
-
return `<section class="starters">${note}${selector}<h2>Starter flows</h2>${rows}${nobody}<p class="meta"><a href="/flows">All flows</a></p></section>`;
|
|
24
|
+
: postForm("/settings/flows/on", html `<button type="submit">Switch on</button>`, { attrs: { class: "starter-action" }, hidden: { repo: input.repo, starter: one.id } });
|
|
25
|
+
// What the yes agrees to, in full, before it; nothing to agree to once it's on or can't be.
|
|
26
|
+
const agrees = one.on === null && one.blocked === null && html `<ul>${one.does.map(line => html `<li>${line}</li>`)}</ul><p class="starter-never">${one.never}</p>`;
|
|
27
|
+
const head = html `<h3>${one.name}${one.on === null ? "" : html ` <span class="starter-on"><i aria-hidden="true"></i>On</span>`}</h3>`;
|
|
28
|
+
return html `<div class="starter" id="starter-${one.id}" data-starter="${one.id}" data-suggested="${String(one.id === input.suggested && one.on === null)}">${head}<p>${one.summary}</p>${agrees}${one.on === null && one.blocked !== null && html `<p class="starter-blocked">${one.blocked}</p>`}${action}</div>`;
|
|
29
|
+
});
|
|
30
|
+
const nobody = !input.canSwitch && input.starters.some(one => one.on === null) ? html `<p class="meta">An approver switches these on.</p>` : "";
|
|
31
|
+
return html `<section class="starters">${note}${selector}<h2>Starter flows</h2>${rows}${nobody}<p class="meta"><a href="/flows">All flows</a></p></section>`;
|
|
35
32
|
}
|
package/dist/flow-triggers.d.ts
CHANGED
|
@@ -208,6 +208,7 @@ export declare const FORM_PATH = "/hooks/form/";
|
|
|
208
208
|
/** Share a button as a secret link anyone can fill in, without signing in. The link is shown once; only its hash is kept. */
|
|
209
209
|
export declare function shareFlowButton(store: Store, trigger: FlowTriggerRow, now: Date, dir: string | null): TriggerMade;
|
|
210
210
|
export declare function stopSharingFlowButton(store: Store, trigger: FlowTriggerRow, now: Date): void;
|
|
211
|
+
/** The page's bytes (the public route writes them itself, with its own headers). */
|
|
211
212
|
export type FormAnswer = {
|
|
212
213
|
status: number;
|
|
213
214
|
html: string;
|
package/dist/flow-triggers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { adapterPolicy } from "./server/route-policy.js";
|
|
2
|
+
import { html, htmlString, postForm, styleElement } from "./html.js";
|
|
2
3
|
/**
|
|
3
4
|
* Flow triggers (v82): what starts cards in a flow without someone adding
|
|
4
5
|
* them by hand.
|
|
@@ -1107,11 +1108,7 @@ export function shareFlowButton(store, trigger, now, dir) {
|
|
|
1107
1108
|
export function stopSharingFlowButton(store, trigger, now) {
|
|
1108
1109
|
store.updateFlowTrigger(trigger.id, { hookHash: null }, now);
|
|
1109
1110
|
}
|
|
1110
|
-
const
|
|
1111
|
-
/** The public page: the button's name and its questions, nothing about the flow, the project or anyone in it. */
|
|
1112
|
-
function formHtml(label, body) {
|
|
1113
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex">
|
|
1114
|
-
<title>${esc(label)}</title><style>
|
|
1111
|
+
const FORM_CSS = `
|
|
1115
1112
|
:root{color-scheme:light dark;--bg:#f7f7f5;--card:#fff;--text:#1c1d1b;--muted:#666a63;--line:#dcded8;--accent:#2d5a45}
|
|
1116
1113
|
@media (prefers-color-scheme:dark){:root{--bg:#141614;--card:#1d201d;--text:#ecefe9;--muted:#9ea39a;--line:#343934;--accent:#8fd0ae}}
|
|
1117
1114
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
@@ -1119,18 +1116,23 @@ main{max-width:560px;margin:0 auto;padding:40px 16px}h1{font-size:1.5rem;margin:
|
|
|
1119
1116
|
label{display:grid;gap:6px;margin:0 0 16px;font-weight:600}input,textarea{font:inherit;color:inherit;background:transparent;border:1px solid var(--line);border-radius:10px;padding:10px 12px;width:100%}
|
|
1120
1117
|
textarea{min-height:96px;resize:vertical}button{font:inherit;font-weight:600;border:0;border-radius:10px;padding:12px 18px;min-height:44px;background:var(--accent);color:var(--bg);cursor:pointer}
|
|
1121
1118
|
p{color:var(--muted)}.trap{position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden}a{color:var(--accent)}
|
|
1122
|
-
|
|
1119
|
+
`;
|
|
1120
|
+
/** The public page: the button's name and its questions, nothing about the flow, the project or anyone in it. */
|
|
1121
|
+
function formHtml(label, body) {
|
|
1122
|
+
return htmlString(html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex">
|
|
1123
|
+
<title>${label}</title>${styleElement(FORM_CSS)}</head><body><main><h1>${label}</h1><div class="card">${body}</div></main></body></html>`);
|
|
1123
1124
|
}
|
|
1124
1125
|
/** GET a shared form: its questions. */
|
|
1125
1126
|
export function flowFormPage(store, token, now) {
|
|
1126
1127
|
const trigger = /^[A-Za-z0-9_-]{20,64}$/.test(token) ? store.flowTriggerByHook(hookHash(token)) : null;
|
|
1127
1128
|
const config = trigger === null ? null : triggerConfigOf(trigger);
|
|
1128
1129
|
if (trigger === null || config?.kind !== "button")
|
|
1129
|
-
return { status: 404, html: formHtml("Not found",
|
|
1130
|
+
return { status: 404, html: formHtml("Not found", html `<p>This form isn't available. Ask whoever shared it for a new link.</p>`) };
|
|
1130
1131
|
if (!adapterPolicy({ caller: "anonymous", capability: "none" }, undefined, source => source === "flow" && store.getFlow(trigger.flow) !== null).ok)
|
|
1131
1132
|
return { status: 403, html: "" };
|
|
1132
|
-
const fields = config.questions.map((question, index) => `<label>${
|
|
1133
|
-
|
|
1133
|
+
const fields = config.questions.map((question, index) => html `<label>${question}${index === 0 ? html `<input name="a${index}" required maxlength="200" autocomplete="off">` : html `<textarea name="a${index}" maxlength="3000"></textarea>`}</label>`);
|
|
1134
|
+
// Signed out by nature: no session, so no CSRF; the trap, the shown time and the hourly limit guard it. It posts back to its own address.
|
|
1135
|
+
return { status: 200, html: formHtml(config.label, postForm("", html `${fields}<div class="trap" aria-hidden="true"><label>Leave this empty<input name="website" tabindex="-1" autocomplete="off"></label></div><input type="hidden" name="t" value="${now.getTime()}"><button>Send</button>`, { signedOut: true })) };
|
|
1134
1136
|
}
|
|
1135
1137
|
/** POST a shared form: at most one card per submission, keys refused, a limit per hour, and quiet about what a bot did wrong. */
|
|
1136
1138
|
export function receiveFlowForm(store, token, fields, now) {
|
|
@@ -1140,8 +1142,8 @@ export function receiveFlowForm(store, token, fields, now) {
|
|
|
1140
1142
|
return flowFormPage(store, token, now);
|
|
1141
1143
|
if (!adapterPolicy({ caller: "anonymous", capability: "none" }, undefined, source => source === "flow" && store.getFlow(trigger.flow) !== null).ok)
|
|
1142
1144
|
return { status: 403, html: "" };
|
|
1143
|
-
const again = `<p><a href="${
|
|
1144
|
-
const thanks = formHtml(config.label, `<p>Thanks — it's been sent.</p>${again}`);
|
|
1145
|
+
const again = html `<p><a href="${FORM_PATH + token}">Send another</a></p>`;
|
|
1146
|
+
const thanks = formHtml(config.label, html `<p>Thanks — it's been sent.</p>${again}`);
|
|
1145
1147
|
// A filled trap, or a page posted faster than a person could, gets thanks and makes nothing.
|
|
1146
1148
|
const form = readFormSubmission(fields, config.questions.length);
|
|
1147
1149
|
const shown = Number(form.t);
|
|
@@ -1149,14 +1151,14 @@ export function receiveFlowForm(store, token, fields, now) {
|
|
|
1149
1151
|
return { status: 200, html: thanks };
|
|
1150
1152
|
const recent = store.handle.prepare("SELECT COUNT(*) AS n FROM flow_trigger_event WHERE trigger = ? AND key LIKE 'form:%' AND at > ?").get(trigger.id, new Date(now.getTime() - 3_600_000).toISOString());
|
|
1151
1153
|
if (Number(recent?.["n"] ?? 0) >= FORM_CARDS_PER_HOUR)
|
|
1152
|
-
return { status: 429, html: formHtml(config.label, `<p>Too many sent in the last hour. Try again later.</p>`) };
|
|
1154
|
+
return { status: 429, html: formHtml(config.label, html `<p>Too many sent in the last hour. Try again later.</p>`) };
|
|
1153
1155
|
const answers = form.answers.map((answer, index) => (answer ?? "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim().slice(0, index === 0 ? 200 : 3000));
|
|
1154
1156
|
if (answers[0] === "")
|
|
1155
|
-
return { status: 400, html: formHtml(config.label, `<p>Answer “${
|
|
1157
|
+
return { status: 400, html: formHtml(config.label, html `<p>Answer “${config.questions[0]}” first.</p>${again}`) };
|
|
1156
1158
|
const details = config.questions.slice(1).map((question, index) => answers[index + 1] === "" ? null : `${question}\n${answers[index + 1]}`).filter(one => one !== null).join("\n\n");
|
|
1157
1159
|
const made = makeCard(store, trigger, config, { key: `form:${now.toISOString()}:${randomUUID().slice(0, 8)}`, title: answers[0], description: `From the “${config.label}” form:\n\n${details}`.trim(), source: { kind: "form", label: `Form: ${config.label}`, url: null } }, "Form", now);
|
|
1158
1160
|
if (made.made !== "added")
|
|
1159
|
-
return { status: 400, html: formHtml(config.label, `<p>${made.note?.includes("key or password") ? "That looks like it holds a key or password. Take it out and send again." : "That couldn't be sent."}</p>${again}`) };
|
|
1161
|
+
return { status: 400, html: formHtml(config.label, html `<p>${made.note?.includes("key or password") ? "That looks like it holds a key or password. Take it out and send again." : "That couldn't be sent."}</p>${again}`) };
|
|
1160
1162
|
store.updateFlowTrigger(trigger.id, { lastAt: now.toISOString(), lastOutcome: "Someone sent the form." }, now);
|
|
1161
1163
|
return { status: 200, html: thanks };
|
|
1162
1164
|
}
|
package/dist/flows-ui.d.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import type { BrowserFlowView } from "./browser-workspace.js";
|
|
4
4
|
import type { FlowRow, Store } from "./store.js";
|
|
5
5
|
import { type FlowFile, type FlowImportPlan } from "./flow-share.js";
|
|
6
|
+
import { type Html } from "./html.js";
|
|
6
7
|
export declare const FLOWS_CSS = ".flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows .flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.flows .flow-row h2{font-size:1.05rem;margin:0}.flows .flow-counts{font-size:.85rem;color:var(--so-muted)}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows .flow-buttons{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:10px 0 0}.flows .flow-import{margin-top:18px;padding-top:14px;border-top:1px solid var(--so-line)}.flows .flow-import h3{font-size:.95rem;margin:0}.flows .flow-terms{list-style:none;padding:0;margin:8px 0 16px;display:grid;gap:10px}.flows .flow-terms li{white-space:pre-line;overflow-wrap:anywhere}.flows .flow-terms pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.8rem;margin:6px 0 0}.flows .flow-import-preview h2{font-size:1rem;margin:20px 0 4px}.flows .flow-untrusted{font-weight:600}.flows button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary)}.flows fieldset{border:0;padding:0;margin:0;display:grid;gap:10px;min-width:0}.flows legend{font-weight:600;padding:0;margin-bottom:6px}.flows textarea[hidden]{display:none}.flows button.secondary{background:transparent;color:var(--foreground);border:1px solid var(--border)}@media(max-width:600px){.flows input,.flows select{font-size:16px}}@media(max-width:760px){.flows .card{padding:10px 12px;margin:8px 0;position:relative}.flows .flow-row{row-gap:2px}.flows .flow-row h2 a::after{content:\"\";position:absolute;inset:0;border-radius:inherit}.flows .flow-counts{line-height:1.35}}";
|
|
7
8
|
/** The flows a person can open, with how many cards wait in each, and the new-flow form. */
|
|
8
|
-
export declare function flowsListHtml(store: Store, flows: readonly FlowRow[], projects: readonly string[],
|
|
9
|
+
export declare function flowsListHtml(store: Store, flows: readonly FlowRow[], projects: readonly string[], canCreate: boolean, problem: string | null): Html;
|
|
9
10
|
/** Reads the chosen flow file into the form (the server reads it again, whole, before anything is made). */
|
|
10
11
|
export declare const FLOW_IMPORT_SCRIPT = "(function(){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]'),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(!file){reading=null;return;}if(file.size>262144){say('That file is too big: a flow file is at most 256 KB.');input.value='';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.');}});})();";
|
|
11
12
|
/** An import's preview: the plan in plain words, what it asks for, and one Import button after the terms. */
|
|
@@ -18,7 +19,7 @@ export declare function flowImportHtml(input: {
|
|
|
18
19
|
values: Record<string, string>;
|
|
19
20
|
previewed: string;
|
|
20
21
|
problem: string | null;
|
|
21
|
-
}):
|
|
22
|
+
}): Html;
|
|
22
23
|
/** One flow's canvas for one person. */
|
|
23
24
|
export declare function flowView(store: Store, flow: FlowRow, viewer: {
|
|
24
25
|
name: string;
|
|
@@ -31,4 +32,4 @@ export declare function flowView(store: Store, flow: FlowRow, viewer: {
|
|
|
31
32
|
toolHome?: string;
|
|
32
33
|
}): BrowserFlowView;
|
|
33
34
|
/** The page the canvas replaces when scripts can't run: each zone and its cards, in order. */
|
|
34
|
-
export declare function flowFallbackHtml(view: BrowserFlowView):
|
|
35
|
+
export declare function flowFallbackHtml(view: BrowserFlowView): Html;
|
package/dist/flows-ui.js
CHANGED
|
@@ -17,53 +17,42 @@ import { callWords, receiptWords } from "./teammate-tools.js";
|
|
|
17
17
|
import { undoFor } from "./teammate-week.js";
|
|
18
18
|
import { flowCardTaskLine } from "./flow-card-task.js";
|
|
19
19
|
import { readFlowSecrets } from "./flow-secrets.js";
|
|
20
|
-
|
|
20
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
21
21
|
const projectName = (repo) => repo.split(/[\\/]/).filter(Boolean).pop() ?? repo;
|
|
22
22
|
export const FLOWS_CSS = `.flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows .flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.flows .flow-row h2{font-size:1.05rem;margin:0}.flows .flow-counts{font-size:.85rem;color:var(--so-muted)}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows .flow-buttons{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:10px 0 0}.flows .flow-import{margin-top:18px;padding-top:14px;border-top:1px solid var(--so-line)}.flows .flow-import h3{font-size:.95rem;margin:0}.flows .flow-terms{list-style:none;padding:0;margin:8px 0 16px;display:grid;gap:10px}.flows .flow-terms li{white-space:pre-line;overflow-wrap:anywhere}.flows .flow-terms pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.8rem;margin:6px 0 0}.flows .flow-import-preview h2{font-size:1rem;margin:20px 0 4px}.flows .flow-untrusted{font-weight:600}.flows button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary)}.flows fieldset{border:0;padding:0;margin:0;display:grid;gap:10px;min-width:0}.flows legend{font-weight:600;padding:0;margin-bottom:6px}.flows textarea[hidden]{display:none}.flows button.secondary{background:transparent;color:var(--foreground);border:1px solid var(--border)}@media(max-width:600px){.flows input,.flows select{font-size:16px}}@media(max-width:760px){.flows .card{padding:10px 12px;margin:8px 0;position:relative}.flows .flow-row{row-gap:2px}.flows .flow-row h2 a::after{content:"";position:absolute;inset:0;border-radius:inherit}.flows .flow-counts{line-height:1.35}}`;
|
|
23
23
|
/** The flows a person can open, with how many cards wait in each, and the new-flow form. */
|
|
24
|
-
export function flowsListHtml(store, flows, projects,
|
|
24
|
+
export function flowsListHtml(store, flows, projects, canCreate, problem) {
|
|
25
25
|
const rows = flows.map(flow => {
|
|
26
26
|
const cards = store.flowCards(flow.id, false);
|
|
27
27
|
const definition = flowDefinitionOf(flow);
|
|
28
28
|
const waiting = cards.filter(card => ["approval", "choose"].includes(definition?.stages.find(one => one.id === card.stage)?.kind ?? "")).length;
|
|
29
29
|
const buttons = store.flowTriggers(flow.id).filter(one => one.state === "active").flatMap(one => { const config = triggerConfigOf(one); return config?.kind === "button" ? [{ id: one.id, label: config.label }] : []; });
|
|
30
30
|
const trouble = troubleWords(flowInsights(store, flow, new Date(), 7));
|
|
31
|
-
return `<article class="card"><div class="flow-row"><h2><a href="/flows/${flow.id}">${
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const projectField = projects.length === 1 ? `<input type="hidden" name="repo" value="${e(projects[0])}">` : `<label>Project<select name="repo">${projects.map(repo => `<option value="${e(repo)}">${e(projectName(repo))}</option>`).join("")}</select></label>`;
|
|
31
|
+
return html `<article class="card"><div class="flow-row"><h2><a href="/flows/${flow.id}">${flow.name}</a></h2><span class="flow-counts">${projectName(flow.repo)} · ${cards.length} card${cards.length === 1 ? "" : "s"} in progress${waiting > 0 ? ` · ${waiting} waiting for a decision` : ""}${trouble === null ? "" : ` · ${trouble}`}</span></div>${buttons.length > 0 && html `<p class="flow-buttons">${joinHtml(buttons.map(one => html `<a class="button-link" href="/flows/${flow.id}?start=${one.id}">${one.label}</a>`), " ")}</p>`}</article>`;
|
|
32
|
+
});
|
|
33
|
+
const projectField = projects.length === 1 ? html `<input type="hidden" name="repo" value="${projects[0]}">` : html `<label>Project<select name="repo">${projects.map(repo => html `<option value="${repo}">${projectName(repo)}</option>`)}</select></label>`;
|
|
35
34
|
// A flow file (flow-share.ts): chosen here or fetched from a gist, previewed in plain words before anything is made.
|
|
36
|
-
const importForm =
|
|
35
|
+
const importForm = postForm("/flows/import", html `<h3>Or import a flow file</h3>${projectField}<label>File<input type="file" accept=".json,application/json" data-flow-file></label><textarea name="document" hidden data-flow-document></textarea><label>Or its address<input name="url" type="url" inputmode="url" placeholder="https://gist.github.com/…"></label><p class="problem" role="alert" data-flow-file-note hidden></p><button>Preview import</button>`, { attrs: { class: "flow-import", "data-flow-import": true } });
|
|
37
36
|
// A new flow starts from the gallery (Flows → New): templates grouped by what they're for.
|
|
38
|
-
const create = canCreate && projects.length > 0
|
|
37
|
+
const create = canCreate && projects.length > 0 && html `<p class="flow-buttons"><a class="button-link" href="/flows/new">New flow</a></p>${importForm}`;
|
|
39
38
|
// No flows yet: one click makes a working example with a sample question in it.
|
|
40
39
|
const example = flows.length === 0 && canCreate && projects.length > 0
|
|
41
|
-
?
|
|
42
|
-
:
|
|
43
|
-
const intro = `<p class="meta">A flow is your process drawn as zones. Cards move through them: agents do the work, people approve, and the team hears about it.</p>`
|
|
44
|
-
|
|
45
|
-
`<p class="flow-chat">AI teammates can decide and handle cards for you, within rules you write. <a href="/teammates">Teammates</a> · or set one up with its flow in one click: <a href="/kits">Starter kits</a></p>`;
|
|
46
|
-
return `<section class="flows">${problem === null ? "" : `<p class="problem" role="alert">${e(problem)}</p>`}${intro}${create}${rows || example || '<p class="meta">No flows yet.</p>'}</section>`;
|
|
40
|
+
? postForm("/flows/example", html `<h2>See a flow work</h2><p class="meta">Claude drafts a reply to a sample customer question; you approve it here or in your chat app.</p>${projectField}<button>Try an example</button>`, { attrs: { class: "card flow-example" } })
|
|
41
|
+
: null;
|
|
42
|
+
const intro = html `<p class="meta">A flow is your process drawn as zones. Cards move through them: agents do the work, people approve, and the team hears about it.</p>${canCreate && projects.length > 0 && html `<p class="flow-chat">Describe how work should move and your lead drafts the flow for you to confirm, or start from a template. <a href="/chat?draft=${encodeURIComponent("Make a flow for ")}">Describe it in chat</a></p>`}<p class="flow-chat">AI teammates can decide and handle cards for you, within rules you write. <a href="/teammates">Teammates</a> · or set one up with its flow in one click: <a href="/kits">Starter kits</a></p>`;
|
|
43
|
+
return html `<section class="flows">${problem !== null && html `<p class="problem" role="alert">${problem}</p>`}${intro}${create}${rows.length > 0 ? rows : example ?? html `<p class="meta">No flows yet.</p>`}</section>`;
|
|
47
44
|
}
|
|
48
45
|
/** Reads the chosen flow file into the form (the server reads it again, whole, before anything is made). */
|
|
49
46
|
export const FLOW_IMPORT_SCRIPT = `(function(){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]'),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(!file){reading=null;return;}if(file.size>${FLOW_FILE_MAX_BYTES}){say('That file is too big: a flow file is at most ${FLOW_FILE_MAX_BYTES / 1024} KB.');input.value='';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.');}});})();`;
|
|
50
47
|
/** An import's preview: the plan in plain words, what it asks for, and one Import button after the terms. */
|
|
51
48
|
export function flowImportHtml(input) {
|
|
52
49
|
const { plan, file } = input;
|
|
53
|
-
const fields = file.parameters.map(one => `<label><span>${
|
|
54
|
-
const list = (items) => items.length === 0 ? "" : `<ul class="flow-terms">${items.map(term => `<li>${
|
|
50
|
+
const fields = file.parameters.map(one => html `<label><span>${one.about}${one.optional === true && html ` <span class="meta">(optional)</span>`}</span><input name="param.${one.id}" value="${input.values[one.id] ?? plan?.values[one.id] ?? one.default ?? ""}" maxlength="200" autocomplete="off"></label>`);
|
|
51
|
+
const list = (items) => items.length === 0 ? "" : html `<ul class="flow-terms">${items.map(term => html `<li>${term}</li>`)}</ul>`;
|
|
55
52
|
// The steps, what they mean, the triggers and the scripts; each script's code one tap away, as on the Scripts panel.
|
|
56
|
-
const terms = plan === null ? "" :
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
(plan.parts.triggers.length === 0 ? "" : `<h2>Triggers</h2>${list(plan.parts.triggers)}`) +
|
|
60
|
-
(plan.parts.scripts.length + plan.parts.kept.length === 0 ? "" : `<h2>Scripts</h2><ul class="flow-terms">${plan.scripts.add.map((one, index) => `<li>${e(plan.parts.scripts[index])}${one.body === undefined ? "" : `<details><summary>Show the script</summary><pre>${e(one.body)}</pre></details>`}</li>`).join("")}${plan.parts.kept.map(one => `<li>${e(one)}</li>`).join("")}</ul>`) +
|
|
61
|
-
`<h2>Good to know</h2>${list([...plan.parts.notes, ...(plan.parts.needs === null ? [] : [plan.parts.needs])])}`;
|
|
62
|
-
return `<section class="flows flow-import-preview">${input.problem === null ? "" : `<p class="problem" role="alert">${e(input.problem)}</p>`}` +
|
|
63
|
-
`<p class="meta">Into ${e(projectName(input.repo))}. Nothing is made until you import it.</p>${terms}` +
|
|
64
|
-
`<form method="post" action="/flows/import" class="card"><input type="hidden" name="csrf" value="${e(input.csrf)}"><input type="hidden" name="repo" value="${e(input.repo)}"><input type="hidden" name="previewed" value="${e(input.previewed)}"><textarea name="document" hidden>${e(input.document)}</textarea>` +
|
|
65
|
-
(fields === "" ? "" : `<fieldset class="flow-import-fields"><legend>Fill in</legend>${fields}</fieldset>`) +
|
|
66
|
-
`<p class="flow-buttons">${plan === null ? "" : '<button name="confirm" value="yes" class="primary">Import flow</button>'}${fields === "" ? "" : `<button name="confirm" value="" class="${plan === null ? "" : "secondary"}">Update preview</button>`}<a href="/flows">Cancel</a></p></form></section>`;
|
|
53
|
+
const terms = plan === null ? "" : html `${plan.parts.about !== null && html `<p>${plan.parts.about}</p>`}<p class="flow-untrusted">Its instructions come from the file, not from you: read what each step is asked before you import.</p>${list(plan.parts.steps)}${plan.parts.triggers.length > 0 && html `<h2>Triggers</h2>${list(plan.parts.triggers)}`}${plan.parts.scripts.length + plan.parts.kept.length > 0 && html `<h2>Scripts</h2><ul class="flow-terms">${plan.scripts.add.map((one, index) => html `<li>${plan.parts.scripts[index]}${one.body !== undefined && html `<details><summary>Show the script</summary><pre>${one.body}</pre></details>`}</li>`)}${plan.parts.kept.map(one => html `<li>${one}</li>`)}</ul>`}<h2>Good to know</h2>${list([...plan.parts.notes, ...(plan.parts.needs === null ? [] : [plan.parts.needs])])}`;
|
|
54
|
+
const form = postForm("/flows/import", html `<textarea name="document" hidden>${input.document}</textarea>${fields.length > 0 && html `<fieldset class="flow-import-fields"><legend>Fill in</legend>${fields}</fieldset>`}<p class="flow-buttons">${plan !== null && html `<button name="confirm" value="yes" class="primary">Import flow</button>`}${fields.length > 0 && html `<button name="confirm" value="" class="${plan === null ? "" : "secondary"}">Update preview</button>`}<a href="/flows">Cancel</a></p>`, { attrs: { class: "card" }, hidden: { repo: input.repo, previewed: input.previewed } });
|
|
55
|
+
return html `<section class="flows flow-import-preview">${input.problem !== null && html `<p class="problem" role="alert">${input.problem}</p>`}<p class="meta">Into ${projectName(input.repo)}. Nothing is made until you import it.</p>${terms}${form}</section>`;
|
|
67
56
|
}
|
|
68
57
|
const historyText = (event, title, sorts = new Set()) => {
|
|
69
58
|
const who = event.actor === "flow" ? "" : ` by ${event.actor}`;
|
|
@@ -230,8 +219,8 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
230
219
|
}
|
|
231
220
|
/** The page the canvas replaces when scripts can't run: each zone and its cards, in order. */
|
|
232
221
|
export function flowFallbackHtml(view) {
|
|
233
|
-
return `<section class="flows flow-fallback"><p class="meta">${
|
|
222
|
+
return html `<section class="flows flow-fallback"><p class="meta">${view.flow.project}</p><ol>${view.stages.map(stage => {
|
|
234
223
|
const cards = view.cards.filter(card => card.stage === stage.id && card.state === "active");
|
|
235
|
-
return `<li><strong>${
|
|
236
|
-
})
|
|
224
|
+
return html `<li><strong>${stage.title}</strong> · ${FLOW_KIND_WORDS[stage.kind].label}${cards.length > 0 && html `<ul>${cards.map(card => html `<li>${card.title}${card.waiting !== null && ` — ${card.waiting}`}</li>`)}</ul>`}</li>`;
|
|
225
|
+
})}</ol></section>`;
|
|
237
226
|
}
|
package/dist/flows.js
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import { TEXT_LIMITS } from "./text-limits.js";
|
|
51
51
|
import { createHash } from "node:crypto";
|
|
52
|
+
import { projectName } from "./project.js";
|
|
52
53
|
import { parseContract, readVersioned } from "./contracts/contract.js";
|
|
53
54
|
import { stageReferenceProblems } from "./contracts/stage-output.js";
|
|
54
55
|
import { CHOICES_MAX, CHOICES_MIN, FLOW_ALIASES, FLOW_COLORS, FLOW_DEFINITION_VERSION, FLOW_END, FLOW_MERGE_METHODS, FLOW_STAGE_KINDS, flowDefinitionSchema, flowStepsSchema, HEADERS_MAX, LONGEST_WAIT_MINUTES, savedFlowDefinitionSchema, SCRIPT_LANGUAGES, SCRIPT_NAME, sortKeyOf, SURE_AT_MAX, SURE_AT_MIN, ZONE_ID, } from "./contracts/flow.js";
|
|
@@ -869,7 +870,7 @@ export function flowTerms(definition, previous) {
|
|
|
869
870
|
if (stage.kind === "task" && stage.planning !== "auto")
|
|
870
871
|
lines.push(stage.planning === "required" ? "Plans first." : "Builds without a plan.");
|
|
871
872
|
if (stage.kind === "task" && stage.repo !== undefined)
|
|
872
|
-
lines.push(`Builds in
|
|
873
|
+
lines.push(`Builds in project: ${projectName(stage.repo)}`);
|
|
873
874
|
if (stage.kind === "draft")
|
|
874
875
|
lines.push(`Claude writes: ${plain(stage.instructions ?? "")}`, `Then → ${to(stage.next)}`);
|
|
875
876
|
else if (stage.kind === "request" && stage.request !== undefined) {
|
package/dist/guides.js
CHANGED
|
@@ -65,7 +65,7 @@ ${AUTHORITY_LINE}
|
|
|
65
65
|
terminal, answer from the \`console\` guide (\`skills get console\`):
|
|
66
66
|
exact screens, exact verbs, and which acts are theirs alone.
|
|
67
67
|
- Take work as a registered runner: \`claim <id> --runner <name> --token
|
|
68
|
-
<t>\` → \`heartbeat <lease>\` while working → \`release <lease>\`. A
|
|
68
|
+
<t>\` → \`heartbeat <lease>\` while working → \`worker release <lease>\`. A
|
|
69
69
|
replayed claim (same \`--key\`) answers with \`replayed: true\` — do not
|
|
70
70
|
repeat first-time side effects on it.
|
|
71
71
|
- Respect refusals — each is an ANSWER, never an error to retry blindly:
|
|
@@ -189,7 +189,7 @@ registers it; the token is shown once). Work moves through leases:
|
|
|
189
189
|
not repeat first-time side effects.
|
|
190
190
|
- \`heartbeat <lease>\` while working — it extends the lease. A lease
|
|
191
191
|
that runs out is reaped and the task returns to the queue.
|
|
192
|
-
- \`release <lease>\` when done. If the answer is \`fenced\`, STOP: a newer
|
|
192
|
+
- \`worker release <lease>\` when done. If the answer is \`fenced\`, STOP: a newer
|
|
193
193
|
lease superseded yours and the work is no longer yours — do not write,
|
|
194
194
|
commit, or report anything further for it.
|
|
195
195
|
- \`reap\` releases every expired lease (safe to run any time).
|
|
@@ -416,7 +416,112 @@ and crew agent reads from it before working:
|
|
|
416
416
|
and its date, and prefer the newest active decision.
|
|
417
417
|
`,
|
|
418
418
|
};
|
|
419
|
-
|
|
419
|
+
const release = {
|
|
420
|
+
name: "release",
|
|
421
|
+
title: "Releasing Toolroll itself",
|
|
422
|
+
oneLiner: "toolroll release <branch>: one command from a gated commit to npm and Homebrew; the owner's approval is the only human step",
|
|
423
|
+
content: `# Releasing Toolroll itself
|
|
424
|
+
|
|
425
|
+
${AUTHORITY_LINE}
|
|
426
|
+
|
|
427
|
+
\`toolroll release <branch> --repo <gate checkout>\` runs the whole release
|
|
428
|
+
of a pushed branch. The owner approving the release scope is the only
|
|
429
|
+
step that needs a person; the rest runs on its own, each waiting step
|
|
430
|
+
with a time limit.
|
|
431
|
+
|
|
432
|
+
## Before you start
|
|
433
|
+
|
|
434
|
+
- The branch is pushed, with an open pull request into main.
|
|
435
|
+
- package.json has the new version, and the lead wrote its CHANGELOG.md
|
|
436
|
+
entry (\`## <version>\`). No tag or npm package has that version yet.
|
|
437
|
+
- \`gh\` is signed in, the console, worker and both CLI names
|
|
438
|
+
(\`toolroll\`, \`standing-orders\`) run one build, and the gate checkout
|
|
439
|
+
is a clone whose origin is the GitHub repository. Credentials are never
|
|
440
|
+
asked for in chat or written to the release journal.
|
|
441
|
+
|
|
442
|
+
## What it does, in order
|
|
443
|
+
|
|
444
|
+
1. **gate**: files task \`release-<version>-<sha7>\` pinned to the
|
|
445
|
+
branch's exact commit with \`--checks full\`, regardless of the project's default.
|
|
446
|
+
2. **approval**: waits for the owner to approve that exact scope
|
|
447
|
+
(\`toolroll task approve <id> --digest <d>\`, or the console). A
|
|
448
|
+
mode, an AI teammate or the lead approving it stops the release: the
|
|
449
|
+
yes must come from an active account with the approver role. Its standing
|
|
450
|
+
is checked again before completion and deployment.
|
|
451
|
+
3. **check**: requires a sealed, passing **Full** check on that commit.
|
|
452
|
+
Quick passes and Off checks are refused. Failures stay visible; nothing reruns them.
|
|
453
|
+
4. **complete**: marks that exact result Complete.
|
|
454
|
+
5. **quiet**: waits until nothing is building.
|
|
455
|
+
6. **deploy**: the candidate's own deployment
|
|
456
|
+
(\`scripts/deploy-browser.mjs\` from the builder's checkout): drain,
|
|
457
|
+
backup, rehearsal, swap, health. The staging path is saved before it
|
|
458
|
+
starts, and the runtime its final JSON names is kept. It goes on only
|
|
459
|
+
when the console and worker run that runtime and it records the gated
|
|
460
|
+
commit; matching versions alone do not count.
|
|
461
|
+
7. **link**: points \`toolroll\` and \`standing-orders\` at
|
|
462
|
+
\`<runtime>/bin.js\`, each link replaced whole. Where they pointed is
|
|
463
|
+
saved first; if either name doesn't answer as the new build, both go
|
|
464
|
+
back. It goes on only when the console, worker and both names run the
|
|
465
|
+
gated commit. A rerun relinks without deploying again.
|
|
466
|
+
8. **ci**: waits for every pull-request check on the gated commit:
|
|
467
|
+
Ubuntu on Node 22 and 24, Linux and Windows containment. Pending,
|
|
468
|
+
missing, skipped, cancelled or failed never merges.
|
|
469
|
+
9. **merge**: squash-merges with the head pinned to the gated commit,
|
|
470
|
+
once main has nothing the branch lacks, then deletes the remote branch
|
|
471
|
+
only if its head still matches. A resumed merge also finishes this cleanup.
|
|
472
|
+
10. **tree**: main's merge commit must have exactly the gated tree, or
|
|
473
|
+
nothing is tagged.
|
|
474
|
+
11. **tag**: pushes \`v<version>\` on that merge commit. In publish.yml,
|
|
475
|
+
macOS Node 22 and 24 must both pass before npm and GitHub publication.
|
|
476
|
+
macos.yml keeps the nightly matrix.
|
|
477
|
+
12. **publish**: waits for npm, the GitHub release and the tag's macOS
|
|
478
|
+
and publish checks.
|
|
479
|
+
13. **homebrew**: updates the tap's formula, waits for checks to register
|
|
480
|
+
and every registered check to pass, then merges. No checks means waiting,
|
|
481
|
+
within the Homebrew time limit; a failed or skipped check stops the release.
|
|
482
|
+
|
|
483
|
+
## When it stops
|
|
484
|
+
|
|
485
|
+
A time limit, a failure or Ctrl-C ends with the step and the reason,
|
|
486
|
+
and the command to continue: the same \`toolroll release <branch> --repo
|
|
487
|
+
<checkout>\`. Its journal (\`releases/<branch>.json\` in the state folder,
|
|
488
|
+
private) records what each step was about to do before it does it, so a
|
|
489
|
+
rerun finds the task, completion, merge, tag and tap change already made
|
|
490
|
+
instead of making them twice.
|
|
491
|
+
|
|
492
|
+
- \`drift\`: the branch or pull request moved after gating. Release the
|
|
493
|
+
new commit with \`--new\` (refused once merging began).
|
|
494
|
+
- \`check-level\`: the result has no Full check. File the gate with Full
|
|
495
|
+
checks and obtain a passing result; Quick and Off cannot authorize a release.
|
|
496
|
+
- \`check-failed\`: fix the branch and release the new commit with \`--new\`.
|
|
497
|
+
- \`ci-failed\`: fix the failed GitHub check, then rerun. Nothing was merged.
|
|
498
|
+
- \`publish-failed\` / \`homebrew-failed\`: fix the failed workflow before
|
|
499
|
+
resuming. A failed tag check cannot finish publication or update Homebrew.
|
|
500
|
+
- \`base-moved\`: main gained a commit the branch doesn't have, so the
|
|
501
|
+
merge wouldn't be the checked tree. Nothing was merged: update the
|
|
502
|
+
branch and release it with \`--new\`.
|
|
503
|
+
- \`tree-mismatch\`: main changed what was checked. Nothing was tagged;
|
|
504
|
+
the deployed build stays.
|
|
505
|
+
- \`timeout\`: rerun to keep waiting; \`--limit <step>=<minutes>\`
|
|
506
|
+
changes one step's limit.
|
|
507
|
+
- An interrupted deployment resumes with \`deploy-browser.mjs --stage <saved
|
|
508
|
+
path> --phase recover\` before any new attempt, including \`--new\`.
|
|
509
|
+
Failed recovery stops there; the original deployment journal remains available.
|
|
510
|
+
- \`link\`: a CLI name didn't answer as the new build, so both were put
|
|
511
|
+
back. The service already runs the new build; fix the name and rerun.
|
|
512
|
+
- \`gate-held\`: the deployment stopped before its swap but left new work
|
|
513
|
+
paused, and its journal can't say so by itself (typically killed while
|
|
514
|
+
preparing). \`toolroll release --release-gate <id>\` shows the proof that
|
|
515
|
+
the swap never began: the pause is that deployment's, its journal is
|
|
516
|
+
before the swap, the database schema, service definition and running
|
|
517
|
+
service are still the previous build's, and no deployment is running.
|
|
518
|
+
Add \`--yes\` to lift that one pause, then rerun the release. Missing or
|
|
519
|
+
contradicting evidence refuses and leaves the pause in place.
|
|
520
|
+
|
|
521
|
+
A runner's lease is returned with \`toolroll worker release <lease>\`.
|
|
522
|
+
`,
|
|
523
|
+
};
|
|
524
|
+
export const GUIDES = [operating, runner, steering, externalWork, console_, knowledge, release];
|
|
420
525
|
export function guideNamed(name) {
|
|
421
526
|
return GUIDES.find(one => one.name === name) ?? null;
|
|
422
527
|
}
|