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:
commit
5329d265f3
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
})();
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)?$/;
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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"),
|
||||
),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
78
src/infra/openclaw-root.test.ts
Normal file
78
src/infra/openclaw-root.test.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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 {
|
||||
|
||||
26
src/infra/ports-format.test.ts
Normal file
26
src/infra/ports-format.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@ -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")) {
|
||||
|
||||
@ -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),
|
||||
);
|
||||
|
||||
81
src/infra/update-global.test.ts
Normal file
81
src/infra/update-global.test.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
@ -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> {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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).',
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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>");
|
||||
});
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user