@beeos-ai/cli 1.1.1 → 1.1.3
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/dist/index.js +1185 -216
- package/package.json +1 -1
- package/scripts/install.sh +138 -9
package/dist/index.js
CHANGED
|
@@ -127,14 +127,14 @@ function projectSpawnEnv(cfg) {
|
|
|
127
127
|
}
|
|
128
128
|
async function saveSpawnEnvSnapshot(cfg) {
|
|
129
129
|
const p = getPlatformAdapter();
|
|
130
|
-
const
|
|
130
|
+
const path12 = spawnEnvJsonPath();
|
|
131
131
|
const snapshot = projectSpawnEnv(cfg);
|
|
132
132
|
try {
|
|
133
|
-
await p.mkdir(p.dirname(
|
|
134
|
-
await p.writeFile(
|
|
133
|
+
await p.mkdir(p.dirname(path12));
|
|
134
|
+
await p.writeFile(path12, JSON.stringify(snapshot, null, 2) + "\n");
|
|
135
135
|
} catch (err) {
|
|
136
136
|
const message = err instanceof Error ? err.message : String(err);
|
|
137
|
-
console.error(`! warning: could not write ${
|
|
137
|
+
console.error(`! warning: could not write ${path12}: ${message}
|
|
138
138
|
device-agent fleet will fall back to parsing config.toml directly.`);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -208,17 +208,17 @@ function defaultState() {
|
|
|
208
208
|
}
|
|
209
209
|
async function loadState() {
|
|
210
210
|
const p = getPlatformAdapter();
|
|
211
|
-
const
|
|
212
|
-
if (await p.exists(
|
|
211
|
+
const path12 = statePath();
|
|
212
|
+
if (await p.exists(path12)) {
|
|
213
213
|
let raw;
|
|
214
214
|
try {
|
|
215
|
-
raw = await p.readFile(
|
|
215
|
+
raw = await p.readFile(path12);
|
|
216
216
|
} catch (e) {
|
|
217
217
|
throw new BeeosError({
|
|
218
218
|
code: "state_corrupted",
|
|
219
|
-
message: `failed to read ${
|
|
219
|
+
message: `failed to read ${path12}: ${describeError(e)}`,
|
|
220
220
|
hint: "Inspect the file or move it aside and re-run `beeos doctor` to regenerate from legacy state.",
|
|
221
|
-
details: { path:
|
|
221
|
+
details: { path: path12 },
|
|
222
222
|
cause: e
|
|
223
223
|
});
|
|
224
224
|
}
|
|
@@ -228,23 +228,23 @@ async function loadState() {
|
|
|
228
228
|
} catch (e) {
|
|
229
229
|
throw new BeeosError({
|
|
230
230
|
code: "state_corrupted",
|
|
231
|
-
message: `${
|
|
231
|
+
message: `${path12} is not valid JSON: ${describeError(e)}`,
|
|
232
232
|
hint: "Move the file aside (e.g. `mv ~/.beeos/state.json ~/.beeos/state.json.broken`) and re-run `beeos doctor` to migrate from the legacy state files.",
|
|
233
|
-
details: { path:
|
|
233
|
+
details: { path: path12 }
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
|
-
return validateState(parsed,
|
|
236
|
+
return validateState(parsed, path12);
|
|
237
237
|
}
|
|
238
238
|
return migrateLegacy();
|
|
239
239
|
}
|
|
240
240
|
async function saveState(state) {
|
|
241
241
|
const p = getPlatformAdapter();
|
|
242
|
-
const
|
|
243
|
-
await p.mkdir(p.dirname(
|
|
244
|
-
await p.writeFile(
|
|
242
|
+
const path12 = statePath();
|
|
243
|
+
await p.mkdir(p.dirname(path12));
|
|
244
|
+
await p.writeFile(path12, JSON.stringify(state, null, 2) + "\n");
|
|
245
245
|
if (p.platform() !== "win32") {
|
|
246
246
|
try {
|
|
247
|
-
await p.chmod(
|
|
247
|
+
await p.chmod(path12, 384);
|
|
248
248
|
} catch {
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -319,11 +319,11 @@ async function migrateLegacy() {
|
|
|
319
319
|
}
|
|
320
320
|
async function tryReadBinding() {
|
|
321
321
|
const p = getPlatformAdapter();
|
|
322
|
-
const
|
|
323
|
-
if (!await p.exists(
|
|
322
|
+
const path12 = bindingPath();
|
|
323
|
+
if (!await p.exists(path12))
|
|
324
324
|
return null;
|
|
325
325
|
try {
|
|
326
|
-
const raw = await p.readFile(
|
|
326
|
+
const raw = await p.readFile(path12);
|
|
327
327
|
return JSON.parse(raw);
|
|
328
328
|
} catch {
|
|
329
329
|
return null;
|
|
@@ -331,11 +331,11 @@ async function tryReadBinding() {
|
|
|
331
331
|
}
|
|
332
332
|
async function tryReadDevices() {
|
|
333
333
|
const p = getPlatformAdapter();
|
|
334
|
-
const
|
|
335
|
-
if (!await p.exists(
|
|
334
|
+
const path12 = legacyDevicesPath();
|
|
335
|
+
if (!await p.exists(path12))
|
|
336
336
|
return [];
|
|
337
337
|
try {
|
|
338
|
-
const raw = await p.readFile(
|
|
338
|
+
const raw = await p.readFile(path12);
|
|
339
339
|
const parsed = JSON.parse(raw);
|
|
340
340
|
return Array.isArray(parsed?.devices) ? parsed.devices : [];
|
|
341
341
|
} catch {
|
|
@@ -344,11 +344,11 @@ async function tryReadDevices() {
|
|
|
344
344
|
}
|
|
345
345
|
async function tryReadSpawnEnvIntoPlatform(state) {
|
|
346
346
|
const p = getPlatformAdapter();
|
|
347
|
-
const
|
|
348
|
-
if (!await p.exists(
|
|
347
|
+
const path12 = spawnEnvJsonPath();
|
|
348
|
+
if (!await p.exists(path12))
|
|
349
349
|
return;
|
|
350
350
|
try {
|
|
351
|
-
const raw = await p.readFile(
|
|
351
|
+
const raw = await p.readFile(path12);
|
|
352
352
|
const parsed = JSON.parse(raw);
|
|
353
353
|
if (!parsed || typeof parsed !== "object")
|
|
354
354
|
return;
|
|
@@ -364,7 +364,7 @@ async function tryReadSpawnEnvIntoPlatform(state) {
|
|
|
364
364
|
async function tryFoldVlmConfigs(state) {
|
|
365
365
|
const p = getPlatformAdapter();
|
|
366
366
|
const root = beeoHome();
|
|
367
|
-
let entries
|
|
367
|
+
let entries;
|
|
368
368
|
try {
|
|
369
369
|
entries = await p.readdir(root);
|
|
370
370
|
} catch {
|
|
@@ -391,13 +391,13 @@ async function tryFoldVlmConfigs(state) {
|
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
|
-
function validateState(parsed,
|
|
394
|
+
function validateState(parsed, path12) {
|
|
395
395
|
if (!parsed || typeof parsed !== "object") {
|
|
396
396
|
throw new BeeosError({
|
|
397
397
|
code: "state_corrupted",
|
|
398
|
-
message: `${
|
|
398
|
+
message: `${path12} did not deserialise to a JSON object.`,
|
|
399
399
|
hint: "Move the file aside and re-run `beeos doctor` to regenerate.",
|
|
400
|
-
details: { path:
|
|
400
|
+
details: { path: path12 }
|
|
401
401
|
});
|
|
402
402
|
}
|
|
403
403
|
const obj = parsed;
|
|
@@ -405,26 +405,26 @@ function validateState(parsed, path10) {
|
|
|
405
405
|
if (version !== 2) {
|
|
406
406
|
throw new BeeosError({
|
|
407
407
|
code: "state_version_mismatch",
|
|
408
|
-
message: `${
|
|
408
|
+
message: `${path12} declares version=${JSON.stringify(version)}; this CLI only understands version=2.`,
|
|
409
409
|
hint: "Either upgrade the CLI (`beeos --version` to see what you have) or move the file aside and re-bind your agents with the older CLI's flow.",
|
|
410
|
-
details: { path:
|
|
410
|
+
details: { path: path12, observed: version, expected: 2 }
|
|
411
411
|
});
|
|
412
412
|
}
|
|
413
413
|
const platform = obj.platform;
|
|
414
414
|
if (!platform || typeof platform !== "object") {
|
|
415
415
|
throw new BeeosError({
|
|
416
416
|
code: "state_corrupted",
|
|
417
|
-
message: `${
|
|
417
|
+
message: `${path12} is missing a \`platform\` object.`,
|
|
418
418
|
hint: "Move the file aside and re-run `beeos doctor`.",
|
|
419
|
-
details: { path:
|
|
419
|
+
details: { path: path12 }
|
|
420
420
|
});
|
|
421
421
|
}
|
|
422
422
|
if (!Array.isArray(obj.agents)) {
|
|
423
423
|
throw new BeeosError({
|
|
424
424
|
code: "state_corrupted",
|
|
425
|
-
message: `${
|
|
425
|
+
message: `${path12} \`agents\` is not an array.`,
|
|
426
426
|
hint: "Move the file aside and re-run `beeos doctor`.",
|
|
427
|
-
details: { path:
|
|
427
|
+
details: { path: path12 }
|
|
428
428
|
});
|
|
429
429
|
}
|
|
430
430
|
return obj;
|
|
@@ -448,9 +448,9 @@ var init_state = __esm({
|
|
|
448
448
|
import * as TOML from "smol-toml";
|
|
449
449
|
async function loadOrCreateConfig() {
|
|
450
450
|
const p = getPlatformAdapter();
|
|
451
|
-
const
|
|
452
|
-
if (await p.exists(
|
|
453
|
-
const raw = await p.readFile(
|
|
451
|
+
const path12 = configPath();
|
|
452
|
+
if (await p.exists(path12)) {
|
|
453
|
+
const raw = await p.readFile(path12);
|
|
454
454
|
const parsed = TOML.parse(raw);
|
|
455
455
|
const cfg2 = mergeWithDefaults(parsed);
|
|
456
456
|
await saveSpawnEnvSnapshot(cfg2);
|
|
@@ -478,8 +478,8 @@ function applyEnvOverrides(cfg) {
|
|
|
478
478
|
}
|
|
479
479
|
async function saveConfig(cfg) {
|
|
480
480
|
const p = getPlatformAdapter();
|
|
481
|
-
const
|
|
482
|
-
await p.mkdir(p.dirname(
|
|
481
|
+
const path12 = configPath();
|
|
482
|
+
await p.mkdir(p.dirname(path12));
|
|
483
483
|
const platformBlock = {
|
|
484
484
|
api_url: cfg.platform.api_url,
|
|
485
485
|
agent_gateway_url: cfg.platform.agent_gateway_url,
|
|
@@ -496,7 +496,7 @@ async function saveConfig(cfg) {
|
|
|
496
496
|
}
|
|
497
497
|
};
|
|
498
498
|
const raw = TOML.stringify(serializable);
|
|
499
|
-
await p.writeFile(
|
|
499
|
+
await p.writeFile(path12, raw);
|
|
500
500
|
await saveSpawnEnvSnapshot(cfg);
|
|
501
501
|
await refreshStatePlatformBestEffort(cfg);
|
|
502
502
|
}
|
|
@@ -570,22 +570,22 @@ var init_toml = __esm({
|
|
|
570
570
|
// ../core/dist/config/binding.js
|
|
571
571
|
async function loadBindingInfo() {
|
|
572
572
|
const p = getPlatformAdapter();
|
|
573
|
-
const
|
|
574
|
-
if (!await p.exists(
|
|
573
|
+
const path12 = bindingPath();
|
|
574
|
+
if (!await p.exists(path12))
|
|
575
575
|
return null;
|
|
576
|
-
const raw = await p.readFile(
|
|
576
|
+
const raw = await p.readFile(path12);
|
|
577
577
|
return JSON.parse(raw);
|
|
578
578
|
}
|
|
579
579
|
async function saveBindingInfo(info) {
|
|
580
580
|
const p = getPlatformAdapter();
|
|
581
|
-
const
|
|
582
|
-
await p.writeFile(
|
|
581
|
+
const path12 = bindingPath();
|
|
582
|
+
await p.writeFile(path12, JSON.stringify(info, null, 2));
|
|
583
583
|
}
|
|
584
584
|
async function removeBindingInfo() {
|
|
585
585
|
const p = getPlatformAdapter();
|
|
586
|
-
const
|
|
587
|
-
if (await p.exists(
|
|
588
|
-
await p.rm(
|
|
586
|
+
const path12 = bindingPath();
|
|
587
|
+
if (await p.exists(path12)) {
|
|
588
|
+
await p.rm(path12);
|
|
589
589
|
}
|
|
590
590
|
}
|
|
591
591
|
var init_binding = __esm({
|
|
@@ -599,17 +599,17 @@ var init_binding = __esm({
|
|
|
599
599
|
// ../core/dist/config/gateway-token.js
|
|
600
600
|
async function loadOrCreateGatewayToken() {
|
|
601
601
|
const p = getPlatformAdapter();
|
|
602
|
-
const
|
|
603
|
-
if (await p.exists(
|
|
604
|
-
const token2 = (await p.readFile(
|
|
602
|
+
const path12 = p.joinPath(beeoHome(), "gateway_token");
|
|
603
|
+
if (await p.exists(path12)) {
|
|
604
|
+
const token2 = (await p.readFile(path12)).trim();
|
|
605
605
|
if (token2)
|
|
606
606
|
return token2;
|
|
607
607
|
}
|
|
608
608
|
const token = crypto.randomUUID();
|
|
609
|
-
await p.mkdir(p.dirname(
|
|
610
|
-
await p.writeFile(
|
|
609
|
+
await p.mkdir(p.dirname(path12));
|
|
610
|
+
await p.writeFile(path12, token);
|
|
611
611
|
if (p.platform() !== "win32") {
|
|
612
|
-
await p.chmod(
|
|
612
|
+
await p.chmod(path12, 384);
|
|
613
613
|
}
|
|
614
614
|
return token;
|
|
615
615
|
}
|
|
@@ -650,9 +650,9 @@ function fingerprintFromB64(pubkeyB64) {
|
|
|
650
650
|
}
|
|
651
651
|
async function loadOrCreateIdentity() {
|
|
652
652
|
const p = getPlatformAdapter();
|
|
653
|
-
const
|
|
654
|
-
if (await p.exists(
|
|
655
|
-
return loadFromFile(
|
|
653
|
+
const path12 = keypairPath();
|
|
654
|
+
if (await p.exists(path12)) {
|
|
655
|
+
return loadFromFile(path12);
|
|
656
656
|
}
|
|
657
657
|
const id = generate();
|
|
658
658
|
await save(id);
|
|
@@ -684,10 +684,10 @@ async function readPubkeyFromKeyFile(keyPath) {
|
|
|
684
684
|
async function loadIdentityFromKeyFile(keyPath) {
|
|
685
685
|
return loadFromFile(keyPath);
|
|
686
686
|
}
|
|
687
|
-
async function signRequest(method,
|
|
687
|
+
async function signRequest(method, path12, id) {
|
|
688
688
|
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
689
689
|
const nonce = crypto.randomUUID();
|
|
690
|
-
const message = `${method.toUpperCase()}|${
|
|
690
|
+
const message = `${method.toUpperCase()}|${path12}|${timestamp}|${nonce}`;
|
|
691
691
|
const msgBytes = new TextEncoder().encode(message);
|
|
692
692
|
const sig = await ed.signAsync(msgBytes, id.privateKey);
|
|
693
693
|
return {
|
|
@@ -705,9 +705,9 @@ function fingerprintFilePath() {
|
|
|
705
705
|
const p = getPlatformAdapter();
|
|
706
706
|
return p.joinPath(beeoHome(), "identity", "fingerprint");
|
|
707
707
|
}
|
|
708
|
-
async function loadFromFile(
|
|
708
|
+
async function loadFromFile(path12) {
|
|
709
709
|
const p = getPlatformAdapter();
|
|
710
|
-
const raw = await p.readFile(
|
|
710
|
+
const raw = await p.readFile(path12);
|
|
711
711
|
const stored = JSON.parse(raw);
|
|
712
712
|
const privateKey = fromBase64(stored.privateKey);
|
|
713
713
|
if (privateKey.length !== 32) {
|
|
@@ -745,6 +745,62 @@ var init_keypair = __esm({
|
|
|
745
745
|
}
|
|
746
746
|
});
|
|
747
747
|
|
|
748
|
+
// ../core/dist/identity/qr.js
|
|
749
|
+
import QRCode from "qrcode";
|
|
750
|
+
async function qrModules(url) {
|
|
751
|
+
const segments = QRCode.create(url, { errorCorrectionLevel: "M" });
|
|
752
|
+
const size = segments.modules.size;
|
|
753
|
+
const modules = [];
|
|
754
|
+
for (let y = 0; y < size; y++) {
|
|
755
|
+
for (let x = 0; x < size; x++) {
|
|
756
|
+
modules.push(segments.modules.get(x, y) !== 0);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return { width: size, modules };
|
|
760
|
+
}
|
|
761
|
+
async function qrToTerminal(url) {
|
|
762
|
+
const { width, modules } = await qrModules(url);
|
|
763
|
+
const rows = [];
|
|
764
|
+
for (let i = 0; i < modules.length; i += width) {
|
|
765
|
+
rows.push(modules.slice(i, i + width));
|
|
766
|
+
}
|
|
767
|
+
const quiet = 2;
|
|
768
|
+
const fullWidth = width + quiet * 2;
|
|
769
|
+
const blankLine = " ".repeat(fullWidth);
|
|
770
|
+
const lines = [];
|
|
771
|
+
for (let i = 0; i < quiet; i++)
|
|
772
|
+
lines.push(blankLine);
|
|
773
|
+
let y = 0;
|
|
774
|
+
while (y < rows.length) {
|
|
775
|
+
const topRow = rows[y];
|
|
776
|
+
const botRow = y + 1 < rows.length ? rows[y + 1] : null;
|
|
777
|
+
let line = " ".repeat(quiet);
|
|
778
|
+
for (let x = 0; x < width; x++) {
|
|
779
|
+
const top = topRow[x];
|
|
780
|
+
const bot = botRow ? botRow[x] : false;
|
|
781
|
+
if (top && bot)
|
|
782
|
+
line += "\u2588";
|
|
783
|
+
else if (top && !bot)
|
|
784
|
+
line += "\u2580";
|
|
785
|
+
else if (!top && bot)
|
|
786
|
+
line += "\u2584";
|
|
787
|
+
else
|
|
788
|
+
line += " ";
|
|
789
|
+
}
|
|
790
|
+
line += " ".repeat(quiet);
|
|
791
|
+
lines.push(line);
|
|
792
|
+
y += 2;
|
|
793
|
+
}
|
|
794
|
+
for (let i = 0; i < quiet; i++)
|
|
795
|
+
lines.push(blankLine);
|
|
796
|
+
return lines.join("\n");
|
|
797
|
+
}
|
|
798
|
+
var init_qr = __esm({
|
|
799
|
+
"../core/dist/identity/qr.js"() {
|
|
800
|
+
"use strict";
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
748
804
|
// ../core/dist/platform/client.js
|
|
749
805
|
async function bindHttpError(resp, op) {
|
|
750
806
|
const body = await resp.text().catch(() => "");
|
|
@@ -870,62 +926,6 @@ var init_process = __esm({
|
|
|
870
926
|
}
|
|
871
927
|
});
|
|
872
928
|
|
|
873
|
-
// ../core/dist/identity/qr.js
|
|
874
|
-
import QRCode from "qrcode";
|
|
875
|
-
async function qrModules(url) {
|
|
876
|
-
const segments = QRCode.create(url, { errorCorrectionLevel: "M" });
|
|
877
|
-
const size = segments.modules.size;
|
|
878
|
-
const modules = [];
|
|
879
|
-
for (let y = 0; y < size; y++) {
|
|
880
|
-
for (let x = 0; x < size; x++) {
|
|
881
|
-
modules.push(segments.modules.get(x, y) !== 0);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
return { width: size, modules };
|
|
885
|
-
}
|
|
886
|
-
async function qrToTerminal(url) {
|
|
887
|
-
const { width, modules } = await qrModules(url);
|
|
888
|
-
const rows = [];
|
|
889
|
-
for (let i = 0; i < modules.length; i += width) {
|
|
890
|
-
rows.push(modules.slice(i, i + width));
|
|
891
|
-
}
|
|
892
|
-
const quiet = 2;
|
|
893
|
-
const fullWidth = width + quiet * 2;
|
|
894
|
-
const blankLine = " ".repeat(fullWidth);
|
|
895
|
-
const lines = [];
|
|
896
|
-
for (let i = 0; i < quiet; i++)
|
|
897
|
-
lines.push(blankLine);
|
|
898
|
-
let y = 0;
|
|
899
|
-
while (y < rows.length) {
|
|
900
|
-
const topRow = rows[y];
|
|
901
|
-
const botRow = y + 1 < rows.length ? rows[y + 1] : null;
|
|
902
|
-
let line = " ".repeat(quiet);
|
|
903
|
-
for (let x = 0; x < width; x++) {
|
|
904
|
-
const top = topRow[x];
|
|
905
|
-
const bot = botRow ? botRow[x] : false;
|
|
906
|
-
if (top && bot)
|
|
907
|
-
line += "\u2588";
|
|
908
|
-
else if (top && !bot)
|
|
909
|
-
line += "\u2580";
|
|
910
|
-
else if (!top && bot)
|
|
911
|
-
line += "\u2584";
|
|
912
|
-
else
|
|
913
|
-
line += " ";
|
|
914
|
-
}
|
|
915
|
-
line += " ".repeat(quiet);
|
|
916
|
-
lines.push(line);
|
|
917
|
-
y += 2;
|
|
918
|
-
}
|
|
919
|
-
for (let i = 0; i < quiet; i++)
|
|
920
|
-
lines.push(blankLine);
|
|
921
|
-
return lines.join("\n");
|
|
922
|
-
}
|
|
923
|
-
var init_qr = __esm({
|
|
924
|
-
"../core/dist/identity/qr.js"() {
|
|
925
|
-
"use strict";
|
|
926
|
-
}
|
|
927
|
-
});
|
|
928
|
-
|
|
929
929
|
// ../core/dist/bind/orchestrator.js
|
|
930
930
|
async function bindAgent(opts) {
|
|
931
931
|
const log = opts.log ?? ((m) => console.log(m));
|
|
@@ -1112,6 +1112,135 @@ var init_orchestrator = __esm({
|
|
|
1112
1112
|
}
|
|
1113
1113
|
});
|
|
1114
1114
|
|
|
1115
|
+
// ../core/dist/runtime/npm-prefix-self-heal.js
|
|
1116
|
+
function looksLikeEacces(stderr) {
|
|
1117
|
+
const s = stderr.toLowerCase();
|
|
1118
|
+
return s.includes("eacces") || s.includes("eperm") || s.includes("permission denied");
|
|
1119
|
+
}
|
|
1120
|
+
function looksLikeEnospc(stderr) {
|
|
1121
|
+
const s = stderr.toLowerCase();
|
|
1122
|
+
return s.includes("enospc") || s.includes("no space left");
|
|
1123
|
+
}
|
|
1124
|
+
async function trySwitchToUserPrefix(progress) {
|
|
1125
|
+
const p = getPlatformAdapter();
|
|
1126
|
+
const optOut = (p.env("BEEOS_NO_NPM_PREFIX_FIX") ?? "").trim();
|
|
1127
|
+
if (optOut === "1" || optOut.toLowerCase() === "true") {
|
|
1128
|
+
return {
|
|
1129
|
+
success: false,
|
|
1130
|
+
prefix: "",
|
|
1131
|
+
persisted: false,
|
|
1132
|
+
note: "BEEOS_NO_NPM_PREFIX_FIX=1 \u2014 refusing to auto-switch npm prefix."
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const home = p.homeDir();
|
|
1136
|
+
const newPrefix = p.joinPath(home, ".npm-global");
|
|
1137
|
+
const current = await readCurrentPrefix();
|
|
1138
|
+
if (current && current.startsWith(home)) {
|
|
1139
|
+
return {
|
|
1140
|
+
success: true,
|
|
1141
|
+
prefix: current,
|
|
1142
|
+
persisted: false,
|
|
1143
|
+
note: `npm prefix already user-owned: ${current}`
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
progress.onStatus(`Switching npm global prefix to ${newPrefix} (user-owned).`);
|
|
1147
|
+
await p.mkdir(newPrefix);
|
|
1148
|
+
const setRes = await p.exec("npm", ["config", "set", "prefix", newPrefix]);
|
|
1149
|
+
if (setRes.code !== 0) {
|
|
1150
|
+
return {
|
|
1151
|
+
success: false,
|
|
1152
|
+
prefix: newPrefix,
|
|
1153
|
+
persisted: false,
|
|
1154
|
+
note: `npm config set prefix failed: ${(setRes.stderr || setRes.stdout).trim()}`
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
const binDir = p.platform() === "win32" ? newPrefix : p.joinPath(newPrefix, "bin");
|
|
1158
|
+
const pathKey = p.platform() === "win32" ? "Path" : "PATH";
|
|
1159
|
+
const sep = p.platform() === "win32" ? ";" : ":";
|
|
1160
|
+
const existing = process.env[pathKey] ?? process.env.PATH ?? "";
|
|
1161
|
+
if (!existing.split(sep).includes(binDir)) {
|
|
1162
|
+
process.env[pathKey] = `${binDir}${sep}${existing}`;
|
|
1163
|
+
if (pathKey !== "PATH")
|
|
1164
|
+
process.env.PATH = process.env[pathKey];
|
|
1165
|
+
}
|
|
1166
|
+
let persisted = false;
|
|
1167
|
+
if (p.platform() !== "win32") {
|
|
1168
|
+
persisted = await persistShellRcBestEffort(binDir);
|
|
1169
|
+
}
|
|
1170
|
+
return {
|
|
1171
|
+
success: true,
|
|
1172
|
+
prefix: newPrefix,
|
|
1173
|
+
persisted,
|
|
1174
|
+
note: persisted ? `Persisted PATH to your shell rc.` : `PATH updated for this session; add '${binDir}' to your shell init to make it permanent.`
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
async function readCurrentPrefix() {
|
|
1178
|
+
const p = getPlatformAdapter();
|
|
1179
|
+
try {
|
|
1180
|
+
const res = await p.exec("npm", ["prefix", "-g"], { timeout: 1e4 });
|
|
1181
|
+
if (res.code !== 0)
|
|
1182
|
+
return null;
|
|
1183
|
+
const trimmed = res.stdout.trim();
|
|
1184
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1185
|
+
} catch {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
async function persistShellRcBestEffort(binDir) {
|
|
1190
|
+
const p = getPlatformAdapter();
|
|
1191
|
+
const rc = pickRcPath();
|
|
1192
|
+
if (!rc)
|
|
1193
|
+
return false;
|
|
1194
|
+
let existing = "";
|
|
1195
|
+
try {
|
|
1196
|
+
if (await p.exists(rc))
|
|
1197
|
+
existing = await p.readFile(rc);
|
|
1198
|
+
} catch {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
if (existing.includes(SENTINEL_BEGIN)) {
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1205
|
+
const block = `
|
|
1206
|
+
${SENTINEL_BEGIN}
|
|
1207
|
+
# Added by @beeos/core npm-prefix-self-heal on ${stamp}
|
|
1208
|
+
# Reason: lazy install hit EACCES, switched npm prefix to a user-owned dir.
|
|
1209
|
+
# To revert: delete this block AND run \`npm config delete prefix\`.
|
|
1210
|
+
export PATH="${binDir}:$PATH"
|
|
1211
|
+
${SENTINEL_END}
|
|
1212
|
+
`;
|
|
1213
|
+
try {
|
|
1214
|
+
await p.writeFile(rc, existing + block);
|
|
1215
|
+
return true;
|
|
1216
|
+
} catch {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function pickRcPath() {
|
|
1221
|
+
const p = getPlatformAdapter();
|
|
1222
|
+
const home = p.homeDir();
|
|
1223
|
+
const shell = p.env("SHELL") ?? "";
|
|
1224
|
+
const base = shell.split("/").pop() ?? "";
|
|
1225
|
+
switch (base) {
|
|
1226
|
+
case "zsh":
|
|
1227
|
+
return p.joinPath(home, ".zshrc");
|
|
1228
|
+
case "bash":
|
|
1229
|
+
return p.platform() === "darwin" ? p.joinPath(home, ".bash_profile") : p.joinPath(home, ".bashrc");
|
|
1230
|
+
default:
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
var SENTINEL_BEGIN, SENTINEL_END;
|
|
1235
|
+
var init_npm_prefix_self_heal = __esm({
|
|
1236
|
+
"../core/dist/runtime/npm-prefix-self-heal.js"() {
|
|
1237
|
+
"use strict";
|
|
1238
|
+
init_platform_adapter();
|
|
1239
|
+
SENTINEL_BEGIN = "# >>> beeos npm-prefix >>>";
|
|
1240
|
+
SENTINEL_END = "# <<< beeos npm-prefix <<<";
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1115
1244
|
// ../core/dist/upgrade.js
|
|
1116
1245
|
function readPinSourcesFromEnv() {
|
|
1117
1246
|
const env = globalThis.process?.env ?? {};
|
|
@@ -1183,13 +1312,23 @@ async function upgradeBeeosSuite(opts) {
|
|
|
1183
1312
|
if (needsInstall) {
|
|
1184
1313
|
const args = ["install", "-g", ...specs.map((s) => s.spec)];
|
|
1185
1314
|
progress?.onStatus(`npm ${args.join(" ")}`);
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1315
|
+
installFailure = await runNpmInstallOnce(args);
|
|
1316
|
+
if (installFailure && looksLikeEacces(installFailure)) {
|
|
1317
|
+
const reporter = progress ?? {
|
|
1318
|
+
onStatus() {
|
|
1319
|
+
},
|
|
1320
|
+
onComplete() {
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
reporter.onStatus("npm install -g failed with EACCES \u2014 attempting prefix self-heal.");
|
|
1324
|
+
const heal = await trySwitchToUserPrefix(reporter);
|
|
1325
|
+
reporter.onStatus(heal.note);
|
|
1326
|
+
if (heal.success) {
|
|
1327
|
+
reporter.onStatus(`Retrying: npm ${args.join(" ")}`);
|
|
1328
|
+
installFailure = await runNpmInstallOnce(args);
|
|
1190
1329
|
}
|
|
1191
|
-
}
|
|
1192
|
-
installFailure =
|
|
1330
|
+
} else if (installFailure && looksLikeEnospc(installFailure)) {
|
|
1331
|
+
installFailure = "npm install -g failed because the filesystem is out of space (typically /usr/local/lib/node_modules or %APPDATA%\\npm). Free disk space and retry. Original error:\n" + installFailure;
|
|
1193
1332
|
}
|
|
1194
1333
|
} else {
|
|
1195
1334
|
progress?.onStatus("Already up to date \u2014 skipping npm install -g");
|
|
@@ -1220,6 +1359,58 @@ async function upgradeBeeosSuite(opts) {
|
|
|
1220
1359
|
}
|
|
1221
1360
|
return result;
|
|
1222
1361
|
}
|
|
1362
|
+
async function runNpmInstallOnce(args) {
|
|
1363
|
+
const p = getPlatformAdapter();
|
|
1364
|
+
const useInherit = shouldInheritStdio();
|
|
1365
|
+
try {
|
|
1366
|
+
const result = await p.exec("npm", args, {
|
|
1367
|
+
timeout: 6e5,
|
|
1368
|
+
stdio: useInherit ? "inherit" : "pipe"
|
|
1369
|
+
});
|
|
1370
|
+
if (result.code !== 0) {
|
|
1371
|
+
if (useInherit) {
|
|
1372
|
+
const probe = await probeEaccesViaSet().catch(() => null);
|
|
1373
|
+
if (probe)
|
|
1374
|
+
return `npm install -g exited ${result.code}; ${probe}`;
|
|
1375
|
+
return `npm install -g exited ${result.code}`;
|
|
1376
|
+
}
|
|
1377
|
+
return (result.stderr || result.stdout || `npm install -g exited ${result.code}`).trim();
|
|
1378
|
+
}
|
|
1379
|
+
return void 0;
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
return e instanceof Error ? e.message : String(e);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
function shouldInheritStdio() {
|
|
1385
|
+
const proc = globalThis.process;
|
|
1386
|
+
if (!proc)
|
|
1387
|
+
return false;
|
|
1388
|
+
const isTty = Boolean(proc.stdout?.isTTY);
|
|
1389
|
+
if (!isTty)
|
|
1390
|
+
return false;
|
|
1391
|
+
const env = proc.env ?? {};
|
|
1392
|
+
if (env.BEEOS_NPM_QUIET === "1")
|
|
1393
|
+
return false;
|
|
1394
|
+
const argv = proc.argv ?? [];
|
|
1395
|
+
if (argv.some((a) => a === "--json"))
|
|
1396
|
+
return false;
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
async function probeEaccesViaSet() {
|
|
1400
|
+
const p = getPlatformAdapter();
|
|
1401
|
+
const res = await p.exec("npm", ["config", "get", "prefix"], {
|
|
1402
|
+
timeout: 1e4
|
|
1403
|
+
});
|
|
1404
|
+
if (res.code !== 0)
|
|
1405
|
+
return null;
|
|
1406
|
+
const prefix = res.stdout.trim();
|
|
1407
|
+
if (!prefix)
|
|
1408
|
+
return null;
|
|
1409
|
+
const home = p.homeDir();
|
|
1410
|
+
if (prefix.startsWith(home))
|
|
1411
|
+
return null;
|
|
1412
|
+
return `EACCES likely (prefix ${prefix} outside $HOME)`;
|
|
1413
|
+
}
|
|
1223
1414
|
function trimOrUndef(v) {
|
|
1224
1415
|
if (v === void 0)
|
|
1225
1416
|
return void 0;
|
|
@@ -1231,6 +1422,7 @@ var init_upgrade = __esm({
|
|
|
1231
1422
|
"../core/dist/upgrade.js"() {
|
|
1232
1423
|
"use strict";
|
|
1233
1424
|
init_platform_adapter();
|
|
1425
|
+
init_npm_prefix_self_heal();
|
|
1234
1426
|
NPM_PKGS = {
|
|
1235
1427
|
CLI: "@beeos-ai/cli",
|
|
1236
1428
|
DEVICE_AGENT: "@beeos-ai/device-agent",
|
|
@@ -1371,6 +1563,75 @@ var init_device_setup = __esm({
|
|
|
1371
1563
|
}
|
|
1372
1564
|
});
|
|
1373
1565
|
|
|
1566
|
+
// ../core/dist/runtime/http-fetch-retry.js
|
|
1567
|
+
async function fetchWithRetry(url, opts) {
|
|
1568
|
+
const p = getPlatformAdapter();
|
|
1569
|
+
const max = opts.maxAttempts ?? 3;
|
|
1570
|
+
let lastError = "";
|
|
1571
|
+
let lastStatus = null;
|
|
1572
|
+
for (let attempt = 1; attempt <= max; attempt++) {
|
|
1573
|
+
let resp;
|
|
1574
|
+
try {
|
|
1575
|
+
resp = await p.fetch(url);
|
|
1576
|
+
} catch (e) {
|
|
1577
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
1578
|
+
opts.progress.onStatus(`${opts.label} attempt ${attempt}/${max} failed: ${lastError}`);
|
|
1579
|
+
if (attempt < max)
|
|
1580
|
+
await sleepBackoff(attempt);
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
if (!resp.ok) {
|
|
1584
|
+
lastStatus = resp.status;
|
|
1585
|
+
opts.progress.onStatus(`${opts.label} attempt ${attempt}/${max}: HTTP ${resp.status} ${resp.statusText}`);
|
|
1586
|
+
if (resp.status === 401 || resp.status === 403 || resp.status === 404) {
|
|
1587
|
+
return {
|
|
1588
|
+
ok: false,
|
|
1589
|
+
status: resp.status,
|
|
1590
|
+
lastError: `HTTP ${resp.status} ${resp.statusText}`
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
lastError = `HTTP ${resp.status} ${resp.statusText}`;
|
|
1594
|
+
if (attempt < max)
|
|
1595
|
+
await sleepBackoff(attempt);
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
try {
|
|
1599
|
+
return { ok: true, bytes: new Uint8Array(await resp.arrayBuffer()) };
|
|
1600
|
+
} catch (e) {
|
|
1601
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
1602
|
+
opts.progress.onStatus(`${opts.label} stream interrupted on attempt ${attempt}/${max}: ${lastError}`);
|
|
1603
|
+
if (attempt < max)
|
|
1604
|
+
await sleepBackoff(attempt);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
opts.progress.onStatus(`${opts.label} gave up after ${max} attempts (last: ${lastError || "unknown"}).`);
|
|
1608
|
+
return { ok: false, status: lastStatus, lastError };
|
|
1609
|
+
}
|
|
1610
|
+
async function fetchWithRetryOrNull(url, opts) {
|
|
1611
|
+
const out = await fetchWithRetry(url, opts);
|
|
1612
|
+
return out.ok ? out.bytes : null;
|
|
1613
|
+
}
|
|
1614
|
+
async function fetchTextWithRetryOrNull(url, opts) {
|
|
1615
|
+
const bytes = await fetchWithRetryOrNull(url, opts);
|
|
1616
|
+
if (!bytes)
|
|
1617
|
+
return null;
|
|
1618
|
+
try {
|
|
1619
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
1620
|
+
} catch {
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
function sleepBackoff(attempt) {
|
|
1625
|
+
const ms = 1e3 * Math.pow(2, attempt - 1);
|
|
1626
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1627
|
+
}
|
|
1628
|
+
var init_http_fetch_retry = __esm({
|
|
1629
|
+
"../core/dist/runtime/http-fetch-retry.js"() {
|
|
1630
|
+
"use strict";
|
|
1631
|
+
init_platform_adapter();
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1374
1635
|
// ../core/dist/adb-setup.js
|
|
1375
1636
|
import { createHash } from "crypto";
|
|
1376
1637
|
function managedAdbPath() {
|
|
@@ -1408,11 +1669,14 @@ async function installAdb(progress) {
|
|
|
1408
1669
|
}
|
|
1409
1670
|
const url = `${PLATFORM_TOOLS_BASE}/${zipName}`;
|
|
1410
1671
|
progress.onStatus(`Downloading Android platform-tools (${zipName})...`);
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1672
|
+
const result = await fetchWithRetry(url, {
|
|
1673
|
+
label: "platform-tools",
|
|
1674
|
+
progress
|
|
1675
|
+
});
|
|
1676
|
+
if (!result.ok) {
|
|
1677
|
+
throw new Error(`platform-tools download failed: ${result.lastError}` + (result.status ? ` (HTTP ${result.status})` : ""));
|
|
1414
1678
|
}
|
|
1415
|
-
const data =
|
|
1679
|
+
const data = result.bytes;
|
|
1416
1680
|
verifyPlatformToolsChecksum(zipName, data, progress);
|
|
1417
1681
|
const binDir = p.joinPath(beeoHome(), "bin");
|
|
1418
1682
|
await p.mkdir(binDir);
|
|
@@ -1511,6 +1775,7 @@ var init_adb_setup = __esm({
|
|
|
1511
1775
|
init_platform_adapter();
|
|
1512
1776
|
init_paths();
|
|
1513
1777
|
init_errors();
|
|
1778
|
+
init_http_fetch_retry();
|
|
1514
1779
|
PLATFORM_TOOLS_BASE = "https://dl.google.com/android/repository";
|
|
1515
1780
|
PLATFORM_TOOLS_REVISION = "r37.0.0";
|
|
1516
1781
|
PLATFORM_TOOLS_ARCHIVE = {
|
|
@@ -1797,46 +2062,16 @@ function archiveName(cfg, target) {
|
|
|
1797
2062
|
const suffix = target.includes("windows") ? "zip" : "tar.gz";
|
|
1798
2063
|
return `${cfg.name}-${target}.${suffix}`;
|
|
1799
2064
|
}
|
|
1800
|
-
async function fetchArchiveBuffer(cfg, url, progress
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
progress.onStatus(`${cfg.name} download attempt ${attempt}/${maxAttempts} failed: ${lastError}`);
|
|
1810
|
-
if (attempt < maxAttempts) {
|
|
1811
|
-
await new Promise((r) => setTimeout(r, 1e3 * 2 ** (attempt - 1)));
|
|
1812
|
-
}
|
|
1813
|
-
continue;
|
|
1814
|
-
}
|
|
1815
|
-
if (!resp.ok) {
|
|
1816
|
-
progress.onStatus(`${cfg.name} download: ${resp.status} ${resp.statusText} (${url})`);
|
|
1817
|
-
if (resp.status === 404) {
|
|
1818
|
-
progress.onStatus(`Hint: the current release may not include the requested target. Try setting $${cfg.binaryEnv} to a locally built binary, ` + (cfg.releaseUrlEnv ? `or $${cfg.releaseUrlEnv} to a different release URL.` : `or check for a newer release.`));
|
|
1819
|
-
}
|
|
1820
|
-
if (resp.status === 404 || resp.status === 403 || resp.status === 401) {
|
|
1821
|
-
return null;
|
|
1822
|
-
}
|
|
1823
|
-
lastError = `${resp.status} ${resp.statusText}`;
|
|
1824
|
-
if (attempt < maxAttempts) {
|
|
1825
|
-
await new Promise((r) => setTimeout(r, 1e3 * 2 ** (attempt - 1)));
|
|
1826
|
-
}
|
|
1827
|
-
continue;
|
|
1828
|
-
}
|
|
1829
|
-
try {
|
|
1830
|
-
return new Uint8Array(await resp.arrayBuffer());
|
|
1831
|
-
} catch (e) {
|
|
1832
|
-
lastError = String(e);
|
|
1833
|
-
progress.onStatus(`${cfg.name} stream interrupted on attempt ${attempt}/${maxAttempts}: ${lastError}`);
|
|
1834
|
-
if (attempt < maxAttempts) {
|
|
1835
|
-
await new Promise((r) => setTimeout(r, 1e3 * 2 ** (attempt - 1)));
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
2065
|
+
async function fetchArchiveBuffer(cfg, url, progress) {
|
|
2066
|
+
const out = await fetchWithRetry(url, {
|
|
2067
|
+
label: cfg.name,
|
|
2068
|
+
progress
|
|
2069
|
+
});
|
|
2070
|
+
if (out.ok)
|
|
2071
|
+
return out.bytes;
|
|
2072
|
+
if (out.status === 404) {
|
|
2073
|
+
progress.onStatus(`Hint: the current release may not include the requested target. Try setting $${cfg.binaryEnv} to a locally built binary, ` + (cfg.releaseUrlEnv ? `or $${cfg.releaseUrlEnv} to a different release URL.` : `or check for a newer release.`));
|
|
1838
2074
|
}
|
|
1839
|
-
progress.onStatus(`${cfg.name} download gave up after ${maxAttempts} attempts (last: ${lastError ?? "unknown"}).`);
|
|
1840
2075
|
return null;
|
|
1841
2076
|
}
|
|
1842
2077
|
async function downloadManagedBinary(cfg, target, progress) {
|
|
@@ -1851,20 +2086,19 @@ async function downloadManagedBinary(cfg, target, progress) {
|
|
|
1851
2086
|
const allowUnverified = (process.env?.BEEOS_ALLOW_UNVERIFIED_SIDECAR ?? "").toLowerCase() === "1" || (process.env?.BEEOS_ALLOW_UNVERIFIED_SIDECAR ?? "").toLowerCase() === "true";
|
|
1852
2087
|
let expected = null;
|
|
1853
2088
|
let digestFetchError = null;
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
digestFetchError = `
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
digestFetchError = e instanceof Error ? e.message : String(e);
|
|
2089
|
+
const digestText = await fetchTextWithRetryOrNull(digestUrl, {
|
|
2090
|
+
label: `${cfg.name}.sha256`,
|
|
2091
|
+
progress
|
|
2092
|
+
});
|
|
2093
|
+
if (digestText !== null) {
|
|
2094
|
+
const trimmed = digestText.trim();
|
|
2095
|
+
const first = trimmed.split(/\s+/)[0] ?? "";
|
|
2096
|
+
if (/^[a-f0-9]{64}$/i.test(first))
|
|
2097
|
+
expected = first.toLowerCase();
|
|
2098
|
+
else
|
|
2099
|
+
digestFetchError = `malformed .sha256 body: ${trimmed.slice(0, 80)}`;
|
|
2100
|
+
} else {
|
|
2101
|
+
digestFetchError = "fetch failed (see attempts above)";
|
|
1868
2102
|
}
|
|
1869
2103
|
if (expected) {
|
|
1870
2104
|
const actual = createHash2("sha256").update(data).digest("hex");
|
|
@@ -1955,6 +2189,7 @@ var init_cargo_dist = __esm({
|
|
|
1955
2189
|
init_platform_adapter();
|
|
1956
2190
|
init_paths();
|
|
1957
2191
|
init_errors();
|
|
2192
|
+
init_http_fetch_retry();
|
|
1958
2193
|
}
|
|
1959
2194
|
});
|
|
1960
2195
|
|
|
@@ -2301,8 +2536,8 @@ function normalizeOpenAiBaseUrl(input) {
|
|
|
2301
2536
|
} catch {
|
|
2302
2537
|
return input;
|
|
2303
2538
|
}
|
|
2304
|
-
const
|
|
2305
|
-
if (
|
|
2539
|
+
const path12 = url.pathname.replace(/\/+$/, "");
|
|
2540
|
+
if (path12 === "" || path12 === "/") {
|
|
2306
2541
|
url.pathname = "/v1";
|
|
2307
2542
|
return url.toString().replace(/\/+$/, "");
|
|
2308
2543
|
}
|
|
@@ -2637,7 +2872,7 @@ async function identifyGateway(opts) {
|
|
|
2637
2872
|
const url = `http://${host}:${port}/beeos/status`;
|
|
2638
2873
|
const controller = globalThis.AbortController ? new AbortController() : null;
|
|
2639
2874
|
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
2640
|
-
let resp
|
|
2875
|
+
let resp;
|
|
2641
2876
|
try {
|
|
2642
2877
|
resp = await p.fetch(url, {
|
|
2643
2878
|
method: "GET",
|
|
@@ -2799,12 +3034,12 @@ async function generateOpenclawConfig(ctx) {
|
|
|
2799
3034
|
seedResult = await p.exec(ctx.agentBinary, ["setup", "--non-interactive", "--mode", "local"], { env });
|
|
2800
3035
|
}
|
|
2801
3036
|
if (seedResult.code === 0) {
|
|
2802
|
-
for (const [
|
|
3037
|
+
for (const [path12, value] of [
|
|
2803
3038
|
["gateway.auth.token", ctx.gatewayToken],
|
|
2804
3039
|
["gateway.remote.token", ctx.gatewayToken],
|
|
2805
3040
|
["gateway.bind", "lan"]
|
|
2806
3041
|
]) {
|
|
2807
|
-
await runConfigSet(ctx.agentBinary, ctx.agentHome,
|
|
3042
|
+
await runConfigSet(ctx.agentBinary, ctx.agentHome, path12, value, false, false);
|
|
2808
3043
|
}
|
|
2809
3044
|
await runConfigSet(ctx.agentBinary, ctx.agentHome, "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback", "true", true, false);
|
|
2810
3045
|
await configurePluginViaCli(ctx);
|
|
@@ -2869,9 +3104,9 @@ async function writeConfigFallback(ctx) {
|
|
|
2869
3104
|
await p.writeFile(configPath2, JSON.stringify(config, null, 2));
|
|
2870
3105
|
await writePairedJson(ctx.agentHome, ctx.keyFile);
|
|
2871
3106
|
}
|
|
2872
|
-
async function runConfigSet(bin, home,
|
|
3107
|
+
async function runConfigSet(bin, home, path12, value, json, isSystemHome) {
|
|
2873
3108
|
const p = getPlatformAdapter();
|
|
2874
|
-
const args = ["config", "set",
|
|
3109
|
+
const args = ["config", "set", path12, value];
|
|
2875
3110
|
if (json)
|
|
2876
3111
|
args.push("--json");
|
|
2877
3112
|
const env = {};
|
|
@@ -2879,7 +3114,7 @@ async function runConfigSet(bin, home, path10, value, json, isSystemHome) {
|
|
|
2879
3114
|
env.OPENCLAW_STATE_DIR = home;
|
|
2880
3115
|
const result = await p.exec(bin, args, { env });
|
|
2881
3116
|
if (result.code !== 0) {
|
|
2882
|
-
throw new Error(`\`openclaw config set ${
|
|
3117
|
+
throw new Error(`\`openclaw config set ${path12}\` exited with ${result.code}`);
|
|
2883
3118
|
}
|
|
2884
3119
|
}
|
|
2885
3120
|
async function writePairedJson(home, keyFile) {
|
|
@@ -3512,7 +3747,7 @@ async function detectColdStartState() {
|
|
|
3512
3747
|
const p = getPlatformAdapter();
|
|
3513
3748
|
const port5900Listening = await p.tcpProbe("127.0.0.1", 5900, PORT_5900_PROBE_TIMEOUT_MS).catch(() => false);
|
|
3514
3749
|
const passwordFile = p.joinPath(beeoHome(), "vnc.password");
|
|
3515
|
-
let vncPasswordFileExists
|
|
3750
|
+
let vncPasswordFileExists;
|
|
3516
3751
|
try {
|
|
3517
3752
|
const raw = await p.readFile(passwordFile);
|
|
3518
3753
|
vncPasswordFileExists = raw.trim().length > 0;
|
|
@@ -3648,8 +3883,8 @@ launchctl kickstart -k system/com.apple.screensharing
|
|
|
3648
3883
|
`;
|
|
3649
3884
|
const aslEscaped = shellPayload.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
3650
3885
|
const osascriptArg = `do shell script "${aslEscaped}" with administrator privileges`;
|
|
3651
|
-
let stderr
|
|
3652
|
-
let code
|
|
3886
|
+
let stderr;
|
|
3887
|
+
let code;
|
|
3653
3888
|
try {
|
|
3654
3889
|
const result = await p.exec("osascript", ["-e", osascriptArg], {
|
|
3655
3890
|
timeout: OSASCRIPT_TIMEOUT_MS
|
|
@@ -3782,7 +4017,7 @@ async function detectLinuxColdStartState() {
|
|
|
3782
4017
|
const p = getPlatformAdapter();
|
|
3783
4018
|
const port5901Listening = await p.tcpProbe("127.0.0.1", LINUX_VNC_PORT, PORT_PROBE_TIMEOUT_MS).catch(() => false);
|
|
3784
4019
|
const beeoFile = p.joinPath(beeoHome(), "vnc.password");
|
|
3785
|
-
let beeoPasswordFileExists
|
|
4020
|
+
let beeoPasswordFileExists;
|
|
3786
4021
|
try {
|
|
3787
4022
|
const raw = await p.readFile(beeoFile);
|
|
3788
4023
|
beeoPasswordFileExists = raw.trim().length > 0;
|
|
@@ -3790,7 +4025,7 @@ async function detectLinuxColdStartState() {
|
|
|
3790
4025
|
beeoPasswordFileExists = false;
|
|
3791
4026
|
}
|
|
3792
4027
|
const systemFile = p.joinPath(p.homeDir(), ".vnc", "passwd");
|
|
3793
|
-
let systemPasswordFileExists
|
|
4028
|
+
let systemPasswordFileExists;
|
|
3794
4029
|
try {
|
|
3795
4030
|
const raw = await p.readFile(systemFile);
|
|
3796
4031
|
systemPasswordFileExists = raw.length > 0;
|
|
@@ -4050,7 +4285,7 @@ async function detectWindowsColdStartState() {
|
|
|
4050
4285
|
const p = getPlatformAdapter();
|
|
4051
4286
|
const port5900Listening = await p.tcpProbe("127.0.0.1", WIN_VNC_PORT, PORT_PROBE_TIMEOUT_MS2).catch(() => false);
|
|
4052
4287
|
const beeoFile = p.joinPath(beeoHome(), "vnc.password");
|
|
4053
|
-
let beeoPasswordFileExists
|
|
4288
|
+
let beeoPasswordFileExists;
|
|
4054
4289
|
try {
|
|
4055
4290
|
const raw = await p.readFile(beeoFile);
|
|
4056
4291
|
beeoPasswordFileExists = raw.trim().length > 0;
|
|
@@ -5923,7 +6158,7 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
|
5923
6158
|
return false;
|
|
5924
6159
|
}
|
|
5925
6160
|
}
|
|
5926
|
-
let stale
|
|
6161
|
+
let stale;
|
|
5927
6162
|
try {
|
|
5928
6163
|
const stat = await fs5.stat(lockFile);
|
|
5929
6164
|
stale = Date.now() - stat.mtimeMs > STALE_LOCK_AGE_MS;
|
|
@@ -6167,6 +6402,7 @@ var init_dist = __esm({
|
|
|
6167
6402
|
init_gateway_token();
|
|
6168
6403
|
init_state();
|
|
6169
6404
|
init_keypair();
|
|
6405
|
+
init_qr();
|
|
6170
6406
|
init_client();
|
|
6171
6407
|
init_orchestrator();
|
|
6172
6408
|
init_process();
|
|
@@ -6185,6 +6421,10 @@ var init_dist = __esm({
|
|
|
6185
6421
|
init_driver2();
|
|
6186
6422
|
init_constants();
|
|
6187
6423
|
init_agent_status();
|
|
6424
|
+
init_config();
|
|
6425
|
+
init_download();
|
|
6426
|
+
init_plugin();
|
|
6427
|
+
init_token();
|
|
6188
6428
|
init_desktop_detect();
|
|
6189
6429
|
init_macos_desktop_cold_start();
|
|
6190
6430
|
init_desktop();
|
|
@@ -6197,6 +6437,8 @@ var init_dist = __esm({
|
|
|
6197
6437
|
init_tail_logs();
|
|
6198
6438
|
init_cli_version();
|
|
6199
6439
|
init_upgrade();
|
|
6440
|
+
init_npm_prefix_self_heal();
|
|
6441
|
+
init_http_fetch_retry();
|
|
6200
6442
|
}
|
|
6201
6443
|
});
|
|
6202
6444
|
|
|
@@ -6341,6 +6583,60 @@ function printFleetNotRunningHint() {
|
|
|
6341
6583
|
console.log(" device-agent fleet start # foreground (debug-friendly)");
|
|
6342
6584
|
console.log("");
|
|
6343
6585
|
}
|
|
6586
|
+
async function getFleetDeviceSnapshotBestEffort(serial, baseUrl = FLEET_STATUS_BASE_URL) {
|
|
6587
|
+
let res;
|
|
6588
|
+
try {
|
|
6589
|
+
res = await fetch(
|
|
6590
|
+
`${baseUrl}/fleet/devices/${encodeURIComponent(serial)}`,
|
|
6591
|
+
{
|
|
6592
|
+
method: "GET",
|
|
6593
|
+
signal: AbortSignal.timeout(FLEET_NOTIFY_TIMEOUT_MS)
|
|
6594
|
+
}
|
|
6595
|
+
);
|
|
6596
|
+
} catch {
|
|
6597
|
+
return null;
|
|
6598
|
+
}
|
|
6599
|
+
if (!res.ok) {
|
|
6600
|
+
return null;
|
|
6601
|
+
}
|
|
6602
|
+
let body;
|
|
6603
|
+
try {
|
|
6604
|
+
body = await res.json();
|
|
6605
|
+
} catch {
|
|
6606
|
+
return null;
|
|
6607
|
+
}
|
|
6608
|
+
return parseFleetDeviceSnapshot(body);
|
|
6609
|
+
}
|
|
6610
|
+
function parseFleetDeviceSnapshot(raw) {
|
|
6611
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
6612
|
+
const r = raw;
|
|
6613
|
+
if (typeof r.serial !== "string" || r.serial.length === 0) return null;
|
|
6614
|
+
if (!isFleetDeviceState(r.state)) return null;
|
|
6615
|
+
return {
|
|
6616
|
+
serial: r.serial,
|
|
6617
|
+
state: r.state,
|
|
6618
|
+
instanceId: typeof r.instanceId === "string" ? r.instanceId : null,
|
|
6619
|
+
mcpPid: typeof r.mcpPid === "number" ? r.mcpPid : null,
|
|
6620
|
+
daemonPid: typeof r.daemonPid === "number" ? r.daemonPid : null,
|
|
6621
|
+
sinceMs: typeof r.sinceMs === "number" ? r.sinceMs : 0
|
|
6622
|
+
};
|
|
6623
|
+
}
|
|
6624
|
+
function isFleetDeviceState(v) {
|
|
6625
|
+
switch (v) {
|
|
6626
|
+
case "adb_offline":
|
|
6627
|
+
case "adb_unauthorized":
|
|
6628
|
+
case "awaiting_bind":
|
|
6629
|
+
case "spawning":
|
|
6630
|
+
case "healthy":
|
|
6631
|
+
case "grace":
|
|
6632
|
+
case "degraded":
|
|
6633
|
+
case "gone":
|
|
6634
|
+
case "crashed":
|
|
6635
|
+
return true;
|
|
6636
|
+
default:
|
|
6637
|
+
return false;
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6344
6640
|
var FLEET_STATUS_BASE_URL, FLEET_NOTIFY_TIMEOUT_MS, FLEET_SHUTDOWN_TIMEOUT_MS;
|
|
6345
6641
|
var init_fleet_notify = __esm({
|
|
6346
6642
|
"src/commands/device/fleet-notify.ts"() {
|
|
@@ -6613,6 +6909,169 @@ var init_instance_picker = __esm({
|
|
|
6613
6909
|
}
|
|
6614
6910
|
});
|
|
6615
6911
|
|
|
6912
|
+
// src/commands/device/wait-for-fleet-healthy.ts
|
|
6913
|
+
import ora2 from "ora";
|
|
6914
|
+
async function waitForFleetHealthy(opts) {
|
|
6915
|
+
const timeoutMs = resolveTimeoutMs(opts.timeoutMs);
|
|
6916
|
+
if (timeoutMs <= 0) {
|
|
6917
|
+
return { kind: "skipped", reason: "opt_out" };
|
|
6918
|
+
}
|
|
6919
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
6920
|
+
const useSpinner = (opts.spinner ?? true) && Boolean(process.stdout?.isTTY);
|
|
6921
|
+
const reporter = useSpinner ? new SpinnerReporter() : new BareReporter();
|
|
6922
|
+
reporter.start(opts.serial, timeoutMs);
|
|
6923
|
+
const startedAt = Date.now();
|
|
6924
|
+
let nullStreak = 0;
|
|
6925
|
+
let lastSnapshot = null;
|
|
6926
|
+
try {
|
|
6927
|
+
while (true) {
|
|
6928
|
+
const snapshot = await getFleetDeviceSnapshotBestEffort(
|
|
6929
|
+
opts.serial,
|
|
6930
|
+
opts.fleetBaseUrl
|
|
6931
|
+
);
|
|
6932
|
+
const elapsedMs = Date.now() - startedAt;
|
|
6933
|
+
if (snapshot === null) {
|
|
6934
|
+
nullStreak += 1;
|
|
6935
|
+
if (nullStreak >= FLEET_DEAD_NULL_THRESHOLD) {
|
|
6936
|
+
reporter.skipFleetDown();
|
|
6937
|
+
return { kind: "skipped", reason: "fleet_not_running" };
|
|
6938
|
+
}
|
|
6939
|
+
} else {
|
|
6940
|
+
nullStreak = 0;
|
|
6941
|
+
lastSnapshot = snapshot;
|
|
6942
|
+
if (snapshot.state === "healthy") {
|
|
6943
|
+
reporter.online(snapshot, elapsedMs);
|
|
6944
|
+
return { kind: "online", elapsedMs, snapshot };
|
|
6945
|
+
}
|
|
6946
|
+
reporter.update(snapshot, elapsedMs);
|
|
6947
|
+
}
|
|
6948
|
+
if (elapsedMs + intervalMs >= timeoutMs) {
|
|
6949
|
+
reporter.timeout(lastSnapshot, elapsedMs);
|
|
6950
|
+
return { kind: "timeout", elapsedMs, lastSnapshot };
|
|
6951
|
+
}
|
|
6952
|
+
await sleep6(intervalMs);
|
|
6953
|
+
}
|
|
6954
|
+
} finally {
|
|
6955
|
+
reporter.dispose();
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
6958
|
+
function resolveTimeoutMs(optsTimeoutMs) {
|
|
6959
|
+
const envRaw = process.env.BEEOS_AGENT_WAIT_TIMEOUT_MS;
|
|
6960
|
+
if (envRaw !== void 0 && envRaw !== "") {
|
|
6961
|
+
const parsed = Number(envRaw);
|
|
6962
|
+
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
6963
|
+
}
|
|
6964
|
+
if (process.env.BEEOS_NO_WAIT_AGENT === "1") return 0;
|
|
6965
|
+
if (optsTimeoutMs !== void 0) return optsTimeoutMs;
|
|
6966
|
+
return DEFAULT_TIMEOUT_MS2;
|
|
6967
|
+
}
|
|
6968
|
+
function sleep6(ms) {
|
|
6969
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6970
|
+
}
|
|
6971
|
+
function formatSec(ms) {
|
|
6972
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
6973
|
+
}
|
|
6974
|
+
function formatOnline(snapshot, elapsedMs) {
|
|
6975
|
+
const mcp = snapshot.mcpPid ?? "?";
|
|
6976
|
+
const daemon = snapshot.daemonPid ?? "?";
|
|
6977
|
+
return `Device fleet healthy: mcp_pid=${mcp} daemon_pid=${daemon} (${formatSec(elapsedMs)})`;
|
|
6978
|
+
}
|
|
6979
|
+
function formatTimeout(serial, lastSnapshot, elapsedMs) {
|
|
6980
|
+
const state = lastSnapshot?.state ?? "unknown";
|
|
6981
|
+
const daemonPid = lastSnapshot?.daemonPid ?? null;
|
|
6982
|
+
return `Waiting for ${serial} healthy timed out after ${formatSec(elapsedMs)} \u2014 current state: ${state}, daemon_pid=${daemonPid ?? "null"}`;
|
|
6983
|
+
}
|
|
6984
|
+
var DEFAULT_TIMEOUT_MS2, DEFAULT_INTERVAL_MS, FLEET_DEAD_NULL_THRESHOLD, SpinnerReporter, BareReporter;
|
|
6985
|
+
var init_wait_for_fleet_healthy = __esm({
|
|
6986
|
+
"src/commands/device/wait-for-fleet-healthy.ts"() {
|
|
6987
|
+
"use strict";
|
|
6988
|
+
init_fleet_notify();
|
|
6989
|
+
DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
6990
|
+
DEFAULT_INTERVAL_MS = 1e3;
|
|
6991
|
+
FLEET_DEAD_NULL_THRESHOLD = 3;
|
|
6992
|
+
SpinnerReporter = class {
|
|
6993
|
+
spinner = null;
|
|
6994
|
+
serial = "";
|
|
6995
|
+
start(serial, timeoutMs) {
|
|
6996
|
+
this.serial = serial;
|
|
6997
|
+
this.spinner = ora2({
|
|
6998
|
+
text: `Waiting for device-agent fleet to mark ${serial} healthy (timeout=${formatSec(timeoutMs)})...`,
|
|
6999
|
+
stream: process.stderr
|
|
7000
|
+
}).start();
|
|
7001
|
+
}
|
|
7002
|
+
update(snapshot, elapsedMs) {
|
|
7003
|
+
if (!this.spinner) return;
|
|
7004
|
+
this.spinner.text = `Waiting for ${this.serial} healthy \u2014 current state=${snapshot.state} (${formatSec(elapsedMs)})`;
|
|
7005
|
+
}
|
|
7006
|
+
online(snapshot, elapsedMs) {
|
|
7007
|
+
if (!this.spinner) return;
|
|
7008
|
+
this.spinner.succeed(formatOnline(snapshot, elapsedMs));
|
|
7009
|
+
this.spinner = null;
|
|
7010
|
+
console.log(
|
|
7011
|
+
" Agent may take ~1-2s more to register on dashboard."
|
|
7012
|
+
);
|
|
7013
|
+
}
|
|
7014
|
+
timeout(lastSnapshot, elapsedMs) {
|
|
7015
|
+
if (!this.spinner) return;
|
|
7016
|
+
this.spinner.warn(formatTimeout(this.serial, lastSnapshot, elapsedMs));
|
|
7017
|
+
this.spinner = null;
|
|
7018
|
+
console.log(
|
|
7019
|
+
` See ~/.beeos/logs/services/device-agent-${this.serial}.log for details.`
|
|
7020
|
+
);
|
|
7021
|
+
}
|
|
7022
|
+
skipFleetDown() {
|
|
7023
|
+
if (!this.spinner) return;
|
|
7024
|
+
this.spinner.info(
|
|
7025
|
+
"Skipped fleet wait \u2014 supervisor not running. Run `beeos start` or `beeos doctor`."
|
|
7026
|
+
);
|
|
7027
|
+
this.spinner = null;
|
|
7028
|
+
}
|
|
7029
|
+
dispose() {
|
|
7030
|
+
if (this.spinner) {
|
|
7031
|
+
this.spinner.stop();
|
|
7032
|
+
this.spinner = null;
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
};
|
|
7036
|
+
BareReporter = class {
|
|
7037
|
+
serial = "";
|
|
7038
|
+
lastState = null;
|
|
7039
|
+
start(serial, timeoutMs) {
|
|
7040
|
+
this.serial = serial;
|
|
7041
|
+
console.log(
|
|
7042
|
+
`Waiting for device-agent fleet to mark ${serial} healthy (timeout=${formatSec(timeoutMs)})...`
|
|
7043
|
+
);
|
|
7044
|
+
}
|
|
7045
|
+
update(snapshot, elapsedMs) {
|
|
7046
|
+
if (snapshot.state === this.lastState) return;
|
|
7047
|
+
this.lastState = snapshot.state;
|
|
7048
|
+
console.log(
|
|
7049
|
+
`[${formatSec(elapsedMs)}] ${this.serial} state=${snapshot.state}`
|
|
7050
|
+
);
|
|
7051
|
+
}
|
|
7052
|
+
online(snapshot, elapsedMs) {
|
|
7053
|
+
console.log(`\u2713 ${formatOnline(snapshot, elapsedMs)}`);
|
|
7054
|
+
console.log(
|
|
7055
|
+
" Agent may take ~1-2s more to register on dashboard."
|
|
7056
|
+
);
|
|
7057
|
+
}
|
|
7058
|
+
timeout(lastSnapshot, elapsedMs) {
|
|
7059
|
+
console.log(`\u26A0 ${formatTimeout(this.serial, lastSnapshot, elapsedMs)}`);
|
|
7060
|
+
console.log(
|
|
7061
|
+
` See ~/.beeos/logs/services/device-agent-${this.serial}.log for details.`
|
|
7062
|
+
);
|
|
7063
|
+
}
|
|
7064
|
+
skipFleetDown() {
|
|
7065
|
+
console.log(
|
|
7066
|
+
"Skipped fleet wait \u2014 supervisor not running. Run `beeos start` or `beeos doctor`."
|
|
7067
|
+
);
|
|
7068
|
+
}
|
|
7069
|
+
dispose() {
|
|
7070
|
+
}
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
7073
|
+
});
|
|
7074
|
+
|
|
6616
7075
|
// src/commands/device/state.ts
|
|
6617
7076
|
import lockfile from "proper-lockfile";
|
|
6618
7077
|
function deviceAgentTargetId(serial) {
|
|
@@ -6630,10 +7089,10 @@ function devicesPath() {
|
|
|
6630
7089
|
}
|
|
6631
7090
|
async function loadDeviceState() {
|
|
6632
7091
|
const p = getPlatformAdapter();
|
|
6633
|
-
const
|
|
6634
|
-
if (!await p.exists(
|
|
7092
|
+
const path12 = devicesPath();
|
|
7093
|
+
if (!await p.exists(path12)) return { devices: [] };
|
|
6635
7094
|
try {
|
|
6636
|
-
const raw = await p.readFile(
|
|
7095
|
+
const raw = await p.readFile(path12);
|
|
6637
7096
|
return JSON.parse(raw);
|
|
6638
7097
|
} catch {
|
|
6639
7098
|
return { devices: [] };
|
|
@@ -6645,12 +7104,12 @@ async function saveDeviceState(state) {
|
|
|
6645
7104
|
}
|
|
6646
7105
|
async function withDeviceLock(fn) {
|
|
6647
7106
|
const p = getPlatformAdapter();
|
|
6648
|
-
const
|
|
6649
|
-
await p.mkdir(p.dirname(
|
|
6650
|
-
if (!await p.exists(
|
|
6651
|
-
await p.writeFile(
|
|
7107
|
+
const path12 = devicesPath();
|
|
7108
|
+
await p.mkdir(p.dirname(path12));
|
|
7109
|
+
if (!await p.exists(path12)) {
|
|
7110
|
+
await p.writeFile(path12, JSON.stringify({ devices: [] }));
|
|
6652
7111
|
}
|
|
6653
|
-
const release = await lockfile.lock(
|
|
7112
|
+
const release = await lockfile.lock(path12, { retries: 3 });
|
|
6654
7113
|
try {
|
|
6655
7114
|
return await fn();
|
|
6656
7115
|
} finally {
|
|
@@ -6859,10 +7318,17 @@ async function attach(options) {
|
|
|
6859
7318
|
const outcome = await notifyFleetRestartDeviceBestEffort(serial);
|
|
6860
7319
|
if (outcome === "not_running") {
|
|
6861
7320
|
await maybeNotifyFleetWithHint(cfg);
|
|
7321
|
+
} else {
|
|
7322
|
+
await maybeWaitForFleetHealthy(serial, options);
|
|
6862
7323
|
}
|
|
6863
7324
|
return;
|
|
6864
7325
|
}
|
|
6865
7326
|
await maybeNotifyFleetWithHint(cfg);
|
|
7327
|
+
await maybeWaitForFleetHealthy(serial, options);
|
|
7328
|
+
}
|
|
7329
|
+
async function maybeWaitForFleetHealthy(serial, options) {
|
|
7330
|
+
if (options.wait === false) return;
|
|
7331
|
+
await waitForFleetHealthy({ serial });
|
|
6866
7332
|
}
|
|
6867
7333
|
async function attachAll(cfg, reporter, withVideo, options) {
|
|
6868
7334
|
const all = await deviceRuntime.listAdbDevices();
|
|
@@ -6988,6 +7454,7 @@ async function attachAll(cfg, reporter, withVideo, options) {
|
|
|
6988
7454
|
}
|
|
6989
7455
|
console.log(`Attached ${committed.length} device(s); supervision via device-agent fleet.`);
|
|
6990
7456
|
});
|
|
7457
|
+
const committedSerials = commits.filter((c) => c.ok).map((c) => c.serial);
|
|
6991
7458
|
if (reboundSerials.length > 0) {
|
|
6992
7459
|
let anyNotRunning = false;
|
|
6993
7460
|
for (const serial of reboundSerials) {
|
|
@@ -6996,12 +7463,27 @@ async function attachAll(cfg, reporter, withVideo, options) {
|
|
|
6996
7463
|
}
|
|
6997
7464
|
if (anyNotRunning) {
|
|
6998
7465
|
await maybeNotifyFleetWithHint(cfg);
|
|
6999
|
-
} else if (reboundSerials.length <
|
|
7466
|
+
} else if (reboundSerials.length < committedSerials.length) {
|
|
7000
7467
|
await maybeNotifyFleetWithHint(cfg);
|
|
7001
7468
|
}
|
|
7469
|
+
await maybeWaitForFleetHealthyMany(committedSerials, options);
|
|
7002
7470
|
return;
|
|
7003
7471
|
}
|
|
7004
7472
|
await maybeNotifyFleetWithHint(cfg);
|
|
7473
|
+
await maybeWaitForFleetHealthyMany(committedSerials, options);
|
|
7474
|
+
}
|
|
7475
|
+
async function maybeWaitForFleetHealthyMany(serials, options) {
|
|
7476
|
+
if (options.wait === false || serials.length === 0) return;
|
|
7477
|
+
await Promise.allSettled(
|
|
7478
|
+
serials.map(
|
|
7479
|
+
(s) => (
|
|
7480
|
+
// Bare mode: ora doesn't compose with parallel callers cleanly,
|
|
7481
|
+
// so we route every serial through the line-per-state-change
|
|
7482
|
+
// reporter even on a TTY.
|
|
7483
|
+
waitForFleetHealthy({ serial: s, spinner: false })
|
|
7484
|
+
)
|
|
7485
|
+
)
|
|
7486
|
+
);
|
|
7005
7487
|
}
|
|
7006
7488
|
function commitAttachResults(state, commits) {
|
|
7007
7489
|
const committed = [];
|
|
@@ -7316,14 +7798,18 @@ var init_attach = __esm({
|
|
|
7316
7798
|
init_instance_picker();
|
|
7317
7799
|
init_fallback_banner();
|
|
7318
7800
|
init_fleet_notify();
|
|
7801
|
+
init_wait_for_fleet_healthy();
|
|
7319
7802
|
init_state2();
|
|
7320
7803
|
}
|
|
7321
7804
|
});
|
|
7322
7805
|
|
|
7323
7806
|
// src/commands/device/detach.ts
|
|
7807
|
+
import fs9 from "fs/promises";
|
|
7808
|
+
import path9 from "path";
|
|
7324
7809
|
async function detach(options) {
|
|
7325
7810
|
const cfg = await loadOrCreateConfig();
|
|
7326
7811
|
const mgr = await getServiceManager();
|
|
7812
|
+
const rotated = [];
|
|
7327
7813
|
await withDeviceLock(async () => {
|
|
7328
7814
|
const state = await loadDeviceState();
|
|
7329
7815
|
if (options.all) {
|
|
@@ -7331,11 +7817,19 @@ async function detach(options) {
|
|
|
7331
7817
|
await removeTargetsForSerial(mgr, entry2.serial);
|
|
7332
7818
|
legacyKillDeviceEntry(entry2);
|
|
7333
7819
|
await notifyOffline(cfg.platform.api_url, entry2.instance_id);
|
|
7820
|
+
if (options.rotateIdentity) {
|
|
7821
|
+
if (await tryRotateIdentity(entry2.serial)) rotated.push(entry2.serial);
|
|
7822
|
+
}
|
|
7334
7823
|
}
|
|
7335
7824
|
const count = state.devices.length;
|
|
7336
7825
|
state.devices = [];
|
|
7337
7826
|
await saveDeviceState(state);
|
|
7338
7827
|
console.log(`Detached ${count} device(s)`);
|
|
7828
|
+
if (options.rotateIdentity) {
|
|
7829
|
+
console.log(
|
|
7830
|
+
`Rotated identity for ${rotated.length} device(s); next attach will re-bind from scratch.`
|
|
7831
|
+
);
|
|
7832
|
+
}
|
|
7339
7833
|
return;
|
|
7340
7834
|
}
|
|
7341
7835
|
if (!options.serial) {
|
|
@@ -7348,10 +7842,35 @@ async function detach(options) {
|
|
|
7348
7842
|
legacyKillDeviceEntry(entry);
|
|
7349
7843
|
await notifyOffline(cfg.platform.api_url, entry.instance_id);
|
|
7350
7844
|
await saveDeviceState(state);
|
|
7845
|
+
if (options.rotateIdentity) {
|
|
7846
|
+
if (await tryRotateIdentity(entry.serial)) rotated.push(entry.serial);
|
|
7847
|
+
}
|
|
7351
7848
|
console.log(`Detached device ${options.serial}`);
|
|
7849
|
+
if (options.rotateIdentity) {
|
|
7850
|
+
console.log(
|
|
7851
|
+
rotated.length > 0 ? `Rotated local identity (~/.beeos/identity/device-${options.serial}.key.json removed). Next attach will re-bind from scratch.` : `No local identity file found for ${options.serial} \u2014 nothing to rotate.`
|
|
7852
|
+
);
|
|
7853
|
+
}
|
|
7352
7854
|
});
|
|
7353
7855
|
await notifyFleetReloadBestEffort();
|
|
7354
7856
|
}
|
|
7857
|
+
async function tryRotateIdentity(serial) {
|
|
7858
|
+
const keyPath = path9.join(
|
|
7859
|
+
beeoHome(),
|
|
7860
|
+
"identity",
|
|
7861
|
+
`device-${serial}.key.json`
|
|
7862
|
+
);
|
|
7863
|
+
try {
|
|
7864
|
+
await fs9.unlink(keyPath);
|
|
7865
|
+
return true;
|
|
7866
|
+
} catch (e) {
|
|
7867
|
+
if (e.code === "ENOENT") return false;
|
|
7868
|
+
console.error(
|
|
7869
|
+
` identity file ${keyPath} could not be removed: ${e.message}`
|
|
7870
|
+
);
|
|
7871
|
+
return false;
|
|
7872
|
+
}
|
|
7873
|
+
}
|
|
7355
7874
|
function legacyKillDeviceEntry(entry) {
|
|
7356
7875
|
if (entry.pid && isProcessAlive(entry.pid)) killProcess(entry.pid);
|
|
7357
7876
|
if (entry.bridge_pid && isProcessAlive(entry.bridge_pid)) killProcess(entry.bridge_pid);
|
|
@@ -7714,8 +8233,36 @@ var NodePlatformAdapter = class {
|
|
|
7714
8233
|
}
|
|
7715
8234
|
// ── Process execution ───────────────────────────────────
|
|
7716
8235
|
exec(cmd, args, options) {
|
|
8236
|
+
const env = options?.env ? { ...process.env, ...options.env } : process.env;
|
|
8237
|
+
if (options?.stdio === "inherit") {
|
|
8238
|
+
return new Promise((resolve) => {
|
|
8239
|
+
const child = nodeSpawn(cmd, args, {
|
|
8240
|
+
cwd: options?.cwd,
|
|
8241
|
+
env,
|
|
8242
|
+
shell: process.platform === "win32",
|
|
8243
|
+
windowsHide: true,
|
|
8244
|
+
stdio: "inherit"
|
|
8245
|
+
});
|
|
8246
|
+
let timer = null;
|
|
8247
|
+
if (options?.timeout && options.timeout > 0) {
|
|
8248
|
+
timer = setTimeout(() => {
|
|
8249
|
+
try {
|
|
8250
|
+
child.kill("SIGTERM");
|
|
8251
|
+
} catch {
|
|
8252
|
+
}
|
|
8253
|
+
}, options.timeout);
|
|
8254
|
+
}
|
|
8255
|
+
child.on("error", () => {
|
|
8256
|
+
if (timer) clearTimeout(timer);
|
|
8257
|
+
resolve({ stdout: "", stderr: "", code: 1 });
|
|
8258
|
+
});
|
|
8259
|
+
child.on("exit", (code) => {
|
|
8260
|
+
if (timer) clearTimeout(timer);
|
|
8261
|
+
resolve({ stdout: "", stderr: "", code: code ?? 1 });
|
|
8262
|
+
});
|
|
8263
|
+
});
|
|
8264
|
+
}
|
|
7717
8265
|
return new Promise((resolve) => {
|
|
7718
|
-
const env = options?.env ? { ...process.env, ...options.env } : process.env;
|
|
7719
8266
|
execFile6(
|
|
7720
8267
|
cmd,
|
|
7721
8268
|
args,
|
|
@@ -7899,10 +8446,9 @@ async function run(agentFramework, options) {
|
|
|
7899
8446
|
const ttyHints = !options.json && process.stdin.isTTY === true;
|
|
7900
8447
|
const desktopCapable = hasDesktopCapability(driver);
|
|
7901
8448
|
const desktopPipeline = desktopCapable ? getDesktopPipeline() : null;
|
|
7902
|
-
let coldStart = null;
|
|
7903
8449
|
if (desktopPipeline) {
|
|
7904
8450
|
const interactive = !options.json && process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
7905
|
-
coldStart = await desktopPipeline.coldStart({
|
|
8451
|
+
const coldStart = await desktopPipeline.coldStart({
|
|
7906
8452
|
prompt: interactive ? promptYesNo : void 0,
|
|
7907
8453
|
log: options.json ? () => void 0 : (m) => console.log(m),
|
|
7908
8454
|
yes: options.force === true
|
|
@@ -8638,8 +9184,8 @@ async function pickInstalledPackages() {
|
|
|
8638
9184
|
// src/commands/doctor.ts
|
|
8639
9185
|
init_dist();
|
|
8640
9186
|
init_json_envelope();
|
|
8641
|
-
import
|
|
8642
|
-
import
|
|
9187
|
+
import fs10 from "fs/promises";
|
|
9188
|
+
import path10 from "path";
|
|
8643
9189
|
var LOG_SIZE_WARN_BYTES = 50 * 1024 * 1024;
|
|
8644
9190
|
async function run8(options) {
|
|
8645
9191
|
const state = await detectExistingInstall();
|
|
@@ -8656,8 +9202,13 @@ async function run8(options) {
|
|
|
8656
9202
|
const resolvedAgentGatewayUrl = resolveAgentGatewayUrl(cfg);
|
|
8657
9203
|
const agentGatewayHealth = await probeAgentGateway(resolvedAgentGatewayUrl);
|
|
8658
9204
|
const tools = await collectToolStatus();
|
|
9205
|
+
const adbDaemon = tools.adb.path ? await probeAdbDaemon(tools.adb.path) : { reachable: false, deviceCount: 0, detail: "adb not on PATH" };
|
|
9206
|
+
const identityFile = await probeIdentityKeypair();
|
|
9207
|
+
const ports = await probePorts();
|
|
9208
|
+
const nodeEngines = await probeNodeEngines();
|
|
8659
9209
|
const hasBoundDevices = state.devices.entries.length > 0;
|
|
8660
9210
|
const shimReport = await collectShimReport(hasBoundDevices);
|
|
9211
|
+
const npmPrefix = await readCurrentPrefix();
|
|
8661
9212
|
const warnings = [];
|
|
8662
9213
|
const hints = [];
|
|
8663
9214
|
if (!state.hasIdentity) {
|
|
@@ -8731,6 +9282,42 @@ async function run8(options) {
|
|
|
8731
9282
|
warnings.push(
|
|
8732
9283
|
"adb not found \u2014 `beeos device attach` will download Android platform-tools on first use"
|
|
8733
9284
|
);
|
|
9285
|
+
} else if (!adbDaemon.reachable) {
|
|
9286
|
+
warnings.push(`adb daemon unreachable: ${adbDaemon.detail}`);
|
|
9287
|
+
hints.push(
|
|
9288
|
+
"try `adb kill-server && adb start-server` to reset; on Linux check udev rules; on Windows reinstall Android USB drivers"
|
|
9289
|
+
);
|
|
9290
|
+
}
|
|
9291
|
+
if (state.hasIdentity && !identityFile.parseable) {
|
|
9292
|
+
warnings.push(
|
|
9293
|
+
`identity keypair file present but unreadable/malformed (${identityFile.detail}). Binding will fail; backup and regenerate via \`beeos init\` (option [3] reset all).`
|
|
9294
|
+
);
|
|
9295
|
+
}
|
|
9296
|
+
if (ports.port7900Occupied) {
|
|
9297
|
+
warnings.push(
|
|
9298
|
+
"port 7900 is in use \u2014 device-mcp-server may not start cleanly on the next attach."
|
|
9299
|
+
);
|
|
9300
|
+
hints.push(
|
|
9301
|
+
"find the holder: `lsof -i :7900` (POSIX) or `netstat -ano | findstr :7900` (Windows)"
|
|
9302
|
+
);
|
|
9303
|
+
}
|
|
9304
|
+
if (ports.port7950Occupied) {
|
|
9305
|
+
warnings.push(
|
|
9306
|
+
"port 7950 is in use \u2014 fleet IPC may not bind on the next attach."
|
|
9307
|
+
);
|
|
9308
|
+
hints.push(
|
|
9309
|
+
"find the holder: `lsof -i :7950` (POSIX) or `netstat -ano | findstr :7950` (Windows)"
|
|
9310
|
+
);
|
|
9311
|
+
}
|
|
9312
|
+
for (const e of nodeEngines.entries) {
|
|
9313
|
+
if (e.satisfies === false) {
|
|
9314
|
+
warnings.push(
|
|
9315
|
+
`${e.pkg}@${e.installed} requires Node ${e.required} but you are on ${e.current}.`
|
|
9316
|
+
);
|
|
9317
|
+
hints.push(
|
|
9318
|
+
`upgrade Node (e.g. 'nvm install --lts' / 'fnm install --lts') and re-run \`beeos device attach\``
|
|
9319
|
+
);
|
|
9320
|
+
}
|
|
8734
9321
|
}
|
|
8735
9322
|
for (const e of shimReport.entries) {
|
|
8736
9323
|
if (e.outcome === "outdated") {
|
|
@@ -8750,7 +9337,7 @@ async function run8(options) {
|
|
|
8750
9337
|
const bloatedLogs = await checkLogFileSizes();
|
|
8751
9338
|
for (const l of bloatedLogs) {
|
|
8752
9339
|
warnings.push(
|
|
8753
|
-
`Service log large: ${
|
|
9340
|
+
`Service log large: ${path10.basename(l.path)} = ${formatBytes(l.size)}`
|
|
8754
9341
|
);
|
|
8755
9342
|
hints.push(`truncate safely: : > ${l.path}`);
|
|
8756
9343
|
}
|
|
@@ -8771,7 +9358,12 @@ async function run8(options) {
|
|
|
8771
9358
|
},
|
|
8772
9359
|
agentGateway: agentGatewayHealth,
|
|
8773
9360
|
tools,
|
|
9361
|
+
adbDaemon,
|
|
9362
|
+
identityFile,
|
|
9363
|
+
ports,
|
|
9364
|
+
nodeEngines,
|
|
8774
9365
|
npmShims: shimReport,
|
|
9366
|
+
npmPrefix,
|
|
8775
9367
|
warnings,
|
|
8776
9368
|
hints
|
|
8777
9369
|
})
|
|
@@ -8798,8 +9390,20 @@ async function run8(options) {
|
|
|
8798
9390
|
console.log(` - ${s.id.padEnd(28)} ${status2}${pid}`);
|
|
8799
9391
|
}
|
|
8800
9392
|
console.log(` adb : ${tools.adb.path ?? "not found"}`);
|
|
9393
|
+
if (tools.adb.path) {
|
|
9394
|
+
console.log(
|
|
9395
|
+
` daemon : ${adbDaemon.reachable ? `reachable (${adbDaemon.deviceCount} device${adbDaemon.deviceCount === 1 ? "" : "s"})` : `unreachable \u2014 ${adbDaemon.detail}`}`
|
|
9396
|
+
);
|
|
9397
|
+
}
|
|
8801
9398
|
console.log(` scrcpy-bridge: ${tools.scrcpyBridge.path ?? "not installed (auto on attach)"}`);
|
|
8802
9399
|
console.log(` vnc-bridge : ${tools.vncBridge.path ?? "not installed (auto on --vnc-host)"}`);
|
|
9400
|
+
console.log(` npm prefix : ${npmPrefix ?? "(unknown \u2014 npm not on PATH?)"}`);
|
|
9401
|
+
console.log(
|
|
9402
|
+
` identity : ${identityFile.parseable ? "ok" : identityFile.exists ? `corrupt (${identityFile.detail})` : "not yet created"}`
|
|
9403
|
+
);
|
|
9404
|
+
console.log(
|
|
9405
|
+
` ports : 7900 ${ports.port7900Occupied ? "in use" : "free"}, 7950 ${ports.port7950Occupied ? "in use" : "free"}`
|
|
9406
|
+
);
|
|
8803
9407
|
console.log("");
|
|
8804
9408
|
console.log(" npm shims:");
|
|
8805
9409
|
for (const e of shimReport.entries) {
|
|
@@ -8882,19 +9486,19 @@ function shimMarker(outcome) {
|
|
|
8882
9486
|
}
|
|
8883
9487
|
}
|
|
8884
9488
|
async function checkLogFileSizes() {
|
|
8885
|
-
const dir =
|
|
9489
|
+
const dir = path10.join(beeoHome(), "logs", "services");
|
|
8886
9490
|
let entries;
|
|
8887
9491
|
try {
|
|
8888
|
-
entries = await
|
|
9492
|
+
entries = await fs10.readdir(dir);
|
|
8889
9493
|
} catch {
|
|
8890
9494
|
return [];
|
|
8891
9495
|
}
|
|
8892
9496
|
const bloated = [];
|
|
8893
9497
|
for (const name of entries) {
|
|
8894
9498
|
if (!name.endsWith(".log")) continue;
|
|
8895
|
-
const full =
|
|
9499
|
+
const full = path10.join(dir, name);
|
|
8896
9500
|
try {
|
|
8897
|
-
const st = await
|
|
9501
|
+
const st = await fs10.stat(full);
|
|
8898
9502
|
if (st.isFile() && st.size >= LOG_SIZE_WARN_BYTES) {
|
|
8899
9503
|
bloated.push({ path: full, size: st.size });
|
|
8900
9504
|
}
|
|
@@ -8961,6 +9565,173 @@ function formatAgentGatewayStatus(h) {
|
|
|
8961
9565
|
if (h.status === "unhealthy") return `HTTP ${h.httpStatus ?? "?"}`;
|
|
8962
9566
|
return h.detail;
|
|
8963
9567
|
}
|
|
9568
|
+
var ADB_PROBE_START_SERVER_TIMEOUT_MS = 3e3;
|
|
9569
|
+
var ADB_PROBE_DEVICES_TIMEOUT_MS = 1500;
|
|
9570
|
+
async function probeAdbDaemon(adbPath) {
|
|
9571
|
+
const p = getPlatformAdapter();
|
|
9572
|
+
try {
|
|
9573
|
+
const start = await p.exec(adbPath, ["start-server"], {
|
|
9574
|
+
timeout: ADB_PROBE_START_SERVER_TIMEOUT_MS
|
|
9575
|
+
});
|
|
9576
|
+
if (start.code !== 0) {
|
|
9577
|
+
return {
|
|
9578
|
+
reachable: false,
|
|
9579
|
+
deviceCount: 0,
|
|
9580
|
+
detail: `adb start-server exited ${start.code} (timeout ${ADB_PROBE_START_SERVER_TIMEOUT_MS}ms): ${(start.stderr || start.stdout).trim().slice(0, 120)}`
|
|
9581
|
+
};
|
|
9582
|
+
}
|
|
9583
|
+
} catch (e) {
|
|
9584
|
+
return {
|
|
9585
|
+
reachable: false,
|
|
9586
|
+
deviceCount: 0,
|
|
9587
|
+
detail: `adb start-server failed (timeout ${ADB_PROBE_START_SERVER_TIMEOUT_MS}ms): ${e instanceof Error ? e.message : String(e)}`
|
|
9588
|
+
};
|
|
9589
|
+
}
|
|
9590
|
+
try {
|
|
9591
|
+
const list2 = await p.exec(adbPath, ["devices"], {
|
|
9592
|
+
timeout: ADB_PROBE_DEVICES_TIMEOUT_MS
|
|
9593
|
+
});
|
|
9594
|
+
if (list2.code !== 0) {
|
|
9595
|
+
return {
|
|
9596
|
+
reachable: false,
|
|
9597
|
+
deviceCount: 0,
|
|
9598
|
+
detail: `adb devices exited ${list2.code} (timeout ${ADB_PROBE_DEVICES_TIMEOUT_MS}ms)`
|
|
9599
|
+
};
|
|
9600
|
+
}
|
|
9601
|
+
const lines = list2.stdout.split("\n").slice(1).map((l) => l.trim());
|
|
9602
|
+
const deviceCount = lines.filter((l) => l.endsWith(" device")).length;
|
|
9603
|
+
return {
|
|
9604
|
+
reachable: true,
|
|
9605
|
+
deviceCount,
|
|
9606
|
+
detail: `${deviceCount} device(s) authorized`
|
|
9607
|
+
};
|
|
9608
|
+
} catch (e) {
|
|
9609
|
+
return {
|
|
9610
|
+
reachable: false,
|
|
9611
|
+
deviceCount: 0,
|
|
9612
|
+
detail: `adb devices failed (timeout ${ADB_PROBE_DEVICES_TIMEOUT_MS}ms): ${e instanceof Error ? e.message : String(e)}`
|
|
9613
|
+
};
|
|
9614
|
+
}
|
|
9615
|
+
}
|
|
9616
|
+
async function probeIdentityKeypair() {
|
|
9617
|
+
const p = getPlatformAdapter();
|
|
9618
|
+
const kpPath = p.joinPath(beeoHome(), "identity", "keypair.json");
|
|
9619
|
+
if (!await p.exists(kpPath)) {
|
|
9620
|
+
return {
|
|
9621
|
+
exists: false,
|
|
9622
|
+
readable: false,
|
|
9623
|
+
parseable: false,
|
|
9624
|
+
detail: "not yet generated"
|
|
9625
|
+
};
|
|
9626
|
+
}
|
|
9627
|
+
let raw;
|
|
9628
|
+
try {
|
|
9629
|
+
raw = await p.readFile(kpPath);
|
|
9630
|
+
} catch (e) {
|
|
9631
|
+
return {
|
|
9632
|
+
exists: true,
|
|
9633
|
+
readable: false,
|
|
9634
|
+
parseable: false,
|
|
9635
|
+
detail: `read failed: ${e instanceof Error ? e.message : String(e)}`
|
|
9636
|
+
};
|
|
9637
|
+
}
|
|
9638
|
+
try {
|
|
9639
|
+
const parsed = JSON.parse(raw);
|
|
9640
|
+
if (typeof parsed.publicKey !== "string" || typeof parsed.privateKey !== "string") {
|
|
9641
|
+
return {
|
|
9642
|
+
exists: true,
|
|
9643
|
+
readable: true,
|
|
9644
|
+
parseable: false,
|
|
9645
|
+
detail: "missing publicKey / privateKey fields"
|
|
9646
|
+
};
|
|
9647
|
+
}
|
|
9648
|
+
return {
|
|
9649
|
+
exists: true,
|
|
9650
|
+
readable: true,
|
|
9651
|
+
parseable: true,
|
|
9652
|
+
detail: "ok"
|
|
9653
|
+
};
|
|
9654
|
+
} catch (e) {
|
|
9655
|
+
return {
|
|
9656
|
+
exists: true,
|
|
9657
|
+
readable: true,
|
|
9658
|
+
parseable: false,
|
|
9659
|
+
detail: `JSON parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
9660
|
+
};
|
|
9661
|
+
}
|
|
9662
|
+
}
|
|
9663
|
+
async function probePorts() {
|
|
9664
|
+
const p = getPlatformAdapter();
|
|
9665
|
+
const [p7900, p7950] = await Promise.all([
|
|
9666
|
+
p.tcpProbe("127.0.0.1", 7900, 500),
|
|
9667
|
+
p.tcpProbe("127.0.0.1", 7950, 500)
|
|
9668
|
+
]);
|
|
9669
|
+
return { port7900Occupied: p7900, port7950Occupied: p7950 };
|
|
9670
|
+
}
|
|
9671
|
+
async function probeNodeEngines() {
|
|
9672
|
+
const targets = [NPM_PKGS.DEVICE_AGENT, NPM_PKGS.DEVICE_MCP_SERVER];
|
|
9673
|
+
const current = process.version.replace(/^v/, "");
|
|
9674
|
+
const entries = [];
|
|
9675
|
+
for (const pkg of targets) {
|
|
9676
|
+
const installed = await npmGlobalVersion(pkg).catch(() => null);
|
|
9677
|
+
if (!installed) {
|
|
9678
|
+
entries.push({
|
|
9679
|
+
pkg,
|
|
9680
|
+
installed: null,
|
|
9681
|
+
required: null,
|
|
9682
|
+
current,
|
|
9683
|
+
satisfies: null
|
|
9684
|
+
});
|
|
9685
|
+
continue;
|
|
9686
|
+
}
|
|
9687
|
+
const required = await readPkgEnginesNode(pkg);
|
|
9688
|
+
entries.push({
|
|
9689
|
+
pkg,
|
|
9690
|
+
installed,
|
|
9691
|
+
required,
|
|
9692
|
+
current,
|
|
9693
|
+
satisfies: required ? satisfiesMinNode(current, required) : null
|
|
9694
|
+
});
|
|
9695
|
+
}
|
|
9696
|
+
return { entries };
|
|
9697
|
+
}
|
|
9698
|
+
async function readPkgEnginesNode(pkg) {
|
|
9699
|
+
const p = getPlatformAdapter();
|
|
9700
|
+
const rootRes = await p.exec("npm", ["root", "-g"], { timeout: 5e3 });
|
|
9701
|
+
if (rootRes.code !== 0) return null;
|
|
9702
|
+
const root = rootRes.stdout.trim();
|
|
9703
|
+
if (!root) return null;
|
|
9704
|
+
const pkgJsonPath = path10.join(root, pkg, "package.json");
|
|
9705
|
+
try {
|
|
9706
|
+
const raw = await fs10.readFile(pkgJsonPath, "utf-8");
|
|
9707
|
+
const parsed = JSON.parse(raw);
|
|
9708
|
+
const node = parsed.engines?.node?.trim();
|
|
9709
|
+
return node && node.length > 0 ? node : null;
|
|
9710
|
+
} catch {
|
|
9711
|
+
return null;
|
|
9712
|
+
}
|
|
9713
|
+
}
|
|
9714
|
+
function satisfiesMinNode(current, spec) {
|
|
9715
|
+
const m = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(spec.trim());
|
|
9716
|
+
if (!m) return null;
|
|
9717
|
+
const want = [
|
|
9718
|
+
Number(m[1]),
|
|
9719
|
+
Number(m[2] ?? 0),
|
|
9720
|
+
Number(m[3] ?? 0)
|
|
9721
|
+
];
|
|
9722
|
+
const have = parseSemver(current);
|
|
9723
|
+
if (!have) return null;
|
|
9724
|
+
for (let i = 0; i < 3; i++) {
|
|
9725
|
+
if (have[i] > want[i]) return true;
|
|
9726
|
+
if (have[i] < want[i]) return false;
|
|
9727
|
+
}
|
|
9728
|
+
return true;
|
|
9729
|
+
}
|
|
9730
|
+
function parseSemver(v) {
|
|
9731
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
9732
|
+
if (!m) return null;
|
|
9733
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
9734
|
+
}
|
|
8964
9735
|
|
|
8965
9736
|
// src/commands/service.ts
|
|
8966
9737
|
init_dist();
|
|
@@ -9178,6 +9949,194 @@ async function run10(idArg, options) {
|
|
|
9178
9949
|
}
|
|
9179
9950
|
}
|
|
9180
9951
|
|
|
9952
|
+
// src/commands/report.ts
|
|
9953
|
+
init_dist();
|
|
9954
|
+
init_json_envelope();
|
|
9955
|
+
import fs11 from "fs/promises";
|
|
9956
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
9957
|
+
import path11 from "path";
|
|
9958
|
+
import { spawn as spawn3 } from "child_process";
|
|
9959
|
+
import { create as tarCreate } from "tar";
|
|
9960
|
+
var LOG_TAIL_BYTES = 1024 * 1024;
|
|
9961
|
+
async function run11(opts = {}) {
|
|
9962
|
+
const home = beeoHome();
|
|
9963
|
+
const reportsDir = path11.join(home, "reports");
|
|
9964
|
+
await fs11.mkdir(reportsDir, { recursive: true });
|
|
9965
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
9966
|
+
const reportName = `beeos-report-${stamp}`;
|
|
9967
|
+
const stagingDir = path11.join(reportsDir, reportName);
|
|
9968
|
+
await fs11.mkdir(stagingDir, { recursive: true });
|
|
9969
|
+
try {
|
|
9970
|
+
if (!opts.skipDoctor) {
|
|
9971
|
+
const doctorJson = await runDoctorJson();
|
|
9972
|
+
await fs11.writeFile(
|
|
9973
|
+
path11.join(stagingDir, "doctor.json"),
|
|
9974
|
+
doctorJson,
|
|
9975
|
+
"utf-8"
|
|
9976
|
+
);
|
|
9977
|
+
}
|
|
9978
|
+
try {
|
|
9979
|
+
const cfgRaw = await fs11.readFile(configPath(), "utf-8");
|
|
9980
|
+
await fs11.writeFile(
|
|
9981
|
+
path11.join(stagingDir, "config.toml"),
|
|
9982
|
+
cfgRaw,
|
|
9983
|
+
"utf-8"
|
|
9984
|
+
);
|
|
9985
|
+
} catch {
|
|
9986
|
+
await fs11.writeFile(
|
|
9987
|
+
path11.join(stagingDir, "config.toml"),
|
|
9988
|
+
"# (no ~/.beeos/config.toml on this machine)\n",
|
|
9989
|
+
"utf-8"
|
|
9990
|
+
);
|
|
9991
|
+
}
|
|
9992
|
+
try {
|
|
9993
|
+
const state = await loadState();
|
|
9994
|
+
await fs11.writeFile(
|
|
9995
|
+
path11.join(stagingDir, "state.json"),
|
|
9996
|
+
JSON.stringify(redactSecrets(state), null, 2),
|
|
9997
|
+
"utf-8"
|
|
9998
|
+
);
|
|
9999
|
+
} catch {
|
|
10000
|
+
await fs11.writeFile(
|
|
10001
|
+
path11.join(stagingDir, "state.json"),
|
|
10002
|
+
"{}\n",
|
|
10003
|
+
"utf-8"
|
|
10004
|
+
);
|
|
10005
|
+
}
|
|
10006
|
+
const logsSrc = path11.join(home, "logs", "services");
|
|
10007
|
+
const logsDst = path11.join(stagingDir, "services-logs");
|
|
10008
|
+
await fs11.mkdir(logsDst, { recursive: true });
|
|
10009
|
+
let logEntries = [];
|
|
10010
|
+
try {
|
|
10011
|
+
logEntries = await fs11.readdir(logsSrc);
|
|
10012
|
+
} catch {
|
|
10013
|
+
}
|
|
10014
|
+
for (const name of logEntries) {
|
|
10015
|
+
if (!name.endsWith(".log")) continue;
|
|
10016
|
+
const src = path11.join(logsSrc, name);
|
|
10017
|
+
const dst = path11.join(logsDst, name);
|
|
10018
|
+
try {
|
|
10019
|
+
await tailCopy(src, dst, LOG_TAIL_BYTES);
|
|
10020
|
+
} catch {
|
|
10021
|
+
}
|
|
10022
|
+
}
|
|
10023
|
+
const tarballPath = path11.join(reportsDir, `${reportName}.tar.gz`);
|
|
10024
|
+
await tarCreate(
|
|
10025
|
+
{ gzip: true, file: tarballPath, cwd: reportsDir },
|
|
10026
|
+
[reportName]
|
|
10027
|
+
);
|
|
10028
|
+
if (opts.json) {
|
|
10029
|
+
emitJsonEnvelope(
|
|
10030
|
+
jsonOk({
|
|
10031
|
+
tarball: tarballPath,
|
|
10032
|
+
included: [
|
|
10033
|
+
"doctor.json",
|
|
10034
|
+
"config.toml",
|
|
10035
|
+
"state.json",
|
|
10036
|
+
"services-logs/"
|
|
10037
|
+
],
|
|
10038
|
+
excluded: [
|
|
10039
|
+
"identity/keypair.json",
|
|
10040
|
+
"identity/device-*.key.json"
|
|
10041
|
+
]
|
|
10042
|
+
})
|
|
10043
|
+
);
|
|
10044
|
+
} else {
|
|
10045
|
+
console.log("");
|
|
10046
|
+
console.log(` Report: ${tarballPath}`);
|
|
10047
|
+
console.log("");
|
|
10048
|
+
console.log(" Attach this file to your support ticket.");
|
|
10049
|
+
console.log(
|
|
10050
|
+
" It contains: doctor.json, config.toml, state.json, last 1MiB of every service log."
|
|
10051
|
+
);
|
|
10052
|
+
console.log(" It does NOT contain: identity/keypair.json (private key).");
|
|
10053
|
+
console.log("");
|
|
10054
|
+
}
|
|
10055
|
+
} finally {
|
|
10056
|
+
await fs11.rm(stagingDir, { recursive: true, force: true }).catch(() => {
|
|
10057
|
+
});
|
|
10058
|
+
}
|
|
10059
|
+
}
|
|
10060
|
+
async function runDoctorJson() {
|
|
10061
|
+
const cliEntry = process.argv[1];
|
|
10062
|
+
if (!cliEntry) {
|
|
10063
|
+
return JSON.stringify(
|
|
10064
|
+
{ ok: false, error: { code: "no_entry", message: "process.argv[1] missing" } },
|
|
10065
|
+
null,
|
|
10066
|
+
2
|
|
10067
|
+
);
|
|
10068
|
+
}
|
|
10069
|
+
return new Promise((resolve) => {
|
|
10070
|
+
let stdout = "";
|
|
10071
|
+
let stderr = "";
|
|
10072
|
+
const child = spawn3(process.execPath, [cliEntry, "doctor", "--json"], {
|
|
10073
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
10074
|
+
});
|
|
10075
|
+
child.stdout?.on("data", (c) => {
|
|
10076
|
+
stdout += c.toString();
|
|
10077
|
+
});
|
|
10078
|
+
child.stderr?.on("data", (c) => {
|
|
10079
|
+
stderr += c.toString();
|
|
10080
|
+
});
|
|
10081
|
+
const timer = setTimeout(() => {
|
|
10082
|
+
try {
|
|
10083
|
+
child.kill("SIGTERM");
|
|
10084
|
+
} catch {
|
|
10085
|
+
}
|
|
10086
|
+
}, 3e4);
|
|
10087
|
+
child.on("close", (code) => {
|
|
10088
|
+
clearTimeout(timer);
|
|
10089
|
+
if (code === 0 && stdout.trim().length > 0) {
|
|
10090
|
+
resolve(stdout);
|
|
10091
|
+
} else {
|
|
10092
|
+
resolve(
|
|
10093
|
+
JSON.stringify(
|
|
10094
|
+
{
|
|
10095
|
+
ok: false,
|
|
10096
|
+
error: {
|
|
10097
|
+
code: "doctor_failed",
|
|
10098
|
+
exitCode: code,
|
|
10099
|
+
stderr: stderr.trim()
|
|
10100
|
+
}
|
|
10101
|
+
},
|
|
10102
|
+
null,
|
|
10103
|
+
2
|
|
10104
|
+
)
|
|
10105
|
+
);
|
|
10106
|
+
}
|
|
10107
|
+
});
|
|
10108
|
+
});
|
|
10109
|
+
}
|
|
10110
|
+
async function tailCopy(src, dst, tailBytes) {
|
|
10111
|
+
const stat = await fs11.stat(src);
|
|
10112
|
+
const start = stat.size > tailBytes ? stat.size - tailBytes : 0;
|
|
10113
|
+
await new Promise((resolve, reject) => {
|
|
10114
|
+
const rs = createReadStream(src, { start });
|
|
10115
|
+
const ws = createWriteStream(dst);
|
|
10116
|
+
rs.on("error", reject);
|
|
10117
|
+
ws.on("error", reject);
|
|
10118
|
+
ws.on("finish", () => resolve());
|
|
10119
|
+
rs.pipe(ws);
|
|
10120
|
+
});
|
|
10121
|
+
}
|
|
10122
|
+
function redactSecrets(state) {
|
|
10123
|
+
const SECRET_KEY_RE = /(token|secret|password|api_?key|bearer|jwt|cookie)/i;
|
|
10124
|
+
function walk(value) {
|
|
10125
|
+
if (value === null || typeof value !== "object") return value;
|
|
10126
|
+
if (Array.isArray(value)) return value.map(walk);
|
|
10127
|
+
const out = {};
|
|
10128
|
+
for (const [k, v] of Object.entries(value)) {
|
|
10129
|
+
if (typeof v === "string" && SECRET_KEY_RE.test(k)) {
|
|
10130
|
+
out[k] = `<redacted ${v.length}b>`;
|
|
10131
|
+
} else {
|
|
10132
|
+
out[k] = walk(v);
|
|
10133
|
+
}
|
|
10134
|
+
}
|
|
10135
|
+
return out;
|
|
10136
|
+
}
|
|
10137
|
+
return walk(state);
|
|
10138
|
+
}
|
|
10139
|
+
|
|
9181
10140
|
// src/index.ts
|
|
9182
10141
|
setPlatformAdapter(new NodePlatformAdapter());
|
|
9183
10142
|
var program = new Command();
|
|
@@ -9185,6 +10144,9 @@ var cliVersion = getCliVersion(import.meta.url, resolveActiveDistTag());
|
|
|
9185
10144
|
program.name("beeos").version(cliVersion.display).description("BeeOS \u2014 run AI agents from your desktop");
|
|
9186
10145
|
program.command("init").description("One-shot install + bind flow (default entry from the curl installer)").option("--framework <name>", "Agent framework to install (default: openclaw)").option("--yes", "Non-interactive \u2014 accept the default action", false).option("--json", "Output machine-readable JSON (implies --yes)", false).option("--no-browser", "Don't auto-open a browser for bind confirmation").option("--headless", "Never open a browser (use terminal QR only)", false).option("--skip-service-prompt", "Skip the system-service install prompt", false).option("--device", "Jump straight into `beeos device attach` instead", false).action((opts) => run7(opts));
|
|
9187
10146
|
program.command("doctor").description("Inspect local BeeOS state (install, binding, services, warnings)").option("--json", "Output machine-readable JSON", false).action((opts) => run8(opts));
|
|
10147
|
+
program.command("report").description(
|
|
10148
|
+
"Bundle a diagnostic tarball (~/.beeos/reports/beeos-report-<ts>.tar.gz) for support tickets. Includes doctor.json + redacted config + last 1MiB of every service log. NEVER includes identity/keypair.json."
|
|
10149
|
+
).option("--json", "Output machine-readable JSON envelope ({ok,data})", false).option("--skip-doctor", "Skip running `beeos doctor` as a subprocess", false).action((opts) => run11(opts));
|
|
9188
10150
|
program.command("migrate").description("Migrate legacy Node supervisor state to OS-native services (one-shot)").option("--json", "Output machine-readable JSON", false).action((opts) => run9(opts));
|
|
9189
10151
|
program.command("logs").description(
|
|
9190
10152
|
"Tail logs for a BeeOS service. Pass an ADB serial or service id; omit to list every registered service's log file path."
|
|
@@ -9211,8 +10173,15 @@ deviceCmd.command("attach").description("Attach an ADB-connected device as an AI
|
|
|
9211
10173
|
).option(
|
|
9212
10174
|
"--agent-gateway-url <url>",
|
|
9213
10175
|
"Override the Agent Gateway URL for THIS device only (advanced; multi-region staging). Persists to the device entry so the fleet supervisor reuses it on every restart. Falls back to the global config when omitted."
|
|
10176
|
+
).option(
|
|
10177
|
+
"--no-wait",
|
|
10178
|
+
"Skip waiting for the fleet supervisor to mark the device healthy after bind. By default, attach polls fleet on 127.0.0.1:7950 for up to 30s (BEEOS_AGENT_WAIT_TIMEOUT_MS overrides) so terminal exit aligns with dashboard agent-online status. Set this for CI/scripts that don't care about post-bind dashboard sync."
|
|
9214
10179
|
).action(attach);
|
|
9215
|
-
deviceCmd.command("detach").description("Detach a device agent").option("--serial <serial>", "ADB device serial number").option("--all", "Detach all devices", false).
|
|
10180
|
+
deviceCmd.command("detach").description("Detach a device agent").option("--serial <serial>", "ADB device serial number").option("--all", "Detach all devices", false).option(
|
|
10181
|
+
"--rotate-identity",
|
|
10182
|
+
"Also delete ~/.beeos/identity/device-<serial>.key.json so the next `attach` re-binds from scratch (use this to switch the device to a different BeeOS account / org).",
|
|
10183
|
+
false
|
|
10184
|
+
).action(detach);
|
|
9216
10185
|
deviceCmd.command("list").description("List attached devices").option("--local", "Only show locally running device agents", false).action(list);
|
|
9217
10186
|
deviceCmd.command("exec").description("Send a natural language command to a device").option("--serial <serial>", "ADB device serial number").argument("<prompt>", "The command/prompt to execute").action(exec);
|
|
9218
10187
|
deviceCmd.command("upgrade").description("Upgrade the device-agent (and scrcpy/vnc bridges) to the latest version").option("--no-bridges", "Skip upgrading scrcpy-bridge / vnc-bridge binaries").action(upgrade);
|