toolroll 0.9.89 → 0.9.91
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 +63 -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 +32 -32
- 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-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/first-run.d.ts +77 -0
- package/dist/first-run.js +45 -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-knowledge.d.ts +7 -7
- package/dist/quick-start.d.ts +3 -1
- package/dist/quick-start.js +6 -1
- package/dist/release-adapters.d.ts +2 -0
- package/dist/release-adapters.js +30 -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 +129 -22
- 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 +10 -7
- 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 +12 -4
- 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/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/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/package.json +4 -2
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EASY START (easy-start plan A, D, E): bare `toolroll` at a terminal, and `toolroll open`.
|
|
3
|
+
*
|
|
4
|
+
* At a person's terminal (stdin and stdout a TTY, no --json, not under a coding agent), bare `toolroll` makes sure this
|
|
5
|
+
* computer's Toolroll is running and opens the terminal UI on it:
|
|
6
|
+
*
|
|
7
|
+
* - The console is proved live first (console-instance.ts, both channels). When it isn't running: the background
|
|
8
|
+
* service, if installed, is started through its supervisor; otherwise `up --laptop` starts as an ORDINARY child of
|
|
9
|
+
* this process, so quitting the screen ends it through `up`'s own parent-death drain (today's foreground semantics).
|
|
10
|
+
* - With local mode on, the terminal UI opens in this process as the local owner (runAsLocalOwner): reads in-process,
|
|
11
|
+
* writes through `runOperate`, refused wherever the local owner is. `--web` opens the browser instead, signed in
|
|
12
|
+
* through the console's socket `link` verb (or, with nothing running, runs `up --laptop` in the foreground, which
|
|
13
|
+
* opens it).
|
|
14
|
+
* - With local mode off (an install with passwords), nothing in the terminal writes: today's flow opens the browser,
|
|
15
|
+
* where the person signs in with their password.
|
|
16
|
+
*
|
|
17
|
+
* Every outside effect is a seam; `defaultEasyStartSeams` gives the real ones. Under an agent, without a terminal or
|
|
18
|
+
* with --json, cli.ts never comes here: those keep exactly the behaviour they had (quick-start.ts and the scan).
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
21
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
22
|
+
import { pathToFileURL } from "node:url";
|
|
23
|
+
import { requestLocalLink } from "./console-instance.js";
|
|
24
|
+
import { backgroundInstalled, backgroundOn, startInstalledService } from "./laptop-background.js";
|
|
25
|
+
import { proveConsoleReadOnly } from "./quick-start.js";
|
|
26
|
+
import { backgroundAsked, localModeOf, openStore, openStoreReadOnly, recordBackgroundAsked } from "./store.js";
|
|
27
|
+
export { easyStartApplies, easyStartArgs } from "./easy-start-args.js";
|
|
28
|
+
const EXIT = { ok: 0, failed: 1, refused: 3 };
|
|
29
|
+
/**
|
|
30
|
+
* Bare `toolroll` ("start") or `toolroll open` ("open") at a person's terminal. Resolves with the exit code once the
|
|
31
|
+
* screen (or the foreground `up`, or the browser hand-off) is done.
|
|
32
|
+
*/
|
|
33
|
+
export async function runEasyStart(mode, args, seams) {
|
|
34
|
+
const { write } = seams;
|
|
35
|
+
const rootArgs = args.projectRoot === null ? [] : ["--project-root", args.projectRoot];
|
|
36
|
+
const sleep = seams.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
37
|
+
const first = await seams.prove();
|
|
38
|
+
if (!first.ok && first.state === "refused") {
|
|
39
|
+
write(`Toolroll can't open: ${first.message}.`);
|
|
40
|
+
return EXIT.refused;
|
|
41
|
+
}
|
|
42
|
+
// Attaching to a console already running: this folder's repository joins it, as bare `toolroll` always did.
|
|
43
|
+
const joinRepo = async () => {
|
|
44
|
+
if (mode !== "start")
|
|
45
|
+
return EXIT.ok;
|
|
46
|
+
const found = await seams.findRepo(seams.cwd);
|
|
47
|
+
if (found === null)
|
|
48
|
+
return EXIT.ok;
|
|
49
|
+
let repo = found;
|
|
50
|
+
try {
|
|
51
|
+
repo = realpathSync(found);
|
|
52
|
+
}
|
|
53
|
+
catch { /* as found */ }
|
|
54
|
+
return (await seams.enrolled(repo)) ? EXIT.ok : seams.admit(repo);
|
|
55
|
+
};
|
|
56
|
+
// An install with passwords: today's flow, in the browser, where the person signs in. Nothing here writes.
|
|
57
|
+
if (seams.localMode() === "off") {
|
|
58
|
+
if (!first.ok)
|
|
59
|
+
return seams.runUpForeground(rootArgs);
|
|
60
|
+
const joined = await joinRepo();
|
|
61
|
+
if (joined !== EXIT.ok)
|
|
62
|
+
return joined;
|
|
63
|
+
write(`Toolroll is on ${first.url}`);
|
|
64
|
+
await seams.openBrowser(first.url);
|
|
65
|
+
return EXIT.ok;
|
|
66
|
+
}
|
|
67
|
+
// --web with nothing running and no service: `up --laptop` in this terminal opens the browser itself, signed in.
|
|
68
|
+
if (args.web && !first.ok && !seams.serviceInstalled())
|
|
69
|
+
return seams.runUpForeground(["--laptop", ...rootArgs]);
|
|
70
|
+
let owned = null;
|
|
71
|
+
const upArgs = ["--laptop", "--no-open", ...rootArgs];
|
|
72
|
+
/** Start our own `up` and wait until its console proves live (or it ends, or the wait runs out). */
|
|
73
|
+
const startOwned = async () => {
|
|
74
|
+
const child = seams.spawnUp(upArgs);
|
|
75
|
+
owned = child;
|
|
76
|
+
const deadline = Date.now() + (seams.startTimeoutMs ?? 90_000);
|
|
77
|
+
for (;;) {
|
|
78
|
+
const proof = await seams.prove();
|
|
79
|
+
if (proof.ok)
|
|
80
|
+
return proof;
|
|
81
|
+
if (!child.running()) {
|
|
82
|
+
const last = child.tail().filter(line => line.trim() !== "").slice(-3).join(" — ");
|
|
83
|
+
return { ok: false, state: "refused", message: last === "" ? "it stopped while starting" : last };
|
|
84
|
+
}
|
|
85
|
+
if (Date.now() >= deadline) {
|
|
86
|
+
child.stop();
|
|
87
|
+
return { ok: false, state: "refused", message: "it didn't answer in time" };
|
|
88
|
+
}
|
|
89
|
+
await sleep(250);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
let proof = first;
|
|
93
|
+
if (!proof.ok) {
|
|
94
|
+
write("Starting Toolroll…");
|
|
95
|
+
proof = seams.serviceInstalled() ? await seams.startService() : await startOwned();
|
|
96
|
+
if (!proof.ok) {
|
|
97
|
+
write(`Toolroll didn't start: ${proof.message}.`);
|
|
98
|
+
return EXIT.failed;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const joined = await joinRepo();
|
|
103
|
+
if (joined !== EXIT.ok)
|
|
104
|
+
return joined;
|
|
105
|
+
}
|
|
106
|
+
const url = proof.url;
|
|
107
|
+
/** The `up` this process owns, while it runs. */
|
|
108
|
+
const ownedNow = () => (owned !== null && owned.running() ? owned : null);
|
|
109
|
+
const waitOwned = async () => {
|
|
110
|
+
const child = ownedNow();
|
|
111
|
+
if (child === null)
|
|
112
|
+
return EXIT.ok;
|
|
113
|
+
write("Ctrl-C stops it.");
|
|
114
|
+
await child.exited;
|
|
115
|
+
return EXIT.ok;
|
|
116
|
+
};
|
|
117
|
+
// No local mode here (a new install on a computer with no sandbox): the browser, with the password `up` saved.
|
|
118
|
+
if (seams.localMode() !== "on") {
|
|
119
|
+
write(`Toolroll is on ${url}. Sign in with your password.`);
|
|
120
|
+
await seams.openBrowser(url);
|
|
121
|
+
return waitOwned();
|
|
122
|
+
}
|
|
123
|
+
if (args.web) {
|
|
124
|
+
const link = await seams.requestLink();
|
|
125
|
+
if (!link.ok) {
|
|
126
|
+
write(`Toolroll is on ${url}, but it couldn't sign you in: ${link.message}.`);
|
|
127
|
+
ownedNow()?.stop();
|
|
128
|
+
return EXIT.refused;
|
|
129
|
+
}
|
|
130
|
+
write(`Toolroll is on ${url}`);
|
|
131
|
+
await seams.openBrowser(link.url);
|
|
132
|
+
return waitOwned();
|
|
133
|
+
}
|
|
134
|
+
/** Move to the background service, handing over from our own `up` (background on stops it and proves it gone). */
|
|
135
|
+
const handoff = async () => {
|
|
136
|
+
const child = ownedNow();
|
|
137
|
+
const result = await seams.backgroundOn(child === null ? null : {
|
|
138
|
+
controllerPid: child.pid,
|
|
139
|
+
// The service didn't take over: our own `up` starts again, as before.
|
|
140
|
+
restart: async () => { const again = await startOwned(); if (!again.ok)
|
|
141
|
+
throw Error(again.message); },
|
|
142
|
+
});
|
|
143
|
+
if (result.ok)
|
|
144
|
+
owned = null;
|
|
145
|
+
return result.message;
|
|
146
|
+
};
|
|
147
|
+
const session = {
|
|
148
|
+
owned: () => ownedNow() !== null,
|
|
149
|
+
openBrowser: async () => {
|
|
150
|
+
const link = await seams.requestLink();
|
|
151
|
+
if (!link.ok)
|
|
152
|
+
return `The browser couldn't be opened: ${link.message}.`;
|
|
153
|
+
await seams.openBrowser(link.url);
|
|
154
|
+
return "Opened in your browser.";
|
|
155
|
+
},
|
|
156
|
+
shouldAskBackground: () => ownedNow() !== null && !seams.serviceInstalled() && !seams.backgroundAsked(),
|
|
157
|
+
answerBackground: async (yes) => {
|
|
158
|
+
seams.recordBackgroundAnswer(yes);
|
|
159
|
+
return yes ? handoff() : "Okay. To change your mind later, run `toolroll background on`.";
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
const exit = await seams.openTui(session);
|
|
163
|
+
const child = ownedNow();
|
|
164
|
+
if (child !== null) {
|
|
165
|
+
if (exit === "keep") {
|
|
166
|
+
const said = await handoff();
|
|
167
|
+
write(said);
|
|
168
|
+
if (ownedNow() === null)
|
|
169
|
+
return EXIT.ok;
|
|
170
|
+
}
|
|
171
|
+
write("Stopping Toolroll… (running work is saved)");
|
|
172
|
+
child.stop();
|
|
173
|
+
await child.exited;
|
|
174
|
+
write("Toolroll stopped.");
|
|
175
|
+
}
|
|
176
|
+
return EXIT.ok;
|
|
177
|
+
}
|
|
178
|
+
/** `up` as an ordinary child of this process: its output kept (last lines, for a failed start), never on the screen. */
|
|
179
|
+
export function spawnOwnedUp(args, options) {
|
|
180
|
+
const entry = options.entry ?? process.argv[1];
|
|
181
|
+
if (entry === undefined)
|
|
182
|
+
throw Error("can't find Toolroll's own command to start it");
|
|
183
|
+
const child = spawn(process.execPath, [...process.execArgv, entry, "up", ...args, "--db", options.databaseFile], {
|
|
184
|
+
cwd: options.cwd,
|
|
185
|
+
// An IPC channel: if this process ends without stopping it, `up` sees its parent go and drains (operate.ts).
|
|
186
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
187
|
+
});
|
|
188
|
+
const lines = [];
|
|
189
|
+
const keep = (chunk) => {
|
|
190
|
+
for (const line of String(chunk).split("\n"))
|
|
191
|
+
if (line !== "")
|
|
192
|
+
lines.push(line);
|
|
193
|
+
if (lines.length > 40)
|
|
194
|
+
lines.splice(0, lines.length - 40);
|
|
195
|
+
};
|
|
196
|
+
child.stdout?.on("data", keep);
|
|
197
|
+
child.stderr?.on("data", keep);
|
|
198
|
+
let running = child.pid !== undefined;
|
|
199
|
+
const exited = new Promise(resolve => {
|
|
200
|
+
child.once("exit", code => { running = false; resolve(code); });
|
|
201
|
+
child.once("error", error => { running = false; lines.push(error.message); resolve(null); });
|
|
202
|
+
});
|
|
203
|
+
return { pid: child.pid ?? -1, exited, running: () => running, stop: () => { if (running)
|
|
204
|
+
child.kill("SIGTERM"); }, tail: () => [...lines] };
|
|
205
|
+
}
|
|
206
|
+
/** The real seams, for this installation's database. */
|
|
207
|
+
export function defaultEasyStartSeams(options) {
|
|
208
|
+
const file = options.databaseFile;
|
|
209
|
+
const cwd = options.cwd ?? process.cwd();
|
|
210
|
+
return {
|
|
211
|
+
databaseFile: file,
|
|
212
|
+
cwd,
|
|
213
|
+
write: options.write,
|
|
214
|
+
prove: () => proveConsoleReadOnly(file, options.build),
|
|
215
|
+
localMode: () => localModeRead(file),
|
|
216
|
+
serviceInstalled: () => backgroundInstalled(),
|
|
217
|
+
startService: async () => {
|
|
218
|
+
const store = openStore(file);
|
|
219
|
+
try {
|
|
220
|
+
return await startInstalledService(store, file);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
store.close();
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
spawnUp: args => spawnOwnedUp(args, { cwd, databaseFile: file }),
|
|
227
|
+
runUpForeground: async (args) => (await import("./operate.js")).runOperate("up", [...args, "--db", file], options.write, options.operate ?? {}),
|
|
228
|
+
openBrowser: async (url) => { (await import("./operate.js")).openBrowser(url); },
|
|
229
|
+
requestLink: async () => {
|
|
230
|
+
const store = openStoreReadOnly(file);
|
|
231
|
+
if (store === null)
|
|
232
|
+
return { ok: false, message: "Toolroll's database can't be read" };
|
|
233
|
+
try {
|
|
234
|
+
return await requestLocalLink(file, store, options.build);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
store.close();
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
openTui: async (session) => {
|
|
241
|
+
const { createTuiSource } = await import("./tui-source.js");
|
|
242
|
+
const { runAsLocalOwner } = await import("./local-owner.js");
|
|
243
|
+
const source = createTuiSource({ databaseFile: file, ...(options.operate === undefined ? {} : { operate: options.operate }) });
|
|
244
|
+
try {
|
|
245
|
+
const tui = await loadTui();
|
|
246
|
+
return await runAsLocalOwner(() => tui.startTui(source, session));
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
source.close();
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
backgroundOn: async (foreground) => {
|
|
253
|
+
const store = openStore(file);
|
|
254
|
+
try {
|
|
255
|
+
// At this terminal, but the screen owns its input: another service on this database is refused, not asked.
|
|
256
|
+
const on = await backgroundOn(store, file, null, { terminal: false, ...(foreground === null ? {} : { foreground }) });
|
|
257
|
+
if (on.ok)
|
|
258
|
+
return { ok: true, message: `Toolroll now runs in the background at ${on.url}. It starts when you log in.` };
|
|
259
|
+
return { ok: false, message: `Toolroll couldn't move to the background: ${on.message}.${(on.rolledBack ?? []).map(line => ` ${line}.`).join("")}` };
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
store.close();
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
backgroundAsked: () => {
|
|
266
|
+
const store = openStoreReadOnly(file);
|
|
267
|
+
// Unreadable: never ask on the strength of a database this build can't read.
|
|
268
|
+
if (store === null)
|
|
269
|
+
return true;
|
|
270
|
+
try {
|
|
271
|
+
return backgroundAsked(store) !== null;
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
store.close();
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
recordBackgroundAnswer: yes => {
|
|
278
|
+
const store = openStore(file);
|
|
279
|
+
try {
|
|
280
|
+
recordBackgroundAsked(store, yes ? "yes" : "no", new Date());
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
store.close();
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
findRepo: async (dir) => {
|
|
287
|
+
const { run } = await import("./exec.js");
|
|
288
|
+
const top = await run("git", ["rev-parse", "--show-toplevel"], { cwd: dir, timeoutMs: 10_000 });
|
|
289
|
+
return top.code === 0 ? top.stdout.trim() : null;
|
|
290
|
+
},
|
|
291
|
+
enrolled: async (repo) => {
|
|
292
|
+
const { loadRepos } = await import("./repos.js");
|
|
293
|
+
const { registryBeside } = await import("./project-admission.js");
|
|
294
|
+
const loaded = await loadRepos(registryBeside(file));
|
|
295
|
+
return !("error" in loaded) && loaded.repos.includes(repo);
|
|
296
|
+
},
|
|
297
|
+
admit: options.admit,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** The terminal UI bundle (dist/tui.js, built from src/tui/entry.tsx), loaded only when the screen opens. */
|
|
301
|
+
async function loadTui() {
|
|
302
|
+
const bundled = new URL("./tui.js", import.meta.url);
|
|
303
|
+
if (existsSync(bundled))
|
|
304
|
+
return await import(bundled.href);
|
|
305
|
+
// From source (`npm run dev`): the entry itself.
|
|
306
|
+
return await import(pathToFileURL(new URL("./tui/entry.tsx", import.meta.url).pathname).href);
|
|
307
|
+
}
|
|
308
|
+
/** Local mode as saved, read only: "absent" with no database yet; an older database has none ("off"). */
|
|
309
|
+
export function localModeRead(file) {
|
|
310
|
+
if (file !== ":memory:" && !existsSync(file))
|
|
311
|
+
return "absent";
|
|
312
|
+
const store = openStoreReadOnly(file);
|
|
313
|
+
if (store === null)
|
|
314
|
+
return "off";
|
|
315
|
+
try {
|
|
316
|
+
return localModeOf(store)?.enabled === true ? "on" : "off";
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
store.close();
|
|
320
|
+
}
|
|
321
|
+
}
|
package/dist/first-run.d.ts
CHANGED
|
@@ -113,4 +113,81 @@ export declare function taskRoad(read: Parameters<typeof firstTaskJourney>[0], a
|
|
|
113
113
|
report?: boolean;
|
|
114
114
|
checks?: RoadChecks;
|
|
115
115
|
}): JourneyStep[];
|
|
116
|
+
/** The agents setup offers: the two whose sign-in check, readiness and chat are supported end to end. */
|
|
117
|
+
export type WelcomeAgentId = "claude" | "codex";
|
|
118
|
+
export declare const WELCOME_AGENTS: readonly {
|
|
119
|
+
id: WelcomeAgentId;
|
|
120
|
+
name: string;
|
|
121
|
+
signIn: string;
|
|
122
|
+
install: string;
|
|
123
|
+
}[];
|
|
124
|
+
/** The sample project's folder name under the project folder. */
|
|
125
|
+
export declare const SAMPLE_PROJECT_NAME = "toolroll-sample";
|
|
126
|
+
export type WelcomeAgentState = "signed-in" | "signed-out" | "not-installed" | "checking";
|
|
127
|
+
export type WelcomeAgent = {
|
|
128
|
+
id: WelcomeAgentId;
|
|
129
|
+
name: string;
|
|
130
|
+
state: WelcomeAgentState;
|
|
131
|
+
/** The exact command that signs it in on this computer (installing it first when it isn't there); null once signed in or while checking. */
|
|
132
|
+
command: string | null;
|
|
133
|
+
inUse: boolean;
|
|
134
|
+
/** "Use this one" can choose it: signed in with an account, not already in use, and this person may choose. */
|
|
135
|
+
canUse: boolean;
|
|
136
|
+
};
|
|
137
|
+
export type WelcomeStarter = {
|
|
138
|
+
label: string;
|
|
139
|
+
draft: string;
|
|
140
|
+
href: string;
|
|
141
|
+
};
|
|
142
|
+
export type WelcomeModel = {
|
|
143
|
+
agent: {
|
|
144
|
+
done: boolean;
|
|
145
|
+
checking: boolean;
|
|
146
|
+
agents: WelcomeAgent[];
|
|
147
|
+
inUse: WelcomeAgentId | null;
|
|
148
|
+
/** Tasks are built with another agent than the one in use (Settings → Models decides): say so, with where to change it. */
|
|
149
|
+
buildsWith: string | null;
|
|
150
|
+
};
|
|
151
|
+
project: {
|
|
152
|
+
done: boolean;
|
|
153
|
+
projects: {
|
|
154
|
+
name: string;
|
|
155
|
+
path: string;
|
|
156
|
+
}[];
|
|
157
|
+
/** Where "Try a sample project" puts it; null when there is no project folder or it is already added. */
|
|
158
|
+
sample: string | null;
|
|
159
|
+
/** A project is added but nothing on this computer takes its tasks yet. */
|
|
160
|
+
notTakingWork: boolean;
|
|
161
|
+
};
|
|
162
|
+
task: {
|
|
163
|
+
done: boolean;
|
|
164
|
+
open: boolean;
|
|
165
|
+
starters: WelcomeStarter[];
|
|
166
|
+
};
|
|
167
|
+
/** The step the screen's one primary belongs to, or null when nothing is left that a page can do. */
|
|
168
|
+
next: "agent" | "project" | "task" | null;
|
|
169
|
+
left: number;
|
|
170
|
+
};
|
|
171
|
+
export type WelcomeFacts = {
|
|
172
|
+
/** Each agent's last sign-in check on this computer; null until one has answered. */
|
|
173
|
+
agents: Record<WelcomeAgentId, SignInState | null>;
|
|
174
|
+
inUse: WelcomeAgentId | null;
|
|
175
|
+
/** The provider routine builds run on now (claude by default). */
|
|
176
|
+
buildsOn: string | null;
|
|
177
|
+
canChoose: boolean;
|
|
178
|
+
projects: readonly {
|
|
179
|
+
name: string;
|
|
180
|
+
path: string;
|
|
181
|
+
}[];
|
|
182
|
+
/** The folder new projects go in; null when this console has none. */
|
|
183
|
+
projectRoot: string | null;
|
|
184
|
+
ready: boolean;
|
|
185
|
+
hasTask: boolean;
|
|
186
|
+
starters: readonly FirstTaskSuggestion[];
|
|
187
|
+
platform: NodeJS.Platform;
|
|
188
|
+
};
|
|
189
|
+
/** The welcome's three steps (Agent · Project · First task) from live facts, the same for every screen that shows them. */
|
|
190
|
+
export declare function welcomeModel(facts: WelcomeFacts): WelcomeModel;
|
|
191
|
+
/** First tasks for the sample project: the changes its README suggests. */
|
|
192
|
+
export declare const SAMPLE_FIRST_TASKS: readonly FirstTaskSuggestion[];
|
|
116
193
|
export {};
|
package/dist/first-run.js
CHANGED
|
@@ -225,3 +225,48 @@ export function taskRoad(read, approved, built, facts) {
|
|
|
225
225
|
const waiting = read.stage === "needs-you";
|
|
226
226
|
return steps.map(step => waiting && (step.state === "current" || step.state === "stuck") ? { ...step, needsYou: true } : step);
|
|
227
227
|
}
|
|
228
|
+
export const WELCOME_AGENTS = [
|
|
229
|
+
{ id: "claude", name: "Claude Code", signIn: SIGN_IN_COMMAND, install: "npm install -g @anthropic-ai/claude-code" },
|
|
230
|
+
{ id: "codex", name: "Codex", signIn: "codex login", install: "npm install -g @openai/codex" },
|
|
231
|
+
];
|
|
232
|
+
/** The sample project's folder name under the project folder. */
|
|
233
|
+
export const SAMPLE_PROJECT_NAME = "toolroll-sample";
|
|
234
|
+
const SIGNED_IN = new Set(["connected", "key-works", "key-present"]);
|
|
235
|
+
/** The welcome's three steps (Agent · Project · First task) from live facts, the same for every screen that shows them. */
|
|
236
|
+
export function welcomeModel(facts) {
|
|
237
|
+
const join = facts.platform === "win32" ? ";" : " &&";
|
|
238
|
+
const agents = WELCOME_AGENTS.map((one) => {
|
|
239
|
+
const seen = facts.agents[one.id];
|
|
240
|
+
const state = seen === null ? "checking" : SIGNED_IN.has(seen) ? "signed-in" : seen === "not-installed" ? "not-installed" : "signed-out";
|
|
241
|
+
const command = state === "signed-in" || state === "checking" ? null : state === "not-installed" ? `${one.install}${join} ${one.signIn}` : one.signIn;
|
|
242
|
+
const inUse = facts.inUse === one.id;
|
|
243
|
+
return { id: one.id, name: one.name, state, command, inUse, canUse: facts.canChoose && !inUse && seen === "connected" };
|
|
244
|
+
});
|
|
245
|
+
const signedIn = agents.some(one => one.state === "signed-in");
|
|
246
|
+
const checking = !signedIn && agents.some(one => one.state === "checking");
|
|
247
|
+
const builds = facts.buildsOn === null ? null : WELCOME_AGENTS.find(one => one.id === facts.buildsOn)?.name ?? facts.buildsOn;
|
|
248
|
+
const buildsWith = facts.inUse !== null && facts.buildsOn !== null && facts.buildsOn !== facts.inUse ? builds : null;
|
|
249
|
+
const hasProject = facts.projects.length > 0;
|
|
250
|
+
const samplePath = facts.projectRoot === null ? null : `${facts.projectRoot.replace(/\/+$/, "")}/${SAMPLE_PROJECT_NAME}`;
|
|
251
|
+
const sample = samplePath !== null && !facts.projects.some(one => one.path === samplePath) ? samplePath : null;
|
|
252
|
+
const projectDone = hasProject && facts.ready;
|
|
253
|
+
const taskOpen = hasProject;
|
|
254
|
+
const starters = facts.starters.slice(0, 3).map(one => ({ label: one.label, draft: one.draft, href: `/tasks/new?text=${encodeURIComponent(one.draft)}` }));
|
|
255
|
+
const model = {
|
|
256
|
+
agent: { done: signedIn, checking, agents, inUse: facts.inUse, buildsWith },
|
|
257
|
+
project: { done: projectDone, projects: [...facts.projects], sample, notTakingWork: hasProject && !facts.ready },
|
|
258
|
+
task: { done: facts.hasTask, open: taskOpen, starters: taskOpen ? starters : [] },
|
|
259
|
+
};
|
|
260
|
+
// The next step a page can move: the agent only when there is something to click (Use this one); its sign-in is a
|
|
261
|
+
// command on the computer, so a project or the first task leads meanwhile.
|
|
262
|
+
const agentAct = !signedIn ? false : facts.inUse === null && agents.some(one => one.canUse);
|
|
263
|
+
const next = agentAct ? "agent" : !projectDone ? "project" : !facts.hasTask ? "task" : null;
|
|
264
|
+
const left = [model.agent.done, model.project.done, model.task.done].filter(done => !done).length;
|
|
265
|
+
return { ...model, next, left };
|
|
266
|
+
}
|
|
267
|
+
/** First tasks for the sample project: the changes its README suggests. */
|
|
268
|
+
export const SAMPLE_FIRST_TASKS = [
|
|
269
|
+
{ source: "generic", label: "Round prices to whole cents", draft: "Round priceWithDiscount's result to whole cents and add a test for it." },
|
|
270
|
+
{ source: "generic", label: "Refuse a negative discount", draft: "Make priceWithDiscount refuse a negative discount with a clear error, and test it." },
|
|
271
|
+
{ source: "generic", label: "Add a usage example to the README", draft: "Add a short usage example for priceWithDiscount to the README." },
|
|
272
|
+
];
|
package/dist/guides.js
CHANGED
|
@@ -288,7 +288,7 @@ ${AUTHORITY_LINE}
|
|
|
288
288
|
- One command, inside the repository: \`npx toolroll up\` (or
|
|
289
289
|
\`bunx toolroll up\`; Node 22.13+ must be installed — Bun's own
|
|
290
290
|
runtime cannot host it). \`up\` mints the first login and saves it beside the
|
|
291
|
-
database
|
|
291
|
+
database in \`.toolroll-secrets/up-login.txt\` (every start prints that path and the
|
|
292
292
|
\`cat\` command that shows the password, never the password itself),
|
|
293
293
|
registers the machine as a worker, runs the console, opens the browser. Later starts ask for the
|
|
294
294
|
password at most once and remember it on that machine.
|
package/dist/invoke.js
CHANGED
|
@@ -38,7 +38,8 @@ import { homedir, tmpdir } from "node:os";
|
|
|
38
38
|
import { dirname, join } from "node:path";
|
|
39
39
|
import { poolRootOf } from "./worktree.js";
|
|
40
40
|
import { checkLaunchRoot, signInGrants, signInIsItself } from "./agent-fence.js";
|
|
41
|
-
import {
|
|
41
|
+
import { localModeOn } from "./local-owner.js";
|
|
42
|
+
import { CHECK_ACCESS_DEFAULT, checkAccessHere, localModeCap, narrowestAccess, narrowestNetwork, signInsAvailable } from "./check-access.js";
|
|
42
43
|
import { readDenyPath } from "./project-sandbox.js";
|
|
43
44
|
import { sandboxRuntimeWords } from "./sandbox-runtime.js";
|
|
44
45
|
import { canonicalJson } from "./canonical-json.js";
|
|
@@ -999,8 +1000,9 @@ export function fencedCommands(store, runId, writes, inner, available = () => ma
|
|
|
999
1000
|
try {
|
|
1000
1001
|
const temp = join(own, "tmp");
|
|
1001
1002
|
mkdirSync(temp, { mode: 0o700 });
|
|
1002
|
-
// Not sandboxed (check-access.ts): the project's checks are trusted by the whole instance, as before 0.9.66.
|
|
1003
|
-
|
|
1003
|
+
// Not sandboxed (check-access.ts): the project's checks are trusted by the whole instance, as before 0.9.66. Never
|
|
1004
|
+
// while local mode is on (easy-start C5): then they run in the fence, sandboxed.
|
|
1005
|
+
if (access === "unsandboxed" && !localModeOn(store))
|
|
1004
1006
|
return await inner(file, args, { ...options, env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp } });
|
|
1005
1007
|
const fenced = runFence(store, runId, "command", undefined, own, writes, sandbox);
|
|
1006
1008
|
if (fenced.problem !== undefined)
|
|
@@ -1060,8 +1062,9 @@ export function fencedCheckoutCommands(store, repo, checkout, inner, available =
|
|
|
1060
1062
|
try {
|
|
1061
1063
|
const temp = join(own, "tmp");
|
|
1062
1064
|
mkdirSync(temp, { mode: 0o700 });
|
|
1063
|
-
// Not sandboxed (check-access.ts): the project's checks are trusted by the whole instance, as before 0.9.66.
|
|
1064
|
-
|
|
1065
|
+
// Not sandboxed (check-access.ts): the project's checks are trusted by the whole instance, as before 0.9.66. Never
|
|
1066
|
+
// while local mode is on (easy-start C5): then they run in the fence, sandboxed.
|
|
1067
|
+
if (access === "unsandboxed" && !localModeOn(store))
|
|
1065
1068
|
return await inner(file, args, { ...options, env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp } });
|
|
1066
1069
|
let binding;
|
|
1067
1070
|
try {
|
|
@@ -1259,7 +1262,9 @@ export function actualCheckAccess(store, access) {
|
|
|
1259
1262
|
*/
|
|
1260
1263
|
export function checkoutLaunchOf(store, repo, runIds, platform = process.platform) {
|
|
1261
1264
|
// As checks can get it: sign-ins (off for now; a stamp or setting from before, or another database) run sandboxed.
|
|
1262
|
-
const
|
|
1265
|
+
const asked = checkAccessHere(narrowestAccess([store.projectCheckAccess(repo), ...runIds.map(id => store.runCheckAccess(id))]), platform);
|
|
1266
|
+
// Easy-start C5: while local mode is on, never unsandboxed (re-read here, at launch).
|
|
1267
|
+
const access = localModeOn(store) ? localModeCap(asked) : asked;
|
|
1263
1268
|
if (access !== "sign-ins")
|
|
1264
1269
|
return { access, network: undefined };
|
|
1265
1270
|
const sealed = runIds.map(id => store.runCheckNetwork(id));
|