@lark-apaas/openclaw-scripts-diagnose-cli 0.1.1-alpha.25 → 0.1.1-alpha.26

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 +176 -83
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -365,6 +365,74 @@ ConfigSyntaxRule = __decorate([Rule({
365
365
  repairMode: "ai"
366
366
  })], ConfigSyntaxRule);
367
367
  //#endregion
368
+ //#region src/rules/template-vars-unreplaced.ts
369
+ /**
370
+ * Placeholder format used by miaoda-openclaw-template and Go-side templateVars,
371
+ * e.g. `$$__FEISHU_APP_ID__`. Double underscores on both sides act as a natural
372
+ * boundary so split-join replacement can't accidentally overlap between keys.
373
+ */
374
+ const PLACEHOLDER_RE = /\$\$__[A-Z0-9_]+__/g;
375
+ let TemplateVarsUnreplacedRule = class TemplateVarsUnreplacedRule extends DiagnoseRule {
376
+ validate(ctx) {
377
+ const found = /* @__PURE__ */ new Set();
378
+ collectPlaceholders(ctx.config, found);
379
+ if (found.size === 0) return { pass: true };
380
+ return {
381
+ pass: false,
382
+ message: "存在未替换的模板占位符: " + [...found].sort().join(", ")
383
+ };
384
+ }
385
+ repair(ctx) {
386
+ const map = ctx.templateVars;
387
+ if (!map || Object.keys(map).length === 0) return;
388
+ replaceInPlace(ctx.config, Object.entries(map));
389
+ }
390
+ };
391
+ TemplateVarsUnreplacedRule = __decorate([Rule({
392
+ key: "template_vars_unreplaced",
393
+ dependsOn: ["config_syntax_check"],
394
+ repairMode: "standard"
395
+ })], TemplateVarsUnreplacedRule);
396
+ function collectPlaceholders(value, found) {
397
+ if (typeof value === "string") {
398
+ const matches = value.match(PLACEHOLDER_RE);
399
+ if (matches) for (const m of matches) found.add(m);
400
+ return;
401
+ }
402
+ if (Array.isArray(value)) {
403
+ for (const v of value) collectPlaceholders(v, found);
404
+ return;
405
+ }
406
+ if (value && typeof value === "object") for (const v of Object.values(value)) collectPlaceholders(v, found);
407
+ }
408
+ function replaceInPlace(value, entries) {
409
+ if (Array.isArray(value)) {
410
+ for (let i = 0; i < value.length; i++) {
411
+ const el = value[i];
412
+ if (typeof el === "string") value[i] = applyVars(el, entries);
413
+ else replaceInPlace(el, entries);
414
+ }
415
+ return;
416
+ }
417
+ if (value && typeof value === "object") {
418
+ const obj = value;
419
+ for (const key of Object.keys(obj)) {
420
+ const v = obj[key];
421
+ if (typeof v === "string") obj[key] = applyVars(v, entries);
422
+ else replaceInPlace(v, entries);
423
+ }
424
+ }
425
+ }
426
+ /** Split-join replacement — matches the algorithm in reset.ts:120 and avoids regex-escaping `$$`. */
427
+ function applyVars(str, entries) {
428
+ let out = str;
429
+ for (const [placeholder, value] of entries) {
430
+ if (!value) continue;
431
+ if (out.includes(placeholder)) out = out.split(placeholder).join(value);
432
+ }
433
+ return out;
434
+ }
435
+ //#endregion
368
436
  //#region src/rules/model-provider.ts
369
437
  var _ModelProviderRule;
370
438
  let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
@@ -877,74 +945,6 @@ SecretsRule = __decorate([Rule({
877
945
  skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaSecretProvider
878
946
  })], SecretsRule);
879
947
  //#endregion
880
- //#region src/rules/template-vars-unreplaced.ts
881
- /**
882
- * Placeholder format used by miaoda-openclaw-template and Go-side templateVars,
883
- * e.g. `$$__FEISHU_APP_ID__`. Double underscores on both sides act as a natural
884
- * boundary so split-join replacement can't accidentally overlap between keys.
885
- */
886
- const PLACEHOLDER_RE = /\$\$__[A-Z0-9_]+__/g;
887
- let TemplateVarsUnreplacedRule = class TemplateVarsUnreplacedRule extends DiagnoseRule {
888
- validate(ctx) {
889
- const found = /* @__PURE__ */ new Set();
890
- collectPlaceholders(ctx.config, found);
891
- if (found.size === 0) return { pass: true };
892
- return {
893
- pass: false,
894
- message: "存在未替换的模板占位符: " + [...found].sort().join(", ")
895
- };
896
- }
897
- repair(ctx) {
898
- const map = ctx.templateVars;
899
- if (!map || Object.keys(map).length === 0) return;
900
- replaceInPlace(ctx.config, Object.entries(map));
901
- }
902
- };
903
- TemplateVarsUnreplacedRule = __decorate([Rule({
904
- key: "template_vars_unreplaced",
905
- dependsOn: ["config_syntax_check"],
906
- repairMode: "standard"
907
- })], TemplateVarsUnreplacedRule);
908
- function collectPlaceholders(value, found) {
909
- if (typeof value === "string") {
910
- const matches = value.match(PLACEHOLDER_RE);
911
- if (matches) for (const m of matches) found.add(m);
912
- return;
913
- }
914
- if (Array.isArray(value)) {
915
- for (const v of value) collectPlaceholders(v, found);
916
- return;
917
- }
918
- if (value && typeof value === "object") for (const v of Object.values(value)) collectPlaceholders(v, found);
919
- }
920
- function replaceInPlace(value, entries) {
921
- if (Array.isArray(value)) {
922
- for (let i = 0; i < value.length; i++) {
923
- const el = value[i];
924
- if (typeof el === "string") value[i] = applyVars(el, entries);
925
- else replaceInPlace(el, entries);
926
- }
927
- return;
928
- }
929
- if (value && typeof value === "object") {
930
- const obj = value;
931
- for (const key of Object.keys(obj)) {
932
- const v = obj[key];
933
- if (typeof v === "string") obj[key] = applyVars(v, entries);
934
- else replaceInPlace(v, entries);
935
- }
936
- }
937
- }
938
- /** Split-join replacement — matches the algorithm in reset.ts:120 and avoids regex-escaping `$$`. */
939
- function applyVars(str, entries) {
940
- let out = str;
941
- for (const [placeholder, value] of entries) {
942
- if (!value) continue;
943
- if (out.includes(placeholder)) out = out.split(placeholder).join(value);
944
- }
945
- return out;
946
- }
947
- //#endregion
948
948
  //#region src/rules/cleanup-install-backup-dirs.ts
949
949
  const DIR_PREFIX = ".openclaw-install-";
950
950
  function resolveExtensionsDir(configPath) {
@@ -962,7 +962,7 @@ function findLeftoverDirs(extensionsDir) {
962
962
  }
963
963
  let CleanupInstallBackupDirsRule = class CleanupInstallBackupDirsRule extends DiagnoseRule {
964
964
  validate(ctx) {
965
- const configPath = ctx.config.__configPath;
965
+ const { configPath } = ctx;
966
966
  if (!configPath) return { pass: true };
967
967
  const dirs = findLeftoverDirs(resolveExtensionsDir(configPath));
968
968
  if (dirs.length === 0) return { pass: true };
@@ -972,7 +972,7 @@ let CleanupInstallBackupDirsRule = class CleanupInstallBackupDirsRule extends Di
972
972
  };
973
973
  }
974
974
  repair(ctx) {
975
- const configPath = ctx.config.__configPath;
975
+ const { configPath } = ctx;
976
976
  if (!configPath) return;
977
977
  const dirs = findLeftoverDirs(resolveExtensionsDir(configPath));
978
978
  const failures = [];
@@ -1322,6 +1322,54 @@ const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-s
1322
1322
  const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
1323
1323
  //#endregion
1324
1324
  //#region src/logger.ts
1325
+ /**
1326
+ * Shared CLI log file. Every log line the CLI emits — whether through
1327
+ * `console.error` (rules, helpers, errors) or through the per-task
1328
+ * `makeLogger` (reset worker) — is tee'd here so operators have a single
1329
+ * file to tail when diagnosing a sandbox.
1330
+ *
1331
+ * `/tmp` is ephemeral on sandbox restart; we rely on that for rotation
1332
+ * (no size-based rotation implemented).
1333
+ */
1334
+ const CLI_LOG_FILE = "/tmp/openclaw-diagnose/cli.log";
1335
+ /** Append one line to the shared cli.log. Swallows any fs error —
1336
+ * logging must never break the business flow. */
1337
+ function appendCliLog(line) {
1338
+ try {
1339
+ const dir = node_path.default.dirname(CLI_LOG_FILE);
1340
+ if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
1341
+ node_fs.default.appendFileSync(CLI_LOG_FILE, line);
1342
+ } catch {}
1343
+ }
1344
+ let stderrMirrorInstalled = false;
1345
+ /**
1346
+ * Install a process-wide `console.error` interceptor that mirrors each
1347
+ * line to BOTH the original stderr AND cli.log. Call once at CLI entry
1348
+ * before any subcommand dispatch; idempotent.
1349
+ *
1350
+ * Why console.error and not console.log: the CLI's stdout carries the
1351
+ * structured JSON result protocol consumed by sandbox_console and other
1352
+ * callers — any log line on stdout would corrupt JSON parsing. Rules,
1353
+ * helpers, and error paths therefore must route debug output through
1354
+ * console.error (stderr).
1355
+ */
1356
+ function installStderrMirror() {
1357
+ if (stderrMirrorInstalled) return;
1358
+ stderrMirrorInstalled = true;
1359
+ const original = console.error.bind(console);
1360
+ console.error = (...args) => {
1361
+ original(...args);
1362
+ const body = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
1363
+ appendCliLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${body}\n`);
1364
+ };
1365
+ }
1366
+ function safeStringify(v) {
1367
+ try {
1368
+ return JSON.stringify(v);
1369
+ } catch {
1370
+ return String(v);
1371
+ }
1372
+ }
1325
1373
  function makeLogger(logFile) {
1326
1374
  try {
1327
1375
  const dir = node_path.default.dirname(logFile);
@@ -1332,9 +1380,45 @@ function makeLogger(logFile) {
1332
1380
  try {
1333
1381
  node_fs.default.appendFileSync(logFile, line);
1334
1382
  } catch {}
1383
+ appendCliLog(line);
1335
1384
  };
1336
1385
  }
1337
1386
  //#endregion
1387
+ //#region src/fs-utils.ts
1388
+ /**
1389
+ * Rename src → dst, falling back to `mv` (which handles cross-device copy)
1390
+ * when the kernel returns EXDEV.
1391
+ *
1392
+ * Sandbox filesystems can put sibling paths on different "devices" from
1393
+ * rename(2)'s point of view: bind mounts, overlayfs copy-up, and
1394
+ * mount-point children inside a single directory all trip EXDEV. Seen in
1395
+ * production when reset's atomic swap did
1396
+ * /home/gem/.npm-global/lib/node_modules/openclaw → openclaw.bak
1397
+ * and the openclaw subdir was a bind-mounted volume.
1398
+ *
1399
+ * Behavior:
1400
+ * - Happy path hits rename(2) — atomic, single syscall, microseconds.
1401
+ * - EXDEV path shells out to `mv`, which does rename() then copy+unlink
1402
+ * on failure. Non-atomic but correct; callers already have rollback
1403
+ * logic (install-openclaw restores from .bak) so loss of atomicity
1404
+ * only matters if the process dies mid-copy, and that's survivable.
1405
+ * - Any other error (ENOENT, EACCES, EBUSY...) rethrows as-is so callers
1406
+ * see the real problem instead of a misleading `mv` fallback failure.
1407
+ */
1408
+ function moveSafe(src, dst) {
1409
+ try {
1410
+ node_fs.default.renameSync(src, dst);
1411
+ } catch (e) {
1412
+ if (e?.code !== "EXDEV") throw e;
1413
+ (0, node_child_process.execSync)(`mv ${shellQuote(src)} ${shellQuote(dst)}`, { stdio: "ignore" });
1414
+ }
1415
+ }
1416
+ /** POSIX single-quote shell escape. Paths with embedded quotes are rare but
1417
+ * the token-file path conventions in sandboxes don't guarantee cleanliness. */
1418
+ function shellQuote(s) {
1419
+ return `'${s.replace(/'/g, `'\\''`)}'`;
1420
+ }
1421
+ //#endregion
1338
1422
  //#region src/reset-async.ts
1339
1423
  /**
1340
1424
  * Start an async reset task: spawn a detached child process and return the taskId.
@@ -1357,7 +1441,7 @@ function startAsyncReset(ctxBase64) {
1357
1441
  const dir = node_path.default.dirname(resultFile);
1358
1442
  if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
1359
1443
  node_fs.default.writeFileSync(tmpPath, JSON.stringify(initial), "utf-8");
1360
- node_fs.default.renameSync(tmpPath, resultFile);
1444
+ moveSafe(tmpPath, resultFile);
1361
1445
  const child = (0, node_child_process.spawn)(process.execPath, [
1362
1446
  process.argv[1],
1363
1447
  "reset",
@@ -1381,7 +1465,7 @@ function startAsyncReset(ctxBase64) {
1381
1465
  };
1382
1466
  const errTmpPath = resultFile + ".tmp";
1383
1467
  node_fs.default.writeFileSync(errTmpPath, JSON.stringify(failResult));
1384
- node_fs.default.renameSync(errTmpPath, resultFile);
1468
+ moveSafe(errTmpPath, resultFile);
1385
1469
  });
1386
1470
  child.unref();
1387
1471
  log(`spawned worker pid=${child.pid}`);
@@ -1389,10 +1473,16 @@ function startAsyncReset(ctxBase64) {
1389
1473
  }
1390
1474
  //#endregion
1391
1475
  //#region src/oss/fetchManifest.ts
1476
+ const MANIFEST_PREFIX = "builtin/manifests/openclaw/recommended/";
1477
+ const MANIFEST_SUFFIX = ".json";
1392
1478
  async function fetchManifest(ossFileMap, tag) {
1393
- const key = `builtin/manifests/openclaw/recommended/${tag}.json`;
1479
+ const key = `${MANIFEST_PREFIX}${tag}${MANIFEST_SUFFIX}`;
1394
1480
  const url = ossFileMap[key];
1395
- if (!url) throw new Error(`manifest signed URL missing for ${key}`);
1481
+ if (!url) {
1482
+ const available = Object.keys(ossFileMap).filter((k) => k.startsWith(MANIFEST_PREFIX) && k.endsWith(MANIFEST_SUFFIX)).map((k) => k.slice(39, -5));
1483
+ const availStr = available.length ? available.join(", ") : "(none)";
1484
+ 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.`);
1485
+ }
1396
1486
  const res = await fetch(url);
1397
1487
  if (!res.ok) throw new Error(`fetch manifest failed: HTTP ${res.status} ${res.statusText}`);
1398
1488
  return await res.json();
@@ -1430,7 +1520,7 @@ async function downloadWithCache(pkg, ossFileMap, opts = {}) {
1430
1520
  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.`);
1431
1521
  } else throw new Error(`integrity mismatch for ${pkg.ossKey}: expected ${expected} got ${actual}`);
1432
1522
  }
1433
- node_fs.default.renameSync(tmpFile, destFile);
1523
+ moveSafe(tmpFile, destFile);
1434
1524
  return destFile;
1435
1525
  } catch (e) {
1436
1526
  try {
@@ -1453,7 +1543,7 @@ async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
1453
1543
  force: true
1454
1544
  });
1455
1545
  const hadExisting = node_fs.default.existsSync(targetDir);
1456
- if (hadExisting) node_fs.default.renameSync(targetDir, bakDir);
1546
+ if (hadExisting) moveSafe(targetDir, bakDir);
1457
1547
  try {
1458
1548
  node_fs.default.mkdirSync(targetDir, { recursive: true });
1459
1549
  (0, node_child_process.execSync)(`tar -xzf '${tarball}' -C '${targetDir}' --strip-components=1`, { stdio: "ignore" });
@@ -1465,7 +1555,7 @@ async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
1465
1555
  force: true
1466
1556
  });
1467
1557
  } catch {}
1468
- if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.renameSync(bakDir, targetDir);
1558
+ if (hadExisting && node_fs.default.existsSync(bakDir)) moveSafe(bakDir, targetDir);
1469
1559
  throw e;
1470
1560
  }
1471
1561
  if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
@@ -1540,7 +1630,7 @@ function updatePluginInstalls(configPath, installedPkgs) {
1540
1630
  } else skipped++;
1541
1631
  const tmpPath = configPath + ".installs-tmp";
1542
1632
  node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
1543
- node_fs.default.renameSync(tmpPath, configPath);
1633
+ moveSafe(tmpPath, configPath);
1544
1634
  console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
1545
1635
  }
1546
1636
  function installOne(pkg, tarball, homeBase) {
@@ -1566,8 +1656,8 @@ function installOne(pkg, tarball, homeBase) {
1566
1656
  throw e;
1567
1657
  }
1568
1658
  const hadOld = node_fs.default.existsSync(destDir);
1569
- if (hadOld) node_fs.default.renameSync(destDir, oldDir);
1570
- node_fs.default.renameSync(stagingDir, destDir);
1659
+ if (hadOld) moveSafe(destDir, oldDir);
1660
+ moveSafe(stagingDir, destDir);
1571
1661
  if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
1572
1662
  recursive: true,
1573
1663
  force: true
@@ -1634,7 +1724,7 @@ function writeResultFile(resultFile, result) {
1634
1724
  if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
1635
1725
  const tmpPath = resultFile + ".tmp";
1636
1726
  node_fs.default.writeFileSync(tmpPath, JSON.stringify(result), "utf-8");
1637
- node_fs.default.renameSync(tmpPath, resultFile);
1727
+ moveSafe(tmpPath, resultFile);
1638
1728
  }
1639
1729
  function updateProgress(resultFile, step, startedAt) {
1640
1730
  writeResultFile(resultFile, {
@@ -1946,7 +2036,8 @@ async function runReset(input, taskId, resultFile) {
1946
2036
  process.exit(1);
1947
2037
  }
1948
2038
  let openclawTag;
1949
- try {
2039
+ if (resetData.openclawTag) openclawTag = resetData.openclawTag;
2040
+ else try {
1950
2041
  openclawTag = getOpenclawTagFromOssFileMap(ossFileMap);
1951
2042
  } catch (e) {
1952
2043
  const err = e.message;
@@ -2340,7 +2431,8 @@ function buildResetInput(raw, configPathOverride) {
2340
2431
  secretsContent: ctx.secrets.secretsContent,
2341
2432
  providerKeyContent: ctx.secrets.providerKeyContent,
2342
2433
  coreBackup: ctx.reset.coreBackup,
2343
- ossFileMap: ctx.install.ossFileMap
2434
+ ossFileMap: ctx.install.ossFileMap,
2435
+ openclawTag: ctx.install.openclawTag
2344
2436
  }
2345
2437
  };
2346
2438
  }
@@ -2400,6 +2492,7 @@ function getMultiFlag(args, name) {
2400
2492
  return args.filter((a) => a.startsWith(prefix)).map((a) => a.slice(prefix.length));
2401
2493
  }
2402
2494
  async function main() {
2495
+ installStderrMirror();
2403
2496
  switch (mode) {
2404
2497
  case "check":
2405
2498
  case "repair": {
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.25",
3
+ "version": "0.1.1-alpha.26",
4
4
  "description": "CLI for OpenClaw config diagnose and repair with JSON5 support",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {