toolroll 0.9.54 → 0.9.56
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 +19 -0
- package/README.md +29 -12
- package/dist/action-ledger.d.ts +2 -4
- package/dist/action-ledger.js +2 -10
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-policy.d.ts +5 -5
- package/dist/approval-policy.js +4 -4
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +17 -22
- 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 +14 -15
- package/dist/browser-workspace.js +0 -3
- package/dist/budget-alerts.js +3 -3
- package/dist/chat-actions.d.ts +23 -18
- package/dist/chat-actions.js +110 -88
- package/dist/chat-ask.d.ts +2 -2
- package/dist/chat-ask.js +5 -5
- package/dist/chat-channel.d.ts +15 -15
- package/dist/chat-channel.js +20 -20
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-core.js +1 -1
- package/dist/chat-delivery-state.js +2 -2
- package/dist/chat-delivery.d.ts +2 -2
- package/dist/chat-delivery.js +27 -34
- package/dist/chat-flow.js +3 -3
- package/dist/chat-messages.d.ts +1 -1
- package/dist/chat-messages.js +4 -4
- package/dist/chat-migration.d.ts +1 -1
- package/dist/chat-migration.js +11 -11
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/chat-rooms.js +3 -3
- package/dist/chat-warmth.d.ts +2 -2
- 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 +15 -10
- 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-actions.d.ts +32 -27
- package/dist/contracts/chat-actions.js +29 -11
- package/dist/contracts/chat-content.d.ts +1 -1
- package/dist/contracts/chat-content.js +1 -1
- package/dist/contracts/chat-tables.js +4 -4
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +7 -7
- package/dist/contracts/console-api.js +13 -12
- package/dist/contracts/flow-propose.d.ts +3 -3
- package/dist/contracts/flow.d.ts +132 -125
- package/dist/contracts/flow.js +27 -12
- package/dist/contracts/health.d.ts +1 -3
- package/dist/contracts/lead-context.d.ts +1 -1
- package/dist/contracts/lead-context.js +2 -2
- package/dist/contracts/lead-tools.d.ts +20 -18
- package/dist/contracts/lead-tools.js +15 -15
- package/dist/contracts/stage-output.d.ts +1 -1
- package/dist/contracts/stage-output.js +1 -1
- package/dist/contracts/store-json.d.ts +23 -22
- package/dist/contracts/store-json.js +17 -17
- package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
- package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/converse.d.ts +23 -23
- package/dist/converse.js +28 -28
- package/dist/coordinator-proposals.d.ts +2 -2
- package/dist/coordinator-proposals.js +1 -1
- package/dist/coordinator.js +1 -1
- 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/deprecations.d.ts +12 -0
- package/dist/deprecations.js +20 -0
- package/dist/desktop-update-gate.js +8 -4
- package/dist/desktop-update.js +6 -1
- package/dist/discord-chat.js +6 -6
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +17 -42
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/envelope.js +10 -1
- 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 +7 -7
- package/dist/flow-engine.d.ts +2 -2
- package/dist/flow-engine.js +21 -21
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-insights.d.ts +1 -1
- package/dist/flow-share.js +13 -8
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-steps.d.ts +5 -5
- package/dist/flow-steps.js +19 -19
- package/dist/flow-triggers.d.ts +2 -1
- package/dist/flow-triggers.js +17 -15
- package/dist/flows-cli.js +1 -1
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +38 -49
- package/dist/flows.js +21 -20
- package/dist/guides.js +112 -6
- 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/invoke.js +1 -1
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +22 -24
- package/dist/kits.d.ts +12 -12
- package/dist/kits.js +31 -31
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
- package/dist/{mate-cli.js → lead-cli.js} +38 -38
- package/dist/lead-commitments.js +4 -4
- package/dist/lead-context.d.ts +12 -2
- package/dist/lead-context.js +15 -17
- package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
- package/dist/{mate-contract.js → lead-contract.js} +7 -6
- package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
- package/dist/{mate-doors.js → lead-doors.js} +24 -24
- package/dist/lead-follow.d.ts +5 -5
- package/dist/lead-follow.js +9 -9
- package/dist/lead-people.d.ts +5 -5
- package/dist/lead-people.js +14 -14
- package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
- package/dist/{mate-progress.js → lead-progress.js} +3 -3
- package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
- package/dist/{mate-tools.js → lead-tools.js} +109 -101
- package/dist/{mate.d.ts → lead.d.ts} +27 -27
- package/dist/{mate.js → lead.js} +102 -102
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +3 -2
- package/dist/limits-ui.js +8 -8
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mcp-connect.d.ts +1 -1
- package/dist/mcp-connect.js +3 -3
- package/dist/mcp-core.js +1 -1
- package/dist/mcp-oauth.js +35 -36
- package/dist/memory-pass.d.ts +2 -2
- package/dist/memory-pass.js +7 -7
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/notification-rows.d.ts +2 -2
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.d.ts +3 -3
- package/dist/operate.js +46 -41
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/policy.d.ts +1 -1
- package/dist/principal.d.ts +1 -1
- package/dist/principal.js +2 -2
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +16 -28
- package/dist/project-delete.d.ts +1 -1
- package/dist/project-delete.js +26 -26
- package/dist/project-memory.js +1 -1
- package/dist/project-tools.d.ts +1 -1
- package/dist/project-tools.js +1 -1
- 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/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/retention.js +4 -4
- package/dist/scope.d.ts +2 -2
- package/dist/scout-report.d.ts +1 -1
- package/dist/scout-report.js +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +105 -99
- package/dist/server/chat.d.ts +7 -10
- package/dist/server/chat.js +984 -1021
- 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 +854 -773
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +8 -5
- 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 +581 -573
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +39 -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 +154 -0
- package/dist/server/render-settings.js +580 -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 +167 -77
- package/dist/server/runtime.d.ts +28 -20
- 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 +2186 -2121
- 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 +1489 -1420
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/setup-guide.d.ts +2 -2
- 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-chat.d.ts +2 -2
- package/dist/slack-chat.js +6 -6
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +17 -42
- package/dist/spend-ui.d.ts +5 -4
- package/dist/spend-ui.js +17 -40
- package/dist/spend.d.ts +14 -14
- package/dist/spend.js +31 -31
- 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-baseline.d.ts +19 -0
- package/dist/store-baseline.js +2873 -0
- package/dist/store.d.ts +197 -249
- package/dist/store.js +747 -3084
- package/dist/subagent-admin.d.ts +53 -0
- package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
- package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
- package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
- package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
- package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
- package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
- package/dist/{teammate-question.js → subagent-question.js} +17 -17
- package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
- package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
- package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
- package/dist/{teammate-week.js → subagent-week.js} +23 -23
- package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
- package/dist/{teammate-work.js → subagent-work.js} +50 -50
- package/dist/subagents-ui.d.ts +22 -0
- package/dist/subagents-ui.js +137 -0
- package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
- package/dist/{teammates.js → subagents.js} +21 -21
- package/dist/subscription-chat.d.ts +13 -13
- package/dist/subscription-chat.js +19 -19
- package/dist/surface.d.ts +19 -2
- package/dist/surface.js +20 -33
- 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-cli.d.ts +1 -0
- package/dist/team-cli.js +14 -2
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-leads.js +13 -13
- package/dist/team-runtime.d.ts +2 -2
- package/dist/team-runtime.js +8 -8
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teams-chat.js +6 -6
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +20 -31
- package/dist/telegram-flow.d.ts +3 -3
- package/dist/telegram-flow.js +3 -3
- package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
- package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/telegram-status.js +1 -1
- package/dist/telegram-team.d.ts +1 -1
- package/dist/telegram-team.js +5 -5
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +9 -30
- package/dist/text-limits.d.ts +18 -18
- package/dist/text-limits.js +18 -18
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/toolroll-update.d.ts +8 -0
- package/dist/toolroll-update.js +45 -8
- 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-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -2
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
- package/dist/teammate-admin.d.ts +0 -53
- package/dist/teammates-ui.d.ts +0 -20
- package/dist/teammates-ui.js +0 -166
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toolroll release --release-gate <id>`: lift one update pause that a browser deployment (scripts/deploy-browser.mjs)
|
|
3
|
+
* left behind when it stopped before the swap and its own recovery could not tell. The typical case is a deployment
|
|
4
|
+
* killed between installing the pause and journaling that it had: the journal still says `preparing`, so
|
|
5
|
+
* `--phase recover` leaves the pause alone, and new work stays paused.
|
|
6
|
+
*
|
|
7
|
+
* The id alone proves nothing, and neither does the journal's phase. The pause is lifted only when every piece of
|
|
8
|
+
* evidence agrees that the service was never swapped:
|
|
9
|
+
*
|
|
10
|
+
* - the live database's update pause is owned by exactly this id
|
|
11
|
+
* - exactly one intact deployment journal names this id and this database, at a phase before the swap, with
|
|
12
|
+
* nothing recorded that only the swap writes (stopping, migration, the new service), and no new service definition
|
|
13
|
+
* staged
|
|
14
|
+
* - the live database is still at the schema that journal recorded
|
|
15
|
+
* - the service definition still names the runtime the journal started from (and is the one it saved), never the
|
|
16
|
+
* staged one
|
|
17
|
+
* - that runtime's service is running, nothing runs from the staged runtime, and no deployment is still running
|
|
18
|
+
*
|
|
19
|
+
* Anything missing, damaged or contradicting refuses and leaves the pause as it is. Without --yes it only shows the
|
|
20
|
+
* proof; with --yes it lifts that one pause (the owner is checked again inside the same write) and journals it.
|
|
21
|
+
*/
|
|
22
|
+
import type { Database } from "./store.js";
|
|
23
|
+
/** The journal phases a deployment passes before it stops the old service (deploy-phases.mjs). */
|
|
24
|
+
export declare const BEFORE_SWAP: readonly string[];
|
|
25
|
+
export type GateReleaseWorld = {
|
|
26
|
+
/** The live database file. */
|
|
27
|
+
database: string;
|
|
28
|
+
/** The installation's state folder: deployments stage under staged-upgrades/ there. */
|
|
29
|
+
stateDir: string;
|
|
30
|
+
/** The live database, opened without migrating it. */
|
|
31
|
+
open(): Database;
|
|
32
|
+
/** The installed browser service definitions (LaunchAgents), path and text. */
|
|
33
|
+
servicePlists(): {
|
|
34
|
+
path: string;
|
|
35
|
+
text: string;
|
|
36
|
+
}[];
|
|
37
|
+
/** Every process on this machine: pid and full command line. */
|
|
38
|
+
processes(): {
|
|
39
|
+
pid: number;
|
|
40
|
+
command: string;
|
|
41
|
+
}[];
|
|
42
|
+
now(): Date;
|
|
43
|
+
};
|
|
44
|
+
type Journal = Record<string, unknown> & {
|
|
45
|
+
id: string;
|
|
46
|
+
phase: string;
|
|
47
|
+
database: string;
|
|
48
|
+
schema: number;
|
|
49
|
+
priorRuntime: string;
|
|
50
|
+
nextRuntime: string;
|
|
51
|
+
};
|
|
52
|
+
export type GateProof = {
|
|
53
|
+
ok: true;
|
|
54
|
+
id: string;
|
|
55
|
+
stage: string;
|
|
56
|
+
journal: Journal;
|
|
57
|
+
proof: string[];
|
|
58
|
+
} | {
|
|
59
|
+
ok: false;
|
|
60
|
+
reason: string;
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
/** The owner id written into the live pause's triggers, when there is one pause and it names one id. */
|
|
64
|
+
export declare function gateOwner(db: Database): string | null;
|
|
65
|
+
/** Whether the pause owned by `id` was left by a deployment that never began its swap, and the evidence that says so. */
|
|
66
|
+
export declare function proveBeforeSwap(id: string, world: GateReleaseWorld): GateProof;
|
|
67
|
+
export type GateReleaseOutcome = {
|
|
68
|
+
ok: true;
|
|
69
|
+
released: boolean;
|
|
70
|
+
id: string;
|
|
71
|
+
stage: string;
|
|
72
|
+
proof: string[];
|
|
73
|
+
} | {
|
|
74
|
+
ok: false;
|
|
75
|
+
reason: string;
|
|
76
|
+
message: string;
|
|
77
|
+
};
|
|
78
|
+
/** Prove, then (with `yes`) lift exactly that pause and journal the release in the deployment's own journal. */
|
|
79
|
+
export declare function releaseGate(id: string, world: GateReleaseWorld, yes: boolean): GateReleaseOutcome;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toolroll release --release-gate <id>`: lift one update pause that a browser deployment (scripts/deploy-browser.mjs)
|
|
3
|
+
* left behind when it stopped before the swap and its own recovery could not tell. The typical case is a deployment
|
|
4
|
+
* killed between installing the pause and journaling that it had: the journal still says `preparing`, so
|
|
5
|
+
* `--phase recover` leaves the pause alone, and new work stays paused.
|
|
6
|
+
*
|
|
7
|
+
* The id alone proves nothing, and neither does the journal's phase. The pause is lifted only when every piece of
|
|
8
|
+
* evidence agrees that the service was never swapped:
|
|
9
|
+
*
|
|
10
|
+
* - the live database's update pause is owned by exactly this id
|
|
11
|
+
* - exactly one intact deployment journal names this id and this database, at a phase before the swap, with
|
|
12
|
+
* nothing recorded that only the swap writes (stopping, migration, the new service), and no new service definition
|
|
13
|
+
* staged
|
|
14
|
+
* - the live database is still at the schema that journal recorded
|
|
15
|
+
* - the service definition still names the runtime the journal started from (and is the one it saved), never the
|
|
16
|
+
* staged one
|
|
17
|
+
* - that runtime's service is running, nothing runs from the staged runtime, and no deployment is still running
|
|
18
|
+
*
|
|
19
|
+
* Anything missing, damaged or contradicting refuses and leaves the pause as it is. Without --yes it only shows the
|
|
20
|
+
* proof; with --yes it lifts that one pause (the owner is checked again inside the same write) and journals it.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { durableJson } from "./desktop-update.js";
|
|
25
|
+
import { removeUpdateGate, updateGateOwned } from "./desktop-update-gate.js";
|
|
26
|
+
/** The journal phases a deployment passes before it stops the old service (deploy-phases.mjs). */
|
|
27
|
+
export const BEFORE_SWAP = Object.freeze(["preparing", "admission-paused", "frozen", "backup-verified", "rehearsed"]);
|
|
28
|
+
/** Journal fields that only the swap and later phases write. */
|
|
29
|
+
const SWAP_FIELDS = ["stoppingService", "codingBackupBeforeSwap", "migration", "newService", "deployedAt"];
|
|
30
|
+
const GATE_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
31
|
+
const refuse = (reason, message) => ({ ok: false, reason, message: `${message} The update pause was left as it is.` });
|
|
32
|
+
/** The owner id written into the live pause's triggers, when there is one pause and it names one id. */
|
|
33
|
+
export function gateOwner(db) {
|
|
34
|
+
const rows = db.prepare("SELECT sql FROM sqlite_master WHERE type='trigger' AND name GLOB 'so_desktop_update_*'").all();
|
|
35
|
+
const ids = new Set(rows.map(row => /\[([a-f0-9-]{36})\]'\)/.exec(String(row["sql"]))?.[1] ?? "?"));
|
|
36
|
+
return ids.size === 1 ? [...ids][0] : null;
|
|
37
|
+
}
|
|
38
|
+
/** Every deployment journal under staged-upgrades/: the stage it is in and what it holds, or why it can't be read. */
|
|
39
|
+
function journals(stateDir) {
|
|
40
|
+
const root = join(stateDir, "staged-upgrades");
|
|
41
|
+
let stages = [];
|
|
42
|
+
try {
|
|
43
|
+
stages = readdirSync(root, { withFileTypes: true }).filter(one => one.isDirectory()).map(one => join(root, one.name));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return stages.filter(stage => existsSync(join(stage, "deployment.json"))).map(stage => {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(readFileSync(join(stage, "deployment.json"), "utf8"));
|
|
51
|
+
return { stage, journal: parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { stage, journal: null };
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Whether the pause owned by `id` was left by a deployment that never began its swap, and the evidence that says so. */
|
|
59
|
+
export function proveBeforeSwap(id, world) {
|
|
60
|
+
if (!GATE_ID.test(id))
|
|
61
|
+
return refuse("usage", `${id} is not an update id.`);
|
|
62
|
+
const proof = [];
|
|
63
|
+
let owner, owned, schema;
|
|
64
|
+
const db = world.open();
|
|
65
|
+
try {
|
|
66
|
+
owner = gateOwner(db);
|
|
67
|
+
owned = updateGateOwned(db, id);
|
|
68
|
+
schema = Number(db.prepare("SELECT version FROM schema_version").get()?.["version"]);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
db.close();
|
|
72
|
+
}
|
|
73
|
+
if (owner === null)
|
|
74
|
+
return refuse("not-paused", "No single update pause is held on the live database.");
|
|
75
|
+
if (owner !== id || !owned)
|
|
76
|
+
return refuse("not-owner", `The live update pause belongs to ${owner}, not ${id}.`);
|
|
77
|
+
proof.push(`The live database's update pause belongs to ${id}.`);
|
|
78
|
+
const all = journals(world.stateDir);
|
|
79
|
+
const mine = all.filter(one => one.journal?.["id"] === id);
|
|
80
|
+
if (mine.length > 1)
|
|
81
|
+
return refuse("conflict", `${mine.length} deployment journals name ${id}: ${mine.map(one => one.stage).join(", ")}.`);
|
|
82
|
+
if (mine.length === 0) {
|
|
83
|
+
const damaged = all.filter(one => one.journal === null);
|
|
84
|
+
return refuse(damaged.length > 0 ? "damaged" : "no-journal", damaged.length > 0
|
|
85
|
+
? `No readable deployment journal names ${id}, and ${damaged.map(one => join(one.stage, "deployment.json")).join(", ")} can't be read; it may be that deployment's.`
|
|
86
|
+
: `No deployment journal under ${join(world.stateDir, "staged-upgrades")} names ${id}; this pause was not left by a browser deployment.`);
|
|
87
|
+
}
|
|
88
|
+
const { stage, journal: raw } = mine[0];
|
|
89
|
+
const r = raw;
|
|
90
|
+
if (typeof r.phase !== "string" || typeof r.priorRuntime !== "string" || typeof r.nextRuntime !== "string" || typeof r.schema !== "number" || typeof r.database !== "string") {
|
|
91
|
+
return refuse("damaged", `${join(stage, "deployment.json")} is missing its phase, runtimes, schema or database.`);
|
|
92
|
+
}
|
|
93
|
+
if (r.database !== world.database)
|
|
94
|
+
return refuse("conflict", `That deployment ran on ${r.database}, not ${world.database}.`);
|
|
95
|
+
if (!BEFORE_SWAP.includes(r.phase))
|
|
96
|
+
return refuse("after-swap", `That deployment is at ${r.phase}, past the point where it stops the service. Recover it with \`node scripts/deploy-browser.mjs --stage ${stage} --phase recover\` from its checkout.`);
|
|
97
|
+
const swapped = SWAP_FIELDS.filter(field => r[field] !== undefined && r[field] !== null);
|
|
98
|
+
if (swapped.length > 0)
|
|
99
|
+
return refuse("conflict", `The journal says ${r.phase} but records ${swapped.join(", ")}, which only the swap writes.`);
|
|
100
|
+
if (existsSync(join(stage, "browser.next.plist")))
|
|
101
|
+
return refuse("conflict", `The journal says ${r.phase} but a new service definition was staged (${join(stage, "browser.next.plist")}).`);
|
|
102
|
+
proof.push(`Deployment journal ${join(stage, "deployment.json")} stopped at ${r.phase}, before the swap, and records nothing the swap writes.`);
|
|
103
|
+
if (!Number.isInteger(schema) || schema !== r.schema)
|
|
104
|
+
return refuse("conflict", `The live database is at schema ${schema}; the deployment started at ${r.schema}.`);
|
|
105
|
+
proof.push(`The live database is still at schema ${schema}.`);
|
|
106
|
+
const plists = world.servicePlists();
|
|
107
|
+
if (plists.length !== 1)
|
|
108
|
+
return refuse("conflict", plists.length === 0 ? "No browser service definition is installed." : `More than one browser service definition is installed: ${plists.map(one => one.path).join(", ")}.`);
|
|
109
|
+
const plist = plists[0];
|
|
110
|
+
if (!plist.text.includes(`<string>${r.priorRuntime}/cli.js</string>`) || plist.text.includes(r.nextRuntime))
|
|
111
|
+
return refuse("conflict", `${plist.path} does not name the runtime the deployment started from (${r.priorRuntime}).`);
|
|
112
|
+
const saved = join(stage, "browser.saved.plist");
|
|
113
|
+
if (existsSync(saved) && readFileSync(saved, "utf8") !== plist.text)
|
|
114
|
+
return refuse("conflict", `${plist.path} differs from the definition the deployment saved (${saved}).`);
|
|
115
|
+
proof.push(`${plist.path} still names ${r.priorRuntime}${existsSync(saved) ? " and matches the saved copy" : ""}.`);
|
|
116
|
+
const processes = world.processes();
|
|
117
|
+
const deploying = processes.filter(one => one.command.includes("deploy-browser.mjs"));
|
|
118
|
+
if (deploying.length > 0)
|
|
119
|
+
return refuse("deploying", `A deployment is still running (process ${deploying.map(one => one.pid).join(", ")}); let it finish or recover.`);
|
|
120
|
+
const staged = processes.filter(one => one.command.includes(r.nextRuntime));
|
|
121
|
+
if (staged.length > 0)
|
|
122
|
+
return refuse("after-swap", `Process ${staged.map(one => one.pid).join(", ")} runs from the staged runtime ${r.nextRuntime}.`);
|
|
123
|
+
const supervisor = processes.find(one => one.command.includes(`${r.priorRuntime}/controller-service.js`));
|
|
124
|
+
const worker = processes.find(one => one.command.includes(`${r.priorRuntime}/cli.js up --db ${world.database}`));
|
|
125
|
+
if (supervisor === undefined || worker === undefined)
|
|
126
|
+
return refuse("conflict", `The service is not running from ${r.priorRuntime}, so it can't be shown that the swap never began.`);
|
|
127
|
+
proof.push(`The service runs from ${r.priorRuntime} (process ${supervisor.pid}); nothing runs from the staged runtime, and no deployment is running.`);
|
|
128
|
+
return { ok: true, id, stage, journal: r, proof };
|
|
129
|
+
}
|
|
130
|
+
/** Prove, then (with `yes`) lift exactly that pause and journal the release in the deployment's own journal. */
|
|
131
|
+
export function releaseGate(id, world, yes) {
|
|
132
|
+
const proved = proveBeforeSwap(id, world);
|
|
133
|
+
if (!proved.ok)
|
|
134
|
+
return proved;
|
|
135
|
+
if (!yes)
|
|
136
|
+
return { ok: true, released: false, id, stage: proved.stage, proof: proved.proof };
|
|
137
|
+
const db = world.open();
|
|
138
|
+
// removeUpdateGate checks the owner again inside its own write: a pause that changed hands is not touched.
|
|
139
|
+
try {
|
|
140
|
+
removeUpdateGate(db, id);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
db.close();
|
|
144
|
+
}
|
|
145
|
+
durableJson(join(proved.stage, "deployment.json"), {
|
|
146
|
+
...proved.journal, phase: "released", updatedAt: world.now().toISOString(),
|
|
147
|
+
releasedBeforeSwap: { at: world.now().toISOString(), by: "toolroll release --release-gate", from: proved.journal.phase, proof: proved.proof },
|
|
148
|
+
});
|
|
149
|
+
return { ok: true, released: true, id, stage: proved.stage, proof: proved.proof };
|
|
150
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Both CLI names, moved to a deployed runtime (release.ts, step `link`): `toolroll` and `standing-orders` are pointed
|
|
3
|
+
* at `<runtime>/bin.js`, each link replaced whole (a new link renamed over the old one). Each name must then resolve to
|
|
4
|
+
* that file and answer with the released version, or every link goes back to where it pointed before.
|
|
5
|
+
*
|
|
6
|
+
* The coordinator saves what `findCliLinks` saw before switching, so a release killed halfway through relinking still
|
|
7
|
+
* knows where to put each name back. A name on PATH that is a file rather than a link is someone else's and never
|
|
8
|
+
* replaced.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CLI_NAMES: readonly ["toolroll", "standing-orders"];
|
|
11
|
+
/** One name on PATH: where its link is and what it pointed at before the release (null: not there yet). */
|
|
12
|
+
export type CliLink = {
|
|
13
|
+
name: string;
|
|
14
|
+
path: string;
|
|
15
|
+
before: string | null;
|
|
16
|
+
foreign?: true;
|
|
17
|
+
};
|
|
18
|
+
/** Where each name is on PATH. A missing name is made beside the other one; none on PATH at all is an empty list. */
|
|
19
|
+
export declare function findCliLinks(path?: string, names?: readonly string[]): CliLink[];
|
|
20
|
+
/** Point `link` at `target`, atomically: a new link beside it, renamed over it. */
|
|
21
|
+
export declare function pointLink(link: string, target: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Switch every link to `bin`. `answers(path)` says whether that name now answers as the released build. Any failure
|
|
24
|
+
* puts every link back where `links` says it was, and throws. Switching a link already at `bin` changes nothing, so a
|
|
25
|
+
* resumed relink repeats safely.
|
|
26
|
+
*/
|
|
27
|
+
export declare function switchCliLinks(links: readonly CliLink[], bin: string, answers: (path: string) => boolean): CliLink[];
|
|
28
|
+
/** Each link where it pointed before; a link that wasn't there before is removed. */
|
|
29
|
+
export declare function restoreCliLinks(links: readonly CliLink[]): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Both CLI names, moved to a deployed runtime (release.ts, step `link`): `toolroll` and `standing-orders` are pointed
|
|
3
|
+
* at `<runtime>/bin.js`, each link replaced whole (a new link renamed over the old one). Each name must then resolve to
|
|
4
|
+
* that file and answer with the released version, or every link goes back to where it pointed before.
|
|
5
|
+
*
|
|
6
|
+
* The coordinator saves what `findCliLinks` saw before switching, so a release killed halfway through relinking still
|
|
7
|
+
* knows where to put each name back. A name on PATH that is a file rather than a link is someone else's and never
|
|
8
|
+
* replaced.
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { lstatSync, readlinkSync, realpathSync, renameSync, rmSync, symlinkSync } from "node:fs";
|
|
12
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
13
|
+
export const CLI_NAMES = Object.freeze(["toolroll", "standing-orders"]);
|
|
14
|
+
/** Where each name is on PATH. A missing name is made beside the other one; none on PATH at all is an empty list. */
|
|
15
|
+
export function findCliLinks(path = process.env["PATH"] ?? "", names = CLI_NAMES) {
|
|
16
|
+
const found = names.map((name) => {
|
|
17
|
+
for (const dir of path.split(delimiter).filter(Boolean)) {
|
|
18
|
+
const at = join(dir, name);
|
|
19
|
+
let entry;
|
|
20
|
+
try {
|
|
21
|
+
entry = lstatSync(at);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!entry.isSymbolicLink())
|
|
27
|
+
return { name, path: at, before: null, foreign: true };
|
|
28
|
+
return { name, path: at, before: resolve(dirname(at), readlinkSync(at)) };
|
|
29
|
+
}
|
|
30
|
+
return { name, path: null, before: null };
|
|
31
|
+
});
|
|
32
|
+
const home = found.find(one => one.path !== null && one.foreign === undefined);
|
|
33
|
+
if (home === undefined)
|
|
34
|
+
return found.filter((one) => one.path !== null);
|
|
35
|
+
return found.map(one => ({ ...one, path: one.path ?? join(dirname(home.path), one.name) }));
|
|
36
|
+
}
|
|
37
|
+
/** Point `link` at `target`, atomically: a new link beside it, renamed over it. */
|
|
38
|
+
export function pointLink(link, target) {
|
|
39
|
+
const temp = `${link}.${randomUUID().slice(0, 8)}.link`;
|
|
40
|
+
symlinkSync(target, temp);
|
|
41
|
+
try {
|
|
42
|
+
renameSync(temp, link);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
rmSync(temp, { force: true });
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const target = (link) => { try {
|
|
50
|
+
return resolve(dirname(link), readlinkSync(link));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
} };
|
|
55
|
+
const real = (path) => { try {
|
|
56
|
+
return realpathSync(path);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
} };
|
|
61
|
+
/**
|
|
62
|
+
* Switch every link to `bin`. `answers(path)` says whether that name now answers as the released build. Any failure
|
|
63
|
+
* puts every link back where `links` says it was, and throws. Switching a link already at `bin` changes nothing, so a
|
|
64
|
+
* resumed relink repeats safely.
|
|
65
|
+
*/
|
|
66
|
+
export function switchCliLinks(links, bin, answers) {
|
|
67
|
+
const foreign = links.find(one => one.foreign === true);
|
|
68
|
+
if (foreign !== undefined)
|
|
69
|
+
throw new Error(`${foreign.path} is not a link; it was left as it is`);
|
|
70
|
+
if (links.length < CLI_NAMES.length)
|
|
71
|
+
throw new Error(`found ${links.length === 0 ? "no CLI name" : `only ${links.map(one => one.name).join(", ")}`} on PATH; link both with \`toolroll link\` first`);
|
|
72
|
+
const wanted = real(bin);
|
|
73
|
+
if (wanted === null)
|
|
74
|
+
throw new Error(`${bin} does not exist`);
|
|
75
|
+
try {
|
|
76
|
+
for (const link of links)
|
|
77
|
+
if (target(link.path) !== bin)
|
|
78
|
+
pointLink(link.path, bin);
|
|
79
|
+
for (const link of links) {
|
|
80
|
+
if (real(link.path) !== wanted)
|
|
81
|
+
throw new Error(`${link.path} does not resolve to ${bin}`);
|
|
82
|
+
if (!answers(link.path))
|
|
83
|
+
throw new Error(`${link.path} does not answer as the released build`);
|
|
84
|
+
}
|
|
85
|
+
return links.map(({ name, path, before }) => ({ name, path, before }));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
restoreCliLinks(links);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Each link where it pointed before; a link that wasn't there before is removed. */
|
|
93
|
+
export function restoreCliLinks(links) {
|
|
94
|
+
for (const link of links) {
|
|
95
|
+
if (link.foreign === true)
|
|
96
|
+
continue;
|
|
97
|
+
if (link.before === null)
|
|
98
|
+
rmSync(link.path, { force: true });
|
|
99
|
+
else if (target(link.path) !== link.before)
|
|
100
|
+
pointLink(link.path, link.before);
|
|
101
|
+
}
|
|
102
|
+
}
|