openclaw/scripts/lib/run-node-input-state.mts
Peter Steinberger 9491d5a5a3
refactor(scripts): deslop scripts/lib (#159362)
* refactor(scripts): deslop scripts/lib

Consolidate script lifecycle, scenario, planner, report, release and
packaging helpers while retaining CLI and publication contracts.

Keep config-boundary source caches within one scan so repeated in-process
checks observe edits. Carry extracted helpers through selective fixtures
and iOS scope routing. Leave PR tooling and its library import closure intact.

* fix(scripts): retain sanitizer fast path after helper cleanup

* fix(scripts): model watchdog shell consumer in dead-code checks
2026-09-27 17:13:58 -07:00

178 lines
5.6 KiB
TypeScript

import type { SpawnSyncOptionsWithStringEncoding } from "node:child_process";
// Canonical watched-input state shared by artifact producers and freshness readers.
import type fs from "node:fs";
import path from "node:path";
import {
extensionRestartMetadataFiles,
isBuildRelevantRunNodePath,
normalizeRunNodePath as normalizePath,
runNodeWatchedPaths,
} from "../run-node-watch-paths.mts";
import { collectSourceCheckoutPluginBuildEntries } from "./bundled-plugin-build-entries.mjs";
import { BUNDLED_PLUGIN_PATH_PREFIX, BUNDLED_PLUGIN_ROOT_DIR } from "./bundled-plugin-paths.mjs";
import { listStaticExtensionAssetSources } from "./static-extension-assets.mts";
export type RunNodeInputDeps = {
cwd: string;
env: NodeJS.ProcessEnv;
fs: typeof fs;
distRoot: string;
spawnSync: (
command: string,
args: string[],
options: SpawnSyncOptionsWithStringEncoding,
) => { status: number | null; stdout?: string | null };
};
export type BundledPluginBuildEntry = ReturnType<
typeof collectSourceCheckoutPluginBuildEntries
>[number] & {
hasManifest: boolean;
};
export const runtimePostBuildWatchedPaths = [
"scripts/check-built-plugin-control-plane-modules.mts",
"scripts/copy-bundled-plugin-metadata.mjs",
"scripts/copy-bundled-plugin-metadata.mts",
"scripts/copy-hook-metadata.ts",
"scripts/lib",
"scripts/lib/local-build-metadata.mts",
"scripts/lib/local-build-metadata-paths.mts",
"scripts/npm-runner.mts",
"scripts/runtime-postbuild-stamp.mts",
"scripts/runtime-postbuild-shared.mjs",
"scripts/runtime-postbuild.mjs",
"scripts/runtime-postbuild.mts",
"scripts/stage-bundled-plugin-runtime.mjs",
"scripts/stage-bundled-plugin-runtime.mts",
"scripts/windows-cmd-helpers.mjs",
"scripts/write-build-info.ts",
"scripts/write-official-channel-catalog.mjs",
"scripts/write-official-channel-catalog.mts",
BUNDLED_PLUGIN_ROOT_DIR,
];
const runtimePostBuildScriptPaths = new Set(
runtimePostBuildWatchedPaths.filter((entry) => entry.startsWith("scripts/")),
);
const runtimePostBuildStaticAssetPaths = new Set(listStaticExtensionAssetSources());
const readGitStatus = (deps: RunNodeInputDeps, paths: string[] = runNodeWatchedPaths) => {
try {
const result = deps.spawnSync(
"git",
// NUL framing preserves filenames; separate delete/add records keep both rename sides.
[
"status",
"--porcelain=v1",
"-z",
"--no-renames",
"--untracked-files=normal",
"--",
...paths,
],
{
cwd: deps.cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
},
);
if (result.status !== 0) {
return null;
}
return result.stdout ?? "";
} catch {
return null;
}
};
const parseGitStatusPaths = (output: string) =>
output
.split("\0")
.map((entry) => entry.slice(3))
.filter(Boolean);
export const hasDirtySourceTree = (deps: RunNodeInputDeps) => {
const output = readGitStatus(deps);
if (output === null) {
return null;
}
return parseGitStatusPaths(output).some(
(repoPath) =>
isBuildRelevantRunNodePath(repoPath) ||
isDirtyBundledPluginPackageEntryChangeWithoutBuiltOutputs(repoPath, deps),
);
};
export const isRuntimePostBuildRelevantPath = (repoPath: string) => {
const normalizedPath = normalizePath(repoPath);
if (runtimePostBuildStaticAssetPaths.has(normalizedPath)) {
return true;
}
if (
normalizedPath.startsWith("scripts/") &&
(runtimePostBuildScriptPaths.has(normalizedPath) || normalizedPath.startsWith("scripts/lib/"))
) {
return true;
}
if (!normalizedPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
return false;
}
const pluginRelativePath = normalizedPath.slice(BUNDLED_PLUGIN_PATH_PREFIX.length);
const pluginLocalPath = pluginRelativePath.split("/").slice(1).join("/");
if (pluginLocalPath === "skills" || pluginLocalPath.startsWith("skills/")) {
return true;
}
return extensionRestartMetadataFiles.has(path.posix.basename(pluginRelativePath));
};
export const hasDirtyRuntimePostBuildInputs = (deps: RunNodeInputDeps) => {
const output = readGitStatus(deps, runtimePostBuildWatchedPaths);
if (output === null) {
return null;
}
return parseGitStatusPaths(output).some((repoPath) => isRuntimePostBuildRelevantPath(repoPath));
};
export const collectRunNodeBundledPluginBuildEntries = (deps: RunNodeInputDeps) => {
if (!deps.fs.existsSync(path.join(deps.cwd, BUNDLED_PLUGIN_ROOT_DIR))) {
return [];
}
return collectSourceCheckoutPluginBuildEntries({ cwd: deps.cwd, env: deps.env });
};
export const listBundledPluginRuntimeEntryPaths = (
pluginEntry: BundledPluginBuildEntry,
deps: RunNodeInputDeps,
) => {
return pluginEntry.sourceEntries
.map((sourceEntry) =>
path.join(
deps.distRoot,
"extensions",
pluginEntry.id,
sourceEntry.replace(/^\.\//, "").replace(/\.[^.]+$/u, pluginEntry.runtimeExtension),
),
)
.toSorted((left, right) => left.localeCompare(right));
};
const isDirtyBundledPluginPackageEntryChangeWithoutBuiltOutputs = (
normalizedPath: string,
deps: RunNodeInputDeps,
) => {
if (!normalizedPath.startsWith("extensions/") || !normalizedPath.endsWith("/package.json")) {
return false;
}
const [, pluginId] = normalizedPath.split("/");
if (!pluginId) {
return false;
}
const pluginEntry = collectRunNodeBundledPluginBuildEntries(deps).find(
(entry) => entry.id === pluginId,
);
if (!pluginEntry) {
return false;
}
return listBundledPluginRuntimeEntryPaths(pluginEntry, deps).some(
(filePath) => !deps.fs.existsSync(filePath),
);
};