@cabane/companion 0.6.214 → 0.6.216
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/README.md +41 -7
- package/dist/cli.js +410 -190
- package/dist/credential-broker.js +653 -0
- package/dist/launch.js +4 -1
- package/dist/pairing-config.js +100 -37
- package/dist/runtime.js +44 -8
- package/package.json +10 -1
package/dist/cli.js
CHANGED
|
@@ -778,7 +778,10 @@ var init_config = __esm({
|
|
|
778
778
|
// credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
|
|
779
779
|
// what a wrapper can rely on.
|
|
780
780
|
runner: z2.object({
|
|
781
|
-
command: z2.array(z2.string().min(1)).min(1)
|
|
781
|
+
command: z2.array(z2.string().min(1)).min(1),
|
|
782
|
+
// The external wrapper supplies only broker placeholders and removes
|
|
783
|
+
// credential mounts. Do not hand provider logins to that runner on stdin.
|
|
784
|
+
brokeredCredentials: z2.boolean().optional()
|
|
782
785
|
}).strict().optional()
|
|
783
786
|
});
|
|
784
787
|
RETIRED_ROW_KEYS = ["sandbox"];
|
|
@@ -1418,7 +1421,7 @@ function pathForLog(path) {
|
|
|
1418
1421
|
const q = path.indexOf("?");
|
|
1419
1422
|
return q === -1 ? path : path.slice(0, q);
|
|
1420
1423
|
}
|
|
1421
|
-
function
|
|
1424
|
+
function sleep2(ms, signal) {
|
|
1422
1425
|
return new Promise((resolve2) => {
|
|
1423
1426
|
if (signal?.aborted) return resolve2();
|
|
1424
1427
|
const timer = setTimeout(() => {
|
|
@@ -1521,7 +1524,7 @@ var init_api = __esm({
|
|
|
1521
1524
|
},
|
|
1522
1525
|
"companion: API call failed transiently; retrying"
|
|
1523
1526
|
);
|
|
1524
|
-
await
|
|
1527
|
+
await sleep2(delayMs, signal);
|
|
1525
1528
|
if (signal?.aborted) throw err;
|
|
1526
1529
|
}
|
|
1527
1530
|
}
|
|
@@ -7510,7 +7513,7 @@ var init_opencode_lock = __esm({
|
|
|
7510
7513
|
});
|
|
7511
7514
|
|
|
7512
7515
|
// src/turn-containment.ts
|
|
7513
|
-
import { execFile, execFileSync, spawn as
|
|
7516
|
+
import { execFile, execFileSync, spawn as spawn4 } from "child_process";
|
|
7514
7517
|
import { readFileSync as readFileSync6 } from "fs";
|
|
7515
7518
|
import { promisify as promisify2 } from "util";
|
|
7516
7519
|
function turnScopeUnit(turnId, attempt) {
|
|
@@ -7639,7 +7642,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7639
7642
|
command: options.command
|
|
7640
7643
|
});
|
|
7641
7644
|
return asContained(
|
|
7642
|
-
|
|
7645
|
+
spawn4("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
|
|
7643
7646
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
7644
7647
|
env: options.env,
|
|
7645
7648
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7647,7 +7650,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7647
7650
|
);
|
|
7648
7651
|
}
|
|
7649
7652
|
function spawnInGroup(options) {
|
|
7650
|
-
const child =
|
|
7653
|
+
const child = spawn4(options.command, options.args, {
|
|
7651
7654
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
7652
7655
|
env: options.env,
|
|
7653
7656
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -7729,7 +7732,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7729
7732
|
} catch {
|
|
7730
7733
|
}
|
|
7731
7734
|
}
|
|
7732
|
-
await
|
|
7735
|
+
await sleep3(fallbackGraceMs);
|
|
7733
7736
|
const afterTerm = processGroupMembers(pgid);
|
|
7734
7737
|
if (!afterTerm.ok) {
|
|
7735
7738
|
result.failures.push({
|
|
@@ -7792,7 +7795,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7792
7795
|
}
|
|
7793
7796
|
};
|
|
7794
7797
|
}
|
|
7795
|
-
var execFileAsync, asContained, CONTAINMENT_STOP_GRACE_SEC, FALLBACK_GRACE_MS, CAPABILITY_PROBE_TIMEOUT_MS, OOM_READ_TIMEOUT_MS,
|
|
7798
|
+
var execFileAsync, asContained, CONTAINMENT_STOP_GRACE_SEC, FALLBACK_GRACE_MS, CAPABILITY_PROBE_TIMEOUT_MS, OOM_READ_TIMEOUT_MS, sleep3, MANAGER_ENV_KEYS, managerEnvKey;
|
|
7796
7799
|
var init_turn_containment = __esm({
|
|
7797
7800
|
"src/turn-containment.ts"() {
|
|
7798
7801
|
"use strict";
|
|
@@ -7802,7 +7805,7 @@ var init_turn_containment = __esm({
|
|
|
7802
7805
|
FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
|
|
7803
7806
|
CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
|
|
7804
7807
|
OOM_READ_TIMEOUT_MS = 1500;
|
|
7805
|
-
|
|
7808
|
+
sleep3 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
7806
7809
|
MANAGER_ENV_KEYS = ["DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR", "PATH"];
|
|
7807
7810
|
managerEnvKey = (env) => MANAGER_ENV_KEYS.map((k) => `${k}=${env[k] ?? ""}`).join("\0");
|
|
7808
7811
|
}
|
|
@@ -8464,7 +8467,7 @@ var init_build_options = __esm({
|
|
|
8464
8467
|
import { readFile as readFile2 } from "fs/promises";
|
|
8465
8468
|
import { homedir as homedir7 } from "os";
|
|
8466
8469
|
import { join as join16 } from "path";
|
|
8467
|
-
import { parse } from "smol-toml";
|
|
8470
|
+
import { parse as parse2 } from "smol-toml";
|
|
8468
8471
|
function codexConfigPath(env = process.env, home = homedir7()) {
|
|
8469
8472
|
return join16(env.CODEX_HOME || join16(home, ".codex"), "config.toml");
|
|
8470
8473
|
}
|
|
@@ -8479,7 +8482,7 @@ async function readCodexConfigSetsSandboxMode(path = codexConfigPath()) {
|
|
|
8479
8482
|
}
|
|
8480
8483
|
function configSetsSandboxMode(text) {
|
|
8481
8484
|
try {
|
|
8482
|
-
return
|
|
8485
|
+
return parse2(text).sandbox_mode !== void 0;
|
|
8483
8486
|
} catch {
|
|
8484
8487
|
return false;
|
|
8485
8488
|
}
|
|
@@ -11729,7 +11732,6 @@ async function liveCompanionState() {
|
|
|
11729
11732
|
// src/commands/setup.ts
|
|
11730
11733
|
init_cabane_command();
|
|
11731
11734
|
init_config();
|
|
11732
|
-
import { hostname as hostname2 } from "os";
|
|
11733
11735
|
|
|
11734
11736
|
// src/logger.ts
|
|
11735
11737
|
init_config();
|
|
@@ -11904,61 +11906,346 @@ function getLogger() {
|
|
|
11904
11906
|
return cached;
|
|
11905
11907
|
}
|
|
11906
11908
|
|
|
11909
|
+
// src/pair-flow.ts
|
|
11910
|
+
init_cabane_command();
|
|
11911
|
+
import { spawn as spawn3 } from "child_process";
|
|
11912
|
+
import { hostname } from "os";
|
|
11913
|
+
|
|
11907
11914
|
// src/enrollment.ts
|
|
11908
11915
|
init_errors();
|
|
11909
|
-
|
|
11916
|
+
var EnrollmentExpiredError = class extends CompanionError {
|
|
11917
|
+
constructor() {
|
|
11918
|
+
super("this pairing code expired before it was confirmed.");
|
|
11919
|
+
this.name = "EnrollmentExpiredError";
|
|
11920
|
+
}
|
|
11921
|
+
};
|
|
11922
|
+
var EnrollmentCancelledError = class extends CompanionError {
|
|
11923
|
+
constructor() {
|
|
11924
|
+
super("pairing was cancelled.");
|
|
11925
|
+
this.name = "EnrollmentCancelledError";
|
|
11926
|
+
}
|
|
11927
|
+
};
|
|
11928
|
+
function trimBase(baseUrl) {
|
|
11929
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
11930
|
+
}
|
|
11931
|
+
async function postJson(baseUrl, path, body, signal) {
|
|
11910
11932
|
let res;
|
|
11911
11933
|
try {
|
|
11912
|
-
res = await fetch(`${baseUrl
|
|
11934
|
+
res = await fetch(`${trimBase(baseUrl)}${path}`, {
|
|
11913
11935
|
method: "POST",
|
|
11914
|
-
headers: {
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
},
|
|
11918
|
-
body: JSON.stringify({
|
|
11919
|
-
userCode: input.userCode,
|
|
11920
|
-
label: input.label,
|
|
11921
|
-
platform: process.platform,
|
|
11922
|
-
arch: process.arch,
|
|
11923
|
-
nodeMajor: Number.parseInt(process.versions.node.split(".")[0], 10)
|
|
11924
|
-
})
|
|
11936
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
11937
|
+
body: JSON.stringify(body),
|
|
11938
|
+
...signal ? { signal } : {}
|
|
11925
11939
|
});
|
|
11926
11940
|
} catch {
|
|
11941
|
+
if (signal?.aborted) throw new EnrollmentCancelledError();
|
|
11927
11942
|
throw new CompanionError(
|
|
11928
11943
|
`Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
|
|
11929
11944
|
);
|
|
11930
11945
|
}
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
throw new CompanionError(
|
|
11937
|
-
"The setup code or saved device credential is unavailable. If this device was removed, run cabane logout, then take a fresh setup line from your workspace\u2019s Settings \u2192 Devices."
|
|
11938
|
-
);
|
|
11939
|
-
if (res.status === 404 || res.status === 410)
|
|
11946
|
+
let raw;
|
|
11947
|
+
try {
|
|
11948
|
+
raw = await res.text();
|
|
11949
|
+
} catch {
|
|
11950
|
+
if (signal?.aborted) throw new EnrollmentCancelledError();
|
|
11940
11951
|
throw new CompanionError(
|
|
11941
|
-
|
|
11952
|
+
`Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
|
|
11942
11953
|
);
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
if (
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11954
|
+
}
|
|
11955
|
+
let parsed = void 0;
|
|
11956
|
+
if (raw.length > 0) {
|
|
11957
|
+
try {
|
|
11958
|
+
parsed = JSON.parse(raw);
|
|
11959
|
+
} catch {
|
|
11960
|
+
parsed = raw;
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
11963
|
+
if (res.status >= 400) {
|
|
11964
|
+
if (res.status === 429)
|
|
11965
|
+
throw new CompanionError(
|
|
11966
|
+
"Too many pairing requests from this network. Wait a few minutes and run cabane setup again."
|
|
11967
|
+
);
|
|
11968
|
+
if (res.status === 404 && path.endsWith("/code"))
|
|
11969
|
+
throw new CompanionError(
|
|
11970
|
+
`No pairing endpoint at ${baseUrl}. Either that server is too old for this companion, or it isn't a Cabane API origin: the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
|
|
11971
|
+
);
|
|
11972
|
+
const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
|
|
11973
|
+
throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
|
|
11974
|
+
}
|
|
11975
|
+
return parsed;
|
|
11976
|
+
}
|
|
11977
|
+
function requestEnrollmentCode(baseUrl, opts = {}) {
|
|
11978
|
+
return postJson(
|
|
11979
|
+
baseUrl,
|
|
11980
|
+
"/api/device-enrollment/code",
|
|
11981
|
+
{ ...opts.label ? { label: opts.label } : {}, ...hostFacts() },
|
|
11982
|
+
opts.signal
|
|
11983
|
+
);
|
|
11984
|
+
}
|
|
11985
|
+
function hostFacts() {
|
|
11986
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "", 10);
|
|
11949
11987
|
return {
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
..."workspace" in body && body.workspace && typeof body.workspace === "object" && "name" in body.workspace && typeof body.workspace.name === "string" && "slug" in body.workspace && typeof body.workspace.slug === "string" ? { workspace: { name: body.workspace.name, slug: body.workspace.slug } } : {},
|
|
11954
|
-
..."devicesUrl" in body && typeof body.devicesUrl === "string" ? { devicesUrl: body.devicesUrl } : {},
|
|
11955
|
-
..."deviceLabel" in body && typeof body.deviceLabel === "string" ? { deviceLabel: body.deviceLabel } : {}
|
|
11988
|
+
platform: process.platform,
|
|
11989
|
+
arch: process.arch,
|
|
11990
|
+
...Number.isInteger(nodeMajor) ? { nodeMajor } : {}
|
|
11956
11991
|
};
|
|
11957
11992
|
}
|
|
11958
11993
|
function deviceLabelFromHostname(hostname3) {
|
|
11959
11994
|
const trimmed = hostname3.trim().replace(/\.local$/i, "");
|
|
11960
11995
|
return trimmed.length ? trimmed.slice(0, 120) : void 0;
|
|
11961
11996
|
}
|
|
11997
|
+
function pollOnce(baseUrl, deviceCode, signal) {
|
|
11998
|
+
return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode }, signal);
|
|
11999
|
+
}
|
|
12000
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
12001
|
+
async function awaitEnrollment(baseUrl, code, opts = {}) {
|
|
12002
|
+
const now = opts.now ?? (() => Date.now());
|
|
12003
|
+
const wait = opts.sleepMs ?? sleep;
|
|
12004
|
+
const deadline = now() + code.expiresIn * 1e3;
|
|
12005
|
+
let intervalMs = Math.max(1, code.interval) * 1e3;
|
|
12006
|
+
while (now() < deadline) {
|
|
12007
|
+
await wait(intervalMs);
|
|
12008
|
+
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
12009
|
+
const res = await pollOnce(baseUrl, code.deviceCode, opts.signal);
|
|
12010
|
+
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
12011
|
+
if (res.status === "complete") {
|
|
12012
|
+
if (!res.deviceToken || !res.baseUrl)
|
|
12013
|
+
throw new CompanionError("Cabane reported the pairing complete but returned no token.");
|
|
12014
|
+
return {
|
|
12015
|
+
baseUrl: res.baseUrl,
|
|
12016
|
+
deviceToken: res.deviceToken,
|
|
12017
|
+
...res.deviceId ? { deviceId: res.deviceId } : {},
|
|
12018
|
+
...res.deviceLabel ? { deviceLabel: res.deviceLabel } : {}
|
|
12019
|
+
};
|
|
12020
|
+
}
|
|
12021
|
+
if (res.status === "expired") throw new EnrollmentExpiredError();
|
|
12022
|
+
if (res.status === "slow_down") intervalMs += 1e3;
|
|
12023
|
+
}
|
|
12024
|
+
throw new EnrollmentExpiredError();
|
|
12025
|
+
}
|
|
12026
|
+
|
|
12027
|
+
// src/terminal-copy.ts
|
|
12028
|
+
init_cabane_command();
|
|
12029
|
+
var UNPAIRED = `This device isn't paired yet. Run ${cabane("setup")} on it.`;
|
|
12030
|
+
var lease = DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS;
|
|
12031
|
+
function age(at, now) {
|
|
12032
|
+
const seconds = Math.max(0, Math.floor((now - Date.parse(at)) / 1e3));
|
|
12033
|
+
return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m` : `${Math.floor(seconds / 3600)}h`;
|
|
12034
|
+
}
|
|
12035
|
+
function deviceLine(f, now = Date.now()) {
|
|
12036
|
+
if (!f.running) return `not running \u2014 start it with ${cabane("start")}`;
|
|
12037
|
+
if (f.lastHeartbeatRefused)
|
|
12038
|
+
return `refused by the server \u2014 it may be deactivated; see ${f.devicesUrl}`;
|
|
12039
|
+
if (!f.lastHeartbeatAckAt) return "starting \u2014 hasn't checked in yet";
|
|
12040
|
+
const since = age(f.lastHeartbeatAckAt, now);
|
|
12041
|
+
if (now - Date.parse(f.lastHeartbeatAckAt) > lease)
|
|
12042
|
+
return `can't reach the server \u2014 last checked in ${since} ago`;
|
|
12043
|
+
const view = f.lastAcknowledgedDevice;
|
|
12044
|
+
if (!view) return `checked in ${since} ago`;
|
|
12045
|
+
const word = view.state === "online" && view.health ? view.health === "checking" ? "online, checking harnesses\u2026" : DEVICE_HEALTH_LABEL[view.health] : DEVICE_STATE_LABEL[view.state];
|
|
12046
|
+
return `${word.toLowerCase()} \u2014 checked in ${since} ago`;
|
|
12047
|
+
}
|
|
12048
|
+
function pairingLine(pair) {
|
|
12049
|
+
return `\u2713 Paired as "${pair.deviceLabel ?? "Computer"}".`;
|
|
12050
|
+
}
|
|
12051
|
+
function pathLines(reload) {
|
|
12052
|
+
return reload ? [` To use cabane in this window: ${reload}`, ""] : [];
|
|
12053
|
+
}
|
|
12054
|
+
function agentLines(detection, opts) {
|
|
12055
|
+
const rows = agentRows(detection, opts);
|
|
12056
|
+
const names = rows.map((row3) => row3.usable ? `\u2713 ${row3.label}` : row3.label);
|
|
12057
|
+
const width = Math.max(...names.map((name) => name.length)) + 3;
|
|
12058
|
+
return rows.map((row3, i) => `${names[i].padEnd(width)}${row3.text}`);
|
|
12059
|
+
}
|
|
12060
|
+
var modeLines = {
|
|
12061
|
+
background: "Cabane is running in the background",
|
|
12062
|
+
service: "Cabane is running as a service",
|
|
12063
|
+
attached: "Cabane is running in this process",
|
|
12064
|
+
existing: "Cabane was already running"
|
|
12065
|
+
};
|
|
12066
|
+
function closingBlock(input, now = Date.now()) {
|
|
12067
|
+
const { mode, facts } = input;
|
|
12068
|
+
const device = deviceLine(facts, now);
|
|
12069
|
+
const noAck = !facts.lastHeartbeatAckAt && !facts.lastHeartbeatRefused;
|
|
12070
|
+
const stale = facts.lastHeartbeatAckAt !== null && now - Date.parse(facts.lastHeartbeatAckAt) > lease;
|
|
12071
|
+
const modeLine = modeLines[mode];
|
|
12072
|
+
const sentence = noAck ? `${mode === "background" ? "Cabane started in the background" : modeLine} but hasn't checked in yet.` : `${modeLine}. ${facts.lastHeartbeatRefused || stale ? device[0].toUpperCase() + device.slice(1) : `This device ${device.startsWith("checked in") || device.startsWith("needs attention") ? "" : "is "}${device}`}.`;
|
|
12073
|
+
const lines2 = [` ${sentence}`, "", ...pathLines(input.reload)];
|
|
12074
|
+
if (noAck || stale || facts.lastHeartbeatRefused) {
|
|
12075
|
+
lines2.push(
|
|
12076
|
+
` See what it's doing: ${cabane("status")}${facts.lastHeartbeatRefused ? "" : ` Log: ${cabane("logs")}`}`
|
|
12077
|
+
);
|
|
12078
|
+
} else {
|
|
12079
|
+
lines2.push(" Harnesses on this device");
|
|
12080
|
+
lines2.push(
|
|
12081
|
+
...(input.detection ? agentLines(input.detection, { signInHint: true }) : ["Claude Code checking", "Codex checking", "OpenCode checking"]).map((line) => ` ${line}`)
|
|
12082
|
+
);
|
|
12083
|
+
lines2.push(
|
|
12084
|
+
"",
|
|
12085
|
+
` Status ${cabane("status")}${mode === "attached" ? "" : ` Stop ${cabane("stop")}`} Log ${cabane("logs")}`
|
|
12086
|
+
);
|
|
12087
|
+
if (!input.service && input.servicePlatform) {
|
|
12088
|
+
lines2.push(
|
|
12089
|
+
` Start at login and keep Cabane updating itself: ${cabane("service install")}${input.servicePlatform === "launchd" ? " (macOS will ask you to allow a background item)" : ""}`
|
|
12090
|
+
);
|
|
12091
|
+
lines2.push(
|
|
12092
|
+
` Updates itself only as a service; to turn that off: ${cabane("update --auto off")}`
|
|
12093
|
+
);
|
|
12094
|
+
} else if (input.service && input.autoUpdate)
|
|
12095
|
+
lines2.push(` Updating itself; to turn that off: ${cabane("update --auto off")}`);
|
|
12096
|
+
}
|
|
12097
|
+
if (input.address) lines2.push("", ` In Cabane: ${input.address}`);
|
|
12098
|
+
if (mode === "attached") lines2.push("", " Listening for messages\u2026");
|
|
12099
|
+
return lines2;
|
|
12100
|
+
}
|
|
12101
|
+
function startFailure(message2, reload) {
|
|
12102
|
+
return [
|
|
12103
|
+
...pathLines(reload),
|
|
12104
|
+
`\u2717 Cabane couldn't start: ${message2}`,
|
|
12105
|
+
"",
|
|
12106
|
+
` Log: ${cabane("logs")} Try again: ${cabane("start")}`
|
|
12107
|
+
];
|
|
12108
|
+
}
|
|
12109
|
+
|
|
12110
|
+
// src/pair-flow.ts
|
|
12111
|
+
var MAX_CODES = 3;
|
|
12112
|
+
async function pairDevice(opts, deps = {}) {
|
|
12113
|
+
const requestCode = deps.requestCode ?? requestEnrollmentCode;
|
|
12114
|
+
const wait = deps.await ?? awaitEnrollment;
|
|
12115
|
+
const open2 = deps.open ?? openLink;
|
|
12116
|
+
const { ui, interactive } = opts;
|
|
12117
|
+
const label = deviceLabelFromHostname(hostname());
|
|
12118
|
+
const aborter = new AbortController();
|
|
12119
|
+
const onSigint = () => aborter.abort();
|
|
12120
|
+
process.on("SIGINT", onSigint);
|
|
12121
|
+
const spinner = ui.spinner();
|
|
12122
|
+
let current = null;
|
|
12123
|
+
const keys = interactive ? watchReturn({
|
|
12124
|
+
onReturn: () => {
|
|
12125
|
+
if (!current) return;
|
|
12126
|
+
const link = current.verificationUriComplete;
|
|
12127
|
+
void open2(link).then((opened) => {
|
|
12128
|
+
if (!opened)
|
|
12129
|
+
spinner.message(
|
|
12130
|
+
`Couldn't open a browser here. Open this page on any device: ${link}`
|
|
12131
|
+
);
|
|
12132
|
+
});
|
|
12133
|
+
},
|
|
12134
|
+
onInterrupt: () => aborter.abort(),
|
|
12135
|
+
...deps.input ? { input: deps.input } : {}
|
|
12136
|
+
}) : null;
|
|
12137
|
+
let minutesWaited = 0;
|
|
12138
|
+
let waiting = false;
|
|
12139
|
+
try {
|
|
12140
|
+
for (let attempt = 1; attempt <= MAX_CODES; attempt += 1) {
|
|
12141
|
+
let code;
|
|
12142
|
+
try {
|
|
12143
|
+
code = await requestCode(opts.baseUrl, {
|
|
12144
|
+
...label ? { label } : {},
|
|
12145
|
+
signal: aborter.signal
|
|
12146
|
+
});
|
|
12147
|
+
} catch (err) {
|
|
12148
|
+
if (!(err instanceof EnrollmentCancelledError)) throw err;
|
|
12149
|
+
ui.cancel(`Pairing cancelled. Run ${cabane("setup")} when you're ready.`);
|
|
12150
|
+
process.exitCode = 130;
|
|
12151
|
+
return null;
|
|
12152
|
+
}
|
|
12153
|
+
current = code;
|
|
12154
|
+
const minutes = Math.max(1, Math.round(code.expiresIn / 60));
|
|
12155
|
+
const codeLines = ["", ` ${code.userCode}`, ` ${code.verificationUriComplete}`, ""];
|
|
12156
|
+
if (attempt === 1) {
|
|
12157
|
+
ui.step([
|
|
12158
|
+
"Pair this device",
|
|
12159
|
+
interactive ? "Enter this code in Cabane, or press Return to open the page:" : "Enter this code in Cabane:",
|
|
12160
|
+
...codeLines
|
|
12161
|
+
]);
|
|
12162
|
+
} else {
|
|
12163
|
+
ui.step(codeLines.slice(1));
|
|
12164
|
+
}
|
|
12165
|
+
spinner.start(
|
|
12166
|
+
`Waiting for you to confirm in Cabane\u2026 (the code is good for ${minutes} minutes)`
|
|
12167
|
+
);
|
|
12168
|
+
waiting = true;
|
|
12169
|
+
try {
|
|
12170
|
+
const device = await wait(opts.baseUrl, code, { signal: aborter.signal });
|
|
12171
|
+
waiting = false;
|
|
12172
|
+
spinner.stop(pairingLine(device));
|
|
12173
|
+
return device;
|
|
12174
|
+
} catch (err) {
|
|
12175
|
+
waiting = false;
|
|
12176
|
+
if (err instanceof EnrollmentCancelledError) {
|
|
12177
|
+
spinner.stop("Pairing stopped.");
|
|
12178
|
+
ui.cancel(`Pairing cancelled. Run ${cabane("setup")} when you're ready.`);
|
|
12179
|
+
process.exitCode = 130;
|
|
12180
|
+
return null;
|
|
12181
|
+
}
|
|
12182
|
+
if (!(err instanceof EnrollmentExpiredError)) {
|
|
12183
|
+
spinner.stop("Pairing stopped.");
|
|
12184
|
+
throw err;
|
|
12185
|
+
}
|
|
12186
|
+
minutesWaited += minutes;
|
|
12187
|
+
if (attempt < MAX_CODES) {
|
|
12188
|
+
spinner.stop("That code expired. Here's a fresh one:");
|
|
12189
|
+
continue;
|
|
12190
|
+
}
|
|
12191
|
+
spinner.stop(
|
|
12192
|
+
`Nothing confirmed in ${minutesWaited} minutes. Run ${cabane("setup")} when you're ready.`
|
|
12193
|
+
);
|
|
12194
|
+
process.exitCode = 1;
|
|
12195
|
+
return null;
|
|
12196
|
+
}
|
|
12197
|
+
}
|
|
12198
|
+
return null;
|
|
12199
|
+
} finally {
|
|
12200
|
+
if (waiting) spinner.stop("Pairing stopped.");
|
|
12201
|
+
keys?.release();
|
|
12202
|
+
process.removeListener("SIGINT", onSigint);
|
|
12203
|
+
}
|
|
12204
|
+
}
|
|
12205
|
+
function watchReturn(handlers) {
|
|
12206
|
+
const input = handlers.input ?? process.stdin;
|
|
12207
|
+
if (!input.isTTY || typeof input.setRawMode !== "function") return { release: () => {
|
|
12208
|
+
} };
|
|
12209
|
+
const wasRaw = input.isRaw;
|
|
12210
|
+
const onData = (chunk) => {
|
|
12211
|
+
const text = chunk.toString();
|
|
12212
|
+
if (text.includes("")) handlers.onInterrupt();
|
|
12213
|
+
else if (text.includes("\r") || text.includes("\n")) handlers.onReturn();
|
|
12214
|
+
};
|
|
12215
|
+
input.setRawMode(true);
|
|
12216
|
+
input.on("data", onData);
|
|
12217
|
+
input.resume();
|
|
12218
|
+
return {
|
|
12219
|
+
release: () => {
|
|
12220
|
+
input.removeListener("data", onData);
|
|
12221
|
+
input.setRawMode(wasRaw);
|
|
12222
|
+
input.pause();
|
|
12223
|
+
}
|
|
12224
|
+
};
|
|
12225
|
+
}
|
|
12226
|
+
function pageOpener(platform, link) {
|
|
12227
|
+
if (platform === "darwin") return { command: "open", args: [link] };
|
|
12228
|
+
if (platform === "win32") return { command: "cmd", args: ["/c", "start", '""', `"${link}"`] };
|
|
12229
|
+
return { command: "xdg-open", args: [link] };
|
|
12230
|
+
}
|
|
12231
|
+
function openLink(link) {
|
|
12232
|
+
return new Promise((resolve2) => {
|
|
12233
|
+
const { command, args } = pageOpener(process.platform, link);
|
|
12234
|
+
try {
|
|
12235
|
+
const child = spawn3(command, args, {
|
|
12236
|
+
stdio: "ignore",
|
|
12237
|
+
detached: true,
|
|
12238
|
+
// `cmd` parses its own line: the quoting above is the one it needs, as written.
|
|
12239
|
+
...process.platform === "win32" ? { windowsVerbatimArguments: true } : {}
|
|
12240
|
+
});
|
|
12241
|
+
child.once("error", () => resolve2(false));
|
|
12242
|
+
child.once("exit", (code) => resolve2(code === 0));
|
|
12243
|
+
child.unref();
|
|
12244
|
+
} catch {
|
|
12245
|
+
resolve2(false);
|
|
12246
|
+
}
|
|
12247
|
+
});
|
|
12248
|
+
}
|
|
11962
12249
|
|
|
11963
12250
|
// src/commands/start.ts
|
|
11964
12251
|
init_cabane_command();
|
|
@@ -12501,7 +12788,7 @@ var emptyHeartbeatStatus = () => ({
|
|
|
12501
12788
|
|
|
12502
12789
|
// src/supervisor.ts
|
|
12503
12790
|
init_cabane_command();
|
|
12504
|
-
import { hostname } from "os";
|
|
12791
|
+
import { hostname as hostname2 } from "os";
|
|
12505
12792
|
|
|
12506
12793
|
// src/log-copy.ts
|
|
12507
12794
|
init_failure();
|
|
@@ -12971,6 +13258,32 @@ init_boot_id();
|
|
|
12971
13258
|
init_turn_containment();
|
|
12972
13259
|
init_protocol();
|
|
12973
13260
|
import { createInterface } from "readline";
|
|
13261
|
+
|
|
13262
|
+
// src/credential-broker/client-config.ts
|
|
13263
|
+
import { parse, stringify } from "smol-toml";
|
|
13264
|
+
var BROKERED_CREDENTIAL_NAMES = /* @__PURE__ */ new Set([
|
|
13265
|
+
"ANTHROPIC_API_KEY",
|
|
13266
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
13267
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
13268
|
+
"CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
|
|
13269
|
+
"CLAUDE_CODE_SESSION_ACCESS_TOKEN",
|
|
13270
|
+
"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
|
|
13271
|
+
"CLAUDE_CODE_HOST_CREDS_FILE",
|
|
13272
|
+
"CODEX_API_KEY",
|
|
13273
|
+
"OPENAI_API_KEY",
|
|
13274
|
+
"OPENAI_ACCESS_TOKEN",
|
|
13275
|
+
"GH_TOKEN",
|
|
13276
|
+
"GITHUB_TOKEN",
|
|
13277
|
+
"GH_ENTERPRISE_TOKEN",
|
|
13278
|
+
"GITHUB_ENTERPRISE_TOKEN"
|
|
13279
|
+
]);
|
|
13280
|
+
function withoutBrokeredCredentials(values) {
|
|
13281
|
+
return Object.fromEntries(
|
|
13282
|
+
Object.entries(values).filter(([name]) => !BROKERED_CREDENTIAL_NAMES.has(name))
|
|
13283
|
+
);
|
|
13284
|
+
}
|
|
13285
|
+
|
|
13286
|
+
// src/runner/launch.ts
|
|
12974
13287
|
function gzipBodiesSignal() {
|
|
12975
13288
|
let on = false;
|
|
12976
13289
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -13233,12 +13546,22 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
13233
13546
|
}
|
|
13234
13547
|
try {
|
|
13235
13548
|
const wrapped = launch.wrapped === true;
|
|
13236
|
-
|
|
13237
|
-
|
|
13549
|
+
if (launch.brokeredCredentials && !wrapped)
|
|
13550
|
+
throw new Error("credential broker requires a wrapped runner");
|
|
13551
|
+
const inherited = runnerExecEnv(launch.env ?? process.env, wrapped);
|
|
13552
|
+
const env = launch.brokeredCredentials ? withoutBrokeredCredentials(inherited) : inherited;
|
|
13553
|
+
const directGrants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
|
|
13554
|
+
const grants = launch.brokeredCredentials ? Object.fromEntries(
|
|
13555
|
+
Object.entries(directGrants).map(([key, values]) => [
|
|
13556
|
+
key,
|
|
13557
|
+
withoutBrokeredCredentials(values)
|
|
13558
|
+
])
|
|
13559
|
+
) : directGrants;
|
|
13560
|
+
const secrets = wrapped ? launch.secrets?.() ?? {} : void 0;
|
|
13238
13561
|
const input = {
|
|
13239
13562
|
baseUrl: opts.baseUrl,
|
|
13240
13563
|
credential: minted.credential,
|
|
13241
|
-
...
|
|
13564
|
+
...secrets ? { secrets: launch.brokeredCredentials ? withoutBrokeredCredentials(secrets) : secrets } : {},
|
|
13242
13565
|
...Object.keys(grants).length > 0 ? { grants } : {},
|
|
13243
13566
|
dispatch: payload,
|
|
13244
13567
|
turnId,
|
|
@@ -13541,7 +13864,7 @@ function hostReport() {
|
|
|
13541
13864
|
}
|
|
13542
13865
|
|
|
13543
13866
|
// src/providers/local-process.ts
|
|
13544
|
-
import { spawn as
|
|
13867
|
+
import { spawn as spawn5 } from "child_process";
|
|
13545
13868
|
import { constants as constants2 } from "os";
|
|
13546
13869
|
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
13547
13870
|
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
@@ -13552,7 +13875,7 @@ function exitCode(code, signal) {
|
|
|
13552
13875
|
}
|
|
13553
13876
|
function createLocalProcessProvider(opts = {}) {
|
|
13554
13877
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
13555
|
-
const spawnFn = opts.spawnFn ??
|
|
13878
|
+
const spawnFn = opts.spawnFn ?? spawn5;
|
|
13556
13879
|
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
13557
13880
|
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
13558
13881
|
const groupAlive = (pgid) => {
|
|
@@ -13928,7 +14251,7 @@ var SseSubscriber = class {
|
|
|
13928
14251
|
init_transcript_writer();
|
|
13929
14252
|
|
|
13930
14253
|
// src/updater.ts
|
|
13931
|
-
import { spawn as
|
|
14254
|
+
import { spawn as spawn6 } from "child_process";
|
|
13932
14255
|
import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
|
|
13933
14256
|
import { dirname as dirname11, join as join26 } from "path";
|
|
13934
14257
|
|
|
@@ -14143,7 +14466,7 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
|
14143
14466
|
let stderr = "";
|
|
14144
14467
|
let child;
|
|
14145
14468
|
try {
|
|
14146
|
-
child =
|
|
14469
|
+
child = spawn6(command, [...args], {
|
|
14147
14470
|
stdio: ["ignore", "pipe", "pipe"],
|
|
14148
14471
|
env: opts.env ?? process.env
|
|
14149
14472
|
});
|
|
@@ -14647,7 +14970,7 @@ var CompanionSupervisor = class {
|
|
|
14647
14970
|
if (this.stopped) return;
|
|
14648
14971
|
this.log.info(
|
|
14649
14972
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
14650
|
-
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ??
|
|
14973
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
|
|
14651
14974
|
);
|
|
14652
14975
|
const retired = retiredConfigKeysOnDisk();
|
|
14653
14976
|
if (retired.length > 0) {
|
|
@@ -14682,9 +15005,7 @@ var CompanionSupervisor = class {
|
|
|
14682
15005
|
this.detectTimer = setInterval(() => void this.detect(), DETECT_INTERVAL_MS);
|
|
14683
15006
|
this.detectTimer.unref?.();
|
|
14684
15007
|
if (!this.deviceApi || !this.config.deviceToken) {
|
|
14685
|
-
this.log.warn(
|
|
14686
|
-
"This device isn't paired. Get a setup line from a workspace's Settings \u2192 Devices."
|
|
14687
|
-
);
|
|
15008
|
+
this.log.warn(`This device isn't paired. Run ${cabane("setup")} on it.`);
|
|
14688
15009
|
return;
|
|
14689
15010
|
}
|
|
14690
15011
|
this.deviceSub = new SseSubscriber({
|
|
@@ -14695,7 +15016,7 @@ var CompanionSupervisor = class {
|
|
|
14695
15016
|
log: this.log,
|
|
14696
15017
|
lastEventId: null,
|
|
14697
15018
|
onOpen: () => {
|
|
14698
|
-
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ??
|
|
15019
|
+
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`);
|
|
14699
15020
|
this.deviceConnected = true;
|
|
14700
15021
|
for (const wr of this.workspaces.values()) {
|
|
14701
15022
|
this.hub.setConnected(wr.workspaceId, true);
|
|
@@ -15244,6 +15565,7 @@ var CompanionSupervisor = class {
|
|
|
15244
15565
|
),
|
|
15245
15566
|
...this.config.runner?.command ? {
|
|
15246
15567
|
wrapped: true,
|
|
15568
|
+
brokeredCredentials: this.config.runner.brokeredCredentials,
|
|
15247
15569
|
secrets: () => loadSecretValuesTolerant(
|
|
15248
15570
|
(m) => this.log.warn({ err: m }, "Could not read the secrets file for a wrapped turn.")
|
|
15249
15571
|
)
|
|
@@ -15928,9 +16250,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
15928
16250
|
}
|
|
15929
16251
|
function defaultReexec() {
|
|
15930
16252
|
clearRuntimeState();
|
|
15931
|
-
void import("child_process").then(({ spawn:
|
|
16253
|
+
void import("child_process").then(({ spawn: spawn8 }) => {
|
|
15932
16254
|
try {
|
|
15933
|
-
const child =
|
|
16255
|
+
const child = spawn8(process.execPath, process.argv.slice(1), {
|
|
15934
16256
|
stdio: "inherit",
|
|
15935
16257
|
detached: false
|
|
15936
16258
|
});
|
|
@@ -16279,13 +16601,13 @@ async function observeStart(deps = {}) {
|
|
|
16279
16601
|
const read2 = deps.readState ?? readLiveRuntimeState;
|
|
16280
16602
|
const verify = deps.verify ?? verifyRuntime;
|
|
16281
16603
|
const now = deps.now ?? Date.now;
|
|
16282
|
-
const
|
|
16604
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
16283
16605
|
const deadline = now() + 8e3;
|
|
16284
16606
|
for (; ; ) {
|
|
16285
16607
|
const state = await read2();
|
|
16286
16608
|
if (state?.socket && await verify(state) === "ours") return state;
|
|
16287
16609
|
if (now() >= deadline) return null;
|
|
16288
|
-
await
|
|
16610
|
+
await sleep4(Math.min(150, Math.max(0, deadline - now())));
|
|
16289
16611
|
}
|
|
16290
16612
|
}
|
|
16291
16613
|
function markLaunchRequester() {
|
|
@@ -16703,7 +17025,7 @@ async function controlService(action, deps) {
|
|
|
16703
17025
|
init_cabane_command();
|
|
16704
17026
|
init_cli_entry();
|
|
16705
17027
|
init_config();
|
|
16706
|
-
import { spawn as
|
|
17028
|
+
import { spawn as spawn7 } from "child_process";
|
|
16707
17029
|
import { closeSync as closeSync3, mkdirSync as mkdirSync17, openSync as openSync4 } from "fs";
|
|
16708
17030
|
init_config();
|
|
16709
17031
|
async function startDaemon(opts = {}, deps = {}) {
|
|
@@ -16785,7 +17107,7 @@ function defaultSpawnDetached(args) {
|
|
|
16785
17107
|
mkdirSync17(cabaneDir(), { recursive: true });
|
|
16786
17108
|
const logFd = openSync4(companionLogPath(), "a");
|
|
16787
17109
|
try {
|
|
16788
|
-
const child =
|
|
17110
|
+
const child = spawn7(process.execPath, [cliPath, ...args], {
|
|
16789
17111
|
detached: true,
|
|
16790
17112
|
stdio: ["ignore", logFd, logFd],
|
|
16791
17113
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -16803,89 +17125,6 @@ function defaultSpawnDetached(args) {
|
|
|
16803
17125
|
}
|
|
16804
17126
|
}
|
|
16805
17127
|
|
|
16806
|
-
// src/terminal-copy.ts
|
|
16807
|
-
init_cabane_command();
|
|
16808
|
-
var UNPAIRED = "This device isn't paired yet. Get a setup line from a workspace's Settings \u2192 Devices and paste it here.";
|
|
16809
|
-
var lease = DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS;
|
|
16810
|
-
function age(at, now) {
|
|
16811
|
-
const seconds = Math.max(0, Math.floor((now - Date.parse(at)) / 1e3));
|
|
16812
|
-
return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m` : `${Math.floor(seconds / 3600)}h`;
|
|
16813
|
-
}
|
|
16814
|
-
function deviceLine(f, now = Date.now()) {
|
|
16815
|
-
if (!f.running) return `not running \u2014 start it with ${cabane("start")}`;
|
|
16816
|
-
if (f.lastHeartbeatRefused)
|
|
16817
|
-
return `refused by the server \u2014 it may be deactivated; see ${f.devicesUrl}`;
|
|
16818
|
-
if (!f.lastHeartbeatAckAt) return "starting \u2014 hasn't checked in yet";
|
|
16819
|
-
const since = age(f.lastHeartbeatAckAt, now);
|
|
16820
|
-
if (now - Date.parse(f.lastHeartbeatAckAt) > lease)
|
|
16821
|
-
return `can't reach the server \u2014 last checked in ${since} ago`;
|
|
16822
|
-
const view = f.lastAcknowledgedDevice;
|
|
16823
|
-
if (!view) return `checked in ${since} ago`;
|
|
16824
|
-
const word = view.state === "online" && view.health ? view.health === "checking" ? "online, checking harnesses\u2026" : DEVICE_HEALTH_LABEL[view.health] : DEVICE_STATE_LABEL[view.state];
|
|
16825
|
-
return `${word.toLowerCase()} \u2014 checked in ${since} ago`;
|
|
16826
|
-
}
|
|
16827
|
-
function pairingLine(pair) {
|
|
16828
|
-
return `\u2713 Paired as "${pair.deviceLabel ?? "Computer"}" with ${pair.workspace?.name ?? "the workspace"}.`;
|
|
16829
|
-
}
|
|
16830
|
-
function pathLines(reload) {
|
|
16831
|
-
return reload ? [` To use cabane in this window: ${reload}`, ""] : [];
|
|
16832
|
-
}
|
|
16833
|
-
function agentLines(detection, opts) {
|
|
16834
|
-
const rows = agentRows(detection, opts);
|
|
16835
|
-
const names = rows.map((row3) => row3.usable ? `\u2713 ${row3.label}` : row3.label);
|
|
16836
|
-
const width = Math.max(...names.map((name) => name.length)) + 3;
|
|
16837
|
-
return rows.map((row3, i) => `${names[i].padEnd(width)}${row3.text}`);
|
|
16838
|
-
}
|
|
16839
|
-
var modeLines = {
|
|
16840
|
-
background: "Cabane is running in the background",
|
|
16841
|
-
service: "Cabane is running as a service",
|
|
16842
|
-
attached: "Cabane is running in this process",
|
|
16843
|
-
existing: "Cabane was already running"
|
|
16844
|
-
};
|
|
16845
|
-
function closingBlock(input, now = Date.now()) {
|
|
16846
|
-
const { mode, facts } = input;
|
|
16847
|
-
const device = deviceLine(facts, now);
|
|
16848
|
-
const noAck = !facts.lastHeartbeatAckAt && !facts.lastHeartbeatRefused;
|
|
16849
|
-
const stale = facts.lastHeartbeatAckAt !== null && now - Date.parse(facts.lastHeartbeatAckAt) > lease;
|
|
16850
|
-
const modeLine = modeLines[mode];
|
|
16851
|
-
const sentence = noAck ? `${mode === "background" ? "Cabane started in the background" : modeLine} but hasn't checked in yet.` : `${modeLine}. ${facts.lastHeartbeatRefused || stale ? device[0].toUpperCase() + device.slice(1) : `This device ${device.startsWith("checked in") || device.startsWith("needs attention") ? "" : "is "}${device}`}.`;
|
|
16852
|
-
const lines2 = [` ${sentence}`, "", ...pathLines(input.reload)];
|
|
16853
|
-
if (noAck || stale || facts.lastHeartbeatRefused) {
|
|
16854
|
-
lines2.push(
|
|
16855
|
-
` See what it's doing: ${cabane("status")}${facts.lastHeartbeatRefused ? "" : ` Log: ${cabane("logs")}`}`
|
|
16856
|
-
);
|
|
16857
|
-
} else {
|
|
16858
|
-
lines2.push(" Harnesses on this device");
|
|
16859
|
-
lines2.push(
|
|
16860
|
-
...(input.detection ? agentLines(input.detection, { signInHint: true }) : ["Claude Code checking", "Codex checking", "OpenCode checking"]).map((line) => ` ${line}`)
|
|
16861
|
-
);
|
|
16862
|
-
lines2.push(
|
|
16863
|
-
"",
|
|
16864
|
-
` Status ${cabane("status")}${mode === "attached" ? "" : ` Stop ${cabane("stop")}`} Log ${cabane("logs")}`
|
|
16865
|
-
);
|
|
16866
|
-
if (!input.service && input.servicePlatform) {
|
|
16867
|
-
lines2.push(
|
|
16868
|
-
` Start at login and keep Cabane updating itself: ${cabane("service install")}${input.servicePlatform === "launchd" ? " (macOS will ask you to allow a background item)" : ""}`
|
|
16869
|
-
);
|
|
16870
|
-
lines2.push(
|
|
16871
|
-
` Updates itself only as a service; to turn that off: ${cabane("update --auto off")}`
|
|
16872
|
-
);
|
|
16873
|
-
} else if (input.service && input.autoUpdate)
|
|
16874
|
-
lines2.push(` Updating itself; to turn that off: ${cabane("update --auto off")}`);
|
|
16875
|
-
}
|
|
16876
|
-
if (input.address) lines2.push("", ` In Cabane: ${input.address}`);
|
|
16877
|
-
if (mode === "attached") lines2.push("", " Listening for messages\u2026");
|
|
16878
|
-
return lines2;
|
|
16879
|
-
}
|
|
16880
|
-
function startFailure(message2, reload) {
|
|
16881
|
-
return [
|
|
16882
|
-
...pathLines(reload),
|
|
16883
|
-
`\u2717 Cabane couldn't start: ${message2}`,
|
|
16884
|
-
"",
|
|
16885
|
-
` Log: ${cabane("logs")} Try again: ${cabane("start")}`
|
|
16886
|
-
];
|
|
16887
|
-
}
|
|
16888
|
-
|
|
16889
17128
|
// src/terminal-status.ts
|
|
16890
17129
|
async function readTerminalStatus(socket, timeoutMs = 1e3) {
|
|
16891
17130
|
if (!socket) return emptyHeartbeatStatus();
|
|
@@ -17024,18 +17263,17 @@ async function startScript(opts, deps) {
|
|
|
17024
17263
|
if (service2.state !== "running" || service2.pid !== current.pid) mode = "existing";
|
|
17025
17264
|
}
|
|
17026
17265
|
}
|
|
17027
|
-
const
|
|
17028
|
-
const address = opts.pairing ? opts.pairing.devicesUrl : personalUrl;
|
|
17266
|
+
const address = `${cfg.baseUrl.replace(/\/$/, "")}/settings/devices`;
|
|
17029
17267
|
if (!daemon)
|
|
17030
17268
|
for (const line of closingBlock({
|
|
17031
17269
|
mode,
|
|
17032
|
-
facts: { ...status2, running: true, devicesUrl: address
|
|
17270
|
+
facts: { ...status2, running: true, devicesUrl: address },
|
|
17033
17271
|
detection,
|
|
17034
17272
|
reload: reloadLine(),
|
|
17035
17273
|
service: installed,
|
|
17036
17274
|
servicePlatform: servicePlatform(),
|
|
17037
17275
|
autoUpdate: cfg.update?.auto !== false,
|
|
17038
|
-
|
|
17276
|
+
address
|
|
17039
17277
|
}))
|
|
17040
17278
|
write(line);
|
|
17041
17279
|
if (runtime) {
|
|
@@ -17171,22 +17409,22 @@ async function setup(opts = {}, deps = {}) {
|
|
|
17171
17409
|
} catch (err) {
|
|
17172
17410
|
for (const line of pathLines(reloadLine())) write(line);
|
|
17173
17411
|
write(err instanceof Error ? err.message : String(err));
|
|
17174
|
-
write(
|
|
17175
|
-
"Check the server connection, then take a fresh setup line from the workspace's Settings \u2192 Devices."
|
|
17176
|
-
);
|
|
17412
|
+
write(`Check the server connection, then run ${cabane("setup")} again.`);
|
|
17177
17413
|
process.exitCode = 1;
|
|
17178
17414
|
} finally {
|
|
17179
17415
|
setConsoleLogging(true);
|
|
17180
17416
|
}
|
|
17181
17417
|
}
|
|
17182
17418
|
async function setupScript(opts, deps) {
|
|
17183
|
-
|
|
17184
|
-
|
|
17185
|
-
|
|
17186
|
-
|
|
17419
|
+
if (opts.code) {
|
|
17420
|
+
write(
|
|
17421
|
+
`Setup codes come from this device now: run ${cabane("setup")} and enter the code it shows in Cabane.`
|
|
17422
|
+
);
|
|
17187
17423
|
process.exitCode = 1;
|
|
17188
17424
|
return;
|
|
17189
17425
|
}
|
|
17426
|
+
const interactive = deps.interactive ?? isInteractive();
|
|
17427
|
+
const paired = isDevicePaired();
|
|
17190
17428
|
const ui = deps.ui ?? (interactive ? terminalUi() : plainUi());
|
|
17191
17429
|
const running = await (deps.live ?? liveCompanionState)();
|
|
17192
17430
|
const cfg = safeLoadConfig();
|
|
@@ -17197,32 +17435,16 @@ async function setupScript(opts, deps) {
|
|
|
17197
17435
|
process.exitCode = 1;
|
|
17198
17436
|
return;
|
|
17199
17437
|
}
|
|
17200
|
-
let
|
|
17201
|
-
if (
|
|
17438
|
+
let justPaired = false;
|
|
17439
|
+
if (!paired) {
|
|
17202
17440
|
const baseUrl = resolvePairBaseUrl(opts.server ?? cfg?.baseUrl);
|
|
17203
|
-
|
|
17204
|
-
|
|
17205
|
-
write(
|
|
17206
|
-
"This device is paired to another Cabane server. Log out before setting it up on a different server."
|
|
17207
|
-
);
|
|
17208
|
-
write(`Run ${cabane("logout")} before using the new setup line.`);
|
|
17209
|
-
process.exitCode = 1;
|
|
17210
|
-
return;
|
|
17211
|
-
}
|
|
17212
|
-
device = await (deps.claim ?? claimSetupCode)(baseUrl, {
|
|
17213
|
-
userCode: opts.code,
|
|
17214
|
-
label: deviceLabelFromHostname(hostname2()) ?? "Computer",
|
|
17215
|
-
...paired && cfg?.deviceToken ? { deviceToken: cfg.deviceToken } : {}
|
|
17216
|
-
});
|
|
17441
|
+
const device = await (deps.pair ?? pairDevice)({ baseUrl, interactive, ui });
|
|
17442
|
+
if (!device) return;
|
|
17217
17443
|
writePairedConfig(device);
|
|
17218
|
-
|
|
17444
|
+
justPaired = true;
|
|
17219
17445
|
}
|
|
17220
17446
|
const detection = await start(
|
|
17221
|
-
{
|
|
17222
|
-
...opts,
|
|
17223
|
-
...device ? { pairing: device } : {},
|
|
17224
|
-
...!interactive && device ? { foreground: true } : {}
|
|
17225
|
-
},
|
|
17447
|
+
{ ...opts, ...!interactive && justPaired ? { foreground: true } : {} },
|
|
17226
17448
|
{ ...deps, interactive }
|
|
17227
17449
|
);
|
|
17228
17450
|
if (interactive && detection && !anyUsable(detection)) {
|
|
@@ -17443,7 +17665,7 @@ async function stop(deps = {}) {
|
|
|
17443
17665
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
17444
17666
|
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
17445
17667
|
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
17446
|
-
const
|
|
17668
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
17447
17669
|
const now = deps.now ?? (() => Date.now());
|
|
17448
17670
|
const state = readState();
|
|
17449
17671
|
if (!state) {
|
|
@@ -17471,7 +17693,7 @@ async function stop(deps = {}) {
|
|
|
17471
17693
|
process.stdout.write("companion: stopped.\n");
|
|
17472
17694
|
return;
|
|
17473
17695
|
}
|
|
17474
|
-
await
|
|
17696
|
+
await sleep4(POLL_INTERVAL_MS);
|
|
17475
17697
|
}
|
|
17476
17698
|
process.stdout.write(
|
|
17477
17699
|
`companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
|
|
@@ -17497,7 +17719,7 @@ function isAlive(kill, pid) {
|
|
|
17497
17719
|
var STARTUP_WAIT_MS = 2e4;
|
|
17498
17720
|
async function installAndStartService(deps = {}) {
|
|
17499
17721
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
17500
|
-
const
|
|
17722
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
17501
17723
|
const now = deps.now ?? Date.now;
|
|
17502
17724
|
const before = await serviceState(deps);
|
|
17503
17725
|
const running = readState();
|
|
@@ -17513,7 +17735,7 @@ async function installAndStartService(deps = {}) {
|
|
|
17513
17735
|
while (now() < deadline) {
|
|
17514
17736
|
const state = readState();
|
|
17515
17737
|
if (state?.socket) return true;
|
|
17516
|
-
await
|
|
17738
|
+
await sleep4(250);
|
|
17517
17739
|
}
|
|
17518
17740
|
write(
|
|
17519
17741
|
`${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
|
|
@@ -17643,9 +17865,7 @@ async function status(deps = {}) {
|
|
|
17643
17865
|
cfg = null;
|
|
17644
17866
|
}
|
|
17645
17867
|
if (!cfg || !cfg.deviceToken) {
|
|
17646
|
-
out(
|
|
17647
|
-
" Not paired yet. Get a setup line from a workspace's Settings \u2192 Devices and paste it here."
|
|
17648
|
-
);
|
|
17868
|
+
out(` Not paired yet. Run ${cabane("setup")} on this device.`);
|
|
17649
17869
|
for (const line of strays) out(line);
|
|
17650
17870
|
process.exitCode = 1;
|
|
17651
17871
|
return;
|
|
@@ -18308,7 +18528,7 @@ init_cabane_command();
|
|
|
18308
18528
|
rememberInvokedCommand();
|
|
18309
18529
|
var program = new Command();
|
|
18310
18530
|
program.name("cabane").description("Run your Cabane agents on this device, with Claude Code, Codex or OpenCode.").version(COMPANION_VERSION);
|
|
18311
|
-
program.command("setup
|
|
18531
|
+
program.command("setup").argument("[code]", "not taken: run cabane setup and enter the code it shows in Cabane").description("set up this device: find your agents, pair it with Cabane, and keep it running.").allowExcessArguments(false).option("--server <url>", "the Cabane instance to pair with (default https://app.cabane.ai)").addOption(
|
|
18312
18532
|
new Option("--log-level <level>", "info (default), warn, or debug").choices([
|
|
18313
18533
|
"info",
|
|
18314
18534
|
"warn",
|