Merge pull request #2 from kumarabhirup/dench-workspace

Ironclaw rename: update CLI binary references, fix Next.js invocation, harden package resolution
This commit is contained in:
Kumar Abhirup 2026-02-12 12:21:33 -08:00 committed by GitHub
commit 5329d265f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 340 additions and 85 deletions

View File

@ -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;

View File

@ -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",

View File

@ -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();

View File

@ -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);
});
});

View File

@ -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<string> | 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;
})();

View File

@ -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);

View File

@ -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)?$/;

View File

@ -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 () => {

View File

@ -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",
);
});
});

View File

@ -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");

View File

@ -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;
}

View File

@ -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", () => {

View File

@ -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/,
);
});
});

View File

@ -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/);
});
});
});

View File

@ -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"),
),

View File

@ -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;

View File

@ -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 },

View File

@ -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"), "<html></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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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<string> {
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 });
}
});
});

View File

@ -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<string | null> {
try {

View File

@ -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");
});
});

View File

@ -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")) {

View File

@ -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),
);

View File

@ -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 });
});
});

View File

@ -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<string> {

View File

@ -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;
}

View File

@ -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).',

View File

@ -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);
});

View File

@ -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("<code>");
});
it("does not resend pairing code when a request is already pending", async () => {

View File

@ -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("<code>");
});

View File

@ -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(