toolroll 0.7.0 → 0.8.0
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 +413 -0
- package/README.md +15 -0
- package/dist/agent-onboard.d.ts +106 -0
- package/dist/agent-onboard.js +282 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +23 -23
- package/dist/browser-shell.js +5 -3
- package/dist/browser-workspace.d.ts +11 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +8 -1
- package/dist/control-setup.d.ts +4 -4
- package/dist/daemon.d.ts +4 -0
- package/dist/daemon.js +28 -4
- package/dist/desktop-host.js +2 -2
- package/dist/desktop-update.d.ts +8 -2
- package/dist/desktop-update.js +39 -22
- package/dist/evidence.d.ts +24 -0
- package/dist/evidence.js +103 -5
- package/dist/first-run.d.ts +54 -0
- package/dist/first-run.js +101 -0
- package/dist/install-method.d.ts +1 -1
- package/dist/install-method.js +4 -0
- package/dist/operate.d.ts +9 -0
- package/dist/operate.js +68 -4
- package/dist/releases.d.ts +9 -0
- package/dist/releases.js +14 -1
- package/dist/serve.d.ts +16 -0
- package/dist/serve.js +151 -82
- package/dist/skills.d.ts +2 -0
- package/dist/skills.js +3 -1
- package/dist/store.d.ts +3 -3
- package/dist/store.js +9 -4
- package/dist/surface.js +2 -0
- package/dist/toolroll-update-cli.d.ts +15 -0
- package/dist/toolroll-update-cli.js +155 -0
- package/dist/toolroll-update-ui.d.ts +38 -0
- package/dist/toolroll-update-ui.js +89 -0
- package/dist/toolroll-update.d.ts +235 -0
- package/dist/toolroll-update.js +929 -0
- package/package.json +2 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Runner } from "./backend.js";
|
|
2
|
+
export declare const SIGN_IN_COMMAND = "claude auth login";
|
|
3
|
+
export declare const SANDBOX_COMMAND = "npx toolroll demo";
|
|
4
|
+
export type FirstRunAction = {
|
|
5
|
+
kind: "link";
|
|
6
|
+
label: string;
|
|
7
|
+
href: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "command";
|
|
10
|
+
command: string;
|
|
11
|
+
};
|
|
12
|
+
export type FirstRunStep = {
|
|
13
|
+
key: "agent" | "project" | "task";
|
|
14
|
+
title: string;
|
|
15
|
+
done: boolean;
|
|
16
|
+
action: FirstRunAction | null;
|
|
17
|
+
};
|
|
18
|
+
export type FirstRunFacts = {
|
|
19
|
+
agentSignedIn: boolean;
|
|
20
|
+
projects: number;
|
|
21
|
+
hasTask: boolean;
|
|
22
|
+
firstResultAt: string | null;
|
|
23
|
+
};
|
|
24
|
+
/** The three steps, each done or with the one action that does it; null once the first result has arrived. */
|
|
25
|
+
export declare function firstRunSteps(facts: FirstRunFacts): FirstRunStep[] | null;
|
|
26
|
+
export type OpenIssue = {
|
|
27
|
+
number: number;
|
|
28
|
+
title: string;
|
|
29
|
+
};
|
|
30
|
+
export type TodoComment = {
|
|
31
|
+
file: string;
|
|
32
|
+
line: number;
|
|
33
|
+
text: string;
|
|
34
|
+
};
|
|
35
|
+
export type FirstTaskSuggestion = {
|
|
36
|
+
source: "issue" | "todo" | "generic";
|
|
37
|
+
label: string;
|
|
38
|
+
draft: string;
|
|
39
|
+
};
|
|
40
|
+
export declare const GENERIC_FIRST_TASKS: readonly FirstTaskSuggestion[];
|
|
41
|
+
/** Issue titles and TODO text are written by anyone: keep only what a person can see, so the draft they send is the
|
|
42
|
+
* draft the lead reads (no control, format, zero-width, bidi, tag or variation-selector characters). */
|
|
43
|
+
export declare const visibleText: (text: string) => string;
|
|
44
|
+
/** Three first tasks: open GitHub issues first, then TODO or FIXME comments, then safe generic ones. */
|
|
45
|
+
export declare function firstTaskSuggestions(found: {
|
|
46
|
+
issues: readonly OpenIssue[];
|
|
47
|
+
todos: readonly TodoComment[];
|
|
48
|
+
}): FirstTaskSuggestion[];
|
|
49
|
+
/** `git grep` lines ("file:line:text") to TODO or FIXME notes, one per file. */
|
|
50
|
+
export declare function parseTodoLines(stdout: string, limit?: number): TodoComment[];
|
|
51
|
+
/** Read the project's own sources for suggestions: its open issues through the `gh` login, then its TODO comments. Failures fall through quietly. */
|
|
52
|
+
export declare function findFirstTasks(repo: string, runner?: Runner): Promise<FirstTaskSuggestion[]>;
|
|
53
|
+
/** "First result in 7 min": how long after the installation began its first Ready result arrived. */
|
|
54
|
+
export declare function firstResultWords(since: string, at: string): string | null;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first run: three plain steps to a first result, and three first tasks
|
|
3
|
+
* to try. Everything here is derived from live facts on each render; the
|
|
4
|
+
* list retires permanently with the first Ready result (the
|
|
5
|
+
* 'first-success-at' installation fact). A suggestion only drafts words in
|
|
6
|
+
* the composer: nothing is filed until the person sends it and confirms.
|
|
7
|
+
*/
|
|
8
|
+
import { run } from "./exec.js";
|
|
9
|
+
import { githubIssues } from "./issues.js";
|
|
10
|
+
export const SIGN_IN_COMMAND = "claude auth login";
|
|
11
|
+
export const SANDBOX_COMMAND = "npx toolroll demo";
|
|
12
|
+
/** The three steps, each done or with the one action that does it; null once the first result has arrived. */
|
|
13
|
+
export function firstRunSteps(facts) {
|
|
14
|
+
if (facts.firstResultAt !== null)
|
|
15
|
+
return null;
|
|
16
|
+
const project = facts.projects > 0;
|
|
17
|
+
return [
|
|
18
|
+
{ key: "agent", title: "Agent signed in", done: facts.agentSignedIn, action: facts.agentSignedIn ? null : { kind: "command", command: SIGN_IN_COMMAND } },
|
|
19
|
+
{ key: "project", title: "Project added", done: project, action: project ? null : { kind: "link", label: "Add a project", href: "/projects" } },
|
|
20
|
+
{ key: "task", title: "Your first task", done: facts.hasTask, action: facts.hasTask ? null : { kind: "link", label: "New task", href: "/tasks/new" } },
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
export const GENERIC_FIRST_TASKS = [
|
|
24
|
+
{ source: "generic", label: "Add a test for an untested function", draft: "Find one function without a test and add a focused test for it." },
|
|
25
|
+
{ source: "generic", label: "Fix a lint warning", draft: "Fix one lint warning without changing behaviour." },
|
|
26
|
+
{ source: "generic", label: "Improve the README's setup section", draft: "Improve the setup section of the README so a newcomer can install and run the project." },
|
|
27
|
+
];
|
|
28
|
+
const clip = (text, max) => (text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text);
|
|
29
|
+
/** Issue titles and TODO text are written by anyone: keep only what a person can see, so the draft they send is the
|
|
30
|
+
* draft the lead reads (no control, format, zero-width, bidi, tag or variation-selector characters). */
|
|
31
|
+
export const visibleText = (text) => text.replace(/[\p{Cf}\u{FE00}-\u{FE0F}\u{E0100}-\u{E01EF}]/gu, "").replace(/[\p{Cc}\s]+/gu, " ").trim();
|
|
32
|
+
/** Three first tasks: open GitHub issues first, then TODO or FIXME comments, then safe generic ones. */
|
|
33
|
+
export function firstTaskSuggestions(found) {
|
|
34
|
+
const fromIssues = found.issues.map(({ number, title: raw }) => {
|
|
35
|
+
const title = visibleText(raw);
|
|
36
|
+
return { source: "issue", label: clip(`#${number} ${title}`, 80), draft: `Fix GitHub issue #${number}: ${title}` };
|
|
37
|
+
});
|
|
38
|
+
const fromTodos = found.todos.map(({ file: rawFile, line, text: rawText }) => ({ file: visibleText(rawFile), line, text: visibleText(rawText) })).map((todo) => ({
|
|
39
|
+
source: "todo", label: clip(todo.text === "" ? `The note in ${todo.file}` : todo.text.charAt(0).toUpperCase() + todo.text.slice(1), 80),
|
|
40
|
+
draft: `Resolve the note in ${todo.file} line ${todo.line}${todo.text === "" ? "" : `: ${todo.text}`}`,
|
|
41
|
+
}));
|
|
42
|
+
return [...fromIssues, ...fromTodos, ...GENERIC_FIRST_TASKS].slice(0, 3);
|
|
43
|
+
}
|
|
44
|
+
const SKIPPED = /(^|\/)(node_modules|dist|build|vendor|third_party|\.git)\//;
|
|
45
|
+
/** `git grep` lines ("file:line:text") to TODO or FIXME notes, one per file. */
|
|
46
|
+
export function parseTodoLines(stdout, limit = 3) {
|
|
47
|
+
const found = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const line of stdout.split("\n")) {
|
|
50
|
+
const match = /^(.+?):(\d+):(.*)$/.exec(line);
|
|
51
|
+
if (match === null || SKIPPED.test(match[1]) || seen.has(match[1]))
|
|
52
|
+
continue;
|
|
53
|
+
const note = /\b(?:TODO|FIXME)\b(?:\([^)]*\))?[:\s-]*(.*)$/.exec(match[3]);
|
|
54
|
+
if (note === null)
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(match[1]);
|
|
57
|
+
found.push({ file: match[1], line: Number(match[2]), text: clip(note[1].replace(/\s*(\*\/|-->)\s*$/, "").trim(), 120) });
|
|
58
|
+
if (found.length === limit)
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
/** Read the project's own sources for suggestions: its open issues through the `gh` login, then its TODO comments. Failures fall through quietly. */
|
|
64
|
+
export async function findFirstTasks(repo, runner = run) {
|
|
65
|
+
let issues = [];
|
|
66
|
+
try {
|
|
67
|
+
const listed = await githubIssues({ repo, runner, limit: 3, timeoutMs: 5_000 }).listReady();
|
|
68
|
+
if (listed.ok)
|
|
69
|
+
issues = listed.value.slice(0, 3).map(one => ({ number: Number(one.id), title: one.title })).filter(one => Number.isSafeInteger(one.number));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
issues = [];
|
|
73
|
+
}
|
|
74
|
+
let todos = [];
|
|
75
|
+
if (issues.length < 3) {
|
|
76
|
+
try {
|
|
77
|
+
const result = await runner("git", ["grep", "-n", "-I", "-w", "--max-count=1", "-E", "TODO|FIXME"], { cwd: repo, timeoutMs: 5_000 });
|
|
78
|
+
if (result.code === 0 || result.stdout !== "")
|
|
79
|
+
todos = parseTodoLines(result.stdout);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
todos = [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return firstTaskSuggestions({ issues, todos });
|
|
86
|
+
}
|
|
87
|
+
/** "First result in 7 min": how long after the installation began its first Ready result arrived. */
|
|
88
|
+
export function firstResultWords(since, at) {
|
|
89
|
+
const minutes = Math.round((Date.parse(at) - Date.parse(since)) / 60_000);
|
|
90
|
+
if (!Number.isFinite(minutes) || minutes < 0)
|
|
91
|
+
return null;
|
|
92
|
+
if (minutes < 1)
|
|
93
|
+
return "First result in under a minute";
|
|
94
|
+
if (minutes < 60)
|
|
95
|
+
return `First result in ${minutes} min`;
|
|
96
|
+
if (minutes < 48 * 60) {
|
|
97
|
+
const hours = Math.floor(minutes / 60), rest = minutes % 60;
|
|
98
|
+
return `First result in ${hours} h${rest === 0 ? "" : ` ${rest} min`}`;
|
|
99
|
+
}
|
|
100
|
+
return `First result in ${Math.round(minutes / (24 * 60))} days`;
|
|
101
|
+
}
|
package/dist/install-method.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the one command that updates it that way. Only the path is read: nothing is
|
|
4
4
|
* run and nothing leaves the computer.
|
|
5
5
|
*/
|
|
6
|
-
export type InstallKind = "npm" | "pnpm" | "bun" | "yarn" | "homebrew" | "npx" | "source";
|
|
6
|
+
export type InstallKind = "npm" | "pnpm" | "bun" | "yarn" | "homebrew" | "npx" | "source" | "managed";
|
|
7
7
|
export type InstallMethod = {
|
|
8
8
|
kind: InstallKind;
|
|
9
9
|
updateCommand: string;
|
package/dist/install-method.js
CHANGED
|
@@ -13,6 +13,8 @@ export const UPDATE_COMMANDS = {
|
|
|
13
13
|
homebrew: "brew upgrade ap9000/toolroll/toolroll",
|
|
14
14
|
npx: "npx toolroll@latest",
|
|
15
15
|
source: "git pull && npm install && npm run build",
|
|
16
|
+
// A runtime `toolroll update` installed (staged-upgrades/release-* or rollback-*): it updates itself.
|
|
17
|
+
managed: "toolroll update",
|
|
16
18
|
};
|
|
17
19
|
/** The install method of the bin at `bin` (the running one by default). Unknown shapes read as npm, the common case. */
|
|
18
20
|
export function installMethod(bin = process.argv[1], probe = {}) {
|
|
@@ -34,6 +36,8 @@ function installKind(bin, probe) {
|
|
|
34
36
|
// by deploying from source, never from npm.
|
|
35
37
|
if (path.includes("/staged-upgrades/browser-"))
|
|
36
38
|
return "source";
|
|
39
|
+
if (/\/staged-upgrades\/(release|rollback)-/.test(path))
|
|
40
|
+
return "managed";
|
|
37
41
|
if (/\/Cellar\/toolroll\//i.test(path))
|
|
38
42
|
return "homebrew";
|
|
39
43
|
if (path.includes("/_npx/"))
|
package/dist/operate.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OnboardIo } from "./agent-onboard.js";
|
|
1
2
|
import { type TriggerIo } from "./flow-triggers.js";
|
|
2
3
|
import { type StepIo } from "./flow-steps.js";
|
|
3
4
|
import { type Store } from "./store.js";
|
|
@@ -66,7 +67,15 @@ export type OperateOptions = {
|
|
|
66
67
|
heldCoordinator?: import("./held.js").HeldSessionCoordinator;
|
|
67
68
|
/** Test seam for the short indexed wait loop. */
|
|
68
69
|
waitSleep?: (milliseconds: number) => Promise<void>;
|
|
70
|
+
/** Injected by tests: `onboard`'s home folder, terminal and probes. */
|
|
71
|
+
onboardSeams?: OnboardSeams;
|
|
72
|
+
/** Injected by tests: whether `up` has a person at a terminal, and how it opens a browser. */
|
|
73
|
+
upSeams?: {
|
|
74
|
+
terminal?: boolean;
|
|
75
|
+
openBrowser?: (url: string) => void;
|
|
76
|
+
};
|
|
69
77
|
};
|
|
78
|
+
export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection">>;
|
|
70
79
|
export declare const OPERATE_HELP: string;
|
|
71
80
|
/** Parsed flags, with the positionals left over. */
|
|
72
81
|
type Args = {
|
package/dist/operate.js
CHANGED
|
@@ -12,6 +12,7 @@ import { buildExport, exportSummary, exportZip, writeExportFolder } from "./expo
|
|
|
12
12
|
import { startBudgetAlerts } from "./budget-alerts.js";
|
|
13
13
|
import { liftAuthPause, openAuthPauses, signInGate, signInWords, startSignInProbes } from "./provider-auth.js";
|
|
14
14
|
import { createConnectionChecker } from "./provider-connection.js";
|
|
15
|
+
import { buildHandoff, handoffLines, loginAccount, runOnboard } from "./agent-onboard.js";
|
|
15
16
|
import { backupFiles, backupFolderOf, backupOwner, backupNow, restoreDatabase, startBackups } from "./backup.js";
|
|
16
17
|
import { pushLimitSink } from "./provider-limits.js";
|
|
17
18
|
import { limitsView } from "./limits-ui.js";
|
|
@@ -565,13 +566,13 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
565
566
|
"command", "timeout-seconds", "setup-digest", "stop-grace", "title", "name", "every", "lines",
|
|
566
567
|
"label", "reviewers", "limit", "role", "key-file", "weekly-usd", "daily-turns", "per-hour", "token-file", "race", "compare", "race-per-usd", "race-total-usd", "race-count", "race-agents", "budget-usd", "build-usd", "sync-max-age", "merge-method",
|
|
567
568
|
"phase", "risk", "tier", "clear-phase",
|
|
568
|
-
"run", "containment",
|
|
569
|
+
"run", "containment", "agent",
|
|
569
570
|
"token-env", "after", "repair-max-attempts", "consumer", "batch", "feedback", "source", "view", "cursor", "why", "supersedes", "decision", "sessions", "timeout",
|
|
570
571
|
]);
|
|
571
572
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
572
573
|
"json", "yes", "all", "local", "history", "latest-watch", "dry-run", "file", "allow-paid-fallback",
|
|
573
574
|
"clear", "follow", "ready", "all-tasks", "inbound-only", "help", "undo", "anyone", "allow-dispatch", "allow-merge", "merge-delete-branch",
|
|
574
|
-
"no-open", "no-verify", "no-follow", "end", "report", "off", "tmux",
|
|
575
|
+
"no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
|
|
575
576
|
"self-heal", "plan-auto", "repair-auto", "review-retry-auto", "no-local",
|
|
576
577
|
"html", "csv", "alerts-only",
|
|
577
578
|
// v105: the full export.
|
|
@@ -725,6 +726,8 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
725
726
|
...(options.waitSleep === undefined ? {} : { waitSleep: options.waitSleep }),
|
|
726
727
|
...(options.releaseIo === undefined ? {} : { releaseIo: options.releaseIo }),
|
|
727
728
|
...(options.installBin === undefined ? {} : { installBin: options.installBin }),
|
|
729
|
+
...(options.onboardSeams === undefined ? {} : { onboardSeams: options.onboardSeams }),
|
|
730
|
+
...(options.upSeams === undefined ? {} : { upSeams: options.upSeams }),
|
|
728
731
|
});
|
|
729
732
|
}
|
|
730
733
|
catch (error) {
|
|
@@ -939,6 +942,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
939
942
|
return watchCommand(flags, context);
|
|
940
943
|
case "up":
|
|
941
944
|
return upCommand(flags, context);
|
|
945
|
+
case "onboard":
|
|
946
|
+
return onboardCommand(flags, context);
|
|
942
947
|
case "daemon":
|
|
943
948
|
return daemonCommand(positional, flags, context);
|
|
944
949
|
case "bridge":
|
|
@@ -7660,6 +7665,54 @@ function openBrowser(url) {
|
|
|
7660
7665
|
// A browser that will not open is a URL the greeting already printed.
|
|
7661
7666
|
}
|
|
7662
7667
|
}
|
|
7668
|
+
/**
|
|
7669
|
+
* `onboard`: the agent that installed Toolroll becomes its lead. The work
|
|
7670
|
+
* lives in agent-onboard.ts; this wires it to the installation: the same
|
|
7671
|
+
* registry and project row Projects → add writes, and the same sign-in
|
|
7672
|
+
* checks Settings → AI providers makes.
|
|
7673
|
+
*/
|
|
7674
|
+
async function onboardCommand(flags, context) {
|
|
7675
|
+
const { store, write, json, clock } = context;
|
|
7676
|
+
const seams = context.onboardSeams ?? {};
|
|
7677
|
+
const portGiven = text(flags, "port");
|
|
7678
|
+
const port = Number(portGiven ?? 4180);
|
|
7679
|
+
if (portGiven !== undefined && (!Number.isInteger(port) || port < 1 || port >= 65536)) {
|
|
7680
|
+
return fail(write, json, "onboard", "usage", "--port is a whole number under 65536", EXIT.usage);
|
|
7681
|
+
}
|
|
7682
|
+
const home = seams.home ?? homedir();
|
|
7683
|
+
const env = seams.env ?? process.env;
|
|
7684
|
+
const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
|
|
7685
|
+
const registryPath = registryPathOf(context);
|
|
7686
|
+
const checker = createConnectionChecker({ home, env });
|
|
7687
|
+
return runOnboard({
|
|
7688
|
+
write,
|
|
7689
|
+
json,
|
|
7690
|
+
yes: flag(flags, "yes"),
|
|
7691
|
+
remove: flag(flags, "remove"),
|
|
7692
|
+
agentFlag: text(flags, "agent"),
|
|
7693
|
+
url: `http://127.0.0.1:${port}`,
|
|
7694
|
+
loginFile: join(dirname(context.databaseFile), UP_LOGIN_FILE),
|
|
7695
|
+
home,
|
|
7696
|
+
env,
|
|
7697
|
+
version: PACKAGE_VERSION,
|
|
7698
|
+
cwd: seams.cwd ?? process.cwd(),
|
|
7699
|
+
interactive: seams.interactive ?? interactive(),
|
|
7700
|
+
confirm: seams.confirm ?? (async (question) => /^y(es)?$/i.test(await ask(`${question} `))),
|
|
7701
|
+
findRepo: seams.findRepo ?? (async (cwd) => {
|
|
7702
|
+
const top = await gitRun("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
7703
|
+
return top.code === 0 ? canonicalProject(top.stdout.trim()) : null;
|
|
7704
|
+
}),
|
|
7705
|
+
enroll: async (repo) => {
|
|
7706
|
+
const before = await loadProjectRegistry(registryPath);
|
|
7707
|
+
const enrolled = await updateRepos(registryPath, repos => addRepos(repos, [repo]));
|
|
7708
|
+
if (!enrolled.ok)
|
|
7709
|
+
return { ok: false, message: enrolled.message };
|
|
7710
|
+
store.upsertProject(repo, projectName(repo), clock());
|
|
7711
|
+
return { ok: true, added: "error" in before || !before.repos.includes(repo) };
|
|
7712
|
+
},
|
|
7713
|
+
checkConnection: seams.checkConnection ?? (agent => checker(agent)),
|
|
7714
|
+
});
|
|
7715
|
+
}
|
|
7663
7716
|
async function upCommand(flags, context) {
|
|
7664
7717
|
const { store, write, json, clock } = context;
|
|
7665
7718
|
const demoFence = refuseDemo(context, "up");
|
|
@@ -8099,6 +8152,8 @@ async function upCommand(flags, context) {
|
|
|
8099
8152
|
// 9. Readiness, then the ONE startup envelope / greeting (finding 11/20).
|
|
8100
8153
|
const approverPlan2 = approver;
|
|
8101
8154
|
const url = console_.url;
|
|
8155
|
+
const terminal = context.upSeams?.terminal ?? process.stdout.isTTY === true;
|
|
8156
|
+
const upHandoff = buildHandoff({ url, loginFile: approverPlan2.passwordFile, account: approverPlan2.passwordFile === null ? approverPlan2.approver : loginAccount(approverPlan2.passwordFile) ?? approverPlan2.approver });
|
|
8102
8157
|
if (fatal === null) {
|
|
8103
8158
|
if (json) {
|
|
8104
8159
|
write(envelopeJson({
|
|
@@ -8111,8 +8166,17 @@ async function upCommand(flags, context) {
|
|
|
8111
8166
|
approvers: approverPlan2.approvers,
|
|
8112
8167
|
approverVerified: approverPlan2.verified,
|
|
8113
8168
|
...(approverPlan2.passwordFile === null ? {} : { passwordFile: approverPlan2.passwordFile }),
|
|
8169
|
+
handoff: upHandoff,
|
|
8114
8170
|
}));
|
|
8115
8171
|
}
|
|
8172
|
+
else if (!terminal) {
|
|
8173
|
+
// No person at a terminal: an agent started this. It gets the
|
|
8174
|
+
// handoff to relay, never the password, and no browser opens.
|
|
8175
|
+
write("");
|
|
8176
|
+
for (const line of handoffLines(upHandoff))
|
|
8177
|
+
write(line);
|
|
8178
|
+
write("Run `toolroll onboard` inside a repository to add it and install the Toolroll skill for your agent.");
|
|
8179
|
+
}
|
|
8116
8180
|
else {
|
|
8117
8181
|
write("");
|
|
8118
8182
|
write(`The console is on ${url}`);
|
|
@@ -8137,8 +8201,8 @@ async function upCommand(flags, context) {
|
|
|
8137
8201
|
write(" The inbox checklist shows what remains before approved work builds unattended.");
|
|
8138
8202
|
write(" Ctrl-C stops Toolroll on this machine.");
|
|
8139
8203
|
}
|
|
8140
|
-
if (!json && !flags.has("no-open") &&
|
|
8141
|
-
openBrowser(url);
|
|
8204
|
+
if (!json && !flags.has("no-open") && terminal)
|
|
8205
|
+
(context.upSeams?.openBrowser ?? openBrowser)(url);
|
|
8142
8206
|
}
|
|
8143
8207
|
// 10. Supervise to the end.
|
|
8144
8208
|
await stopped;
|
package/dist/releases.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export declare const UPDATE_SWITCH_FILE = "update-check";
|
|
|
16
16
|
export declare const RUNNER_VERSIONS_FILE = "runner-versions.json";
|
|
17
17
|
export declare const CHECK_EVERY_MS: number;
|
|
18
18
|
export declare const CHECK_TIMEOUT_MS = 5000;
|
|
19
|
+
/** The npm registry `toolroll update` downloads releases from. */
|
|
20
|
+
export declare const REGISTRY = "https://registry.npmjs.org";
|
|
19
21
|
export declare const REGISTRY_URL = "https://registry.npmjs.org/toolroll/latest";
|
|
20
22
|
export declare const RELEASES_PAGE = "https://github.com/ap9000/toolroll/releases/tag/";
|
|
21
23
|
export type Release = {
|
|
@@ -74,3 +76,10 @@ export declare function runnerVersions(dir: string): RunnerVersion[];
|
|
|
74
76
|
export declare function startUpdateChecks(store: Store, dir: string, current: string, updateCommand: string, io?: ReleaseIo & {
|
|
75
77
|
everyMs?: number;
|
|
76
78
|
}): () => void;
|
|
79
|
+
/** The latest version, asked of npm now: for an explicit update (the command, Check now), not the once-a-day
|
|
80
|
+
* notice, so neither its cache nor its switch applies. Throws with npm's own reason. */
|
|
81
|
+
export declare function latestVersionNow(io?: {
|
|
82
|
+
fetch?: typeof fetch;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
version: string;
|
|
85
|
+
}>;
|
package/dist/releases.js
CHANGED
|
@@ -18,7 +18,9 @@ export const UPDATE_SWITCH_FILE = "update-check";
|
|
|
18
18
|
export const RUNNER_VERSIONS_FILE = "runner-versions.json";
|
|
19
19
|
export const CHECK_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
20
20
|
export const CHECK_TIMEOUT_MS = 5_000;
|
|
21
|
-
|
|
21
|
+
/** The npm registry `toolroll update` downloads releases from. */
|
|
22
|
+
export const REGISTRY = "https://registry.npmjs.org";
|
|
23
|
+
export const REGISTRY_URL = `${REGISTRY}/toolroll/latest`;
|
|
22
24
|
export const RELEASES_PAGE = "https://github.com/ap9000/toolroll/releases/tag/";
|
|
23
25
|
const NOTES_API = "https://api.github.com/repos/ap9000/toolroll/releases/tags/";
|
|
24
26
|
const NOTES_MAX = 20_000;
|
|
@@ -223,3 +225,14 @@ function writeAtomic(file, content) {
|
|
|
223
225
|
writeFileSync(temporary, content, { mode: 0o600 });
|
|
224
226
|
renameSync(temporary, file);
|
|
225
227
|
}
|
|
228
|
+
/** The latest version, asked of npm now: for an explicit update (the command, Check now), not the once-a-day
|
|
229
|
+
* notice, so neither its cache nor its switch applies. Throws with npm's own reason. */
|
|
230
|
+
export async function latestVersionNow(io = {}) {
|
|
231
|
+
const answer = await (io.fetch ?? fetch)(REGISTRY_URL, { signal: AbortSignal.timeout(10_000), headers: { accept: "application/json" } });
|
|
232
|
+
if (!answer.ok)
|
|
233
|
+
throw new Error(`npm answered ${answer.status}`);
|
|
234
|
+
const version = (await answer.json()).version;
|
|
235
|
+
if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version))
|
|
236
|
+
throw new Error("npm did not name a version");
|
|
237
|
+
return { version };
|
|
238
|
+
}
|
package/dist/serve.d.ts
CHANGED
|
@@ -11,8 +11,10 @@ import { run as execRun } from "./exec.js";
|
|
|
11
11
|
import { type AgentChoice } from "./agentconfig.js";
|
|
12
12
|
import { type RouteProjection, type RouteOverride, type RouteStamp, type RiskLevel } from "./phase-routing.js";
|
|
13
13
|
import { type Phase } from "./provider.js";
|
|
14
|
+
import { launchRuntimeUpdate } from "./toolroll-update.js";
|
|
14
15
|
import type { RevisionLineage } from "./store.js";
|
|
15
16
|
import type { SubscriptionMateRunner } from "./subscription-chat.js";
|
|
17
|
+
import { type InstallMethod } from "./install-method.js";
|
|
16
18
|
export type ServeOptions = {
|
|
17
19
|
/** Tests: the bin whose real path says how Toolroll was installed (Settings → Updates' command). */
|
|
18
20
|
installBin?: string;
|
|
@@ -53,6 +55,18 @@ export type ServeOptions = {
|
|
|
53
55
|
configDir?: string;
|
|
54
56
|
/** Test seam for the Slack setup handshake. */
|
|
55
57
|
slackFetcher?: typeof fetch;
|
|
58
|
+
/** Settings → Updates seams: the latest release, how this Toolroll was
|
|
59
|
+
* installed, its version, and how the updater job starts. Tests and
|
|
60
|
+
* screenshots inject these; production reads the registry and launchd. */
|
|
61
|
+
updates?: {
|
|
62
|
+
latest?: () => Promise<{
|
|
63
|
+
version: string;
|
|
64
|
+
}>;
|
|
65
|
+
method?: InstallMethod;
|
|
66
|
+
current?: string;
|
|
67
|
+
dist?: string;
|
|
68
|
+
launch?: typeof launchRuntimeUpdate;
|
|
69
|
+
};
|
|
56
70
|
discordFetcher?: typeof fetch;
|
|
57
71
|
/** Injected by tests: Microsoft sign-in, key metadata and Teams conversation calls. */
|
|
58
72
|
teamsFetcher?: typeof fetch;
|
|
@@ -107,6 +121,8 @@ export type ServeOptions = {
|
|
|
107
121
|
additionalProjectRepos?: () => readonly string[];
|
|
108
122
|
connectionProbe?: typeof execRun;
|
|
109
123
|
connectionHome?: string;
|
|
124
|
+
/** Test seam for Chat's first tasks: the `gh` and `git grep` reads. */
|
|
125
|
+
firstTaskRunner?: typeof execRun;
|
|
110
126
|
modelCatalogFetcher?: typeof fetch;
|
|
111
127
|
/** Test seams for Settings → Models: CLI version probes and the PATH they search. */
|
|
112
128
|
modelRunner?: VersionRunner;
|