2026-01-18 18:21:13 +00:00
|
|
|
#!/usr/bin/env node
|
2026-02-14 01:18:20 +00:00
|
|
|
import { spawn, spawnSync } from "node:child_process";
|
2026-01-31 21:21:09 +09:00
|
|
|
import fs from "node:fs";
|
|
|
|
|
import path from "node:path";
|
|
|
|
|
import process from "node:process";
|
2026-02-14 15:20:30 +00:00
|
|
|
import { pathToFileURL } from "node:url";
|
2026-03-15 20:44:03 +00:00
|
|
|
import { runRuntimePostBuild } from "./runtime-postbuild.mjs";
|
2026-01-18 18:21:13 +00:00
|
|
|
|
chore: Migrate to tsdown, speed up JS bundling by ~10x (thanks @hyf0).
The previous migration to tsdown was reverted because it caused a ~20x slowdown when running OpenClaw from the repo. @hyf0 investigated and found that simply renaming the `dist` folder also caused the same slowdown. It turns out the Plugin script loader has a bunch of voodoo vibe logic to determine if it should load files from source and compile them, or if it should load them from dist. When building with tsdown, the filesystem layout is different (bundled), and so some files weren't in the right location, and the Plugin script loader decided to compile source files from scratch using Jiti.
The new implementation uses tsdown to embed `NODE_ENV: 'production'`, which we now use to determine if we are running OpenClaw from a "production environmen" (ie. from dist). This removes the slop in favor of a deterministic toggle, and doesn't rely on directory names or similar.
There is some code reaching into `dist` to load specific modules, primarily in the voice-call extension, which I simplified into loading an "officially" exported `extensionAPI.js` file. With tsdown, entry points need to be explicitly configured, so we should be able to avoid sloppy code reaching into internals from now on. This might break some existing users, but if it does, it's because they were using "private" APIs.
2026-02-02 17:20:24 +09:00
|
|
|
const compiler = "tsdown";
|
2026-02-06 01:14:00 -05:00
|
|
|
const compilerArgs = ["exec", compiler, "--no-clean"];
|
2026-01-18 18:21:13 +00:00
|
|
|
|
2026-03-15 16:19:27 -04:00
|
|
|
const runNodeSourceRoots = ["src", "extensions"];
|
|
|
|
|
const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.config.ts"];
|
|
|
|
|
export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles];
|
|
|
|
|
const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/;
|
|
|
|
|
const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]);
|
|
|
|
|
|
|
|
|
|
const normalizePath = (filePath) => String(filePath ?? "").replaceAll("\\", "/");
|
|
|
|
|
|
|
|
|
|
const isIgnoredSourcePath = (relativePath) => {
|
|
|
|
|
const normalizedPath = normalizePath(relativePath);
|
|
|
|
|
return (
|
|
|
|
|
normalizedPath.endsWith(".test.ts") ||
|
|
|
|
|
normalizedPath.endsWith(".test.tsx") ||
|
|
|
|
|
normalizedPath.endsWith("test-helpers.ts")
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isBuildRelevantSourcePath = (relativePath) => {
|
|
|
|
|
const normalizedPath = normalizePath(relativePath);
|
|
|
|
|
return extensionSourceFilePattern.test(normalizedPath) && !isIgnoredSourcePath(normalizedPath);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const isBuildRelevantRunNodePath = (repoPath) => {
|
|
|
|
|
const normalizedPath = normalizePath(repoPath).replace(/^\.\/+/, "");
|
|
|
|
|
if (runNodeConfigFiles.includes(normalizedPath)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (normalizedPath.startsWith("src/")) {
|
|
|
|
|
return !isIgnoredSourcePath(normalizedPath.slice("src/".length));
|
|
|
|
|
}
|
|
|
|
|
if (normalizedPath.startsWith("extensions/")) {
|
|
|
|
|
return isBuildRelevantSourcePath(normalizedPath.slice("extensions/".length));
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isRestartRelevantExtensionPath = (relativePath) => {
|
|
|
|
|
const normalizedPath = normalizePath(relativePath);
|
|
|
|
|
if (extensionRestartMetadataFiles.has(path.posix.basename(normalizedPath))) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return isBuildRelevantSourcePath(normalizedPath);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const isRestartRelevantRunNodePath = (repoPath) => {
|
|
|
|
|
const normalizedPath = normalizePath(repoPath).replace(/^\.\/+/, "");
|
|
|
|
|
if (runNodeConfigFiles.includes(normalizedPath)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (normalizedPath.startsWith("src/")) {
|
|
|
|
|
return !isIgnoredSourcePath(normalizedPath.slice("src/".length));
|
|
|
|
|
}
|
|
|
|
|
if (normalizedPath.startsWith("extensions/")) {
|
|
|
|
|
return isRestartRelevantExtensionPath(normalizedPath.slice("extensions/".length));
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
2026-01-18 18:21:13 +00:00
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const statMtime = (filePath, fsImpl = fs) => {
|
2026-01-19 13:12:33 -06:00
|
|
|
try {
|
2026-02-14 15:20:30 +00:00
|
|
|
return fsImpl.statSync(filePath).mtimeMs;
|
2026-01-19 13:12:33 -06:00
|
|
|
} catch {
|
|
|
|
|
return null;
|
2026-01-18 18:21:13 +00:00
|
|
|
}
|
2026-01-19 13:12:33 -06:00
|
|
|
};
|
|
|
|
|
|
2026-03-15 16:19:27 -04:00
|
|
|
const isExcludedSource = (filePath, sourceRoot, sourceRootName) => {
|
|
|
|
|
const relativePath = normalizePath(path.relative(sourceRoot, filePath));
|
2026-01-31 21:29:14 +09:00
|
|
|
if (relativePath.startsWith("..")) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-03-15 16:19:27 -04:00
|
|
|
return !isBuildRelevantRunNodePath(path.posix.join(sourceRootName, relativePath));
|
2026-01-19 13:12:33 -06:00
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const findLatestMtime = (dirPath, shouldSkip, deps) => {
|
2026-01-19 13:12:33 -06:00
|
|
|
let latest = null;
|
|
|
|
|
const queue = [dirPath];
|
|
|
|
|
while (queue.length > 0) {
|
|
|
|
|
const current = queue.pop();
|
2026-01-31 21:29:14 +09:00
|
|
|
if (!current) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-19 13:12:33 -06:00
|
|
|
let entries = [];
|
|
|
|
|
try {
|
2026-02-14 15:20:30 +00:00
|
|
|
entries = deps.fs.readdirSync(current, { withFileTypes: true });
|
2026-01-19 13:12:33 -06:00
|
|
|
} catch {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const fullPath = path.join(current, entry.name);
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
queue.push(fullPath);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-31 21:29:14 +09:00
|
|
|
if (!entry.isFile()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (shouldSkip?.(fullPath)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-02-14 15:20:30 +00:00
|
|
|
const mtime = statMtime(fullPath, deps.fs);
|
2026-01-31 21:29:14 +09:00
|
|
|
if (mtime == null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-19 13:12:33 -06:00
|
|
|
if (latest == null || mtime > latest) {
|
|
|
|
|
latest = mtime;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return latest;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const runGit = (gitArgs, deps) => {
|
2026-02-14 01:18:20 +00:00
|
|
|
try {
|
2026-02-14 15:20:30 +00:00
|
|
|
const result = deps.spawnSync("git", gitArgs, {
|
|
|
|
|
cwd: deps.cwd,
|
2026-02-14 01:18:20 +00:00
|
|
|
encoding: "utf8",
|
|
|
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
|
|
|
});
|
|
|
|
|
if (result.status !== 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return (result.stdout ?? "").trim();
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const resolveGitHead = (deps) => {
|
|
|
|
|
const head = runGit(["rev-parse", "HEAD"], deps);
|
2026-02-14 01:18:20 +00:00
|
|
|
return head || null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-15 16:19:27 -04:00
|
|
|
const readGitStatus = (deps) => {
|
|
|
|
|
try {
|
|
|
|
|
const result = deps.spawnSync(
|
|
|
|
|
"git",
|
|
|
|
|
["status", "--porcelain", "--untracked-files=normal", "--", ...runNodeWatchedPaths],
|
|
|
|
|
{
|
|
|
|
|
cwd: deps.cwd,
|
|
|
|
|
encoding: "utf8",
|
|
|
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
if (result.status !== 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return result.stdout ?? "";
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parseGitStatusPaths = (output) =>
|
|
|
|
|
output
|
|
|
|
|
.split("\n")
|
|
|
|
|
.flatMap((line) => line.slice(3).split(" -> "))
|
|
|
|
|
.map((entry) => normalizePath(entry.trim()))
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const hasDirtySourceTree = (deps) => {
|
2026-03-15 16:19:27 -04:00
|
|
|
const output = readGitStatus(deps);
|
2026-02-14 01:18:20 +00:00
|
|
|
if (output === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-03-15 16:19:27 -04:00
|
|
|
return parseGitStatusPaths(output).some((repoPath) => isBuildRelevantRunNodePath(repoPath));
|
2026-02-14 01:18:20 +00:00
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const readBuildStamp = (deps) => {
|
|
|
|
|
const mtime = statMtime(deps.buildStampPath, deps.fs);
|
2026-02-14 01:18:20 +00:00
|
|
|
if (mtime == null) {
|
|
|
|
|
return { mtime: null, head: null };
|
|
|
|
|
}
|
|
|
|
|
try {
|
2026-02-14 15:20:30 +00:00
|
|
|
const raw = deps.fs.readFileSync(deps.buildStampPath, "utf8").trim();
|
2026-02-14 01:18:20 +00:00
|
|
|
if (!raw.startsWith("{")) {
|
|
|
|
|
return { mtime, head: null };
|
|
|
|
|
}
|
|
|
|
|
const parsed = JSON.parse(raw);
|
|
|
|
|
const head = typeof parsed?.head === "string" && parsed.head.trim() ? parsed.head.trim() : null;
|
|
|
|
|
return { mtime, head };
|
|
|
|
|
} catch {
|
|
|
|
|
return { mtime, head: null };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const hasSourceMtimeChanged = (stampMtime, deps) => {
|
2026-03-15 16:19:27 -04:00
|
|
|
let latestSourceMtime = null;
|
|
|
|
|
for (const sourceRoot of deps.sourceRoots) {
|
|
|
|
|
const sourceMtime = findLatestMtime(
|
|
|
|
|
sourceRoot.path,
|
|
|
|
|
(candidate) => isExcludedSource(candidate, sourceRoot.path, sourceRoot.name),
|
|
|
|
|
deps,
|
|
|
|
|
);
|
|
|
|
|
if (sourceMtime != null && (latestSourceMtime == null || sourceMtime > latestSourceMtime)) {
|
|
|
|
|
latestSourceMtime = sourceMtime;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return latestSourceMtime != null && latestSourceMtime > stampMtime;
|
2026-02-14 01:18:20 +00:00
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const shouldBuild = (deps) => {
|
|
|
|
|
if (deps.env.OPENCLAW_FORCE_BUILD === "1") {
|
2026-01-31 21:29:14 +09:00
|
|
|
return true;
|
|
|
|
|
}
|
2026-02-14 15:20:30 +00:00
|
|
|
const stamp = readBuildStamp(deps);
|
2026-02-14 01:18:20 +00:00
|
|
|
if (stamp.mtime == null) {
|
2026-01-31 21:29:14 +09:00
|
|
|
return true;
|
|
|
|
|
}
|
2026-02-14 15:20:30 +00:00
|
|
|
if (statMtime(deps.distEntry, deps.fs) == null) {
|
2026-01-31 21:29:14 +09:00
|
|
|
return true;
|
|
|
|
|
}
|
2026-01-19 13:12:33 -06:00
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
for (const filePath of deps.configFiles) {
|
|
|
|
|
const mtime = statMtime(filePath, deps.fs);
|
2026-02-14 01:18:20 +00:00
|
|
|
if (mtime != null && mtime > stamp.mtime) {
|
2026-01-31 21:29:14 +09:00
|
|
|
return true;
|
|
|
|
|
}
|
2026-01-18 18:21:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const currentHead = resolveGitHead(deps);
|
2026-02-14 01:18:20 +00:00
|
|
|
if (currentHead && !stamp.head) {
|
2026-02-14 15:20:30 +00:00
|
|
|
return hasSourceMtimeChanged(stamp.mtime, deps);
|
2026-02-14 01:18:20 +00:00
|
|
|
}
|
|
|
|
|
if (currentHead && stamp.head && currentHead !== stamp.head) {
|
2026-02-14 15:20:30 +00:00
|
|
|
return hasSourceMtimeChanged(stamp.mtime, deps);
|
2026-02-14 01:18:20 +00:00
|
|
|
}
|
|
|
|
|
if (currentHead) {
|
2026-02-14 15:20:30 +00:00
|
|
|
const dirty = hasDirtySourceTree(deps);
|
2026-02-14 01:18:20 +00:00
|
|
|
if (dirty === true) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (dirty === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
if (hasSourceMtimeChanged(stamp.mtime, deps)) {
|
2026-01-31 21:29:14 +09:00
|
|
|
return true;
|
|
|
|
|
}
|
2026-01-19 13:12:33 -06:00
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const logRunner = (message, deps) => {
|
|
|
|
|
if (deps.env.OPENCLAW_RUNNER_LOG === "0") {
|
2026-01-31 21:29:14 +09:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-14 15:20:30 +00:00
|
|
|
deps.stderr.write(`[openclaw] ${message}\n`);
|
2026-01-19 13:12:33 -06:00
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const runOpenClaw = async (deps) => {
|
|
|
|
|
const nodeProcess = deps.spawn(deps.execPath, ["openclaw.mjs", ...deps.args], {
|
|
|
|
|
cwd: deps.cwd,
|
|
|
|
|
env: deps.env,
|
2026-01-31 21:21:09 +09:00
|
|
|
stdio: "inherit",
|
2026-01-18 18:21:13 +00:00
|
|
|
});
|
2026-02-14 15:20:30 +00:00
|
|
|
const res = await new Promise((resolve) => {
|
|
|
|
|
nodeProcess.on("exit", (exitCode, exitSignal) => {
|
|
|
|
|
resolve({ exitCode, exitSignal });
|
|
|
|
|
});
|
2026-01-18 18:21:13 +00:00
|
|
|
});
|
2026-02-14 15:20:30 +00:00
|
|
|
if (res.exitSignal) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
return res.exitCode ?? 1;
|
2026-01-19 13:12:33 -06:00
|
|
|
};
|
|
|
|
|
|
2026-03-15 20:44:03 +00:00
|
|
|
const syncRuntimeArtifacts = (deps) => {
|
|
|
|
|
try {
|
|
|
|
|
runRuntimePostBuild({ cwd: deps.cwd });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logRunner(
|
|
|
|
|
`Failed to write runtime build artifacts: ${error?.message ?? "unknown error"}`,
|
|
|
|
|
deps,
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const writeBuildStamp = (deps) => {
|
2026-01-19 13:12:33 -06:00
|
|
|
try {
|
2026-02-14 15:20:30 +00:00
|
|
|
deps.fs.mkdirSync(deps.distRoot, { recursive: true });
|
2026-02-14 01:18:20 +00:00
|
|
|
const stamp = {
|
|
|
|
|
builtAt: Date.now(),
|
2026-02-14 15:20:30 +00:00
|
|
|
head: resolveGitHead(deps),
|
2026-02-14 01:18:20 +00:00
|
|
|
};
|
2026-02-14 15:20:30 +00:00
|
|
|
deps.fs.writeFileSync(deps.buildStampPath, `${JSON.stringify(stamp)}\n`);
|
2026-01-19 13:12:33 -06:00
|
|
|
} catch (error) {
|
|
|
|
|
// Best-effort stamp; still allow the runner to start.
|
2026-02-14 15:20:30 +00:00
|
|
|
logRunner(`Failed to write build stamp: ${error?.message ?? "unknown error"}`, deps);
|
2026-01-19 13:12:33 -06:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
export async function runNodeMain(params = {}) {
|
|
|
|
|
const deps = {
|
|
|
|
|
spawn: params.spawn ?? spawn,
|
|
|
|
|
spawnSync: params.spawnSync ?? spawnSync,
|
|
|
|
|
fs: params.fs ?? fs,
|
|
|
|
|
stderr: params.stderr ?? process.stderr,
|
|
|
|
|
execPath: params.execPath ?? process.execPath,
|
|
|
|
|
cwd: params.cwd ?? process.cwd(),
|
|
|
|
|
args: params.args ?? process.argv.slice(2),
|
|
|
|
|
env: params.env ? { ...params.env } : { ...process.env },
|
|
|
|
|
platform: params.platform ?? process.platform,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
deps.distRoot = path.join(deps.cwd, "dist");
|
|
|
|
|
deps.distEntry = path.join(deps.distRoot, "/entry.js");
|
|
|
|
|
deps.buildStampPath = path.join(deps.distRoot, ".buildstamp");
|
2026-03-15 16:19:27 -04:00
|
|
|
deps.sourceRoots = runNodeSourceRoots.map((sourceRoot) => ({
|
|
|
|
|
name: sourceRoot,
|
|
|
|
|
path: path.join(deps.cwd, sourceRoot),
|
|
|
|
|
}));
|
|
|
|
|
deps.configFiles = runNodeConfigFiles.map((filePath) => path.join(deps.cwd, filePath));
|
2026-02-14 15:20:30 +00:00
|
|
|
|
|
|
|
|
if (!shouldBuild(deps)) {
|
2026-03-15 20:44:03 +00:00
|
|
|
if (!syncRuntimeArtifacts(deps)) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
2026-02-14 15:20:30 +00:00
|
|
|
return await runOpenClaw(deps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logRunner("Building TypeScript (dist is stale).", deps);
|
|
|
|
|
const buildCmd = deps.platform === "win32" ? "cmd.exe" : "pnpm";
|
2026-01-22 22:11:15 -06:00
|
|
|
const buildArgs =
|
2026-02-14 15:20:30 +00:00
|
|
|
deps.platform === "win32" ? ["/d", "/s", "/c", "pnpm", ...compilerArgs] : compilerArgs;
|
|
|
|
|
const build = deps.spawn(buildCmd, buildArgs, {
|
|
|
|
|
cwd: deps.cwd,
|
|
|
|
|
env: deps.env,
|
2026-01-31 21:21:09 +09:00
|
|
|
stdio: "inherit",
|
2026-01-19 13:12:33 -06:00
|
|
|
});
|
|
|
|
|
|
2026-02-14 15:20:30 +00:00
|
|
|
const buildRes = await new Promise((resolve) => {
|
|
|
|
|
build.on("exit", (exitCode, exitSignal) => resolve({ exitCode, exitSignal }));
|
2026-01-19 13:12:33 -06:00
|
|
|
});
|
2026-02-14 15:20:30 +00:00
|
|
|
if (buildRes.exitSignal) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (buildRes.exitCode !== 0 && buildRes.exitCode !== null) {
|
|
|
|
|
return buildRes.exitCode;
|
|
|
|
|
}
|
2026-03-15 20:44:03 +00:00
|
|
|
if (!syncRuntimeArtifacts(deps)) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
2026-02-14 15:20:30 +00:00
|
|
|
writeBuildStamp(deps);
|
|
|
|
|
return await runOpenClaw(deps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
|
|
|
void runNodeMain()
|
|
|
|
|
.then((code) => process.exit(code))
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error(err);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|
2026-01-19 13:12:33 -06:00
|
|
|
}
|