@bondedhq/runner 0.0.0-stage → 0.1.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.
@@ -0,0 +1,73 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ /**
5
+ * Where a project path really is, refused when it would land outside the project (`..`, an
6
+ * absolute path, or a symlinked folder that points elsewhere).
7
+ */
8
+ export function insideProject(root, path) {
9
+ if (isAbsolute(path))
10
+ throw new Error(`refusing to write ${path}: not a path in the project`);
11
+ const realRoot = realpathSync(root);
12
+ const target = resolve(realRoot, path);
13
+ let existing = target;
14
+ while (!existsSync(existing))
15
+ existing = dirname(existing);
16
+ const real = join(realpathSync(existing), relative(existing, target));
17
+ const rel = relative(realRoot, real);
18
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel))
19
+ throw new Error(`refusing to write ${path}: it is outside the project`);
20
+ return real;
21
+ }
22
+ /**
23
+ * Writes the plan's files. Each changed file must still be as it was when the plan was made, so
24
+ * nothing written in between is overwritten.
25
+ */
26
+ export function applyPlan(root, plan) {
27
+ const targets = plan.changes.map((c) => ({ change: c, abs: insideProject(root, c.path) }));
28
+ for (const { change, abs } of targets) {
29
+ const now = existsSync(abs) ? readFileSync(abs, "utf8") : undefined;
30
+ if (now !== change.before)
31
+ throw new Error(`${change.path} changed since init read it; run init again`);
32
+ }
33
+ for (const { change, abs } of targets) {
34
+ mkdirSync(dirname(abs), { recursive: true });
35
+ writeFileSync(abs, change.after);
36
+ }
37
+ return targets.map((t) => t.change.path);
38
+ }
39
+ /**
40
+ * Appends the names that aren't in the env file yet, with empty values. Never reads values out or
41
+ * changes a line that is there.
42
+ */
43
+ export function appendEnvNames(root, names, file = ".env") {
44
+ const abs = insideProject(root, file);
45
+ const text = existsSync(abs) ? readFileSync(abs, "utf8") : "";
46
+ const present = new Set(text
47
+ .split("\n")
48
+ .map((l) => /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(l)?.[1])
49
+ .filter(Boolean));
50
+ const missing = [...new Set(names)].filter((n) => !present.has(n));
51
+ if (!missing.length)
52
+ return [];
53
+ const block = `${text && !text.endsWith("\n") ? "\n" : ""}${text ? "\n" : ""}# Bonded (bonded-runner init): fill these in\n${missing.map((n) => `${n}=`).join("\n")}\n`;
54
+ writeFileSync(abs, text + block);
55
+ return missing;
56
+ }
57
+ /** The install command for the project's package manager. */
58
+ export function installCommand(pm, packages) {
59
+ return pm === "npm" ? ["npm", "install", ...packages] : [pm, "add", ...packages];
60
+ }
61
+ /** Runs a command in the project, its output shown as it runs. Resolves with its exit code. */
62
+ export function run(command, cwd) {
63
+ return new Promise((done) => {
64
+ const child = spawn(command[0], command.slice(1), {
65
+ cwd,
66
+ stdio: "inherit",
67
+ shell: process.platform === "win32",
68
+ });
69
+ child.once("error", () => done(127));
70
+ child.once("close", (code) => done(code ?? 1));
71
+ });
72
+ }
73
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1,79 @@
1
+ /**
2
+ * A client for Bonded's connection check, so `init` can run one right after connecting. With the
3
+ * runner token alone: `POST /runner/check` (the token as the bearer, no body) checks the preview
4
+ * or agent the token belongs to and answers with a stream token, then `GET
5
+ * /checks/:id/events?token=` follows it to the result. With an owner's session (the older way):
6
+ * `POST /previews/:id/checks` or `/self/agents/:id/checks`, then the same events with the
7
+ * session. The API decides pass or fail; this only prints it.
8
+ */
9
+ /** The Bonded API the runner talks to when neither --api nor BONDED_API_URL says otherwise. */
10
+ export declare const DEFAULT_API_URL = "https://api-production-ba50.up.railway.app";
11
+ /** What a connection check streams (the API's `ConnectionCheckEvent`). */
12
+ export type CheckEvent = {
13
+ type: "started";
14
+ } | {
15
+ type: "tool";
16
+ name: string;
17
+ surface?: string | null;
18
+ ok: boolean;
19
+ error?: string;
20
+ } | {
21
+ type: "tx";
22
+ ok: boolean;
23
+ reason?: string;
24
+ } | {
25
+ type: "done";
26
+ exitCode?: number | null;
27
+ durationMs: number;
28
+ stdoutTail?: string;
29
+ stderrTail?: string;
30
+ } | ({
31
+ type: "result";
32
+ } & CheckResult);
33
+ export interface CheckResult {
34
+ passed: boolean;
35
+ problems: string[];
36
+ warnings?: string[];
37
+ /** The same sentences with their troubleshooting codes. */
38
+ issues?: {
39
+ code: string;
40
+ severity: "problem" | "warning";
41
+ message: string;
42
+ }[];
43
+ taskCompleted: boolean | null;
44
+ toolsCalled: number;
45
+ surfacesRead: string[];
46
+ durationMs?: number;
47
+ codeHash?: string;
48
+ }
49
+ export type CheckTarget = {
50
+ /** The Bonded API, e.g. https://api.bonded.example. */
51
+ api: string;
52
+ /** The runner token (`bonded_rt_...`): it names the preview or agent to check. */
53
+ runnerToken: string;
54
+ } | {
55
+ api: string;
56
+ /** A session token from signing in (`bonded_ss_...`). */
57
+ session: string;
58
+ /** A preview (`/previews/:id`) or a registered agent (`/self/agents/:id`). */
59
+ subject: {
60
+ kind: "preview" | "agent";
61
+ id: string;
62
+ };
63
+ };
64
+ export declare function runConnectionCheck(target: CheckTarget, onEvent: (event: CheckEvent) => void, options?: {
65
+ fetch?: typeof fetch;
66
+ timeoutMs?: number;
67
+ }): Promise<CheckResult>;
68
+ /** Server-sent events from a response body: each message's `id` and `data`. */
69
+ export declare function serverSentEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<{
70
+ id?: string;
71
+ data: string;
72
+ }>;
73
+ /** One check event as a line for the terminal (the web app's wording). */
74
+ export declare function eventLine(event: CheckEvent): string;
75
+ /** Where each problem's fix is explained, by its code (the anchor). */
76
+ export declare const TROUBLESHOOTING_URL = "https://bonded-nu.vercel.app/docs/guides/troubleshooting/";
77
+ /** The result as lines: the verdict, then each problem and warning, with its fix's link. */
78
+ export declare function resultLines(result: CheckResult): string[];
79
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1,158 @@
1
+ /**
2
+ * A client for Bonded's connection check, so `init` can run one right after connecting. With the
3
+ * runner token alone: `POST /runner/check` (the token as the bearer, no body) checks the preview
4
+ * or agent the token belongs to and answers with a stream token, then `GET
5
+ * /checks/:id/events?token=` follows it to the result. With an owner's session (the older way):
6
+ * `POST /previews/:id/checks` or `/self/agents/:id/checks`, then the same events with the
7
+ * session. The API decides pass or fail; this only prints it.
8
+ */
9
+ /** The Bonded API the runner talks to when neither --api nor BONDED_API_URL says otherwise. */
10
+ export const DEFAULT_API_URL = "https://api-production-ba50.up.railway.app";
11
+ export async function runConnectionCheck(target, onEvent, options = {}) {
12
+ const fetcher = options.fetch ?? fetch;
13
+ const base = target.api.replace(/\/+$/, "");
14
+ const byToken = "runnerToken" in target;
15
+ const started = byToken
16
+ ? await fetcher(`${base}/runner/check`, {
17
+ method: "POST",
18
+ headers: { authorization: `Bearer ${target.runnerToken}` },
19
+ })
20
+ : await fetcher(`${base}${target.subject.kind === "preview"
21
+ ? `/previews/${encodeURIComponent(target.subject.id)}/checks`
22
+ : `/self/agents/${encodeURIComponent(target.subject.id)}/checks`}`, {
23
+ method: "POST",
24
+ headers: {
25
+ authorization: `Bearer ${target.session}`,
26
+ "content-type": "application/json",
27
+ },
28
+ body: "{}",
29
+ });
30
+ const body = (await started.json().catch(() => ({})));
31
+ if (!started.ok || !body.checkId)
32
+ throw new Error(body.error ?? `the API answered ${started.status} to the check`);
33
+ if (byToken && !body.streamToken)
34
+ throw new Error("the API didn't give a stream token for the check's events");
35
+ // The events: with the stream token the check came with, or with the session.
36
+ const eventsUrl = byToken
37
+ ? `${base}/checks/${encodeURIComponent(body.checkId)}/events?token=${encodeURIComponent(body.streamToken)}`
38
+ : `${base}/checks/${encodeURIComponent(body.checkId)}/events`;
39
+ const headers = byToken
40
+ ? {}
41
+ : { authorization: `Bearer ${target.session}` };
42
+ const seen = body.events ?? [];
43
+ seen.forEach(onEvent);
44
+ if (body.result) {
45
+ if (!seen.some((e) => e.type === "result"))
46
+ onEvent({ type: "result", ...body.result });
47
+ return body.result;
48
+ }
49
+ const deadline = Date.now() + (options.timeoutMs ?? 5 * 60_000);
50
+ let lastId = "";
51
+ let skip = seen.length;
52
+ while (Date.now() < deadline) {
53
+ const response = await fetcher(eventsUrl, {
54
+ headers: {
55
+ ...headers,
56
+ accept: "text/event-stream",
57
+ ...(lastId ? { "last-event-id": lastId } : {}),
58
+ },
59
+ signal: AbortSignal.timeout(Math.max(1_000, deadline - Date.now())),
60
+ });
61
+ if (!response.ok || !response.body)
62
+ throw new Error(`the API answered ${response.status} to the check's events`);
63
+ for await (const message of serverSentEvents(response.body)) {
64
+ if (message.id)
65
+ lastId = message.id;
66
+ let event;
67
+ try {
68
+ event = JSON.parse(message.data);
69
+ }
70
+ catch {
71
+ continue;
72
+ }
73
+ // The first stream starts from the beginning: skip what the POST already showed.
74
+ if (skip > 0) {
75
+ skip--;
76
+ continue;
77
+ }
78
+ onEvent(event);
79
+ if (event.type === "result") {
80
+ const { type: _type, ...result } = event;
81
+ void _type;
82
+ return result;
83
+ }
84
+ }
85
+ skip = 0;
86
+ }
87
+ throw new Error("the check didn't finish in time; see it on your agent's page");
88
+ }
89
+ /** Server-sent events from a response body: each message's `id` and `data`. */
90
+ export async function* serverSentEvents(body) {
91
+ const decoder = new TextDecoder();
92
+ let buffer = "";
93
+ let id;
94
+ let data = [];
95
+ for await (const chunk of body) {
96
+ buffer += decoder.decode(chunk, { stream: true });
97
+ let newline;
98
+ while ((newline = buffer.indexOf("\n")) >= 0) {
99
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
100
+ buffer = buffer.slice(newline + 1);
101
+ if (line === "") {
102
+ if (data.length)
103
+ yield { ...(id ? { id } : {}), data: data.join("\n") };
104
+ data = [];
105
+ id = undefined;
106
+ }
107
+ else if (line.startsWith("data:"))
108
+ data.push(line.slice(5).replace(/^ /, ""));
109
+ else if (line.startsWith("id:"))
110
+ id = line.slice(3).trim();
111
+ }
112
+ }
113
+ if (data.length)
114
+ yield { ...(id ? { id } : {}), data: data.join("\n") };
115
+ }
116
+ /** One check event as a line for the terminal (the web app's wording). */
117
+ export function eventLine(event) {
118
+ switch (event.type) {
119
+ case "started":
120
+ return "Episode started: your agent got its task";
121
+ case "tool": {
122
+ const surface = event.surface ? ` (read ${event.surface})` : "";
123
+ return event.ok
124
+ ? `Called ${event.name}${surface}`
125
+ : `Called ${event.name}${surface}: ${event.error ?? "it failed"}`;
126
+ }
127
+ case "tx":
128
+ return event.ok
129
+ ? "Sent a transaction to its vault"
130
+ : `A transaction was refused: ${event.reason ?? "no reason given"}`;
131
+ case "done": {
132
+ const exit = event.exitCode === undefined || event.exitCode === null
133
+ ? ""
134
+ : `, exit code ${event.exitCode}`;
135
+ return `Finished after ${(event.durationMs / 1000).toFixed(1)} s${exit}`;
136
+ }
137
+ case "result":
138
+ return event.passed ? "The check passed" : "The check found problems";
139
+ }
140
+ }
141
+ /** Where each problem's fix is explained, by its code (the anchor). */
142
+ export const TROUBLESHOOTING_URL = "https://bonded-nu.vercel.app/docs/guides/troubleshooting/";
143
+ /** The result as lines: the verdict, then each problem and warning, with its fix's link. */
144
+ export function resultLines(result) {
145
+ const line = (kind, text) => {
146
+ const code = result.issues?.find((i) => i.message === text && i.severity === kind)?.code;
147
+ return code && code !== "other" && /^[a-z0-9-]+$/.test(code)
148
+ ? [` ${kind}: ${text}`, ` how to fix: ${TROUBLESHOOTING_URL}#${code}`]
149
+ : [` ${kind}: ${text}`];
150
+ };
151
+ return [
152
+ result.passed ? "Connection check passed: a rating will work." : "Connection check failed:",
153
+ ...result.problems.flatMap((p) => line("problem", p)),
154
+ ...(result.warnings ?? []).flatMap((w) => line("warning", w)),
155
+ ` ${result.toolsCalled} tool call${result.toolsCalled === 1 ? "" : "s"}, task ${result.taskCompleted ? "done" : "not done"}${result.surfacesRead.length ? `, read ${result.surfacesRead.join(", ")}` : ""}`,
156
+ ];
157
+ }
158
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1,34 @@
1
+ import { AgentMode } from "@bondedhq/shared";
2
+ import { type CheckTarget } from "./check.js";
3
+ import { type Project } from "./detect.js";
4
+ export declare const INIT_USAGE: string;
5
+ export interface InitIO {
6
+ cwd: string;
7
+ env: Record<string, string | undefined>;
8
+ out: (line: string) => void;
9
+ err: (line: string) => void;
10
+ /** Asks a yes/no question; undefined when there is no terminal to ask in. */
11
+ confirm?: (question: string) => Promise<boolean>;
12
+ /** Runs a command (the package install). */
13
+ run?: (command: string[], cwd: string) => Promise<number>;
14
+ /** Starts the runner and resolves when it stops (tests replace it). */
15
+ connect?: (options: ConnectOptions) => Promise<number>;
16
+ }
17
+ export interface ConnectOptions {
18
+ gateway: string;
19
+ token: string;
20
+ mode: AgentMode;
21
+ command: string[];
22
+ cwd: string;
23
+ check?: CheckTarget;
24
+ out: (line: string) => void;
25
+ err: (line: string) => void;
26
+ }
27
+ export declare function initCommand(argv: string[], io: InitIO): Promise<number>;
28
+ /** Why an API URL can't be used: it must be https, except on this machine. */
29
+ export declare function apiUrlProblem(api: string): string | undefined;
30
+ /** The agent's command: what came after --, else the package.json start script. */
31
+ export declare function agentCommand(project: Project, given: string[]): string[];
32
+ /** Asks on the terminal; undefined when stdin isn't one. */
33
+ export declare function terminalConfirm(): InitIO["confirm"];
34
+ //# sourceMappingURL=command.d.ts.map
@@ -0,0 +1,314 @@
1
+ import { resolve } from "node:path";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { parseArgs } from "node:util";
4
+ import { AgentMode } from "@bondedhq/shared";
5
+ import { codeIdentity } from "../code-hash.js";
6
+ import { Runner } from "../runner.js";
7
+ import { gatewayUrlProblem } from "../tail.js";
8
+ import { appendEnvNames, applyPlan, installCommand, run } from "./apply.js";
9
+ import { DEFAULT_API_URL, eventLine, resultLines, runConnectionCheck, } from "./check.js";
10
+ import { detectProject, FRAMEWORKS } from "./detect.js";
11
+ import { unifiedDiff } from "./diff.js";
12
+ import { frameworkLine, planInit } from "./plan.js";
13
+ export const INIT_USAGE = `Usage: bonded-runner init [folder] [options] [-- <agent command...>]
14
+
15
+ Run in your agent's folder. Finds its framework, then proposes the changes that connect it to
16
+ Bonded, as a diff, and asks before writing anything:
17
+ - the rating hook: while Bonded rates the agent, it uses the episode's tools;
18
+ - trading through your Mandate Vault in production (once BONDED_VAULT_ADDRESS is set).
19
+ On yes it writes them and adds @bondedhq/sdk with your package manager.
20
+
21
+ --print print the proposed changes and exit; write nothing
22
+ --yes, -y apply without asking
23
+ --framework <name> skip detection: ${FRAMEWORKS.filter((f) => f !== "python").join(", ")}
24
+ --url also add an Agent URL route (Next.js, Express, Hono, or a node:http server)
25
+ --env append the new variables' names, with empty values, to .env
26
+ --skip-install don't add @bondedhq/sdk
27
+ --overwrite-generated
28
+ rewrite files init wrote before even if you edited them (the diff shows
29
+ what goes). Without it, an edited file is kept
30
+
31
+ Connect right after (optional). With a runner token and the gateway, init starts the runner
32
+ with the agent command after -- (or your package.json start script) and, once it is connected,
33
+ runs a connection check with the runner token and prints its events and result:
34
+ BONDED_RUNNER_TOKEN your runner token, from your agent's page. Prefer this to --token, which
35
+ anyone on the machine can read in the process list
36
+ --token <token> the same, on the command line
37
+ --gateway <url> the Arena Gateway (or BONDED_GATEWAY_URL)
38
+ --mode tools|chain default tools
39
+ --api <url> the Bonded API (or BONDED_API_URL); default ${DEFAULT_API_URL}
40
+ --skip-check start the runner without running a check
41
+ --session <token> the older way: check with your session token (or BONDED_SESSION_TOKEN)
42
+ --preview <id> | --agent <id>
43
+ and the preview or agent to check (needed only with --session)
44
+
45
+ Example:
46
+ npx @bondedhq/runner init
47
+ BONDED_RUNNER_TOKEN=... npx @bondedhq/runner init --yes --gateway https://arena.example.com -- node agent.js`;
48
+ export async function initCommand(argv, io) {
49
+ const dash = argv.indexOf("--");
50
+ const own = dash >= 0 ? argv.slice(0, dash) : argv;
51
+ const command = dash >= 0 ? argv.slice(dash + 1) : [];
52
+ let values;
53
+ let positionals;
54
+ try {
55
+ ({ values, positionals } = parseArgs({
56
+ args: own,
57
+ allowPositionals: true,
58
+ options: {
59
+ print: { type: "boolean", default: false },
60
+ yes: { type: "boolean", short: "y", default: false },
61
+ framework: { type: "string" },
62
+ url: { type: "boolean", default: false },
63
+ env: { type: "boolean", default: false },
64
+ "skip-install": { type: "boolean", default: false },
65
+ "overwrite-generated": { type: "boolean", default: false },
66
+ token: { type: "string" },
67
+ gateway: { type: "string" },
68
+ mode: { type: "string", default: "tools" },
69
+ api: { type: "string" },
70
+ "skip-check": { type: "boolean", default: false },
71
+ session: { type: "string" },
72
+ preview: { type: "string" },
73
+ agent: { type: "string" },
74
+ help: { type: "boolean", short: "h", default: false },
75
+ },
76
+ }));
77
+ }
78
+ catch (error) {
79
+ io.err(`${error instanceof Error ? error.message : String(error)}\n\n${INIT_USAGE}`);
80
+ return 1;
81
+ }
82
+ if (values.help) {
83
+ io.out(INIT_USAGE);
84
+ return 0;
85
+ }
86
+ if (values.framework && !FRAMEWORKS.includes(values.framework)) {
87
+ io.err(`--framework must be one of ${FRAMEWORKS.join(", ")}`);
88
+ return 1;
89
+ }
90
+ const mode = AgentMode.safeParse(values.mode);
91
+ if (!mode.success) {
92
+ io.err("--mode must be tools or chain");
93
+ return 1;
94
+ }
95
+ const root = resolve(io.cwd, positionals[0] ?? ".");
96
+ const project = detectProject(root, values.framework);
97
+ const plan = planInit(project, {
98
+ url: values.url,
99
+ overwriteGenerated: values["overwrite-generated"],
100
+ });
101
+ printPlan(project, plan, io.out);
102
+ const install = values["skip-install"] ? [] : plan.install;
103
+ const installCmd = install.length ? installCommand(project.packageManager, install) : undefined;
104
+ const hasWork = plan.changes.length > 0 || installCmd !== undefined || values.env;
105
+ if (values.print)
106
+ return 0;
107
+ if (hasWork) {
108
+ let yes = values.yes;
109
+ if (!yes) {
110
+ if (!io.confirm) {
111
+ io.out("\nNothing written: not a terminal. Run again with --yes to apply, or --print to only print.");
112
+ return 0;
113
+ }
114
+ const what = [
115
+ plan.changes.length
116
+ ? `write ${plan.changes.length} file${plan.changes.length === 1 ? "" : "s"}`
117
+ : "",
118
+ installCmd ? `run \`${installCmd.join(" ")}\`` : "",
119
+ values.env ? "append the variable names to .env" : "",
120
+ ]
121
+ .filter(Boolean)
122
+ .join(", ");
123
+ yes = await io.confirm(`\n${capitalise(what)}?`);
124
+ }
125
+ if (!yes) {
126
+ io.out("Nothing written.");
127
+ return 0;
128
+ }
129
+ try {
130
+ for (const path of applyPlan(root, plan))
131
+ io.out(`wrote ${path}`);
132
+ if (values.env) {
133
+ const added = appendEnvNames(root, plan.env);
134
+ io.out(added.length
135
+ ? `added to .env: ${added.join(", ")} (empty: fill them in)`
136
+ : ".env already names them all");
137
+ }
138
+ }
139
+ catch (error) {
140
+ io.err(error instanceof Error ? error.message : String(error));
141
+ return 1;
142
+ }
143
+ if (installCmd) {
144
+ io.out(`\n$ ${installCmd.join(" ")}`);
145
+ const code = await (io.run ?? run)(installCmd, root);
146
+ if (code !== 0) {
147
+ io.err(`\`${installCmd.join(" ")}\` failed (exit ${code}). Run it yourself, then start your agent.`);
148
+ return 1;
149
+ }
150
+ }
151
+ }
152
+ const token = values.token ?? io.env.BONDED_RUNNER_TOKEN;
153
+ const gateway = values.gateway ?? io.env.BONDED_GATEWAY_URL;
154
+ if (!token || !gateway) {
155
+ io.out(`\nNext: start the runner with your runner token (from your agent's page):\n BONDED_RUNNER_TOKEN=... npx @bondedhq/runner --gateway <gateway url>${mode.data === "chain" ? " --mode chain" : ""} -- ${agentCommand(project, command).join(" ") || "<your agent command>"}\nthen run a connection check on the page. Or run init again with BONDED_RUNNER_TOKEN and --gateway: it starts the runner and runs the check here.`);
156
+ return 0;
157
+ }
158
+ const problem = gatewayUrlProblem(gateway);
159
+ if (problem) {
160
+ io.err(problem);
161
+ return 1;
162
+ }
163
+ const agent = agentCommand(project, command);
164
+ if (!agent.length) {
165
+ io.err("No agent command: put it after -- (e.g. -- node agent.js), or add a start script to package.json.");
166
+ return 1;
167
+ }
168
+ if (values.token)
169
+ io.err("Note: a token given with --token shows in the process list. Next time, set BONDED_RUNNER_TOKEN instead.");
170
+ const api = (values.api ?? io.env.BONDED_API_URL ?? DEFAULT_API_URL).trim();
171
+ const apiProblem = apiUrlProblem(api);
172
+ if (apiProblem) {
173
+ io.err(apiProblem);
174
+ return 1;
175
+ }
176
+ const session = values.session ?? io.env.BONDED_SESSION_TOKEN;
177
+ const subject = values.preview
178
+ ? { kind: "preview", id: values.preview }
179
+ : values.agent
180
+ ? { kind: "agent", id: values.agent }
181
+ : undefined;
182
+ // The runner token names what to check; a session and a subject is the older way, still read.
183
+ const check = values["skip-check"]
184
+ ? undefined
185
+ : session && subject
186
+ ? { api, session, subject }
187
+ : { api, runnerToken: token };
188
+ return (io.connect ?? connect)({
189
+ gateway,
190
+ token,
191
+ mode: mode.data,
192
+ command: agent,
193
+ cwd: root,
194
+ ...(check ? { check } : {}),
195
+ out: io.out,
196
+ err: io.err,
197
+ });
198
+ }
199
+ /** Why an API URL can't be used: it must be https, except on this machine. */
200
+ export function apiUrlProblem(api) {
201
+ let url;
202
+ try {
203
+ url = new URL(api);
204
+ }
205
+ catch {
206
+ return `--api must be a URL, e.g. ${DEFAULT_API_URL}`;
207
+ }
208
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
209
+ if (url.protocol === "https:" || (url.protocol === "http:" && local))
210
+ return undefined;
211
+ return "--api must be https (plain http only on localhost)";
212
+ }
213
+ /** The agent's command: what came after --, else the package.json start script. */
214
+ export function agentCommand(project, given) {
215
+ if (given.length)
216
+ return given;
217
+ if (project.scripts.start)
218
+ return [project.packageManager, "run", "start"];
219
+ return [];
220
+ }
221
+ function printPlan(project, plan, out) {
222
+ out(`Framework: ${frameworkLine(project)}`);
223
+ out(`Package manager: ${project.packageManager}${project.typescript ? ", TypeScript" : ""}`);
224
+ if (plan.changes.length) {
225
+ out("\nProposed changes:\n");
226
+ for (const c of plan.changes) {
227
+ out(`# ${c.path}${c.before === undefined ? " (new file)" : ""}: ${c.why}`);
228
+ out(unifiedDiff(c.path, c.before, c.after).trimEnd());
229
+ out("");
230
+ }
231
+ }
232
+ else
233
+ out("\nNo file changes needed.");
234
+ if (plan.install.length)
235
+ out(`Adds: ${plan.install.join(" ")}`);
236
+ if (plan.manual.length) {
237
+ out("\nBy hand (init didn't change these, to be safe):");
238
+ for (const m of plan.manual)
239
+ out(m.includes("\n") ? m : ` - ${m}`);
240
+ }
241
+ if (plan.notes.length) {
242
+ out("\nGood to know:");
243
+ for (const n of plan.notes)
244
+ out(` - ${n}`);
245
+ }
246
+ if (plan.env.length)
247
+ out(`\nVariables: ${plan.env.join(", ")}`);
248
+ }
249
+ /** Starts the runner; with a check target, runs a connection check once it is connected. */
250
+ async function connect(options) {
251
+ const log = (line) => options.err(`[bonded-runner] ${line}`);
252
+ const code = codeIdentity({ command: options.command, cwd: options.cwd });
253
+ log(`code hash ${code.codeHash} (${code.codeSource}: ${code.detail})`);
254
+ let checked = false;
255
+ let failed = false;
256
+ const runner = new Runner({
257
+ code,
258
+ gateway: options.gateway,
259
+ token: options.token,
260
+ mode: options.mode,
261
+ command: options.command,
262
+ cwd: options.cwd,
263
+ log,
264
+ onConnect: () => {
265
+ if (checked)
266
+ return;
267
+ checked = true;
268
+ if (!options.check) {
269
+ options.out("\nConnected. Run a connection check on your agent's page; leave this running for your rating (Ctrl-C stops it).");
270
+ return;
271
+ }
272
+ options.out("\nConnected. Running a connection check...");
273
+ runConnectionCheck(options.check, (event) => options.out(` ${eventLine(event)}`)).then((result) => {
274
+ for (const line of resultLines(result))
275
+ options.out(line);
276
+ if (result.passed)
277
+ options.out("Leave the runner running for your rating (Ctrl-C stops it).");
278
+ else {
279
+ failed = true;
280
+ void runner.stop();
281
+ }
282
+ }, (error) => {
283
+ options.err(`The connection check couldn't run: ${error instanceof Error ? error.message : String(error)}`);
284
+ options.out("The runner is still connected: run the check on your agent's page.");
285
+ });
286
+ },
287
+ });
288
+ const stop = () => void runner.stop();
289
+ process.once("SIGINT", stop);
290
+ process.once("SIGTERM", stop);
291
+ const result = await runner.start();
292
+ if (result.code !== 0)
293
+ log(result.reason);
294
+ return failed ? 1 : result.code;
295
+ }
296
+ function capitalise(text) {
297
+ return text.charAt(0).toUpperCase() + text.slice(1);
298
+ }
299
+ /** Asks on the terminal; undefined when stdin isn't one. */
300
+ export function terminalConfirm() {
301
+ if (!process.stdin.isTTY)
302
+ return undefined;
303
+ return async (question) => {
304
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
305
+ try {
306
+ const answer = await rl.question(`${question} [y/N] `);
307
+ return /^y(es)?$/i.test(answer.trim());
308
+ }
309
+ finally {
310
+ rl.close();
311
+ }
312
+ };
313
+ }
314
+ //# sourceMappingURL=command.js.map