@lark-apaas/openclaw-scripts-diagnose-cli 0.1.4-alpha.0 → 0.1.4-alpha.1

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.
Files changed (2) hide show
  1. package/dist/index.cjs +1633 -433
  2. package/package.json +3 -1
package/dist/index.cjs CHANGED
@@ -36,9 +36,26 @@ let node_os = require("node:os");
36
36
  node_os = __toESM(node_os);
37
37
  let node_stream = require("node:stream");
38
38
  let node_stream_promises = require("node:stream/promises");
39
+ let _lark_apaas_http_client = require("@lark-apaas/http-client");
39
40
  let node_assert = require("node:assert");
40
41
  node_assert = __toESM(node_assert);
41
- let _lark_apaas_http_client = require("@lark-apaas/http-client");
42
+ let json_diff = require("json-diff");
43
+ //#region src/version.ts
44
+ /**
45
+ * Machine-readable identifier of the running CLI build.
46
+ *
47
+ * prod build → "0.1.3" (the published semver)
48
+ * local-test → "local-test@2026-04-28T12:30:11.123Z"
49
+ *
50
+ * Exists separately from `help.ts`'s `versionBanner()`, which is for human
51
+ * display and includes prose like "(local test build, not a published
52
+ * release)". This one goes into telemetry payloads and log slicing — keep
53
+ * it terse and parseable.
54
+ */
55
+ function getVersion() {
56
+ return "0.1.4-alpha.1";
57
+ }
58
+ //#endregion
42
59
  //#region src/rule-engine/base.ts
43
60
  /** Abstract base class for all diagnose rules */
44
61
  var DiagnoseRule = class {
@@ -415,7 +432,7 @@ let TemplateVarsUnreplacedRule = class TemplateVarsUnreplacedRule extends Diagno
415
432
  };
416
433
  }
417
434
  repair(ctx) {
418
- const map = ctx.templateVars;
435
+ const map = ctx.vars.templateVars;
419
436
  if (!map || Object.keys(map).length === 0) return;
420
437
  replaceInPlace(ctx.config, Object.entries(map));
421
438
  }
@@ -423,7 +440,8 @@ let TemplateVarsUnreplacedRule = class TemplateVarsUnreplacedRule extends Diagno
423
440
  TemplateVarsUnreplacedRule = __decorate([Rule({
424
441
  key: "template_vars_unreplaced",
425
442
  dependsOn: ["config_syntax_check"],
426
- repairMode: "standard"
443
+ repairMode: "standard",
444
+ usesVars: ["templateVars"]
427
445
  })], TemplateVarsUnreplacedRule);
428
446
  function collectPlaceholders(value, found) {
429
447
  if (typeof value === "string") {
@@ -466,32 +484,33 @@ function applyVars(str, entries) {
466
484
  }
467
485
  //#endregion
468
486
  //#region src/rules/model-provider.ts
469
- var _ModelProviderRule;
470
- let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
471
- static {
472
- _ModelProviderRule = this;
473
- }
474
- static DEFAULT_API_KEY = {
475
- source: "file",
476
- provider: "miaoda-provider",
477
- id: "value"
478
- };
479
- static DEFAULT_X_API_KEY_HEADER = {
480
- source: "file",
481
- provider: "miaoda-secret-provider",
482
- id: "/models_providers_miaoda_headers_x_api_key"
487
+ const DEFAULT_API_KEY = {
488
+ source: "file",
489
+ provider: "miaoda-provider",
490
+ id: "value"
491
+ };
492
+ const DEFAULT_X_API_KEY_HEADER = {
493
+ source: "file",
494
+ provider: "miaoda-secret-provider",
495
+ id: "/models_providers_miaoda_headers_x_api_key"
496
+ };
497
+ const DEFAULT_API = "openai-completions";
498
+ function getExpected(vars) {
499
+ return {
500
+ baseUrl: vars.baseURL + "/api/v1/sgw/model/proxy",
501
+ apiKey: DEFAULT_API_KEY,
502
+ api: DEFAULT_API,
503
+ headers: { "x-api-key": DEFAULT_X_API_KEY_HEADER }
483
504
  };
484
- static DEFAULT_API = "openai-completions";
485
- static getExpectedBaseUrl(vars) {
486
- return vars.baseURL + "/api/v1/sgw/model/proxy";
487
- }
505
+ }
506
+ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
488
507
  validate(ctx) {
489
508
  const provider = getNestedMap(ctx.config, "models", "providers", "miaoda");
490
509
  if (!provider) return {
491
510
  pass: false,
492
511
  message: "models.providers.miaoda not found"
493
512
  };
494
- const expected = this.getExpected(ctx.vars);
513
+ const expected = getExpected(ctx.vars);
495
514
  if (provider.baseUrl !== expected.baseUrl) return {
496
515
  pass: false,
497
516
  message: "baseUrl mismatch: got " + provider.baseUrl + ", expected " + expected.baseUrl
@@ -539,7 +558,7 @@ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
539
558
  return { pass: true };
540
559
  }
541
560
  repair(ctx) {
542
- const expected = this.getExpected(ctx.vars);
561
+ const expected = getExpected(ctx.vars);
543
562
  setNestedValue(ctx.config, [
544
563
  "models",
545
564
  "providers",
@@ -565,19 +584,12 @@ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
565
584
  "headers"
566
585
  ], expected.headers);
567
586
  }
568
- getExpected(vars) {
569
- return {
570
- baseUrl: _ModelProviderRule.getExpectedBaseUrl(vars),
571
- apiKey: _ModelProviderRule.DEFAULT_API_KEY,
572
- api: _ModelProviderRule.DEFAULT_API,
573
- headers: { "x-api-key": _ModelProviderRule.DEFAULT_X_API_KEY_HEADER }
574
- };
575
- }
576
587
  };
577
- ModelProviderRule = _ModelProviderRule = __decorate([Rule({
588
+ ModelProviderRule = __decorate([Rule({
578
589
  key: "model_provider",
579
590
  dependsOn: ["config_syntax_check"],
580
591
  repairMode: "standard",
592
+ usesVars: ["innerAPIKey", "baseURL"],
581
593
  skipWhen: ({ hasMiaoda }) => !hasMiaoda
582
594
  })], ModelProviderRule);
583
595
  //#endregion
@@ -707,7 +719,8 @@ let FeishuChannelRule = class FeishuChannelRule extends DiagnoseRule {
707
719
  FeishuChannelRule = __decorate([Rule({
708
720
  key: "feishu_channel",
709
721
  dependsOn: ["config_syntax_check", "feishu_default_account"],
710
- repairMode: "standard"
722
+ repairMode: "standard",
723
+ usesVars: ["feishuAppID", "feishuAppSecret"]
711
724
  })], FeishuChannelRule);
712
725
  //#endregion
713
726
  //#region src/rules/feishu-default-account.ts
@@ -848,7 +861,8 @@ let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRu
848
861
  FeishuDefaultAccountRule = __decorate([Rule({
849
862
  key: "feishu_default_account",
850
863
  dependsOn: ["config_syntax_check"],
851
- repairMode: "standard"
864
+ repairMode: "standard",
865
+ usesVars: ["feishuAppID", "teamChatID"]
852
866
  })], FeishuDefaultAccountRule);
853
867
  function nonEmpty(v) {
854
868
  return typeof v === "string" && v !== "" ? v : void 0;
@@ -879,23 +893,19 @@ function secretMatchesCanonical(secret) {
879
893
  }
880
894
  //#endregion
881
895
  //#region src/rules/gateway.ts
882
- var _GatewayRule;
896
+ const DEFAULT_PORT = 18789;
897
+ const DEFAULT_MODE = "local";
898
+ const DEFAULT_BIND = "loopback";
899
+ const DEFAULT_AUTH_MODE = "token";
900
+ const DEFAULT_AUTH_TOKEN = {
901
+ source: "file",
902
+ provider: "miaoda-secret-provider",
903
+ id: "/gateway_auth_token"
904
+ };
905
+ /** Required entries in gateway.trustedProxies. Repair appends any missing
906
+ * entries while preserving caller-added extras (no overwrite). */
907
+ const DEFAULT_TRUSTED_PROXIES = ["::1", "127.0.0.1"];
883
908
  let GatewayRule = class GatewayRule extends DiagnoseRule {
884
- static {
885
- _GatewayRule = this;
886
- }
887
- static DEFAULT_PORT = 18789;
888
- static DEFAULT_MODE = "local";
889
- static DEFAULT_BIND = "loopback";
890
- static DEFAULT_AUTH_MODE = "token";
891
- static DEFAULT_AUTH_TOKEN = {
892
- source: "file",
893
- provider: "miaoda-secret-provider",
894
- id: "/gateway_auth_token"
895
- };
896
- /** Required entries in gateway.trustedProxies. Repair appends any missing
897
- * entries while preserving caller-added extras (no overwrite). */
898
- static DEFAULT_TRUSTED_PROXIES = ["::1", "127.0.0.1"];
899
909
  validate(ctx) {
900
910
  const gateway = ctx.config.gateway;
901
911
  if (!gateway || typeof gateway !== "object") return {
@@ -903,17 +913,17 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
903
913
  message: "gateway not found"
904
914
  };
905
915
  const gw = gateway;
906
- if (gw.port !== _GatewayRule.DEFAULT_PORT) return {
916
+ if (gw.port !== DEFAULT_PORT) return {
907
917
  pass: false,
908
- message: "gateway.port mismatch: got " + gw.port + ", expected " + _GatewayRule.DEFAULT_PORT
918
+ message: "gateway.port mismatch: got " + gw.port + ", expected 18789"
909
919
  };
910
- if (gw.mode !== _GatewayRule.DEFAULT_MODE) return {
920
+ if (gw.mode !== DEFAULT_MODE) return {
911
921
  pass: false,
912
- message: "gateway.mode mismatch: got " + gw.mode + ", expected " + _GatewayRule.DEFAULT_MODE
922
+ message: "gateway.mode mismatch: got " + gw.mode + ", expected local"
913
923
  };
914
- if (gw.bind !== _GatewayRule.DEFAULT_BIND) return {
924
+ if (gw.bind !== DEFAULT_BIND) return {
915
925
  pass: false,
916
- message: "gateway.bind mismatch: got " + gw.bind + ", expected " + _GatewayRule.DEFAULT_BIND
926
+ message: "gateway.bind mismatch: got " + gw.bind + ", expected loopback"
917
927
  };
918
928
  const auth = gw.auth;
919
929
  if (!auth || typeof auth !== "object") return {
@@ -921,9 +931,9 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
921
931
  message: "gateway.auth not found"
922
932
  };
923
933
  const authObj = auth;
924
- if (authObj.mode !== _GatewayRule.DEFAULT_AUTH_MODE) return {
934
+ if (authObj.mode !== DEFAULT_AUTH_MODE) return {
925
935
  pass: false,
926
- message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected " + _GatewayRule.DEFAULT_AUTH_MODE
936
+ message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected token"
927
937
  };
928
938
  const token = authObj.token;
929
939
  if (typeof token === "string") {
@@ -932,7 +942,7 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
932
942
  message: "gateway.auth.token string value mismatch"
933
943
  };
934
944
  } else if (typeof token === "object" && token !== null && !Array.isArray(token)) {
935
- if (!matchMap(token, _GatewayRule.DEFAULT_AUTH_TOKEN)) return {
945
+ if (!matchMap(token, DEFAULT_AUTH_TOKEN)) return {
936
946
  pass: false,
937
947
  message: "gateway.auth.token object mismatch: got " + JSON.stringify(token)
938
948
  };
@@ -954,7 +964,7 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
954
964
  pass: false,
955
965
  message: "gateway.trustedProxies missing or not an array"
956
966
  };
957
- const missing = _GatewayRule.DEFAULT_TRUSTED_PROXIES.filter((p) => !proxies.includes(p));
967
+ const missing = DEFAULT_TRUSTED_PROXIES.filter((p) => !proxies.includes(p));
958
968
  if (missing.length > 0) return {
959
969
  pass: false,
960
970
  message: "gateway.trustedProxies missing: " + JSON.stringify(missing)
@@ -962,19 +972,19 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
962
972
  return { pass: true };
963
973
  }
964
974
  repair(ctx) {
965
- setNestedValue(ctx.config, ["gateway", "port"], _GatewayRule.DEFAULT_PORT);
966
- setNestedValue(ctx.config, ["gateway", "mode"], _GatewayRule.DEFAULT_MODE);
967
- setNestedValue(ctx.config, ["gateway", "bind"], _GatewayRule.DEFAULT_BIND);
975
+ setNestedValue(ctx.config, ["gateway", "port"], DEFAULT_PORT);
976
+ setNestedValue(ctx.config, ["gateway", "mode"], DEFAULT_MODE);
977
+ setNestedValue(ctx.config, ["gateway", "bind"], DEFAULT_BIND);
968
978
  setNestedValue(ctx.config, [
969
979
  "gateway",
970
980
  "auth",
971
981
  "mode"
972
- ], _GatewayRule.DEFAULT_AUTH_MODE);
982
+ ], DEFAULT_AUTH_MODE);
973
983
  setNestedValue(ctx.config, [
974
984
  "gateway",
975
985
  "auth",
976
986
  "token"
977
- ], _GatewayRule.DEFAULT_AUTH_TOKEN);
987
+ ], DEFAULT_AUTH_TOKEN);
978
988
  setNestedValue(ctx.config, [
979
989
  "gateway",
980
990
  "controlUi",
@@ -983,17 +993,18 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
983
993
  const gw = ctx.config.gateway ?? {};
984
994
  const current = Array.isArray(gw.trustedProxies) ? gw.trustedProxies.slice() : [];
985
995
  const seen = new Set(current.map((v) => String(v)));
986
- for (const p of _GatewayRule.DEFAULT_TRUSTED_PROXIES) if (!seen.has(p)) {
996
+ for (const p of DEFAULT_TRUSTED_PROXIES) if (!seen.has(p)) {
987
997
  current.push(p);
988
998
  seen.add(p);
989
999
  }
990
1000
  setNestedValue(ctx.config, ["gateway", "trustedProxies"], current);
991
1001
  }
992
1002
  };
993
- GatewayRule = _GatewayRule = __decorate([Rule({
1003
+ GatewayRule = __decorate([Rule({
994
1004
  key: "gateway",
995
1005
  dependsOn: ["config_syntax_check"],
996
- repairMode: "standard"
1006
+ repairMode: "standard",
1007
+ usesVars: ["gatewayToken"]
997
1008
  })], GatewayRule);
998
1009
  //#endregion
999
1010
  //#region src/rules/allowed-origins.ts
@@ -1037,7 +1048,8 @@ let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
1037
1048
  AllowedOriginsRule = __decorate([Rule({
1038
1049
  key: "allowed_origins",
1039
1050
  dependsOn: ["config_syntax_check"],
1040
- repairMode: "standard"
1051
+ repairMode: "standard",
1052
+ usesVars: ["expectedOrigins"]
1041
1053
  })], AllowedOriginsRule);
1042
1054
  function getExpectedOrigins(vars) {
1043
1055
  return Array.isArray(vars.expectedOrigins) ? vars.expectedOrigins : [];
@@ -1116,11 +1128,12 @@ JwtTokenRule = __decorate([Rule({
1116
1128
  key: "jwt_token",
1117
1129
  dependsOn: ["config_syntax_check"],
1118
1130
  repairMode: "standard",
1131
+ usesVars: ["providerFilePath"],
1119
1132
  skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaProvider
1120
1133
  })], JwtTokenRule);
1121
1134
  //#endregion
1122
1135
  //#region src/rules/secrets-file.ts
1123
- let SecretsRule = class SecretsRule extends DiagnoseRule {
1136
+ let SecretsFileRule = class SecretsFileRule extends DiagnoseRule {
1124
1137
  validate(ctx) {
1125
1138
  const filePath = ctx.vars.secretsFilePath;
1126
1139
  if (!filePath) return {
@@ -1167,12 +1180,18 @@ let SecretsRule = class SecretsRule extends DiagnoseRule {
1167
1180
  };
1168
1181
  }
1169
1182
  };
1170
- SecretsRule = __decorate([Rule({
1183
+ SecretsFileRule = __decorate([Rule({
1171
1184
  key: "secrets_file",
1172
1185
  dependsOn: ["config_syntax_check"],
1173
1186
  repairMode: "standard",
1187
+ usesVars: [
1188
+ "secretsFilePath",
1189
+ "feishuAppSecret",
1190
+ "gatewayToken",
1191
+ "innerAPIKey"
1192
+ ],
1174
1193
  skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaSecretProvider
1175
- })], SecretsRule);
1194
+ })], SecretsFileRule);
1176
1195
  //#endregion
1177
1196
  //#region src/rules/cleanup-install-backup-dirs.ts
1178
1197
  const DIR_PREFIX = ".openclaw-install-";
@@ -1386,16 +1405,26 @@ function getAllow(config) {
1386
1405
  }
1387
1406
  //#endregion
1388
1407
  //#region src/check.ts
1389
- function runCheck(input) {
1408
+ /** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
1409
+ * AND a DoctorReport-shape payload (for `openclaw.report_cli_run`). The
1410
+ * two views are computed from the same single rule-loop pass. */
1411
+ function runCheckWithReport(input) {
1412
+ return runCheckImpl(input);
1413
+ }
1414
+ function runCheckImpl(input) {
1415
+ const tStart = Date.now();
1390
1416
  const result = { failedRules: {
1391
1417
  standard: [],
1392
1418
  ai: [],
1393
1419
  reset: []
1394
1420
  } };
1421
+ const outcomes = [];
1395
1422
  const disabledSet = new Set(input.disabledRules || []);
1396
1423
  const rules = getAllRules();
1397
1424
  const failedKeys = /* @__PURE__ */ new Set();
1398
1425
  let configParsed = false;
1426
+ let aborted = false;
1427
+ console.error(`runCheck: begin configPath=${input.configPath} disabledRules=[${[...disabledSet].join(",")}] rules=${rules.length}`);
1399
1428
  let ctx = {
1400
1429
  config: {},
1401
1430
  configPath: input.configPath,
@@ -1403,13 +1432,29 @@ function runCheck(input) {
1403
1432
  providerDeps: {
1404
1433
  usesMiaodaProvider: false,
1405
1434
  usesMiaodaSecretProvider: false
1406
- },
1407
- templateVars: input.templateVars
1435
+ }
1408
1436
  };
1409
1437
  for (const rule of rules) {
1410
1438
  const meta = rule.meta;
1411
- if (disabledSet.has(meta.key)) continue;
1412
- if (meta.dependsOn?.some((dep) => failedKeys.has(dep))) continue;
1439
+ if (disabledSet.has(meta.key)) {
1440
+ console.error(`rule ${meta.key}: skipped reason=disabledRules`);
1441
+ outcomes.push({
1442
+ rule: meta.key,
1443
+ status: "skipped",
1444
+ message: "disabledRules"
1445
+ });
1446
+ continue;
1447
+ }
1448
+ if (meta.dependsOn?.some((dep) => failedKeys.has(dep))) {
1449
+ const blockers = meta.dependsOn?.filter((d) => failedKeys.has(d)).join(",");
1450
+ console.error(`rule ${meta.key}: skipped reason=dependsOn-failed blockers=${blockers}`);
1451
+ outcomes.push({
1452
+ rule: meta.key,
1453
+ status: "skipped",
1454
+ message: `dependsOn failed: ${blockers}`
1455
+ });
1456
+ continue;
1457
+ }
1413
1458
  if (meta.dependsOn?.includes("config_syntax_check") && !configParsed) try {
1414
1459
  const parsed = loadJSON5().parse(readFile(input.configPath));
1415
1460
  const deps = analyzeProviderDeps(parsed);
@@ -1417,11 +1462,17 @@ function runCheck(input) {
1417
1462
  config: parsed,
1418
1463
  configPath: input.configPath,
1419
1464
  vars: input.vars,
1420
- providerDeps: deps,
1421
- templateVars: input.templateVars
1465
+ providerDeps: deps
1422
1466
  };
1423
1467
  configParsed = true;
1424
- } catch {
1468
+ } catch (e) {
1469
+ console.error(`runCheck: config parse failed at ${meta.key} message=${e.message}`);
1470
+ outcomes.push({
1471
+ rule: meta.key,
1472
+ status: "error",
1473
+ message: "config parse failed: " + e.message
1474
+ });
1475
+ aborted = true;
1425
1476
  break;
1426
1477
  }
1427
1478
  if (meta.skipWhen && configParsed) {
@@ -1429,15 +1480,53 @@ function runCheck(input) {
1429
1480
  if (meta.skipWhen({
1430
1481
  hasMiaoda,
1431
1482
  deps: ctx.providerDeps
1432
- })) continue;
1483
+ })) {
1484
+ console.error(`rule ${meta.key}: skipped reason=skipWhen`);
1485
+ outcomes.push({
1486
+ rule: meta.key,
1487
+ status: "skipped",
1488
+ message: "skipWhen"
1489
+ });
1490
+ continue;
1491
+ }
1492
+ }
1493
+ const t = Date.now();
1494
+ let r;
1495
+ try {
1496
+ r = rule.validate(ctx);
1497
+ } catch (e) {
1498
+ const msg = e.message;
1499
+ console.error(`rule ${meta.key}: validate -> error durationMs=${Date.now() - t} message=${msg}`);
1500
+ outcomes.push({
1501
+ rule: meta.key,
1502
+ status: "error",
1503
+ message: "validate threw: " + msg
1504
+ });
1505
+ failedKeys.add(meta.key);
1506
+ continue;
1433
1507
  }
1434
- const r = rule.validate(ctx);
1435
- if (!r.pass) {
1508
+ if (r.pass) {
1509
+ console.error(`rule ${meta.key}: validate -> pass durationMs=${Date.now() - t}`);
1510
+ outcomes.push({
1511
+ rule: meta.key,
1512
+ status: "pass"
1513
+ });
1514
+ } else {
1515
+ console.error(`rule ${meta.key}: validate -> failed durationMs=${Date.now() - t} message=${r.message ?? ""}`);
1516
+ outcomes.push({
1517
+ rule: meta.key,
1518
+ status: "failed",
1519
+ message: r.message
1520
+ });
1436
1521
  failedKeys.add(meta.key);
1437
1522
  pushFailedRule(result, meta.repairMode, meta.key, r.message || "");
1438
1523
  }
1439
1524
  }
1440
- return result;
1525
+ console.error(`runCheck: end totalMs=${Date.now() - tStart} failed={standard:${result.failedRules.standard.length},ai:${result.failedRules.ai.length},reset:${result.failedRules.reset.length}}`);
1526
+ return {
1527
+ legacy: result,
1528
+ report: finalize$2(outcomes, aborted)
1529
+ };
1441
1530
  }
1442
1531
  function pushFailedRule(result, repairMode, key, detail) {
1443
1532
  switch (repairMode) {
@@ -1458,158 +1547,43 @@ function pushFailedRule(result, repairMode, key, detail) {
1458
1547
  break;
1459
1548
  }
1460
1549
  }
1461
- //#endregion
1462
- //#region src/repair.ts
1463
- function runRepair(input) {
1464
- try {
1465
- const failedSet = new Set(input.failedRules || []);
1466
- const repairData = input.repairData || {};
1467
- const rules = getAllRules();
1468
- for (const rule of rules) {
1469
- if (!failedSet.has(rule.meta.key)) continue;
1470
- if (rule.meta.repairMode !== "standard") continue;
1471
- if (rule.meta.dependsOn?.includes("config_syntax_check")) continue;
1472
- rule.repair({
1473
- config: {},
1474
- configPath: input.configPath,
1475
- vars: input.vars,
1476
- providerDeps: {
1477
- usesMiaodaProvider: false,
1478
- usesMiaodaSecretProvider: false
1479
- },
1480
- templateVars: input.templateVars
1481
- });
1482
- }
1483
- const JSON5 = loadJSON5();
1484
- let config;
1485
- try {
1486
- config = JSON5.parse(readFile(input.configPath));
1487
- } catch (e) {
1488
- return {
1489
- success: false,
1490
- error: "cannot parse config for repair: " + e.message
1491
- };
1492
- }
1493
- const deps = analyzeProviderDeps(config);
1494
- const ctx = {
1495
- config,
1496
- configPath: input.configPath,
1497
- vars: input.vars,
1498
- providerDeps: deps,
1499
- templateVars: input.templateVars
1500
- };
1501
- let configDirty = false;
1502
- for (const rule of rules) {
1503
- if (!failedSet.has(rule.meta.key)) continue;
1504
- if (rule.meta.repairMode !== "standard") continue;
1505
- if (!rule.meta.dependsOn?.includes("config_syntax_check")) continue;
1506
- rule.repair(ctx);
1507
- configDirty = true;
1508
- }
1509
- if (configDirty) writeFile(input.configPath, JSON.stringify(config, null, 2));
1510
- if (repairData.secretsContent && input.vars.secretsFilePath) writeFile(input.vars.secretsFilePath, repairData.secretsContent);
1511
- if (repairData.providerKeyContent && input.vars.providerFilePath) writeFile(input.vars.providerFilePath, repairData.providerKeyContent);
1512
- if (repairData.restartCommand) try {
1513
- shell(repairData.restartCommand, 3e4);
1514
- } catch (e) {
1515
- return {
1516
- success: false,
1517
- error: "restart command failed: " + e.message
1518
- };
1519
- }
1520
- return { success: true };
1521
- } catch (e) {
1522
- return {
1523
- success: false,
1524
- error: "repair failed: " + e.message
1525
- };
1526
- }
1527
- }
1528
- //#endregion
1529
- //#region src/paths.ts
1530
- /**
1531
- * Central directory for all ephemeral diagnose/reset artifacts: task status
1532
- * files (`reset-<taskId>.json`) and human-readable step logs
1533
- * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
1534
- * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
1535
- * run, and each run's log is right next to its state.
1536
- */
1537
- const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
1538
- function resetResultFile(taskId) {
1539
- return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
1540
- }
1541
- function resetLogFile(taskId) {
1542
- return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
1543
- }
1544
- /** Sandbox workspace root where openclaw config + agent state lives. */
1545
- const WORKSPACE_DIR = "/home/gem/workspace/agent";
1546
- /** File containing the provider key used by the openclaw miaoda provider. */
1547
- const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
1548
- /** File containing the miaoda openclaw secrets JSON. */
1549
- const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
1550
- /** Absolute path to the openclaw config JSON. */
1551
- const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
1552
- //#endregion
1553
- //#region src/logger.ts
1554
- /**
1555
- * Shared CLI log file. Every log line the CLI emits — whether through
1556
- * `console.error` (rules, helpers, errors) or through the per-task
1557
- * `makeLogger` (reset worker) — is tee'd here so operators have a single
1558
- * file to tail when diagnosing a sandbox.
1559
- *
1560
- * `/tmp` is ephemeral on sandbox restart; we rely on that for rotation
1561
- * (no size-based rotation implemented).
1562
- */
1563
- const CLI_LOG_FILE = "/tmp/openclaw-diagnose/cli.log";
1564
- /** Append one line to the shared cli.log. Swallows any fs error —
1565
- * logging must never break the business flow. */
1566
- function appendCliLog(line) {
1567
- try {
1568
- const dir = node_path.default.dirname(CLI_LOG_FILE);
1569
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
1570
- node_fs.default.appendFileSync(CLI_LOG_FILE, line);
1571
- } catch {}
1572
- }
1573
- let stderrMirrorInstalled = false;
1574
- /**
1575
- * Install a process-wide `console.error` interceptor that mirrors each
1576
- * line to BOTH the original stderr AND cli.log. Call once at CLI entry
1577
- * before any subcommand dispatch; idempotent.
1578
- *
1579
- * Why console.error and not console.log: the CLI's stdout carries the
1580
- * structured JSON result protocol consumed by sandbox_console and other
1581
- * callers — any log line on stdout would corrupt JSON parsing. Rules,
1582
- * helpers, and error paths therefore must route debug output through
1583
- * console.error (stderr).
1584
- */
1585
- function installStderrMirror() {
1586
- if (stderrMirrorInstalled) return;
1587
- stderrMirrorInstalled = true;
1588
- const original = console.error.bind(console);
1589
- console.error = (...args) => {
1590
- original(...args);
1591
- const body = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
1592
- appendCliLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${body}\n`);
1550
+ function finalize$2(results, aborted) {
1551
+ const summary = {
1552
+ pass: 0,
1553
+ failed: 0,
1554
+ fixed: 0,
1555
+ stillBroken: 0,
1556
+ skipped: 0,
1557
+ error: 0,
1558
+ unknown: 0
1593
1559
  };
1594
- }
1595
- function safeStringify(v) {
1596
- try {
1597
- return JSON.stringify(v);
1598
- } catch {
1599
- return String(v);
1560
+ for (const r of results) switch (r.status) {
1561
+ case "pass":
1562
+ summary.pass++;
1563
+ break;
1564
+ case "failed":
1565
+ summary.failed++;
1566
+ break;
1567
+ case "fixed":
1568
+ summary.fixed++;
1569
+ break;
1570
+ case "still-broken":
1571
+ summary.stillBroken++;
1572
+ break;
1573
+ case "skipped":
1574
+ summary.skipped++;
1575
+ break;
1576
+ case "error":
1577
+ summary.error++;
1578
+ break;
1579
+ case "unknown":
1580
+ summary.unknown++;
1581
+ break;
1600
1582
  }
1601
- }
1602
- function makeLogger(logFile) {
1603
- try {
1604
- const dir = node_path.default.dirname(logFile);
1605
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
1606
- } catch {}
1607
- return (msg) => {
1608
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}\n`;
1609
- try {
1610
- node_fs.default.appendFileSync(logFile, line);
1611
- } catch {}
1612
- appendCliLog(line);
1583
+ return {
1584
+ results,
1585
+ summary,
1586
+ aborted
1613
1587
  };
1614
1588
  }
1615
1589
  //#endregion
@@ -1673,6 +1647,40 @@ function shellQuote(s) {
1673
1647
  return `'${s.replace(/'/g, `'\\''`)}'`;
1674
1648
  }
1675
1649
  /**
1650
+ * Snapshot the current `openclaw.json` to `<configPath>.<YYYYMMDD_HHMMSS>.bak`
1651
+ * before a repair / fix flow rewrites it. Local-time stamp matches the
1652
+ * shell-style backup convention (`cp openclaw.json openclaw.json.$(date
1653
+ * +%Y%m%d_%H%M%S).bak`) so operators triaging on the sandbox see the
1654
+ * familiar filename pattern.
1655
+ *
1656
+ * Returns the absolute backup path on success, or `null` when:
1657
+ * - source doesn't exist (first-time setup; nothing to back up)
1658
+ * - copy fails for any reason (disk full, permission denied, etc.)
1659
+ *
1660
+ * Failure is logged to cli.log via console.error and swallowed — the
1661
+ * caller's repair/fix work goes ahead either way. The backup is a safety
1662
+ * net, not a correctness gate; better to do the repair than refuse over
1663
+ * a missing rollback file.
1664
+ */
1665
+ function backupConfigSync(configPath) {
1666
+ if (!node_fs.default.existsSync(configPath)) {
1667
+ console.error(`backupConfigSync: skip — source missing path=${configPath}`);
1668
+ return null;
1669
+ }
1670
+ const d = /* @__PURE__ */ new Date();
1671
+ const pad = (n) => String(n).padStart(2, "0");
1672
+ const backupPath = `${configPath}.${`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`}.bak`;
1673
+ try {
1674
+ node_fs.default.copyFileSync(configPath, backupPath);
1675
+ const bytes = node_fs.default.statSync(backupPath).size;
1676
+ console.error(`backupConfigSync: ok src=${configPath} dst=${backupPath} bytes=${bytes}`);
1677
+ return backupPath;
1678
+ } catch (e) {
1679
+ console.error(`backupConfigSync: failed src=${configPath} message=${e.message}`);
1680
+ return null;
1681
+ }
1682
+ }
1683
+ /**
1676
1684
  * Extract an npm-packed gzipped tarball.
1677
1685
  *
1678
1686
  * ## The problem this works around
@@ -1746,59 +1754,510 @@ function extractTarballTolerant(tarball, destDir, opts = {}) {
1746
1754
  }
1747
1755
  }
1748
1756
  //#endregion
1749
- //#region src/reset-async.ts
1750
- /**
1751
- * Start an async reset task: spawn a detached child process and return the taskId.
1752
- *
1753
- * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
1754
- */
1755
- function startAsyncReset(ctxBase64) {
1756
- const taskId = (0, node_crypto.randomUUID)();
1757
- const resultFile = resetResultFile(taskId);
1758
- const log = makeLogger(resetLogFile(taskId));
1759
- log(`=== startAsyncReset spawning worker for taskId=${taskId} ===`);
1760
- const initial = {
1761
- status: "running",
1762
- step: 0,
1763
- totalSteps: 9,
1764
- progress: "初始化...",
1765
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
1766
- };
1767
- const tmpPath = resultFile + ".tmp";
1768
- const dir = node_path.default.dirname(resultFile);
1769
- if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
1770
- node_fs.default.writeFileSync(tmpPath, JSON.stringify(initial), "utf-8");
1771
- moveSafe(tmpPath, resultFile);
1772
- const child = (0, node_child_process.spawn)(process.execPath, [
1773
- process.argv[1],
1774
- "reset",
1775
- "--worker",
1776
- `--task-id=${taskId}`,
1777
- `--ctx=${ctxBase64}`
1778
- ], {
1779
- detached: true,
1780
- stdio: "ignore"
1781
- });
1782
- child.on("error", (err) => {
1783
- log(`FATAL worker failed to start: ${err.message}`);
1784
- const failResult = {
1785
- status: "failed",
1786
- step: 0,
1787
- totalSteps: 9,
1788
- progress: "Worker process failed to start",
1789
- error: err.message,
1790
- startedAt: initial.startedAt,
1791
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
1792
- };
1793
- const errTmpPath = resultFile + ".tmp";
1794
- node_fs.default.writeFileSync(errTmpPath, JSON.stringify(failResult));
1795
- moveSafe(errTmpPath, resultFile);
1796
- });
1797
- child.unref();
1798
- log(`spawned worker pid=${child.pid}`);
1757
+ //#region src/repair.ts
1758
+ /** Telemetry-aware entry. Same per-rule pass + side effects, but also
1759
+ * builds a `DoctorReport`-shape payload from per-rule revalidate
1760
+ * outcomes (`fixed` / `still-broken` / `error`) so `report_cli_run`
1761
+ * carries the same shape doctor produces. */
1762
+ function runRepairWithReport(input) {
1763
+ return runRepairImpl(input);
1764
+ }
1765
+ function runRepairImpl(input) {
1766
+ const tStart = Date.now();
1767
+ console.error(`runRepair: begin configPath=${input.configPath} failedRules=[${(input.failedRules ?? []).join(",")}]`);
1768
+ const outcomes = [];
1769
+ let aborted = false;
1770
+ let legacy = { success: true };
1771
+ try {
1772
+ const failedSet = new Set(input.failedRules || []);
1773
+ const repairData = input.repairData || {};
1774
+ const rules = getAllRules();
1775
+ const phase1Ctx = {
1776
+ config: {},
1777
+ configPath: input.configPath,
1778
+ vars: input.vars,
1779
+ providerDeps: {
1780
+ usesMiaodaProvider: false,
1781
+ usesMiaodaSecretProvider: false
1782
+ }
1783
+ };
1784
+ for (const rule of rules) {
1785
+ if (!failedSet.has(rule.meta.key)) continue;
1786
+ if (rule.meta.repairMode !== "standard") continue;
1787
+ if (rule.meta.dependsOn?.includes("config_syntax_check")) continue;
1788
+ const tRepair = Date.now();
1789
+ try {
1790
+ rule.repair(phase1Ctx);
1791
+ } catch (e) {
1792
+ const msg = e.message;
1793
+ console.error(`rule ${rule.meta.key}: repair (phase1) -> error durationMs=${Date.now() - tRepair} message=${msg}`);
1794
+ outcomes.push({
1795
+ rule: rule.meta.key,
1796
+ status: "error",
1797
+ message: "repair threw: " + msg
1798
+ });
1799
+ aborted = true;
1800
+ continue;
1801
+ }
1802
+ console.error(`rule ${rule.meta.key}: repair (phase1) -> ok durationMs=${Date.now() - tRepair}`);
1803
+ const tRev = Date.now();
1804
+ try {
1805
+ const v = rule.validate(phase1Ctx);
1806
+ if (v.pass) {
1807
+ console.error(`rule ${rule.meta.key}: revalidate (phase1) -> pass (fixed) durationMs=${Date.now() - tRev}`);
1808
+ outcomes.push({
1809
+ rule: rule.meta.key,
1810
+ status: "fixed"
1811
+ });
1812
+ } else {
1813
+ console.error(`rule ${rule.meta.key}: revalidate (phase1) -> still-broken durationMs=${Date.now() - tRev} after=${v.message ?? ""}`);
1814
+ outcomes.push({
1815
+ rule: rule.meta.key,
1816
+ status: "still-broken",
1817
+ after: v.message
1818
+ });
1819
+ }
1820
+ } catch (e) {
1821
+ const msg = e.message;
1822
+ console.error(`rule ${rule.meta.key}: revalidate (phase1) -> error durationMs=${Date.now() - tRev} message=${msg}`);
1823
+ outcomes.push({
1824
+ rule: rule.meta.key,
1825
+ status: "error",
1826
+ message: "post-repair validate threw: " + msg
1827
+ });
1828
+ aborted = true;
1829
+ }
1830
+ }
1831
+ const JSON5 = loadJSON5();
1832
+ let config;
1833
+ try {
1834
+ config = JSON5.parse(readFile(input.configPath));
1835
+ } catch (e) {
1836
+ const msg = e.message;
1837
+ console.error(`runRepair: config parse failed message=${msg}`);
1838
+ outcomes.push({
1839
+ rule: "config_syntax_check",
1840
+ status: "error",
1841
+ message: "config parse failed: " + msg
1842
+ });
1843
+ legacy = {
1844
+ success: false,
1845
+ error: "cannot parse config for repair: " + msg
1846
+ };
1847
+ return {
1848
+ legacy,
1849
+ report: finalize$1(outcomes, true)
1850
+ };
1851
+ }
1852
+ const deps = analyzeProviderDeps(config);
1853
+ const ctx = {
1854
+ config,
1855
+ configPath: input.configPath,
1856
+ vars: input.vars,
1857
+ providerDeps: deps
1858
+ };
1859
+ let configDirty = false;
1860
+ for (const rule of rules) {
1861
+ if (!failedSet.has(rule.meta.key)) continue;
1862
+ if (rule.meta.repairMode !== "standard") continue;
1863
+ if (!rule.meta.dependsOn?.includes("config_syntax_check")) continue;
1864
+ const tRepair = Date.now();
1865
+ try {
1866
+ rule.repair(ctx);
1867
+ } catch (e) {
1868
+ const msg = e.message;
1869
+ console.error(`rule ${rule.meta.key}: repair (phase2) -> error durationMs=${Date.now() - tRepair} message=${msg}`);
1870
+ outcomes.push({
1871
+ rule: rule.meta.key,
1872
+ status: "error",
1873
+ message: "repair threw: " + msg
1874
+ });
1875
+ aborted = true;
1876
+ continue;
1877
+ }
1878
+ console.error(`rule ${rule.meta.key}: repair (phase2) -> ok durationMs=${Date.now() - tRepair}`);
1879
+ configDirty = true;
1880
+ const tRev = Date.now();
1881
+ try {
1882
+ const v = rule.validate(ctx);
1883
+ if (v.pass) {
1884
+ console.error(`rule ${rule.meta.key}: revalidate (phase2) -> pass (fixed) durationMs=${Date.now() - tRev}`);
1885
+ outcomes.push({
1886
+ rule: rule.meta.key,
1887
+ status: "fixed"
1888
+ });
1889
+ } else {
1890
+ console.error(`rule ${rule.meta.key}: revalidate (phase2) -> still-broken durationMs=${Date.now() - tRev} after=${v.message ?? ""}`);
1891
+ outcomes.push({
1892
+ rule: rule.meta.key,
1893
+ status: "still-broken",
1894
+ after: v.message
1895
+ });
1896
+ }
1897
+ } catch (e) {
1898
+ const msg = e.message;
1899
+ console.error(`rule ${rule.meta.key}: revalidate (phase2) -> error durationMs=${Date.now() - tRev} message=${msg}`);
1900
+ outcomes.push({
1901
+ rule: rule.meta.key,
1902
+ status: "error",
1903
+ message: "post-repair validate threw: " + msg
1904
+ });
1905
+ aborted = true;
1906
+ }
1907
+ }
1908
+ if (configDirty) {
1909
+ backupConfigSync(input.configPath);
1910
+ const serialized = JSON.stringify(config, null, 2);
1911
+ writeFile(input.configPath, serialized);
1912
+ console.error(`runRepair: config writeback ok path=${input.configPath} bytes=${serialized.length}`);
1913
+ }
1914
+ if (repairData.secretsContent && input.vars.secretsFilePath) {
1915
+ writeFile(input.vars.secretsFilePath, repairData.secretsContent);
1916
+ console.error(`runRepair: secrets writeback ok path=${input.vars.secretsFilePath} bytes=${repairData.secretsContent.length}`);
1917
+ }
1918
+ if (repairData.providerKeyContent && input.vars.providerFilePath) {
1919
+ writeFile(input.vars.providerFilePath, repairData.providerKeyContent);
1920
+ console.error(`runRepair: provider key writeback ok path=${input.vars.providerFilePath} bytes=${repairData.providerKeyContent.length}`);
1921
+ }
1922
+ if (repairData.restartCommand) {
1923
+ const t = Date.now();
1924
+ try {
1925
+ console.error(`runRepair: restart begin cmd=${JSON.stringify(repairData.restartCommand)}`);
1926
+ shell(repairData.restartCommand, 3e4);
1927
+ console.error(`runRepair: restart ok durationMs=${Date.now() - t}`);
1928
+ } catch (e) {
1929
+ const msg = e.message;
1930
+ console.error(`runRepair: restart failed durationMs=${Date.now() - t} message=${msg}`);
1931
+ legacy = {
1932
+ success: false,
1933
+ error: "restart command failed: " + msg
1934
+ };
1935
+ return {
1936
+ legacy,
1937
+ report: finalize$1(outcomes, true)
1938
+ };
1939
+ }
1940
+ }
1941
+ console.error(`runRepair: end success=true totalMs=${Date.now() - tStart}`);
1942
+ legacy = { success: true };
1943
+ return {
1944
+ legacy,
1945
+ report: finalize$1(outcomes, aborted)
1946
+ };
1947
+ } catch (e) {
1948
+ const msg = e.message;
1949
+ console.error(`runRepair: end success=false totalMs=${Date.now() - tStart} message=${msg}`);
1950
+ legacy = {
1951
+ success: false,
1952
+ error: "repair failed: " + msg
1953
+ };
1954
+ return {
1955
+ legacy,
1956
+ report: finalize$1(outcomes, true)
1957
+ };
1958
+ }
1959
+ }
1960
+ function finalize$1(results, aborted) {
1961
+ const summary = {
1962
+ pass: 0,
1963
+ failed: 0,
1964
+ fixed: 0,
1965
+ stillBroken: 0,
1966
+ skipped: 0,
1967
+ error: 0,
1968
+ unknown: 0
1969
+ };
1970
+ for (const r of results) switch (r.status) {
1971
+ case "pass":
1972
+ summary.pass++;
1973
+ break;
1974
+ case "failed":
1975
+ summary.failed++;
1976
+ break;
1977
+ case "fixed":
1978
+ summary.fixed++;
1979
+ break;
1980
+ case "still-broken":
1981
+ summary.stillBroken++;
1982
+ break;
1983
+ case "skipped":
1984
+ summary.skipped++;
1985
+ break;
1986
+ case "error":
1987
+ summary.error++;
1988
+ break;
1989
+ case "unknown":
1990
+ summary.unknown++;
1991
+ break;
1992
+ }
1993
+ return {
1994
+ results,
1995
+ summary,
1996
+ aborted
1997
+ };
1998
+ }
1999
+ //#endregion
2000
+ //#region src/paths.ts
2001
+ /**
2002
+ * Central directory for all ephemeral diagnose/reset artifacts: task status
2003
+ * files (`reset-<taskId>.json`) and human-readable step logs
2004
+ * (`reset-<taskId>.log`). Having everything under one dir makes debugging a
2005
+ * stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
2006
+ * run, and each run's log is right next to its state.
2007
+ */
2008
+ const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
2009
+ function resetResultFile(taskId) {
2010
+ return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
2011
+ }
2012
+ function resetLogFile(taskId) {
2013
+ return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
2014
+ }
2015
+ /** Sandbox workspace root where openclaw config + agent state lives. */
2016
+ const WORKSPACE_DIR = "/home/gem/workspace/agent";
2017
+ /** File containing the provider key used by the openclaw miaoda provider. */
2018
+ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
2019
+ /** File containing the miaoda openclaw secrets JSON. */
2020
+ const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
2021
+ /** Absolute path to the openclaw config JSON. */
2022
+ const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
2023
+ //#endregion
2024
+ //#region src/run-log.ts
2025
+ let currentRunContext;
2026
+ /**
2027
+ * Install the run context for this CLI execution. Must be called once at
2028
+ * the top of `main()` before any subcommand work; idempotent (last call
2029
+ * wins, but `main` only calls it once).
2030
+ *
2031
+ * Resolution order for `runId`:
2032
+ * 1. `OPENCLAW_RUN_ID` env var — set by a parent CLI process (e.g. the
2033
+ * `reset --async` dispatcher exports it before spawning the worker)
2034
+ * so cli.log lines for both processes carry the same `[run=...]`
2035
+ * tag. Any non-empty value is honoured verbatim.
2036
+ * 2. `--trace-id=<id>` flag — caller-supplied upstream trace id.
2037
+ * 3. Generated locally as `gen-<8 hex>`.
2038
+ *
2039
+ * Safe to call without any input — runId falls through to the generated
2040
+ * branch and both metadata fields stay undefined.
2041
+ */
2042
+ function setRunContext(opts) {
2043
+ const inherited = process.env.OPENCLAW_RUN_ID;
2044
+ if (inherited && inherited !== "") currentRunContext = {
2045
+ runId: inherited,
2046
+ caller: opts.caller,
2047
+ traceId: opts.traceId,
2048
+ generated: false
2049
+ };
2050
+ else if (opts.traceId) currentRunContext = {
2051
+ runId: opts.traceId,
2052
+ caller: opts.caller,
2053
+ traceId: opts.traceId,
2054
+ generated: false
2055
+ };
2056
+ else currentRunContext = {
2057
+ runId: "gen-" + (0, node_crypto.randomBytes)(4).toString("hex"),
2058
+ caller: opts.caller,
2059
+ traceId: void 0,
2060
+ generated: true
2061
+ };
2062
+ return currentRunContext;
2063
+ }
2064
+ function getRunContext() {
2065
+ return currentRunContext;
2066
+ }
2067
+ /**
2068
+ * Format the run-scope tag the stderr mirror injects into cli.log lines.
2069
+ * Empty string when no context is set (mirror falls back to plain
2070
+ * timestamped lines, preserving pre-context startup logging).
2071
+ */
2072
+ function runTag(rc = currentRunContext) {
2073
+ if (!rc) return "";
2074
+ const parts = [`run=${rc.runId}`];
2075
+ if (rc.caller) parts.push(`caller=${rc.caller}`);
2076
+ return `[${parts.join(" ")}]`;
2077
+ }
2078
+ //#endregion
2079
+ //#region src/logger.ts
2080
+ /**
2081
+ * Shared CLI log file. Every log line the CLI emits — whether through
2082
+ * `console.error` (rules, helpers, errors) or through the per-task
2083
+ * `makeLogger` (reset worker) — is tee'd here so operators have a single
2084
+ * file to tail when diagnosing a sandbox.
2085
+ *
2086
+ * `/tmp` is ephemeral on sandbox restart; we rely on that for rotation
2087
+ * (no size-based rotation implemented).
2088
+ */
2089
+ const CLI_LOG_FILE = "/tmp/openclaw-diagnose/cli.log";
2090
+ /** Append one line to the shared cli.log. Swallows any fs error —
2091
+ * logging must never break the business flow. */
2092
+ function appendCliLog(line) {
2093
+ try {
2094
+ const dir = node_path.default.dirname(CLI_LOG_FILE);
2095
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
2096
+ node_fs.default.appendFileSync(CLI_LOG_FILE, line);
2097
+ } catch {}
2098
+ }
2099
+ let stderrMirrorInstalled = false;
2100
+ /**
2101
+ * Install a process-wide `console.error` interceptor. Always tees each
2102
+ * line to cli.log; the stderr passthrough is opt-in via `stderrEnabled`.
2103
+ *
2104
+ * Default (`stderrEnabled: false`): the terminal sees ONLY this command's
2105
+ * stdout (typically a single JSON result line). Lifecycle / progress /
2106
+ * error logs are fully captured in `/tmp/openclaw-diagnose/cli.log`,
2107
+ * which is the canonical debugging surface and already carries
2108
+ * timestamps and `[run=...]` tags.
2109
+ *
2110
+ * Verbose (`stderrEnabled: true`, opt in with the top-level `-x` flag):
2111
+ * lifecycle logs also stream to stderr in real time. Useful when
2112
+ * iterating on a single sandbox.
2113
+ *
2114
+ * Idempotent: calling twice is a no-op (first install wins). Tests that
2115
+ * need to swap mode should call `resetStderrMirrorForTests()` first.
2116
+ *
2117
+ * Why console.error (and not console.log): the CLI's stdout carries the
2118
+ * structured JSON result protocol consumed by sandbox_console and other
2119
+ * callers — any log line on stdout would corrupt JSON parsing.
2120
+ */
2121
+ function installStderrMirror(opts = {}) {
2122
+ if (stderrMirrorInstalled) return;
2123
+ stderrMirrorInstalled = true;
2124
+ const stderrEnabled = opts.stderrEnabled ?? false;
2125
+ const original = console.error.bind(console);
2126
+ console.error = (...args) => {
2127
+ if (stderrEnabled) original(...args);
2128
+ const body = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
2129
+ const tag = runTag();
2130
+ appendCliLog(`[${(/* @__PURE__ */ new Date()).toISOString()}]${tag ? ` ${tag}` : ""} ${body}\n`);
2131
+ };
2132
+ }
2133
+ function safeStringify(v) {
2134
+ try {
2135
+ return JSON.stringify(v);
2136
+ } catch {
2137
+ return String(v);
2138
+ }
2139
+ }
2140
+ function makeLogger(logFile) {
2141
+ try {
2142
+ const dir = node_path.default.dirname(logFile);
2143
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
2144
+ } catch {}
2145
+ return (msg) => {
2146
+ const tag = runTag();
2147
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}]${tag ? ` ${tag}` : ""} ${msg}\n`;
2148
+ try {
2149
+ node_fs.default.appendFileSync(logFile, line);
2150
+ } catch {}
2151
+ appendCliLog(line);
2152
+ };
2153
+ }
2154
+ //#endregion
2155
+ //#region src/reset-async.ts
2156
+ /**
2157
+ * Start an async reset task: spawn a detached child process and return the taskId.
2158
+ *
2159
+ * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
2160
+ */
2161
+ function startAsyncReset(ctxBase64) {
2162
+ const taskId = (0, node_crypto.randomUUID)();
2163
+ const resultFile = resetResultFile(taskId);
2164
+ const log = makeLogger(resetLogFile(taskId));
2165
+ log(`=== startAsyncReset spawning worker for taskId=${taskId} ===`);
2166
+ const initial = {
2167
+ status: "running",
2168
+ step: 0,
2169
+ totalSteps: 9,
2170
+ progress: "初始化...",
2171
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2172
+ };
2173
+ const tmpPath = resultFile + ".tmp";
2174
+ const dir = node_path.default.dirname(resultFile);
2175
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
2176
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(initial), "utf-8");
2177
+ moveSafe(tmpPath, resultFile);
2178
+ const rc = getRunContext();
2179
+ const childEnv = { ...process.env };
2180
+ if (rc?.runId) childEnv.OPENCLAW_RUN_ID = rc.runId;
2181
+ const child = (0, node_child_process.spawn)(process.execPath, [
2182
+ process.argv[1],
2183
+ "reset",
2184
+ "--worker",
2185
+ `--task-id=${taskId}`,
2186
+ `--ctx=${ctxBase64}`
2187
+ ], {
2188
+ detached: true,
2189
+ stdio: "ignore",
2190
+ env: childEnv
2191
+ });
2192
+ child.on("error", (err) => {
2193
+ log(`FATAL worker failed to start: ${err.message}`);
2194
+ const failResult = {
2195
+ status: "failed",
2196
+ step: 0,
2197
+ totalSteps: 9,
2198
+ progress: "Worker process failed to start",
2199
+ error: err.message,
2200
+ startedAt: initial.startedAt,
2201
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
2202
+ };
2203
+ const errTmpPath = resultFile + ".tmp";
2204
+ node_fs.default.writeFileSync(errTmpPath, JSON.stringify(failResult));
2205
+ moveSafe(errTmpPath, resultFile);
2206
+ });
2207
+ child.unref();
2208
+ log(`spawned worker pid=${child.pid}`);
1799
2209
  return { taskId };
1800
2210
  }
1801
2211
  //#endregion
2212
+ //#region src/oss/fetchWithDiag.ts
2213
+ async function fetchWithDiag(url, opts = {}) {
2214
+ const label = opts.label ?? originOf(url);
2215
+ const timeoutMs = opts.timeoutMs ?? 3e4;
2216
+ const start = Date.now();
2217
+ const ac = new AbortController();
2218
+ const timer = timeoutMs > 0 && Number.isFinite(timeoutMs) ? setTimeout(() => ac.abort(), timeoutMs) : void 0;
2219
+ try {
2220
+ return await fetch(url, { signal: ac.signal });
2221
+ } catch (e) {
2222
+ const durationMs = Date.now() - start;
2223
+ const causeStr = e.name === "AbortError" || ac.signal.aborted && timeoutMs > 0 ? `request aborted after ${timeoutMs}ms (timeout)` : describeCause(e.cause) || e.message;
2224
+ throw new Error(`fetch ${label} failed: ${causeStr} (url=${redactUrl(url)} durationMs=${durationMs})`);
2225
+ } finally {
2226
+ if (timer) clearTimeout(timer);
2227
+ }
2228
+ }
2229
+ /** Walk the Error.cause chain and produce a single-line summary like
2230
+ * `ENOTFOUND getaddrinfo ENOTFOUND oss.example.com`. */
2231
+ function describeCause(c, depth = 0) {
2232
+ if (!c || depth > 3) return "";
2233
+ if (c instanceof Error) {
2234
+ const code = c.code;
2235
+ const head = code ? `${code} ${c.message}` : c.message;
2236
+ const inner = describeCause(c.cause, depth + 1);
2237
+ return inner ? `${head} <- ${inner}` : head;
2238
+ }
2239
+ return String(c);
2240
+ }
2241
+ /** Strip query string — OSS signed URLs put the auth token there. Keeps
2242
+ * protocol/host/path so the operator can tell *which* OSS bucket / object
2243
+ * was being fetched. */
2244
+ function redactUrl(raw) {
2245
+ try {
2246
+ const u = new URL(raw);
2247
+ const tail = u.search ? "?<redacted>" : "";
2248
+ return `${u.protocol}//${u.host}${u.pathname}${tail}`;
2249
+ } catch {
2250
+ return "<invalid-url>";
2251
+ }
2252
+ }
2253
+ function originOf(raw) {
2254
+ try {
2255
+ return new URL(raw).host;
2256
+ } catch {
2257
+ return "unknown-host";
2258
+ }
2259
+ }
2260
+ //#endregion
1802
2261
  //#region src/oss/fetchManifest.ts
1803
2262
  const MANIFEST_PREFIX = "builtin/manifests/openclaw/recommended/";
1804
2263
  const MANIFEST_SUFFIX = ".json";
@@ -1810,10 +2269,24 @@ async function fetchManifest(ossFileMap, tag) {
1810
2269
  const availStr = available.length ? available.join(", ") : "(none)";
1811
2270
  throw new Error(`manifest signed URL missing for tag "${tag}" (key ${key}). Available tags in ossFileMap: ${availStr}. Either pass an available tag or update the studio_server TCC openclaw_upgrade_config supported_versions.`);
1812
2271
  }
1813
- const res = await fetch(url);
1814
- if (!res.ok) throw new Error(`fetch manifest failed: HTTP ${res.status} ${res.statusText}`);
2272
+ const label = `openclaw manifest tag=${tag}`;
2273
+ const res = await fetchWithDiag(url, { label });
2274
+ if (!res.ok) {
2275
+ const body = await readPreview(res);
2276
+ throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (body ? ` body=${body}` : ""));
2277
+ }
1815
2278
  return await res.json();
1816
2279
  }
2280
+ /** Best-effort body preview for HTTP-error diagnostics. OSS errors are
2281
+ * XML; first 256 chars usually contain the <Code> + <Message> we care
2282
+ * about. Failures here are swallowed (logging-only, not load-bearing). */
2283
+ async function readPreview(res) {
2284
+ try {
2285
+ return (await res.text()).slice(0, 256);
2286
+ } catch {
2287
+ return "";
2288
+ }
2289
+ }
1817
2290
  async function downloadWithCache(pkg, ossFileMap, opts = {}) {
1818
2291
  const cacheRoot = opts.cacheRoot ?? "/tmp/openclaw-diagnose/resources";
1819
2292
  const shortHash = pkg.shasum.slice(0, 16);
@@ -1827,9 +2300,19 @@ async function downloadWithCache(pkg, ossFileMap, opts = {}) {
1827
2300
  const expected = pkg.integrity.slice(7);
1828
2301
  const tmpFile = node_path.default.join(destDir, `.tmp.${process.pid}.${node_crypto.default.randomBytes(4).toString("hex")}`);
1829
2302
  try {
1830
- const res = await fetch(url);
1831
- if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
1832
- if (!res.body) throw new Error(`download failed: empty body for ${pkg.ossKey}`);
2303
+ const label = `download ${pkg.ossKey}`;
2304
+ const res = await fetchWithDiag(url, {
2305
+ label,
2306
+ timeoutMs: 6e4
2307
+ });
2308
+ if (!res.ok) {
2309
+ let preview = "";
2310
+ try {
2311
+ preview = (await res.text()).slice(0, 256);
2312
+ } catch {}
2313
+ throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (preview ? ` body=${preview}` : ""));
2314
+ }
2315
+ if (!res.body) throw new Error(`${label}: empty body`);
1833
2316
  const hasher = node_crypto.default.createHash("sha512");
1834
2317
  const source = node_stream.Readable.fromWeb(res.body);
1835
2318
  async function* teeAndHash(src) {
@@ -2354,16 +2837,6 @@ function writeSecretsAndRestart(vars, resetData, configDir, log) {
2354
2837
  log(`restart.sh done in ${Date.now() - t}ms`);
2355
2838
  } else log(`no restart.sh at ${restartScript}, skip`);
2356
2839
  }
2357
- /**
2358
- * Run the 9-step reset process. Called from the worker entry point.
2359
- *
2360
- * Each step is an independent function. The orchestrator handles progress
2361
- * reporting, error handling, and process-level exception guards.
2362
- *
2363
- * Template assets (openclaw.json + scripts/) are downloaded from OSS into a
2364
- * scratch dir via `stageTemplate` before step 1 — there is no bundled
2365
- * `template/` directory at runtime any more.
2366
- */
2367
2840
  async function runReset(input, taskId, resultFile) {
2368
2841
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2369
2842
  const { configPath, vars, resetData } = input;
@@ -2371,6 +2844,7 @@ async function runReset(input, taskId, resultFile) {
2371
2844
  const stagedDir = node_path.default.join(DIAGNOSE_DIR, `reset-${taskId}-template`);
2372
2845
  let currentStep = 0;
2373
2846
  let stepStartedAt = Date.now();
2847
+ const stepResults = [];
2374
2848
  const log = makeLogger(resetLogFile(taskId));
2375
2849
  log(`=== reset started, taskId=${taskId}, pid=${process.pid} ===`);
2376
2850
  log(`configPath=${configPath}, configDir=${configDir}, stagedDir=${stagedDir}`);
@@ -2379,7 +2853,12 @@ async function runReset(input, taskId, resultFile) {
2379
2853
  const err = "resetData.ossFileMap missing or empty";
2380
2854
  log(`ERROR: ${err}`);
2381
2855
  markFailed(resultFile, 0, err, startedAt);
2382
- process.exit(1);
2856
+ return {
2857
+ success: false,
2858
+ steps: stepResults,
2859
+ error: err,
2860
+ failedStep: 0
2861
+ };
2383
2862
  }
2384
2863
  let openclawTag;
2385
2864
  if (resetData.openclawTag) openclawTag = resetData.openclawTag;
@@ -2389,7 +2868,12 @@ async function runReset(input, taskId, resultFile) {
2389
2868
  const err = e.message;
2390
2869
  log(`ERROR: ${err}`);
2391
2870
  markFailed(resultFile, 0, err, startedAt);
2392
- process.exit(1);
2871
+ return {
2872
+ success: false,
2873
+ steps: stepResults,
2874
+ error: err,
2875
+ failedStep: 0
2876
+ };
2393
2877
  }
2394
2878
  log(`openclawTag=${openclawTag}`);
2395
2879
  process.on("uncaughtException", (err) => {
@@ -2402,9 +2886,19 @@ async function runReset(input, taskId, resultFile) {
2402
2886
  markFailed(resultFile, currentStep, `unhandled rejection: ${reason}`, startedAt);
2403
2887
  process.exit(1);
2404
2888
  });
2405
- /** Advance to the next step, updating the progress file and logging a boundary. */
2889
+ /** Advance to the next step, recording the previous step's success
2890
+ * duration and updating the on-disk progress file. */
2406
2891
  const step = (n) => {
2407
- if (currentStep > 0) log(`step ${currentStep} "${STEPS[currentStep - 1]}" done in ${Date.now() - stepStartedAt}ms`);
2892
+ if (currentStep > 0) {
2893
+ const dur = Date.now() - stepStartedAt;
2894
+ log(`step ${currentStep} "${STEPS[currentStep - 1]}" done in ${dur}ms`);
2895
+ stepResults.push({
2896
+ step: currentStep,
2897
+ name: STEPS[currentStep - 1],
2898
+ status: "ok",
2899
+ durationMs: dur
2900
+ });
2901
+ }
2408
2902
  currentStep = n;
2409
2903
  stepStartedAt = Date.now();
2410
2904
  log(`--- step ${n}/${TOTAL_STEPS}: ${STEPS[n - 1]} ---`);
@@ -2430,14 +2924,38 @@ async function runReset(input, taskId, resultFile) {
2430
2924
  await step8InstallExtensions(openclawTag, ossFileMap, log);
2431
2925
  step(9);
2432
2926
  writeSecretsAndRestart(vars, resetData, configDir, log);
2433
- log(`step 9 "${STEPS[8]}" done in ${Date.now() - stepStartedAt}ms`);
2927
+ const lastDur = Date.now() - stepStartedAt;
2928
+ log(`step 9 "${STEPS[8]}" done in ${lastDur}ms`);
2929
+ stepResults.push({
2930
+ step: 9,
2931
+ name: STEPS[8],
2932
+ status: "ok",
2933
+ durationMs: lastDur
2934
+ });
2434
2935
  log("=== reset completed successfully ===");
2435
2936
  markDone(resultFile, startedAt);
2937
+ return {
2938
+ success: true,
2939
+ steps: stepResults
2940
+ };
2436
2941
  } catch (e) {
2437
2942
  const err = e.message;
2438
- log(`ERROR in step ${currentStep} "${STEPS[currentStep - 1] ?? "init"}" after ${Date.now() - stepStartedAt}ms: ${err}\n${e.stack ?? ""}`);
2943
+ const dur = Date.now() - stepStartedAt;
2944
+ log(`ERROR in step ${currentStep} "${STEPS[currentStep - 1] ?? "init"}" after ${dur}ms: ${err}\n${e.stack ?? ""}`);
2945
+ stepResults.push({
2946
+ step: currentStep,
2947
+ name: STEPS[currentStep - 1] ?? "init",
2948
+ status: "fail",
2949
+ durationMs: dur,
2950
+ error: err
2951
+ });
2439
2952
  markFailed(resultFile, currentStep, err, startedAt);
2440
- process.exit(1);
2953
+ return {
2954
+ success: false,
2955
+ steps: stepResults,
2956
+ error: err,
2957
+ failedStep: currentStep
2958
+ };
2441
2959
  } finally {
2442
2960
  try {
2443
2961
  node_fs.default.rmSync(stagedDir, {
@@ -2504,38 +3022,24 @@ function resolveOssFileMap(args) {
2504
3022
  throw new Error("ossFileMap missing: provide --oss_file_map flag, ctx.install.ossFileMap, or resetData.ossFileMap");
2505
3023
  }
2506
3024
  //#endregion
2507
- //#region src/innerapi/fetchCtx.ts
3025
+ //#region src/innerapi/client.ts
2508
3026
  /**
2509
- * CLI-side client for studio_server's `openclaw.get_doctor_ctx` inner API.
2510
- *
2511
- * Mirrors the proven pattern in
2512
- * `packages/openclaw/extensions/miaoda/src/shared/innerapi-client.ts`:
3027
+ * Shared innerapi HTTP client + path constants for `openclaw.*` calls.
2513
3028
  *
2514
3029
  * - `baseURL` from env `FORCE_AUTHN_INNERAPI_DOMAIN` (injected into every
2515
3030
  * openclaw sandbox).
2516
3031
  * - `platform: { enabled, tokenProvider: { type: 'file' } }` — the platform
2517
3032
  * plugin auto-attaches the sandbox's identity JWT loaded from the
2518
3033
  * rootfs token file. Same auth that the miaoda extension already uses.
2519
- * - POST `/api/v1/studio/innerapi/integration_apis/call`
2520
- * body = { apiName: 'openclaw.get_doctor_ctx', input: {}, bizType: 'openclaw' }
2521
- * the server-side APICall dispatches by `apiName` to
2522
- * `GetDoctorCtxAPICall.Execute` whose `Name()` returns that string.
2523
- * - Response envelope: { status_code, error_msg?, data: { success, output, ... } }.
2524
- * `status_code` is a *string* ('0' = success).
2525
- * Actual DoctorCtx lives in `data.output`.
2526
- * - `x-tt-logid` header is logged on every failure path for cross-service
2527
- * traceability.
2528
- *
2529
- * On HTTP 401 (sandbox identity token expired/invalid) we `process.exit(77)`
2530
- * instead of throwing — the outer catch in `index.ts` cannot then mask auth
2531
- * failure as a generic "Error: ...". Caller (e.g. sandbox_console) sees the
2532
- * exit code and can refresh the token + retry.
3034
+ * - POST `/api/v1/studio/innerapi/integration_apis/call` with body
3035
+ * `{ apiName, bizType, input }` is the universal dispatcher; specific
3036
+ * apiName values (e.g. `openclaw.get_doctor_ctx`,
3037
+ * `openclaw.report_doctor_result`) are owned by individual call-site
3038
+ * modules.
2533
3039
  */
2534
3040
  const INNERAPI_CALL_PATH = "/api/v1/studio/innerapi/integration_apis/call";
2535
- const API_NAME = "openclaw.get_doctor_ctx";
2536
3041
  const BIZ_TYPE = "openclaw";
2537
3042
  const API_TIMEOUT_MS = 3e4;
2538
- const MAX_LOG_BODY = 500;
2539
3043
  let clientInstance = null;
2540
3044
  function getHttpClient() {
2541
3045
  if (!clientInstance) {
@@ -2552,6 +3056,35 @@ function getHttpClient() {
2552
3056
  }
2553
3057
  return clientInstance;
2554
3058
  }
3059
+ //#endregion
3060
+ //#region src/innerapi/fetchCtx.ts
3061
+ /**
3062
+ * CLI-side client for studio_server's `openclaw.get_doctor_ctx` inner API.
3063
+ *
3064
+ * Mirrors the proven pattern in
3065
+ * `packages/openclaw/extensions/miaoda/src/shared/innerapi-client.ts`:
3066
+ *
3067
+ * - `baseURL` from env `FORCE_AUTHN_INNERAPI_DOMAIN` (injected into every
3068
+ * openclaw sandbox).
3069
+ * - `platform: { enabled, tokenProvider: { type: 'file' } }` — the platform
3070
+ * plugin auto-attaches the sandbox's identity JWT loaded from the
3071
+ * rootfs token file. Same auth that the miaoda extension already uses.
3072
+ * - POST `/api/v1/studio/innerapi/integration_apis/call`
3073
+ * body = { apiName: 'openclaw.get_doctor_ctx', input: {}, bizType: 'openclaw' }
3074
+ * — the server-side APICall dispatches by `apiName` to
3075
+ * `GetDoctorCtxAPICall.Execute` whose `Name()` returns that string.
3076
+ * - Response envelope: { status_code, error_msg?, data: { success, output, ... } }.
3077
+ * `status_code` is a *string* ('0' = success).
3078
+ * Actual DoctorCtx lives in `data.output`.
3079
+ * - `x-tt-logid` header is logged on every failure path for cross-service
3080
+ * traceability.
3081
+ *
3082
+ * On HTTP 401 (sandbox identity token expired/invalid) we `process.exit(77)`
3083
+ * instead of throwing — the outer catch in `index.ts` cannot then mask auth
3084
+ * failure as a generic "Error: ...". Caller (e.g. sandbox_console) sees the
3085
+ * exit code and can refresh the token + retry.
3086
+ */
3087
+ const API_NAME$1 = "openclaw.get_doctor_ctx";
2555
3088
  /**
2556
3089
  * Fetch the sandbox's DoctorCtx by calling the innerapi's generic
2557
3090
  * `integration_apis/call` dispatcher with apiName=openclaw.get_doctor_ctx.
@@ -2559,14 +3092,19 @@ function getHttpClient() {
2559
3092
  * Throws on HTTP (non-401) / decode / business errors. On 401 calls
2560
3093
  * `process.exit(77)` directly.
2561
3094
  */
2562
- async function fetchCtxViaInnerApi() {
3095
+ async function fetchCtxViaInnerApi(opts = {}) {
2563
3096
  const client = getHttpClient();
3097
+ const input = {};
3098
+ if (opts.populate !== void 0) input.populate = opts.populate;
3099
+ if (opts.caller) input.caller = opts.caller;
3100
+ if (opts.traceId) input.traceId = opts.traceId;
2564
3101
  const body = {
2565
- apiName: API_NAME,
2566
- input: {},
3102
+ apiName: API_NAME$1,
3103
+ input,
2567
3104
  bizType: BIZ_TYPE
2568
3105
  };
2569
3106
  const start = Date.now();
3107
+ console.error(`fetchCtx: start populate=${JSON.stringify(opts.populate ?? {})}${opts.caller ? ` caller=${opts.caller}` : ""}${opts.traceId ? ` traceId=${opts.traceId}` : ""}`);
2570
3108
  const headers = { "Content-Type": "application/json" };
2571
3109
  const ttEnv = process.env.X_TT_ENV;
2572
3110
  if (ttEnv) headers["x-tt-env"] = ttEnv;
@@ -2596,7 +3134,7 @@ async function fetchCtxViaInnerApi() {
2596
3134
  }
2597
3135
  let preview = "";
2598
3136
  try {
2599
- preview = (await response.text()).slice(0, MAX_LOG_BODY);
3137
+ preview = (await response.text()).slice(0, 500);
2600
3138
  } catch {}
2601
3139
  throw new Error(`fetchCtxViaInnerApi HTTP ${response.status} ${response.statusText} (logID: ${logId}, durationMs: ${durationMs})${preview ? ` body=${preview}` : ""}`);
2602
3140
  }
@@ -2610,6 +3148,7 @@ async function fetchCtxViaInnerApi() {
2610
3148
  if (envelope.data && envelope.data.success === false) throw new Error(`fetchCtxViaInnerApi business error (logID: ${logId}, durationMs: ${durationMs}): ${envelope.error_msg ?? JSON.stringify(envelope.data)}`);
2611
3149
  const output = envelope.data?.output;
2612
3150
  if (!output || typeof output !== "object") throw new Error(`fetchCtxViaInnerApi empty/invalid output (logID: ${logId}, durationMs: ${durationMs})`);
3151
+ console.error(`fetchCtx: ok logId=${logId} durationMs=${durationMs} groups=[${Object.keys(output).join(",")}]`);
2613
3152
  return output;
2614
3153
  }
2615
3154
  //#endregion
@@ -2628,26 +3167,40 @@ async function fetchCtxViaInnerApi() {
2628
3167
  */
2629
3168
  function normalizeCtx(raw) {
2630
3169
  const r = raw ?? {};
2631
- if (r.app && typeof r.app === "object" && r.install && typeof r.install === "object" && r.secrets && typeof r.secrets === "object" && r.reset && typeof r.reset === "object") return {
2632
- app: fillApp(r.app),
2633
- install: {
2634
- openclawTag: r.install.openclawTag,
2635
- ossFileMap: r.install.ossFileMap ?? {}
2636
- },
2637
- secrets: {
2638
- secretsContent: r.secrets.secretsContent ?? "",
2639
- providerKeyContent: r.secrets.providerKeyContent ?? ""
2640
- },
2641
- reset: {
2642
- templateVars: r.reset.templateVars ?? {},
2643
- coreBackup: r.reset.coreBackup
2644
- }
2645
- };
3170
+ if (r.app && typeof r.app === "object" && r.install && typeof r.install === "object" && r.secrets && typeof r.secrets === "object" && r.reset && typeof r.reset === "object") {
3171
+ const templateVars = r.app.templateVars && typeof r.app.templateVars === "object" ? r.app.templateVars : r.reset.templateVars ?? {};
3172
+ const recommendedOpenclawTag = typeof r.app.recommendedOpenclawTag === "string" && r.app.recommendedOpenclawTag !== "" ? r.app.recommendedOpenclawTag : typeof r.install.openclawTag === "string" && r.install.openclawTag !== "" ? r.install.openclawTag : void 0;
3173
+ return {
3174
+ app: {
3175
+ ...fillApp(r.app),
3176
+ templateVars,
3177
+ recommendedOpenclawTag
3178
+ },
3179
+ install: {
3180
+ openclawTag: r.install.openclawTag,
3181
+ ossFileMap: r.install.ossFileMap ?? {}
3182
+ },
3183
+ secrets: {
3184
+ secretsContent: r.secrets.secretsContent ?? "",
3185
+ providerKeyContent: r.secrets.providerKeyContent ?? ""
3186
+ },
3187
+ reset: {
3188
+ templateVars: r.reset.templateVars ?? {},
3189
+ coreBackup: r.reset.coreBackup
3190
+ }
3191
+ };
3192
+ }
2646
3193
  const vars = r.vars ?? {};
2647
3194
  const resetData = r.resetData ?? {};
2648
3195
  const repairData = r.repairData ?? {};
3196
+ const legacyTemplateVars = vars.templateVars && typeof vars.templateVars === "object" ? vars.templateVars : resetData.templateVars ?? r.templateVars ?? {};
3197
+ const legacyRecommendedTag = typeof vars.recommendedOpenclawTag === "string" && vars.recommendedOpenclawTag !== "" ? vars.recommendedOpenclawTag : typeof resetData.openclawTag === "string" && resetData.openclawTag !== "" ? resetData.openclawTag : typeof r.openclawTag === "string" && r.openclawTag !== "" ? r.openclawTag : void 0;
2649
3198
  return {
2650
- app: fillApp(vars),
3199
+ app: {
3200
+ ...fillApp(vars),
3201
+ templateVars: legacyTemplateVars,
3202
+ recommendedOpenclawTag: legacyRecommendedTag
3203
+ },
2651
3204
  install: {
2652
3205
  openclawTag: r.install?.openclawTag ?? r.openclawTag,
2653
3206
  ossFileMap: r.install?.ossFileMap ?? resetData.ossFileMap ?? r.ossFileMap ?? {}
@@ -2700,11 +3253,15 @@ function fillApp(src) {
2700
3253
  function buildCheckInput(raw, configPathOverride) {
2701
3254
  const r = raw ?? {};
2702
3255
  if (r.configPath && r.vars) {
2703
- if (configPathOverride) return {
3256
+ const out = configPathOverride ? {
2704
3257
  ...r,
2705
3258
  configPath: configPathOverride
3259
+ } : { ...r };
3260
+ if (out.vars && !out.vars.templateVars) out.vars = {
3261
+ ...out.vars,
3262
+ templateVars: out.templateVars ?? {}
2706
3263
  };
2707
- return r;
3264
+ return out;
2708
3265
  }
2709
3266
  const ctx = normalizeCtx(raw);
2710
3267
  return {
@@ -2718,19 +3275,25 @@ function buildCheckInput(raw, configPathOverride) {
2718
3275
  baseURL: ctx.app.baseURL,
2719
3276
  expectedOrigins: ctx.app.expectedOrigins,
2720
3277
  providerFilePath: PROVIDER_FILE_PATH,
2721
- secretsFilePath: SECRETS_FILE_PATH
3278
+ secretsFilePath: SECRETS_FILE_PATH,
3279
+ templateVars: ctx.app.templateVars,
3280
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
2722
3281
  },
2723
- templateVars: ctx.reset.templateVars
3282
+ templateVars: ctx.app.templateVars
2724
3283
  };
2725
3284
  }
2726
3285
  function buildRepairInput(raw, configPathOverride) {
2727
3286
  const r = raw ?? {};
2728
3287
  if (r.configPath && r.vars) {
2729
- if (configPathOverride) return {
3288
+ const out = configPathOverride ? {
2730
3289
  ...r,
2731
3290
  configPath: configPathOverride
3291
+ } : { ...r };
3292
+ if (out.vars && !out.vars.templateVars) out.vars = {
3293
+ ...out.vars,
3294
+ templateVars: out.templateVars ?? {}
2732
3295
  };
2733
- return r;
3296
+ return out;
2734
3297
  }
2735
3298
  const ctx = normalizeCtx(raw);
2736
3299
  return {
@@ -2744,23 +3307,29 @@ function buildRepairInput(raw, configPathOverride) {
2744
3307
  baseURL: ctx.app.baseURL,
2745
3308
  expectedOrigins: ctx.app.expectedOrigins,
2746
3309
  providerFilePath: PROVIDER_FILE_PATH,
2747
- secretsFilePath: SECRETS_FILE_PATH
3310
+ secretsFilePath: SECRETS_FILE_PATH,
3311
+ templateVars: ctx.app.templateVars,
3312
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
2748
3313
  },
2749
3314
  repairData: {
2750
3315
  secretsContent: ctx.secrets.secretsContent,
2751
3316
  providerKeyContent: ctx.secrets.providerKeyContent
2752
3317
  },
2753
- templateVars: ctx.reset.templateVars
3318
+ templateVars: ctx.app.templateVars
2754
3319
  };
2755
3320
  }
2756
3321
  function buildResetInput(raw, configPathOverride) {
2757
3322
  const r = raw ?? {};
2758
3323
  if (r.configPath && r.vars && r.resetData) {
2759
- if (configPathOverride) return {
3324
+ const out = configPathOverride ? {
2760
3325
  ...r,
2761
3326
  configPath: configPathOverride
3327
+ } : { ...r };
3328
+ if (out.vars && !out.vars.templateVars) out.vars = {
3329
+ ...out.vars,
3330
+ templateVars: out.resetData?.templateVars ?? {}
2762
3331
  };
2763
- return r;
3332
+ return out;
2764
3333
  }
2765
3334
  const ctx = normalizeCtx(raw);
2766
3335
  return {
@@ -2774,7 +3343,9 @@ function buildResetInput(raw, configPathOverride) {
2774
3343
  baseURL: ctx.app.baseURL,
2775
3344
  expectedOrigins: ctx.app.expectedOrigins,
2776
3345
  providerFilePath: PROVIDER_FILE_PATH,
2777
- secretsFilePath: SECRETS_FILE_PATH
3346
+ secretsFilePath: SECRETS_FILE_PATH,
3347
+ templateVars: ctx.app.templateVars,
3348
+ recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
2778
3349
  },
2779
3350
  resetData: {
2780
3351
  templateVars: ctx.reset.templateVars,
@@ -2788,30 +3359,379 @@ function buildResetInput(raw, configPathOverride) {
2788
3359
  }
2789
3360
  //#endregion
2790
3361
  //#region src/doctor.ts
3362
+ /**
3363
+ * Per-rule orchestration with telemetry-friendly outcomes.
3364
+ *
3365
+ * Flow per rule (in topological order, after disabledRules / dependsOn /
3366
+ * skipWhen filtering):
3367
+ *
3368
+ * 1. validate — exception → status='error', abort
3369
+ * 2. pass → status='pass'
3370
+ * 3. fail and !opts.fix → status='failed' (with v1.message)
3371
+ * 4. fail and opts.fix →
3372
+ * a. only standard-mode rules attempt repair; ai/reset stay 'failed'
3373
+ * b. repair — exception → status='error', abort
3374
+ * c. re-validate — exception → status='error', abort
3375
+ * d. v2.pass → status='fixed' (before = v1.message)
3376
+ * e. v2.fail → status='still-broken' (before+after)
3377
+ *
3378
+ * Aborted runs return a partial DoctorReport with `aborted: true`. The
3379
+ * caller should `console.log(JSON.stringify(report))` first, THEN
3380
+ * `process.exit(1)` so sweep / telemetry callers always get the partial
3381
+ * data plus the non-zero signal.
3382
+ *
3383
+ * Config writeback: if opts.fix and at least one rule transitioned
3384
+ * pass→fixed, the in-memory ctx.config is JSON-serialized and written
3385
+ * back to opts.configPath. On abort, no write — keep the on-disk file
3386
+ * matching whatever was successful before the failure.
3387
+ *
3388
+ * Secrets file / provider-key / restart-command writes are NOT performed
3389
+ * by doctor. Those belong to the standalone `repair` subcommand which
3390
+ * sandbox_console push uses. Doctor is for diagnosis + per-rule fix
3391
+ * with structured outcomes.
3392
+ */
2791
3393
  async function runDoctor(rawCtx, opts) {
2792
- if (opts.fix && opts.rules.length > 0) {
2793
- const repairInput = buildRepairInput(rawCtx, opts.configPath);
2794
- repairInput.failedRules = opts.rules;
2795
- repairInput.repairData = {
2796
- ...repairInput.repairData ?? {},
2797
- restartCommand: ""
2798
- };
2799
- return { repair: runRepair(repairInput) };
3394
+ const results = [];
3395
+ console.error(`runDoctor: begin fix=${opts.fix} rules=[${opts.rules.join(",")}] configPath=${opts.configPath ?? "(default)"}`);
3396
+ const checkInput = buildCheckInput(rawCtx, opts.configPath);
3397
+ let configRaw;
3398
+ let config;
3399
+ try {
3400
+ configRaw = readFile(checkInput.configPath);
3401
+ } catch (e) {
3402
+ console.error(`runDoctor: read config failed at ${checkInput.configPath} message=${e.message}`);
3403
+ return finalize([{
3404
+ rule: "config_syntax_check",
3405
+ status: "error",
3406
+ message: "read config failed: " + e.message
3407
+ }], true);
3408
+ }
3409
+ try {
3410
+ config = loadJSON5().parse(configRaw);
3411
+ } catch (e) {
3412
+ console.error(`runDoctor: config JSON5 parse failed message=${e.message}`);
3413
+ results.push({
3414
+ rule: "config_syntax_check",
3415
+ status: opts.fix ? "still-broken" : "failed",
3416
+ message: "config JSON5 parse failed: " + e.message
3417
+ });
3418
+ return finalize(results, true);
3419
+ }
3420
+ const ctx = {
3421
+ config,
3422
+ configPath: checkInput.configPath,
3423
+ vars: checkInput.vars,
3424
+ providerDeps: analyzeProviderDeps(config)
3425
+ };
3426
+ const originalConfig = opts.showDiff && opts.fix ? deepClone(config) : null;
3427
+ const allRules = getAllRules();
3428
+ const onlyKeys = opts.rules.length > 0 ? new Set(opts.rules) : null;
3429
+ const skipRuleKeys = new Set(opts.skipRules ?? []);
3430
+ const disabledKeys = new Set([...checkInput.disabledRules ?? [], ...skipRuleKeys]);
3431
+ if (onlyKeys) {
3432
+ const registeredKeys = new Set(allRules.map((r) => r.meta.key));
3433
+ for (const k of opts.rules) if (!registeredKeys.has(k)) results.push({
3434
+ rule: k,
3435
+ status: "unknown"
3436
+ });
3437
+ }
3438
+ const failedKeys = /* @__PURE__ */ new Set();
3439
+ let configDirty = false;
3440
+ let aborted = false;
3441
+ const hasMiaoda = shouldCheckMiaodaRules(config);
3442
+ const plannedRules = allRules.filter((r) => !onlyKeys || onlyKeys.has(r.meta.key));
3443
+ console.error(`runDoctor: planned ${plannedRules.length} rules hasMiaoda=${hasMiaoda} providerDeps=${JSON.stringify(ctx.providerDeps)}`);
3444
+ outer: for (const rule of allRules) {
3445
+ const key = rule.meta.key;
3446
+ if (onlyKeys && !onlyKeys.has(key)) continue;
3447
+ if (disabledKeys.has(key)) {
3448
+ const reason = skipRuleKeys.has(key) ? "--skip-rule" : "disabledRules";
3449
+ console.error(`rule ${key}: skipped reason=${reason}`);
3450
+ results.push({
3451
+ rule: key,
3452
+ status: "skipped",
3453
+ message: reason
3454
+ });
3455
+ continue;
3456
+ }
3457
+ if (rule.meta.dependsOn?.some((d) => failedKeys.has(d))) {
3458
+ const blockers = rule.meta.dependsOn?.filter((d) => failedKeys.has(d)).join(",");
3459
+ console.error(`rule ${key}: skipped reason=dependsOn-failed blockers=${blockers}`);
3460
+ results.push({
3461
+ rule: key,
3462
+ status: "skipped",
3463
+ message: `dependsOn failed: ${blockers}`
3464
+ });
3465
+ continue;
3466
+ }
3467
+ if (rule.meta.skipWhen?.({
3468
+ hasMiaoda,
3469
+ deps: ctx.providerDeps
3470
+ })) {
3471
+ console.error(`rule ${key}: skipped reason=skipWhen`);
3472
+ results.push({
3473
+ rule: key,
3474
+ status: "skipped",
3475
+ message: "skipWhen"
3476
+ });
3477
+ continue;
3478
+ }
3479
+ const tValidate = Date.now();
3480
+ let v1;
3481
+ try {
3482
+ v1 = rule.validate(ctx);
3483
+ } catch (e) {
3484
+ const msg = e.message;
3485
+ console.error(`rule ${key}: validate -> error durationMs=${Date.now() - tValidate} message=${msg}`);
3486
+ results.push({
3487
+ rule: key,
3488
+ status: "error",
3489
+ message: "validate threw: " + msg
3490
+ });
3491
+ aborted = true;
3492
+ break outer;
3493
+ }
3494
+ if (v1.pass) {
3495
+ console.error(`rule ${key}: validate -> pass durationMs=${Date.now() - tValidate}`);
3496
+ results.push({
3497
+ rule: key,
3498
+ status: "pass"
3499
+ });
3500
+ continue;
3501
+ }
3502
+ if (!opts.fix) {
3503
+ console.error(`rule ${key}: validate -> failed durationMs=${Date.now() - tValidate} message=${v1.message ?? ""}`);
3504
+ results.push({
3505
+ rule: key,
3506
+ status: "failed",
3507
+ message: v1.message
3508
+ });
3509
+ failedKeys.add(key);
3510
+ continue;
3511
+ }
3512
+ if (rule.meta.repairMode !== "standard") {
3513
+ console.error(`rule ${key}: validate -> failed (no auto-repair, repairMode=${rule.meta.repairMode}) durationMs=${Date.now() - tValidate} message=${v1.message ?? ""}`);
3514
+ results.push({
3515
+ rule: key,
3516
+ status: "failed",
3517
+ message: v1.message
3518
+ });
3519
+ failedKeys.add(key);
3520
+ continue;
3521
+ }
3522
+ console.error(`rule ${key}: validate -> failed durationMs=${Date.now() - tValidate} message=${v1.message ?? ""}`);
3523
+ const tRepair = Date.now();
3524
+ try {
3525
+ rule.repair(ctx);
3526
+ } catch (e) {
3527
+ const msg = e.message;
3528
+ console.error(`rule ${key}: repair -> error durationMs=${Date.now() - tRepair} message=${msg}`);
3529
+ results.push({
3530
+ rule: key,
3531
+ status: "error",
3532
+ message: "repair threw: " + msg,
3533
+ before: v1.message
3534
+ });
3535
+ aborted = true;
3536
+ break outer;
3537
+ }
3538
+ console.error(`rule ${key}: repair -> ok durationMs=${Date.now() - tRepair}`);
3539
+ configDirty = true;
3540
+ const tRevalidate = Date.now();
3541
+ let v2;
3542
+ try {
3543
+ v2 = rule.validate(ctx);
3544
+ } catch (e) {
3545
+ const msg = e.message;
3546
+ console.error(`rule ${key}: revalidate -> error durationMs=${Date.now() - tRevalidate} message=${msg}`);
3547
+ results.push({
3548
+ rule: key,
3549
+ status: "error",
3550
+ message: "post-repair validate threw: " + msg,
3551
+ before: v1.message
3552
+ });
3553
+ aborted = true;
3554
+ break outer;
3555
+ }
3556
+ if (v2.pass) {
3557
+ console.error(`rule ${key}: revalidate -> pass (fixed) durationMs=${Date.now() - tRevalidate}`);
3558
+ results.push({
3559
+ rule: key,
3560
+ status: "fixed",
3561
+ before: v1.message
3562
+ });
3563
+ } else {
3564
+ console.error(`rule ${key}: revalidate -> still-broken durationMs=${Date.now() - tRevalidate} after=${v2.message ?? ""}`);
3565
+ results.push({
3566
+ rule: key,
3567
+ status: "still-broken",
3568
+ before: v1.message,
3569
+ after: v2.message
3570
+ });
3571
+ failedKeys.add(key);
3572
+ }
3573
+ }
3574
+ let backupPath = null;
3575
+ if (configDirty && !aborted) {
3576
+ backupPath = backupConfigSync(ctx.configPath);
3577
+ const serialized = JSON.stringify(ctx.config, null, 2);
3578
+ try {
3579
+ writeFile(ctx.configPath, serialized);
3580
+ console.error(`runDoctor: writeback ok path=${ctx.configPath} bytes=${serialized.length}`);
3581
+ } catch (e) {
3582
+ const msg = e.message;
3583
+ console.error(`runDoctor: writeback failed path=${ctx.configPath} message=${msg}`);
3584
+ results.push({
3585
+ rule: "*config-writeback*",
3586
+ status: "error",
3587
+ message: "config write failed: " + msg
3588
+ });
3589
+ aborted = true;
3590
+ }
3591
+ } else if (configDirty && aborted) console.error("runDoctor: writeback skipped (aborted, leaving on-disk config untouched)");
3592
+ else console.error("runDoctor: writeback skipped (no rule transitioned to fixed)");
3593
+ console.error(`runDoctor: end aborted=${aborted} results=${results.length}`);
3594
+ if (originalConfig !== null) {
3595
+ const d = (0, json_diff.diffString)(originalConfig, ctx.config, { color: false });
3596
+ console.error(`runDoctor: diff backupPath=${backupPath ?? "(none)"} changed=${!!d}`);
3597
+ if (d) {
3598
+ process.stdout.write(`original: ${backupPath ?? "(none)"}\n`);
3599
+ process.stdout.write(`after fixed: ${ctx.configPath}\n`);
3600
+ process.stdout.write("diff:\n");
3601
+ process.stdout.write(d);
3602
+ if (!d.endsWith("\n")) process.stdout.write("\n");
3603
+ } else process.stdout.write("(no openclaw.json changes)\n");
3604
+ }
3605
+ return finalize(results, aborted);
3606
+ }
3607
+ /** Deep-clone a JSON-shaped value. Uses structuredClone where available
3608
+ * (Node 17+); falls back to JSON round-trip otherwise. The config we
3609
+ * clone here is JSON5-parsed so it's strictly tree-shaped — no
3610
+ * references, no functions — making both paths equivalent. */
3611
+ function deepClone(v) {
3612
+ if (typeof structuredClone === "function") return structuredClone(v);
3613
+ return JSON.parse(JSON.stringify(v));
3614
+ }
3615
+ function finalize(results, aborted) {
3616
+ const summary = {
3617
+ pass: 0,
3618
+ failed: 0,
3619
+ fixed: 0,
3620
+ stillBroken: 0,
3621
+ skipped: 0,
3622
+ error: 0,
3623
+ unknown: 0
3624
+ };
3625
+ for (const r of results) switch (r.status) {
3626
+ case "pass":
3627
+ summary.pass++;
3628
+ break;
3629
+ case "failed":
3630
+ summary.failed++;
3631
+ break;
3632
+ case "fixed":
3633
+ summary.fixed++;
3634
+ break;
3635
+ case "still-broken":
3636
+ summary.stillBroken++;
3637
+ break;
3638
+ case "skipped":
3639
+ summary.skipped++;
3640
+ break;
3641
+ case "error":
3642
+ summary.error++;
3643
+ break;
3644
+ case "unknown":
3645
+ summary.unknown++;
3646
+ break;
2800
3647
  }
2801
- const check = runCheck(buildCheckInput(rawCtx, opts.configPath));
2802
- if (!opts.fix) return { failedRules: check.failedRules };
2803
- const repairInput = buildRepairInput(rawCtx, opts.configPath);
2804
- repairInput.failedRules = check.failedRules.standard;
2805
3648
  return {
2806
- check,
2807
- repair: runRepair(repairInput)
3649
+ results,
3650
+ summary,
3651
+ aborted
2808
3652
  };
2809
3653
  }
2810
3654
  //#endregion
3655
+ //#region src/innerapi/reportCliRun.ts
3656
+ /**
3657
+ * CLI-side client for studio_server's `openclaw.report_cli_run` inner
3658
+ * API — the unified telemetry sink for every CLI command.
3659
+ *
3660
+ * Best-effort: gated by `DoctorCtx.telemetry.reportCliRun` returned by
3661
+ * `get_doctor_ctx`. When the server hasn't rolled out the API yet the
3662
+ * flag is absent and the CLI never invokes this. When enabled, the CLI
3663
+ * fires-and-forgets one report per command after work is done.
3664
+ *
3665
+ * Failures here MUST NOT change exit code or otherwise affect the
3666
+ * command's data path — telemetry is auxiliary. Caller is expected to
3667
+ * `try/catch` and just `console.error` on rejection.
3668
+ *
3669
+ * Replaces the earlier `openclaw.report_doctor_result` API. The schema
3670
+ * generalises so check / repair / install-* / download-resource / reset
3671
+ * (worker) can all use the same envelope; per-command shape lives under
3672
+ * `result`.
3673
+ */
3674
+ const API_NAME = "openclaw.report_cli_run";
3675
+ async function reportCliRun(opts) {
3676
+ const client = getHttpClient();
3677
+ const input = {
3678
+ command: opts.command,
3679
+ runId: opts.runId,
3680
+ version: opts.version,
3681
+ invocation: opts.invocation,
3682
+ durationMs: opts.durationMs,
3683
+ success: opts.success
3684
+ };
3685
+ if (opts.caller) input.caller = opts.caller;
3686
+ if (opts.traceId) input.traceId = opts.traceId;
3687
+ if (opts.result !== void 0) input.result = opts.result;
3688
+ if (opts.error) input.error = opts.error;
3689
+ const body = {
3690
+ apiName: API_NAME,
3691
+ input,
3692
+ bizType: BIZ_TYPE
3693
+ };
3694
+ const headers = { "Content-Type": "application/json" };
3695
+ const ttEnv = process.env.X_TT_ENV;
3696
+ if (ttEnv) headers["x-tt-env"] = ttEnv;
3697
+ const start = Date.now();
3698
+ console.error(`reportCliRun: start command=${opts.command} success=${opts.success} version=${opts.version} invocation=${JSON.stringify(opts.invocation)} durationMs=${opts.durationMs}`);
3699
+ let response;
3700
+ try {
3701
+ response = await client.post(INNERAPI_CALL_PATH, body, { headers });
3702
+ } catch (e) {
3703
+ if (e instanceof _lark_apaas_http_client.HttpError && e.response) {
3704
+ const status = e.response.status;
3705
+ const logId = e.response.headers.get("x-tt-logid") ?? "";
3706
+ throw new Error(`reportCliRun HTTP ${status} ${e.response.statusText} (logID: ${logId})`);
3707
+ }
3708
+ const msg = e instanceof Error ? e.message : String(e);
3709
+ throw new Error(`reportCliRun network error: ${msg}`);
3710
+ }
3711
+ const logId = response.headers.get("x-tt-logid") ?? "";
3712
+ if (!response.ok) {
3713
+ let preview = "";
3714
+ try {
3715
+ preview = (await response.text()).slice(0, 500);
3716
+ } catch {}
3717
+ throw new Error(`reportCliRun HTTP ${response.status} ${response.statusText} (logID: ${logId})${preview ? ` body=${preview}` : ""}`);
3718
+ }
3719
+ let envelope;
3720
+ try {
3721
+ envelope = await response.json();
3722
+ } catch {
3723
+ console.error(`reportCliRun: ok logId=${logId} httpDurationMs=${Date.now() - start} (body unparseable, treated as success)`);
3724
+ return;
3725
+ }
3726
+ if (envelope.status_code !== void 0 && envelope.status_code !== "0") throw new Error(`reportCliRun API error (logID: ${logId}): code=${envelope.status_code}, message=${envelope.error_msg ?? ""}`);
3727
+ if (envelope.data && envelope.data.success === false) throw new Error(`reportCliRun business error (logID: ${logId}): ${envelope.error_msg ?? JSON.stringify(envelope.data)}`);
3728
+ console.error(`reportCliRun: ok logId=${logId} httpDurationMs=${Date.now() - start}`);
3729
+ }
3730
+ //#endregion
2811
3731
  //#region src/help.ts
2812
3732
  const BIN = "mclaw-diagnose";
2813
3733
  function versionBanner() {
2814
- return `v0.1.4-alpha.0`;
3734
+ return `v0.1.4-alpha.1`;
2815
3735
  }
2816
3736
  const COMMANDS = [
2817
3737
  {
@@ -2819,46 +3739,85 @@ const COMMANDS = [
2819
3739
  hidden: false,
2820
3740
  summary: "Diagnose openclaw config; apply repairs with --fix",
2821
3741
  help: `USAGE
2822
- ${BIN} doctor [--fix] [--rule=<key>]...
3742
+ ${BIN} doctor [--fix] [--show-diff] [--rule=<key>]... [--skip-rule=<key>]...
3743
+ [--caller=<n>] [--trace-id=<id>]
2823
3744
 
2824
3745
  DESCRIPTION
2825
- Fetches DoctorCtx via innerapi, then runs one of three modes depending
2826
- on the flags. Output is a single JSON object on stdout.
3746
+ Fetches DoctorCtx via innerapi, then orchestrates each rule:
2827
3747
 
2828
- MODES
2829
- (no flags) Check-only. Runs the rule engine against the
2830
- sandbox's current openclaw config and returns
2831
- { failedRules: { standard, ai, reset } }
2832
- No files are mutated. Use this when you just
2833
- want to know what's wrong.
3748
+ validate (always)
3749
+ pass → status='pass'
3750
+ fail and !--fix → status='failed'
3751
+ fail and --fix
3752
+ repair re-validate
3753
+ pass → status='fixed'
3754
+ still fail → status='still-broken'
2834
3755
 
2835
- --fix Check + repair-all. First runs the rule engine,
2836
- then repairs every failing standard-mode rule.
2837
- Returns
2838
- { check: {...}, repair: {...} }
2839
- Use this as the default "fix everything" action.
3756
+ Output is a single JSON object on stdout:
2840
3757
 
2841
- --fix --rule=<key>... Targeted repair. Skips the check pass entirely
2842
- and runs repair against the listed rule keys
2843
- only. Unknown keys are silently ignored.
2844
- Returns { repair: {...} } with only those
2845
- rules' outcomes. Use this when you already
2846
- know which rules need fixing.
3758
+ {
3759
+ "results": [{ "rule": "...", "status": "...", "message"?: "...", ...}],
3760
+ "summary": { "pass": N, "failed": N, "fixed": N, "stillBroken": N,
3761
+ "skipped": N, "error": N, "unknown": N },
3762
+ "aborted": false
3763
+ }
3764
+
3765
+ STATUSES
3766
+ pass Rule's validate passed; no repair attempted.
3767
+ failed Rule's validate failed; --fix was not given.
3768
+ fixed Rule's validate failed; repair ran; re-validate passed.
3769
+ still-broken Rule's validate failed; repair ran; re-validate still failed.
3770
+ skipped Rule excluded by disabledRules / --skip-rule / dependsOn /
3771
+ skipWhen.
3772
+ error validate or repair threw an exception. ABORTS the run —
3773
+ exit code becomes 1; partial 'results' still written to stdout.
3774
+ unknown --rule=<k> with k not registered. Reported, not aborting.
2847
3775
 
2848
3776
  OPTIONS
2849
- --fix Enable repair. See MODES above.
2850
- --rule=<key> Repair only this rule key. Repeatable. Only
2851
- meaningful together with --fix.
3777
+ --fix Enable repair. Without it, validate runs but failures
3778
+ are reported as 'failed' instead of being repaired.
3779
+ --show-diff With --fix: replace the JSON report on stdout with
3780
+ a plain-text section showing
3781
+ original: <backup path>
3782
+ after fixed: <openclaw.json path>
3783
+ diff:
3784
+ <unified-diff body, same format as \`json-diff\`>
3785
+ Interactive flag — sandbox_console's push path never
3786
+ sets it, so the JSON contract is preserved for that
3787
+ caller. No-op without --fix (no mutation, no diff).
3788
+ --rule=<key> Whitelist: restrict the run to this rule key (repeatable).
3789
+ Without --rule, every registered rule runs. With --rule,
3790
+ the planner narrows the innerapi vars fetch to just
3791
+ that rule's needs (see lazy-vars).
3792
+ --skip-rule=<key> Blacklist: skip the rule entirely — neither validate
3793
+ nor repair runs (repeatable). Outcome is 'skipped' with
3794
+ message='--skip-rule'. Stacks with the server's
3795
+ disabledRules. Skipped rules also subtract their vars
3796
+ from the planner's union.
3797
+ --caller=<name> Optional metadata; passed through verbatim to innerapi
3798
+ (fetchCtx + report_cli_run body.input.caller).
3799
+ E.g. "sweep_job", "user_yxy". Server uses for audit /
3800
+ correlation; CLI doesn't act on it.
3801
+ --trace-id=<id> Optional log-correlation id; passed through verbatim
3802
+ to innerapi body.input.traceId. Typically the upstream
3803
+ caller's logID (e.g. sandbox_console's request id).
2852
3804
 
2853
3805
  EXAMPLES
2854
- ${BIN} doctor # check only
2855
- ${BIN} doctor --fix # check then repair all
2856
- ${BIN} doctor --fix --rule=gateway # repair 'gateway' only
2857
- ${BIN} doctor --fix --rule=gateway --rule=jwt_token # repair multiple
3806
+ ${BIN} doctor # diagnose, no fixes
3807
+ ${BIN} doctor --fix # diagnose + fix everything
3808
+ ${BIN} doctor --fix --rule=gateway # fix only 'gateway'
3809
+ ${BIN} doctor --fix --skip-rule=feishu_channel # fix everything except feishu
3810
+ ${BIN} doctor --fix --skip-rule=feishu_channel --skip-rule=feishu_default_account
3811
+ # skip multiple rules
3812
+ ${BIN} doctor --fix --show-diff # see exactly what changed
3813
+ # (plain-text diff after
3814
+ # the JSON report)
2858
3815
 
2859
3816
  EXIT CODES
2860
- 0 success
2861
- 1 generic error
3817
+ 0 completed (results may include 'failed' / 'still-broken' — those
3818
+ are normal data outcomes, not run failures)
3819
+ 1 aborted because a rule's validate or repair threw. Partial
3820
+ results still on stdout.
2862
3821
  77 innerapi authentication failed (sandbox JWT expired/invalid)
2863
3822
  `
2864
3823
  },
@@ -3047,6 +4006,62 @@ function formatCommandHelp(name) {
3047
4006
  return cmd.help;
3048
4007
  }
3049
4008
  //#endregion
4009
+ //#region src/rule-engine/planner.ts
4010
+ /**
4011
+ * Compute the union of `Vars` field names that the **enabled** rules will
4012
+ * actually read. Sources from each rule's `@Rule({ usesVars: [...] })`
4013
+ * declaration; drift is caught at test time by `strict-vars.test.ts`.
4014
+ *
4015
+ * Inactive rules (skipped via skipWhen at runtime) are STILL counted —
4016
+ * skipWhen depends on parsed-config state we don't have at planning time.
4017
+ * Slight over-fetch in those cases, but no correctness issue.
4018
+ *
4019
+ * Result is stable-sorted for deterministic logging / test snapshots.
4020
+ */
4021
+ function planVarsFields(opts = {}) {
4022
+ const disabled = new Set(opts.disabled ?? []);
4023
+ const only = opts.onlyRules && opts.onlyRules.length > 0 ? new Set(opts.onlyRules) : null;
4024
+ const fields = /* @__PURE__ */ new Set();
4025
+ for (const rule of getAllRules()) {
4026
+ const key = rule.meta.key;
4027
+ if (disabled.has(key)) continue;
4028
+ if (only && !only.has(key)) continue;
4029
+ for (const f of rule.meta.usesVars ?? []) fields.add(f);
4030
+ }
4031
+ return [...fields].sort();
4032
+ }
4033
+ /**
4034
+ * Plan a `populate` selector for a given subcommand.
4035
+ *
4036
+ * Per-command group needs:
4037
+ *
4038
+ * doctor / check app (rule-driven)
4039
+ * repair app + secrets (writes secretsContent / providerKeyContent)
4040
+ * reset app + secrets + install + reset (the works)
4041
+ * install-* install only
4042
+ *
4043
+ * Empty result (`{}`) means "no group needed" — the CLI can skip the
4044
+ * `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
4045
+ * Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
4046
+ * `doctor`.
4047
+ */
4048
+ function planCtxPopulate(opts) {
4049
+ if (opts.command === "install") return { install: true };
4050
+ const populate = {};
4051
+ const appFields = planVarsFields({
4052
+ disabled: opts.disabled,
4053
+ onlyRules: opts.onlyRules
4054
+ });
4055
+ if (appFields.length > 0) populate.app = appFields;
4056
+ if (opts.command === "repair") populate.secrets = true;
4057
+ else if (opts.command === "reset") {
4058
+ populate.secrets = true;
4059
+ populate.install = true;
4060
+ populate.reset = true;
4061
+ }
4062
+ return populate;
4063
+ }
4064
+ //#endregion
3050
4065
  //#region src/index.ts
3051
4066
  const args = node_process.default.argv.slice(2);
3052
4067
  const mode = args.find((a) => !a.startsWith("-"));
@@ -3080,8 +4095,39 @@ function getMultiFlag(args, name) {
3080
4095
  const prefix = `--${name}=`;
3081
4096
  return args.filter((a) => a.startsWith(prefix)).map((a) => a.slice(prefix.length));
3082
4097
  }
4098
+ /**
4099
+ * Per-case telemetry helper: fires `openclaw.report_cli_run` once with
4100
+ * the command's outcome. Always called — the previous server-flag gate
4101
+ * was removed (server controls receipt by routing the apiName).
4102
+ *
4103
+ * Best-effort: a thrown reportCliRun is logged to cli.log and swallowed.
4104
+ * Never alters the data path's exit code — the user-visible work has
4105
+ * already finished by the time we report.
4106
+ *
4107
+ * `raw` is kept in the signature for backward symmetry with the doctor
4108
+ * case but is no longer consulted.
4109
+ */
4110
+ async function reportRun(command, rc, _raw, invocation, durationMs, outcome) {
4111
+ console.error(`${command}: telemetry calling report_cli_run`);
4112
+ try {
4113
+ await reportCliRun({
4114
+ command,
4115
+ runId: rc.runId,
4116
+ version: getVersion(),
4117
+ invocation,
4118
+ durationMs,
4119
+ caller: rc.caller,
4120
+ traceId: rc.traceId,
4121
+ success: outcome.success,
4122
+ result: outcome.result,
4123
+ error: outcome.error ? { message: outcome.error.message } : void 0
4124
+ });
4125
+ } catch (e) {
4126
+ console.error(`[telemetry] reportCliRun failed: ${e.message}`);
4127
+ }
4128
+ }
3083
4129
  async function main() {
3084
- installStderrMirror();
4130
+ installStderrMirror({ stderrEnabled: args.includes("-x") });
3085
4131
  const helpFlags = parseHelpFlags(args);
3086
4132
  if (mode && helpFlags.help) {
3087
4133
  const body = formatCommandHelp(mode);
@@ -3101,22 +4147,79 @@ async function main() {
3101
4147
  node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
3102
4148
  node_process.default.exit(1);
3103
4149
  }
4150
+ const caller = getFlag(args, "caller");
4151
+ const traceId = getFlag(args, "trace-id");
4152
+ const rc = setRunContext({
4153
+ caller,
4154
+ traceId
4155
+ });
4156
+ const t0 = Date.now();
4157
+ console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
3104
4158
  switch (mode) {
3105
- case "check":
4159
+ case "check": {
4160
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
4161
+ populate: planCtxPopulate({ command: "check" }),
4162
+ caller,
4163
+ traceId
4164
+ });
4165
+ const { legacy, report } = runCheckWithReport(buildCheckInput(raw));
4166
+ console.log(JSON.stringify(legacy));
4167
+ await reportRun("check", rc, raw, args.join(" "), Date.now() - t0, {
4168
+ success: !report.aborted,
4169
+ result: report,
4170
+ error: report.aborted ? /* @__PURE__ */ new Error("check run aborted") : void 0
4171
+ });
4172
+ break;
4173
+ }
3106
4174
  case "repair": {
3107
- const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi();
3108
- if (mode === "check") console.log(JSON.stringify(runCheck(buildCheckInput(raw))));
3109
- else console.log(JSON.stringify(runRepair(buildRepairInput(raw))));
4175
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
4176
+ populate: planCtxPopulate({ command: "repair" }),
4177
+ caller,
4178
+ traceId
4179
+ });
4180
+ const { legacy, report } = runRepairWithReport(buildRepairInput(raw));
4181
+ console.log(JSON.stringify(legacy));
4182
+ await reportRun("repair", rc, raw, args.join(" "), Date.now() - t0, {
4183
+ success: legacy.success,
4184
+ result: report,
4185
+ error: legacy.success ? void 0 : new Error(legacy.error ?? "repair failed")
4186
+ });
3110
4187
  break;
3111
4188
  }
3112
4189
  case "doctor": {
3113
4190
  const fix = args.includes("--fix");
3114
4191
  const rules = getMultiFlag(args, "rule");
3115
- const result = await runDoctor(await fetchCtxViaInnerApi(), {
4192
+ const skipRules = getMultiFlag(args, "skip-rule");
4193
+ const showDiff = args.includes("--show-diff");
4194
+ const populate = planCtxPopulate({
4195
+ command: "doctor",
4196
+ onlyRules: fix && rules.length > 0 ? rules : void 0,
4197
+ disabled: skipRules
4198
+ });
4199
+ console.error(`doctor: populate=${JSON.stringify(populate)} fix=${fix} rules=[${rules.join(",")}] skipRules=[${skipRules.join(",")}]`);
4200
+ let raw;
4201
+ if (Object.keys(populate).length === 0) {
4202
+ console.error("doctor: skipping fetchCtx (empty populate)");
4203
+ raw = {};
4204
+ } else raw = await fetchCtxViaInnerApi({
4205
+ populate,
4206
+ caller,
4207
+ traceId
4208
+ });
4209
+ const result = await runDoctor(raw, {
3116
4210
  fix,
3117
- rules
4211
+ rules,
4212
+ skipRules,
4213
+ showDiff
4214
+ });
4215
+ if (!(showDiff && fix)) console.log(JSON.stringify(result));
4216
+ await reportRun("doctor", rc, raw, args.join(" "), Date.now() - t0, {
4217
+ success: !result.aborted,
4218
+ result,
4219
+ error: result.aborted ? /* @__PURE__ */ new Error("doctor run aborted") : void 0
3118
4220
  });
3119
- console.log(JSON.stringify(result));
4221
+ console.error(`doctor: end aborted=${result.aborted} totalMs=${Date.now() - t0} summary=${JSON.stringify(result.summary)}`);
4222
+ if (result.aborted) node_process.default.exit(1);
3120
4223
  break;
3121
4224
  }
3122
4225
  case "reset":
@@ -3125,7 +4228,11 @@ async function main() {
3125
4228
  let ctxBase64;
3126
4229
  if (ctxArg) ctxBase64 = ctxArg.slice(6);
3127
4230
  else {
3128
- const fetched = await fetchCtxViaInnerApi();
4231
+ const fetched = await fetchCtxViaInnerApi({
4232
+ populate: planCtxPopulate({ command: "reset" }),
4233
+ caller,
4234
+ traceId
4235
+ });
3129
4236
  ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
3130
4237
  }
3131
4238
  console.log(JSON.stringify(startAsyncReset(ctxBase64)));
@@ -3136,7 +4243,22 @@ async function main() {
3136
4243
  node_process.default.exit(1);
3137
4244
  }
3138
4245
  const resultFile = resetResultFile(taskId);
3139
- await runReset(buildResetInput(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()), taskId, resultFile);
4246
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
4247
+ populate: planCtxPopulate({ command: "reset" }),
4248
+ caller,
4249
+ traceId
4250
+ });
4251
+ const outcome = await runReset(buildResetInput(raw), taskId, resultFile);
4252
+ await reportRun("reset", rc, raw, args.join(" "), Date.now() - t0, {
4253
+ success: outcome.success,
4254
+ result: {
4255
+ taskId,
4256
+ steps: outcome.steps,
4257
+ failedStep: outcome.failedStep
4258
+ },
4259
+ error: outcome.success ? void 0 : new Error(outcome.error ?? "reset failed")
4260
+ });
4261
+ if (!outcome.success) node_process.default.exit(1);
3140
4262
  } else {
3141
4263
  console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
3142
4264
  node_process.default.exit(1);
@@ -3159,12 +4281,34 @@ async function main() {
3159
4281
  }
3160
4282
  const ossFileMapFlag = getFlag(args, "oss_file_map");
3161
4283
  let installOssFileMap;
3162
- if (!ossFileMapFlag) installOssFileMap = normalizeCtx(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()).install.ossFileMap;
3163
- await installOpenclaw(tag, resolveOssFileMap({
4284
+ let rawForTelemetry;
4285
+ if (!ossFileMapFlag) {
4286
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
4287
+ populate: planCtxPopulate({ command: "install" }),
4288
+ caller,
4289
+ traceId
4290
+ });
4291
+ installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
4292
+ }
4293
+ const ossFileMap = resolveOssFileMap({
3164
4294
  ossFileMapFlag,
3165
4295
  installOssFileMap
3166
- }));
3167
- console.log(JSON.stringify({ ok: true }));
4296
+ });
4297
+ let success = true;
4298
+ let error;
4299
+ try {
4300
+ await installOpenclaw(tag, ossFileMap);
4301
+ } catch (e) {
4302
+ success = false;
4303
+ error = e;
4304
+ }
4305
+ if (success) console.log(JSON.stringify({ ok: true }));
4306
+ await reportRun("install-openclaw", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
4307
+ success,
4308
+ result: { tag },
4309
+ error
4310
+ });
4311
+ if (error) throw error;
3168
4312
  break;
3169
4313
  }
3170
4314
  case "install-extension": {
@@ -3180,18 +4324,45 @@ async function main() {
3180
4324
  const skipConfigUpdate = args.includes("--skip-config-update");
3181
4325
  const ossFileMapFlag = getFlag(args, "oss_file_map");
3182
4326
  let installOssFileMap;
3183
- if (!ossFileMapFlag) installOssFileMap = normalizeCtx(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()).install.ossFileMap;
3184
- await installExtension(tag, resolveOssFileMap({
4327
+ let rawForTelemetry;
4328
+ if (!ossFileMapFlag) {
4329
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
4330
+ populate: planCtxPopulate({ command: "install" }),
4331
+ caller,
4332
+ traceId
4333
+ });
4334
+ installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
4335
+ }
4336
+ const ossFileMap = resolveOssFileMap({
3185
4337
  ossFileMapFlag,
3186
4338
  installOssFileMap
3187
- }), {
3188
- all,
3189
- names: names.length > 0 ? names : void 0,
3190
- homeBase,
3191
- configPath,
3192
- skipConfigUpdate
3193
4339
  });
3194
- console.log(JSON.stringify({ ok: true }));
4340
+ let success = true;
4341
+ let error;
4342
+ try {
4343
+ await installExtension(tag, ossFileMap, {
4344
+ all,
4345
+ names: names.length > 0 ? names : void 0,
4346
+ homeBase,
4347
+ configPath,
4348
+ skipConfigUpdate
4349
+ });
4350
+ } catch (e) {
4351
+ success = false;
4352
+ error = e;
4353
+ }
4354
+ if (success) console.log(JSON.stringify({ ok: true }));
4355
+ await reportRun("install-extension", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
4356
+ success,
4357
+ result: {
4358
+ tag,
4359
+ all,
4360
+ names,
4361
+ skipConfigUpdate
4362
+ },
4363
+ error
4364
+ });
4365
+ if (error) throw error;
3195
4366
  break;
3196
4367
  }
3197
4368
  case "download-resource": {
@@ -3209,16 +4380,43 @@ async function main() {
3209
4380
  }
3210
4381
  const ossFileMapFlag = getFlag(args, "oss_file_map");
3211
4382
  let installOssFileMap;
3212
- if (!ossFileMapFlag) installOssFileMap = normalizeCtx(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()).install.ossFileMap;
3213
- await downloadResource(tag, resolveOssFileMap({
4383
+ let rawForTelemetry;
4384
+ if (!ossFileMapFlag) {
4385
+ rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
4386
+ populate: planCtxPopulate({ command: "install" }),
4387
+ caller,
4388
+ traceId
4389
+ });
4390
+ installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
4391
+ }
4392
+ const ossFileMap = resolveOssFileMap({
3214
4393
  ossFileMapFlag,
3215
4394
  installOssFileMap
3216
- }), {
3217
- role,
3218
- name,
3219
- dir
3220
4395
  });
3221
- console.log(JSON.stringify({ ok: true }));
4396
+ let success = true;
4397
+ let error;
4398
+ try {
4399
+ await downloadResource(tag, ossFileMap, {
4400
+ role,
4401
+ name,
4402
+ dir
4403
+ });
4404
+ } catch (e) {
4405
+ success = false;
4406
+ error = e;
4407
+ }
4408
+ if (success) console.log(JSON.stringify({ ok: true }));
4409
+ await reportRun("download-resource", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
4410
+ success,
4411
+ result: {
4412
+ tag,
4413
+ role,
4414
+ name,
4415
+ dir
4416
+ },
4417
+ error
4418
+ });
4419
+ if (error) throw error;
3222
4420
  break;
3223
4421
  }
3224
4422
  default:
@@ -3226,10 +4424,12 @@ async function main() {
3226
4424
  node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
3227
4425
  node_process.default.exit(1);
3228
4426
  }
4427
+ console.error(`${mode}: end totalMs=${Date.now() - t0}`);
3229
4428
  }
3230
4429
  main().catch((err) => {
3231
4430
  const msg = err instanceof Error ? err.message : String(err);
3232
- console.error(`Error: ${msg}`);
4431
+ console.error(`${mode ?? "<no-mode>"}: error message=${msg}`);
4432
+ node_process.default.stderr.write(`Error: ${msg}\n`);
3233
4433
  node_process.default.exit(1);
3234
4434
  });
3235
4435
  //#endregion