mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 06:57:48 +00:00
* feat(doctor): add --lint mode + structured HealthFinding shape
Adds the core machinery for `openclaw doctor --lint` per the
doctor-lint-and-oc-rules upstream proposal. PR-1 of the proposal:
no new top-level verb, no public plugin SDK; everything internal.
Files:
- src/flows/checks.ts ? HealthFinding / HealthCheck / HealthCheckContext
types. Findings carry severity per-finding; checks return
readonly HealthFinding[]. Mode tag (doctor/lint/fix) lets a check
distinguish the calling posture.
- src/flows/health-check-registry.ts ? module-level registry with
duplicate-id rejection + test reset helper.
- src/flows/doctor-lint-flow.ts ? runner over registered checks.
Catches throws into synthetic error findings (anchored at check id;
message scrubbed of control chars, capped at 256 bytes). Sorts
findings by severity desc, check id, path. Exports
exitCodeFromFindings (1 if any warning/error, 0 otherwise).
- src/flows/doctor-core-checks.ts ? 4 modern HealthChecks rewriting
logic from existing legacy run*Health functions:
core/doctor/gateway-config (warning)
core/doctor/command-owner (info)
core/doctor/workspace-status (info)
core/doctor/final-config-validation (error)
Each was audited safe per the proposal's adapter constraints
(no writes, no repair calls, no prompts, no probes incl. local-bind).
Legacy run*Health contributions in doctor-health-contributions.ts
are unchanged ? doctor mode (no --lint) still runs the existing 35.
- src/commands/doctor-lint.ts ? CLI dispatch for --lint. Reads config
snapshot, builds HealthCheckContext (mode: "lint"), runs the registry,
filters by --severity-min, emits human or JSON output, returns exit
code from unfiltered set so --severity-min hides info findings
without changing CI signal.
- src/cli/program/register.maintenance.ts ? adds --lint, --json,
--severity-min, --skip, --only flags to existing doctor command.
--lint branches to runDoctorLintCli; without --lint, doctor runs
unchanged.
LoC: 382 src across 6 files. Tests + doc + oc-path-side rule packs
follow as separate commits on this branch.
* fix: avoid string spread in doctor errors
* chore: refresh plugin SDK API baseline
* docs: clarify doctor lint usage
* feat(doctor): prepare repairs for dry-run reporting
140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
import type { Command } from "commander";
|
|
import { setVerbose } from "../../globals.js";
|
|
import type { LogLevel } from "../../logging/levels.js";
|
|
import { defaultRuntime } from "../../runtime.js";
|
|
import { resolveCliArgvInvocation } from "../argv-invocation.js";
|
|
import { getVerboseFlag, isHelpOrVersionInvocation } from "../argv.js";
|
|
import { resolveCliName } from "../cli-name.js";
|
|
import {
|
|
applyCliExecutionStartupPresentation,
|
|
ensureCliExecutionBootstrap,
|
|
resolveCliExecutionStartupContext,
|
|
} from "../command-execution-startup.js";
|
|
import { shouldBypassConfigGuardForCommandPath } from "../command-startup-policy.js";
|
|
import {
|
|
resolvePluginInstallInvalidConfigPolicy,
|
|
resolvePluginInstallPreactionRequest,
|
|
} from "../plugin-install-config-policy.js";
|
|
import { isCommandJsonOutputMode } from "./json-mode.js";
|
|
import { isParentDefaultHelpAction } from "./parent-default-help.js";
|
|
|
|
function setProcessTitleForCommand(actionCommand: Command) {
|
|
let current: Command = actionCommand;
|
|
while (current.parent && current.parent.parent) {
|
|
current = current.parent;
|
|
}
|
|
const name = current.name();
|
|
const cliName = resolveCliName();
|
|
if (!name || name === cliName) {
|
|
return;
|
|
}
|
|
process.title = `${cliName}-${name}`;
|
|
}
|
|
|
|
function shouldAllowInvalidConfigForAction(actionCommand: Command, commandPath: string[]): boolean {
|
|
return (
|
|
resolvePluginInstallInvalidConfigPolicy(
|
|
resolvePluginInstallPreactionRequest({
|
|
actionCommand,
|
|
commandPath,
|
|
argv: process.argv,
|
|
}),
|
|
) === "allow-plugin-recovery"
|
|
);
|
|
}
|
|
|
|
function getRootCommand(command: Command): Command {
|
|
let current = command;
|
|
while (current.parent) {
|
|
current = current.parent;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
function getCliLogLevel(actionCommand: Command): LogLevel | undefined {
|
|
const root = getRootCommand(actionCommand);
|
|
if (typeof root.getOptionValueSource !== "function") {
|
|
return undefined;
|
|
}
|
|
if (root.getOptionValueSource("logLevel") !== "cli") {
|
|
return undefined;
|
|
}
|
|
const logLevel = root.opts<Record<string, unknown>>().logLevel;
|
|
return typeof logLevel === "string" ? (logLevel as LogLevel) : undefined;
|
|
}
|
|
|
|
function isBareParentDefaultHelpInvocation(actionCommand: Command, argv: string[]): boolean {
|
|
if (!isParentDefaultHelpAction(actionCommand)) {
|
|
return false;
|
|
}
|
|
const { commandPath } = resolveCliArgvInvocation(argv);
|
|
const [primary, extra] = commandPath;
|
|
if (extra !== undefined || !primary) {
|
|
return false;
|
|
}
|
|
return primary === actionCommand.name() || actionCommand.aliases().includes(primary);
|
|
}
|
|
|
|
function isGuidedConfigAction(actionCommand: Command): boolean {
|
|
return actionCommand.name() === "config" && !actionCommand.parent?.parent;
|
|
}
|
|
|
|
function isGuidedConfigCommandPath(commandPath: string[]): boolean {
|
|
const [primary, secondary, extra] = commandPath;
|
|
if (primary !== "config" || extra !== undefined) {
|
|
return false;
|
|
}
|
|
return (
|
|
secondary !== "get" &&
|
|
secondary !== "set" &&
|
|
secondary !== "patch" &&
|
|
secondary !== "unset" &&
|
|
secondary !== "file" &&
|
|
secondary !== "schema" &&
|
|
secondary !== "validate"
|
|
);
|
|
}
|
|
|
|
export function registerPreActionHooks(program: Command, programVersion: string) {
|
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
setProcessTitleForCommand(actionCommand);
|
|
const argv = process.argv;
|
|
if (isHelpOrVersionInvocation(argv) || isBareParentDefaultHelpInvocation(actionCommand, argv)) {
|
|
return;
|
|
}
|
|
const jsonOutputMode = isCommandJsonOutputMode(actionCommand, argv);
|
|
const { commandPath, startupPolicy } = resolveCliExecutionStartupContext({
|
|
argv,
|
|
jsonOutputMode,
|
|
env: process.env,
|
|
});
|
|
await applyCliExecutionStartupPresentation({
|
|
startupPolicy,
|
|
version: programVersion,
|
|
});
|
|
const verbose = getVerboseFlag(argv, { includeDebug: true });
|
|
setVerbose(verbose);
|
|
const cliLogLevel = getCliLogLevel(actionCommand);
|
|
if (cliLogLevel) {
|
|
process.env.OPENCLAW_LOG_LEVEL = cliLogLevel;
|
|
}
|
|
if (!verbose) {
|
|
process.env.NODE_NO_WARNINGS ??= "1";
|
|
}
|
|
if (
|
|
shouldBypassConfigGuardForCommandPath(commandPath) ||
|
|
isGuidedConfigAction(actionCommand) ||
|
|
isGuidedConfigCommandPath(commandPath)
|
|
) {
|
|
return;
|
|
}
|
|
await ensureCliExecutionBootstrap({
|
|
runtime: defaultRuntime,
|
|
commandPath,
|
|
startupPolicy,
|
|
allowInvalid: shouldAllowInvalidConfigForAction(actionCommand, commandPath),
|
|
skipConfigGuard: shouldBypassConfigGuardForCommandPath(commandPath),
|
|
});
|
|
});
|
|
}
|