@lark-apaas/openclaw-scripts-diagnose-cli 0.1.1-alpha.22 → 0.1.1-alpha.24

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 (3) hide show
  1. package/README.md +7 -1
  2. package/dist/index.cjs +1044 -235
  3. package/package.json +3 -1
package/dist/index.cjs CHANGED
@@ -31,6 +31,12 @@ let node_path = require("node:path");
31
31
  node_path = __toESM(node_path);
32
32
  let node_child_process = require("node:child_process");
33
33
  let node_crypto = require("node:crypto");
34
+ node_crypto = __toESM(node_crypto);
35
+ let node_stream = require("node:stream");
36
+ let node_stream_promises = require("node:stream/promises");
37
+ let node_assert = require("node:assert");
38
+ node_assert = __toESM(node_assert);
39
+ let _lark_apaas_http_client = require("@lark-apaas/http-client");
34
40
  //#region src/rule-engine/base.ts
35
41
  /** Abstract base class for all diagnose rules */
36
42
  var DiagnoseRule = class {
@@ -127,6 +133,14 @@ function isValidJWT(token) {
127
133
  return false;
128
134
  }
129
135
  }
136
+ /**
137
+ * Return `val` as a plain-object record (non-null, non-array object), or
138
+ * `undefined` otherwise. Cheaper than `getNestedMap` when the value is already
139
+ * at hand.
140
+ */
141
+ function asRecord(val) {
142
+ return val != null && typeof val === "object" && !Array.isArray(val) ? val : void 0;
143
+ }
130
144
  /** Set a deeply nested value, creating intermediate objects as needed. */
131
145
  function setNestedValue(obj, keys, value) {
132
146
  let current = obj;
@@ -268,7 +282,7 @@ function findHighestBackup(backupFiles) {
268
282
  }
269
283
  let ConfigFileBackupRule = class ConfigFileBackupRule extends DiagnoseRule {
270
284
  validate(ctx) {
271
- const configPath = ctx.config.__configPath;
285
+ const { configPath } = ctx;
272
286
  if (!configPath) return {
273
287
  pass: false,
274
288
  message: "configPath not provided"
@@ -281,7 +295,7 @@ let ConfigFileBackupRule = class ConfigFileBackupRule extends DiagnoseRule {
281
295
  return { pass: true };
282
296
  }
283
297
  repair(ctx) {
284
- const configPath = ctx.config.__configPath;
298
+ const { configPath } = ctx;
285
299
  if (!configPath) return;
286
300
  const best = findHighestBackup(findBackupFiles(configPath));
287
301
  if (!best) return;
@@ -310,7 +324,7 @@ function hasBackupFiles(configPath) {
310
324
  }
311
325
  let ConfigFileMissingRule = class ConfigFileMissingRule extends DiagnoseRule {
312
326
  validate(ctx) {
313
- const configPath = ctx.config.__configPath;
327
+ const { configPath } = ctx;
314
328
  if (!configPath) return {
315
329
  pass: false,
316
330
  message: "configPath not provided"
@@ -332,7 +346,7 @@ ConfigFileMissingRule = __decorate([Rule({
332
346
  //#region src/rules/config-syntax.ts
333
347
  let ConfigSyntaxRule = class ConfigSyntaxRule extends DiagnoseRule {
334
348
  validate(ctx) {
335
- const configPath = ctx.config.__configPath;
349
+ const { configPath } = ctx;
336
350
  if (!fileExists(configPath)) return { pass: true };
337
351
  try {
338
352
  loadJSON5().parse(readFile(configPath));
@@ -600,27 +614,6 @@ FeishuChannelRule = _FeishuChannelRule = __decorate([Rule({
600
614
  repairMode: "standard"
601
615
  })], FeishuChannelRule);
602
616
  //#endregion
603
- //#region src/rules/feishu-default-account.ts
604
- let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRule {
605
- validate(ctx) {
606
- const feishu = getNestedMap(ctx.config, "channels", "feishu");
607
- if (feishu && "defaultAccount" in feishu) return {
608
- pass: false,
609
- message: "channels.feishu.defaultAccount should be removed"
610
- };
611
- return { pass: true };
612
- }
613
- repair(ctx) {
614
- const feishu = getNestedMap(ctx.config, "channels", "feishu");
615
- if (feishu && "defaultAccount" in feishu) delete feishu.defaultAccount;
616
- }
617
- };
618
- FeishuDefaultAccountRule = __decorate([Rule({
619
- key: "feishu_default_account",
620
- dependsOn: ["config_syntax_check"],
621
- repairMode: "standard"
622
- })], FeishuDefaultAccountRule);
623
- //#endregion
624
617
  //#region src/rules/gateway.ts
625
618
  var _GatewayRule;
626
619
  let GatewayRule = class GatewayRule extends DiagnoseRule {
@@ -884,6 +877,217 @@ SecretsRule = __decorate([Rule({
884
877
  skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaSecretProvider
885
878
  })], SecretsRule);
886
879
  //#endregion
880
+ //#region src/rules/cleanup-install-backup-dirs.ts
881
+ const DIR_PREFIX = ".openclaw-install-";
882
+ function resolveExtensionsDir(configPath) {
883
+ return node_path.default.join(node_path.default.dirname(configPath), "extensions");
884
+ }
885
+ function findLeftoverDirs(extensionsDir) {
886
+ if (!fileExists(extensionsDir)) return [];
887
+ let entries;
888
+ try {
889
+ entries = node_fs.default.readdirSync(extensionsDir, { withFileTypes: true });
890
+ } catch {
891
+ return [];
892
+ }
893
+ return entries.filter((e) => e.isDirectory() && e.name.startsWith(DIR_PREFIX)).map((e) => node_path.default.join(extensionsDir, e.name));
894
+ }
895
+ let CleanupInstallBackupDirsRule = class CleanupInstallBackupDirsRule extends DiagnoseRule {
896
+ validate(ctx) {
897
+ const configPath = ctx.config.__configPath;
898
+ if (!configPath) return { pass: true };
899
+ const dirs = findLeftoverDirs(resolveExtensionsDir(configPath));
900
+ if (dirs.length === 0) return { pass: true };
901
+ return {
902
+ pass: false,
903
+ message: `extensions 目录下发现 ${dirs.length} 个 ${DIR_PREFIX}* 脏目录需要清理`
904
+ };
905
+ }
906
+ repair(ctx) {
907
+ const configPath = ctx.config.__configPath;
908
+ if (!configPath) return;
909
+ const dirs = findLeftoverDirs(resolveExtensionsDir(configPath));
910
+ const failures = [];
911
+ for (const dir of dirs) try {
912
+ node_fs.default.rmSync(dir, {
913
+ recursive: true,
914
+ force: true
915
+ });
916
+ } catch (e) {
917
+ failures.push(`${node_path.default.basename(dir)}: ${e.message}`);
918
+ }
919
+ if (dirs.length > 0 && failures.length === dirs.length) throw new Error(`cleanup_install_backup_dirs: 全部清理失败: ${failures.join("; ")}`);
920
+ }
921
+ };
922
+ CleanupInstallBackupDirsRule = __decorate([Rule({
923
+ key: "cleanup_install_backup_dirs",
924
+ repairMode: "standard"
925
+ })], CleanupInstallBackupDirsRule);
926
+ //#endregion
927
+ //#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
928
+ /**
929
+ * Official miaoda-side plugins that must track manifest — version-locked specs
930
+ * here block upgrades. Third-party / user-installed plugins are intentionally
931
+ * out of scope (users may pin them deliberately).
932
+ */
933
+ const OFFICIAL_PLUGIN_NAMES = new Set([
934
+ "openclaw-extension-miaoda",
935
+ "openclaw-extension-miaoda-coding",
936
+ "openclaw-guardian-plugin",
937
+ "openclaw-mem0-plugin",
938
+ "openclaw-lark"
939
+ ]);
940
+ const LOCKED_NPM_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*@[^@/:#\s]+$/i;
941
+ function isLockedNpmSpec(spec) {
942
+ return typeof spec === "string" && LOCKED_NPM_SPEC.test(spec);
943
+ }
944
+ function unlockSpec(spec) {
945
+ const slash = spec.indexOf("/");
946
+ const cut = slash === -1 ? spec.indexOf("@") : spec.indexOf("@", slash + 1);
947
+ return spec.slice(0, cut);
948
+ }
949
+ /** Yield `[key, lockedSpec]` for every official-plugin install whose `spec` is locked. */
950
+ function* iterLockedOfficialInstalls(config) {
951
+ const installs = getNestedMap(config, "plugins", "installs");
952
+ if (!installs) return;
953
+ for (const [key, entry] of Object.entries(installs)) {
954
+ if (!OFFICIAL_PLUGIN_NAMES.has(key)) continue;
955
+ const spec = asRecord(entry)?.spec;
956
+ if (isLockedNpmSpec(spec)) yield [key, spec];
957
+ }
958
+ }
959
+ let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
960
+ validate(ctx) {
961
+ const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
962
+ if (locked.length === 0) return { pass: true };
963
+ return {
964
+ pass: false,
965
+ message: "plugins.installs 中官方插件存在锁版本的 spec: " + locked.sort().join(",")
966
+ };
967
+ }
968
+ repair(ctx) {
969
+ for (const [key, spec] of iterLockedOfficialInstalls(ctx.config)) setNestedValue(ctx.config, [
970
+ "plugins",
971
+ "installs",
972
+ key,
973
+ "spec"
974
+ ], unlockSpec(spec));
975
+ }
976
+ };
977
+ MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
978
+ key: "miaoda_official_plugins_install_spec_unlock",
979
+ dependsOn: ["config_syntax_check"],
980
+ repairMode: "standard"
981
+ })], MiaodaOfficialPluginsInstallSpecUnlockRule);
982
+ //#endregion
983
+ //#region src/rules/old-miaoda-plugins-cleanup.ts
984
+ const NEW_MIAODA = "openclaw-extension-miaoda";
985
+ const OLD_PLUGIN_NAMES = Object.freeze([
986
+ "openclaw-feishu-greeting",
987
+ "openclaw-miaoda-keepalive",
988
+ "feishu-greeting",
989
+ "miaoda-keepalive"
990
+ ]);
991
+ function getPluginMaps(config) {
992
+ const rawAllow = asRecord(config.plugins)?.allow;
993
+ return {
994
+ entries: getNestedMap(config, "plugins", "entries"),
995
+ installs: getNestedMap(config, "plugins", "installs"),
996
+ allow: Array.isArray(rawAllow) ? rawAllow : void 0
997
+ };
998
+ }
999
+ function getExtensionsDir(configPath) {
1000
+ return node_path.default.join(node_path.default.dirname(configPath), "extensions");
1001
+ }
1002
+ function hasNewMiaoda({ entries, installs, allow }) {
1003
+ return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null || (allow?.includes(NEW_MIAODA) ?? false);
1004
+ }
1005
+ function findResiduals({ entries, installs, allow }, extensionsDir) {
1006
+ return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || (allow?.includes(name) ?? false) || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
1007
+ }
1008
+ let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
1009
+ validate(ctx) {
1010
+ const maps = getPluginMaps(ctx.config);
1011
+ if (!hasNewMiaoda(maps)) return { pass: true };
1012
+ const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
1013
+ if (residuals.length === 0) return { pass: true };
1014
+ return {
1015
+ pass: false,
1016
+ message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
1017
+ };
1018
+ }
1019
+ repair(ctx) {
1020
+ const maps = getPluginMaps(ctx.config);
1021
+ if (!hasNewMiaoda(maps)) return;
1022
+ const extensionsDir = getExtensionsDir(ctx.configPath);
1023
+ const { entries, installs, allow } = maps;
1024
+ const oldSet = new Set(OLD_PLUGIN_NAMES);
1025
+ if (allow) for (let i = allow.length - 1; i >= 0; i--) {
1026
+ const v = allow[i];
1027
+ if (typeof v === "string" && oldSet.has(v)) allow.splice(i, 1);
1028
+ }
1029
+ for (const name of OLD_PLUGIN_NAMES) {
1030
+ if (entries && name in entries) delete entries[name];
1031
+ if (installs && name in installs) delete installs[name];
1032
+ const target = node_path.default.join(extensionsDir, name);
1033
+ const rel = node_path.default.relative(extensionsDir, target);
1034
+ if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
1035
+ try {
1036
+ node_fs.default.rmSync(target, {
1037
+ recursive: true,
1038
+ force: true
1039
+ });
1040
+ } catch (e) {
1041
+ console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
1042
+ }
1043
+ }
1044
+ }
1045
+ };
1046
+ OldMiaodaPluginsCleanupRule = __decorate([Rule({
1047
+ key: "old_miaoda_plugins_cleanup",
1048
+ dependsOn: ["config_syntax_check"],
1049
+ repairMode: "standard"
1050
+ })], OldMiaodaPluginsCleanupRule);
1051
+ //#endregion
1052
+ //#region src/rules/lark-plugin-allow.ts
1053
+ const LARK_PLUGIN = "openclaw-lark";
1054
+ const LARK_PLUGIN_NAMES = [LARK_PLUGIN, "feishu-openclaw-plugin"];
1055
+ let LarkPluginAllowRule = class LarkPluginAllowRule extends DiagnoseRule {
1056
+ validate(ctx) {
1057
+ const allow = getAllow(ctx.config);
1058
+ if (LARK_PLUGIN_NAMES.some((name) => allow.includes(name))) return { pass: true };
1059
+ return {
1060
+ pass: false,
1061
+ message: `plugins.allow 缺少飞书插件 (expected one of: ${LARK_PLUGIN_NAMES.join(", ")})`
1062
+ };
1063
+ }
1064
+ repair(ctx) {
1065
+ if (ctx.config.plugins == null || typeof ctx.config.plugins !== "object" || Array.isArray(ctx.config.plugins)) {
1066
+ ctx.config.plugins = { allow: [LARK_PLUGIN] };
1067
+ return;
1068
+ }
1069
+ const pluginsMap = ctx.config.plugins;
1070
+ const rawAllow = pluginsMap.allow;
1071
+ const original = Array.isArray(rawAllow) ? rawAllow : [];
1072
+ const stringAllow = original.filter((e) => typeof e === "string");
1073
+ if (LARK_PLUGIN_NAMES.some((name) => stringAllow.includes(name))) return;
1074
+ original.push(LARK_PLUGIN);
1075
+ pluginsMap.allow = original;
1076
+ }
1077
+ };
1078
+ LarkPluginAllowRule = __decorate([Rule({
1079
+ key: "lark_plugin_allow",
1080
+ dependsOn: ["config_syntax_check"],
1081
+ repairMode: "standard"
1082
+ })], LarkPluginAllowRule);
1083
+ function getAllow(config) {
1084
+ const plugins = config.plugins;
1085
+ if (plugins == null || typeof plugins !== "object" || Array.isArray(plugins)) return [];
1086
+ const allow = plugins.allow;
1087
+ if (!Array.isArray(allow)) return [];
1088
+ return allow.filter((e) => typeof e === "string");
1089
+ }
1090
+ //#endregion
887
1091
  //#region src/check.ts
888
1092
  function runCheck(input) {
889
1093
  const result = { failedRules: {
@@ -896,12 +1100,14 @@ function runCheck(input) {
896
1100
  const failedKeys = /* @__PURE__ */ new Set();
897
1101
  let configParsed = false;
898
1102
  let ctx = {
899
- config: { __configPath: input.configPath },
1103
+ config: {},
1104
+ configPath: input.configPath,
900
1105
  vars: input.vars,
901
1106
  providerDeps: {
902
1107
  usesMiaodaProvider: false,
903
1108
  usesMiaodaSecretProvider: false
904
- }
1109
+ },
1110
+ templateVars: input.templateVars
905
1111
  };
906
1112
  for (const rule of rules) {
907
1113
  const meta = rule.meta;
@@ -912,8 +1118,10 @@ function runCheck(input) {
912
1118
  const deps = analyzeProviderDeps(parsed);
913
1119
  ctx = {
914
1120
  config: parsed,
1121
+ configPath: input.configPath,
915
1122
  vars: input.vars,
916
- providerDeps: deps
1123
+ providerDeps: deps,
1124
+ templateVars: input.templateVars
917
1125
  };
918
1126
  configParsed = true;
919
1127
  } catch {
@@ -965,12 +1173,14 @@ function runRepair(input) {
965
1173
  if (rule.meta.repairMode !== "standard") continue;
966
1174
  if (rule.meta.dependsOn?.includes("config_syntax_check")) continue;
967
1175
  rule.repair({
968
- config: { __configPath: input.configPath },
1176
+ config: {},
1177
+ configPath: input.configPath,
969
1178
  vars: input.vars,
970
1179
  providerDeps: {
971
1180
  usesMiaodaProvider: false,
972
1181
  usesMiaodaSecretProvider: false
973
- }
1182
+ },
1183
+ templateVars: input.templateVars
974
1184
  });
975
1185
  }
976
1186
  const JSON5 = loadJSON5();
@@ -986,8 +1196,10 @@ function runRepair(input) {
986
1196
  const deps = analyzeProviderDeps(config);
987
1197
  const ctx = {
988
1198
  config,
1199
+ configPath: input.configPath,
989
1200
  vars: input.vars,
990
- providerDeps: deps
1201
+ providerDeps: deps,
1202
+ templateVars: input.templateVars
991
1203
  };
992
1204
  let configDirty = false;
993
1205
  for (const rule of rules) {
@@ -1032,6 +1244,14 @@ function resetResultFile(taskId) {
1032
1244
  function resetLogFile(taskId) {
1033
1245
  return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
1034
1246
  }
1247
+ /** Sandbox workspace root where openclaw config + agent state lives. */
1248
+ const WORKSPACE_DIR = "/home/gem/workspace/agent";
1249
+ /** File containing the provider key used by the openclaw miaoda provider. */
1250
+ const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
1251
+ /** File containing the miaoda openclaw secrets JSON. */
1252
+ const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
1253
+ /** Absolute path to the openclaw config JSON. */
1254
+ const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
1035
1255
  //#endregion
1036
1256
  //#region src/logger.ts
1037
1257
  function makeLogger(logFile) {
@@ -1100,6 +1320,234 @@ function startAsyncReset(ctxBase64) {
1100
1320
  return { taskId };
1101
1321
  }
1102
1322
  //#endregion
1323
+ //#region src/oss/fetchManifest.ts
1324
+ async function fetchManifest(ossFileMap, tag) {
1325
+ const key = `builtin/manifests/openclaw/recommended/${tag}.json`;
1326
+ const url = ossFileMap[key];
1327
+ if (!url) throw new Error(`manifest signed URL missing for ${key}`);
1328
+ const res = await fetch(url);
1329
+ if (!res.ok) throw new Error(`fetch manifest failed: HTTP ${res.status} ${res.statusText}`);
1330
+ return await res.json();
1331
+ }
1332
+ async function downloadWithCache(pkg, ossFileMap, opts = {}) {
1333
+ const cacheRoot = opts.cacheRoot ?? "/tmp/openclaw-diagnose/resources";
1334
+ const shortHash = pkg.shasum.slice(0, 16);
1335
+ const destDir = node_path.default.join(cacheRoot, shortHash);
1336
+ const destFile = node_path.default.join(destDir, node_path.default.posix.basename(pkg.ossKey));
1337
+ node_fs.default.mkdirSync(destDir, { recursive: true });
1338
+ if (node_fs.default.existsSync(destFile)) return destFile;
1339
+ const url = ossFileMap[pkg.ossKey];
1340
+ if (!url) throw new Error(`signed URL missing for ${pkg.ossKey}`);
1341
+ if (!pkg.integrity.startsWith("sha512-")) throw new Error(`unsupported integrity format: ${pkg.integrity}`);
1342
+ const expected = pkg.integrity.slice(7);
1343
+ const tmpFile = node_path.default.join(destDir, `.tmp.${process.pid}.${node_crypto.default.randomBytes(4).toString("hex")}`);
1344
+ try {
1345
+ const res = await fetch(url);
1346
+ if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
1347
+ if (!res.body) throw new Error(`download failed: empty body for ${pkg.ossKey}`);
1348
+ const hasher = node_crypto.default.createHash("sha512");
1349
+ const source = node_stream.Readable.fromWeb(res.body);
1350
+ async function* teeAndHash(src) {
1351
+ for await (const chunk of src) {
1352
+ hasher.update(chunk);
1353
+ yield chunk;
1354
+ }
1355
+ }
1356
+ await (0, node_stream_promises.pipeline)(source, teeAndHash, node_fs.default.createWriteStream(tmpFile));
1357
+ const actual = hasher.digest("base64");
1358
+ if (actual !== expected) {
1359
+ const envBypass = process.env.OPENCLAW_DEBUG_SKIP_INTEGRITY === "1";
1360
+ if (opts.skipIntegrity || envBypass) {
1361
+ const sourceLabel = opts.skipIntegrity ? "skipIntegrity=true" : "OPENCLAW_DEBUG_SKIP_INTEGRITY=1";
1362
+ console.error(`⚠ [downloadWithCache] INTEGRITY BYPASS for ${pkg.ossKey}: expected ${expected.slice(0, 12)}… got ${actual.slice(0, 12)}… — ${sourceLabel}. DO NOT use this flag in production.`);
1363
+ } else throw new Error(`integrity mismatch for ${pkg.ossKey}: expected ${expected} got ${actual}`);
1364
+ }
1365
+ node_fs.default.renameSync(tmpFile, destFile);
1366
+ return destFile;
1367
+ } catch (e) {
1368
+ try {
1369
+ node_fs.default.unlinkSync(tmpFile);
1370
+ } catch {}
1371
+ throw e;
1372
+ }
1373
+ }
1374
+ async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
1375
+ const homeBase = opts.homeBase ?? "/home/gem";
1376
+ const t0 = Date.now();
1377
+ const pkg = (await fetchManifest(ossFileMap, openclawTag)).packages.find((p) => p.role === "cli" && p.name === "openclaw");
1378
+ if (!pkg) throw new Error("install-openclaw: role=cli,name=openclaw not found in manifest");
1379
+ const targetDir = opts.targetDir ?? node_path.default.join(homeBase, pkg.installPath);
1380
+ const bakDir = targetDir + ".bak";
1381
+ const tarball = await downloadWithCache(pkg, ossFileMap, opts);
1382
+ console.error(`[install-openclaw] tag=${openclawTag} shasum=${pkg.shasum.slice(0, 12)}...`);
1383
+ if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
1384
+ recursive: true,
1385
+ force: true
1386
+ });
1387
+ const hadExisting = node_fs.default.existsSync(targetDir);
1388
+ if (hadExisting) node_fs.default.renameSync(targetDir, bakDir);
1389
+ try {
1390
+ node_fs.default.mkdirSync(targetDir, { recursive: true });
1391
+ (0, node_child_process.execSync)(`tar -xzf '${tarball}' -C '${targetDir}' --strip-components=1`, { stdio: "ignore" });
1392
+ if (!node_fs.default.existsSync(node_path.default.join(targetDir, "package.json"))) throw new Error("extracted tarball missing package.json");
1393
+ } catch (e) {
1394
+ try {
1395
+ node_fs.default.rmSync(targetDir, {
1396
+ recursive: true,
1397
+ force: true
1398
+ });
1399
+ } catch {}
1400
+ if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.renameSync(bakDir, targetDir);
1401
+ throw e;
1402
+ }
1403
+ if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
1404
+ recursive: true,
1405
+ force: true
1406
+ });
1407
+ console.error(`[install-openclaw] done in ${Date.now() - t0}ms`);
1408
+ }
1409
+ async function installExtension(tag, ossFileMap, opts = {}) {
1410
+ const homeBase = opts.homeBase ?? "/home/gem";
1411
+ const hasAll = !!opts.all;
1412
+ const hasNames = (opts.names?.length ?? 0) > 0;
1413
+ if (hasAll && hasNames) throw new Error("install-extension: --all and --extension are mutually exclusive");
1414
+ if (!hasAll && !hasNames) throw new Error("install-extension: must provide --all or --extension=<name>");
1415
+ const allExts = (await fetchManifest(ossFileMap, tag)).packages.filter((p) => p.role === "extension");
1416
+ let targets;
1417
+ if (hasAll) targets = allExts;
1418
+ else {
1419
+ const wanted = new Set(opts.names);
1420
+ targets = allExts.filter((p) => wanted.has(p.name) || p.packageName != null && wanted.has(p.packageName));
1421
+ const foundKeys = /* @__PURE__ */ new Set();
1422
+ for (const t of targets) {
1423
+ foundKeys.add(t.name);
1424
+ if (t.packageName) foundKeys.add(t.packageName);
1425
+ }
1426
+ const missing = opts.names.filter((n) => !foundKeys.has(n));
1427
+ if (missing.length > 0) throw new Error(`install-extension: not found in manifest: ${missing.join(", ")}`);
1428
+ }
1429
+ console.error(`[install-extension] tag=${tag} targets=${targets.length}`);
1430
+ const t0 = Date.now();
1431
+ const tarballs = await Promise.all(targets.map(async (p) => {
1432
+ const tb = await downloadWithCache(p, ossFileMap, opts);
1433
+ console.error(`[install-extension] ${p.name}: downloaded`);
1434
+ return {
1435
+ pkg: p,
1436
+ tarball: tb
1437
+ };
1438
+ }));
1439
+ for (const { pkg, tarball } of tarballs) {
1440
+ installOne(pkg, tarball, homeBase);
1441
+ console.error(`[install-extension] ${pkg.name}: installed`);
1442
+ }
1443
+ if (!opts.skipConfigUpdate) updatePluginInstalls(opts.configPath ?? node_path.default.join(homeBase, "workspace/agent/openclaw.json"), targets);
1444
+ else console.error(`[install-extension] skipConfigUpdate=true — not touching openclaw.json`);
1445
+ console.error(`[install-extension] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
1446
+ }
1447
+ /**
1448
+ * Merge each installed extension's installMetadata into openclaw.json's
1449
+ * plugins.installs[<pkg.name>]. Atomic write via tmp + rename.
1450
+ *
1451
+ * - No openclaw.json → log + return (not an error; some install contexts don't have it yet)
1452
+ * - Extension without installMetadata in manifest → skip that entry (log)
1453
+ * - Existing plugins.installs entries for other extensions left untouched
1454
+ */
1455
+ function updatePluginInstalls(configPath, installedPkgs) {
1456
+ if (!node_fs.default.existsSync(configPath)) {
1457
+ console.error(`[install-extension] no config at ${configPath} — skip plugins.installs update`);
1458
+ return;
1459
+ }
1460
+ const JSON5 = loadJSON5();
1461
+ const raw = node_fs.default.readFileSync(configPath, "utf-8");
1462
+ const config = JSON5.parse(raw);
1463
+ if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
1464
+ const plugins = config.plugins;
1465
+ if (!plugins.installs || typeof plugins.installs !== "object") plugins.installs = {};
1466
+ const installs = plugins.installs;
1467
+ let updated = 0;
1468
+ let skipped = 0;
1469
+ for (const pkg of installedPkgs) if (pkg.installMetadata) {
1470
+ installs[pkg.name] = pkg.installMetadata;
1471
+ updated++;
1472
+ } else skipped++;
1473
+ const tmpPath = configPath + ".installs-tmp";
1474
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
1475
+ node_fs.default.renameSync(tmpPath, configPath);
1476
+ console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
1477
+ }
1478
+ function installOne(pkg, tarball, homeBase) {
1479
+ const destDir = node_path.default.join(homeBase, pkg.installPath);
1480
+ const stagingDir = destDir + ".new";
1481
+ const oldDir = destDir + ".old";
1482
+ node_fs.default.mkdirSync(node_path.default.dirname(destDir), { recursive: true });
1483
+ if (node_fs.default.existsSync(stagingDir)) node_fs.default.rmSync(stagingDir, {
1484
+ recursive: true,
1485
+ force: true
1486
+ });
1487
+ node_fs.default.mkdirSync(stagingDir);
1488
+ try {
1489
+ (0, node_child_process.execSync)(`tar -xzf '${tarball}' -C '${stagingDir}' --strip-components=1`, { stdio: "ignore" });
1490
+ if (!node_fs.default.existsSync(node_path.default.join(stagingDir, "package.json"))) throw new Error(`extension tarball missing package.json: ${pkg.name}`);
1491
+ } catch (e) {
1492
+ try {
1493
+ node_fs.default.rmSync(stagingDir, {
1494
+ recursive: true,
1495
+ force: true
1496
+ });
1497
+ } catch {}
1498
+ throw e;
1499
+ }
1500
+ const hadOld = node_fs.default.existsSync(destDir);
1501
+ if (hadOld) node_fs.default.renameSync(destDir, oldDir);
1502
+ node_fs.default.renameSync(stagingDir, destDir);
1503
+ if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
1504
+ recursive: true,
1505
+ force: true
1506
+ });
1507
+ }
1508
+ /**
1509
+ * Download + extract a config/template package to its install destination.
1510
+ *
1511
+ * Current manifest has all resources as format=tgz with content at the root
1512
+ * (config: openclaw.json file at root; template: scripts/ dir at root), so we
1513
+ * always `tar -xzf` without --strip-components into `dirname(fullInstallPath)`.
1514
+ * The final artefact ends up at exactly `homeBase + pkg.installPath`.
1515
+ */
1516
+ async function downloadResource(tag, ossFileMap, opts) {
1517
+ const homeBase = opts.homeBase ?? "/home/gem";
1518
+ const pkg = (await fetchManifest(ossFileMap, tag)).packages.find((p) => p.role === opts.role && p.name === opts.name);
1519
+ if (!pkg) throw new Error(`download-resource: not found in manifest: role=${opts.role} name=${opts.name}`);
1520
+ const file = await downloadWithCache(pkg, ossFileMap, opts);
1521
+ const fullInstallPath = node_path.default.join(homeBase, pkg.installPath);
1522
+ const extractDir = opts.dir ?? node_path.default.dirname(fullInstallPath);
1523
+ node_fs.default.mkdirSync(extractDir, { recursive: true });
1524
+ const format = (pkg.format ?? "").toLowerCase();
1525
+ const lower = pkg.ossKey.toLowerCase();
1526
+ if (format === "tgz" || lower.endsWith(".tgz") || lower.endsWith(".tar.gz")) {
1527
+ (0, node_child_process.execSync)(`tar -xzf '${file}' -C '${extractDir}'`, { stdio: "ignore" });
1528
+ console.error(`[download-resource] ${opts.role}/${opts.name}: extracted to ${extractDir}`);
1529
+ } else {
1530
+ const basename = node_path.default.posix.basename(pkg.ossKey);
1531
+ node_fs.default.copyFileSync(file, node_path.default.join(extractDir, basename));
1532
+ console.error(`[download-resource] ${opts.role}/${opts.name}: copied ${basename} to ${extractDir}`);
1533
+ }
1534
+ }
1535
+ //#endregion
1536
+ //#region src/oss/getOpenclawTag.ts
1537
+ /**
1538
+ * Extracts the openclaw tag from the manifest key present in ossFileMap.
1539
+ * Avoids passing an extra ctx field — we already know the tag from the
1540
+ * well-known manifest key studio_server included.
1541
+ *
1542
+ * Manifest key shape: builtin/manifests/openclaw/recommended/<tag>.json
1543
+ */
1544
+ function getOpenclawTagFromOssFileMap(ossFileMap) {
1545
+ const prefix = "builtin/manifests/openclaw/recommended/";
1546
+ const suffix = ".json";
1547
+ for (const key of Object.keys(ossFileMap)) if (key.startsWith(prefix) && key.endsWith(suffix)) return key.slice(39, -5);
1548
+ throw new Error("cannot resolve openclaw tag: ossFileMap missing manifest key");
1549
+ }
1550
+ //#endregion
1103
1551
  //#region src/reset.ts
1104
1552
  const STEPS = [
1105
1553
  "备份当前配置",
@@ -1108,21 +1556,11 @@ const STEPS = [
1108
1556
  "等待沙箱初始化完成",
1109
1557
  "确认 openclaw 版本",
1110
1558
  "合并核心备份配置",
1111
- "复制启动脚本",
1559
+ "检查启动脚本",
1112
1560
  "安装扩展",
1113
1561
  "启动并验证"
1114
1562
  ];
1115
1563
  const TOTAL_STEPS = STEPS.length;
1116
- /** Pre-packed extensions archive on OSS. Update this URL when releasing a new version. */
1117
- const EXTENSIONS_OSS_URL = "https://miaoda-template-online.oss-cn-beijing.aliyuncs.com/builtin/tool/pkg/openclaw-extensions-2026.4.9.tar.gz";
1118
- /**
1119
- * Directory holding the bundled openclaw template (openclaw.json + scripts/).
1120
- * Synced from git@code.byted.org:apaas/miaoda-openclaw-template.git via
1121
- * scripts/sync-template.sh and published alongside dist/.
1122
- *
1123
- * At runtime, __dirname points to dist/, so the template lives one level up.
1124
- */
1125
- const TEMPLATE_DIR = node_path.default.resolve(__dirname, "..", "template");
1126
1564
  function writeResultFile(resultFile, result) {
1127
1565
  const dir = node_path.default.dirname(resultFile);
1128
1566
  if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
@@ -1160,6 +1598,33 @@ function markFailed(resultFile, step, error, startedAt) {
1160
1598
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
1161
1599
  });
1162
1600
  }
1601
+ /**
1602
+ * Download the template assets (config/openclaw.json + template/scripts) from
1603
+ * OSS into a scratch directory so the existing step 2 (generateDefaultConfig)
1604
+ * and step 7 (copyStartupScripts) can consume them as local files — the rest
1605
+ * of the orchestrator code stays untouched.
1606
+ *
1607
+ * Called once before step 1. The caller is responsible for rm -rf'ing
1608
+ * stagedDir in a finally{} block after the reset completes (or fails).
1609
+ */
1610
+ async function stageTemplate(openclawTag, ossFileMap, stagedDir, configDir, log) {
1611
+ if (node_fs.default.existsSync(stagedDir)) node_fs.default.rmSync(stagedDir, {
1612
+ recursive: true,
1613
+ force: true
1614
+ });
1615
+ node_fs.default.mkdirSync(stagedDir, { recursive: true });
1616
+ await downloadResource(openclawTag, ossFileMap, {
1617
+ role: "config",
1618
+ name: "openclaw.json",
1619
+ dir: stagedDir
1620
+ });
1621
+ await downloadResource(openclawTag, ossFileMap, {
1622
+ role: "template",
1623
+ name: "scripts",
1624
+ dir: configDir
1625
+ });
1626
+ log(`staged openclaw.json to ${stagedDir}, scripts directly to ${configDir}/scripts`);
1627
+ }
1163
1628
  /** Step 1: Backup current config as openclaw.json.bak.N */
1164
1629
  function backupCurrentConfig(configPath, log) {
1165
1630
  if (!fileExists(configPath)) {
@@ -1184,7 +1649,7 @@ function backupCurrentConfig(configPath, log) {
1184
1649
  /** Step 2: Replace $$__XXX__ placeholders and write default config. */
1185
1650
  function generateDefaultConfig(srcDir, configPath, templateVars, log) {
1186
1651
  const srcConfigPath = node_path.default.join(srcDir, "openclaw.json");
1187
- if (!fileExists(srcConfigPath)) throw new Error("template openclaw.json not found at " + srcConfigPath);
1652
+ if (!fileExists(srcConfigPath)) throw new Error("staged openclaw.json not found at " + srcConfigPath);
1188
1653
  let content = node_fs.default.readFileSync(srcConfigPath, "utf-8");
1189
1654
  let replaced = 0;
1190
1655
  for (const [placeholder, value] of Object.entries(templateVars)) {
@@ -1205,11 +1670,14 @@ function killOpenclawProcesses(log) {
1205
1670
  }
1206
1671
  /**
1207
1672
  * Step 4: Wait for the sandbox's own init (init_sandbox.sh / concurrent npm
1208
- * install) to finish before we start our own npm i -g. Two npm processes
1209
- * sharing ~/.npm cache + competing for disk/network just makes everything
1210
- * crawl; letting init finish first is the cleanest way to get exclusive
1211
- * access. Polls every 10s up to `maxWaitMs`. If the deadline is hit we fall
1212
- * through anyway — better to try than to fail the reset outright.
1673
+ * install) to finish before we start our own work. Two processes sharing
1674
+ * ~/.npm cache + competing for disk/network just makes everything crawl;
1675
+ * letting init finish first is the cleanest way to get exclusive access.
1676
+ * Polls every 10s up to `maxWaitMs`. If the deadline is hit we fall through
1677
+ * anyway — better to try than to fail the reset outright.
1678
+ *
1679
+ * Kept even after we switched off `npm install` because the sandbox init
1680
+ * script still runs `npm install` for other packages and holds cache locks.
1213
1681
  */
1214
1682
  function waitForInitNpm(maxWaitMs, log) {
1215
1683
  const deadline = Date.now() + maxWaitMs;
@@ -1237,81 +1705,15 @@ function waitForInitNpm(maxWaitMs, log) {
1237
1705
  log(`deadline (${maxWaitMs}ms) hit after ${polls} poll(s), proceeding anyway`);
1238
1706
  }
1239
1707
  /**
1240
- * Step 5: Ensure openclaw binary is at the template's recommended version.
1241
- *
1242
- * Only checks/installs the binary — does NOT run `doctor --fix` any more.
1243
- * Extensions are installed separately via OSS tar.gz (Step 8), which means
1244
- * the openclaw-lark extension schema is already in place when openclaw
1245
- * starts, avoiding the schema-priority mismatch that caused doctor to
1246
- * reject valid config fields like threadSession / footer.
1708
+ * Step 5: Install openclaw from the OSS-provided tarball at the target tag,
1709
+ * then verify `openclaw --version` output contains that tag. No npm involved.
1247
1710
  */
1248
- function ensureOpenclawBinary(srcDir, configPath, log) {
1249
- const targetVersion = loadJSON5().parse(node_fs.default.readFileSync(node_path.default.join(srcDir, "openclaw.json"), "utf-8")).meta?.lastTouchedVersion;
1250
- log(`target openclaw version: ${targetVersion ?? "<unset>"}`);
1251
- if (targetVersion && isOpenclawAtVersion(targetVersion)) {
1252
- log("openclaw already at target version, nothing to do");
1253
- return;
1254
- }
1255
- if (isOpenclawInstalled()) {
1256
- const updateCmd = `openclaw update${targetVersion ? " --tag " + targetVersion : ""} --yes --no-restart`;
1257
- log(`openclaw installed but version mismatched, running: ${updateCmd}`);
1258
- const timeout = 12 * 6e4;
1259
- const t = Date.now();
1260
- const tmpConfig = configPath + ".update-tmp";
1261
- const hadConfig = node_fs.default.existsSync(configPath);
1262
- if (hadConfig) {
1263
- node_fs.default.renameSync(configPath, tmpConfig);
1264
- log("temporarily hid config to bypass config guard");
1265
- }
1266
- try {
1267
- shell(updateCmd, timeout);
1268
- log(`openclaw update done in ${Date.now() - t}ms`);
1269
- } catch (e) {
1270
- const elapsed = Date.now() - t;
1271
- if (elapsed >= timeout - 1e3) log(`openclaw update timed out after ${elapsed}ms (non-fatal, continuing)`);
1272
- else throw e;
1273
- } finally {
1274
- if (hadConfig && node_fs.default.existsSync(tmpConfig)) {
1275
- node_fs.default.renameSync(tmpConfig, configPath);
1276
- log("restored config after update");
1277
- }
1278
- }
1279
- } else {
1280
- log("openclaw binary not found, running full reinstall");
1281
- fullReinstall(targetVersion, log);
1282
- }
1283
- }
1284
- /** Check if openclaw command exists (regardless of version). */
1285
- function isOpenclawInstalled() {
1286
- try {
1287
- shell("which openclaw 2>/dev/null", 5e3);
1288
- return true;
1289
- } catch {
1290
- return false;
1291
- }
1292
- }
1293
- /** Full uninstall + reinstall from npm (slow path, triggers postinstall). */
1294
- function fullReinstall(targetVersion, log) {
1295
- try {
1296
- shell("npm uninstall -g openclaw 2>/dev/null || true", 6e4);
1297
- } catch {}
1298
- try {
1299
- shell("rm -rf /home/gem/.npm-global/lib/node_modules/openclaw /home/gem/.npm-global/bin/openclaw 2>/dev/null || true", 1e4);
1300
- log("force-cleaned residual openclaw global dir");
1301
- } catch {}
1302
- const installCmd = `npm i -g openclaw@${targetVersion || "latest"}`;
1303
- log(`running: ${installCmd}`);
1304
- const t = Date.now();
1305
- shell(installCmd, 30 * 6e4);
1306
- log(`npm install done in ${Date.now() - t}ms`);
1307
- }
1308
- /** Return true if `openclaw --version` output contains `targetVersion`. */
1309
- function isOpenclawAtVersion(targetVersion) {
1310
- try {
1311
- return shell("openclaw --version 2>&1 || true", 1e4).includes(targetVersion);
1312
- } catch {
1313
- return false;
1314
- }
1711
+ async function step5InstallOpenclaw(openclawTag, ossFileMap, log) {
1712
+ log(`install-openclaw tag=${openclawTag}`);
1713
+ await installOpenclaw(openclawTag, ossFileMap);
1714
+ const out = shell("openclaw --version 2>&1 || true", 1e4).trim();
1715
+ if (!out.includes(openclawTag)) throw new Error(`openclaw version verify failed: got "${out}"`);
1716
+ log(`openclaw version verified: ${out}`);
1315
1717
  }
1316
1718
  /** Step 6: Merge coreBackup from resetData + ensure allowedOrigins. */
1317
1719
  function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
@@ -1405,73 +1807,31 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
1405
1807
  node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
1406
1808
  log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
1407
1809
  }
1408
- /** Step 7: Copy startup scripts from template to agent dir. */
1409
- function copyStartupScripts(srcDir, configDir, log) {
1410
- const srcScriptsDir = node_path.default.join(srcDir, "scripts");
1810
+ /**
1811
+ * Step 7: Verify startup scripts landed in configDir/scripts/.
1812
+ *
1813
+ * Scripts are extracted directly to configDir/scripts/ during stageTemplate —
1814
+ * there's no intermediate copy any more. This step is now a verification gate
1815
+ * (rather than a copy action) so the step count stays at 9 and we fail early
1816
+ * if the template tgz didn't carry a scripts/ dir.
1817
+ */
1818
+ function verifyStartupScripts(configDir, log) {
1411
1819
  const targetScriptsDir = node_path.default.join(configDir, "scripts");
1412
- if (!node_fs.default.existsSync(srcScriptsDir)) {
1413
- log(`no scripts/ in template, skip`);
1414
- return;
1415
- }
1416
- if (!node_fs.default.existsSync(targetScriptsDir)) node_fs.default.mkdirSync(targetScriptsDir, { recursive: true });
1417
- shell(`cp -r '${srcScriptsDir}'/* '${targetScriptsDir}/'`, 1e4);
1418
- log(`copied scripts/* -> ${targetScriptsDir}`);
1820
+ if (!node_fs.default.existsSync(targetScriptsDir)) throw new Error(`scripts dir missing at ${targetScriptsDir} — template download failed?`);
1821
+ log(`scripts dir present at ${targetScriptsDir}`);
1419
1822
  }
1420
1823
  /**
1421
- * Step 8: Install extensions from OSS tar.gz or fall back to `openclaw plugins update`.
1422
- *
1423
- * 1. Derive the OSS URL from the openclaw version in the template config
1424
- * 2. Download tar.gz → extract to a staging dir
1425
- * 3. For each included extension, backup (mv) the user's existing copy
1426
- * to a temp dir — extensions NOT in the archive are left untouched
1427
- * 4. Move the fresh extensions from staging to the target dir
1428
- *
1429
- * This bypasses npm entirely and avoids the schema-priority mismatch where
1430
- * `openclaw doctor --fix` would validate config against the bundled feishu
1431
- * plugin's strict schema before openclaw-lark is installed.
1432
- *
1433
- * Falls back to `openclaw plugins update --all` if the OSS download fails.
1824
+ * Step 8: Install all extensions listed in the OSS manifest at `openclawTag`.
1825
+ * Replaces the old `plugins update --all` / pre-packed tar.gz flow — the
1826
+ * manifest is now the single source of truth for which extensions ship.
1434
1827
  */
1435
- function installExtensions(configDir, log) {
1436
- const ossUrl = EXTENSIONS_OSS_URL;
1437
- const targetExtDir = node_path.default.join(configDir, "extensions");
1438
- const tmpDir = node_path.default.join(configDir, `.ext-reset-tmp-${Date.now()}`);
1439
- const tarPath = node_path.default.join(tmpDir, "extensions.tar.gz");
1440
- node_fs.default.mkdirSync(tmpDir, { recursive: true });
1441
- try {
1442
- log(`downloading extensions from ${ossUrl}`);
1443
- const dlStart = Date.now();
1444
- shell(`curl -fsSL '${ossUrl}' -o '${tarPath}'`, 5 * 6e4);
1445
- const size = node_fs.default.statSync(tarPath).size;
1446
- log(`download done in ${Date.now() - dlStart}ms (${(size / 1024 / 1024).toFixed(1)}MB)`);
1447
- shell(`tar -xzf '${tarPath}' -C '${tmpDir}'`, 6e4);
1448
- const stagedExtDir = node_path.default.join(tmpDir, "extensions");
1449
- if (!node_fs.default.existsSync(stagedExtDir)) throw new Error("tar.gz does not contain an extensions/ directory");
1450
- const extNames = node_fs.default.readdirSync(stagedExtDir).filter((name) => node_fs.default.statSync(node_path.default.join(stagedExtDir, name)).isDirectory());
1451
- log(`archive contains ${extNames.length} extension(s): ${extNames.join(", ")}`);
1452
- for (const name of extNames) {
1453
- const target = node_path.default.join(targetExtDir, name);
1454
- if (node_fs.default.existsSync(target)) node_fs.default.rmSync(target, {
1455
- recursive: true,
1456
- force: true
1457
- });
1458
- node_fs.default.renameSync(node_path.default.join(stagedExtDir, name), target);
1459
- log(` ${name}: installed`);
1460
- }
1461
- const packinfo = node_path.default.join(stagedExtDir, ".packinfo.json");
1462
- if (node_fs.default.existsSync(packinfo)) {
1463
- node_fs.default.copyFileSync(packinfo, node_path.default.join(targetExtDir, ".packinfo.json"));
1464
- log(`packinfo: ${node_fs.default.readFileSync(packinfo, "utf-8").trim()}`);
1465
- }
1466
- log("extensions installed from OSS successfully");
1467
- } finally {
1468
- try {
1469
- node_fs.default.rmSync(tmpDir, {
1470
- recursive: true,
1471
- force: true
1472
- });
1473
- } catch {}
1474
- }
1828
+ async function step8InstallExtensions(openclawTag, ossFileMap, log) {
1829
+ log(`install-extension --all tag=${openclawTag}`);
1830
+ await installExtension(openclawTag, ossFileMap, {
1831
+ all: true,
1832
+ skipConfigUpdate: true
1833
+ });
1834
+ log("extensions installed");
1475
1835
  }
1476
1836
  /** Step 9: Write secrets/provider key files and restart openclaw. */
1477
1837
  function writeSecretsAndRestart(vars, resetData, configDir, log) {
@@ -1496,26 +1856,37 @@ function writeSecretsAndRestart(vars, resetData, configDir, log) {
1496
1856
  * Each step is an independent function. The orchestrator handles progress
1497
1857
  * reporting, error handling, and process-level exception guards.
1498
1858
  *
1499
- * The openclaw.json / scripts/*.sh template files are bundled with this CLI
1500
- * (see TEMPLATE_DIR) and synced from the miaoda-openclaw-template repo via
1501
- * scripts/sync-template.sh, so no runtime download is required.
1859
+ * Template assets (openclaw.json + scripts/) are downloaded from OSS into a
1860
+ * scratch dir via `stageTemplate` before step 1 there is no bundled
1861
+ * `template/` directory at runtime any more.
1502
1862
  */
1503
- function runReset(input, taskId, resultFile) {
1863
+ async function runReset(input, taskId, resultFile) {
1504
1864
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1505
1865
  const { configPath, vars, resetData } = input;
1506
1866
  const configDir = node_path.default.dirname(configPath);
1507
- const srcDir = TEMPLATE_DIR;
1867
+ const stagedDir = node_path.default.join(DIAGNOSE_DIR, `reset-${taskId}-template`);
1508
1868
  let currentStep = 0;
1509
1869
  let stepStartedAt = Date.now();
1510
1870
  const log = makeLogger(resetLogFile(taskId));
1511
1871
  log(`=== reset started, taskId=${taskId}, pid=${process.pid} ===`);
1512
- log(`configPath=${configPath}, configDir=${configDir}, templateDir=${srcDir}`);
1513
- if (!node_fs.default.existsSync(node_path.default.join(srcDir, "openclaw.json"))) {
1514
- const err = `bundled template not found at ${srcDir}`;
1872
+ log(`configPath=${configPath}, configDir=${configDir}, stagedDir=${stagedDir}`);
1873
+ const ossFileMap = resetData.ossFileMap;
1874
+ if (!ossFileMap || Object.keys(ossFileMap).length === 0) {
1875
+ const err = "resetData.ossFileMap missing or empty";
1515
1876
  log(`ERROR: ${err}`);
1516
1877
  markFailed(resultFile, 0, err, startedAt);
1517
1878
  process.exit(1);
1518
1879
  }
1880
+ let openclawTag;
1881
+ try {
1882
+ openclawTag = getOpenclawTagFromOssFileMap(ossFileMap);
1883
+ } catch (e) {
1884
+ const err = e.message;
1885
+ log(`ERROR: ${err}`);
1886
+ markFailed(resultFile, 0, err, startedAt);
1887
+ process.exit(1);
1888
+ }
1889
+ log(`openclawTag=${openclawTag}`);
1519
1890
  process.on("uncaughtException", (err) => {
1520
1891
  log(`FATAL uncaughtException: ${err.message}\n${err.stack ?? ""}`);
1521
1892
  markFailed(resultFile, currentStep, `uncaught exception: ${err.message}`, startedAt);
@@ -1535,22 +1906,23 @@ function runReset(input, taskId, resultFile) {
1535
1906
  updateProgress(resultFile, n, startedAt);
1536
1907
  };
1537
1908
  try {
1909
+ await stageTemplate(openclawTag, ossFileMap, stagedDir, configDir, log);
1538
1910
  step(1);
1539
1911
  backupCurrentConfig(configPath, log);
1540
1912
  step(2);
1541
- generateDefaultConfig(srcDir, configPath, resetData.templateVars, log);
1913
+ generateDefaultConfig(stagedDir, configPath, resetData.templateVars, log);
1542
1914
  step(3);
1543
1915
  killOpenclawProcesses(log);
1544
1916
  step(4);
1545
1917
  waitForInitNpm(10 * 6e4, log);
1546
1918
  step(5);
1547
- ensureOpenclawBinary(srcDir, configPath, log);
1919
+ await step5InstallOpenclaw(openclawTag, ossFileMap, log);
1548
1920
  step(6);
1549
1921
  mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
1550
1922
  step(7);
1551
- copyStartupScripts(srcDir, configDir, log);
1923
+ verifyStartupScripts(configDir, log);
1552
1924
  step(8);
1553
- installExtensions(configDir, log);
1925
+ await step8InstallExtensions(openclawTag, ossFileMap, log);
1554
1926
  step(9);
1555
1927
  writeSecretsAndRestart(vars, resetData, configDir, log);
1556
1928
  log(`step 9 "${STEPS[8]}" done in ${Date.now() - stepStartedAt}ms`);
@@ -1561,6 +1933,13 @@ function runReset(input, taskId, resultFile) {
1561
1933
  log(`ERROR in step ${currentStep} "${STEPS[currentStep - 1] ?? "init"}" after ${Date.now() - stepStartedAt}ms: ${err}\n${e.stack ?? ""}`);
1562
1934
  markFailed(resultFile, currentStep, err, startedAt);
1563
1935
  process.exit(1);
1936
+ } finally {
1937
+ try {
1938
+ node_fs.default.rmSync(stagedDir, {
1939
+ recursive: true,
1940
+ force: true
1941
+ });
1942
+ } catch {}
1564
1943
  }
1565
1944
  }
1566
1945
  //#endregion
@@ -1602,55 +1981,485 @@ function sleepSync(ms) {
1602
1981
  Atomics.wait(arr, 0, 0, ms);
1603
1982
  }
1604
1983
  //#endregion
1984
+ //#region src/oss/resolveOssFileMap.ts
1985
+ /**
1986
+ * Pick an OssFileMap in the order of decreasing specificity:
1987
+ * 1. `--oss_file_map=` flag — operator override (manual invocations, tests)
1988
+ * 2. `ctx.install.ossFileMap` — new shape (innerapi-driven DoctorCtx)
1989
+ * 3. `ctx.resetData.ossFileMap` — legacy shape (sandbox_console push path)
1990
+ *
1991
+ * Throws when none of the three yields a non-empty map. Empty maps are
1992
+ * treated as missing — an empty map is useless downstream and almost always
1993
+ * indicates a ctx wiring bug.
1994
+ */
1995
+ function resolveOssFileMap(args) {
1996
+ if (args.ossFileMapFlag) return JSON.parse(Buffer.from(args.ossFileMapFlag, "base64").toString("utf-8"));
1997
+ if (args.installOssFileMap && Object.keys(args.installOssFileMap).length > 0) return args.installOssFileMap;
1998
+ if (args.resetDataOssFileMap && Object.keys(args.resetDataOssFileMap).length > 0) return args.resetDataOssFileMap;
1999
+ throw new Error("ossFileMap missing: provide --oss_file_map flag, ctx.install.ossFileMap, or resetData.ossFileMap");
2000
+ }
2001
+ //#endregion
2002
+ //#region src/innerapi/fetchCtx.ts
2003
+ /**
2004
+ * CLI-side client for studio_server's `openclaw.get_doctor_ctx` inner API.
2005
+ *
2006
+ * Mirrors the proven pattern in
2007
+ * `packages/openclaw/extensions/miaoda/src/shared/innerapi-client.ts`:
2008
+ *
2009
+ * - `baseURL` from env `FORCE_AUTHN_INNERAPI_DOMAIN` (injected into every
2010
+ * openclaw sandbox).
2011
+ * - `platform: { enabled, tokenProvider: { type: 'file' } }` — the platform
2012
+ * plugin auto-attaches the sandbox's identity JWT loaded from the
2013
+ * rootfs token file. Same auth that the miaoda extension already uses.
2014
+ * - POST `/api/v1/studio/innerapi/integration_apis/call`
2015
+ * body = { apiName: 'openclaw.get_doctor_ctx', input: {}, bizType: 'openclaw' }
2016
+ * — the server-side APICall dispatches by `apiName` to
2017
+ * `GetDoctorCtxAPICall.Execute` whose `Name()` returns that string.
2018
+ * - Response envelope: { status_code, error_msg?, data: { success, output, ... } }.
2019
+ * `status_code` is a *string* ('0' = success).
2020
+ * Actual DoctorCtx lives in `data.output`.
2021
+ * - `x-tt-logid` header is logged on every failure path for cross-service
2022
+ * traceability.
2023
+ *
2024
+ * On HTTP 401 (sandbox identity token expired/invalid) we `process.exit(77)`
2025
+ * instead of throwing — the outer catch in `index.ts` cannot then mask auth
2026
+ * failure as a generic "Error: ...". Caller (e.g. sandbox_console) sees the
2027
+ * exit code and can refresh the token + retry.
2028
+ */
2029
+ const INNERAPI_CALL_PATH = "/api/v1/studio/innerapi/integration_apis/call";
2030
+ const API_NAME = "openclaw.get_doctor_ctx";
2031
+ const BIZ_TYPE = "openclaw";
2032
+ const API_TIMEOUT_MS = 3e4;
2033
+ const MAX_LOG_BODY = 500;
2034
+ let clientInstance = null;
2035
+ function getHttpClient() {
2036
+ if (!clientInstance) {
2037
+ const apiUrl = process.env.FORCE_AUTHN_INNERAPI_DOMAIN;
2038
+ (0, node_assert.default)(apiUrl, "missing env: FORCE_AUTHN_INNERAPI_DOMAIN (openclaw sandbox runtime must expose this)");
2039
+ clientInstance = new _lark_apaas_http_client.HttpClient({
2040
+ baseURL: apiUrl,
2041
+ timeout: API_TIMEOUT_MS,
2042
+ platform: {
2043
+ enabled: true,
2044
+ tokenProvider: { type: "file" }
2045
+ }
2046
+ });
2047
+ }
2048
+ return clientInstance;
2049
+ }
2050
+ /**
2051
+ * Fetch the sandbox's DoctorCtx by calling the innerapi's generic
2052
+ * `integration_apis/call` dispatcher with apiName=openclaw.get_doctor_ctx.
2053
+ *
2054
+ * Throws on HTTP (non-401) / decode / business errors. On 401 calls
2055
+ * `process.exit(77)` directly.
2056
+ */
2057
+ async function fetchCtxViaInnerApi() {
2058
+ const client = getHttpClient();
2059
+ const body = {
2060
+ apiName: API_NAME,
2061
+ input: {},
2062
+ bizType: BIZ_TYPE
2063
+ };
2064
+ const start = Date.now();
2065
+ const headers = { "Content-Type": "application/json" };
2066
+ const ttEnv = process.env.X_TT_ENV;
2067
+ if (ttEnv) headers["x-tt-env"] = ttEnv;
2068
+ let response;
2069
+ try {
2070
+ response = await client.post(INNERAPI_CALL_PATH, body, { headers });
2071
+ } catch (e) {
2072
+ const durationMs = Date.now() - start;
2073
+ if (e instanceof _lark_apaas_http_client.HttpError && e.response) {
2074
+ const status = e.response.status;
2075
+ const logId = e.response.headers.get("x-tt-logid") ?? "";
2076
+ if (status === 401) {
2077
+ console.error(`[CLI] innerapi 401 (logID: ${logId}) — sandbox identity token expired/invalid; exiting 77`);
2078
+ process.exit(77);
2079
+ }
2080
+ throw new Error(`fetchCtxViaInnerApi HTTP ${status} ${e.response.statusText} (logID: ${logId}, durationMs: ${durationMs})`);
2081
+ }
2082
+ const msg = e instanceof Error ? e.message : String(e);
2083
+ throw new Error(`fetchCtxViaInnerApi network error: ${msg} (durationMs: ${durationMs})`);
2084
+ }
2085
+ const logId = response.headers.get("x-tt-logid") ?? "";
2086
+ const durationMs = Date.now() - start;
2087
+ if (!response.ok) {
2088
+ if (response.status === 401) {
2089
+ console.error(`[CLI] innerapi 401 (logID: ${logId}) — sandbox identity token expired/invalid; exiting 77`);
2090
+ process.exit(77);
2091
+ }
2092
+ let preview = "";
2093
+ try {
2094
+ preview = (await response.text()).slice(0, MAX_LOG_BODY);
2095
+ } catch {}
2096
+ throw new Error(`fetchCtxViaInnerApi HTTP ${response.status} ${response.statusText} (logID: ${logId}, durationMs: ${durationMs})${preview ? ` body=${preview}` : ""}`);
2097
+ }
2098
+ let envelope;
2099
+ try {
2100
+ envelope = await response.json();
2101
+ } catch {
2102
+ throw new Error(`fetchCtxViaInnerApi decode error (logID: ${logId}, durationMs: ${durationMs})`);
2103
+ }
2104
+ if (envelope.status_code !== "0") throw new Error(`fetchCtxViaInnerApi API error (logID: ${logId}, durationMs: ${durationMs}): code=${envelope.status_code}, message=${envelope.error_msg ?? ""}`);
2105
+ if (envelope.data && envelope.data.success === false) throw new Error(`fetchCtxViaInnerApi business error (logID: ${logId}, durationMs: ${durationMs}): ${envelope.error_msg ?? JSON.stringify(envelope.data)}`);
2106
+ const output = envelope.data?.output;
2107
+ if (!output || typeof output !== "object") throw new Error(`fetchCtxViaInnerApi empty/invalid output (logID: ${logId}, durationMs: ${durationMs})`);
2108
+ return output;
2109
+ }
2110
+ //#endregion
2111
+ //#region src/ctx/normalize.ts
2112
+ /**
2113
+ * Accept raw ctx from any of these sources and produce a uniform view:
2114
+ * - New shape (DoctorCtx): `{ app, install, secrets, reset }` — from innerapi.
2115
+ * - Old shape (ResetInput): `{ configPath, vars, resetData }` — from
2116
+ * sandbox_console push path.
2117
+ * Detection is structural: if the top-level has all four new-shape groups we
2118
+ * pass through; otherwise we remap from the old shape.
2119
+ *
2120
+ * Missing fields fall back to safe empty defaults (empty strings / arrays /
2121
+ * maps) so every downstream consumer can read e.g. `ctx.app.feishuAppID`
2122
+ * without an extra nullish guard.
2123
+ */
2124
+ function normalizeCtx(raw) {
2125
+ const r = raw ?? {};
2126
+ 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 {
2127
+ app: fillApp(r.app),
2128
+ install: {
2129
+ openclawTag: r.install.openclawTag,
2130
+ ossFileMap: r.install.ossFileMap ?? {}
2131
+ },
2132
+ secrets: {
2133
+ secretsContent: r.secrets.secretsContent ?? "",
2134
+ providerKeyContent: r.secrets.providerKeyContent ?? ""
2135
+ },
2136
+ reset: {
2137
+ templateVars: r.reset.templateVars ?? {},
2138
+ coreBackup: r.reset.coreBackup
2139
+ }
2140
+ };
2141
+ const vars = r.vars ?? {};
2142
+ const resetData = r.resetData ?? {};
2143
+ const repairData = r.repairData ?? {};
2144
+ return {
2145
+ app: fillApp(vars),
2146
+ install: {
2147
+ openclawTag: r.install?.openclawTag ?? r.openclawTag,
2148
+ ossFileMap: r.install?.ossFileMap ?? resetData.ossFileMap ?? r.ossFileMap ?? {}
2149
+ },
2150
+ secrets: {
2151
+ secretsContent: resetData.secretsContent ?? repairData.secretsContent ?? "",
2152
+ providerKeyContent: resetData.providerKeyContent ?? repairData.providerKeyContent ?? ""
2153
+ },
2154
+ reset: {
2155
+ templateVars: resetData.templateVars ?? {},
2156
+ coreBackup: resetData.coreBackup
2157
+ }
2158
+ };
2159
+ }
2160
+ function fillApp(src) {
2161
+ return {
2162
+ feishuAppID: src.feishuAppID ?? "",
2163
+ feishuAppSecret: src.feishuAppSecret ?? "",
2164
+ feishuOpenID: src.feishuOpenID ?? "",
2165
+ openClawName: src.openClawName ?? "",
2166
+ gatewayToken: src.gatewayToken ?? "",
2167
+ innerAPIKey: src.innerAPIKey ?? "",
2168
+ baseURL: src.baseURL ?? "",
2169
+ miaodaDomain: src.miaodaDomain ?? "",
2170
+ miaodaOrigin: src.miaodaOrigin ?? "",
2171
+ expectedOrigins: Array.isArray(src.expectedOrigins) ? src.expectedOrigins : []
2172
+ };
2173
+ }
2174
+ //#endregion
2175
+ //#region src/ctx-input.ts
2176
+ /**
2177
+ * Build legacy Check/Repair/Reset input shapes from a raw ctx object. Shared
2178
+ * by both the top-level CLI dispatcher (`index.ts`) and the new `doctor`
2179
+ * subcommand (`doctor.ts`), which need identical input synthesis.
2180
+ *
2181
+ * Behavior:
2182
+ * - If `raw` already carries the legacy `configPath + vars` shape (the one
2183
+ * sandbox_console push emits), it's trusted and returned as-is. This
2184
+ * keeps the existing sandbox_console push contract working.
2185
+ * - Otherwise `raw` is treated as the new-shape DoctorCtx (or anything
2186
+ * structurally close — `normalizeCtx` fills the gaps with safe empties)
2187
+ * and the legacy Vars shape is synthesised using the hardcoded sandbox
2188
+ * path invariants from `paths.ts`.
2189
+ *
2190
+ * The optional `configPathOverride` lets unit tests point the builder at a
2191
+ * tmp file; production callers should leave it undefined so the sandbox
2192
+ * invariant from `paths.ts` is used.
2193
+ */
2194
+ function buildCheckInput(raw, configPathOverride) {
2195
+ const r = raw ?? {};
2196
+ if (r.configPath && r.vars) {
2197
+ if (configPathOverride) return {
2198
+ ...r,
2199
+ configPath: configPathOverride
2200
+ };
2201
+ return r;
2202
+ }
2203
+ const ctx = normalizeCtx(raw);
2204
+ return {
2205
+ configPath: configPathOverride ?? CONFIG_PATH,
2206
+ vars: {
2207
+ feishuAppID: ctx.app.feishuAppID,
2208
+ feishuAppSecret: ctx.app.feishuAppSecret,
2209
+ innerAPIKey: ctx.app.innerAPIKey,
2210
+ gatewayToken: ctx.app.gatewayToken,
2211
+ baseURL: ctx.app.baseURL,
2212
+ expectedOrigins: ctx.app.expectedOrigins,
2213
+ providerFilePath: PROVIDER_FILE_PATH,
2214
+ secretsFilePath: SECRETS_FILE_PATH
2215
+ },
2216
+ templateVars: ctx.reset.templateVars
2217
+ };
2218
+ }
2219
+ function buildRepairInput(raw, configPathOverride) {
2220
+ const r = raw ?? {};
2221
+ if (r.configPath && r.vars) {
2222
+ if (configPathOverride) return {
2223
+ ...r,
2224
+ configPath: configPathOverride
2225
+ };
2226
+ return r;
2227
+ }
2228
+ const ctx = normalizeCtx(raw);
2229
+ return {
2230
+ configPath: configPathOverride ?? CONFIG_PATH,
2231
+ vars: {
2232
+ feishuAppID: ctx.app.feishuAppID,
2233
+ feishuAppSecret: ctx.app.feishuAppSecret,
2234
+ innerAPIKey: ctx.app.innerAPIKey,
2235
+ gatewayToken: ctx.app.gatewayToken,
2236
+ baseURL: ctx.app.baseURL,
2237
+ expectedOrigins: ctx.app.expectedOrigins,
2238
+ providerFilePath: PROVIDER_FILE_PATH,
2239
+ secretsFilePath: SECRETS_FILE_PATH
2240
+ },
2241
+ repairData: {
2242
+ secretsContent: ctx.secrets.secretsContent,
2243
+ providerKeyContent: ctx.secrets.providerKeyContent
2244
+ },
2245
+ templateVars: ctx.reset.templateVars
2246
+ };
2247
+ }
2248
+ function buildResetInput(raw, configPathOverride) {
2249
+ const r = raw ?? {};
2250
+ if (r.configPath && r.vars && r.resetData) {
2251
+ if (configPathOverride) return {
2252
+ ...r,
2253
+ configPath: configPathOverride
2254
+ };
2255
+ return r;
2256
+ }
2257
+ const ctx = normalizeCtx(raw);
2258
+ return {
2259
+ configPath: configPathOverride ?? CONFIG_PATH,
2260
+ vars: {
2261
+ feishuAppID: ctx.app.feishuAppID,
2262
+ feishuAppSecret: ctx.app.feishuAppSecret,
2263
+ innerAPIKey: ctx.app.innerAPIKey,
2264
+ gatewayToken: ctx.app.gatewayToken,
2265
+ baseURL: ctx.app.baseURL,
2266
+ expectedOrigins: ctx.app.expectedOrigins,
2267
+ providerFilePath: PROVIDER_FILE_PATH,
2268
+ secretsFilePath: SECRETS_FILE_PATH
2269
+ },
2270
+ resetData: {
2271
+ templateVars: ctx.reset.templateVars,
2272
+ secretsContent: ctx.secrets.secretsContent,
2273
+ providerKeyContent: ctx.secrets.providerKeyContent,
2274
+ coreBackup: ctx.reset.coreBackup,
2275
+ ossFileMap: ctx.install.ossFileMap
2276
+ }
2277
+ };
2278
+ }
2279
+ //#endregion
2280
+ //#region src/doctor.ts
2281
+ async function runDoctor(rawCtx, opts) {
2282
+ if (opts.fix && opts.rules.length > 0) {
2283
+ const repairInput = buildRepairInput(rawCtx, opts.configPath);
2284
+ repairInput.failedRules = opts.rules;
2285
+ repairInput.repairData = {
2286
+ ...repairInput.repairData ?? {},
2287
+ restartCommand: ""
2288
+ };
2289
+ return { repair: runRepair(repairInput) };
2290
+ }
2291
+ const check = runCheck(buildCheckInput(rawCtx, opts.configPath));
2292
+ if (!opts.fix) return { failedRules: check.failedRules };
2293
+ const repairInput = buildRepairInput(rawCtx, opts.configPath);
2294
+ repairInput.failedRules = check.failedRules.standard;
2295
+ return {
2296
+ check,
2297
+ repair: runRepair(repairInput)
2298
+ };
2299
+ }
2300
+ //#endregion
1605
2301
  //#region src/index.ts
1606
2302
  const args = node_process.default.argv.slice(2);
1607
2303
  const mode = args.find((a) => !a.startsWith("--"));
1608
- switch (mode) {
1609
- case "check":
1610
- case "repair": {
1611
- const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
1612
- if (!ctx) {
1613
- console.error("Error: --ctx=<base64> is required");
1614
- node_process.default.exit(1);
2304
+ /**
2305
+ * Decode `--ctx=<base64>` into an opaque JSON object. Returns undefined when
2306
+ * the flag isn't present — the caller decides whether to fall back to the
2307
+ * innerapi or to error out.
2308
+ *
2309
+ * The object's shape is not enforced here; downstream code consumes it via
2310
+ * either `normalizeCtx()` (new path) or direct field access for the legacy
2311
+ * check/repair/reset contract still used by sandbox_console push.
2312
+ */
2313
+ function parseCtxFlag(args) {
2314
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
2315
+ if (!ctxArg) return void 0;
2316
+ const b64 = ctxArg.slice(6);
2317
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
2318
+ }
2319
+ /**
2320
+ * Pull the first non-flag positional after the mode name.
2321
+ * (The mode itself is args[0] in the filtered set, so we skip index 0.)
2322
+ */
2323
+ function getPositionalTag(args, modeName) {
2324
+ return args.find((a, i) => i > 0 && !a.startsWith("--") && a !== modeName);
2325
+ }
2326
+ function getFlag(args, name) {
2327
+ const prefix = `--${name}=`;
2328
+ return args.find((a) => a.startsWith(prefix))?.slice(prefix.length);
2329
+ }
2330
+ function getMultiFlag(args, name) {
2331
+ const prefix = `--${name}=`;
2332
+ return args.filter((a) => a.startsWith(prefix)).map((a) => a.slice(prefix.length));
2333
+ }
2334
+ async function main() {
2335
+ switch (mode) {
2336
+ case "check":
2337
+ case "repair": {
2338
+ const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi();
2339
+ if (mode === "check") console.log(JSON.stringify(runCheck(buildCheckInput(raw))));
2340
+ else console.log(JSON.stringify(runRepair(buildRepairInput(raw))));
2341
+ break;
2342
+ }
2343
+ case "doctor": {
2344
+ const fix = args.includes("--fix");
2345
+ const rules = getMultiFlag(args, "rule");
2346
+ const result = await runDoctor(await fetchCtxViaInnerApi(), {
2347
+ fix,
2348
+ rules
2349
+ });
2350
+ console.log(JSON.stringify(result));
2351
+ break;
1615
2352
  }
1616
- const input = JSON.parse(Buffer.from(ctx, "base64").toString("utf-8"));
1617
- if (mode === "check") console.log(JSON.stringify(runCheck(input)));
1618
- else console.log(JSON.stringify(runRepair(input)));
1619
- break;
1620
- }
1621
- case "reset":
1622
- if (args.includes("--async")) {
1623
- const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
1624
- if (!ctx) {
1625
- console.error("Error: --ctx=<base64> is required");
2353
+ case "reset":
2354
+ if (args.includes("--async")) {
2355
+ const ctxArg = args.find((a) => a.startsWith("--ctx="));
2356
+ let ctxBase64;
2357
+ if (ctxArg) ctxBase64 = ctxArg.slice(6);
2358
+ else {
2359
+ const fetched = await fetchCtxViaInnerApi();
2360
+ ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
2361
+ }
2362
+ console.log(JSON.stringify(startAsyncReset(ctxBase64)));
2363
+ } else if (args.includes("--worker")) {
2364
+ const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
2365
+ if (!taskId) {
2366
+ console.error("Error: --task-id=<id> is required for worker");
2367
+ node_process.default.exit(1);
2368
+ }
2369
+ const resultFile = resetResultFile(taskId);
2370
+ await runReset(buildResetInput(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()), taskId, resultFile);
2371
+ } else {
2372
+ console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
1626
2373
  node_process.default.exit(1);
1627
2374
  }
1628
- console.log(JSON.stringify(startAsyncReset(ctx)));
1629
- } else if (args.includes("--worker")) {
1630
- const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
2375
+ break;
2376
+ case "get_reset_task": {
1631
2377
  const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
1632
- if (!ctx || !taskId) {
1633
- console.error("Error: --ctx=<base64> and --task-id=<id> are required for worker");
2378
+ if (!taskId) {
2379
+ console.error("Error: --task-id=<id> is required");
1634
2380
  node_process.default.exit(1);
1635
2381
  }
1636
- const resultFile = resetResultFile(taskId);
1637
- runReset(JSON.parse(Buffer.from(ctx, "base64").toString("utf-8")), taskId, resultFile);
1638
- } else {
1639
- console.error("Usage: reset --async --ctx=<base64> | reset --worker --task-id=<id> --ctx=<base64>");
1640
- node_process.default.exit(1);
2382
+ console.log(JSON.stringify(getResetTask(taskId)));
2383
+ break;
1641
2384
  }
1642
- break;
1643
- case "get_reset_task": {
1644
- const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
1645
- if (!taskId) {
1646
- console.error("Error: --task-id=<id> is required");
1647
- node_process.default.exit(1);
2385
+ case "install-openclaw": {
2386
+ const tag = getPositionalTag(args, "install-openclaw");
2387
+ if (!tag) {
2388
+ console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
2389
+ node_process.default.exit(1);
2390
+ }
2391
+ const ossFileMapFlag = getFlag(args, "oss_file_map");
2392
+ let installOssFileMap;
2393
+ if (!ossFileMapFlag) installOssFileMap = normalizeCtx(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()).install.ossFileMap;
2394
+ await installOpenclaw(tag, resolveOssFileMap({
2395
+ ossFileMapFlag,
2396
+ installOssFileMap
2397
+ }));
2398
+ console.log(JSON.stringify({ ok: true }));
2399
+ break;
2400
+ }
2401
+ case "install-extension": {
2402
+ const tag = getPositionalTag(args, "install-extension");
2403
+ if (!tag) {
2404
+ console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--ctx=<base64> | --oss_file_map=<base64>]");
2405
+ node_process.default.exit(1);
2406
+ }
2407
+ const all = args.includes("--all");
2408
+ const names = getMultiFlag(args, "extension");
2409
+ const homeBase = getFlag(args, "home_base");
2410
+ const configPath = getFlag(args, "config_path");
2411
+ const skipConfigUpdate = args.includes("--skip-config-update");
2412
+ const ossFileMapFlag = getFlag(args, "oss_file_map");
2413
+ let installOssFileMap;
2414
+ if (!ossFileMapFlag) installOssFileMap = normalizeCtx(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()).install.ossFileMap;
2415
+ await installExtension(tag, resolveOssFileMap({
2416
+ ossFileMapFlag,
2417
+ installOssFileMap
2418
+ }), {
2419
+ all,
2420
+ names: names.length > 0 ? names : void 0,
2421
+ homeBase,
2422
+ configPath,
2423
+ skipConfigUpdate
2424
+ });
2425
+ console.log(JSON.stringify({ ok: true }));
2426
+ break;
2427
+ }
2428
+ case "download-resource": {
2429
+ const tag = getPositionalTag(args, "download-resource");
2430
+ if (!tag) {
2431
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
2432
+ node_process.default.exit(1);
2433
+ }
2434
+ const role = getFlag(args, "role");
2435
+ const name = getFlag(args, "name");
2436
+ const dir = getFlag(args, "dir");
2437
+ if (!role || !name) {
2438
+ console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>]");
2439
+ node_process.default.exit(1);
2440
+ }
2441
+ const ossFileMapFlag = getFlag(args, "oss_file_map");
2442
+ let installOssFileMap;
2443
+ if (!ossFileMapFlag) installOssFileMap = normalizeCtx(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()).install.ossFileMap;
2444
+ await downloadResource(tag, resolveOssFileMap({
2445
+ ossFileMapFlag,
2446
+ installOssFileMap
2447
+ }), {
2448
+ role,
2449
+ name,
2450
+ dir
2451
+ });
2452
+ console.log(JSON.stringify({ ok: true }));
2453
+ break;
1648
2454
  }
1649
- console.log(JSON.stringify(getResetTask(taskId)));
1650
- break;
2455
+ default:
2456
+ console.error("Usage: mclaw-diagnose <check|repair|doctor|reset|get_reset_task|install-openclaw|install-extension|download-resource> [options]");
2457
+ node_process.default.exit(1);
1651
2458
  }
1652
- default:
1653
- console.error("Usage: mclaw-diagnose <check|repair|reset|get_reset_task> [options]");
1654
- node_process.default.exit(1);
1655
2459
  }
2460
+ main().catch((err) => {
2461
+ const msg = err instanceof Error ? err.message : String(err);
2462
+ console.error(`Error: ${msg}`);
2463
+ node_process.default.exit(1);
2464
+ });
1656
2465
  //#endregion