fix: harden doctor install checks
This commit is contained in:
@@ -21,7 +21,11 @@
|
|||||||
- Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer.
|
- Gateway: allow Tailscale Serve identity headers to satisfy token auth; rebuild Control UI assets when protocol schema is newer.
|
||||||
- Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
|
- Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
|
||||||
|
|
||||||
|
### Installer
|
||||||
|
- Install: run `clawdbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds.
|
||||||
- Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles.
|
- Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles.
|
||||||
- Gemini: normalize Gemini 3 ids to preview variants; strip Gemini CLI tool call/response ids; downgrade missing `thought_signature`; strip Claude `msg_*` thought_signature fields to avoid base64 decode errors.
|
- Gemini: normalize Gemini 3 ids to preview variants; strip Gemini CLI tool call/response ids; downgrade missing `thought_signature`; strip Claude `msg_*` thought_signature fields to avoid base64 decode errors.
|
||||||
- MiniMax: strip malformed tool invocation XML; include `MiniMax-VL-01` in implicit provider for image pairing.
|
- MiniMax: strip malformed tool invocation XML; include `MiniMax-VL-01` in implicit provider for image pairing.
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ cat ~/.clawdbot/clawdbot.json
|
|||||||
- Security warnings for open DM policies.
|
- Security warnings for open DM policies.
|
||||||
- Gateway auth warnings when no `gateway.auth.token` is set (local mode; offers token generation).
|
- Gateway auth warnings when no `gateway.auth.token` is set (local mode; offers token generation).
|
||||||
- systemd linger check on Linux.
|
- systemd linger check on Linux.
|
||||||
|
- Source install checks (pnpm workspace mismatch, missing UI assets, missing tsx binary).
|
||||||
- Writes updated config + wizard metadata.
|
- Writes updated config + wizard metadata.
|
||||||
|
|
||||||
## Detailed behavior and rationale
|
## Detailed behavior and rationale
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ What it does (high level):
|
|||||||
- `git`: clone/build a source checkout and install a wrapper script
|
- `git`: clone/build a source checkout and install a wrapper script
|
||||||
- On Linux: avoid global npm permission errors by switching npm’s prefix to `~/.npm-global` when needed.
|
- On Linux: avoid global npm permission errors by switching npm’s prefix to `~/.npm-global` when needed.
|
||||||
- If upgrading an existing install: runs `clawdbot doctor --non-interactive` (best effort).
|
- If upgrading an existing install: runs `clawdbot doctor --non-interactive` (best effort).
|
||||||
|
- For git installs: runs `clawdbot doctor --non-interactive` after install/update (best effort).
|
||||||
- Mitigates `sharp` native install gotchas by defaulting `SHARP_IGNORE_GLOBAL_LIBVIPS=1` (avoids building against system libvips).
|
- Mitigates `sharp` native install gotchas by defaulting `SHARP_IGNORE_GLOBAL_LIBVIPS=1` (avoids building against system libvips).
|
||||||
|
|
||||||
If you *want* `sharp` to link against a globally-installed libvips (or you’re debugging), set:
|
If you *want* `sharp` to link against a globally-installed libvips (or you’re debugging), set:
|
||||||
|
|||||||
39
src/commands/doctor-install.ts
Normal file
39
src/commands/doctor-install.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { note } from "../terminal/note.js";
|
||||||
|
|
||||||
|
export function noteSourceInstallIssues(root: string | null) {
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
const workspaceMarker = path.join(root, "pnpm-workspace.yaml");
|
||||||
|
if (!fs.existsSync(workspaceMarker)) return;
|
||||||
|
|
||||||
|
const warnings: string[] = [];
|
||||||
|
const nodeModules = path.join(root, "node_modules");
|
||||||
|
const pnpmStore = path.join(nodeModules, ".pnpm");
|
||||||
|
const tsxBin = path.join(nodeModules, ".bin", "tsx");
|
||||||
|
const srcEntry = path.join(root, "src", "entry.ts");
|
||||||
|
|
||||||
|
if (fs.existsSync(nodeModules) && !fs.existsSync(pnpmStore)) {
|
||||||
|
warnings.push(
|
||||||
|
"- node_modules was not installed by pnpm (missing node_modules/.pnpm). Run: pnpm install",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(path.join(root, "package-lock.json"))) {
|
||||||
|
warnings.push(
|
||||||
|
"- package-lock.json present in a pnpm workspace. If you ran npm install, remove it and reinstall with pnpm.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(srcEntry) && !fs.existsSync(tsxBin)) {
|
||||||
|
warnings.push(
|
||||||
|
"- tsx binary is missing for source runs. Run: pnpm install",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
note(warnings.join("\n"), "Install");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,10 +23,52 @@ export async function maybeRepairUiProtocolFreshness(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [schemaStats, uiStats] = await Promise.all([
|
const [schemaStats, uiStats] = await Promise.all([
|
||||||
fs.stat(schemaPath),
|
fs.stat(schemaPath).catch(() => null),
|
||||||
fs.stat(uiIndexPath),
|
fs.stat(uiIndexPath).catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (schemaStats && !uiStats) {
|
||||||
|
note(
|
||||||
|
[
|
||||||
|
"- Control UI assets are missing.",
|
||||||
|
"- Run: pnpm ui:build",
|
||||||
|
].join("\n"),
|
||||||
|
"UI",
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldRepair = await prompter.confirmRepair({
|
||||||
|
message: "Build Control UI assets now?",
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldRepair) {
|
||||||
|
note("Building Control UI assets... (this may take a moment)", "UI");
|
||||||
|
const uiScriptPath = path.join(root, "scripts/ui.js");
|
||||||
|
const buildResult = await runCommandWithTimeout(
|
||||||
|
[process.execPath, uiScriptPath, "build"],
|
||||||
|
{
|
||||||
|
cwd: root,
|
||||||
|
timeoutMs: 120_000,
|
||||||
|
env: { ...process.env, FORCE_COLOR: "1" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (buildResult.code === 0) {
|
||||||
|
note("UI build complete.", "UI");
|
||||||
|
} else {
|
||||||
|
const details = [
|
||||||
|
`UI build failed (exit ${buildResult.code ?? "unknown"}).`,
|
||||||
|
buildResult.stderr.trim() ? buildResult.stderr.trim() : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
note(details, "UI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schemaStats || !uiStats) return;
|
||||||
|
|
||||||
if (schemaStats.mtime > uiStats.mtime) {
|
if (schemaStats.mtime > uiStats.mtime) {
|
||||||
const uiMtimeIso = uiStats.mtime.toISOString();
|
const uiMtimeIso = uiStats.mtime.toISOString();
|
||||||
// Find changes since the UI build
|
// Find changes since the UI build
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
maybeRepairGatewayServiceConfig,
|
maybeRepairGatewayServiceConfig,
|
||||||
maybeScanExtraGatewayServices,
|
maybeScanExtraGatewayServices,
|
||||||
} from "./doctor-gateway-services.js";
|
} from "./doctor-gateway-services.js";
|
||||||
|
import { noteSourceInstallIssues } from "./doctor-install.js";
|
||||||
import {
|
import {
|
||||||
maybeMigrateLegacyConfigFile,
|
maybeMigrateLegacyConfigFile,
|
||||||
normalizeLegacyConfigValues,
|
normalizeLegacyConfigValues,
|
||||||
@@ -187,6 +188,12 @@ export async function doctorCommand(
|
|||||||
printWizardHeader(runtime);
|
printWizardHeader(runtime);
|
||||||
intro("Clawdbot doctor");
|
intro("Clawdbot doctor");
|
||||||
|
|
||||||
|
const root = await resolveClawdbotPackageRoot({
|
||||||
|
moduleUrl: import.meta.url,
|
||||||
|
argv1: process.argv[1],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
const updateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS === "1";
|
const updateInProgress = process.env.CLAWDBOT_UPDATE_IN_PROGRESS === "1";
|
||||||
const canOfferUpdate =
|
const canOfferUpdate =
|
||||||
!updateInProgress &&
|
!updateInProgress &&
|
||||||
@@ -195,11 +202,6 @@ export async function doctorCommand(
|
|||||||
options.repair !== true &&
|
options.repair !== true &&
|
||||||
Boolean(process.stdin.isTTY);
|
Boolean(process.stdin.isTTY);
|
||||||
if (canOfferUpdate) {
|
if (canOfferUpdate) {
|
||||||
const root = await resolveClawdbotPackageRoot({
|
|
||||||
moduleUrl: import.meta.url,
|
|
||||||
argv1: process.argv[1],
|
|
||||||
cwd: process.cwd(),
|
|
||||||
});
|
|
||||||
if (root) {
|
if (root) {
|
||||||
const git = await detectClawdbotGitCheckout(root);
|
const git = await detectClawdbotGitCheckout(root);
|
||||||
if (git === "git") {
|
if (git === "git") {
|
||||||
@@ -250,6 +252,7 @@ export async function doctorCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await maybeRepairUiProtocolFreshness(runtime, prompter);
|
await maybeRepairUiProtocolFreshness(runtime, prompter);
|
||||||
|
noteSourceInstallIssues(root);
|
||||||
|
|
||||||
await maybeMigrateLegacyConfigFile(runtime);
|
await maybeMigrateLegacyConfigFile(runtime);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user