mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(cli): make doctor --json imply read-only lint mode (#122662)
* fix(cli): make doctor json imply lint * fix(cli): preserve doctor session selector errors
This commit is contained in:
committed by
GitHub
parent
44335ac7ce
commit
6093e3477d
+7
-5
@@ -32,17 +32,18 @@ Doctor has five postures:
|
||||
| ------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| Inspect | `openclaw doctor` | Human-oriented checks and guided prompts. |
|
||||
| Repair | `openclaw doctor --fix` | Applies supported repairs, using prompts unless non-interactive repair is safe. |
|
||||
| Lint | `openclaw doctor --lint` | Read-only structured findings for CI, preflight, and review gates. |
|
||||
| Lint | `openclaw doctor --json` | Read-only JSON findings for deployment preflight and CI gates. |
|
||||
| Shared SQLite maintenance | `openclaw doctor --state-sqlite compact` | Explicitly checkpoints, compacts, and verifies the canonical shared state DB. |
|
||||
| Session SQLite migration | `openclaw doctor --session-sqlite <mode>` | Inspects, imports, validates, compacts, recovers, or restores session state. |
|
||||
|
||||
Prefer `--lint` when automation needs a stable result. Prefer `--fix` when a human operator wants doctor to edit config or state.
|
||||
Use `openclaw doctor --json` as the machine-readable deployment preflight. It runs the same read-only checks, JSON output, and exit codes as `openclaw doctor --lint --json`. Prefer `--fix` when a human operator wants doctor to edit config or state.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
openclaw doctor --lint
|
||||
openclaw doctor --json
|
||||
openclaw doctor --lint --json
|
||||
openclaw doctor --lint --severity-min warning
|
||||
openclaw doctor --lint --all
|
||||
@@ -93,19 +94,20 @@ openclaw channels status --probe
|
||||
| `--session-sqlite-agent <id>` | With `--session-sqlite`: select one configured agent. |
|
||||
| `--session-sqlite-all-agents` | With `--session-sqlite`: select configured and discovered agent stores. |
|
||||
| `--github-issue` | With `--session-sqlite recover`: prepare a sanitized openclaw/openclaw issue report; doctor creates it with `gh` after `--yes` or interactive confirmation. |
|
||||
| `--json` | With `--lint`: JSON findings. With `--post-upgrade`: `{ probesRun, findings }`. With `--state-sqlite` or `--session-sqlite`: the maintenance report as JSON. |
|
||||
| `--json` | Run lint checks in read-only mode and emit JSON. With another machine mode, emit that mode's existing JSON report. |
|
||||
| `--severity-min <level>` | With `--lint`: drop findings below `info`, `warning`, or `error`. |
|
||||
| `--all` | With `--lint`: run all registered checks, including opt-in checks excluded from the default set. |
|
||||
| `--skip <id>` | With `--lint`: skip a check id. Repeatable. |
|
||||
| `--only <id>` | With `--lint`: run only the given check id(s). Repeatable. |
|
||||
|
||||
`--severity-min`, `--all`, `--only`, and `--skip` are only accepted together with `--lint`; `--json` is accepted with `--lint`, `--post-upgrade`, `--state-sqlite`, and `--session-sqlite`.
|
||||
`--severity-min`, `--all`, `--only`, and `--skip` are only accepted together with `--lint`. Bare `--json` implies lint mode. It cannot be combined with `--repair`, `--fix`, or `--force` unless another machine mode owns the command.
|
||||
|
||||
## Lint mode
|
||||
|
||||
`openclaw doctor --lint` is read-only: no prompts, no repair, no config/state rewrites.
|
||||
`openclaw doctor --json` is the deployment-preflight form of lint mode. It is read-only and non-interactive: no prompts, repairs, or config/state rewrites. `openclaw doctor --lint --json` remains an equivalent explicit spelling.
|
||||
|
||||
```bash
|
||||
openclaw doctor --json
|
||||
openclaw doctor --lint
|
||||
openclaw doctor --lint --severity-min warning
|
||||
openclaw doctor --lint --json
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { MachineOutputResolverParams } from "./machine-output-argv.js";
|
||||
import { hasMachineOutputOption } from "./machine-output-argv.js";
|
||||
|
||||
/** Doctor lint follows Unix convention and emits JSON when stdout is not a terminal. */
|
||||
/** Bare doctor JSON and non-TTY lint runs own machine-readable stdout. */
|
||||
export function isDoctorMachineOutput(params: MachineOutputResolverParams): boolean {
|
||||
return (
|
||||
hasMachineOutputOption(params.argv, "--lint") &&
|
||||
(hasMachineOutputOption(params.argv, "--json") || !params.stdoutIsTTY)
|
||||
);
|
||||
const lint = hasMachineOutputOption(params.argv, "--lint");
|
||||
if (lint) {
|
||||
return hasMachineOutputOption(params.argv, "--json") || !params.stdoutIsTTY;
|
||||
}
|
||||
const existingMachineMode =
|
||||
hasMachineOutputOption(params.argv, "--post-upgrade") ||
|
||||
hasMachineOutputOption(params.argv, "--state-sqlite") ||
|
||||
hasMachineOutputOption(params.argv, "--session-sqlite");
|
||||
return hasMachineOutputOption(params.argv, "--json") && !existingMachineMode;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,23 @@ describe("built-in machine-output resolvers", () => {
|
||||
expect(isDoctorMachineOutput({ argv, stdoutIsTTY: true })).toBe(false);
|
||||
});
|
||||
|
||||
it("reserves doctor JSON output with or without explicit lint mode", () => {
|
||||
for (const argv of [
|
||||
["node", "openclaw", "doctor", "--json"],
|
||||
["node", "openclaw", "doctor", "--lint", "--json"],
|
||||
]) {
|
||||
expect(isDoctorMachineOutput({ argv, stdoutIsTTY: true })).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["--post-upgrade", "--state-sqlite=compact", "--session-sqlite=dry-run"])(
|
||||
"preserves registered-command JSON handling for doctor %s",
|
||||
(mode) => {
|
||||
const argv = ["node", "openclaw", "doctor", mode, "--json"];
|
||||
expect(isDoctorMachineOutput({ argv, stdoutIsTTY: true })).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["blob", "coverage", "purge", "query", "sessions"])(
|
||||
"detects proxy %s output",
|
||||
(command) => {
|
||||
|
||||
@@ -231,10 +231,14 @@ describe("registerMaintenanceCommands doctor action", () => {
|
||||
expect(runtime.exit).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it("rejects session sqlite selectors without session sqlite mode", async () => {
|
||||
await runMaintenanceCli(["doctor", "--session-sqlite-agent", "main"]);
|
||||
it.each([
|
||||
["without JSON", ["--session-sqlite-agent", "main"]],
|
||||
["with JSON", ["--json", "--session-sqlite-agent", "main"]],
|
||||
])("rejects session sqlite selectors without session sqlite mode %s", async (_label, args) => {
|
||||
await runMaintenanceCli(["doctor", ...args]);
|
||||
|
||||
expect(doctorCommand).not.toHaveBeenCalled();
|
||||
expect(runDoctorLintCli).not.toHaveBeenCalled();
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"doctor session SQLite options require --session-sqlite. Use `openclaw doctor --session-sqlite dry-run ...`.",
|
||||
);
|
||||
@@ -271,6 +275,54 @@ describe("registerMaintenanceCommands doctor action", () => {
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("treats bare --json as lint mode and emits machine-readable output", async () => {
|
||||
const output: string[] = [];
|
||||
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
runDoctorLintCli.mockImplementationOnce(async () => {
|
||||
process.stdout.write('{"ok":true,"checksRun":1,"checksSkipped":0,"findings":[]}\n');
|
||||
return 0;
|
||||
});
|
||||
|
||||
try {
|
||||
await runMaintenanceCli(["doctor", "--json"]);
|
||||
|
||||
expect(doctorCommand).not.toHaveBeenCalled();
|
||||
expect(runDoctorLintCli).toHaveBeenCalledWith(runtime, {
|
||||
json: true,
|
||||
severityMin: undefined,
|
||||
includeAllChecks: false,
|
||||
skipIds: [],
|
||||
onlyIds: [],
|
||||
allowExec: false,
|
||||
deep: false,
|
||||
});
|
||||
expect(JSON.parse(output.join(""))).toEqual({
|
||||
ok: true,
|
||||
checksRun: 1,
|
||||
checksSkipped: 0,
|
||||
findings: [],
|
||||
});
|
||||
expect(runtime.exit).toHaveBeenCalledWith(0);
|
||||
} finally {
|
||||
writeSpy.mockRestore();
|
||||
runDoctorLintCli.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects JSON repair mode before running doctor", async () => {
|
||||
await runMaintenanceCli(["doctor", "--json", "--repair"]);
|
||||
|
||||
expect(doctorCommand).not.toHaveBeenCalled();
|
||||
expect(runDoctorLintCli).not.toHaveBeenCalled();
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"doctor --json runs read-only lint checks and cannot be combined with --repair, --fix, or --force.",
|
||||
);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it("rejects lint selectors outside doctor lint mode", async () => {
|
||||
await runMaintenanceCli(["doctor", "--fix", "--only", "policy/channels-denied-provider"]);
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
)
|
||||
.option(
|
||||
"--json",
|
||||
"With --lint, --post-upgrade, --state-sqlite, or --session-sqlite: emit machine-readable JSON output",
|
||||
"Run read-only lint checks as JSON (or emit JSON for another machine mode)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
@@ -110,7 +110,21 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
defaultRuntime.exit(2);
|
||||
return;
|
||||
}
|
||||
if (opts.lint === true) {
|
||||
const jsonImpliesLint =
|
||||
opts.json === true &&
|
||||
opts.lint !== true &&
|
||||
opts.postUpgrade !== true &&
|
||||
typeof opts.stateSqlite !== "string" &&
|
||||
typeof opts.sessionSqlite !== "string" &&
|
||||
!hasSessionSqliteOnlyDoctorOptions(opts);
|
||||
if (jsonImpliesLint && (opts.repair === true || opts.fix === true || opts.force === true)) {
|
||||
defaultRuntime.error(
|
||||
"doctor --json runs read-only lint checks and cannot be combined with --repair, --fix, or --force.",
|
||||
);
|
||||
defaultRuntime.exit(2);
|
||||
return;
|
||||
}
|
||||
if (opts.lint === true || jsonImpliesLint) {
|
||||
await runCommandWithRuntime(
|
||||
defaultRuntime,
|
||||
async () => {
|
||||
@@ -258,20 +272,12 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
}
|
||||
|
||||
function hasLintOnlyDoctorOptions(opts: {
|
||||
readonly json?: boolean;
|
||||
readonly postUpgrade?: boolean;
|
||||
readonly stateSqlite?: unknown;
|
||||
readonly sessionSqlite?: unknown;
|
||||
readonly severityMin?: unknown;
|
||||
readonly all?: boolean;
|
||||
readonly skip?: unknown;
|
||||
readonly only?: unknown;
|
||||
}): boolean {
|
||||
return (
|
||||
(opts.json === true &&
|
||||
opts.postUpgrade !== true &&
|
||||
typeof opts.stateSqlite !== "string" &&
|
||||
typeof opts.sessionSqlite !== "string") ||
|
||||
typeof opts.severityMin === "string" ||
|
||||
opts.all === true ||
|
||||
(Array.isArray(opts.skip) && opts.skip.length > 0) ||
|
||||
|
||||
Reference in New Issue
Block a user