From 54ee12e7599b852e30a6504d2ac09f4d1057a674 Mon Sep 17 00:00:00 2001 From: kumarabhirup Date: Thu, 12 Feb 2026 12:19:49 -0800 Subject: [PATCH] Ironclaw rename: update CLI binary references, fix Next.js invocation, harden package resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive update to complete the openclaw → ironclaw CLI rename across the codebase, fix build/runtime issues, and add test coverage for infra modules. CLI binary rename (openclaw → ironclaw): - Update DEFAULT_CLI_NAME and all argv parsing to recognize "ironclaw" binary - Extend package name sets (CORE_PACKAGE_NAMES, ALL_PACKAGE_NAMES) to include both "ironclaw" and "openclaw" for backward compatibility - Update NPM registry URL to fetch from ironclaw package - Update gateway lock detection, port listener classification, and launchd/systemd service scanning to recognize ironclaw-prefixed services and binaries - Update daemon inspect markers and legacy detection for ironclaw - Update voice-call extension core-bridge to resolve ironclaw package root - Fix install instructions in embeddings error messages (npm i -g ironclaw@latest) Web app / Next.js fixes: - Replace fragile `npx next` invocations with direct `node next-bin` resolution to avoid broken pnpm virtual-store symlinks in global installs - Add resolveNextBin() helper that resolves apps/web/node_modules/next directly Infra hardening: - Workspace templates: compute both source and dist fallback paths for template directory resolution (fixes templates not found in bundled builds) - Control UI assets: recognize both "openclaw" and "ironclaw" package names - Update-check, update-runner, update-cli: normalize ironclaw@ tag prefixes New tests: - Add openclaw-root.test.ts, ports-format.test.ts, update-global.test.ts - Add workspace-templates.test.ts and control-ui-assets.test.ts coverage - Add argv.test.ts coverage for ironclaw binary detection Test fixes (28 failures → 0): - Update all test assertions expecting "openclaw" CLI command output to "ironclaw" - Fix version.test.ts package name from "openclaw" to "ironclaw" - Fix camera/canvas temp path patterns in nodes-camera and program.nodes-media tests - Fix pairing message, telegram bot, channels, daemon, onboard, gateway tool, status, and profile test expectations Version: 2026.2.10-1.2 (published to npm as ironclaw@2026.2.10-1.2) Co-authored-by: Cursor --- extensions/voice-call/src/core-bridge.ts | 2 +- package.json | 2 +- src/agents/openclaw-gateway-tool.test.ts | 2 +- src/agents/workspace-templates.test.ts | 17 ++++ src/agents/workspace-templates.ts | 16 ++-- src/cli/argv.test.ts | 14 ++++ src/cli/argv.ts | 4 +- src/cli/nodes-camera.test.ts | 2 +- src/cli/profile.test.ts | 58 ++++++------- src/cli/program.nodes-media.test.ts | 4 +- src/cli/update-cli.ts | 11 ++- ....adds-non-default-telegram-account.test.ts | 2 +- src/commands/daemon-install-helpers.test.ts | 2 +- src/commands/onboard-hooks.test.ts | 2 +- src/commands/status.test.ts | 4 +- src/daemon/inspect.ts | 20 +++-- src/gateway/server-web-app.ts | 20 ++++- src/infra/control-ui-assets.test.ts | 16 ++++ src/infra/control-ui-assets.ts | 2 +- src/infra/gateway-lock.ts | 7 +- src/infra/openclaw-root.test.ts | 78 ++++++++++++++++++ src/infra/openclaw-root.ts | 2 +- src/infra/ports-format.test.ts | 26 ++++++ src/infra/ports-format.ts | 2 +- src/infra/update-check.ts | 2 +- src/infra/update-global.test.ts | 81 +++++++++++++++++++ src/infra/update-global.ts | 4 +- src/infra/update-runner.ts | 11 ++- src/memory/embeddings.ts | 2 +- src/pairing/pairing-messages.test.ts | 2 +- ...gram-bot.installs-grammy-throttler.test.ts | 2 +- src/telegram/bot.test.ts | 2 +- src/version.test.ts | 4 +- 33 files changed, 340 insertions(+), 85 deletions(-) create mode 100644 src/infra/openclaw-root.test.ts create mode 100644 src/infra/ports-format.test.ts create mode 100644 src/infra/update-global.test.ts diff --git a/extensions/voice-call/src/core-bridge.ts b/extensions/voice-call/src/core-bridge.ts index 0425eef9dbd..e2daf5401ee 100644 --- a/extensions/voice-call/src/core-bridge.ts +++ b/extensions/voice-call/src/core-bridge.ts @@ -109,7 +109,7 @@ function resolveOpenClawRoot(): string { } for (const start of candidates) { - for (const name of ["openclaw"]) { + for (const name of ["ironclaw", "openclaw"]) { const found = findPackageRoot(start, name); if (found) { coreRootCache = found; diff --git a/package.json b/package.json index d022ceded28..6bbf836dd64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ironclaw", - "version": "2026.2.10-1", + "version": "2026.2.10-1.2", "description": "AI-powered CRM platform with multi-channel agent gateway, DuckDB workspace, and knowledge management", "keywords": [], "license": "MIT", diff --git a/src/agents/openclaw-gateway-tool.test.ts b/src/agents/openclaw-gateway-tool.test.ts index 716d7ee0ad2..e17f1980939 100644 --- a/src/agents/openclaw-gateway-tool.test.ts +++ b/src/agents/openclaw-gateway-tool.test.ts @@ -51,7 +51,7 @@ describe("gateway tool", () => { }; expect(parsed.payload?.kind).toBe("restart"); expect(parsed.payload?.doctorHint).toBe( - "Run: openclaw --profile isolated doctor --non-interactive", + "Run: ironclaw --profile isolated doctor --non-interactive", ); expect(kill).not.toHaveBeenCalled(); diff --git a/src/agents/workspace-templates.test.ts b/src/agents/workspace-templates.test.ts index 39012e48b99..479c15c4f64 100644 --- a/src/agents/workspace-templates.test.ts +++ b/src/agents/workspace-templates.test.ts @@ -29,4 +29,21 @@ describe("resolveWorkspaceTemplateDir", () => { const resolved = await resolveWorkspaceTemplateDir({ cwd: distDir, moduleUrl }); expect(resolved).toBe(templatesDir); }); + + it("resolves templates when package.json name is 'ironclaw'", async () => { + resetWorkspaceTemplateDirCache(); + const root = await makeTempRoot(); + await fs.writeFile(path.join(root, "package.json"), JSON.stringify({ name: "ironclaw" })); + + const templatesDir = path.join(root, "docs", "reference", "templates"); + await fs.mkdir(templatesDir, { recursive: true }); + await fs.writeFile(path.join(templatesDir, "AGENTS.md"), "# ok\n"); + + const distDir = path.join(root, "dist"); + await fs.mkdir(distDir, { recursive: true }); + const moduleUrl = pathToFileURL(path.join(distDir, "entry.mjs")).toString(); + + const resolved = await resolveWorkspaceTemplateDir({ cwd: distDir, moduleUrl }); + expect(resolved).toBe(templatesDir); + }); }); diff --git a/src/agents/workspace-templates.ts b/src/agents/workspace-templates.ts index 11d733fa92c..f01a4e4a81b 100644 --- a/src/agents/workspace-templates.ts +++ b/src/agents/workspace-templates.ts @@ -3,10 +3,14 @@ import { fileURLToPath } from "node:url"; import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js"; import { pathExists } from "../utils.js"; -const FALLBACK_TEMPLATE_DIR = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "../../docs/reference/templates", -); +// In source layout the module lives at src/agents/, so ../../ reaches the repo root. +// In bundled output (tsdown) it lives at dist/, so ../ reaches the package root. +// Compute both candidates and pick whichever exists at resolution time. +const _moduleDir = path.dirname(fileURLToPath(import.meta.url)); +const FALLBACK_TEMPLATE_CANDIDATES = [ + path.resolve(_moduleDir, "../../docs/reference/templates"), + path.resolve(_moduleDir, "../docs/reference/templates"), +]; let cachedTemplateDir: string | undefined; let resolvingTemplateDir: Promise | undefined; @@ -32,7 +36,7 @@ export async function resolveWorkspaceTemplateDir(opts?: { const candidates = [ packageRoot ? path.join(packageRoot, "docs", "reference", "templates") : null, cwd ? path.resolve(cwd, "docs", "reference", "templates") : null, - FALLBACK_TEMPLATE_DIR, + ...FALLBACK_TEMPLATE_CANDIDATES, ].filter(Boolean) as string[]; for (const candidate of candidates) { @@ -42,7 +46,7 @@ export async function resolveWorkspaceTemplateDir(opts?: { } } - cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_DIR; + cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_CANDIDATES[0]; return cachedTemplateDir; })(); diff --git a/src/cli/argv.test.ts b/src/cli/argv.test.ts index 207a28caefe..f5d356f5824 100644 --- a/src/cli/argv.test.ts +++ b/src/cli/argv.test.ts @@ -140,6 +140,20 @@ describe("argv helpers", () => { expect(fallbackArgv).toEqual(["node", "openclaw", "status"]); }); + it("builds parse argv for ironclaw binary name", () => { + const directArgv = buildParseArgv({ + programName: "ironclaw", + rawArgs: ["ironclaw", "status"], + }); + expect(directArgv).toEqual(["node", "ironclaw", "status"]); + + const nodeArgv = buildParseArgv({ + programName: "ironclaw", + rawArgs: ["node", "ironclaw", "status"], + }); + expect(nodeArgv).toEqual(["node", "ironclaw", "status"]); + }); + it("decides when to migrate state", () => { expect(shouldMigrateState(["node", "openclaw", "status"])).toBe(false); expect(shouldMigrateState(["node", "openclaw", "health"])).toBe(false); diff --git a/src/cli/argv.ts b/src/cli/argv.ts index d922e786383..b32e94ed033 100644 --- a/src/cli/argv.ts +++ b/src/cli/argv.ts @@ -119,7 +119,7 @@ export function buildParseArgv(params: { const normalizedArgv = programName && baseArgv[0] === programName ? baseArgv.slice(1) - : baseArgv[0]?.endsWith("openclaw") + : baseArgv[0]?.endsWith("openclaw") || baseArgv[0]?.endsWith("ironclaw") ? baseArgv.slice(1) : baseArgv; const executable = (normalizedArgv[0]?.split(/[/\\]/).pop() ?? "").toLowerCase(); @@ -128,7 +128,7 @@ export function buildParseArgv(params: { if (looksLikeNode) { return normalizedArgv; } - return ["node", programName || "openclaw", ...normalizedArgv]; + return ["node", programName || "ironclaw", ...normalizedArgv]; } const nodeExecutablePattern = /^node-\d+(?:\.\d+)*(?:\.exe)?$/; diff --git a/src/cli/nodes-camera.test.ts b/src/cli/nodes-camera.test.ts index c3ef5411229..2e91f3ac38c 100644 --- a/src/cli/nodes-camera.test.ts +++ b/src/cli/nodes-camera.test.ts @@ -51,7 +51,7 @@ describe("nodes camera helpers", () => { tmpDir: "/tmp", id: "id1", }); - expect(p).toBe(path.join("/tmp", "openclaw-camera-snap-front-id1.jpg")); + expect(p).toBe(path.join("/tmp", "ironclaw-camera-snap-front-id1.jpg")); }); it("writes base64 to file", async () => { diff --git a/src/cli/profile.test.ts b/src/cli/profile.test.ts index 5c78eaa367e..0116296a40d 100644 --- a/src/cli/profile.test.ts +++ b/src/cli/profile.test.ts @@ -7,7 +7,7 @@ describe("parseCliProfileArgs", () => { it("leaves gateway --dev for subcommands", () => { const res = parseCliProfileArgs([ "node", - "openclaw", + "ironclaw", "gateway", "--dev", "--allow-unconfigured", @@ -16,39 +16,39 @@ describe("parseCliProfileArgs", () => { throw new Error(res.error); } expect(res.profile).toBeNull(); - expect(res.argv).toEqual(["node", "openclaw", "gateway", "--dev", "--allow-unconfigured"]); + expect(res.argv).toEqual(["node", "ironclaw", "gateway", "--dev", "--allow-unconfigured"]); }); it("still accepts global --dev before subcommand", () => { - const res = parseCliProfileArgs(["node", "openclaw", "--dev", "gateway"]); + const res = parseCliProfileArgs(["node", "ironclaw", "--dev", "gateway"]); if (!res.ok) { throw new Error(res.error); } expect(res.profile).toBe("dev"); - expect(res.argv).toEqual(["node", "openclaw", "gateway"]); + expect(res.argv).toEqual(["node", "ironclaw", "gateway"]); }); it("parses --profile value and strips it", () => { - const res = parseCliProfileArgs(["node", "openclaw", "--profile", "work", "status"]); + const res = parseCliProfileArgs(["node", "ironclaw", "--profile", "work", "status"]); if (!res.ok) { throw new Error(res.error); } expect(res.profile).toBe("work"); - expect(res.argv).toEqual(["node", "openclaw", "status"]); + expect(res.argv).toEqual(["node", "ironclaw", "status"]); }); it("rejects missing profile value", () => { - const res = parseCliProfileArgs(["node", "openclaw", "--profile"]); + const res = parseCliProfileArgs(["node", "ironclaw", "--profile"]); expect(res.ok).toBe(false); }); it("rejects combining --dev with --profile (dev first)", () => { - const res = parseCliProfileArgs(["node", "openclaw", "--dev", "--profile", "work", "status"]); + const res = parseCliProfileArgs(["node", "ironclaw", "--dev", "--profile", "work", "status"]); expect(res.ok).toBe(false); }); it("rejects combining --dev with --profile (profile first)", () => { - const res = parseCliProfileArgs(["node", "openclaw", "--profile", "work", "--dev", "status"]); + const res = parseCliProfileArgs(["node", "ironclaw", "--profile", "work", "--dev", "status"]); expect(res.ok).toBe(false); }); }); @@ -104,60 +104,60 @@ describe("applyCliProfileEnv", () => { describe("formatCliCommand", () => { it("returns command unchanged when no profile is set", () => { - expect(formatCliCommand("openclaw doctor --fix", {})).toBe("openclaw doctor --fix"); + expect(formatCliCommand("ironclaw doctor --fix", {})).toBe("ironclaw doctor --fix"); }); it("returns command unchanged when profile is default", () => { - expect(formatCliCommand("openclaw doctor --fix", { OPENCLAW_PROFILE: "default" })).toBe( - "openclaw doctor --fix", + expect(formatCliCommand("ironclaw doctor --fix", { OPENCLAW_PROFILE: "default" })).toBe( + "ironclaw doctor --fix", ); }); it("returns command unchanged when profile is Default (case-insensitive)", () => { - expect(formatCliCommand("openclaw doctor --fix", { OPENCLAW_PROFILE: "Default" })).toBe( - "openclaw doctor --fix", + expect(formatCliCommand("ironclaw doctor --fix", { OPENCLAW_PROFILE: "Default" })).toBe( + "ironclaw doctor --fix", ); }); it("returns command unchanged when profile is invalid", () => { - expect(formatCliCommand("openclaw doctor --fix", { OPENCLAW_PROFILE: "bad profile" })).toBe( - "openclaw doctor --fix", + expect(formatCliCommand("ironclaw doctor --fix", { OPENCLAW_PROFILE: "bad profile" })).toBe( + "ironclaw doctor --fix", ); }); it("returns command unchanged when --profile is already present", () => { expect( - formatCliCommand("openclaw --profile work doctor --fix", { OPENCLAW_PROFILE: "work" }), - ).toBe("openclaw --profile work doctor --fix"); + formatCliCommand("ironclaw --profile work doctor --fix", { OPENCLAW_PROFILE: "work" }), + ).toBe("ironclaw --profile work doctor --fix"); }); it("returns command unchanged when --dev is already present", () => { - expect(formatCliCommand("openclaw --dev doctor", { OPENCLAW_PROFILE: "dev" })).toBe( - "openclaw --dev doctor", + expect(formatCliCommand("ironclaw --dev doctor", { OPENCLAW_PROFILE: "dev" })).toBe( + "ironclaw --dev doctor", ); }); it("inserts --profile flag when profile is set", () => { - expect(formatCliCommand("openclaw doctor --fix", { OPENCLAW_PROFILE: "work" })).toBe( - "openclaw --profile work doctor --fix", + expect(formatCliCommand("ironclaw doctor --fix", { OPENCLAW_PROFILE: "work" })).toBe( + "ironclaw --profile work doctor --fix", ); }); it("trims whitespace from profile", () => { - expect(formatCliCommand("openclaw doctor --fix", { OPENCLAW_PROFILE: " jbopenclaw " })).toBe( - "openclaw --profile jbopenclaw doctor --fix", + expect(formatCliCommand("ironclaw doctor --fix", { OPENCLAW_PROFILE: " jbopenclaw " })).toBe( + "ironclaw --profile jbopenclaw doctor --fix", ); }); - it("handles command with no args after openclaw", () => { - expect(formatCliCommand("openclaw", { OPENCLAW_PROFILE: "test" })).toBe( - "openclaw --profile test", + it("handles command with no args after ironclaw", () => { + expect(formatCliCommand("ironclaw", { OPENCLAW_PROFILE: "test" })).toBe( + "ironclaw --profile test", ); }); it("handles pnpm wrapper", () => { - expect(formatCliCommand("pnpm openclaw doctor", { OPENCLAW_PROFILE: "work" })).toBe( - "pnpm openclaw --profile work doctor", + expect(formatCliCommand("pnpm ironclaw doctor", { OPENCLAW_PROFILE: "work" })).toBe( + "pnpm ironclaw --profile work doctor", ); }); }); diff --git a/src/cli/program.nodes-media.test.ts b/src/cli/program.nodes-media.test.ts index adaee5442c8..691a81c507c 100644 --- a/src/cli/program.nodes-media.test.ts +++ b/src/cli/program.nodes-media.test.ts @@ -174,7 +174,7 @@ describe("cli program (nodes media)", () => { const out = String(runtime.log.mock.calls[0]?.[0] ?? ""); const mediaPath = out.replace(/^MEDIA:/, "").trim(); - expect(mediaPath).toMatch(/openclaw-camera-clip-front-.*\.mp4$/); + expect(mediaPath).toMatch(/ironclaw-camera-clip-front-.*\.mp4$/); try { await expect(fs.readFile(mediaPath, "utf8")).resolves.toBe("hi"); @@ -421,7 +421,7 @@ describe("cli program (nodes media)", () => { const out = String(runtime.log.mock.calls[0]?.[0] ?? ""); const mediaPath = out.replace(/^MEDIA:/, "").trim(); - expect(mediaPath).toMatch(/openclaw-canvas-snapshot-.*\.png$/); + expect(mediaPath).toMatch(/ironclaw-canvas-snapshot-.*\.png$/); try { await expect(fs.readFile(mediaPath, "utf8")).resolves.toBe("hi"); diff --git a/src/cli/update-cli.ts b/src/cli/update-cli.ts index c6f3dbd6220..799ed32aa12 100644 --- a/src/cli/update-cli.ts +++ b/src/cli/update-cli.ts @@ -126,7 +126,7 @@ const UPDATE_QUIPS = [ const MAX_LOG_CHARS = 8000; const DEFAULT_PACKAGE_NAME = "openclaw"; -const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]); +const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME, "ironclaw"]); const CLI_NAME = resolveCliName(); const OPENCLAW_REPO_URL = "https://github.com/openclaw/openclaw.git"; @@ -138,11 +138,10 @@ function normalizeTag(value?: string | null): string | null { if (!trimmed) { return null; } - if (trimmed.startsWith("openclaw@")) { - return trimmed.slice("openclaw@".length); - } - if (trimmed.startsWith(`${DEFAULT_PACKAGE_NAME}@`)) { - return trimmed.slice(`${DEFAULT_PACKAGE_NAME}@`.length); + for (const prefix of ["ironclaw@", "openclaw@", `${DEFAULT_PACKAGE_NAME}@`]) { + if (trimmed.startsWith(prefix)) { + return trimmed.slice(prefix.length); + } } return trimmed; } diff --git a/src/commands/channels.adds-non-default-telegram-account.test.ts b/src/commands/channels.adds-non-default-telegram-account.test.ts index a9539141be0..ef0c2e202d9 100644 --- a/src/commands/channels.adds-non-default-telegram-account.test.ts +++ b/src/commands/channels.adds-non-default-telegram-account.test.ts @@ -369,7 +369,7 @@ describe("channels command", () => { }); expect(lines.join("\n")).toMatch(/Warnings:/); expect(lines.join("\n")).toMatch(/Message Content Intent is disabled/i); - expect(lines.join("\n")).toMatch(/Run: (?:openclaw|openclaw)( --profile isolated)? doctor/); + expect(lines.join("\n")).toMatch(/Run: (?:ironclaw|openclaw)( --profile isolated)? doctor/); }); it("surfaces Discord permission audit issues in channels status output", () => { diff --git a/src/commands/daemon-install-helpers.test.ts b/src/commands/daemon-install-helpers.test.ts index 83dff066e5e..ebbceeb5078 100644 --- a/src/commands/daemon-install-helpers.test.ts +++ b/src/commands/daemon-install-helpers.test.ts @@ -235,7 +235,7 @@ describe("gatewayInstallErrorHint", () => { it("returns platform-specific hints", () => { expect(gatewayInstallErrorHint("win32")).toContain("Run as administrator"); expect(gatewayInstallErrorHint("linux")).toMatch( - /(?:openclaw|openclaw)( --profile isolated)? gateway install/, + /(?:ironclaw|openclaw)( --profile isolated)? gateway install/, ); }); }); diff --git a/src/commands/onboard-hooks.test.ts b/src/commands/onboard-hooks.test.ts index 1ab2b47caec..a24d50115b2 100644 --- a/src/commands/onboard-hooks.test.ts +++ b/src/commands/onboard-hooks.test.ts @@ -239,7 +239,7 @@ describe("onboard-hooks", () => { // Second note should confirm configuration expect(noteCalls[1][0]).toContain("Enabled 1 hook: session-memory"); - expect(noteCalls[1][0]).toMatch(/(?:openclaw|openclaw)( --profile isolated)? hooks list/); + expect(noteCalls[1][0]).toMatch(/(?:ironclaw|openclaw)( --profile isolated)? hooks list/); }); }); }); diff --git a/src/commands/status.test.ts b/src/commands/status.test.ts index 0641f7eedbb..73dabb83b6f 100644 --- a/src/commands/status.test.ts +++ b/src/commands/status.test.ts @@ -335,8 +335,8 @@ describe("statusCommand", () => { expect( logs.some( (l) => - l.includes("openclaw status --all") || - l.includes("openclaw --profile isolated status --all") || + l.includes("ironclaw status --all") || + l.includes("ironclaw --profile isolated status --all") || l.includes("openclaw status --all") || l.includes("openclaw --profile isolated status --all"), ), diff --git a/src/daemon/inspect.ts b/src/daemon/inspect.ts index 63970351a9c..af892ad6be4 100644 --- a/src/daemon/inspect.ts +++ b/src/daemon/inspect.ts @@ -15,7 +15,7 @@ export type ExtraGatewayService = { label: string; detail: string; scope: "user" | "system"; - marker?: "openclaw" | "clawdbot" | "moltbot"; + marker?: "openclaw" | "ironclaw" | "clawdbot" | "moltbot"; legacy?: boolean; }; @@ -23,7 +23,7 @@ export type FindExtraGatewayServicesOptions = { deep?: boolean; }; -const EXTRA_MARKERS = ["openclaw", "clawdbot", "moltbot"] as const; +const EXTRA_MARKERS = ["openclaw", "ironclaw", "clawdbot", "moltbot"] as const; const execFileAsync = promisify(execFile); export function renderGatewayServiceCleanupHints( @@ -95,14 +95,14 @@ function isOpenClawGatewayLaunchdService(label: string, contents: string): boole if (!lowerContents.includes("gateway")) { return false; } - return label.startsWith("ai.openclaw."); + return label.startsWith("ai.openclaw.") || label.startsWith("ai.ironclaw."); } function isOpenClawGatewaySystemdService(name: string, contents: string): boolean { if (hasGatewayServiceMarker(contents)) { return true; } - if (!name.startsWith("openclaw-gateway")) { + if (!name.startsWith("openclaw-gateway") && !name.startsWith("ironclaw-gateway")) { return false; } return contents.toLowerCase().includes("gateway"); @@ -114,7 +114,11 @@ function isOpenClawGatewayTaskName(name: string): boolean { return false; } const defaultName = resolveGatewayWindowsTaskName().toLowerCase(); - return normalized === defaultName || normalized.startsWith("openclaw gateway"); + return ( + normalized === defaultName || + normalized.startsWith("openclaw gateway") || + normalized.startsWith("ironclaw gateway") + ); } function tryExtractPlistLabel(contents: string): string | null { @@ -194,7 +198,7 @@ async function scanLaunchdDir(params: { detail: `plist: ${fullPath}`, scope: params.scope, marker, - legacy: marker !== "openclaw" || isLegacyLabel(label), + legacy: (marker !== "openclaw" && marker !== "ironclaw") || isLegacyLabel(label), }); } @@ -241,7 +245,7 @@ async function scanSystemdDir(params: { detail: `unit: ${fullPath}`, scope: params.scope, marker, - legacy: marker !== "openclaw", + legacy: marker !== "openclaw" && marker !== "ironclaw", }); } @@ -435,7 +439,7 @@ export async function findExtraGatewayServices( detail: task.taskToRun ? `task: ${name}, run: ${task.taskToRun}` : name, scope: "system", marker, - legacy: marker !== "openclaw", + legacy: marker !== "openclaw" && marker !== "ironclaw", }); } return results; diff --git a/src/gateway/server-web-app.ts b/src/gateway/server-web-app.ts index 81eb7fc80f2..883b3f316b4 100644 --- a/src/gateway/server-web-app.ts +++ b/src/gateway/server-web-app.ts @@ -89,7 +89,7 @@ export async function ensureWebAppBuilt( try { await ensureDepsInstalled(webAppDir, log); runtime.log("Web app not built; building for production (next build)…"); - await runCommand("npx", ["next", "build"], webAppDir, log); + await runCommand("node", [resolveNextBin(webAppDir), "build"], webAppDir, log); } catch (err) { return { ok: false, @@ -147,7 +147,7 @@ export async function startWebAppIfEnabled( // Dev mode: ensure deps, then `next dev`. await ensureDepsInstalled(webAppDir, log); log.info(`starting web app (dev) on port ${port}…`); - child = spawn("npx", ["next", "dev", "--port", String(port)], { + child = spawn("node", [resolveNextBin(webAppDir), "dev", "--port", String(port)], { cwd: webAppDir, stdio: "pipe", env: { ...process.env, PORT: String(port) }, @@ -158,13 +158,13 @@ export async function startWebAppIfEnabled( if (!hasNextBuild(webAppDir)) { log.info("building web app for production (first run)…"); - await runCommand("npx", ["next", "build"], webAppDir, log); + await runCommand("node", [resolveNextBin(webAppDir), "build"], webAppDir, log); } else { log.info("existing web app build found — skipping build"); } log.info(`starting web app (production) on port ${port}…`); - child = spawn("npx", ["next", "start", "--port", String(port)], { + child = spawn("node", [resolveNextBin(webAppDir), "start", "--port", String(port)], { cwd: webAppDir, stdio: "pipe", env: { ...process.env, PORT: String(port) }, @@ -221,6 +221,18 @@ export async function startWebAppIfEnabled( // ── helpers ────────────────────────────────────────────────────────────────── +/** + * Resolve the local `next` CLI entry script from apps/web/node_modules. + * + * Using `npx next` is fragile in global installs (pnpm, npm) because npx + * walks up the node_modules tree and may hit a broken pnpm virtual-store + * symlink in the parent package. Resolving the local binary directly avoids + * this issue entirely. + */ +function resolveNextBin(webAppDir: string): string { + return path.join(webAppDir, "node_modules", "next", "dist", "bin", "next"); +} + async function ensureDepsInstalled( webAppDir: string, log: { info: (msg: string) => void }, diff --git a/src/infra/control-ui-assets.test.ts b/src/infra/control-ui-assets.test.ts index 7b5acbe5455..2f9c0250934 100644 --- a/src/infra/control-ui-assets.test.ts +++ b/src/infra/control-ui-assets.test.ts @@ -165,6 +165,22 @@ describe("control UI assets helpers", () => { } }); + it("resolves via fallback when package name is 'ironclaw'", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "ironclaw-ui-")); + try { + await fs.writeFile(path.join(tmp, "package.json"), JSON.stringify({ name: "ironclaw" })); + await fs.writeFile(path.join(tmp, "openclaw.mjs"), "export {};\n"); + await fs.mkdir(path.join(tmp, "dist", "control-ui"), { recursive: true }); + await fs.writeFile(path.join(tmp, "dist", "control-ui", "index.html"), "\n"); + + expect(await resolveControlUiDistIndexPath(path.join(tmp, "openclaw.mjs"))).toBe( + path.join(tmp, "dist", "control-ui", "index.html"), + ); + } finally { + await fs.rm(tmp, { recursive: true, force: true }); + } + }); + it("returns null when package name does not match openclaw", async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-")); try { diff --git a/src/infra/control-ui-assets.ts b/src/infra/control-ui-assets.ts index 08e0312c8fa..e791b48939d 100644 --- a/src/infra/control-ui-assets.ts +++ b/src/infra/control-ui-assets.ts @@ -94,7 +94,7 @@ export async function resolveControlUiDistIndexPath( try { const raw = fs.readFileSync(pkgJsonPath, "utf-8"); const parsed = JSON.parse(raw) as { name?: unknown }; - if (parsed.name === "openclaw") { + if (parsed.name === "openclaw" || parsed.name === "ironclaw") { return indexPath; } } catch { diff --git a/src/infra/gateway-lock.ts b/src/infra/gateway-lock.ts index ef89f42a101..6e12d1529a5 100644 --- a/src/infra/gateway-lock.ts +++ b/src/infra/gateway-lock.ts @@ -83,7 +83,12 @@ function isGatewayArgv(args: string[]): boolean { } const exe = normalized[0] ?? ""; - return exe.endsWith("/openclaw") || exe === "openclaw"; + return ( + exe.endsWith("/openclaw") || + exe === "openclaw" || + exe.endsWith("/ironclaw") || + exe === "ironclaw" + ); } function readLinuxCmdline(pid: number): string[] | null { diff --git a/src/infra/openclaw-root.test.ts b/src/infra/openclaw-root.test.ts new file mode 100644 index 00000000000..b5083b1cd2b --- /dev/null +++ b/src/infra/openclaw-root.test.ts @@ -0,0 +1,78 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { describe, expect, it } from "vitest"; +import { resolveOpenClawPackageRoot, resolveOpenClawPackageRootSync } from "./openclaw-root.js"; + +async function makeTempPkg(name: string): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-root-")); + await fs.writeFile(path.join(root, "package.json"), JSON.stringify({ name })); + return root; +} + +describe("resolveOpenClawPackageRoot", () => { + it("finds package root with name 'openclaw'", async () => { + const root = await makeTempPkg("openclaw"); + try { + const distDir = path.join(root, "dist"); + await fs.mkdir(distDir, { recursive: true }); + const moduleUrl = pathToFileURL(path.join(distDir, "entry.js")).toString(); + const result = await resolveOpenClawPackageRoot({ moduleUrl }); + expect(result).toBe(root); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("finds package root with name 'ironclaw'", async () => { + const root = await makeTempPkg("ironclaw"); + try { + const distDir = path.join(root, "dist"); + await fs.mkdir(distDir, { recursive: true }); + const moduleUrl = pathToFileURL(path.join(distDir, "entry.js")).toString(); + const result = await resolveOpenClawPackageRoot({ moduleUrl }); + expect(result).toBe(root); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("returns null for unrelated package name", async () => { + const root = await makeTempPkg("unrelated-package"); + try { + const moduleUrl = pathToFileURL(path.join(root, "index.js")).toString(); + const result = await resolveOpenClawPackageRoot({ moduleUrl, cwd: root }); + expect(result).toBeNull(); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); +}); + +describe("resolveOpenClawPackageRootSync", () => { + it("finds ironclaw package root synchronously", async () => { + const root = await makeTempPkg("ironclaw"); + try { + const distDir = path.join(root, "dist"); + await fs.mkdir(distDir, { recursive: true }); + const moduleUrl = pathToFileURL(path.join(distDir, "entry.js")).toString(); + const result = resolveOpenClawPackageRootSync({ moduleUrl }); + expect(result).toBe(root); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("finds openclaw package root synchronously", async () => { + const root = await makeTempPkg("openclaw"); + try { + const moduleUrl = pathToFileURL(path.join(root, "dist", "x.js")).toString(); + await fs.mkdir(path.join(root, "dist"), { recursive: true }); + const result = resolveOpenClawPackageRootSync({ moduleUrl }); + expect(result).toBe(root); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); +}); diff --git a/src/infra/openclaw-root.ts b/src/infra/openclaw-root.ts index a13f510053e..9a651eb3ae0 100644 --- a/src/infra/openclaw-root.ts +++ b/src/infra/openclaw-root.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -const CORE_PACKAGE_NAMES = new Set(["openclaw"]); +const CORE_PACKAGE_NAMES = new Set(["openclaw", "ironclaw"]); async function readPackageName(dir: string): Promise { try { diff --git a/src/infra/ports-format.test.ts b/src/infra/ports-format.test.ts new file mode 100644 index 00000000000..9d1c8e0dfdf --- /dev/null +++ b/src/infra/ports-format.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { classifyPortListener } from "./ports-format.js"; + +describe("classifyPortListener", () => { + it("classifies openclaw as gateway", () => { + expect(classifyPortListener({ commandLine: "node openclaw gateway run" }, 18789)).toBe( + "gateway", + ); + }); + + it("classifies ironclaw as gateway", () => { + expect(classifyPortListener({ commandLine: "node ironclaw gateway run" }, 18789)).toBe( + "gateway", + ); + }); + + it("classifies ssh tunnels", () => { + expect(classifyPortListener({ commandLine: "ssh -L 18789:localhost:18789" }, 18789)).toBe( + "ssh", + ); + }); + + it("classifies unknown processes", () => { + expect(classifyPortListener({ commandLine: "nginx" }, 18789)).toBe("unknown"); + }); +}); diff --git a/src/infra/ports-format.ts b/src/infra/ports-format.ts index 54fb75b66ca..be79c131601 100644 --- a/src/infra/ports-format.ts +++ b/src/infra/ports-format.ts @@ -3,7 +3,7 @@ import { formatCliCommand } from "../cli/command-format.js"; export function classifyPortListener(listener: PortListener, port: number): PortListenerKind { const raw = `${listener.commandLine ?? ""} ${listener.command ?? ""}`.trim().toLowerCase(); - if (raw.includes("openclaw")) { + if (raw.includes("openclaw") || raw.includes("ironclaw")) { return "gateway"; } if (raw.includes("ssh")) { diff --git a/src/infra/update-check.ts b/src/infra/update-check.ts index 8525f53bf04..afbccaf1569 100644 --- a/src/infra/update-check.ts +++ b/src/infra/update-check.ts @@ -307,7 +307,7 @@ export async function fetchNpmTagVersion(params: { const tag = params.tag; try { const res = await fetchWithTimeout( - `https://registry.npmjs.org/openclaw/${encodeURIComponent(tag)}`, + `https://registry.npmjs.org/ironclaw/${encodeURIComponent(tag)}`, {}, Math.max(250, timeoutMs), ); diff --git a/src/infra/update-global.test.ts b/src/infra/update-global.test.ts new file mode 100644 index 00000000000..e3d1774f1bf --- /dev/null +++ b/src/infra/update-global.test.ts @@ -0,0 +1,81 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import type { CommandRunner } from "./update-global.js"; +import { + detectGlobalInstallManagerByPresence, + detectGlobalInstallManagerForRoot, + resolveGlobalPackageRoot, +} from "./update-global.js"; + +function makeMockRunner(globalRoot: string): CommandRunner { + return async (argv) => { + const cmd = argv.join(" "); + if (cmd === "npm root -g" || cmd === "pnpm root -g") { + return { stdout: globalRoot, stderr: "", code: 0 }; + } + return { stdout: "", stderr: "not found", code: 1 }; + }; +} + +describe("update-global package name detection", () => { + it("resolveGlobalPackageRoot returns ironclaw path", async () => { + const root = await resolveGlobalPackageRoot("npm", makeMockRunner("/tmp/mock-root"), 3000); + expect(root).toBe("/tmp/mock-root/ironclaw"); + }); + + it("detectGlobalInstallManagerForRoot matches ironclaw package root", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-global-")); + const globalRoot = path.join(tmp, "node_modules"); + const pkgRoot = path.join(globalRoot, "ironclaw"); + await fs.mkdir(pkgRoot, { recursive: true }); + + const manager = await detectGlobalInstallManagerForRoot( + makeMockRunner(globalRoot), + pkgRoot, + 3000, + ); + expect(manager).toBe("npm"); + + await fs.rm(tmp, { recursive: true, force: true }); + }); + + it("detectGlobalInstallManagerForRoot matches legacy openclaw package root", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-global-")); + const globalRoot = path.join(tmp, "node_modules"); + const pkgRoot = path.join(globalRoot, "openclaw"); + await fs.mkdir(pkgRoot, { recursive: true }); + + const manager = await detectGlobalInstallManagerForRoot( + makeMockRunner(globalRoot), + pkgRoot, + 3000, + ); + expect(manager).toBe("npm"); + + await fs.rm(tmp, { recursive: true, force: true }); + }); + + it("detectGlobalInstallManagerByPresence finds ironclaw dir", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-global-")); + const ironclawDir = path.join(tmp, "ironclaw"); + await fs.mkdir(ironclawDir, { recursive: true }); + + const manager = await detectGlobalInstallManagerByPresence(makeMockRunner(tmp), 3000); + expect(manager).toBe("npm"); + + await fs.rm(tmp, { recursive: true, force: true }); + }); + + it("detectGlobalInstallManagerByPresence finds openclaw dir", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-global-")); + const openclawDir = path.join(tmp, "openclaw"); + await fs.mkdir(openclawDir, { recursive: true }); + + const manager = await detectGlobalInstallManagerByPresence(makeMockRunner(tmp), 3000); + expect(manager).toBe("npm"); + + await fs.rm(tmp, { recursive: true, force: true }); + }); +}); diff --git a/src/infra/update-global.ts b/src/infra/update-global.ts index e22dd3b1d43..930aced1bda 100644 --- a/src/infra/update-global.ts +++ b/src/infra/update-global.ts @@ -10,8 +10,8 @@ export type CommandRunner = ( options: { timeoutMs: number; cwd?: string; env?: NodeJS.ProcessEnv }, ) => Promise<{ stdout: string; stderr: string; code: number | null }>; -const PRIMARY_PACKAGE_NAME = "openclaw"; -const ALL_PACKAGE_NAMES = [PRIMARY_PACKAGE_NAME] as const; +const PRIMARY_PACKAGE_NAME = "ironclaw"; +const ALL_PACKAGE_NAMES = [PRIMARY_PACKAGE_NAME, "openclaw"] as const; const GLOBAL_RENAME_PREFIX = "."; async function tryRealpath(targetPath: string): Promise { diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index ac774a14126..4790a306a9f 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -81,7 +81,7 @@ const MAX_LOG_CHARS = 8000; const PREFLIGHT_MAX_COMMITS = 10; const START_DIRS = ["cwd", "argv1", "process"]; const DEFAULT_PACKAGE_NAME = "openclaw"; -const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]); +const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME, "ironclaw"]); function normalizeDir(value?: string | null) { if (!value) { @@ -355,11 +355,10 @@ function normalizeTag(tag?: string) { if (!trimmed) { return "latest"; } - if (trimmed.startsWith("openclaw@")) { - return trimmed.slice("openclaw@".length); - } - if (trimmed.startsWith(`${DEFAULT_PACKAGE_NAME}@`)) { - return trimmed.slice(`${DEFAULT_PACKAGE_NAME}@`.length); + for (const prefix of ["ironclaw@", "openclaw@", `${DEFAULT_PACKAGE_NAME}@`]) { + if (trimmed.startsWith(prefix)) { + return trimmed.slice(prefix.length); + } } return trimmed; } diff --git a/src/memory/embeddings.ts b/src/memory/embeddings.ts index a81f5fbabfb..eb04d9dace4 100644 --- a/src/memory/embeddings.ts +++ b/src/memory/embeddings.ts @@ -238,7 +238,7 @@ function formatLocalSetupError(err: unknown): string { "To enable local embeddings:", "1) Use Node 22 LTS (recommended for installs/updates)", missing - ? "2) Reinstall OpenClaw (this should install node-llama-cpp): npm i -g openclaw@latest" + ? "2) Reinstall OpenClaw (this should install node-llama-cpp): npm i -g ironclaw@latest" : null, "3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp", 'Or set agents.defaults.memorySearch.provider = "openai" (remote).', diff --git a/src/pairing/pairing-messages.test.ts b/src/pairing/pairing-messages.test.ts index e63083560a1..e1bc06a3597 100644 --- a/src/pairing/pairing-messages.test.ts +++ b/src/pairing/pairing-messages.test.ts @@ -57,7 +57,7 @@ describe("buildPairingReply", () => { expect(text).toContain(`Pairing code: ${testCase.code}`); // CLI commands should respect OPENCLAW_PROFILE when set (most tests run with isolated profile) const commandRe = new RegExp( - `(?:openclaw|openclaw) --profile isolated pairing approve ${testCase.channel} ${testCase.code}`, + `(?:ironclaw|openclaw) --profile isolated pairing approve ${testCase.channel} ${testCase.code}`, ); expect(text).toMatch(commandRe); }); diff --git a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts index 1b43886f19d..be4f3a710a3 100644 --- a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts +++ b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts @@ -382,7 +382,7 @@ describe("createTelegramBot", () => { expect(pairingText).toContain("Your Telegram user id: 999"); expect(pairingText).toContain("Pairing code:"); expect(pairingText).toContain("PAIRME12"); - expect(pairingText).toContain("openclaw pairing approve telegram PAIRME12"); + expect(pairingText).toContain("ironclaw pairing approve telegram PAIRME12"); expect(pairingText).not.toContain(""); }); it("does not resend pairing code when a request is already pending", async () => { diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 3c2c63a7d40..bffe53336ac 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -595,7 +595,7 @@ describe("createTelegramBot", () => { expect(pairingText).toContain("Your Telegram user id: 999"); expect(pairingText).toContain("Pairing code:"); expect(pairingText).toContain("PAIRME12"); - expect(pairingText).toContain("openclaw pairing approve telegram PAIRME12"); + expect(pairingText).toContain("ironclaw pairing approve telegram PAIRME12"); expect(pairingText).not.toContain(""); }); diff --git a/src/version.test.ts b/src/version.test.ts index 8806d00de89..35af975f92f 100644 --- a/src/version.test.ts +++ b/src/version.test.ts @@ -28,7 +28,7 @@ describe("version resolution", () => { await fs.mkdir(path.join(root, "dist", "plugin-sdk"), { recursive: true }); await fs.writeFile( path.join(root, "package.json"), - JSON.stringify({ name: "openclaw", version: "1.2.3" }), + JSON.stringify({ name: "ironclaw", version: "1.2.3" }), "utf-8", ); @@ -43,7 +43,7 @@ describe("version resolution", () => { await fs.mkdir(path.join(root, "dist", "plugin-sdk"), { recursive: true }); await fs.writeFile( path.join(root, "package.json"), - JSON.stringify({ name: "openclaw", version: "2.3.4" }), + JSON.stringify({ name: "ironclaw", version: "2.3.4" }), "utf-8", ); await fs.writeFile(