refactor: split slack/discord/session maintenance helpers

This commit is contained in:
Peter Steinberger
2026-03-02 23:07:12 +00:00
parent 3043e68dfa
commit 7eda632324
16 changed files with 983 additions and 749 deletions

View File

@@ -0,0 +1,39 @@
import fs from "node:fs/promises";
/**
* Homebrew Cellar paths (e.g. /opt/homebrew/Cellar/node/25.7.0/bin/node)
* break when Homebrew upgrades Node and removes the old version directory.
* Resolve these to a stable Homebrew-managed path that survives upgrades:
* - Default formula "node": <prefix>/opt/node/bin/node or <prefix>/bin/node
* - Versioned formula "node@22": <prefix>/opt/node@22/bin/node (keg-only)
*/
export async function resolveStableNodePath(nodePath: string): Promise<string> {
const cellarMatch = nodePath.match(/^(.+?)\/Cellar\/([^/]+)\/[^/]+\/bin\/node$/);
if (!cellarMatch) {
return nodePath;
}
const prefix = cellarMatch[1]; // e.g. /opt/homebrew
const formula = cellarMatch[2]; // e.g. "node" or "node@22"
// Try the Homebrew opt symlink first — works for both default and versioned formulas.
const optPath = `${prefix}/opt/${formula}/bin/node`;
try {
await fs.access(optPath);
return optPath;
} catch {
// fall through
}
// For the default "node" formula, also try the direct bin symlink.
if (formula === "node") {
const binPath = `${prefix}/bin/node`;
try {
await fs.access(binPath);
return binPath;
} catch {
// fall through
}
}
return nodePath;
}

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { withEnvAsync } from "../test-utils/env.js";
import { pathExists } from "../utils.js";
import { resolveStableNodePath } from "./stable-node-path.js";
import { runGatewayUpdate } from "./update-runner.js";
type CommandResponse = { stdout?: string; stderr?: string; code?: number | null };
@@ -49,7 +50,7 @@ describe("runGatewayUpdate", () => {
// Shared fixtureRoot cleaned up in afterAll.
});
function createStableTagRunner(params: {
async function createStableTagRunner(params: {
stableTag: string;
uiIndexPath: string;
onDoctor?: () => Promise<void>;
@@ -57,7 +58,8 @@ describe("runGatewayUpdate", () => {
}) {
const calls: string[] = [];
let uiBuildCount = 0;
const doctorKey = `${process.execPath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`;
const doctorNodePath = await resolveStableNodePath(process.execPath);
const doctorKey = `${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`;
const runCommand = async (argv: string[]) => {
const key = argv.join(" ");
@@ -287,15 +289,15 @@ describe("runGatewayUpdate", () => {
await setupUiIndex();
const stableTag = "v1.0.1-1";
const betaTag = "v1.0.0-beta.2";
const doctorNodePath = await resolveStableNodePath(process.execPath);
const { runner, calls } = createRunner({
...buildStableTagResponses(stableTag, { additionalTags: [betaTag] }),
"pnpm install": { stdout: "" },
"pnpm build": { stdout: "" },
"pnpm ui:build": { stdout: "" },
[`${process.execPath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`]:
{
stdout: "",
},
[`${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`]: {
stdout: "",
},
});
const result = await runWithRunner(runner, { channel: "beta" });
@@ -544,7 +546,7 @@ describe("runGatewayUpdate", () => {
const uiIndexPath = await setupUiIndex();
const stableTag = "v1.0.1-1";
const { runCommand, calls, doctorKey, getUiBuildCount } = createStableTagRunner({
const { runCommand, calls, doctorKey, getUiBuildCount } = await createStableTagRunner({
stableTag,
uiIndexPath,
onUiBuild: async (count) => {
@@ -567,7 +569,7 @@ describe("runGatewayUpdate", () => {
const uiIndexPath = await setupUiIndex();
const stableTag = "v1.0.1-1";
const { runCommand } = createStableTagRunner({
const { runCommand } = await createStableTagRunner({
stableTag,
uiIndexPath,
onUiBuild: async (count) => {

View File

@@ -10,6 +10,7 @@ import { detectPackageManager as detectPackageManagerImpl } from "./detect-packa
import { readPackageName, readPackageVersion } from "./package-json.js";
import { normalizePackageTagInput } from "./package-tag.js";
import { trimLogTail } from "./restart-sentinel.js";
import { resolveStableNodePath } from "./stable-node-path.js";
import {
channelToNpmTag,
DEFAULT_PACKAGE_CHANNEL,
@@ -766,7 +767,8 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
// Use --fix so that doctor auto-strips unknown config keys introduced by
// schema changes between versions, preventing a startup validation crash.
const doctorArgv = [process.execPath, doctorEntry, "doctor", "--non-interactive", "--fix"];
const doctorNodePath = await resolveStableNodePath(process.execPath);
const doctorArgv = [doctorNodePath, doctorEntry, "doctor", "--non-interactive", "--fix"];
const doctorStep = await runStep(
step("openclaw doctor", doctorArgv, gitRoot, { OPENCLAW_UPDATE_IN_PROGRESS: "1" }),
);