Extend server setup prompts with deployment mode (local_trusted vs authenticated), exposure (private vs public), bind host, and auth config. Add auth bootstrap-ceo command that creates a one-time invite URL for the initial instance admin. Add deployment-auth-check to doctor diagnostics. Register the new command in the CLI entry point. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
165 lines
4.6 KiB
TypeScript
165 lines
4.6 KiB
TypeScript
import * as p from "@clack/prompts";
|
|
import pc from "picocolors";
|
|
import type { PaperclipConfig } from "../config/schema.js";
|
|
import { readConfig, resolveConfigPath } from "../config/store.js";
|
|
import {
|
|
agentJwtSecretCheck,
|
|
configCheck,
|
|
databaseCheck,
|
|
deploymentAuthCheck,
|
|
llmCheck,
|
|
logCheck,
|
|
portCheck,
|
|
secretsCheck,
|
|
storageCheck,
|
|
type CheckResult,
|
|
} from "../checks/index.js";
|
|
|
|
const STATUS_ICON = {
|
|
pass: pc.green("✓"),
|
|
warn: pc.yellow("!"),
|
|
fail: pc.red("✗"),
|
|
} as const;
|
|
|
|
export async function doctor(opts: {
|
|
config?: string;
|
|
repair?: boolean;
|
|
yes?: boolean;
|
|
}): Promise<{ passed: number; warned: number; failed: number }> {
|
|
p.intro(pc.bgCyan(pc.black(" paperclip doctor ")));
|
|
|
|
const configPath = resolveConfigPath(opts.config);
|
|
const results: CheckResult[] = [];
|
|
|
|
// 1. Config check (must pass before others)
|
|
const cfgResult = configCheck(opts.config);
|
|
results.push(cfgResult);
|
|
printResult(cfgResult);
|
|
|
|
if (cfgResult.status === "fail") {
|
|
return printSummary(results);
|
|
}
|
|
|
|
let config: PaperclipConfig;
|
|
try {
|
|
config = readConfig(opts.config)!;
|
|
} catch (err) {
|
|
const readResult: CheckResult = {
|
|
name: "Config file",
|
|
status: "fail",
|
|
message: `Could not read config: ${err instanceof Error ? err.message : String(err)}`,
|
|
canRepair: false,
|
|
repairHint: "Run `paperclip configure --section database` or `paperclip onboard`",
|
|
};
|
|
results.push(readResult);
|
|
printResult(readResult);
|
|
return printSummary(results);
|
|
}
|
|
|
|
// 2. Deployment/auth mode check
|
|
const deploymentAuthResult = deploymentAuthCheck(config);
|
|
results.push(deploymentAuthResult);
|
|
printResult(deploymentAuthResult);
|
|
|
|
// 3. Agent JWT check
|
|
const jwtResult = agentJwtSecretCheck();
|
|
results.push(jwtResult);
|
|
printResult(jwtResult);
|
|
await maybeRepair(jwtResult, opts);
|
|
|
|
// 4. Secrets adapter check
|
|
const secretsResult = secretsCheck(config, configPath);
|
|
results.push(secretsResult);
|
|
printResult(secretsResult);
|
|
await maybeRepair(secretsResult, opts);
|
|
|
|
// 5. Storage check
|
|
const storageResult = storageCheck(config, configPath);
|
|
results.push(storageResult);
|
|
printResult(storageResult);
|
|
await maybeRepair(storageResult, opts);
|
|
|
|
// 6. Database check
|
|
const dbResult = await databaseCheck(config, configPath);
|
|
results.push(dbResult);
|
|
printResult(dbResult);
|
|
await maybeRepair(dbResult, opts);
|
|
|
|
// 7. LLM check
|
|
const llmResult = await llmCheck(config);
|
|
results.push(llmResult);
|
|
printResult(llmResult);
|
|
|
|
// 8. Log directory check
|
|
const logResult = logCheck(config, configPath);
|
|
results.push(logResult);
|
|
printResult(logResult);
|
|
await maybeRepair(logResult, opts);
|
|
|
|
// 9. Port check
|
|
const portResult = await portCheck(config);
|
|
results.push(portResult);
|
|
printResult(portResult);
|
|
|
|
// Summary
|
|
return printSummary(results);
|
|
}
|
|
|
|
function printResult(result: CheckResult): void {
|
|
const icon = STATUS_ICON[result.status];
|
|
p.log.message(`${icon} ${pc.bold(result.name)}: ${result.message}`);
|
|
if (result.status !== "pass" && result.repairHint) {
|
|
p.log.message(` ${pc.dim(result.repairHint)}`);
|
|
}
|
|
}
|
|
|
|
async function maybeRepair(
|
|
result: CheckResult,
|
|
opts: { repair?: boolean; yes?: boolean },
|
|
): Promise<void> {
|
|
if (result.status === "pass" || !result.canRepair || !result.repair) return;
|
|
if (!opts.repair) return;
|
|
|
|
let shouldRepair = opts.yes;
|
|
if (!shouldRepair) {
|
|
const answer = await p.confirm({
|
|
message: `Repair "${result.name}"?`,
|
|
initialValue: true,
|
|
});
|
|
if (p.isCancel(answer)) return;
|
|
shouldRepair = answer;
|
|
}
|
|
|
|
if (shouldRepair) {
|
|
try {
|
|
await result.repair();
|
|
p.log.success(`Repaired: ${result.name}`);
|
|
} catch (err) {
|
|
p.log.error(`Repair failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function printSummary(results: CheckResult[]): { passed: number; warned: number; failed: number } {
|
|
const passed = results.filter((r) => r.status === "pass").length;
|
|
const warned = results.filter((r) => r.status === "warn").length;
|
|
const failed = results.filter((r) => r.status === "fail").length;
|
|
|
|
const parts: string[] = [];
|
|
parts.push(pc.green(`${passed} passed`));
|
|
if (warned) parts.push(pc.yellow(`${warned} warnings`));
|
|
if (failed) parts.push(pc.red(`${failed} failed`));
|
|
|
|
p.note(parts.join(", "), "Summary");
|
|
|
|
if (failed > 0) {
|
|
p.outro(pc.red("Some checks failed. Fix the issues above and re-run doctor."));
|
|
} else if (warned > 0) {
|
|
p.outro(pc.yellow("All critical checks passed with some warnings."));
|
|
} else {
|
|
p.outro(pc.green("All checks passed!"));
|
|
}
|
|
|
|
return { passed, warned, failed };
|
|
}
|