Improve CLI: config store, heartbeat-run, and onboarding
Rework config store with better file handling. Expand heartbeat-run command with richer output and error reporting. Improve configure and onboard commands. Update doctor checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,11 +10,67 @@ export function resolveConfigPath(overridePath?: string): string {
|
||||
return path.resolve(process.cwd(), DEFAULT_CONFIG_PATH);
|
||||
}
|
||||
|
||||
function parseJson(filePath: string): unknown {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateLegacyConfig(raw: unknown): unknown {
|
||||
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
||||
const config = { ...(raw as Record<string, unknown>) };
|
||||
const databaseRaw = config.database;
|
||||
if (typeof databaseRaw !== "object" || databaseRaw === null || Array.isArray(databaseRaw)) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const database = { ...(databaseRaw as Record<string, unknown>) };
|
||||
if (database.mode === "pglite") {
|
||||
database.mode = "embedded-postgres";
|
||||
|
||||
if (typeof database.embeddedPostgresDataDir !== "string" && typeof database.pgliteDataDir === "string") {
|
||||
database.embeddedPostgresDataDir = database.pgliteDataDir;
|
||||
}
|
||||
if (
|
||||
typeof database.embeddedPostgresPort !== "number" &&
|
||||
typeof database.pglitePort === "number" &&
|
||||
Number.isFinite(database.pglitePort)
|
||||
) {
|
||||
database.embeddedPostgresPort = database.pglitePort;
|
||||
}
|
||||
}
|
||||
|
||||
config.database = database;
|
||||
return config;
|
||||
}
|
||||
|
||||
function formatValidationError(err: unknown): string {
|
||||
const issues = (err as { issues?: Array<{ path?: unknown; message?: unknown }> })?.issues;
|
||||
if (Array.isArray(issues) && issues.length > 0) {
|
||||
return issues
|
||||
.map((issue) => {
|
||||
const pathParts = Array.isArray(issue.path) ? issue.path.map(String) : [];
|
||||
const issuePath = pathParts.length > 0 ? pathParts.join(".") : "config";
|
||||
const message = typeof issue.message === "string" ? issue.message : "Invalid value";
|
||||
return `${issuePath}: ${message}`;
|
||||
})
|
||||
.join("; ");
|
||||
}
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
export function readConfig(configPath?: string): PaperclipConfig | null {
|
||||
const filePath = resolveConfigPath(configPath);
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
return paperclipConfigSchema.parse(raw);
|
||||
const raw = parseJson(filePath);
|
||||
const migrated = migrateLegacyConfig(raw);
|
||||
const parsed = paperclipConfigSchema.safeParse(migrated);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid config at ${filePath}: ${formatValidationError(parsed.error)}`);
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export function writeConfig(
|
||||
|
||||
Reference in New Issue
Block a user