@lark-apaas/openclaw-scripts-diagnose-cli 0.1.1-alpha.2 → 0.1.1-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +379 -12
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ node_fs = __toESM(node_fs);
30
30
  let node_path = require("node:path");
31
31
  node_path = __toESM(node_path);
32
32
  let node_child_process = require("node:child_process");
33
+ let node_crypto = require("node:crypto");
33
34
  //#region src/rule-engine/base.ts
34
35
  /** Abstract base class for all diagnose rules */
35
36
  var DiagnoseRule = class {
@@ -984,19 +985,385 @@ function runRepair(input) {
984
985
  }
985
986
  }
986
987
  //#endregion
988
+ //#region src/backup.ts
989
+ const BACKUP_PATH = "/home/gem/workspace/.force/openclaw/core-backup.json";
990
+ function runBackup(input) {
991
+ try {
992
+ const { configPath } = input;
993
+ try {
994
+ const validateOutput = shell("openclaw config validate --json");
995
+ if (!JSON.parse(validateOutput).valid) return {
996
+ success: false,
997
+ error: "config validation failed"
998
+ };
999
+ } catch (e) {
1000
+ return {
1001
+ success: false,
1002
+ error: "config validate command failed: " + e.message
1003
+ };
1004
+ }
1005
+ if (!fileExists(configPath)) return {
1006
+ success: false,
1007
+ error: "config file not found: " + configPath
1008
+ };
1009
+ const config = loadJSON5().parse(readFile(configPath));
1010
+ const backup = { _backup_meta: { created_at: (/* @__PURE__ */ new Date()).toISOString() } };
1011
+ if (config.agents) backup.agents = config.agents;
1012
+ if (config.bindings) backup.bindings = config.bindings;
1013
+ const feishu = config.channels?.feishu;
1014
+ if (feishu?.accounts) backup.channels = { feishu: { accounts: feishu.accounts } };
1015
+ const backupDir = node_path.default.dirname(BACKUP_PATH);
1016
+ if (!node_fs.default.existsSync(backupDir)) node_fs.default.mkdirSync(backupDir, { recursive: true });
1017
+ const tmpPath = BACKUP_PATH + ".tmp";
1018
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(backup, null, 2), "utf-8");
1019
+ node_fs.default.renameSync(tmpPath, BACKUP_PATH);
1020
+ return { success: true };
1021
+ } catch (e) {
1022
+ return {
1023
+ success: false,
1024
+ error: "backup failed: " + e.message
1025
+ };
1026
+ }
1027
+ }
1028
+ //#endregion
1029
+ //#region src/reset-async.ts
1030
+ /**
1031
+ * Start an async reset task: spawn a detached child process and return the taskId.
1032
+ *
1033
+ * The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
1034
+ */
1035
+ function startAsyncReset(ctxBase64) {
1036
+ const taskId = (0, node_crypto.randomUUID)();
1037
+ const resultFile = `/tmp/openclaw-reset-${taskId}.json`;
1038
+ const initial = {
1039
+ status: "running",
1040
+ step: 0,
1041
+ totalSteps: 9,
1042
+ progress: "初始化...",
1043
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1044
+ };
1045
+ const tmpPath = resultFile + ".tmp";
1046
+ const dir = node_path.default.dirname(resultFile);
1047
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
1048
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(initial), "utf-8");
1049
+ node_fs.default.renameSync(tmpPath, resultFile);
1050
+ const child = (0, node_child_process.spawn)(process.execPath, [
1051
+ process.argv[1],
1052
+ "reset",
1053
+ "--worker",
1054
+ `--task-id=${taskId}`,
1055
+ `--ctx=${ctxBase64}`
1056
+ ], {
1057
+ detached: true,
1058
+ stdio: "ignore"
1059
+ });
1060
+ child.on("error", (err) => {
1061
+ const failResult = {
1062
+ status: "failed",
1063
+ step: 0,
1064
+ totalSteps: 9,
1065
+ progress: "Worker process failed to start",
1066
+ error: err.message,
1067
+ startedAt: initial.startedAt,
1068
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
1069
+ };
1070
+ const errTmpPath = resultFile + ".tmp";
1071
+ node_fs.default.writeFileSync(errTmpPath, JSON.stringify(failResult));
1072
+ node_fs.default.renameSync(errTmpPath, resultFile);
1073
+ });
1074
+ child.unref();
1075
+ return { taskId };
1076
+ }
1077
+ //#endregion
1078
+ //#region src/reset.ts
1079
+ const STEPS = [
1080
+ "备份当前配置",
1081
+ "下载技术栈模板",
1082
+ "生成默认配置",
1083
+ "杀掉 openclaw 进程",
1084
+ "重装 openclaw",
1085
+ "合并核心备份配置",
1086
+ "复制启动脚本",
1087
+ "重装内置插件",
1088
+ "启动并验证"
1089
+ ];
1090
+ const TOTAL_STEPS = STEPS.length;
1091
+ const CORE_BACKUP_PATH = "/home/gem/workspace/.force/openclaw/core-backup.json";
1092
+ const TMP_RESET_DIR = "/tmp/openclaw-reset";
1093
+ /**
1094
+ * Atomically write the result file (write .tmp + rename).
1095
+ */
1096
+ function writeResultFile(resultFile, result) {
1097
+ const dir = node_path.default.dirname(resultFile);
1098
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
1099
+ const tmpPath = resultFile + ".tmp";
1100
+ node_fs.default.writeFileSync(tmpPath, JSON.stringify(result), "utf-8");
1101
+ node_fs.default.renameSync(tmpPath, resultFile);
1102
+ }
1103
+ /**
1104
+ * Update progress in the result file.
1105
+ */
1106
+ function updateProgress(resultFile, step, progress, startedAt) {
1107
+ writeResultFile(resultFile, {
1108
+ status: "running",
1109
+ step,
1110
+ totalSteps: TOTAL_STEPS,
1111
+ progress,
1112
+ startedAt
1113
+ });
1114
+ }
1115
+ /**
1116
+ * Mark the task as done.
1117
+ */
1118
+ function markDone(resultFile, startedAt) {
1119
+ writeResultFile(resultFile, {
1120
+ status: "done",
1121
+ step: TOTAL_STEPS,
1122
+ totalSteps: TOTAL_STEPS,
1123
+ progress: "重置完成",
1124
+ startedAt,
1125
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
1126
+ });
1127
+ }
1128
+ /**
1129
+ * Mark the task as failed.
1130
+ */
1131
+ function markFailed(resultFile, step, progress, error, startedAt) {
1132
+ writeResultFile(resultFile, {
1133
+ status: "failed",
1134
+ step,
1135
+ totalSteps: TOTAL_STEPS,
1136
+ progress,
1137
+ error,
1138
+ startedAt,
1139
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
1140
+ });
1141
+ }
1142
+ /**
1143
+ * Run the 9-step reset process. Called from the worker entry point.
1144
+ */
1145
+ function runReset(input, taskId, resultFile) {
1146
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1147
+ const { configPath, resetData } = input;
1148
+ const configDir = node_path.default.dirname(configPath);
1149
+ let currentStep = 0;
1150
+ process.on("uncaughtException", (err) => {
1151
+ const stepName = currentStep > 0 ? STEPS[currentStep - 1] : "初始化";
1152
+ markFailed(resultFile, currentStep, stepName, `uncaught exception: ${err.message}`, startedAt);
1153
+ process.exit(1);
1154
+ });
1155
+ process.on("unhandledRejection", (reason) => {
1156
+ const stepName = currentStep > 0 ? STEPS[currentStep - 1] : "初始化";
1157
+ markFailed(resultFile, currentStep, stepName, `unhandled rejection: ${reason}`, startedAt);
1158
+ process.exit(1);
1159
+ });
1160
+ try {
1161
+ currentStep = 1;
1162
+ updateProgress(resultFile, currentStep, STEPS[0], startedAt);
1163
+ if (fileExists(configPath)) {
1164
+ let maxN = 0;
1165
+ try {
1166
+ const files = node_fs.default.readdirSync(configDir);
1167
+ const bakPattern = /^openclaw\.json\.bak\.(\d+)$/;
1168
+ for (const f of files) {
1169
+ const match = f.match(bakPattern);
1170
+ if (match) {
1171
+ const n = parseInt(match[1], 10);
1172
+ if (n > maxN) maxN = n;
1173
+ }
1174
+ }
1175
+ } catch {}
1176
+ const bakPath = configPath + ".bak." + (maxN + 1);
1177
+ node_fs.default.copyFileSync(configPath, bakPath);
1178
+ }
1179
+ currentStep = 2;
1180
+ updateProgress(resultFile, currentStep, STEPS[1], startedAt);
1181
+ if (!node_fs.default.existsSync(TMP_RESET_DIR)) node_fs.default.mkdirSync(TMP_RESET_DIR, { recursive: true });
1182
+ const zipPath = node_path.default.join(TMP_RESET_DIR, "template.zip");
1183
+ const zipSource = resetData.zipDownloadURL;
1184
+ if (zipSource.startsWith("http://") || zipSource.startsWith("https://")) shell(`curl -sSL -o '${zipPath}' '${zipSource}'`, 12e4);
1185
+ else shell(`cp '${zipSource}' '${zipPath}'`, 1e4);
1186
+ shell(`unzip -o '${zipPath}' -d '${TMP_RESET_DIR}'`, 6e4);
1187
+ const srcDir = findSrcDir(TMP_RESET_DIR);
1188
+ currentStep = 3;
1189
+ updateProgress(resultFile, currentStep, STEPS[2], startedAt);
1190
+ const srcConfigPath = node_path.default.join(srcDir, "openclaw.json");
1191
+ if (!fileExists(srcConfigPath)) throw new Error("template openclaw.json not found in downloaded zip at " + srcConfigPath);
1192
+ let configContent = node_fs.default.readFileSync(srcConfigPath, "utf-8");
1193
+ for (const [placeholder, value] of Object.entries(resetData.templateVars)) configContent = configContent.split(placeholder).join(value);
1194
+ node_fs.default.writeFileSync(configPath, configContent, "utf-8");
1195
+ currentStep = 4;
1196
+ updateProgress(resultFile, currentStep, STEPS[3], startedAt);
1197
+ try {
1198
+ shell("pkill -f openclaw-gateway || true", 5e3);
1199
+ } catch {}
1200
+ shell("sleep 2", 5e3);
1201
+ currentStep = 5;
1202
+ updateProgress(resultFile, currentStep, STEPS[4], startedAt);
1203
+ const JSON5 = loadJSON5();
1204
+ const version = JSON5.parse(node_fs.default.readFileSync(srcConfigPath, "utf-8")).meta?.lastTouchedVersion;
1205
+ if (version) shell(`npm install -g @anthropic-ai/openclaw@${version}`, 3e5);
1206
+ else shell("npm install -g @anthropic-ai/openclaw@latest", 3e5);
1207
+ currentStep = 6;
1208
+ updateProgress(resultFile, currentStep, STEPS[5], startedAt);
1209
+ if (fileExists(CORE_BACKUP_PATH)) {
1210
+ const backup = JSON.parse(node_fs.default.readFileSync(CORE_BACKUP_PATH, "utf-8"));
1211
+ const mergeConfig = JSON5.parse(node_fs.default.readFileSync(configPath, "utf-8"));
1212
+ if (backup.agents) mergeConfig.agents = backup.agents;
1213
+ if (backup.bindings) mergeConfig.bindings = backup.bindings;
1214
+ const backupFeishu = backup.channels?.feishu;
1215
+ if (backupFeishu?.accounts) {
1216
+ if (!mergeConfig.channels) mergeConfig.channels = {};
1217
+ const ch = mergeConfig.channels;
1218
+ if (!ch.feishu) ch.feishu = {};
1219
+ const feishu = ch.feishu;
1220
+ feishu.accounts = backupFeishu.accounts;
1221
+ }
1222
+ node_fs.default.writeFileSync(configPath, JSON.stringify(mergeConfig, null, 2), "utf-8");
1223
+ }
1224
+ currentStep = 7;
1225
+ updateProgress(resultFile, currentStep, STEPS[6], startedAt);
1226
+ const srcScriptsDir = node_path.default.join(srcDir, "scripts");
1227
+ const targetScriptsDir = node_path.default.join(configDir, "scripts");
1228
+ if (node_fs.default.existsSync(srcScriptsDir)) {
1229
+ if (!node_fs.default.existsSync(targetScriptsDir)) node_fs.default.mkdirSync(targetScriptsDir, { recursive: true });
1230
+ shell(`cp -r '${srcScriptsDir}'/* '${targetScriptsDir}/'`, 1e4);
1231
+ }
1232
+ currentStep = 8;
1233
+ updateProgress(resultFile, currentStep, STEPS[7], startedAt);
1234
+ try {
1235
+ shell("openclaw plugins install --all", 3e5);
1236
+ } catch (e) {}
1237
+ currentStep = 9;
1238
+ updateProgress(resultFile, currentStep, STEPS[8], startedAt);
1239
+ if (resetData.secretsContent && input.vars.secretsFilePath) writeFile(input.vars.secretsFilePath, resetData.secretsContent);
1240
+ if (resetData.providerKeyContent && input.vars.providerFilePath) writeFile(input.vars.providerFilePath, resetData.providerKeyContent);
1241
+ const restartScript = node_path.default.join(configDir, "scripts", "restart.sh");
1242
+ if (fileExists(restartScript)) shell(`bash '${restartScript}'`, 3e4);
1243
+ try {
1244
+ node_fs.default.rmSync(TMP_RESET_DIR, {
1245
+ recursive: true,
1246
+ force: true
1247
+ });
1248
+ } catch {}
1249
+ markDone(resultFile, startedAt);
1250
+ } catch (e) {
1251
+ const stepName = currentStep > 0 ? STEPS[currentStep - 1] : "初始化";
1252
+ markFailed(resultFile, currentStep, stepName, e.message, startedAt);
1253
+ process.exit(1);
1254
+ }
1255
+ }
1256
+ /**
1257
+ * Find the source directory within the extracted zip.
1258
+ * Looks for openclaw.json in TMP_RESET_DIR or one level deep.
1259
+ */
1260
+ function findSrcDir(baseDir) {
1261
+ if (node_fs.default.existsSync(node_path.default.join(baseDir, "openclaw.json"))) return baseDir;
1262
+ const entries = node_fs.default.readdirSync(baseDir, { withFileTypes: true });
1263
+ for (const entry of entries) if (entry.isDirectory()) {
1264
+ const candidate = node_path.default.join(baseDir, entry.name);
1265
+ if (node_fs.default.existsSync(node_path.default.join(candidate, "openclaw.json"))) return candidate;
1266
+ }
1267
+ return baseDir;
1268
+ }
1269
+ //#endregion
1270
+ //#region src/get-reset-task.ts
1271
+ /**
1272
+ * Long-poll for a reset task result.
1273
+ * Reads the result file every 1s for up to 30s.
1274
+ * Returns immediately on terminal states (done/failed).
1275
+ */
1276
+ function getResetTask(taskId) {
1277
+ const resultFile = `/tmp/openclaw-reset-${taskId}.json`;
1278
+ const deadline = Date.now() + 3e4;
1279
+ while (Date.now() < deadline) {
1280
+ if (!node_fs.default.existsSync(resultFile)) {
1281
+ sleepSync(1e3);
1282
+ continue;
1283
+ }
1284
+ try {
1285
+ const content = node_fs.default.readFileSync(resultFile, "utf-8");
1286
+ const result = JSON.parse(content);
1287
+ if (result.status === "done" || result.status === "failed") return result;
1288
+ } catch {}
1289
+ sleepSync(1e3);
1290
+ }
1291
+ if (node_fs.default.existsSync(resultFile)) try {
1292
+ return JSON.parse(node_fs.default.readFileSync(resultFile, "utf-8"));
1293
+ } catch {}
1294
+ return {
1295
+ status: "running",
1296
+ progress: "等待中..."
1297
+ };
1298
+ }
1299
+ /**
1300
+ * Synchronous sleep using Atomics.wait on a shared buffer.
1301
+ */
1302
+ function sleepSync(ms) {
1303
+ const buf = new SharedArrayBuffer(4);
1304
+ const arr = new Int32Array(buf);
1305
+ Atomics.wait(arr, 0, 0, ms);
1306
+ }
1307
+ //#endregion
987
1308
  //#region src/index.ts
988
1309
  const args = node_process.default.argv.slice(2);
989
1310
  const mode = args.find((a) => !a.startsWith("--"));
990
- const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
991
- if (!mode || !["check", "repair"].includes(mode)) {
992
- console.error("Usage: mclaw-diagnose <check|repair> --ctx=<base64>");
993
- node_process.default.exit(1);
994
- }
995
- if (!ctx) {
996
- console.error("Error: --ctx=<base64> is required");
997
- node_process.default.exit(1);
998
- }
999
- const input = JSON.parse(Buffer.from(ctx, "base64").toString("utf-8"));
1000
- if (mode === "check") console.log(JSON.stringify(runCheck(input)));
1001
- else console.log(JSON.stringify(runRepair(input)));
1311
+ switch (mode) {
1312
+ case "check":
1313
+ case "repair": {
1314
+ const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
1315
+ if (!ctx) {
1316
+ console.error("Error: --ctx=<base64> is required");
1317
+ node_process.default.exit(1);
1318
+ }
1319
+ const input = JSON.parse(Buffer.from(ctx, "base64").toString("utf-8"));
1320
+ if (mode === "check") console.log(JSON.stringify(runCheck(input)));
1321
+ else console.log(JSON.stringify(runRepair(input)));
1322
+ break;
1323
+ }
1324
+ case "backup": {
1325
+ const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
1326
+ if (!ctx) {
1327
+ console.error("Error: --ctx=<base64> is required");
1328
+ node_process.default.exit(1);
1329
+ }
1330
+ const input = JSON.parse(Buffer.from(ctx, "base64").toString("utf-8"));
1331
+ console.log(JSON.stringify(runBackup(input)));
1332
+ break;
1333
+ }
1334
+ case "reset":
1335
+ if (args.includes("--async")) {
1336
+ const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
1337
+ if (!ctx) {
1338
+ console.error("Error: --ctx=<base64> is required");
1339
+ node_process.default.exit(1);
1340
+ }
1341
+ console.log(JSON.stringify(startAsyncReset(ctx)));
1342
+ } else if (args.includes("--worker")) {
1343
+ const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
1344
+ const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
1345
+ if (!ctx || !taskId) {
1346
+ console.error("Error: --ctx=<base64> and --task-id=<id> are required for worker");
1347
+ node_process.default.exit(1);
1348
+ }
1349
+ const resultFile = `/tmp/openclaw-reset-${taskId}.json`;
1350
+ runReset(JSON.parse(Buffer.from(ctx, "base64").toString("utf-8")), taskId, resultFile);
1351
+ } else {
1352
+ console.error("Usage: reset --async --ctx=<base64> | reset --worker --task-id=<id> --ctx=<base64>");
1353
+ node_process.default.exit(1);
1354
+ }
1355
+ break;
1356
+ case "get_reset_task": {
1357
+ const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
1358
+ if (!taskId) {
1359
+ console.error("Error: --task-id=<id> is required");
1360
+ node_process.default.exit(1);
1361
+ }
1362
+ console.log(JSON.stringify(getResetTask(taskId)));
1363
+ break;
1364
+ }
1365
+ default:
1366
+ console.error("Usage: mclaw-diagnose <check|repair|backup|reset|get_reset_task> [options]");
1367
+ node_process.default.exit(1);
1368
+ }
1002
1369
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/openclaw-scripts-diagnose-cli",
3
- "version": "0.1.1-alpha.2",
3
+ "version": "0.1.1-alpha.3",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {