toolroll 0.9.30 → 0.9.32
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 +35 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +8 -2
- package/dist/batch-checks.d.ts +15 -0
- package/dist/batch-checks.js +244 -0
- package/dist/batch-policy.d.ts +23 -0
- package/dist/batch-policy.js +25 -0
- package/dist/browser/workspace.js +13 -13
- package/dist/builder.js +14 -3
- package/dist/chat-actions.js +2 -2
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +21 -2
- package/dist/checkout-cleanup.d.ts +2 -1
- package/dist/checkout-cleanup.js +24 -8
- package/dist/desktop-update.d.ts +1 -1
- package/dist/exec.d.ts +2 -0
- package/dist/exec.js +11 -0
- package/dist/flow-gallery-ui.d.ts +14 -9
- package/dist/flow-gallery-ui.js +71 -29
- package/dist/flow-gallery.d.ts +26 -2
- package/dist/flow-gallery.js +182 -7
- package/dist/lead-capabilities.d.ts +0 -7
- package/dist/lead-capabilities.js +11 -1
- package/dist/mate-doors.js +2 -2
- package/dist/mate.d.ts +6 -2
- package/dist/mate.js +63 -38
- package/dist/mcp-connect.d.ts +3 -1
- package/dist/mcp-connect.js +11 -6
- package/dist/never-index.d.ts +3 -0
- package/dist/never-index.js +21 -0
- package/dist/operate.js +86 -2
- package/dist/orphan-sweep.d.ts +63 -0
- package/dist/orphan-sweep.js +235 -0
- package/dist/result-follow-ups.d.ts +22 -2
- package/dist/result-follow-ups.js +44 -10
- package/dist/serve.js +52 -19
- package/dist/shared-deps.d.ts +18 -2
- package/dist/shared-deps.js +129 -15
- package/dist/staged-runtimes.d.ts +42 -0
- package/dist/staged-runtimes.js +191 -0
- package/dist/storage-sweep.d.ts +50 -0
- package/dist/storage-sweep.js +203 -0
- package/dist/storage-ui.d.ts +5 -0
- package/dist/storage-ui.js +30 -4
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +15 -3
- package/dist/store.d.ts +17 -2
- package/dist/store.js +37 -5
- package/dist/subscription-chat.d.ts +3 -1
- package/dist/subscription-chat.js +1 -0
- package/dist/surface.js +1 -0
- package/dist/task-status.d.ts +13 -0
- package/dist/task-status.js +37 -3
- package/dist/team-leads.js +6 -2
- package/dist/team-runtime.js +2 -1
- package/dist/test-temp.d.ts +20 -2
- package/dist/test-temp.js +53 -13
- package/dist/toolroll-update.js +2 -0
- package/dist/workspace-ui.js +3 -1
- package/dist/worktree.js +9 -0
- package/package.json +1 -1
package/dist/builder.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { OBSERVATION_MAILBOX, observationBrief, parseObservationCases, collectObservations } from "./observations.js";
|
|
2
2
|
import { checkCommandFor, effectiveCheckLevel, recordRunCheckLevel } from "./check-levels.js";
|
|
3
|
+
import { queueBatchCheck } from "./batch-checks.js";
|
|
4
|
+
import { batchesFullCheck } from "./batch-policy.js";
|
|
3
5
|
import { skillsContext } from "./project-skills.js";
|
|
4
6
|
import { failedVerificationEvidence, sealVerificationReceipt, verificationEvidence, reuseObservationVerification } from "./verification-evidence.js";
|
|
5
7
|
import { learningContext } from "./project-learning.js";
|
|
@@ -684,7 +686,7 @@ export async function build(store, request) {
|
|
|
684
686
|
// copy links it instead of installing its own; a setup that is about to
|
|
685
687
|
// run never reaches through an old link into a shared copy.
|
|
686
688
|
const depsRoot = sharedDepsRootOf(store);
|
|
687
|
-
const sharing = depsRoot === null ? null : sharingFor({ repo: leased.repo, worktree, setup: setupWanted });
|
|
689
|
+
const sharing = depsRoot === null ? null : sharingFor({ repo: leased.repo, worktree, setup: setupWanted, root: depsRoot });
|
|
688
690
|
if (depsRoot !== null)
|
|
689
691
|
detachShared(worktree, depsRoot);
|
|
690
692
|
if (depsRoot !== null && sharing !== null) {
|
|
@@ -2175,6 +2177,11 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2175
2177
|
const configured = chosenCheck?.command ?? null;
|
|
2176
2178
|
if (repo !== null && chosenCheck !== null)
|
|
2177
2179
|
recordRunCheckLevel(store, { id: runId, taskId: request.taskId, repo }, chosenCheck.level, checkChoice.from, now());
|
|
2180
|
+
// Batch checks (batch-checks.ts): with the project's batching on, a Full check waits to run once with
|
|
2181
|
+
// other results on a temporary batch commit. The build seals no check of its own; the batch's sealed
|
|
2182
|
+
// receipt is this result's check, and Merge waits for it.
|
|
2183
|
+
const batched = repo !== null && chosenCheck !== null && batchesFullCheck(store, repo, chosenCheck.level, configured);
|
|
2184
|
+
const gateCommand = batched ? null : configured;
|
|
2178
2185
|
let verifyCommand;
|
|
2179
2186
|
const checkLog = [];
|
|
2180
2187
|
const checkOutcomes = [];
|
|
@@ -2210,6 +2217,10 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2210
2217
|
else if (configured === null) {
|
|
2211
2218
|
verifyCommand = { configured: false };
|
|
2212
2219
|
}
|
|
2220
|
+
else if (batched && repo !== null) {
|
|
2221
|
+
verifyCommand = { configured: false };
|
|
2222
|
+
queueBatchCheck(store, { runId, taskId: request.taskId, repo, head: sealedHead, base: captured.baseRevision, command: configured }, now());
|
|
2223
|
+
}
|
|
2213
2224
|
else if (!store.proveRunnerCustodyForSpawn(runId, now())) {
|
|
2214
2225
|
verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: "custody-lost" };
|
|
2215
2226
|
recordCheckNote("Verification did not start: this worker no longer owned the build.");
|
|
@@ -2258,7 +2269,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2258
2269
|
const live = linked === null ? null : store.liveWorktreeSetup(repo);
|
|
2259
2270
|
if (linked === null || live === null)
|
|
2260
2271
|
return null;
|
|
2261
|
-
if (sharingFor({ repo, worktree, setup: live })?.key === linked && readyCopy(depsRoot, linked) !== null)
|
|
2272
|
+
if (sharingFor({ repo, worktree, setup: live, root: depsRoot })?.key === linked && readyCopy(depsRoot, linked) !== null)
|
|
2262
2273
|
return null;
|
|
2263
2274
|
if (!store.proveRunnerCustodyForSpawn(runId, now()))
|
|
2264
2275
|
return "custody-lost";
|
|
@@ -2497,7 +2508,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2497
2508
|
}
|
|
2498
2509
|
const { verdict, reasons, matrix, machineVerdict } = adjudicate({
|
|
2499
2510
|
directAssessment: true,
|
|
2500
|
-
...(
|
|
2511
|
+
...(gateCommand === null ? {} : { verificationCommand: gateCommand.command }),
|
|
2501
2512
|
proofArtifactPresent,
|
|
2502
2513
|
proofParse,
|
|
2503
2514
|
handoffPresent: handoffArtifact !== null,
|
package/dist/chat-actions.js
CHANGED
|
@@ -5,7 +5,7 @@ import { TEXT_LIMITS } from "./text-limits.js";
|
|
|
5
5
|
import { publicChatText } from "./chat-display.js";
|
|
6
6
|
import { gateWords } from "./approval-policy.js";
|
|
7
7
|
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
-
import { verifiedAuthor } from "./store.js";
|
|
8
|
+
import { mateTurnKeepsProposals, verifiedAuthor } from "./store.js";
|
|
9
9
|
import { isVerifiedApprover, reproveApprover, } from "./principal.js";
|
|
10
10
|
import { changeSkills, importSkill, skillsView, skillsVersion, testSkill, validateSkill, } from "./project-skills.js";
|
|
11
11
|
import { changeKnowledge, knowledgeVersion, knowledgeView, } from "./project-knowledge.js";
|
|
@@ -1025,7 +1025,7 @@ function savedActionContext(store, who, id, state) {
|
|
|
1025
1025
|
session.ceilingDigest !== who.ceilingDigest ||
|
|
1026
1026
|
proposal.ceilingDigest !== who.ceilingDigest ||
|
|
1027
1027
|
(!shared && turn?.session !== session.id) ||
|
|
1028
|
-
turn
|
|
1028
|
+
!mateTurnKeepsProposals(turn))
|
|
1029
1029
|
throw Error("This conversation ended or its project access changed. Ask for a fresh proposal.");
|
|
1030
1030
|
requireActor(store, who, payload.repo, payload.operation);
|
|
1031
1031
|
return payload;
|
package/dist/check-levels-ui.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* one. Every change takes the password and lands in the ledger; approving a
|
|
4
4
|
* command restates exactly what will run, unattended, before the yes. */
|
|
5
5
|
import { CHECK_LEVELS, CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS } from "./check-levels.js";
|
|
6
|
+
import { BATCH_HINT } from "./batch-policy.js";
|
|
6
7
|
const escape = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7
8
|
export function checkSettingsHtml(view) {
|
|
8
9
|
const hidden = (act) => `<input type="hidden" name="csrf" value="${escape(view.csrf)}"><input type="hidden" name="repo" value="${escape(view.repo)}"><input type="hidden" name="act" value="${act}">`;
|
|
@@ -19,7 +20,7 @@ export function checkSettingsHtml(view) {
|
|
|
19
20
|
(fallsBack ? `<p class="meta">No quick command yet, so builds run the full check.</p>` : "") +
|
|
20
21
|
(view.level !== "full" && view.full !== null ? `<p class="meta">The full check still runs when a pull request opens, and Merge waits for it.</p>` : "");
|
|
21
22
|
if (!view.canChange) {
|
|
22
|
-
return head + `<section class="card check-settings">${current}</section>` + reviewHtml(view, "") +
|
|
23
|
+
return head + `<section class="card check-settings">${current}</section>` + batchHtml(view, "") + reviewHtml(view, "") +
|
|
23
24
|
`<section class="card check-settings"><h2>Quick check</h2>${view.quick === null ? `<p class="meta">None yet.</p>` : command(view.quick)}</section>` +
|
|
24
25
|
`<section class="card check-settings"><h2>Full check</h2>${view.full === null ? `<p class="meta">None yet.</p>` : command(view.full)}</section>`;
|
|
25
26
|
}
|
|
@@ -45,7 +46,25 @@ export function checkSettingsHtml(view) {
|
|
|
45
46
|
`<p class="meta">Quick builds run the full check instead.</p>${password}<button type="submit" class="danger">Remove</button></form></details></section>`;
|
|
46
47
|
const full = `<details class="settings-more"><summary>Full check</summary><section class="card check-settings">` +
|
|
47
48
|
(view.full === null ? `<p class="meta">None yet. Approve one with <span class="mono">toolroll verify set --repo … --command "…"</span>.</p>` : command(view.full)) + `</section></details>`;
|
|
48
|
-
return head + levelForm + reviewHtml(view, password) + quick + full;
|
|
49
|
+
return head + levelForm + batchHtml(view, password) + reviewHtml(view, password) + quick + full;
|
|
50
|
+
}
|
|
51
|
+
/** Batch checks: one state and one sentence; turning it on restates exactly what happens, behind a disclosure with the password. */
|
|
52
|
+
function batchHtml(view, password) {
|
|
53
|
+
const batch = view.batch;
|
|
54
|
+
if (batch === undefined)
|
|
55
|
+
return "";
|
|
56
|
+
const act = batch.on ? "Turn off" : "Turn on";
|
|
57
|
+
const terms = batch.on ? `<p class="meta">Each result runs its own full check again. Results already waiting are checked now.</p>`
|
|
58
|
+
: `<ul class="check-terms"><li>When results finish within 10 minutes of each other, they're merged in a temporary checkout and the full check runs once.</li>` +
|
|
59
|
+
`<li>If it fails, the batch is split until the result that breaks it is found. Results that conflict are checked on their own.</li>` +
|
|
60
|
+
`<li>Nothing is merged into a real branch. Each result still lands on its own.</li></ul>`;
|
|
61
|
+
const change = password === "" ? ""
|
|
62
|
+
: `<details class="settings-more"><summary>${act}</summary><form method="post" action="/settings/checks" class="check-settings">` +
|
|
63
|
+
`<input type="hidden" name="csrf" value="${escape(view.csrf)}"><input type="hidden" name="repo" value="${escape(view.repo)}"><input type="hidden" name="act" value="batch">` +
|
|
64
|
+
`<input type="hidden" name="on" value="${batch.on ? "0" : "1"}">${terms}${password}<button type="submit">${act}</button></form></details>`;
|
|
65
|
+
return `<section class="card check-settings" data-batch-checks="${batch.on ? "on" : "off"}"><h2>Batch checks</h2>` +
|
|
66
|
+
`<p><strong>${batch.on ? "On" : "Off"}</strong> · ${escape(BATCH_HINT)}</p>` +
|
|
67
|
+
(batch.on && view.level !== "full" ? `<p class="meta">Only Full checks are batched.</p>` : "") + `${change}</section>`;
|
|
49
68
|
}
|
|
50
69
|
/** Automatic review: one state, one sentence, and its one change behind a disclosure (the password). */
|
|
51
70
|
function reviewHtml(view, password) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type CheckoutCleanup } from "./storage.js";
|
|
2
2
|
import type { Store, WorktreeRow } from "./store.js";
|
|
3
3
|
import { type WorktreePool } from "./worktree.js";
|
|
4
|
-
/** A release candidate's checkout holds the build a deploy installs:
|
|
4
|
+
/** A release candidate's checkout holds the build a deploy installs: the newest complete one waits at most a week for it. */
|
|
5
5
|
export declare const CANDIDATE_KEEP_MS: number;
|
|
6
6
|
/** The worker's pass removes at most this many; a clean-up by hand has no cap. */
|
|
7
7
|
export declare const AUTO_MAX = 20;
|
|
@@ -43,6 +43,7 @@ type Status = {
|
|
|
43
43
|
export type Deployed = {
|
|
44
44
|
head: string | null;
|
|
45
45
|
run: number | null;
|
|
46
|
+
at?: string | null;
|
|
46
47
|
};
|
|
47
48
|
export declare function deployedRelease(stateDir: string | null): Deployed | null;
|
|
48
49
|
/**
|
package/dist/checkout-cleanup.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Always kept: a checkout in use, one whose task isn't finished (on hold, failed, a revision under way), a result
|
|
9
9
|
* still waiting for review, a checkout with changes (files Toolroll wrote itself aside), and one with commits on no
|
|
10
|
-
* branch. A release
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* branch. A release check's checkout (a deploy installs from it) goes once its task is complete, with its dependency
|
|
11
|
+
* install and journey output, except the one deployed now and the project's newest complete one still waiting for its
|
|
12
|
+
* deploy (no deploy since it was complete; at most a week). A superseded or cancelled gate goes at once, whatever its
|
|
13
|
+
* task's state (staged releases keep the rollback copy). A checkout no task names (adopted after a crash) goes only
|
|
13
14
|
* in a clean-up by hand. Every removal is in the ledger.
|
|
14
15
|
*
|
|
15
16
|
* A kept checkout waiting for review or with changes drops its dependencies and build output once its run ends
|
|
@@ -23,7 +24,7 @@ import { COMPLETION_ACTION } from "./result-completion.js";
|
|
|
23
24
|
import { CLEANUP_CHOICES, bytesWords, diskBytes } from "./storage.js";
|
|
24
25
|
import { worktreePath } from "./worktree.js";
|
|
25
26
|
const DAY_MS = 86_400_000;
|
|
26
|
-
/** A release candidate's checkout holds the build a deploy installs:
|
|
27
|
+
/** A release candidate's checkout holds the build a deploy installs: the newest complete one waits at most a week for it. */
|
|
27
28
|
export const CANDIDATE_KEEP_MS = 7 * DAY_MS;
|
|
28
29
|
/** The worker's pass removes at most this many; a clean-up by hand has no cap. */
|
|
29
30
|
export const AUTO_MAX = 20;
|
|
@@ -56,7 +57,7 @@ export function deployedRelease(stateDir) {
|
|
|
56
57
|
continue;
|
|
57
58
|
newest = { at, head: typeof journal["candidate"] === "string" ? journal["candidate"] : null, run: Number.isSafeInteger(journal["builder"]) ? Number(journal["builder"]) : null };
|
|
58
59
|
}
|
|
59
|
-
return newest === null ? null : { head: newest.head, run: newest.run };
|
|
60
|
+
return newest === null ? null : { head: newest.head, run: newest.run, at: newest.at === "" ? null : newest.at };
|
|
60
61
|
}
|
|
61
62
|
/** The folder beside the database (where staged releases live), or null for a database in memory. */
|
|
62
63
|
function stateDirOf(store) {
|
|
@@ -117,6 +118,19 @@ export function taskStatuses(store, now, deployed) {
|
|
|
117
118
|
if (ref !== undefined && (completedRef.get(ref) ?? "") < at)
|
|
118
119
|
completedRef.set(ref, at);
|
|
119
120
|
}
|
|
121
|
+
const completedAtOf = (row) => [completed.get(String(row["id"])), completedRef.get(Number(row["ref"]))].filter((one) => one !== undefined).sort().at(-1) ?? null;
|
|
122
|
+
// Per project, the newest complete gate (by when it was complete): the one a deploy may still be about to install.
|
|
123
|
+
// Once the project deployed after it was complete, it waits for nothing.
|
|
124
|
+
const deployedRepo = deployedRef === undefined ? (live?.head == null ? undefined : gates.find(row => String(row["candidate"]) === live.head)?.["repo"]) : refs.find(row => Number(row["ref"]) === deployedRef)?.["repo"];
|
|
125
|
+
const newestDone = new Map();
|
|
126
|
+
for (const row of gates) {
|
|
127
|
+
if (row["state"] !== "done")
|
|
128
|
+
continue;
|
|
129
|
+
const at = completedAtOf(row) ?? (row["updated_at"] === null ? "" : String(row["updated_at"]));
|
|
130
|
+
const seen = newestDone.get(String(row["repo"]));
|
|
131
|
+
if (seen === undefined || at > seen.at || (at === seen.at && Number(row["ref"]) > seen.ref))
|
|
132
|
+
newestDone.set(String(row["repo"]), { ref: Number(row["ref"]), at });
|
|
133
|
+
}
|
|
120
134
|
const byRef = new Map();
|
|
121
135
|
const byId = new Map();
|
|
122
136
|
const parentOf = new Map();
|
|
@@ -139,11 +153,13 @@ export function taskStatuses(store, now, deployed) {
|
|
|
139
153
|
else if (state === "cancelled")
|
|
140
154
|
status = { keep: null, taskId: id, finishedAt: updated };
|
|
141
155
|
else {
|
|
142
|
-
const completedAt =
|
|
156
|
+
const completedAt = completedAtOf(row);
|
|
143
157
|
if (gate) {
|
|
144
158
|
const since = completedAt ?? updated ?? now.toISOString();
|
|
145
159
|
const until = new Date(Date.parse(since) + CANDIDATE_KEEP_MS).toISOString();
|
|
146
|
-
|
|
160
|
+
const deployedSince = live?.at != null && deployedRepo !== undefined && String(row["repo"]) === String(deployedRepo) && live.at >= since;
|
|
161
|
+
const waiting = newestDone.get(String(row["repo"]))?.ref === ref && !deployedSince && until > now.toISOString();
|
|
162
|
+
status = waiting ? { keep: "release candidate", taskId: id, finishedAt: since, dueAt: until } : { keep: null, taskId: id, finishedAt: since, atOnce: true };
|
|
147
163
|
}
|
|
148
164
|
else
|
|
149
165
|
status = completedAt === null ? { keep: "waiting for review", taskId: id, finishedAt: null } : { keep: null, taskId: id, finishedAt: completedAt };
|
|
@@ -353,7 +369,7 @@ export function whyWords(item) {
|
|
|
353
369
|
case "in use": return "in use";
|
|
354
370
|
case "task not finished": return "its task isn't finished";
|
|
355
371
|
case "waiting for review": return "its result is waiting for review";
|
|
356
|
-
case "release candidate": return item.dueAt === undefined ? "the deployed release candidate" : `newest release candidate, kept until ${item.dueAt.slice(0, 10)}`;
|
|
372
|
+
case "release candidate": return item.dueAt === undefined ? "the deployed release candidate" : `newest release candidate, kept for its deploy until ${item.dueAt.slice(0, 10)}`;
|
|
357
373
|
case "has changes": return "has changes";
|
|
358
374
|
case "has commits": return "has commits on no branch";
|
|
359
375
|
case "unreadable": return "git couldn't read it";
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -125,13 +125,13 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
125
125
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
126
126
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
127
127
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
128
|
-
error?: never;
|
|
129
128
|
active: boolean;
|
|
130
129
|
phase: string;
|
|
131
130
|
detail: string;
|
|
132
131
|
running: boolean;
|
|
133
132
|
canResume: boolean;
|
|
134
133
|
canCancel: boolean;
|
|
134
|
+
error?: never;
|
|
135
135
|
automaticRecovery?: never;
|
|
136
136
|
recoveryAttempts?: never;
|
|
137
137
|
recoveryRepairs?: never;
|
package/dist/exec.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export type RunOptions = ProcessTreeObserver & {
|
|
|
56
56
|
* timeoutMs there is deliberately no absolute wall-clock ceiling.
|
|
57
57
|
*/
|
|
58
58
|
idleTimeoutMs?: number;
|
|
59
|
+
/** Ends a process-group run early, exactly as its timeout does: the whole group is killed and the result reads timed out. */
|
|
60
|
+
signal?: AbortSignal;
|
|
59
61
|
maxBuffer?: number;
|
|
60
62
|
/**
|
|
61
63
|
* Extra environment, merged over the process's own. A capability probe is a
|
package/dist/exec.js
CHANGED
|
@@ -526,6 +526,7 @@ export function run(file, args, options = {}) {
|
|
|
526
526
|
return retryTransientSpawn(() => runBufferedGroup(file, args, {
|
|
527
527
|
timeoutMs,
|
|
528
528
|
maxBuffer,
|
|
529
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
529
530
|
...(options.stdin === undefined ? {} : { stdin: options.stdin }),
|
|
530
531
|
...(cwd === undefined ? {} : { cwd }),
|
|
531
532
|
...(childEnv === undefined ? {} : { childEnv }),
|
|
@@ -610,6 +611,15 @@ function runBufferedGroup(file, args, bag) {
|
|
|
610
611
|
timedOut = true;
|
|
611
612
|
killGroup(child);
|
|
612
613
|
}, bag.timeoutMs);
|
|
614
|
+
// The caller's deadline, when it owns one: the same group kill as the timer.
|
|
615
|
+
const aborted = () => {
|
|
616
|
+
timedOut = true;
|
|
617
|
+
killGroup(child);
|
|
618
|
+
};
|
|
619
|
+
if (bag.signal?.aborted === true)
|
|
620
|
+
aborted();
|
|
621
|
+
else
|
|
622
|
+
bag.signal?.addEventListener("abort", aborted, { once: true });
|
|
613
623
|
child.stdout?.setEncoding("utf8");
|
|
614
624
|
child.stdout?.on("data", (chunk) => {
|
|
615
625
|
stdout += chunk;
|
|
@@ -638,6 +648,7 @@ function runBufferedGroup(file, args, bag) {
|
|
|
638
648
|
return;
|
|
639
649
|
settled = true;
|
|
640
650
|
clearTimeout(timer);
|
|
651
|
+
bag.signal?.removeEventListener("abort", aborted);
|
|
641
652
|
liveProviders.delete(child);
|
|
642
653
|
// The OS object is settled BEFORE the answer: a root that exited is
|
|
643
654
|
// not an empty container until the OS says so.
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* its promise, a small drawing of its zones, what it needs and "Use this". A template's page asks only what it
|
|
4
|
-
* needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
|
|
5
|
-
*/
|
|
6
|
-
import { type GalleryAnswers, type GalleryPreview, type GalleryTemplate } from "./flow-gallery.js";
|
|
7
|
-
import { type FlowDefinition } from "./flows.js";
|
|
1
|
+
import { type GalleryAnswers, type GalleryPreview, type GalleryTemplate, type GalleryTool } from "./flow-gallery.js";
|
|
2
|
+
import { type FlowDefinition, type FlowStage } from "./flows.js";
|
|
8
3
|
export declare const GALLERY_CSS: string;
|
|
9
|
-
/** A
|
|
10
|
-
|
|
4
|
+
/** A flow's main path, as a person reads it: from the start, each step's first way on (a sort's first answer, a choose's
|
|
5
|
+
* first option that goes somewhere, else next; a check's failure when passing just ends), never back to a step shown. */
|
|
6
|
+
export declare function mainPath(definition: FlowDefinition): FlowStage[];
|
|
7
|
+
/** A flow's steps in order, joined by arrows; a person's decisions carry the accent. */
|
|
8
|
+
export declare function stepStrip(definition: FlowDefinition): string;
|
|
11
9
|
/** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
|
|
12
10
|
export declare function galleryHtml(input: {
|
|
13
11
|
repo: string | null;
|
|
14
12
|
canUse: boolean;
|
|
13
|
+
connections?: readonly {
|
|
14
|
+
id: string;
|
|
15
|
+
state: "connected" | "open" | "taken";
|
|
16
|
+
}[] | null;
|
|
15
17
|
}): string;
|
|
16
18
|
/** A template's page: its questions, then what it will do and never do, then Create. */
|
|
17
19
|
export declare function galleryUseHtml(input: {
|
|
@@ -27,4 +29,7 @@ export declare function galleryUseHtml(input: {
|
|
|
27
29
|
problem: string | null;
|
|
28
30
|
csrf: string;
|
|
29
31
|
diagram: FlowDefinition | null;
|
|
32
|
+
/** The template's tools in this project; `said` is how the last Connect went. */
|
|
33
|
+
tools?: readonly GalleryTool[];
|
|
34
|
+
said?: string | null;
|
|
30
35
|
}): string;
|
package/dist/flow-gallery-ui.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The flow gallery in the console (Flows → New and Settings → Flows): cards grouped by what they're for, each with
|
|
3
|
-
* its promise,
|
|
3
|
+
* its promise, its steps in order, what it needs and "Use this". A template's page asks only what it
|
|
4
4
|
* needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
|
|
5
|
+
* A template's tools show their marks and whether the project has them; one it lacks offers Connect first.
|
|
5
6
|
*/
|
|
6
|
-
import {
|
|
7
|
+
import { brandMarkHtml } from "./brand-mark.js";
|
|
8
|
+
import { BLANK, GALLERY, GALLERY_GROUPS, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
|
|
7
9
|
import { choiceTargets } from "./flows.js";
|
|
8
10
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9
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}` +
|
|
10
12
|
`.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}` +
|
|
11
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}` +
|
|
12
14
|
`.gallery-card h3{margin:0;font-size:.9375rem}.gallery-card p{margin:0;overflow-wrap:anywhere}.gallery-card .gallery-promise{line-height:1.5;flex:1}` +
|
|
13
|
-
|
|
14
|
-
`.gallery-
|
|
15
|
+
// A template's steps in order, joined by arrows; a person's own steps carry the attention chip.
|
|
16
|
+
`.gallery-steps{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px;list-style:none;padding:0;margin:0;font-size:12px;font-weight:500;line-height:1.5;color:var(--so-ink)}` +
|
|
17
|
+
`.gallery-steps li{display:inline-flex;align-items:center;gap:6px;min-width:0;overflow-wrap:anywhere}.gallery-steps li>span[aria-hidden]{color:var(--so-muted)}` +
|
|
18
|
+
`.gallery-steps b{font-weight:500;font-size:11.5px;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-signal-soft);color:var(--so-signal)}` +
|
|
15
19
|
`.gallery-needs{display:flex;flex-wrap:wrap;gap:4px;list-style:none;padding:0;margin:0}.gallery-needs li{font-size:11.5px;font-weight:500;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-neutral-soft,var(--so-raised));color:var(--so-neutral-ink,var(--so-muted))}` +
|
|
16
20
|
// An outline action in a list (the Quiet List Rule), over the workspace's ink default for .button-link.
|
|
17
21
|
`.gallery .gallery-card a.button-link{align-self:flex-start;min-height:36px;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
|
|
18
22
|
`.gallery-blank{margin-top:20px}` +
|
|
19
|
-
|
|
23
|
+
// A template's tools: the mark Settings → Integrations shows, its name and where the project stands.
|
|
24
|
+
`.gallery-tools{display:grid;gap:6px;list-style:none;padding:0;margin:0}.gallery-tools li{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap}.gallery-tools li>span:not(.brand-mark){min-width:0;overflow-wrap:anywhere}` +
|
|
25
|
+
`.gallery-tools strong{font-weight:500;overflow-wrap:anywhere}.gallery-connect{display:grid;gap:10px;padding:12px 0;border-top:1px solid var(--so-line)}.gallery-connect:last-child{border-bottom:1px solid var(--so-line);margin-bottom:4px}` +
|
|
26
|
+
`.gallery-connect form{display:grid;gap:8px;margin:0}.gallery-connect p{margin:0;overflow-wrap:anywhere}.gallery-connect .gallery-tool-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.gallery-connect button{justify-self:start;min-height:36px;white-space:nowrap;background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}` +
|
|
27
|
+
`.gallery-use{max-width:720px;min-width:0}.gallery-use .gallery-steps{margin:4px 0 12px}.gallery-use form{display:grid;gap:12px;margin:0}.gallery-use label{display:grid;gap:4px;font-weight:500}` +
|
|
20
28
|
`.gallery-use label small{font-weight:400;color:var(--so-muted)}.gallery-use label.gallery-check{display:flex;align-items:flex-start;gap:8px;font-weight:500}.gallery-use label.gallery-check input{margin-top:3px;width:16px;height:16px;min-height:0;flex:none}.gallery-use label.gallery-check small{display:block}` +
|
|
21
29
|
`.gallery-use input:not([type=hidden]),.gallery-use select{box-sizing:border-box;width:100%;max-width:100%;min-height:32px;padding:0 10px;border:1px solid var(--so-input-line);border-radius:8px;background:var(--so-paper);color:var(--so-ink);font:inherit}` +
|
|
22
30
|
`.gallery-preview{display:grid;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}.gallery-preview h2{margin:0;font-size:.9375rem}` +
|
|
@@ -24,33 +32,47 @@ export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{ma
|
|
|
24
32
|
`.gallery-preview details ol{margin:6px 0 0;padding-left:20px;white-space:pre-line;font-size:.875rem;line-height:1.5;color:var(--so-muted)}.gallery-preview details li{margin:0 0 6px;overflow-wrap:anywhere}` +
|
|
25
33
|
`.gallery-preview summary{cursor:pointer;min-height:36px;display:flex;align-items:center;font-weight:500}` +
|
|
26
34
|
`.gallery-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0}.gallery-actions button{min-height:36px;white-space:nowrap}` +
|
|
27
|
-
`.gallery-use .gallery-actions button[value=create]{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}.gallery-heading{margin:36px 0 0;font-size:1.05rem}` +
|
|
28
|
-
`@media(max-width:760px){.gallery-grid{grid-template-columns:minmax(0,1fr)}.gallery .gallery-card a.button-link{min-height:44px;align-self:stretch;justify-content:center}` +
|
|
35
|
+
`.gallery-use .gallery-actions button[value=create]:not(.secondary){background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}.gallery-heading{margin:36px 0 0;font-size:1.05rem}` +
|
|
36
|
+
`@media(max-width:760px){.gallery-grid{grid-template-columns:minmax(0,1fr)}.gallery-connect button{min-height:44px;justify-self:stretch}.gallery .gallery-card a.button-link{min-height:44px;align-self:stretch;justify-content:center}` +
|
|
29
37
|
`.gallery-use input:not([type=hidden]),.gallery-use select{font-size:16px;min-height:44px}.gallery-actions button{flex:1 1 auto;min-height:44px}}`;
|
|
30
|
-
/** A
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return `<svg class="gallery-zones" viewBox="${box}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${e(label)}">${lines}${rects}</svg>`;
|
|
38
|
+
/** A flow's main path, as a person reads it: from the start, each step's first way on (a sort's first answer, a choose's
|
|
39
|
+
* first option that goes somewhere, else next; a check's failure when passing just ends), never back to a step shown. */
|
|
40
|
+
export function mainPath(definition) {
|
|
41
|
+
const byId = new Map(definition.stages.map(one => [one.id, one]));
|
|
42
|
+
const path = [];
|
|
43
|
+
for (let at = byId.get(definition.start); at !== undefined && at.kind !== "done" && !path.includes(at);) {
|
|
44
|
+
path.push(at);
|
|
45
|
+
const ways = [...(at.sort?.answers.map(one => one.to) ?? []), ...choiceTargets(at), at.next, ...(at.routes?.map(one => one.to) ?? [])].map(id => id === null ? undefined : byId.get(id));
|
|
46
|
+
if (at.kind === "check")
|
|
47
|
+
ways.push(at.onFail === null ? undefined : byId.get(at.onFail));
|
|
48
|
+
at = at.kind === "check" ? ways.find(one => one !== undefined && one.kind !== "done") : ways.find(one => one !== undefined);
|
|
49
|
+
}
|
|
50
|
+
return path;
|
|
44
51
|
}
|
|
45
|
-
|
|
52
|
+
/** A step's short name in the strip: a person's own steps from their side. */
|
|
53
|
+
const stepName = (stage) => stage.kind === "choose" ? "You choose" : stage.kind === "approval" ? "You approve" : stage.kind === "send" ? "Sent to you" : stage.title;
|
|
54
|
+
/** A flow's steps in order, joined by arrows; a person's decisions carry the accent. */
|
|
55
|
+
export function stepStrip(definition) {
|
|
56
|
+
return `<ol class="gallery-steps" aria-label="Steps">${mainPath(definition).map((one, i) => {
|
|
57
|
+
const name = e(stepName(one));
|
|
58
|
+
return `<li>${i === 0 ? "" : `<span aria-hidden="true">→</span>`}${one.kind === "choose" || one.kind === "approval" ? `<b data-person>${name}</b>` : name}</li>`;
|
|
59
|
+
}).join("")}</ol>`;
|
|
60
|
+
}
|
|
61
|
+
const needsList = (template) => template.needs.length === 0 ? (template.tools ?? []).length > 0 ? "" : `<ul class="gallery-needs"><li>Nothing to connect</li></ul>`
|
|
46
62
|
: `<ul class="gallery-needs" aria-label="Needs">${template.needs.map(one => `<li>${e(one)}</li>`).join("")}</ul>`;
|
|
63
|
+
/** A tool's state in words, as Settings → Integrations badges it; nothing without a project. */
|
|
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>`;
|
|
66
|
+
const toolMark = (tool) => brandMarkHtml(tool.id.replace(/-desktop$/, ""), tool.label, tool.state === "connected");
|
|
67
|
+
const toolsList = (tools) => tools.length === 0 ? ""
|
|
68
|
+
: `<ul class="gallery-tools" aria-label="Tools">${tools.map(one => `<li data-tool="${e(one.id)}" data-state="${one.state ?? "unknown"}">${toolMark(one)}<strong>${e(one.label)}</strong>${toolState(one)}</li>`).join("")}</ul>`;
|
|
47
69
|
/** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
|
|
48
70
|
export function galleryHtml(input) {
|
|
49
71
|
const href = (id) => `/flows/new/${id}${input.repo === null ? "" : `?repo=${encodeURIComponent(input.repo)}`}`;
|
|
50
72
|
const card = (template) => {
|
|
51
73
|
const definition = galleryDiagram(template);
|
|
52
74
|
return `<article class="gallery-card" data-template="${e(template.id)}"><h3>${e(template.name)}</h3><p class="gallery-promise">${e(template.promise)}</p>` +
|
|
53
|
-
|
|
75
|
+
stepStrip(definition) + toolsList(galleryToolsOf(template, definition, input.repo === null ? null : input.connections ?? null)) + needsList(template) +
|
|
54
76
|
(input.canUse ? `<a class="button-link" href="${e(href(template.id))}" aria-label="Use ${e(template.name)}">Use this</a>` : "") + `</article>`;
|
|
55
77
|
};
|
|
56
78
|
const groups = GALLERY_GROUPS.map(group => `<h2 id="gallery-${group.id}">${e(group.label)}</h2><div class="gallery-grid" data-group="${group.id}">${GALLERY.filter(one => one.group === group.id).map(card).join("")}</div>`).join("");
|
|
@@ -59,28 +81,48 @@ export function galleryHtml(input) {
|
|
|
59
81
|
/** A template's page: its questions, then what it will do and never do, then Create. */
|
|
60
82
|
export function galleryUseHtml(input) {
|
|
61
83
|
const { template } = input;
|
|
84
|
+
const tools = input.tools ?? input.preview?.tools ?? [];
|
|
85
|
+
// A tool added another way is there: only one the project lacks is missing.
|
|
86
|
+
const missing = tools.filter(one => one.state !== "connected" && one.state !== "taken");
|
|
87
|
+
// 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
|
+
const connect = (tool) => {
|
|
89
|
+
const head = `<p class="gallery-tool-head">${toolMark(tool)} <strong>${e(tool.label)}</strong> ${toolState(tool)}</p>`;
|
|
90
|
+
// Added another way, it is there: nothing to connect.
|
|
91
|
+
if (tool.state === "connected" || tool.state === "taken" || input.csrf === "")
|
|
92
|
+
return `<div class="gallery-connect" data-tool="${e(tool.id)}" data-state="${tool.state}">${head}</div>`;
|
|
93
|
+
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="${e(tool.id)}" data-state="open">${head}<form method="post" action="/settings/tools/connect">` +
|
|
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>`;
|
|
98
|
+
};
|
|
99
|
+
const toolsBlock = tools.length === 0 ? "" : `<section class="gallery-tools-use" aria-label="Tools">${tools.map(connect).join("")}</section>`;
|
|
100
|
+
// Made without a tool, the preview names the zone that needs it.
|
|
101
|
+
const zones = (tool) => tool.zones.map(zone => `“${e(zone)}”`).join(" and ");
|
|
102
|
+
const needing = missing.filter(one => one.zones.length > 0).map(one => `<p class="meta" data-needs-tool="${e(one.id)}">${zones(one)} need${one.zones.length === 1 ? "s" : ""} ${e(one.label)}, which isn't connected.</p>`).join("") +
|
|
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("");
|
|
62
104
|
const field = (ask) => {
|
|
63
105
|
const value = input.answers[ask.key] ?? ask.default;
|
|
64
106
|
const hint = ask.hint === undefined ? "" : ` <small>${e(ask.hint)}</small>`;
|
|
65
107
|
if (ask.key === "outdated")
|
|
66
108
|
return `<label>${e(ask.label)}<select name="outdated">${Object.entries(OUTDATED_COMMANDS).map(([id, one]) => `<option value="${id}"${id === value ? " selected" : ""}>${e(one.label)}</option>`).join("")}</select></label>`;
|
|
67
|
-
return `<label>${e(ask.label)}${hint}<input name="${ask.key}" value="${e(value)}" required maxlength="${ask.key === "command" ? 500 : 100}"${ask.key === "command" ? ' spellcheck="false" autocapitalize="off"' : ""}></label>`;
|
|
109
|
+
return `<label>${e(ask.label)}${hint}<input name="${ask.key}" value="${e(value)}" required maxlength="${ask.key === "command" ? 500 : ask.key === "team" ? 12 : 100}"${ask.key === "command" ? ' spellcheck="false" autocapitalize="off"' : ask.key === "team" ? ' spellcheck="false" autocapitalize="characters"' : ""}></label>`;
|
|
68
110
|
};
|
|
69
111
|
const project = input.projects.length > 1
|
|
70
112
|
? `<label>Project<select name="repo">${input.projects.map(one => `<option value="${e(one.path)}"${one.path === input.repo ? " selected" : ""}>${e(one.name)}</option>`).join("")}</select></label>`
|
|
71
113
|
: `<input type="hidden" name="repo" value="${e(input.repo)}">`;
|
|
72
114
|
const preview = input.preview === null ? "" : `<section class="gallery-preview" aria-labelledby="gallery-will">` +
|
|
73
115
|
`<h2 id="gallery-will">What it will do</h2><ul>${input.preview.built.does.map(one => `<li>${e(one)}</li>`).join("")}</ul>` +
|
|
74
|
-
`<p class="gallery-never">${e(input.preview.built.never)}</p
|
|
116
|
+
`<p class="gallery-never">${e(input.preview.built.never)}</p>${needing}` +
|
|
75
117
|
(input.preview.startsFrom.length === 0 ? `<p class="meta">Cards start when you add them.</p>` : input.preview.startsFrom.map(one => `<p class="meta">Starts from: ${e(one)}</p>`).join("")) +
|
|
76
118
|
`<details><summary>Every step</summary><ol>${input.preview.steps.map(one => `<li>${e(one.replace(/^\d+\.\s/, ""))}</li>`).join("")}</ol></details></section>`;
|
|
77
119
|
return `<section class="gallery-use" data-gallery-template="${e(template.id)}">` +
|
|
78
|
-
(input.problem === null ? "" : `<p class="problem" role="alert">${e(input.problem)}</p>`) +
|
|
79
|
-
`<p>${e(template.promise)}</p>${input.diagram === null ? "" :
|
|
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}` +
|
|
80
122
|
`<form method="post" action="/flows/new/${e(template.id)}" data-gallery-use><input type="hidden" name="csrf" value="${e(input.csrf)}">` +
|
|
81
123
|
`<input type="hidden" name="previewed" value="${e(input.preview?.digest ?? "")}">${project}${template.asks.map(field).join("")}` +
|
|
82
124
|
`<label>Name<input name="name" value="${e(input.name)}" maxlength="80"></label>` +
|
|
83
|
-
`<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
|
|
84
|
-
`<p class="gallery-actions">${input.preview === null ? "" : `<button type="submit" name="intent" value="create">Create flow</button>`}<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button></p>` +
|
|
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>` +
|
|
85
127
|
`</form><p class="meta"><a href="/flows/new">All templates</a></p></section>`;
|
|
86
128
|
}
|
package/dist/flow-gallery.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare const GALLERY_GROUPS: readonly [{
|
|
|
15
15
|
}];
|
|
16
16
|
export type GalleryGroup = (typeof GALLERY_GROUPS)[number]["id"];
|
|
17
17
|
/** What a template may ask, besides the project. */
|
|
18
|
-
export type AskKey = "label" | "branch" | "schedule" | "time" | "command" | "outdated";
|
|
18
|
+
export type AskKey = "label" | "branch" | "schedule" | "time" | "command" | "outdated" | "team";
|
|
19
19
|
export type GalleryAsk = {
|
|
20
20
|
key: AskKey;
|
|
21
21
|
default: string;
|
|
@@ -36,6 +36,8 @@ export type GalleryTemplate = {
|
|
|
36
36
|
promise: string;
|
|
37
37
|
/** What it needs to work, in a word or two each. */
|
|
38
38
|
needs: string[];
|
|
39
|
+
/** The project tools it puts to work: one-click service ids (ONE_CLICK) or "figma-desktop". */
|
|
40
|
+
tools?: readonly string[];
|
|
39
41
|
/** Watches a GitHub repository, so the project must be on GitHub. */
|
|
40
42
|
github?: boolean;
|
|
41
43
|
asks: GalleryAsk[];
|
|
@@ -55,10 +57,28 @@ export type GalleryTemplate = {
|
|
|
55
57
|
does: string[];
|
|
56
58
|
never: string;
|
|
57
59
|
};
|
|
60
|
+
/** The card's Linear issue key, read from its source as the Linear trigger stored it ("Linear ENG-123"); "none" skips the comment. */
|
|
61
|
+
export declare const LINEAR_KEY = "key=\"$(sed -n 's/.*\"source\":{\"kind\":\"linear\",\"label\":\"Linear \\([A-Z][A-Z0-9]*-[0-9][0-9]*\\)\".*/\\1/p' | head -n 1)\"\nif [ -n \"$key\" ]; then echo \"$key\"; echo \"goto: Found\"; else echo none; echo \"goto: None\"; fi";
|
|
58
62
|
export declare const GALLERY: readonly GalleryTemplate[];
|
|
59
63
|
/** The blank flow, for starting from nothing. */
|
|
60
64
|
export declare const BLANK: GalleryTemplate;
|
|
61
65
|
export declare const galleryTemplateOf: (id: string) => GalleryTemplate | null;
|
|
66
|
+
/** A tool a template uses: where the chosen project stands with it (null with no project), and the zones that use it. */
|
|
67
|
+
export type GalleryTool = {
|
|
68
|
+
id: string;
|
|
69
|
+
label: string;
|
|
70
|
+
state: "connected" | "open" | "taken" | null;
|
|
71
|
+
zones: string[];
|
|
72
|
+
};
|
|
73
|
+
/** A tool's name ("Mobbin", "Figma (desktop app)"); null for an id that's neither a one-click service nor an app on this computer. */
|
|
74
|
+
export declare const galleryToolLabel: (id: string) => string | null;
|
|
75
|
+
/** The zones that use a tool: a tool step on it, or a research or build step that names it ("With Mobbin, …"). */
|
|
76
|
+
export declare function zonesUsing(definition: FlowDefinition, id: string): string[];
|
|
77
|
+
/** A template's tools, each with its state in a project's connections (connectionsOf), or none without a project. */
|
|
78
|
+
export declare function galleryToolsOf(template: GalleryTemplate, definition: FlowDefinition, connections: readonly {
|
|
79
|
+
id: string;
|
|
80
|
+
state: "connected" | "open" | "taken";
|
|
81
|
+
}[] | null): GalleryTool[];
|
|
62
82
|
export type GalleryAnswers = Record<string, string>;
|
|
63
83
|
/** Each question's default for a project: the branch pull requests go to, when that's set up. */
|
|
64
84
|
export declare function galleryDefaults(store: Store, template: GalleryTemplate, repo: string | null): GalleryAnswers;
|
|
@@ -68,7 +88,10 @@ export declare const SEND_RESULT: {
|
|
|
68
88
|
readonly title: "Send me the result";
|
|
69
89
|
readonly does: "When a card finishes, sends you what was done (its summary, links and screenshots) in your chat apps.";
|
|
70
90
|
};
|
|
71
|
-
/** A
|
|
91
|
+
/** A template that already sends its result to the person: it isn't offered "Send me the result" again. */
|
|
92
|
+
export declare const sendsAlready: (template: GalleryTemplate) => boolean;
|
|
93
|
+
/** A flow with "Send me the result" before its end: every path into its main Done zone passes through it first.
|
|
94
|
+
* A flow that already has a send zone is left as it is, so nobody gets the result twice. */
|
|
72
95
|
export declare function withSendResult(definition: FlowDefinition): FlowDefinition;
|
|
73
96
|
/** The answers, checked in plain words, with what they fill in. Throws. */
|
|
74
97
|
export declare function checkAnswers(template: GalleryTemplate, given: GalleryAnswers): GalleryAnswers;
|
|
@@ -96,6 +119,7 @@ export type GalleryPreview = {
|
|
|
96
119
|
built: GalleryBuilt;
|
|
97
120
|
startsFrom: string[];
|
|
98
121
|
steps: string[];
|
|
122
|
+
tools: GalleryTool[];
|
|
99
123
|
digest: string;
|
|
100
124
|
};
|
|
101
125
|
/** What using a template will do, in words, before anything is made: its triggers checked as the flow will have them. Throws. */
|