@centralu/host-linux-arm64 0.1.0-beta.37 → 0.1.0-beta.38
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/host/bundle-info.json +2 -2
- package/host/centralu-package.json +5 -5
- package/host/main.mjs +146 -51
- package/host/remote-install.mjs +16 -4
- package/package.json +1 -1
package/host/bundle-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
2
|
+
"commit": "bab6469",
|
|
3
3
|
"protocolVersion": 1,
|
|
4
4
|
"hostProof": 1,
|
|
5
5
|
"tokenOnStdin": 1,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-25T19:49:35.014Z",
|
|
7
7
|
"runtime": "system-node",
|
|
8
8
|
"platform": "linux",
|
|
9
9
|
"arch": "arm64"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "centralu",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.38",
|
|
4
4
|
"description": "Run, watch and steer several Claude Code and Codex CLI sessions from one window",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ijun17",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"win32"
|
|
39
39
|
],
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@centralu/darwin-arm64": "0.1.0-beta.
|
|
42
|
-
"@centralu/linux-arm64": "0.1.0-beta.
|
|
43
|
-
"@centralu/linux-x64": "0.1.0-beta.
|
|
44
|
-
"@centralu/win32-x64": "0.1.0-beta.
|
|
41
|
+
"@centralu/darwin-arm64": "0.1.0-beta.38",
|
|
42
|
+
"@centralu/linux-arm64": "0.1.0-beta.38",
|
|
43
|
+
"@centralu/linux-x64": "0.1.0-beta.38",
|
|
44
|
+
"@centralu/win32-x64": "0.1.0-beta.38"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/host/main.mjs
CHANGED
|
@@ -43053,9 +43053,11 @@ import { hostname as hostname5 } from "node:os";
|
|
|
43053
43053
|
import { dirname as dirname23, join as join52 } from "node:path";
|
|
43054
43054
|
import { mkdirSync as mkdirSync19, writeSync as writeSync3 } from "node:fs";
|
|
43055
43055
|
|
|
43056
|
+
// packages/protocol/src/version.generated.ts
|
|
43057
|
+
var APP_VERSION = "0.1.0-beta.38";
|
|
43058
|
+
|
|
43056
43059
|
// packages/protocol/src/brand.ts
|
|
43057
43060
|
var APP_NAME = "Centralu";
|
|
43058
|
-
var APP_VERSION = "0.1.0-beta.37";
|
|
43059
43061
|
var APP_SLUG = "centralu";
|
|
43060
43062
|
var CLIENT_INFO = { name: APP_SLUG, title: APP_NAME, version: APP_VERSION };
|
|
43061
43063
|
var DATA_DIR = ".centralu";
|
|
@@ -104106,7 +104108,7 @@ var NOT_CONNECTED = "not connected until the person connects it";
|
|
|
104106
104108
|
var NOT_CONNECTED_PHRASE = "is not connected, and nothing here connects it by itself; ask the person to connect it (Settings, Machines, Connect)";
|
|
104107
104109
|
function awayPhrase(why) {
|
|
104108
104110
|
if (why === NOT_CONNECTED) return NOT_CONNECTED_PHRASE;
|
|
104109
|
-
return why === NOT_REACHABLE ? `is ${
|
|
104111
|
+
return why === NOT_REACHABLE || why.startsWith(`${NOT_REACHABLE};`) ? `is ${why}` : `could not be asked just now (${why})`;
|
|
104110
104112
|
}
|
|
104111
104113
|
|
|
104112
104114
|
// packages/agent-host/src/adapters/contract/surface.ts
|
|
@@ -113241,11 +113243,12 @@ var RemoteAgents = class {
|
|
|
113241
113243
|
if (!origin || !key) {
|
|
113242
113244
|
return { error: `This project has ${origin ? "a local folder as its origin remote" : "no origin remote"}, so no checkout of it on ${machine.name} can be told apart. Start the agent here, without machine.` };
|
|
113243
113245
|
}
|
|
113244
|
-
const all = await reader.projects();
|
|
113246
|
+
const all = await reader.projects({ machineId: machine.id });
|
|
113245
113247
|
const away = all.away.find((a) => a.machineId === machine.id);
|
|
113246
113248
|
if (away) {
|
|
113247
113249
|
if (away.why === NOT_CONNECTED) return { error: `${machine.name} ${awayPhrase(NOT_CONNECTED)}, so nothing was started there. Try again once it is connected.` };
|
|
113248
|
-
|
|
113250
|
+
const cannot = away.why.startsWith(`${NOT_REACHABLE};`) ? `is ${away.why}` : `cannot be reached (${away.why})`;
|
|
113251
|
+
return { error: `${machine.name} ${cannot}, so nothing was started there. Try again once its link is back.` };
|
|
113249
113252
|
}
|
|
113250
113253
|
const theirs = all.projects.filter((p) => p.machineId === machine.id);
|
|
113251
113254
|
const read = await Promise.all(theirs.map(async (p) => ({ p, r: await reader.origin(p) })));
|
|
@@ -135715,7 +135718,7 @@ var AskProjectFlow = class {
|
|
|
135715
135718
|
const exactHere = projects.some((p) => p.id === wanted);
|
|
135716
135719
|
const hubs2 = this.hubs;
|
|
135717
135720
|
const none = { projects: [], away: [] };
|
|
135718
|
-
const [there, fromHubs] = (reader || hubs2) && !exactHere ? await Promise.all([reader ? reader.projects() : none, hubs2 ? hubs2.projects() : none]) : [none, none];
|
|
135721
|
+
const [there, fromHubs] = (reader || hubs2) && !exactHere ? await Promise.all([reader ? reader.projects({ ref: wanted }) : none, hubs2 ? hubs2.projects() : none]) : [none, none];
|
|
135719
135722
|
const askable = [
|
|
135720
135723
|
...projects.map((p) => ({ id: p.id, name: p.name, machine: null, machineId: null })),
|
|
135721
135724
|
...there.projects.map((p) => ({ id: p.id, name: p.name, machine: p.machine, machineId: p.machineId })),
|
|
@@ -144805,6 +144808,13 @@ var AUTO_RETRY_DELAYS_MS = [10 * 60 * 1e3, 60 * 60 * 1e3, 6 * 60 * 60 * 1e3];
|
|
|
144805
144808
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
144806
144809
|
var NPM_RETRY_DELAY_MS = 3e4;
|
|
144807
144810
|
var NPM_RETRY_WINDOW_MS = 10 * 60 * 1e3;
|
|
144811
|
+
var UNSTAMPED_VERSION = "0.0.0-dev";
|
|
144812
|
+
function installsByItself(build3, current) {
|
|
144813
|
+
return build3 !== "dev" && current !== UNSTAMPED_VERSION;
|
|
144814
|
+
}
|
|
144815
|
+
function hubUpdatesMachines(f) {
|
|
144816
|
+
return installsByItself(f.build, f.current) && f.autoApply && !f.draining;
|
|
144817
|
+
}
|
|
144808
144818
|
function existingInstalledCopy() {
|
|
144809
144819
|
const path2 = installedCopyPath();
|
|
144810
144820
|
return existsSync22(path2) ? path2 : null;
|
|
@@ -144855,6 +144865,8 @@ var UpdateService = class {
|
|
|
144855
144865
|
constructor(publish, deps = {}) {
|
|
144856
144866
|
this.publish = publish;
|
|
144857
144867
|
this.deps = {
|
|
144868
|
+
current: deps.current ?? APP_VERSION,
|
|
144869
|
+
build: deps.build ?? "",
|
|
144858
144870
|
fetchLatest: deps.fetchLatest ?? fetchLatestFromRegistry,
|
|
144859
144871
|
run: deps.run ?? runCommand,
|
|
144860
144872
|
installedCopy: deps.installedCopy ?? existingInstalledCopy,
|
|
@@ -144880,13 +144892,13 @@ var UpdateService = class {
|
|
|
144880
144892
|
/*
|
|
144881
144893
|
* The honest source for "which build is this".
|
|
144882
144894
|
*
|
|
144883
|
-
* `APP_VERSION` is what the app was compiled from
|
|
144884
|
-
*
|
|
144885
|
-
*
|
|
144886
|
-
*
|
|
144887
|
-
* enough to notice.
|
|
144895
|
+
* `APP_VERSION` is what the app was compiled from: the version a release stamped
|
|
144896
|
+
* into the build with the npm packages that carry it (#1501, `tooling/brand.test.ts`),
|
|
144897
|
+
* or the placeholder `0.0.0-dev` of a source tree. The workspace root's package.json
|
|
144898
|
+
* is not it — that one is private, has no version anyone installs, and would have
|
|
144899
|
+
* read `0.0.0` forever without ever being wrong enough to notice.
|
|
144888
144900
|
*/
|
|
144889
|
-
current:
|
|
144901
|
+
current: this.deps.current,
|
|
144890
144902
|
latest: null,
|
|
144891
144903
|
newer: false,
|
|
144892
144904
|
auto: this.deps.readAuto(),
|
|
@@ -144983,6 +144995,7 @@ var UpdateService = class {
|
|
|
144983
144995
|
*/
|
|
144984
144996
|
installIfAutomatic() {
|
|
144985
144997
|
if (!this.status.autoApply || !this.status.newer || !this.status.latest) return;
|
|
144998
|
+
if (!installsByItself(this.deps.build, this.deps.current)) return;
|
|
144986
144999
|
if (this.status.phase === "idle") {
|
|
144987
145000
|
void this.apply();
|
|
144988
145001
|
return;
|
|
@@ -145006,6 +145019,7 @@ var UpdateService = class {
|
|
|
145006
145019
|
this.retryTimer = this.deps.schedule(() => {
|
|
145007
145020
|
this.retryTimer = null;
|
|
145008
145021
|
if (this.stopped || !this.status.autoApply || this.status.phase !== "failed" || !this.status.newer || !this.status.latest) return;
|
|
145022
|
+
if (!installsByItself(this.deps.build, this.deps.current)) return;
|
|
145009
145023
|
void this.apply();
|
|
145010
145024
|
}, delay);
|
|
145011
145025
|
}
|
|
@@ -147380,8 +147394,8 @@ function failure2(target, code2, detail) {
|
|
|
147380
147394
|
case "dev_dirty":
|
|
147381
147395
|
return `The working copy on ${target} has ${detail} uncommitted change${detail === "1" ? "" : "s"}; commit or stash them, or pass --allow-dirty to build them too. Nothing was built`;
|
|
147382
147396
|
case "dev_base":
|
|
147383
|
-
if (/^0\.0\.0-dev(?:\.|$)/.test(detail))
|
|
147384
|
-
return `The working copy on ${target}
|
|
147397
|
+
if (!detail || /^0\.0\.0-dev(?:\.|$)/.test(detail))
|
|
147398
|
+
return `The working copy on ${target} has no release tag (v*) reachable from its HEAD, which a dev build takes its version from (#1501); fetch the tags there (git fetch --tags) and try again. Nothing was built`;
|
|
147385
147399
|
return `The working copy on ${target} is version ${detail || "(none)"}; a dev build is cut from a prerelease (x.y.z-beta.N)`;
|
|
147386
147400
|
case "dev_protocol": {
|
|
147387
147401
|
const [have = "?", want = "?"] = detail.split(" ");
|
|
@@ -148220,7 +148234,11 @@ var KEEPS_TRYING = "reconnecting";
|
|
|
148220
148234
|
var WAITS_FOR_PERSON = "press Connect to try again";
|
|
148221
148235
|
var NEXT_STEP = new RegExp(`; (${KEEPS_TRYING}|asking the machine again|${WAITS_FOR_PERSON})$`);
|
|
148222
148236
|
var ATTEMPT_WAIT_MS = 6e4;
|
|
148237
|
+
var CALL_WAIT_MS = 1e4;
|
|
148238
|
+
var CALL_FAILURE_KEPT_MS = 6e4;
|
|
148239
|
+
var CALL_WINDOW_MS = 15e3;
|
|
148223
148240
|
var AWAY = /* @__PURE__ */ new Set(["unreachable", "away", "not_running", "refused", "stopped"]);
|
|
148241
|
+
var CALL_RETRIES = /* @__PURE__ */ new Set(["unreachable", "away", "not_running", "refused"]);
|
|
148224
148242
|
var LinkedMachineConnection = class {
|
|
148225
148243
|
constructor(record2, tunnel, deps) {
|
|
148226
148244
|
this.record = record2;
|
|
@@ -148335,6 +148353,10 @@ var LinkedMachineConnection = class {
|
|
|
148335
148353
|
nextAttemptAt = null;
|
|
148336
148354
|
/** Calls waiting for an attempt to connect to end (`settled`) */
|
|
148337
148355
|
statusWaiters = /* @__PURE__ */ new Set();
|
|
148356
|
+
/** When a call last started an attempt (`reachForCall`), and how the attempt a call waited on ended, with when */
|
|
148357
|
+
lastCallAttemptAt = 0;
|
|
148358
|
+
callFailure = null;
|
|
148359
|
+
callFailureAt = 0;
|
|
148338
148360
|
get id() {
|
|
148339
148361
|
return this.record.id;
|
|
148340
148362
|
}
|
|
@@ -148376,7 +148398,11 @@ var LinkedMachineConnection = class {
|
|
|
148376
148398
|
}
|
|
148377
148399
|
/** Resolves once no attempt is under way, or after `waitMs` (a minute): connecting took 2 to 4 s on a LAN, a host started there adds its start */
|
|
148378
148400
|
settled(waitMs = this.deps.attemptWaitMs ?? ATTEMPT_WAIT_MS) {
|
|
148379
|
-
|
|
148401
|
+
return this.waitUntil(() => !this.attemptUnderWay, waitMs);
|
|
148402
|
+
}
|
|
148403
|
+
/** Resolves once `ready()` holds (checked at every change of status and at the end of every opening), or after `waitMs` */
|
|
148404
|
+
waitUntil(ready, waitMs) {
|
|
148405
|
+
if (ready()) return Promise.resolve();
|
|
148380
148406
|
return new Promise((resolve21) => {
|
|
148381
148407
|
const done2 = () => {
|
|
148382
148408
|
clearTimeout(timer);
|
|
@@ -148384,13 +148410,63 @@ var LinkedMachineConnection = class {
|
|
|
148384
148410
|
resolve21();
|
|
148385
148411
|
};
|
|
148386
148412
|
const waiter = () => {
|
|
148387
|
-
if (
|
|
148413
|
+
if (ready()) done2();
|
|
148388
148414
|
};
|
|
148389
148415
|
const timer = setTimeout(done2, waitMs);
|
|
148390
148416
|
timer.unref?.();
|
|
148391
148417
|
this.statusWaiters.add(waiter);
|
|
148392
148418
|
});
|
|
148393
148419
|
}
|
|
148420
|
+
/**
|
|
148421
|
+
* An agent's call needs this machine (#1532): with Auto reconnect on and the link down, it does what the person's Connect
|
|
148422
|
+
* does (`connect`, the one action that opens a link now) instead of waiting out the backoff, and waits for it a short
|
|
148423
|
+
* bound (`CALL_WAIT_MS`). The caller reads `reachable` afterwards: connected in time, the call goes through; not, it is
|
|
148424
|
+
* answered as not reachable with `whyDown`, and the backoff goes on from where it was (`keepBackoff`).
|
|
148425
|
+
*
|
|
148426
|
+
* - one attempt at a time: a call that finds one under way (the backoff's, the person's press, another call's) waits on
|
|
148427
|
+
* it and starts none; so do ten calls at once, and a press at the same moment as a call;
|
|
148428
|
+
* - calls cannot defeat the backoff: at most one attempt started by a call per machine within `CALL_WINDOW_MS`; a call
|
|
148429
|
+
* inside the window waits for nothing and is answered at once;
|
|
148430
|
+
* - with Auto reconnect off nothing is started (#1341): the call is answered as not connected, and the person is named.
|
|
148431
|
+
* Neither does it when the person disconnected the machine, the host was stopped there, an operation holds the link
|
|
148432
|
+
* or the versions differ: a Connect does not help those, or is the person's to press
|
|
148433
|
+
*/
|
|
148434
|
+
async reachForCall() {
|
|
148435
|
+
if (this.reachable) return;
|
|
148436
|
+
const until = Date.now() + (this.deps.callWaitMs ?? CALL_WAIT_MS);
|
|
148437
|
+
const left = () => Math.max(0, until - Date.now());
|
|
148438
|
+
await this.waitUntil(() => this.reachable || !this.redialing, left());
|
|
148439
|
+
if (this.reachable) return;
|
|
148440
|
+
if (this.attemptUnderWay) {
|
|
148441
|
+
await this.settled(Math.min(left(), this.deps.attemptWaitMs ?? ATTEMPT_WAIT_MS));
|
|
148442
|
+
return this.noteCallFailure();
|
|
148443
|
+
}
|
|
148444
|
+
if (this.stopped || this.parked || this.held || this.inOperation() || !this.autoReconnect || this.redialing) return;
|
|
148445
|
+
if (!CALL_RETRIES.has(this.status)) return;
|
|
148446
|
+
const now = Date.now();
|
|
148447
|
+
if (now - this.lastCallAttemptAt < (this.deps.callWindowMs ?? CALL_WINDOW_MS)) return;
|
|
148448
|
+
this.lastCallAttemptAt = now;
|
|
148449
|
+
this.connect({ start: false, keepBackoff: true });
|
|
148450
|
+
await this.settled(Math.min(left(), this.deps.attemptWaitMs ?? ATTEMPT_WAIT_MS));
|
|
148451
|
+
this.noteCallFailure();
|
|
148452
|
+
}
|
|
148453
|
+
/** How the attempt a call waited on ended when it did not connect: the machine's own words. One still going is told as it is (`whyDown`) */
|
|
148454
|
+
noteCallFailure() {
|
|
148455
|
+
if (this.reachable || this.attemptUnderWay) return;
|
|
148456
|
+
this.callFailure = this.error?.replace(NEXT_STEP, "") ?? null;
|
|
148457
|
+
this.callFailureAt = Date.now();
|
|
148458
|
+
}
|
|
148459
|
+
/**
|
|
148460
|
+
* What the answer to a call that could not take it says of the attempt to connect the machine (#1532), as a clause: that
|
|
148461
|
+
* one is still under way, or that the one a call made last failed, with the machine's own words. Null once the link is
|
|
148462
|
+
* up, and when no attempt was made or told of
|
|
148463
|
+
*/
|
|
148464
|
+
get whyDown() {
|
|
148465
|
+
if (this.reachable) return null;
|
|
148466
|
+
if (this.attemptUnderWay) return "an attempt to connect it is still under way";
|
|
148467
|
+
if (!this.callFailure || Date.now() - this.callFailureAt > CALL_FAILURE_KEPT_MS) return null;
|
|
148468
|
+
return `the attempt to connect it just now failed: ${this.callFailure}`;
|
|
148469
|
+
}
|
|
148394
148470
|
/** An install, update or other operation the person started is running: its retries are part of it (`retryLater`) */
|
|
148395
148471
|
inOperation() {
|
|
148396
148472
|
return this.held;
|
|
@@ -148526,7 +148602,9 @@ var LinkedMachineConnection = class {
|
|
|
148526
148602
|
* The person's Connect (`machines.reconnect`, #1134): opens now, forgetting the backoff, also a disconnected link and
|
|
148527
148603
|
* also with Auto reconnect off (#1341), where it is the only way to try again. It is also the person's Start
|
|
148528
148604
|
* (#1143): a host is started there even where they stopped it or the link stopped starting them. `start: false` is
|
|
148529
|
-
* the orchestrator's reconnect without its card, which opens the link and starts nothing the link would not
|
|
148605
|
+
* the orchestrator's reconnect without its card, which opens the link and starts nothing the link would not.
|
|
148606
|
+
* `keepBackoff` is an agent's call that needs the machine (`reachForCall`, #1532): it opens now too, but the backoff
|
|
148607
|
+
* goes on from the step it was at instead of starting over, so calls cannot hold it at its short steps
|
|
148530
148608
|
*/
|
|
148531
148609
|
connect(opts = {}) {
|
|
148532
148610
|
if (opts.start !== false) {
|
|
@@ -148535,7 +148613,7 @@ var LinkedMachineConnection = class {
|
|
|
148535
148613
|
this.restartsSpent = false;
|
|
148536
148614
|
}
|
|
148537
148615
|
if (this.parked) return this.start();
|
|
148538
|
-
this.reconnect();
|
|
148616
|
+
this.reconnect(opts.keepBackoff === true);
|
|
148539
148617
|
}
|
|
148540
148618
|
/**
|
|
148541
148619
|
* Closes the link and tries none until the person connects it again (#1134). The machine stays linked; its sessions
|
|
@@ -148580,9 +148658,9 @@ var LinkedMachineConnection = class {
|
|
|
148580
148658
|
this.start();
|
|
148581
148659
|
}
|
|
148582
148660
|
/** Opens again now, forgetting the backoff (`machines.reconnect`, or the person accepted the versions) */
|
|
148583
|
-
reconnect() {
|
|
148661
|
+
reconnect(keepBackoff = false) {
|
|
148584
148662
|
this.clearRetry();
|
|
148585
|
-
this.attempt = 0;
|
|
148663
|
+
if (!keepBackoff) this.attempt = 0;
|
|
148586
148664
|
const under = this.opening;
|
|
148587
148665
|
if (under) {
|
|
148588
148666
|
if (this.status === "away") this.setStatus("connecting", null);
|
|
@@ -148995,6 +149073,7 @@ var LinkedMachineConnection = class {
|
|
|
148995
149073
|
setStatus(status, error51) {
|
|
148996
149074
|
if (this.status === "connected" && status !== "connected") this.seen();
|
|
148997
149075
|
const changed = status !== this.status || error51 !== this.error;
|
|
149076
|
+
if (status === "connected") this.callFailure = null;
|
|
148998
149077
|
this.status = status;
|
|
148999
149078
|
this.error = error51;
|
|
149000
149079
|
if (changed) {
|
|
@@ -150301,9 +150380,12 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
|
|
|
150301
150380
|
for (const m of machines()) if (m.id === id3) return m;
|
|
150302
150381
|
return null;
|
|
150303
150382
|
};
|
|
150304
|
-
const away = (m) => m.waitsForPerson ? `${m.name} ${awayPhrase(NOT_CONNECTED)}, then try again` : `${m.name} is ${
|
|
150383
|
+
const away = (m) => m.waitsForPerson ? `${m.name} ${awayPhrase(NOT_CONNECTED)}, then try again` : `${m.name} is ${notReachable(m)}; try again once its link is back`;
|
|
150384
|
+
const notReachable = (m) => m.whyDown ? `${NOT_REACHABLE}; ${oneLine3(m.whyDown, 300)}` : NOT_REACHABLE;
|
|
150305
150385
|
const settle = async (m) => {
|
|
150306
|
-
if (m.
|
|
150386
|
+
if (m.reachable) return;
|
|
150387
|
+
if (m.reachForCall) await m.reachForCall();
|
|
150388
|
+
else if (m.attemptUnderWay) await m.settled?.();
|
|
150307
150389
|
};
|
|
150308
150390
|
const attempt = async (m, run4) => {
|
|
150309
150391
|
await settle(m);
|
|
@@ -150373,26 +150455,36 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
|
|
|
150373
150455
|
}
|
|
150374
150456
|
return out;
|
|
150375
150457
|
};
|
|
150376
|
-
const
|
|
150458
|
+
const projectsOf = async (m, tried = false) => {
|
|
150377
150459
|
const away2 = [];
|
|
150378
|
-
const
|
|
150379
|
-
|
|
150380
|
-
|
|
150381
|
-
|
|
150382
|
-
|
|
150383
|
-
|
|
150384
|
-
|
|
150385
|
-
|
|
150386
|
-
|
|
150387
|
-
|
|
150388
|
-
|
|
150389
|
-
|
|
150390
|
-
|
|
150391
|
-
|
|
150392
|
-
|
|
150393
|
-
|
|
150394
|
-
);
|
|
150395
|
-
|
|
150460
|
+
const mirrored = (why) => {
|
|
150461
|
+
away2.push({ machine: m.name, machineId: m.id, why });
|
|
150462
|
+
return lastProjects(m).map((p) => ({ ...p, away: why }));
|
|
150463
|
+
};
|
|
150464
|
+
if (!m.reachable && m.attemptUnderWay && !tried) return { projects: lastProjects(m), away: away2 };
|
|
150465
|
+
if (!m.reachable) return { projects: mirrored(m.waitsForPerson ? NOT_CONNECTED : notReachable(m)), away: away2 };
|
|
150466
|
+
try {
|
|
150467
|
+
const list = await raceTimeout(m.call("projects.list", {}), lookupMs, TIMED_OUT);
|
|
150468
|
+
if (list === TIMED_OUT) throw new Error(notWithin(lookupMs));
|
|
150469
|
+
return { projects: (Array.isArray(list) ? list : []).map((raw) => projectOf(m, raw)).filter((p) => p !== null), away: away2 };
|
|
150470
|
+
} catch (err) {
|
|
150471
|
+
return { projects: mirrored(cause(m, err)), away: away2 };
|
|
150472
|
+
}
|
|
150473
|
+
};
|
|
150474
|
+
const needs = (m, need3) => {
|
|
150475
|
+
if (need3.machineId === m.id) return true;
|
|
150476
|
+
const ref = need3.ref?.trim();
|
|
150477
|
+
if (!ref) return false;
|
|
150478
|
+
const slash = ref.indexOf("/");
|
|
150479
|
+
const prefix = slash > 0 ? ref.slice(0, slash).trim().toLowerCase() : null;
|
|
150480
|
+
if (prefix === m.id || prefix === m.name.toLowerCase() || ref.startsWith(`${m.id}.`)) return true;
|
|
150481
|
+
return lastProjects(m).some((p) => p.id === ref || p.name.toLowerCase() === ref.toLowerCase());
|
|
150482
|
+
};
|
|
150483
|
+
const everyProject = async (need3) => {
|
|
150484
|
+
const all = [...machines()];
|
|
150485
|
+
const wanted = need3 ? all.filter((m) => !m.reachable && needs(m, need3)) : [];
|
|
150486
|
+
const parts = await Promise.all(all.map(async (m) => wanted.includes(m) ? (await settle(m), projectsOf(m, true)) : projectsOf(m)));
|
|
150487
|
+
return { projects: parts.flatMap((p) => p.projects), away: parts.flatMap((p) => p.away) };
|
|
150396
150488
|
};
|
|
150397
150489
|
return {
|
|
150398
150490
|
async send(sessionId, text2, fromName, ask) {
|
|
@@ -150410,7 +150502,7 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
|
|
|
150410
150502
|
},
|
|
150411
150503
|
async findProjects(ref) {
|
|
150412
150504
|
const wanted = ref.trim();
|
|
150413
|
-
return (await everyProject()).projects.filter((p) => p.id === wanted || p.name === wanted);
|
|
150505
|
+
return (await everyProject({ ref: wanted })).projects.filter((p) => p.id === wanted || p.name === wanted);
|
|
150414
150506
|
},
|
|
150415
150507
|
projects: everyProject,
|
|
150416
150508
|
async projectSessions(project2) {
|
|
@@ -150676,7 +150768,7 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
|
|
|
150676
150768
|
const m = owner(sessionId);
|
|
150677
150769
|
if (!m) return { ok: false, error: `Not a session of a linked machine: ${sessionId}` };
|
|
150678
150770
|
await settle(m);
|
|
150679
|
-
if (!m.reachable) return { ok: false, error: m.waitsForPerson ? away(m) : `${m.name} is ${
|
|
150771
|
+
if (!m.reachable) return { ok: false, error: m.waitsForPerson ? away(m) : `${m.name} is ${notReachable(m)}` };
|
|
150680
150772
|
try {
|
|
150681
150773
|
const answer = await m.call("host.lastAnswer", { sessionId: m.q.strip(sessionId, "The session"), maxChars: Math.min(maxChars, ANSWER_MAX_CHARS2) });
|
|
150682
150774
|
const r5 = HostLastAnswerResult.safeParse(answer);
|
|
@@ -150716,7 +150808,7 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
|
|
|
150716
150808
|
const m = owner(sessionId);
|
|
150717
150809
|
if (!m) return { ok: false, error: `Not a session of a linked machine: ${sessionId}` };
|
|
150718
150810
|
await settle(m);
|
|
150719
|
-
if (!m.reachable) return { ok: false, error: m.waitsForPerson ? away(m) : `${m.name} is
|
|
150811
|
+
if (!m.reachable) return { ok: false, error: m.waitsForPerson ? away(m) : `${m.name} is ${notReachable(m)}; read it again once its link is back` };
|
|
150720
150812
|
const toolMode = toolLinesOf(opts?.tools);
|
|
150721
150813
|
try {
|
|
150722
150814
|
const answer = await m.call("host.readSession", {
|
|
@@ -151126,7 +151218,7 @@ var MachineUnreachableError = class extends Error {
|
|
|
151126
151218
|
retryable = true;
|
|
151127
151219
|
data;
|
|
151128
151220
|
constructor(machine) {
|
|
151129
|
-
super(`${machine.name} is not reachable right now; this works again once its link is back`);
|
|
151221
|
+
super(`${machine.name} is not reachable right now${machine.whyDown ? `; ${machine.whyDown}` : ""}; this works again once its link is back`);
|
|
151130
151222
|
this.data = { machine: machine.id, reason: "unreachable" };
|
|
151131
151223
|
}
|
|
151132
151224
|
};
|
|
@@ -151191,7 +151283,8 @@ var Router = class {
|
|
|
151191
151283
|
for (const m of this.opts.machines()) if (m.id === id3) return m;
|
|
151192
151284
|
return null;
|
|
151193
151285
|
}
|
|
151194
|
-
async forward(m, method, params) {
|
|
151286
|
+
async forward(m, method, params, ctx) {
|
|
151287
|
+
if (!m.reachable && ctx === void 0) await m.reachForCall?.();
|
|
151195
151288
|
if (!m.reachable) throw new MachineUnreachableError(m);
|
|
151196
151289
|
return m.call(method, params);
|
|
151197
151290
|
}
|
|
@@ -151201,7 +151294,7 @@ var Router = class {
|
|
|
151201
151294
|
const m2 = this.machineById(params.machine);
|
|
151202
151295
|
if (!m2) throw rpcError(`No linked machine ${params.machine}`);
|
|
151203
151296
|
const { machine: _machine2, ...rest } = params;
|
|
151204
|
-
const r5 = await this.forward(m2, method, route.params ? route.params(rest, m2.q) : rest);
|
|
151297
|
+
const r5 = await this.forward(m2, method, route.params ? route.params(rest, m2.q) : rest, ctx);
|
|
151205
151298
|
return route.result ? route.result(r5, m2.q) : r5;
|
|
151206
151299
|
}
|
|
151207
151300
|
const m = this.owner(params[key]);
|
|
@@ -151212,7 +151305,7 @@ var Router = class {
|
|
|
151212
151305
|
const done2 = this.opts.forwarding?.(method, params);
|
|
151213
151306
|
let r;
|
|
151214
151307
|
try {
|
|
151215
|
-
r = await this.forward(m, method, p);
|
|
151308
|
+
r = await this.forward(m, method, p, ctx);
|
|
151216
151309
|
} catch (e) {
|
|
151217
151310
|
done2?.(false);
|
|
151218
151311
|
throw e;
|
|
@@ -151247,7 +151340,7 @@ var Router = class {
|
|
|
151247
151340
|
const { machine: _machine, ...own } = params;
|
|
151248
151341
|
const p = route.orMachine ? own : { ...params };
|
|
151249
151342
|
for (const key of route.keys) if (typeof p[key] === "string") p[key] = target.q.strip(p[key], "The id");
|
|
151250
|
-
const r = await this.forward(target, method, p);
|
|
151343
|
+
const r = await this.forward(target, method, p, ctx);
|
|
151251
151344
|
return route.result ? route.result(r, target.q) : r;
|
|
151252
151345
|
}
|
|
151253
151346
|
async byMachine(method, params, route, ctx) {
|
|
@@ -151259,7 +151352,7 @@ var Router = class {
|
|
|
151259
151352
|
const shared = await this.sameAccountAnswer(m, method, rest, ctx);
|
|
151260
151353
|
if (shared) return route.result ? route.result(shared, m.q) : shared;
|
|
151261
151354
|
}
|
|
151262
|
-
const r = await this.forward(m, method, rest);
|
|
151355
|
+
const r = await this.forward(m, method, rest, ctx);
|
|
151263
151356
|
return route.result ? route.result(r, m.q) : r;
|
|
151264
151357
|
}
|
|
151265
151358
|
/**
|
|
@@ -151915,7 +152008,7 @@ var handedToken = launch.tokenOnStdin ? await readHandedToken(process.stdin).cat
|
|
|
151915
152008
|
console.error(`[agent-host] the launcher did not hand over the token: ${e.message}`);
|
|
151916
152009
|
process.exit(1);
|
|
151917
152010
|
}) : launch.token;
|
|
151918
|
-
var BUILD = true ? "
|
|
152011
|
+
var BUILD = true ? "bab6469" : "dev";
|
|
151919
152012
|
var { values } = parseArgs({
|
|
151920
152013
|
options: {
|
|
151921
152014
|
port: { type: "string", default: "5175" },
|
|
@@ -152178,6 +152271,8 @@ var { auto: autoUpdateCheck, autoApply: autoApplyUpdates } = updateSwitches(stor
|
|
|
152178
152271
|
var themes = new ThemeFiles(join52(dataRoot(), "themes"), () => events.push({ type: "themes_changed" }));
|
|
152179
152272
|
await themes.start().catch((e) => console.error(`[themes] the themes folder is unavailable: ${e.message}`));
|
|
152180
152273
|
var updates = new UpdateService((status) => events.push({ type: "update_status", status }), {
|
|
152274
|
+
// A host from source never installs by itself, whatever is stored (#1501 review, `installsByItself`)
|
|
152275
|
+
build: BUILD,
|
|
152181
152276
|
// On by default when nothing has been saved yet. Since the check is read-only and every failure
|
|
152182
152277
|
// is swallowed, leaving it on costs nothing, while leaving it off traps **someone who never once
|
|
152183
152278
|
// opens settings** on an old version forever — that person is exactly the one most likely to stay
|
|
@@ -152279,7 +152374,7 @@ var links = new Links({
|
|
|
152279
152374
|
// When agents compact: this hub's setting governs the machines it links (#1376)
|
|
152280
152375
|
autoCompact: () => autoCompactOf(mgr.uiPreferences()),
|
|
152281
152376
|
// Automatic updates cover the linked machines too (#1508): one on an older release follows this hub's version; none starts mid-swap
|
|
152282
|
-
autoUpdate: () => autoApplyUpdates.read()
|
|
152377
|
+
autoUpdate: () => hubUpdatesMachines({ build: BUILD, current: APP_VERSION, autoApply: autoApplyUpdates.read(), draining: hostDrain.draining }),
|
|
152283
152378
|
reverse: reverseCalls({
|
|
152284
152379
|
allowed: (machineId) => store.machines.linkedMachineAsks(machineId),
|
|
152285
152380
|
projects: () => mgr.projectsForMachines(),
|
package/host/remote-install.mjs
CHANGED
|
@@ -462,12 +462,24 @@ function atLeast(a, b) {
|
|
|
462
462
|
/** A release a dev build can be cut from: a prerelease, since `<base>.local.N` is SemVer only on one (`isLocalBuildVersion`) */
|
|
463
463
|
const DEV_BASE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$/
|
|
464
464
|
/**
|
|
465
|
-
* The placeholder a working copy's version files carry
|
|
466
|
-
* (#1501).
|
|
467
|
-
* carrying this from before the switch lands.
|
|
465
|
+
* The placeholder a working copy's version files carry since its release version is stamped from the tag at build time
|
|
466
|
+
* (#1501). Never a dev build's base: `0.0.0-dev.local.N` would be older than every release.
|
|
468
467
|
*/
|
|
469
468
|
const PLACEHOLDER = /^0\.0\.0-dev(?:\.|$)/
|
|
470
469
|
|
|
470
|
+
/**
|
|
471
|
+
* The release a dev build of the working copy is cut from, as `scripts/app-version.mjs` resolves it for a local build
|
|
472
|
+
* (#1501): the newest release tag reachable from HEAD there. A commit from before the switch, in a clone without tags,
|
|
473
|
+
* still carries its version in the shim's manifest, which is taken then. An empty string when neither says.
|
|
474
|
+
*/
|
|
475
|
+
function devBase(repo, shim) {
|
|
476
|
+
const r = spawnSync('git', ['-C', repo, 'describe', '--tags', '--abbrev=0', '--match', 'v[0-9]*'], { cwd: repo, encoding: 'utf8', windowsHide: true })
|
|
477
|
+
const tag = r.status === 0 ? String(r.stdout).trim().replace(/^v/, '') : ''
|
|
478
|
+
if (tag) return tag
|
|
479
|
+
const own = String(shim.version ?? '')
|
|
480
|
+
return PLACEHOLDER.test(own) ? '' : own
|
|
481
|
+
}
|
|
482
|
+
|
|
471
483
|
/** git in the working copy, by name on PATH as the person runs it there; what it printed, or a refusal */
|
|
472
484
|
function git(repo, args) {
|
|
473
485
|
const r = spawnSync('git', ['-C', repo, ...args], { cwd: repo, encoding: 'utf8', windowsHide: true, maxBuffer: 16 * 1024 * 1024 })
|
|
@@ -514,7 +526,7 @@ export function devBuild(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(
|
|
|
514
526
|
const changes = git(repo, ['status', '--porcelain']).split('\n').filter((line) => line.trim()).length
|
|
515
527
|
if (changes > 0 && !p.allowDirty) throw new Refusal('dev_dirty', String(changes))
|
|
516
528
|
const shim = JSON.parse(readFileSync(join(repo, 'packaging', 'npm', 'centralu', 'package.json'), 'utf8'))
|
|
517
|
-
const base =
|
|
529
|
+
const base = devBase(repo, shim)
|
|
518
530
|
if (!DEV_BASE.test(base) || base.includes('.local.') || PLACEHOLDER.test(base)) throw new Refusal('dev_base', base)
|
|
519
531
|
const protocol = Number(/export const PROTOCOL_VERSION = (\d+)/.exec(readFileSync(join(repo, 'packages', 'protocol', 'src', 'envelope.ts'), 'utf8'))?.[1])
|
|
520
532
|
if (protocol !== p.protocol) throw new Refusal('dev_protocol', `${protocol} ${p.protocol}`)
|
package/package.json
CHANGED