chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions

View File

@@ -4,13 +4,17 @@ import { fileURLToPath } from "node:url";
export function resolveBundledHooksDir(): string | undefined {
const override = process.env.OPENCLAW_BUNDLED_HOOKS_DIR?.trim();
if (override) return override;
if (override) {
return override;
}
// bun --compile: ship a sibling `hooks/bundled/` next to the executable.
try {
const execDir = path.dirname(process.execPath);
const sibling = path.join(execDir, "hooks", "bundled");
if (fs.existsSync(sibling)) return sibling;
if (fs.existsSync(sibling)) {
return sibling;
}
} catch {
// ignore
}
@@ -20,7 +24,9 @@ export function resolveBundledHooksDir(): string | undefined {
try {
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const distBundled = path.join(moduleDir, "bundled");
if (fs.existsSync(distBundled)) return distBundled;
if (fs.existsSync(distBundled)) {
return distBundled;
}
} catch {
// ignore
}
@@ -31,7 +37,9 @@ export function resolveBundledHooksDir(): string | undefined {
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(moduleDir, "..", "..");
const srcBundled = path.join(root, "src", "hooks", "bundled");
if (fs.existsSync(srcBundled)) return srcBundled;
if (fs.existsSync(srcBundled)) {
return srcBundled;
}
} catch {
// ignore
}

View File

@@ -6,21 +6,31 @@ import { applySoulEvilOverride, resolveSoulEvilConfigFromHook } from "../../soul
const HOOK_KEY = "soul-evil";
const soulEvilHook: HookHandler = async (event) => {
if (!isAgentBootstrapEvent(event)) return;
if (!isAgentBootstrapEvent(event)) {
return;
}
const context = event.context;
if (context.sessionKey && isSubagentSessionKey(context.sessionKey)) return;
if (context.sessionKey && isSubagentSessionKey(context.sessionKey)) {
return;
}
const cfg = context.cfg;
const hookConfig = resolveHookConfig(cfg, HOOK_KEY);
if (!hookConfig || hookConfig.enabled === false) return;
if (!hookConfig || hookConfig.enabled === false) {
return;
}
const soulConfig = resolveSoulEvilConfigFromHook(hookConfig as Record<string, unknown>, {
warn: (message) => console.warn(`[soul-evil] ${message}`),
});
if (!soulConfig) return;
if (!soulConfig) {
return;
}
const workspaceDir = context.workspaceDir;
if (!workspaceDir || !Array.isArray(context.bootstrapFiles)) return;
if (!workspaceDir || !Array.isArray(context.bootstrapFiles)) {
return;
}
const updated = await applySoulEvilOverride({
files: context.bootstrapFiles,

View File

@@ -11,10 +11,18 @@ const DEFAULT_CONFIG_VALUES: Record<string, boolean> = {
};
function isTruthy(value: unknown): boolean {
if (value === undefined || value === null) return false;
if (typeof value === "boolean") return value;
if (typeof value === "number") return value !== 0;
if (typeof value === "string") return value.trim().length > 0;
if (value === undefined || value === null) {
return false;
}
if (typeof value === "boolean") {
return value;
}
if (typeof value === "number") {
return value !== 0;
}
if (typeof value === "string") {
return value.trim().length > 0;
}
return true;
}
@@ -22,7 +30,9 @@ export function resolveConfigPath(config: OpenClawConfig | undefined, pathStr: s
const parts = pathStr.split(".").filter(Boolean);
let current: unknown = config;
for (const part of parts) {
if (typeof current !== "object" || current === null) return undefined;
if (typeof current !== "object" || current === null) {
return undefined;
}
current = (current as Record<string, unknown>)[part];
}
return current;
@@ -41,9 +51,13 @@ export function resolveHookConfig(
hookKey: string,
): HookConfig | undefined {
const hooks = config?.hooks?.internal?.entries;
if (!hooks || typeof hooks !== "object") return undefined;
if (!hooks || typeof hooks !== "object") {
return undefined;
}
const entry = (hooks as Record<string, HookConfig | undefined>)[hookKey];
if (!entry || typeof entry !== "object") return undefined;
if (!entry || typeof entry !== "object") {
return undefined;
}
return entry;
}
@@ -79,7 +93,9 @@ export function shouldIncludeHook(params: {
const remotePlatforms = eligibility?.remote?.platforms ?? [];
// Check if explicitly disabled
if (!pluginManaged && hookConfig?.enabled === false) return false;
if (!pluginManaged && hookConfig?.enabled === false) {
return false;
}
// Check OS requirement
if (
@@ -99,8 +115,12 @@ export function shouldIncludeHook(params: {
const requiredBins = entry.metadata?.requires?.bins ?? [];
if (requiredBins.length > 0) {
for (const bin of requiredBins) {
if (hasBinary(bin)) continue;
if (eligibility?.remote?.hasBin?.(bin)) continue;
if (hasBinary(bin)) {
continue;
}
if (eligibility?.remote?.hasBin?.(bin)) {
continue;
}
return false;
}
}
@@ -111,15 +131,21 @@ export function shouldIncludeHook(params: {
const anyFound =
requiredAnyBins.some((bin) => hasBinary(bin)) ||
eligibility?.remote?.hasAnyBin?.(requiredAnyBins);
if (!anyFound) return false;
if (!anyFound) {
return false;
}
}
// Check required environment variables
const requiredEnv = entry.metadata?.requires?.env ?? [];
if (requiredEnv.length > 0) {
for (const envName of requiredEnv) {
if (process.env[envName]) continue;
if (hookConfig?.env?.[envName]) continue;
if (process.env[envName]) {
continue;
}
if (hookConfig?.env?.[envName]) {
continue;
}
return false;
}
}
@@ -128,7 +154,9 @@ export function shouldIncludeHook(params: {
const requiredConfig = entry.metadata?.requires?.config ?? [];
if (requiredConfig.length > 0) {
for (const configPath of requiredConfig) {
if (!isConfigPathTruthy(config, configPath)) return false;
if (!isConfigPathTruthy(config, configPath)) {
return false;
}
}
}

View File

@@ -16,7 +16,9 @@ export function parseFrontmatter(content: string): ParsedHookFrontmatter {
}
function normalizeStringList(input: unknown): string[] {
if (!input) return [];
if (!input) {
return [];
}
if (Array.isArray(input)) {
return input.map((value) => String(value).trim()).filter(Boolean);
}
@@ -30,7 +32,9 @@ function normalizeStringList(input: unknown): string[] {
}
function parseInstallSpec(input: unknown): HookInstallSpec | undefined {
if (!input || typeof input !== "object") return undefined;
if (!input || typeof input !== "object") {
return undefined;
}
const raw = input as Record<string, unknown>;
const kindRaw =
typeof raw.kind === "string" ? raw.kind : typeof raw.type === "string" ? raw.type : "";
@@ -43,12 +47,22 @@ function parseInstallSpec(input: unknown): HookInstallSpec | undefined {
kind: kind,
};
if (typeof raw.id === "string") spec.id = raw.id;
if (typeof raw.label === "string") spec.label = raw.label;
if (typeof raw.id === "string") {
spec.id = raw.id;
}
if (typeof raw.label === "string") {
spec.label = raw.label;
}
const bins = normalizeStringList(raw.bins);
if (bins.length > 0) spec.bins = bins;
if (typeof raw.package === "string") spec.package = raw.package;
if (typeof raw.repository === "string") spec.repository = raw.repository;
if (bins.length > 0) {
spec.bins = bins;
}
if (typeof raw.package === "string") {
spec.package = raw.package;
}
if (typeof raw.repository === "string") {
spec.repository = raw.repository;
}
return spec;
}
@@ -67,10 +81,14 @@ export function resolveOpenClawMetadata(
frontmatter: ParsedHookFrontmatter,
): OpenClawHookMetadata | undefined {
const raw = getFrontmatterValue(frontmatter, "metadata");
if (!raw) return undefined;
if (!raw) {
return undefined;
}
try {
const parsed = JSON5.parse(raw);
if (!parsed || typeof parsed !== "object") return undefined;
if (!parsed || typeof parsed !== "object") {
return undefined;
}
const metadataRawCandidates = [MANIFEST_KEY, ...LEGACY_MANIFEST_KEYS];
let metadataRaw: unknown;
for (const key of metadataRawCandidates) {
@@ -80,7 +98,9 @@ export function resolveOpenClawMetadata(
break;
}
}
if (!metadataRaw || typeof metadataRaw !== "object") return undefined;
if (!metadataRaw || typeof metadataRaw !== "object") {
return undefined;
}
const metadataObj = metadataRaw as Record<string, unknown>;
const requiresRaw =
typeof metadataObj.requires === "object" && metadataObj.requires !== null

View File

@@ -332,7 +332,9 @@ export async function runGmailService(opts: GmailRunOptions) {
}, renewMs);
const shutdown = () => {
if (shuttingDown) return;
if (shuttingDown) {
return;
}
shuttingDown = true;
clearInterval(renewTimer);
child.kill("SIGTERM");
@@ -342,10 +344,14 @@ export async function runGmailService(opts: GmailRunOptions) {
process.on("SIGTERM", shutdown);
child.on("exit", () => {
if (shuttingDown) return;
if (shuttingDown) {
return;
}
defaultRuntime.log("gog watch serve exited; restarting in 2s");
setTimeout(() => {
if (shuttingDown) return;
if (shuttingDown) {
return;
}
child = spawnGogServe(runtimeConfig);
}, 2000);
});
@@ -365,7 +371,9 @@ async function startGmailWatch(
const result = await runCommandWithTimeout(args, { timeoutMs: 120_000 });
if (result.code !== 0) {
const message = result.stderr || result.stdout || "gog watch start failed";
if (fatal) throw new Error(message);
if (fatal) {
throw new Error(message);
}
defaultRuntime.error(message);
}
}

View File

@@ -11,8 +11,12 @@ const MAX_OUTPUT_CHARS = 800;
function trimOutput(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "";
if (trimmed.length <= MAX_OUTPUT_CHARS) return trimmed;
if (!trimmed) {
return "";
}
if (trimmed.length <= MAX_OUTPUT_CHARS) {
return trimmed;
}
return `${trimmed.slice(0, MAX_OUTPUT_CHARS)}`;
}
@@ -23,8 +27,12 @@ function formatCommandFailure(command: string, result: SpawnResult): string {
const stderr = trimOutput(result.stderr);
const stdout = trimOutput(result.stdout);
const lines = [`${command} failed (code=${code}${signal}${killed})`];
if (stderr) lines.push(`stderr: ${stderr}`);
if (stdout) lines.push(`stdout: ${stdout}`);
if (stderr) {
lines.push(`stderr: ${stderr}`);
}
if (stdout) {
lines.push(`stdout: ${stdout}`);
}
return lines.join("\n");
}
@@ -35,8 +43,12 @@ function formatCommandResult(command: string, result: SpawnResult): string {
const stderr = trimOutput(result.stderr);
const stdout = trimOutput(result.stdout);
const lines = [`${command} exited (code=${code}${signal}${killed})`];
if (stderr) lines.push(`stderr: ${stderr}`);
if (stdout) lines.push(`stdout: ${stdout}`);
if (stderr) {
lines.push(`stderr: ${stderr}`);
}
if (stdout) {
lines.push(`stdout: ${stdout}`);
}
return lines.join("\n");
}
@@ -57,7 +69,9 @@ function findExecutablesOnPath(bins: string[]): string[] {
for (const part of parts) {
for (const bin of bins) {
const candidate = path.join(part, bin);
if (seen.has(candidate)) continue;
if (seen.has(candidate)) {
continue;
}
try {
fs.accessSync(candidate, fs.constants.X_OK);
matches.push(candidate);
@@ -73,13 +87,17 @@ function findExecutablesOnPath(bins: string[]): string[] {
function ensurePathIncludes(dirPath: string, position: "append" | "prepend") {
const pathEnv = process.env.PATH ?? "";
const parts = pathEnv.split(path.delimiter).filter(Boolean);
if (parts.includes(dirPath)) return;
if (parts.includes(dirPath)) {
return;
}
const next = position === "prepend" ? [dirPath, ...parts] : [...parts, dirPath];
process.env.PATH = next.join(path.delimiter);
}
function ensureGcloudOnPath(): boolean {
if (hasBinary("gcloud")) return true;
if (hasBinary("gcloud")) {
return true;
}
const candidates = [
"/opt/homebrew/share/google-cloud-sdk/bin/gcloud",
"/usr/local/share/google-cloud-sdk/bin/gcloud",
@@ -108,9 +126,13 @@ export async function resolvePythonExecutablePath(): Promise<string | undefined>
[candidate, "-c", "import os, sys; print(os.path.realpath(sys.executable))"],
{ timeoutMs: 2_000 },
);
if (res.code !== 0) continue;
if (res.code !== 0) {
continue;
}
const resolved = res.stdout.trim().split(/\s+/)[0];
if (!resolved) continue;
if (!resolved) {
continue;
}
try {
fs.accessSync(resolved, fs.constants.X_OK);
cachedPythonPath = resolved;
@@ -124,9 +146,13 @@ export async function resolvePythonExecutablePath(): Promise<string | undefined>
}
async function gcloudEnv(): Promise<NodeJS.ProcessEnv | undefined> {
if (process.env.CLOUDSDK_PYTHON) return undefined;
if (process.env.CLOUDSDK_PYTHON) {
return undefined;
}
const pythonPath = await resolvePythonExecutablePath();
if (!pythonPath) return undefined;
if (!pythonPath) {
return undefined;
}
return { CLOUDSDK_PYTHON: pythonPath };
}
@@ -141,8 +167,12 @@ async function runGcloudCommand(
}
export async function ensureDependency(bin: string, brewArgs: string[]) {
if (bin === "gcloud" && ensureGcloudOnPath()) return;
if (hasBinary(bin)) return;
if (bin === "gcloud" && ensureGcloudOnPath()) {
return;
}
if (hasBinary(bin)) {
return;
}
if (process.platform !== "darwin") {
throw new Error(`${bin} not installed; install it and retry`);
}
@@ -167,7 +197,9 @@ export async function ensureGcloudAuth() {
["auth", "list", "--filter", "status:ACTIVE", "--format", "value(account)"],
30_000,
);
if (res.code === 0 && res.stdout.trim()) return;
if (res.code === 0 && res.stdout.trim()) {
return;
}
const login = await runGcloudCommand(["auth", "login"], 600_000);
if (login.code !== 0) {
throw new Error(login.stderr || "gcloud auth login failed");
@@ -187,7 +219,9 @@ export async function ensureTopic(projectId: string, topicName: string) {
["pubsub", "topics", "describe", topicName, "--project", projectId],
30_000,
);
if (describe.code === 0) return;
if (describe.code === 0) {
return;
}
await runGcloud(["pubsub", "topics", "create", topicName, "--project", projectId]);
}
@@ -235,7 +269,9 @@ export async function ensureTailscaleEndpoint(params: {
target?: string;
token?: string;
}): Promise<string> {
if (params.mode === "off") return "";
if (params.mode === "off") {
return "";
}
const statusArgs = ["status", "--json"];
const statusCommand = formatCommand("tailscale", statusArgs);
@@ -283,13 +319,17 @@ export async function ensureTailscaleEndpoint(params: {
export async function resolveProjectIdFromGogCredentials(): Promise<string | null> {
const candidates = gogCredentialsPaths();
for (const candidate of candidates) {
if (!fs.existsSync(candidate)) continue;
if (!fs.existsSync(candidate)) {
continue;
}
try {
const raw = fs.readFileSync(candidate, "utf-8");
const parsed = JSON.parse(raw) as Record<string, unknown>;
const clientId = extractGogClientId(parsed);
const projectNumber = extractProjectNumber(clientId);
if (!projectNumber) continue;
if (!projectNumber) {
continue;
}
const res = await runGcloudCommand(
[
"projects",
@@ -301,9 +341,13 @@ export async function resolveProjectIdFromGogCredentials(): Promise<string | nul
],
30_000,
);
if (res.code !== 0) continue;
if (res.code !== 0) {
continue;
}
const projectId = res.stdout.trim().split(/\s+/)[0];
if (projectId) return projectId;
if (projectId) {
return projectId;
}
} catch {
// keep scanning
}
@@ -332,7 +376,9 @@ function extractGogClientId(parsed: Record<string, unknown>): string | null {
}
function extractProjectNumber(clientId: string | null): string | null {
if (!clientId) return null;
if (!clientId) {
return null;
}
const match = clientId.match(/^(\d+)-/);
return match?.[1] ?? null;
}

View File

@@ -75,12 +75,16 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
child.stdout?.on("data", (data: Buffer) => {
const line = data.toString().trim();
if (line) log.info(`[gog] ${line}`);
if (line) {
log.info(`[gog] ${line}`);
}
});
child.stderr?.on("data", (data: Buffer) => {
const line = data.toString().trim();
if (!line) return;
if (!line) {
return;
}
if (isAddressInUseError(line)) {
addressInUse = true;
}
@@ -92,7 +96,9 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
});
child.on("exit", (code, signal) => {
if (shuttingDown) return;
if (shuttingDown) {
return;
}
if (addressInUse) {
log.warn(
"gog serve failed to bind (address already in use); stopping restarts. " +
@@ -104,7 +110,9 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
log.warn(`gog exited (code=${code}, signal=${signal}); restarting in 5s`);
watcherProcess = null;
setTimeout(() => {
if (shuttingDown || !currentConfig) return;
if (shuttingDown || !currentConfig) {
return;
}
watcherProcess = spawnGogServe(currentConfig);
}, 5000);
});
@@ -180,7 +188,9 @@ export async function startGmailWatcher(cfg: OpenClawConfig): Promise<GmailWatch
// Set up renewal interval
const renewMs = runtimeConfig.renewEveryMinutes * 60_000;
renewInterval = setInterval(() => {
if (shuttingDown) return;
if (shuttingDown) {
return;
}
void startGmailWatch(runtimeConfig);
}, renewMs);

View File

@@ -71,7 +71,9 @@ export function mergeHookPresets(existing: string[] | undefined, preset: string)
export function normalizeHooksPath(raw?: string): string {
const base = raw?.trim() || DEFAULT_HOOKS_PATH;
if (base === "/") return DEFAULT_HOOKS_PATH;
if (base === "/") {
return DEFAULT_HOOKS_PATH;
}
const withSlash = base.startsWith("/") ? base : `/${base}`;
return withSlash.replace(/\/+$/, "");
}
@@ -80,7 +82,9 @@ export function normalizeServePath(raw?: string): string {
const base = raw?.trim() || DEFAULT_GMAIL_SERVE_PATH;
// Tailscale funnel/serve strips the set-path prefix before proxying.
// To accept requests at /<path> externally, gog must listen on "/".
if (base === "/") return "/";
if (base === "/") {
return "/";
}
const withSlash = base.startsWith("/") ? base : `/${base}`;
return withSlash.replace(/\/+$/, "");
}
@@ -253,7 +257,9 @@ export function buildTopicPath(projectId: string, topicName: string): string {
export function parseTopicPath(topic: string): { projectId: string; topicName: string } | null {
const match = topic.trim().match(/^projects\/([^/]+)\/topics\/([^/]+)$/i);
if (!match) return null;
if (!match) {
return null;
}
return { projectId: match[1] ?? "", topicName: match[2] ?? "" };
}

View File

@@ -95,7 +95,9 @@ describe("hooks install (e2e)", () => {
const { installHooksFromPath } = await import("./install.js");
const installResult = await installHooksFromPath({ path: packDir });
expect(installResult.ok).toBe(true);
if (!installResult.ok) return;
if (!installResult.ok) {
return;
}
const { clearInternalHooks, createInternalHookEvent, triggerInternalHook } =
await import("./internal-hooks.js");

View File

@@ -65,7 +65,9 @@ function resolveHookKey(entry: HookEntry): string {
function normalizeInstallOptions(entry: HookEntry): HookInstallOption[] {
const install = entry.metadata?.install ?? [];
if (install.length === 0) return [];
if (install.length === 0) {
return [];
}
// For hooks, we just list all install options
return install.map((spec, index) => {
@@ -115,8 +117,12 @@ function buildHookStatus(
const requiredOs = entry.metadata?.os ?? [];
const missingBins = requiredBins.filter((bin) => {
if (hasBinary(bin)) return false;
if (eligibility?.remote?.hasBin?.(bin)) return false;
if (hasBinary(bin)) {
return false;
}
if (eligibility?.remote?.hasBin?.(bin)) {
return false;
}
return true;
});
@@ -138,8 +144,12 @@ function buildHookStatus(
const missingEnv: string[] = [];
for (const envName of requiredEnv) {
if (process.env[envName]) continue;
if (hookConfig?.env?.[envName]) continue;
if (process.env[envName]) {
continue;
}
if (hookConfig?.env?.[envName]) {
continue;
}
missingEnv.push(envName);
}

View File

@@ -61,7 +61,9 @@ describe("installHooksFromArchive", () => {
const result = await installHooksFromArchive({ archivePath, hooksDir });
expect(result.ok).toBe(true);
if (!result.ok) return;
if (!result.ok) {
return;
}
expect(result.hookPackId).toBe("zip-hooks");
expect(result.hooks).toContain("zip-hook");
expect(result.targetDir).toBe(path.join(stateDir, "hooks", "zip-hooks"));
@@ -109,7 +111,9 @@ describe("installHooksFromArchive", () => {
const result = await installHooksFromArchive({ archivePath, hooksDir });
expect(result.ok).toBe(true);
if (!result.ok) return;
if (!result.ok) {
return;
}
expect(result.hookPackId).toBe("tar-hooks");
expect(result.hooks).toContain("tar-hook");
expect(result.targetDir).toBe(path.join(stateDir, "hooks", "tar-hooks"));
@@ -142,7 +146,9 @@ describe("installHooksFromPath", () => {
const result = await installHooksFromPath({ path: hookDir, hooksDir });
expect(result.ok).toBe(true);
if (!result.ok) return;
if (!result.ok) {
return;
}
expect(result.hookPackId).toBe("my-hook");
expect(result.hooks).toEqual(["my-hook"]);
expect(result.targetDir).toBe(path.join(stateDir, "hooks", "my-hook"));

View File

@@ -39,13 +39,17 @@ const defaultLogger: HookInstallLogger = {};
function unscopedPackageName(name: string): string {
const trimmed = name.trim();
if (!trimmed) return trimmed;
if (!trimmed) {
return trimmed;
}
return trimmed.includes("/") ? (trimmed.split("/").pop() ?? trimmed) : trimmed;
}
function safeDirName(input: string): string {
const trimmed = input.trim();
if (!trimmed) return trimmed;
if (!trimmed) {
return trimmed;
}
return trimmed.replaceAll("/", "__");
}
@@ -349,7 +353,9 @@ export async function installHooksFromNpmSpec(params: {
const dryRun = params.dryRun ?? false;
const expectedHookPackId = params.expectedHookPackId;
const spec = params.spec.trim();
if (!spec) return { ok: false, error: "missing npm spec" };
if (!spec) {
return { ok: false, error: "missing npm spec" };
}
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hook-pack-"));
logger.info?.(`Downloading ${spec}`);

View File

@@ -167,9 +167,15 @@ export function createInternalHookEvent(
}
export function isAgentBootstrapEvent(event: InternalHookEvent): event is AgentBootstrapHookEvent {
if (event.type !== "agent" || event.action !== "bootstrap") return false;
if (event.type !== "agent" || event.action !== "bootstrap") {
return false;
}
const context = event.context as Partial<AgentBootstrapHookContext> | null;
if (!context || typeof context !== "object") return false;
if (typeof context.workspaceDir !== "string") return false;
if (!context || typeof context !== "object") {
return false;
}
if (typeof context.workspaceDir !== "string") {
return false;
}
return Array.isArray(context.bootstrapFiles);
}

View File

@@ -15,7 +15,9 @@ export type PluginHookLoadResult = {
};
function resolveHookDir(api: OpenClawPluginApi, dir: string): string {
if (path.isAbsolute(dir)) return dir;
if (path.isAbsolute(dir)) {
return dir;
}
return path.resolve(path.dirname(api.source), dir);
}

View File

@@ -44,7 +44,9 @@ export function resolveSoulEvilConfigFromHook(
entry: Record<string, unknown> | undefined,
log?: SoulEvilLog,
): SoulEvilConfig | null {
if (!entry) return null;
if (!entry) {
return null;
}
const file = typeof entry.file === "string" ? entry.file : undefined;
if (entry.file !== undefined && !file) {
log?.warn?.("soul-evil config: file must be a string");
@@ -80,23 +82,33 @@ export function resolveSoulEvilConfigFromHook(
log?.warn?.("soul-evil config: purge must be an object");
}
if (!file && chance === undefined && !purge) return null;
if (!file && chance === undefined && !purge) {
return null;
}
return { file, chance, purge };
}
function clampChance(value?: number): number {
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
if (typeof value !== "number" || !Number.isFinite(value)) {
return 0;
}
return Math.min(1, Math.max(0, value));
}
function parsePurgeAt(raw?: string): number | null {
if (!raw) return null;
if (!raw) {
return null;
}
const trimmed = raw.trim();
const match = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(trimmed);
if (!match) return null;
if (!match) {
return null;
}
const hour = Number.parseInt(match[1] ?? "", 10);
const minute = Number.parseInt(match[2] ?? "", 10);
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
if (!Number.isFinite(hour) || !Number.isFinite(minute)) {
return null;
}
return hour * 60 + minute;
}
@@ -111,9 +123,13 @@ function timeOfDayMsInTimezone(date: Date, timeZone: string): number | null {
}).formatToParts(date);
const map: Record<string, string> = {};
for (const part of parts) {
if (part.type !== "literal") map[part.type] = part.value;
if (part.type !== "literal") {
map[part.type] = part.value;
}
}
if (!map.hour || !map.minute || !map.second) {
return null;
}
if (!map.hour || !map.minute || !map.second) return null;
const hour = Number.parseInt(map.hour, 10);
const minute = Number.parseInt(map.minute, 10);
const second = Number.parseInt(map.second, 10);
@@ -132,9 +148,13 @@ function isWithinDailyPurgeWindow(params: {
now: Date;
timeZone: string;
}): boolean {
if (!params.at || !params.duration) return false;
if (!params.at || !params.duration) {
return false;
}
const startMinutes = parsePurgeAt(params.at);
if (startMinutes === null) return false;
if (startMinutes === null) {
return false;
}
let durationMs: number;
try {
@@ -142,13 +162,19 @@ function isWithinDailyPurgeWindow(params: {
} catch {
return false;
}
if (!Number.isFinite(durationMs) || durationMs <= 0) return false;
if (!Number.isFinite(durationMs) || durationMs <= 0) {
return false;
}
const dayMs = 24 * 60 * 60 * 1000;
if (durationMs >= dayMs) return true;
if (durationMs >= dayMs) {
return true;
}
const nowMs = timeOfDayMsInTimezone(params.now, params.timeZone);
if (nowMs === null) return false;
if (nowMs === null) {
return false;
}
const startMs = startMinutes * 60 * 1000;
const endMs = startMs + durationMs;
@@ -204,7 +230,9 @@ export async function applySoulEvilOverride(params: {
now: params.now,
random: params.random,
});
if (!decision.useEvil) return params.files;
if (!decision.useEvil) {
return params.files;
}
const workspaceDir = resolveUserPath(params.workspaceDir);
const evilPath = path.join(workspaceDir, decision.fileName);
@@ -235,11 +263,15 @@ export async function applySoulEvilOverride(params: {
let replaced = false;
const updated = params.files.map((file) => {
if (file.name !== "SOUL.md") return file;
if (file.name !== "SOUL.md") {
return file;
}
replaced = true;
return { ...file, content: evilContent, missing: false };
});
if (!replaced) return params.files;
if (!replaced) {
return params.files;
}
params.log?.debug?.(
`SOUL_EVIL active (${decision.reason ?? "unknown"}) using ${decision.fileName}`,

View File

@@ -34,7 +34,9 @@ function filterHookEntries(
function readHookPackageManifest(dir: string): HookPackageManifest | null {
const manifestPath = path.join(dir, "package.json");
if (!fs.existsSync(manifestPath)) return null;
if (!fs.existsSync(manifestPath)) {
return null;
}
try {
const raw = fs.readFileSync(manifestPath, "utf-8");
return JSON.parse(raw) as HookPackageManifest;
@@ -45,7 +47,9 @@ function readHookPackageManifest(dir: string): HookPackageManifest | null {
function resolvePackageHooks(manifest: HookPackageManifest): string[] {
const raw = manifest[MANIFEST_KEY]?.hooks;
if (!Array.isArray(raw)) return [];
if (!Array.isArray(raw)) {
return [];
}
return raw.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
}
@@ -56,7 +60,9 @@ function loadHookFromDir(params: {
nameHint?: string;
}): Hook | null {
const hookMdPath = path.join(params.hookDir, "HOOK.md");
if (!fs.existsSync(hookMdPath)) return null;
if (!fs.existsSync(hookMdPath)) {
return null;
}
try {
const content = fs.readFileSync(hookMdPath, "utf-8");
@@ -101,16 +107,22 @@ function loadHookFromDir(params: {
function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?: string }): Hook[] {
const { dir, source, pluginId } = params;
if (!fs.existsSync(dir)) return [];
if (!fs.existsSync(dir)) {
return [];
}
const stat = fs.statSync(dir);
if (!stat.isDirectory()) return [];
if (!stat.isDirectory()) {
return [];
}
const hooks: Hook[] = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (!entry.isDirectory()) {
continue;
}
const hookDir = path.join(dir, entry.name);
const manifest = readHookPackageManifest(hookDir);
@@ -125,7 +137,9 @@ function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?:
pluginId,
nameHint: path.basename(resolvedHookDir),
});
if (hook) hooks.push(hook);
if (hook) {
hooks.push(hook);
}
}
continue;
}
@@ -136,7 +150,9 @@ function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?:
pluginId,
nameHint: entry.name,
});
if (hook) hooks.push(hook);
if (hook) {
hooks.push(hook);
}
}
return hooks;
@@ -214,10 +230,18 @@ function loadHookEntries(
const merged = new Map<string, Hook>();
// Precedence: extra < bundled < managed < workspace (workspace wins)
for (const hook of extraHooks) merged.set(hook.name, hook);
for (const hook of bundledHooks) merged.set(hook.name, hook);
for (const hook of managedHooks) merged.set(hook.name, hook);
for (const hook of workspaceHooks) merged.set(hook.name, hook);
for (const hook of extraHooks) {
merged.set(hook.name, hook);
}
for (const hook of bundledHooks) {
merged.set(hook.name, hook);
}
for (const hook of managedHooks) {
merged.set(hook.name, hook);
}
for (const hook of workspaceHooks) {
merged.set(hook.name, hook);
}
return Array.from(merged.values()).map((hook) => {
let frontmatter: ParsedHookFrontmatter = {};