toolroll 0.9.88 → 0.9.90
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 +65 -0
- package/README.md +16 -10
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-board.d.ts +9 -0
- package/dist/agent-board.js +20 -0
- package/dist/agent-fence.js +20 -2
- package/dist/approval-authority.d.ts +7 -0
- package/dist/approval-authority.js +13 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +692 -28
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +30 -30
- package/dist/browser-workspace.d.ts +13 -0
- package/dist/builder.js +5 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +7 -1
- package/dist/chat-channel.js +11 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-task-actions.d.ts +1 -0
- package/dist/check-access.d.ts +3 -0
- package/dist/check-access.js +4 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +6 -2
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +23 -0
- package/dist/coding-ui.js +4 -2
- package/dist/connect-cli.js +2 -1
- package/dist/console-instance.d.ts +34 -1
- package/dist/console-instance.js +124 -15
- package/dist/contracts/cli.d.ts +72 -0
- package/dist/contracts/cli.js +13 -0
- package/dist/contracts/console-api.d.ts +9 -2
- package/dist/contracts/console-api.js +9 -2
- package/dist/desktop-host.js +8 -3
- package/dist/desktop-update.js +2 -1
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +22 -2
- package/dist/easy-start-args.d.ts +13 -0
- package/dist/easy-start-args.js +24 -0
- package/dist/easy-start.d.ts +77 -0
- package/dist/easy-start.js +321 -0
- package/dist/egress.js +5 -0
- package/dist/first-run.d.ts +77 -0
- package/dist/first-run.js +45 -0
- package/dist/flow-starters.js +3 -0
- package/dist/flow-triggers.js +4 -0
- package/dist/folder-gate.d.ts +32 -0
- package/dist/folder-gate.js +43 -0
- package/dist/guides.js +1 -1
- package/dist/invoke.js +11 -6
- package/dist/laptop-background.d.ts +171 -0
- package/dist/laptop-background.js +550 -0
- package/dist/laptop-service.d.ts +111 -0
- package/dist/laptop-service.js +268 -0
- package/dist/lead-cli.js +2 -1
- package/dist/lead-doors.d.ts +6 -0
- package/dist/lead-doors.js +20 -2
- package/dist/live-records.d.ts +1 -1
- package/dist/live-records.js +3 -12
- package/dist/local-owner.d.ts +124 -0
- package/dist/local-owner.js +212 -0
- package/dist/login-secrets.d.ts +60 -0
- package/dist/login-secrets.js +195 -0
- package/dist/mcp-oauth.d.ts +4 -0
- package/dist/mcp-oauth.js +13 -7
- package/dist/model-choices.d.ts +43 -0
- package/dist/model-choices.js +80 -0
- package/dist/operate.d.ts +46 -1
- package/dist/operate.js +461 -40
- package/dist/person-proof.js +3 -0
- package/dist/project-access.js +15 -1
- package/dist/project-admission.d.ts +6 -1
- package/dist/project-admission.js +44 -7
- package/dist/project-identity.d.ts +8 -0
- package/dist/project-identity.js +43 -0
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/project.d.ts +1 -0
- package/dist/project.js +6 -0
- package/dist/proposal.js +11 -1
- package/dist/quick-start.d.ts +3 -1
- package/dist/quick-start.js +6 -1
- package/dist/recipes.js +12 -1
- package/dist/release-cli.js +2 -1
- package/dist/runner-process.d.ts +9 -0
- package/dist/runner-process.js +18 -0
- package/dist/sample-project.d.ts +31 -0
- package/dist/sample-project.js +141 -0
- package/dist/scope.d.ts +24 -2
- package/dist/scope.js +74 -3
- package/dist/serve.js +157 -28
- package/dist/server/chat.js +10 -3
- package/dist/server/chrome.d.ts +10 -2
- package/dist/server/chrome.js +7 -5
- package/dist/server/console.css +10 -0
- package/dist/server/guards.d.ts +3 -0
- package/dist/server/guards.js +22 -2
- package/dist/server/http.d.ts +22 -0
- package/dist/server/http.js +58 -0
- package/dist/server/live.js +4 -14
- package/dist/server/mutation-key.d.ts +70 -0
- package/dist/server/mutation-key.js +142 -0
- package/dist/server/pages.js +12 -9
- package/dist/server/people-tokens.js +240 -20
- package/dist/server/remote.d.ts +2 -0
- package/dist/server/remote.js +19 -4
- package/dist/server/render-new-task.js +5 -1
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-people.d.ts +36 -0
- package/dist/server/render-people.js +65 -1
- package/dist/server/render-projects.js +4 -2
- package/dist/server/render-tasks.d.ts +12 -0
- package/dist/server/render-tasks.js +16 -6
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/route-table.d.ts +16 -0
- package/dist/server/route-table.js +121 -0
- package/dist/server/runtime.d.ts +6 -4
- package/dist/server/session.d.ts +31 -1
- package/dist/server/session.js +12 -2
- package/dist/server/settings.js +63 -8
- package/dist/server/task-views.js +10 -1
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +59 -9
- package/dist/server/terminals.js +15 -2
- package/dist/server/welcome.d.ts +22 -16
- package/dist/server/welcome.js +150 -52
- package/dist/settings-groups-ui.d.ts +7 -1
- package/dist/settings-groups-ui.js +23 -2
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/setup-guide.js +6 -0
- package/dist/store.d.ts +124 -1
- package/dist/store.js +245 -12
- package/dist/surface.d.ts +10 -0
- package/dist/surface.js +40 -1
- package/dist/task-text.d.ts +6 -0
- package/dist/task-text.js +17 -0
- package/dist/teardown-git.js +16 -1
- package/dist/telegram-lead.js +2 -0
- package/dist/terminal-text.d.ts +11 -0
- package/dist/terminal-text.js +26 -0
- package/dist/toolroll-update.d.ts +8 -1
- package/dist/toolroll-update.js +151 -4
- package/dist/tui-contract.d.ts +118 -0
- package/dist/tui-contract.js +9 -0
- package/dist/tui-source.d.ts +14 -0
- package/dist/tui-source.js +182 -0
- package/dist/tui.js +129 -0
- package/dist/up-processes.d.ts +13 -1
- package/dist/up-processes.js +13 -3
- package/dist/welcome-facts.d.ts +18 -0
- package/dist/welcome-facts.js +79 -0
- package/dist/youiyoux-intake.js +11 -1
- package/package.json +4 -2
package/dist/surface.js
CHANGED
|
@@ -431,6 +431,13 @@ const GUIDE_ROWS = [
|
|
|
431
431
|
operator("up", "console + worker + browser, one command"),
|
|
432
432
|
operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), offer the starter flows (--starter <ids>), and print the handoff"),
|
|
433
433
|
operator("daemon", "install the loop under the OS service manager"),
|
|
434
|
+
operator("background on", "keep this computer's Toolroll running: a login item that starts `up --laptop` when you log in; hands over from the `up` this terminal runs and puts everything back if the service doesn't answer (--port)"),
|
|
435
|
+
operator("background off", "stop running in the background: unloads and removes only Toolroll's own login item"),
|
|
436
|
+
operatorRead("background show", "whether Toolroll runs in the background here, whether it is running now, and any other service on this database"),
|
|
437
|
+
operator("stop", "stop this computer's Toolroll after its running work is saved: the background service (until the next login), or the `up` running in a terminal"),
|
|
438
|
+
operator("open", "open this computer's Toolroll at a terminal, signed in: the terminal UI, or the browser with --web (starts it first when it isn't running); never prints a sign-in link"),
|
|
439
|
+
{ ...operatorRead("welcome", "the three steps to a first result (agent, project, first task) as the console's welcome shows them; at a terminal it walks them one at a time"), flags: [jsonFlag, dbFlag] },
|
|
440
|
+
operator("password set", "set this computer's first password, typed twice at your own terminal (never a flag, file or environment variable); every session signs in again and local mode stays on"),
|
|
434
441
|
{ ...operatorRead("doctor", "check that Toolroll's service holds no key, token or password a build could read; names what it finds and the steps to fix it, never a value, and changes nothing"), flags: [jsonFlag] },
|
|
435
442
|
];
|
|
436
443
|
/**
|
|
@@ -450,7 +457,7 @@ const REMOTE_NO = [
|
|
|
450
457
|
// Workers, the loop and the services that run here.
|
|
451
458
|
"claim", "heartbeat", "worker release", "reap", "tick", "build", "reconcile",
|
|
452
459
|
"runner register", "runner retire", "runner bind", "runner capacity", "coordinator mint", "coordinator revoke", "mcp", "outbox deliver", "peek",
|
|
453
|
-
"serve", "serve check-public", "watch", "up", "onboard", "daemon", "doctor", "bridge", "setup show", "setup clear",
|
|
460
|
+
"serve", "serve check-public", "watch", "up", "onboard", "daemon", "background on", "background off", "background show", "stop", "open", "welcome", "password set", "doctor", "bridge", "setup show", "setup clear",
|
|
454
461
|
// Provider keys and model tools on this machine.
|
|
455
462
|
"providers", "keys status", "keys set", "keys clear", "keys verify", "keys auth", "models check", "models update", "models watch",
|
|
456
463
|
// Publication and release.
|
|
@@ -497,6 +504,38 @@ export const REMOTE_POLICY = new Map([
|
|
|
497
504
|
]);
|
|
498
505
|
/** How many times each invocation was classified — the contract test refuses a row stated twice. */
|
|
499
506
|
export const REMOTE_POLICY_COUNT = REMOTE_NO.length + REMOTE_STEP_UP.length + REMOTE_YES.length;
|
|
507
|
+
/**
|
|
508
|
+
* Easy-start C3: the commands a laptop's local owner may run through the terminal UI (local-owner.ts runAsLocalOwner),
|
|
509
|
+
* each with the act it is; every other row (and every command with no row) is refused to the local owner before any
|
|
510
|
+
* command code runs. Credential, people, policy and infrastructure commands are never here.
|
|
511
|
+
*/
|
|
512
|
+
const LOCAL_ALLOW = {
|
|
513
|
+
// Reads.
|
|
514
|
+
pulls: "read", graph: "read", repos: "read", status: "read", ready: "read", gaps: "read", contract: "read", "skills list": "read", "skills get": "read",
|
|
515
|
+
"task list": "read", "task show": "read", "task state": "read", "task wait": "read", "check-progress": "read", "review show": "read", welcome: "read",
|
|
516
|
+
// Acts (plan C1's list).
|
|
517
|
+
"task add": "file-task",
|
|
518
|
+
chat: "lead-send",
|
|
519
|
+
"task steer": "lead-send",
|
|
520
|
+
"task approve": "approve-plan",
|
|
521
|
+
decide: "answer-question",
|
|
522
|
+
"task stop": "stop-run",
|
|
523
|
+
"task pause": "pause-run",
|
|
524
|
+
"task hold": "pause-run",
|
|
525
|
+
"task resume": "resume-run",
|
|
526
|
+
"task continue": "resume-run",
|
|
527
|
+
"task unhold": "resume-run",
|
|
528
|
+
"task complete": "complete-task",
|
|
529
|
+
"task revise": "request-revision",
|
|
530
|
+
"task archive": "archive-task",
|
|
531
|
+
"task restore": "archive-task",
|
|
532
|
+
"repos add": "add-project",
|
|
533
|
+
};
|
|
534
|
+
/** Every guide row's local-owner mark, by invocation: `allow` with its act, else `deny`. Exported for the inventory test. */
|
|
535
|
+
export const LOCAL_POLICY = new Map(GUIDE_ROWS.map(row => [row.invocation,
|
|
536
|
+
Object.prototype.hasOwnProperty.call(LOCAL_ALLOW, row.invocation) ? { local: "allow", act: LOCAL_ALLOW[row.invocation] } : { local: "deny" }]));
|
|
537
|
+
/** The invocations LOCAL_ALLOW names, for the inventory test (one naming no row is stale). */
|
|
538
|
+
export const LOCAL_ALLOWED_INVOCATIONS = Object.keys(LOCAL_ALLOW);
|
|
500
539
|
/** Every declared command, in guide order, paired with the schema of its `--json` answer (src/contracts/cli.ts).
|
|
501
540
|
* An unclassified row is "no": nothing runs remotely by omission. */
|
|
502
541
|
export const COMMAND_ENTRIES = commandEntries(GUIDE_ROWS.map(row => ({ ...row, remote: REMOTE_POLICY.get(row.invocation) ?? "no" })));
|
package/dist/task-text.d.ts
CHANGED
|
@@ -23,3 +23,9 @@ export declare function validateTaskText(fields: {
|
|
|
23
23
|
outOfScope?: string | null;
|
|
24
24
|
touches?: readonly string[];
|
|
25
25
|
}): TaskTextRefusal | null;
|
|
26
|
+
/**
|
|
27
|
+
* A title that fits: words longer than the title limit are cut at the last whole word that fits, with an ellipsis,
|
|
28
|
+
* never mid-word (one unbroken word longer than half the limit is the only thing cut inside a word). Whitespace runs
|
|
29
|
+
* read as one space. Text within the limit is returned as it is.
|
|
30
|
+
*/
|
|
31
|
+
export declare function titleWithin(text: string, max?: number): string;
|
package/dist/task-text.js
CHANGED
|
@@ -41,3 +41,20 @@ export function validateTaskText(fields) {
|
|
|
41
41
|
const bad = validateScopeText(fields);
|
|
42
42
|
return bad === null ? null : refuse("bad-goal", bad.message);
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* A title that fits: words longer than the title limit are cut at the last whole word that fits, with an ellipsis,
|
|
46
|
+
* never mid-word (one unbroken word longer than half the limit is the only thing cut inside a word). Whitespace runs
|
|
47
|
+
* read as one space. Text within the limit is returned as it is.
|
|
48
|
+
*/
|
|
49
|
+
export function titleWithin(text, max = TASK_TEXT_LIMITS.title) {
|
|
50
|
+
if (text.length <= max)
|
|
51
|
+
return text;
|
|
52
|
+
const words = text.replace(/\s+/g, " ").trim();
|
|
53
|
+
if (words.length <= max)
|
|
54
|
+
return words;
|
|
55
|
+
const room = words.slice(0, max - 1);
|
|
56
|
+
// The room ends at a whole word when the next character is a space; else back to the last space in it.
|
|
57
|
+
const space = words[max - 1] === " " ? room.length : room.lastIndexOf(" ");
|
|
58
|
+
const cut = (space >= Math.floor(max / 2) ? room.slice(0, space) : room).replace(/[\s,;:.\-–—]+$/u, "");
|
|
59
|
+
return `${cut}…`;
|
|
60
|
+
}
|
package/dist/teardown-git.js
CHANGED
|
@@ -20,6 +20,7 @@ import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:pa
|
|
|
20
20
|
import { run } from "./exec.js";
|
|
21
21
|
import { ENV, ENV_ALLOWLIST } from "./remote-main.js";
|
|
22
22
|
import { WORKTREE_TIMEOUT_MS } from "./worktree.js";
|
|
23
|
+
import { FOLDER_DIDNT_ANSWER, folderAnswers } from "./folder-gate.js";
|
|
23
24
|
/** Fixed on every teardown command, ahead of its own arguments. */
|
|
24
25
|
export const TEARDOWN_FLAGS = [
|
|
25
26
|
"--no-optional-locks", "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "-c", "protocol.ext.allow=never",
|
|
@@ -346,8 +347,16 @@ function gitDirsOf(checkout) {
|
|
|
346
347
|
catch {
|
|
347
348
|
return null;
|
|
348
349
|
}
|
|
350
|
+
// Where the checkout's own files point (a build can write both): read, in the console, only while that folder
|
|
351
|
+
// answered a look lately, and never vouched for by the checkout pool's spelling (folder-gate.ts). Held back: not one
|
|
352
|
+
// git would read the same way, so every caller refuses.
|
|
353
|
+
if (gitdir !== join(checkout, ".git") && !folderAnswers(gitdir, true))
|
|
354
|
+
return null;
|
|
349
355
|
const named = regularFile(join(gitdir, "commondir"))?.trim();
|
|
350
|
-
|
|
356
|
+
const common = named === undefined || named === "" ? gitdir : resolve(gitdir, named);
|
|
357
|
+
if (common !== gitdir && !folderAnswers(common, true))
|
|
358
|
+
return null;
|
|
359
|
+
return { gitdir, common };
|
|
351
360
|
}
|
|
352
361
|
/** What includeIn answers when Git didn't finish parsing the file in time: still a refusal, named as what it is. */
|
|
353
362
|
export const INCLUDE_PARSE_TIMED_OUT = "Git didn't finish reading it in time";
|
|
@@ -1062,6 +1071,9 @@ export function checkoutGit(inner = run, mode = "build", binding = {}) {
|
|
|
1062
1071
|
if (file !== "git")
|
|
1063
1072
|
return inner(file, args, options);
|
|
1064
1073
|
const cwd = options?.cwd;
|
|
1074
|
+
// In the console, a checkout whose folder hasn't answered a look lately is not read on its thread (folder-gate.ts).
|
|
1075
|
+
if (cwd !== undefined && !folderAnswers(cwd))
|
|
1076
|
+
return refused(FOLDER_DIDNT_ANSWER);
|
|
1065
1077
|
if (cwd === undefined || checkoutIncludes(cwd))
|
|
1066
1078
|
return refused("the checkout's git config includes another file, or couldn't be read as a plain file");
|
|
1067
1079
|
// git-lfs also reads .lfsconfig from the checkout: one that names an extension (a command it would run) is refused.
|
|
@@ -1158,6 +1170,9 @@ export function viewedGitSync(cwd, args, options = {}) {
|
|
|
1158
1170
|
const expire = () => { if (limit !== undefined && limit() <= 0)
|
|
1159
1171
|
throw new Error("the read's deadline passed"); };
|
|
1160
1172
|
expire();
|
|
1173
|
+
// In the console, a checkout whose folder hasn't answered a look lately is not read on its thread (folder-gate.ts).
|
|
1174
|
+
if (!folderAnswers(cwd))
|
|
1175
|
+
throw new Error(FOLDER_DIDNT_ANSWER);
|
|
1161
1176
|
if (checkoutIncludes(cwd, gitFile, limit))
|
|
1162
1177
|
throw new Error("the checkout's git config includes another file, or couldn't be read as a plain file");
|
|
1163
1178
|
expire();
|
package/dist/telegram-lead.js
CHANGED
|
@@ -285,6 +285,8 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
285
285
|
return { effects, confirmed: false, ignored: true };
|
|
286
286
|
}
|
|
287
287
|
const outcome = confirmLeadProposal(store, who, proposal.id, now, {
|
|
288
|
+
// The paired phone speaks for its account's full authority (pairing is a full session's act).
|
|
289
|
+
authority: "full",
|
|
288
290
|
via: "telegram",
|
|
289
291
|
evidenceRoot: options.evidenceRoot,
|
|
290
292
|
confirm: action.phase === "yes",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text someone else wrote, made safe to print on a terminal: the one sanitizer the live view, `peek` and the terminal
|
|
3
|
+
* UI share. Escape sequences in every form go (CSI in 7- or 8-bit form, OSC such as hyperlinks and title changes,
|
|
4
|
+
* every other ESC sequence), and with them every C0 and C1 control character (a carriage return that would
|
|
5
|
+
* overwrite a line, a backspace, a bell), every invisible format character and bidi override; a tab becomes a space.
|
|
6
|
+
* Pure: no imports, so the terminal UI's bundle carries it as is.
|
|
7
|
+
*/
|
|
8
|
+
/** One line: controls, escapes and bidi overrides out; tabs to spaces. A newline is a control here too. */
|
|
9
|
+
export declare function cleanTerminalLine(text: string): string;
|
|
10
|
+
/** Any text for the terminal UI: each line cleaned as above, its newlines kept (a lone carriage return is not one). */
|
|
11
|
+
export declare function terminalText(text: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text someone else wrote, made safe to print on a terminal: the one sanitizer the live view, `peek` and the terminal
|
|
3
|
+
* UI share. Escape sequences in every form go (CSI in 7- or 8-bit form, OSC such as hyperlinks and title changes,
|
|
4
|
+
* every other ESC sequence), and with them every C0 and C1 control character (a carriage return that would
|
|
5
|
+
* overwrite a line, a backspace, a bell), every invisible format character and bidi override; a tab becomes a space.
|
|
6
|
+
* Pure: no imports, so the terminal UI's bundle carries it as is.
|
|
7
|
+
*/
|
|
8
|
+
/** Bidi embedding, override and isolate controls. */
|
|
9
|
+
const BIDI_CONTROLS = /[--]/g;
|
|
10
|
+
/** One line: controls, escapes and bidi overrides out; tabs to spaces. A newline is a control here too. */
|
|
11
|
+
export function cleanTerminalLine(text) {
|
|
12
|
+
return text
|
|
13
|
+
// Escape sequences in every form: CSI (7- or 8-bit, any parameter bytes including ':'), OSC, and the rest.
|
|
14
|
+
.replace(/(?:\x1b\[|\u009b)[0-?]*[ -/]*[@-~]/g, "")
|
|
15
|
+
.replace(/(?:\x1b\]|\u009d)[^\x07\x1b\u009c]*(?:\x07|\x1b\\|\u009c)?/g, "")
|
|
16
|
+
.replace(/\x1b[ -/]*[0-~]/g, "")
|
|
17
|
+
.replace(/\t/g, " ")
|
|
18
|
+
.replace(/[\x00-\x1f\x7f-\x9f]/g, "")
|
|
19
|
+
// Every invisible format character (zero-width, direction marks and overrides, joiners, BOM): never shown.
|
|
20
|
+
.replace(/\p{Cf}/gu, "")
|
|
21
|
+
.replace(BIDI_CONTROLS, "");
|
|
22
|
+
}
|
|
23
|
+
/** Any text for the terminal UI: each line cleaned as above, its newlines kept (a lone carriage return is not one). */
|
|
24
|
+
export function terminalText(text) {
|
|
25
|
+
return text.split("\n").map(cleanTerminalLine).join("\n");
|
|
26
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
2
|
import { type UpdateWaiting } from "./desktop-update.js";
|
|
3
3
|
import { type SupervisorRunner } from "./daemon.js";
|
|
4
|
+
import { type ProcessProbe } from "./runner-process.js";
|
|
4
5
|
import { RUNTIME_PHASES, type RuntimeUpdateJournalRecord } from "./contracts/update-journal.js";
|
|
5
6
|
export declare const PROVENANCE_REPOSITORY = "https://github.com/ap9000/toolroll";
|
|
6
7
|
export declare const PROVENANCE_WORKFLOW = ".github/workflows/publish.yml";
|
|
@@ -226,13 +227,19 @@ type Exec = (command: string, args: string[], options?: {
|
|
|
226
227
|
stderr: string;
|
|
227
228
|
};
|
|
228
229
|
export declare function currentRuntime(version: string): RuntimeRef;
|
|
229
|
-
/** Seams for tests: npm, node and ps (`exec`), launchctl (`run`), process liveness, and launchd's waits
|
|
230
|
+
/** Seams for tests: npm, node and ps (`exec`), launchctl or systemctl (`run`), process liveness, and launchd's waits
|
|
231
|
+
* (`sleep`); for the laptop service on Linux, the platform, process identity (`probe`), the host name its process
|
|
232
|
+
* record names, and the console proof (`consoleProof`: the database's console is live at that version). */
|
|
230
233
|
export type MachineSeams = {
|
|
231
234
|
exec?: Exec;
|
|
232
235
|
run?: SupervisorRunner;
|
|
233
236
|
alive?: (pid: number) => boolean;
|
|
234
237
|
sleep?: (ms: number) => Promise<void>;
|
|
235
238
|
execPath?: string;
|
|
239
|
+
platform?: NodeJS.Platform;
|
|
240
|
+
probe?: ProcessProbe;
|
|
241
|
+
host?: string;
|
|
242
|
+
consoleProof?: (databaseFile: string, version: string) => Promise<boolean>;
|
|
236
243
|
};
|
|
237
244
|
export declare function machineSystem(home?: string, env?: Record<string, string | undefined>, seams?: MachineSeams): UpdateSystem;
|
|
238
245
|
/** npm's own Sigstore verifier. Found from npm itself (the `npm` on PATH, then the one beside node), never only from
|
package/dist/toolroll-update.js
CHANGED
|
@@ -26,7 +26,7 @@ import { inReadOnlyRequest } from "./read-only-request.js";
|
|
|
26
26
|
import { createHash, randomUUID, verify as signatureValid, X509Certificate } from "node:crypto";
|
|
27
27
|
import { spawnSync } from "node:child_process";
|
|
28
28
|
import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statfsSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
29
|
-
import { homedir, userInfo } from "node:os";
|
|
29
|
+
import { homedir, hostname, userInfo } from "node:os";
|
|
30
30
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
31
31
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
32
32
|
import { createRequire } from "node:module";
|
|
@@ -36,8 +36,10 @@ import { processMayBeAlive } from "./process-liveness.js";
|
|
|
36
36
|
import { removeTerminalUpdateGate } from "./terminal-catalog.js";
|
|
37
37
|
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
|
|
38
38
|
import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
|
|
39
|
+
import { LAPTOP_LABEL_DARWIN, LAPTOP_LABEL_LINUX, unitFlag } from "./laptop-service.js";
|
|
40
|
+
import { processGone, readRunnerProcessRecord, realProcessProbe, recordStandsFor, RUNNER_PROCESS_DIR } from "./runner-process.js";
|
|
39
41
|
import { NAME } from "./names.js";
|
|
40
|
-
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS } from "./store.js";
|
|
42
|
+
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS, V147_ADDED_COLUMNS } from "./store.js";
|
|
41
43
|
import { isNewer, REGISTRY } from "./releases.js";
|
|
42
44
|
import { markNeverIndex } from "./never-index.js";
|
|
43
45
|
import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
|
|
@@ -409,7 +411,9 @@ const HISTORY_RULES = {
|
|
|
409
411
|
// v126: a receipt's state, acceptance time and held item, a trigger's credential generation and turned-away count,
|
|
410
412
|
// and who made a card read none, 0, or none. A table with an earlier rule (flow_trigger's v115 move) keeps it too.
|
|
411
413
|
// v128: a lead message's pending flag reads waiting exactly for a team queue's unadmitted operator message.
|
|
412
|
-
|
|
414
|
+
// v147: an account's password flag reads set (1), a browser session reads full with no local binding or key, and the
|
|
415
|
+
// console record reads no key epoch.
|
|
416
|
+
...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS, V147_ADDED_COLUMNS))
|
|
413
417
|
.map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
|
|
414
418
|
};
|
|
415
419
|
/** The tables a migration may remove whole (v115's removed features). */
|
|
@@ -1541,7 +1545,7 @@ export function markWhatsNewSeen(stateDir) {
|
|
|
1541
1545
|
durableJson(join(j.stageDir, "update.json"), j);
|
|
1542
1546
|
}
|
|
1543
1547
|
const exec = (command, args, options = {}) => spawnSync(command, args, { encoding: "utf8", timeout: options.timeout ?? 300_000, maxBuffer: 16 * 1024 * 1024, ...(options.cwd ? { cwd: options.cwd } : {}), ...(options.input !== undefined ? { input: options.input } : {}) });
|
|
1544
|
-
const LABELS = ["com.toolroll.browser", "com.standing-orders.browser"];
|
|
1548
|
+
const LABELS = ["com.toolroll.browser", "com.standing-orders.browser", LAPTOP_LABEL_DARWIN];
|
|
1545
1549
|
export function currentRuntime(version) {
|
|
1546
1550
|
return { version, dist: dirname(fileURLToPath(import.meta.url)) };
|
|
1547
1551
|
}
|
|
@@ -1568,6 +1572,9 @@ require(input.verifier).verify(input.bundle, { certificateIssuer: input.issuer,
|
|
|
1568
1572
|
export function machineSystem(home = homedir(), env = process.env, seams = {}) {
|
|
1569
1573
|
const sh = seams.exec ?? exec;
|
|
1570
1574
|
const supervise = seams.run ?? (async (file, args, options) => (await import("./exec.js")).run(file, args, options));
|
|
1575
|
+
// Linux: the laptop service is a systemd user unit; nothing else here runs under systemd (watches use launchd's road).
|
|
1576
|
+
if ((seams.platform ?? process.platform) === "linux")
|
|
1577
|
+
return { ...machineSystem(home, env, { ...seams, platform: "darwin" }), ...systemdLaptopService(home, sh, supervise, seams) };
|
|
1571
1578
|
const unitFor = (from) => {
|
|
1572
1579
|
for (const label of LABELS) {
|
|
1573
1580
|
const unit = join(home, "Library", "LaunchAgents", `${label}.plist`);
|
|
@@ -1807,3 +1814,143 @@ export function retireUpdateJob(id, home = homedir()) {
|
|
|
1807
1814
|
}
|
|
1808
1815
|
catch { /* none */ }
|
|
1809
1816
|
}
|
|
1817
|
+
// ---- the laptop service under systemd (easy-start D) --------------------------------------------------------------
|
|
1818
|
+
/**
|
|
1819
|
+
* The parts of the update's machine that differ for toolroll-up.service, the laptop service on Linux: finding the
|
|
1820
|
+
* unit, which processes it runs, stopping it and proving its controller exited through the controller's own signed
|
|
1821
|
+
* process record (by start identity, never a pid alone), starting it again, and the health check (the unit active,
|
|
1822
|
+
* running the new runtime, and its console proved live at the new version). Every outside effect is `supervise`
|
|
1823
|
+
* (systemctl), `sh` (ps) or a seam, so a test scripts all of it.
|
|
1824
|
+
*/
|
|
1825
|
+
function systemdLaptopService(home, sh, supervise, seams) {
|
|
1826
|
+
const unitPath = join(home, ".config", "systemd", "user", `${LAPTOP_LABEL_LINUX}.service`);
|
|
1827
|
+
const name = `${LAPTOP_LABEL_LINUX}.service`;
|
|
1828
|
+
const probe = seams.probe ?? realProcessProbe;
|
|
1829
|
+
const host = seams.host ?? hostname();
|
|
1830
|
+
const mainPid = async () => {
|
|
1831
|
+
const shown = await supervise("systemctl", ["--user", "show", "--property=MainPID", name]);
|
|
1832
|
+
const pid = Number(/MainPID=(\d+)/.exec(shown.stdout)?.[1] ?? 0);
|
|
1833
|
+
return shown.code === 0 && pid > 1 ? pid : null;
|
|
1834
|
+
};
|
|
1835
|
+
const active = async () => (await supervise("systemctl", ["--user", "is-active", name])).stdout.trim() === "active";
|
|
1836
|
+
/** The processes the controller's signed record names, when that record is authentic for its registration and its
|
|
1837
|
+
* `up` is the unit's main process by start identity. Null: no such proof (the stop then waits on the pid alone). */
|
|
1838
|
+
const recorded = (unit, pid) => {
|
|
1839
|
+
const db = unitFlag(readFileSync(unit, "utf8"), "linux", "--db");
|
|
1840
|
+
if (db === null || !isAbsolute(db))
|
|
1841
|
+
return null;
|
|
1842
|
+
let names;
|
|
1843
|
+
const folder = join(dirname(db), RUNNER_PROCESS_DIR);
|
|
1844
|
+
try {
|
|
1845
|
+
names = readdirSync(folder).filter(one => one.endsWith(".json"));
|
|
1846
|
+
}
|
|
1847
|
+
catch {
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
let handle = null;
|
|
1851
|
+
try {
|
|
1852
|
+
handle = new (sqlite().DatabaseSync)(db, { readOnly: true });
|
|
1853
|
+
for (const file of names) {
|
|
1854
|
+
const record = readRunnerProcessRecord(join(folder, file));
|
|
1855
|
+
const up = record?.processes.find(one => one.role === "up" && one.pid === pid);
|
|
1856
|
+
if (record === null || up === undefined)
|
|
1857
|
+
continue;
|
|
1858
|
+
const row = handle.prepare("SELECT name, host, registered_at, credential_hash FROM runner WHERE name = ?").get(record.runner);
|
|
1859
|
+
if (row === undefined)
|
|
1860
|
+
continue;
|
|
1861
|
+
const holder = { runner: { name: String(row["name"]), host: String(row["host"]), registeredAt: String(row["registered_at"]) }, credentialHash: String(row["credential_hash"]) };
|
|
1862
|
+
if (recordStandsFor(record, holder, host) !== null || probe.start(pid) !== up.started)
|
|
1863
|
+
continue;
|
|
1864
|
+
return record.processes;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
catch {
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
finally {
|
|
1871
|
+
handle?.close();
|
|
1872
|
+
}
|
|
1873
|
+
return null;
|
|
1874
|
+
};
|
|
1875
|
+
return {
|
|
1876
|
+
serviceUnit: from => { try {
|
|
1877
|
+
return readFileSync(unitPath, "utf8").includes(from.dist) ? unitPath : null;
|
|
1878
|
+
}
|
|
1879
|
+
catch {
|
|
1880
|
+
return null;
|
|
1881
|
+
} },
|
|
1882
|
+
// `daemon install` watches keep launchd's road; on Linux the update switches only the laptop service.
|
|
1883
|
+
watchUnits: () => [],
|
|
1884
|
+
serviceLoaded: async () => active(),
|
|
1885
|
+
servicePids: async () => {
|
|
1886
|
+
const pid = await mainPid();
|
|
1887
|
+
if (pid === null)
|
|
1888
|
+
return [];
|
|
1889
|
+
const children = await supervise("pgrep", ["-P", String(pid)]);
|
|
1890
|
+
return [pid, ...(children.code === 0 ? children.stdout.trim().split(/\s+/).map(Number).filter(one => Number.isSafeInteger(one) && one > 1) : [])];
|
|
1891
|
+
},
|
|
1892
|
+
stopService: async (unit) => {
|
|
1893
|
+
const pid = await mainPid();
|
|
1894
|
+
const processes = pid === null ? null : recorded(unit, pid);
|
|
1895
|
+
const stopped = await supervise("systemctl", ["--user", "stop", name]);
|
|
1896
|
+
if (stopped.code !== 0)
|
|
1897
|
+
throw Error(`systemctl could not stop ${LAPTOP_LABEL_LINUX}: ${(stopped.stderr || `exit ${stopped.code}`).trim().split("\n")[0]}. Nothing was replaced.`);
|
|
1898
|
+
if (processes === null)
|
|
1899
|
+
return;
|
|
1900
|
+
// The controller drains and exits; every process its record names must be proved gone before anything is replaced.
|
|
1901
|
+
for (let waited = 0;; waited += 250) {
|
|
1902
|
+
const left = processes.filter(one => !processGone(one, probe));
|
|
1903
|
+
if (left.length === 0)
|
|
1904
|
+
return;
|
|
1905
|
+
if (waited >= UP_EXIT_MS)
|
|
1906
|
+
throw Error(`Toolroll's background service is still running (process ${left.map(one => one.pid).join(", ")}) after it was stopped. Nothing was replaced.`);
|
|
1907
|
+
await (seams.sleep ?? sleep)(250);
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
restartService: async () => {
|
|
1911
|
+
// The switch rewrote the unit on disk: systemd reads it again before starting it.
|
|
1912
|
+
const reload = await supervise("systemctl", ["--user", "daemon-reload"]);
|
|
1913
|
+
if (reload.code !== 0)
|
|
1914
|
+
throw Error(`systemctl daemon-reload failed: ${(reload.stderr || `exit ${reload.code}`).trim()}`);
|
|
1915
|
+
const started = await supervise("systemctl", ["--user", "start", name]);
|
|
1916
|
+
if (started.code !== 0)
|
|
1917
|
+
throw Error(`systemctl could not start ${LAPTOP_LABEL_LINUX}: ${(started.stderr || `exit ${started.code}`).trim()}`);
|
|
1918
|
+
},
|
|
1919
|
+
healthy: async (j) => {
|
|
1920
|
+
const version = sh(process.execPath, [join(j.to.dist, "bin.js"), "--version"], { timeout: 30_000 });
|
|
1921
|
+
if (version.status !== 0 || version.stdout.trim() !== j.to.version)
|
|
1922
|
+
return false;
|
|
1923
|
+
for (const link of j.switched?.links ?? []) {
|
|
1924
|
+
try {
|
|
1925
|
+
if (realpathSync(link.path) !== realpathSync(join(j.to.dist, "bin.js")))
|
|
1926
|
+
return false;
|
|
1927
|
+
}
|
|
1928
|
+
catch {
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
if (!j.switched?.unit?.path)
|
|
1933
|
+
return true;
|
|
1934
|
+
if (!(await active()))
|
|
1935
|
+
return false;
|
|
1936
|
+
const pid = await mainPid();
|
|
1937
|
+
if (pid === null || !sh("/bin/ps", ["-p", String(pid), "-o", "command="]).stdout.includes(j.to.dist))
|
|
1938
|
+
return false;
|
|
1939
|
+
return (seams.consoleProof ?? liveConsoleAt)(j.databaseFile, j.to.version);
|
|
1940
|
+
},
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
/** The database's console is proved live (socket and HTTP) and says it is `version`. */
|
|
1944
|
+
async function liveConsoleAt(databaseFile, version) {
|
|
1945
|
+
const { proveLiveConsole } = await import("./console-instance.js");
|
|
1946
|
+
const handle = new (sqlite().DatabaseSync)(databaseFile, { readOnly: true });
|
|
1947
|
+
try {
|
|
1948
|
+
return (await proveLiveConsole(databaseFile, { handle }, version)).ok;
|
|
1949
|
+
}
|
|
1950
|
+
catch {
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
finally {
|
|
1954
|
+
handle.close();
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the terminal UI (src/tui, bundled into dist/tui.js) reads and asks for, and nothing more. The CLI side
|
|
3
|
+
* (tui-source.ts) implements it with in-process reads of this installation's database and with every write sent
|
|
4
|
+
* through `runOperate` as the local owner (local-owner.ts runAsLocalOwner), so the screen holds no authority of its
|
|
5
|
+
* own: a write the local owner may not make is refused there, and the screen shows the refusal's words.
|
|
6
|
+
*
|
|
7
|
+
* Types only, with no imports: both the CLI build (tsc) and the terminal UI bundle (esbuild) read this file.
|
|
8
|
+
*/
|
|
9
|
+
/** One task in the list, as the board says it. */
|
|
10
|
+
export type TuiRow = {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly title: string;
|
|
13
|
+
/** The board's one status word (Needs you, Running, Ready, …). */
|
|
14
|
+
readonly status: string;
|
|
15
|
+
readonly detail: string;
|
|
16
|
+
readonly needsYou: boolean;
|
|
17
|
+
/** The attempt running now, if any: what Stop names. */
|
|
18
|
+
readonly liveRunId: number | null;
|
|
19
|
+
readonly repo: string | null;
|
|
20
|
+
};
|
|
21
|
+
/** A parked question on a task, answered by choosing one of its options. */
|
|
22
|
+
export type TuiDecision = {
|
|
23
|
+
readonly id: number;
|
|
24
|
+
readonly question: string;
|
|
25
|
+
readonly options: readonly {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly label: string;
|
|
28
|
+
}[];
|
|
29
|
+
};
|
|
30
|
+
/** One task in full. */
|
|
31
|
+
export type TuiTask = {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly title: string;
|
|
34
|
+
readonly status: string;
|
|
35
|
+
readonly detail: string;
|
|
36
|
+
/** The plan waiting for approval: its exact digest (what the yes binds) and every line of it. Null: none waits. */
|
|
37
|
+
readonly plan: {
|
|
38
|
+
readonly digest: string;
|
|
39
|
+
readonly lines: readonly string[];
|
|
40
|
+
} | null;
|
|
41
|
+
readonly liveRunId: number | null;
|
|
42
|
+
/** A finished result that can be marked Complete or sent back with a revision. */
|
|
43
|
+
readonly hasResult: boolean;
|
|
44
|
+
readonly decisions: readonly TuiDecision[];
|
|
45
|
+
};
|
|
46
|
+
/** The welcome's steps, as lines (welcome-facts.ts). */
|
|
47
|
+
export type TuiSetup = {
|
|
48
|
+
readonly left: number;
|
|
49
|
+
readonly lines: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
export type TuiSnapshot = {
|
|
52
|
+
readonly rows: readonly TuiRow[];
|
|
53
|
+
/** One line about what waits on the person, or null when nothing does. */
|
|
54
|
+
readonly needsYou: string | null;
|
|
55
|
+
/** The projects tasks can be filed in (the first is the default). */
|
|
56
|
+
readonly projects: readonly string[];
|
|
57
|
+
/** Whether the local owner has set a password (decides where a refusal leads). */
|
|
58
|
+
readonly passwordSet: boolean;
|
|
59
|
+
};
|
|
60
|
+
export type TuiAct = {
|
|
61
|
+
readonly kind: "new";
|
|
62
|
+
readonly text: string;
|
|
63
|
+
readonly repo: string | null;
|
|
64
|
+
} | {
|
|
65
|
+
readonly kind: "approve";
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly digest: string;
|
|
68
|
+
} | {
|
|
69
|
+
readonly kind: "stop";
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly runId: number;
|
|
72
|
+
} | {
|
|
73
|
+
readonly kind: "revise";
|
|
74
|
+
readonly id: string;
|
|
75
|
+
readonly text: string;
|
|
76
|
+
} | {
|
|
77
|
+
readonly kind: "complete";
|
|
78
|
+
readonly id: string;
|
|
79
|
+
} | {
|
|
80
|
+
readonly kind: "answer";
|
|
81
|
+
readonly decision: number;
|
|
82
|
+
readonly option: string;
|
|
83
|
+
};
|
|
84
|
+
/** A write's answer: its words. `needsPassword`: refused because the local owner may not do it. */
|
|
85
|
+
export type TuiActResult = {
|
|
86
|
+
readonly ok: true;
|
|
87
|
+
readonly message: string;
|
|
88
|
+
readonly id?: string;
|
|
89
|
+
} | {
|
|
90
|
+
readonly ok: false;
|
|
91
|
+
readonly message: string;
|
|
92
|
+
readonly needsPassword: boolean;
|
|
93
|
+
};
|
|
94
|
+
/** The plane, as the screen sees it. */
|
|
95
|
+
export interface TuiSource {
|
|
96
|
+
snapshot(): Promise<TuiSnapshot>;
|
|
97
|
+
task(id: string): Promise<TuiTask | null>;
|
|
98
|
+
/** The newest output lines of the task's live attempt (empty when none runs). */
|
|
99
|
+
live(id: string): readonly string[];
|
|
100
|
+
/** A counter that moves when the plane's work changes (the store's wake sequence). */
|
|
101
|
+
changed(): number;
|
|
102
|
+
act(act: TuiAct): Promise<TuiActResult>;
|
|
103
|
+
/** The welcome's steps, or null once setup is done. */
|
|
104
|
+
setup(): Promise<TuiSetup | null>;
|
|
105
|
+
}
|
|
106
|
+
/** What only the process that opened the screen knows: whether quitting stops its agents, the browser, the ask-once. */
|
|
107
|
+
export interface TuiSession {
|
|
108
|
+
/** Quitting would stop the Toolroll this terminal started (no background service runs it). */
|
|
109
|
+
owned(): boolean;
|
|
110
|
+
/** Open this computer's browser on the console, signed in. Resolves with what to say. */
|
|
111
|
+
openBrowser(): Promise<string>;
|
|
112
|
+
/** Whether to ask, now, "Keep Toolroll running in the background?" (once per installation). */
|
|
113
|
+
shouldAskBackground(): boolean;
|
|
114
|
+
/** Record the answer; yes moves Toolroll to the background service. Resolves with what to say. */
|
|
115
|
+
answerBackground(yes: boolean): Promise<string>;
|
|
116
|
+
}
|
|
117
|
+
/** How the screen ended: leave as it is, stop the agents this terminal started, or keep them running in the background. */
|
|
118
|
+
export type TuiExit = "leave" | "stop" | "keep";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the terminal UI (src/tui, bundled into dist/tui.js) reads and asks for, and nothing more. The CLI side
|
|
3
|
+
* (tui-source.ts) implements it with in-process reads of this installation's database and with every write sent
|
|
4
|
+
* through `runOperate` as the local owner (local-owner.ts runAsLocalOwner), so the screen holds no authority of its
|
|
5
|
+
* own: a write the local owner may not make is refused there, and the screen shows the refusal's words.
|
|
6
|
+
*
|
|
7
|
+
* Types only, with no imports: both the CLI build (tsc) and the terminal UI bundle (esbuild) read this file.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OperateOptions } from "./operate.js";
|
|
2
|
+
import type { ConnectionChecker } from "./provider-connection.js";
|
|
3
|
+
import type { TuiSource } from "./tui-contract.js";
|
|
4
|
+
export type TuiSourceOptions = {
|
|
5
|
+
databaseFile: string;
|
|
6
|
+
clock?: () => Date;
|
|
7
|
+
/** Passed to every runOperate (tests: a fixed clock, seams). */
|
|
8
|
+
operate?: OperateOptions;
|
|
9
|
+
/** The agents' sign-in check for the Setup screen (tests pass their own). */
|
|
10
|
+
check?: ConnectionChecker;
|
|
11
|
+
};
|
|
12
|
+
export declare function createTuiSource(options: TuiSourceOptions): TuiSource & {
|
|
13
|
+
close(): void;
|
|
14
|
+
};
|