From c10b764c0aa85fe177cf64425fa83a96a66dd5b4 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 10 May 2026 13:42:57 +0000 Subject: [PATCH] feat(config): add opencode config fix --- changelog.md | 3 + readme.md | 3 + ts/mod_config/index.ts | 145 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 146 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index 0d9e8e4..841b504 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,9 @@ ## Pending +### Features + +- Add `gitzone config fix` to invoke opencode for configuration repair. ## 2026-05-10 - 2.17.0 diff --git a/readme.md b/readme.md index 07afe21..4194a25 100644 --- a/readme.md +++ b/readme.md @@ -266,6 +266,9 @@ gitzone config release # Validate schema, legacy keys, release targets, registries, and npm auth gitzone config doctor +# Use opencode to repair configuration issues found by doctor +gitzone config fix + # Read the npm release target registries gitzone config get release.targets.npm.registries diff --git a/ts/mod_config/index.ts b/ts/mod_config/index.ts index 8121982..91f8027 100644 --- a/ts/mod_config/index.ts +++ b/ts/mod_config/index.ts @@ -117,6 +117,9 @@ export const run = async (argvArg: any) => { case "doctor": await handleDoctor(mode); break; + case "fix": + await handleFix(argvArg, mode); + break; case "migrate": await handleMigrate(value, mode); break; @@ -165,6 +168,7 @@ async function handleInteractiveMenu(): Promise { { name: "Configure release workflow", value: "release" }, { name: "Configure services", value: "services" }, { name: "Validate configuration (doctor)", value: "doctor" }, + { name: "Fix configuration with opencode", value: "fix" }, { name: "Add an npm target registry", value: "add" }, { name: "Remove an npm target registry", value: "remove" }, { name: "Clear npm target registries", value: "clear" }, @@ -213,6 +217,9 @@ async function handleInteractiveMenu(): Promise { case "doctor": await handleDoctor(defaultCliMode); break; + case "fix": + await handleFix({ _: ["config", "fix"] }, defaultCliMode); + break; case "help": showHelp(); break; @@ -890,6 +897,86 @@ async function handleRelease(mode: ICliMode): Promise { } async function handleDoctor(mode: ICliMode): Promise { + const findings = await collectDoctorFindings(); + printDoctorResult(findings, mode); +} + +async function handleFix(argvArg: any, mode: ICliMode): Promise { + if (mode.json) { + printJson({ + ok: false, + error: "JSON output is not supported for `gitzone config fix`. Use `gitzone config doctor --json` for machine-readable diagnostics.", + }); + process.exitCode = 1; + return; + } + + const findings = await collectDoctorFindings(); + const counts = countDoctorFindings(findings); + const extraInstructions = (argvArg._?.slice(2).join(" ") || "").trim(); + const force = Boolean(argvArg.force); + + if (counts.error === 0 && counts.warn === 0 && !extraInstructions && !force) { + plugins.logger.log( + "success", + "Configuration doctor found no issues. Use `gitzone config fix --force` to run opencode anyway.", + ); + return; + } + + if (!mode.yes) { + if (!mode.interactive) { + throw new Error("Config fix requires an interactive terminal or `-y` to run opencode non-interactively."); + } + const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation( + `Run opencode to fix .smartconfig.json? (${counts.error} error, ${counts.warn} warning)`, + true, + ); + if (!confirmed) { + plugins.logger.log("info", "Config fix cancelled."); + return; + } + } + + const opencodeArgs = [ + "run", + "--title", + "gitzone config fix", + "--dir", + process.cwd(), + ]; + if (mode.yes) { + opencodeArgs.push("--dangerously-skip-permissions"); + } + opencodeArgs.push(buildConfigFixPrompt(findings, extraInstructions)); + + plugins.logger.log("info", "Starting opencode configuration fix..."); + const smartshellInstance = new plugins.smartshell.Smartshell({ + executor: "bash", + sourceFilePaths: [], + }); + + let result: plugins.smartshell.IExecResult; + try { + result = await smartshellInstance.execSpawn("opencode", opencodeArgs, { + passthrough: true, + }); + } catch (error) { + throw new Error(`Failed to run opencode: ${error instanceof Error ? error.message : String(error)}`); + } + + if (result.exitCode !== 0) { + plugins.logger.log("error", `opencode exited with code ${result.exitCode}`); + process.exitCode = result.exitCode || 1; + return; + } + + await formatSmartconfigWithDiff(mode); + const finalFindings = await collectDoctorFindings(); + printDoctorResult(finalFindings, mode); +} + +async function collectDoctorFindings(): Promise { const findings: IDoctorFinding[] = []; const smartconfigPath = getSmartconfigPath(); const smartconfigExists = await plugins.smartfs.file(smartconfigPath).exists(); @@ -900,7 +987,7 @@ async function handleDoctor(mode: ICliMode): Promise { message: ".smartconfig.json does not exist", fix: "Run `gitzone config project` to create project basics.", }); - return printDoctorResult(findings, mode); + return findings; } let smartconfigData: Record; @@ -912,7 +999,7 @@ async function handleDoctor(mode: ICliMode): Promise { message: ".smartconfig.json is not valid JSON", fix: error instanceof Error ? error.message : String(error), }); - return printDoctorResult(findings, mode); + return findings; } const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {}; @@ -958,7 +1045,7 @@ async function handleDoctor(mode: ICliMode): Promise { validateCommitConfig(cliConfig.commit || {}, findings); await validateReleaseConfig(cliConfig.release || {}, findings); - printDoctorResult(findings, mode); + return findings; } /** @@ -1274,14 +1361,56 @@ function getDefaultEnabledTargets(currentTargets: Record): string[] return enabledTargets; } -function printDoctorResult(findings: IDoctorFinding[], mode: ICliMode): void { - const counts = findings.reduce( +function countDoctorFindings( + findings: IDoctorFinding[], +): Record { + return findings.reduce( (accumulator, finding) => { accumulator[finding.level] += 1; return accumulator; }, { ok: 0, warn: 0, error: 0 } as Record, ); +} + +function buildConfigFixPrompt( + findings: IDoctorFinding[], + extraInstructions: string, +): string { + const promptParts = [ + "Other /c-* commands can be found at ~/.config/opencode/commands/*", + "# gitzone config fix", + "", + `Working directory: ${process.cwd()}`, + "", + "Repair the project configuration so `gitzone config doctor --json` passes.", + "", + "Rules:", + "- Read `.smartconfig.json`, `package.json`, and nearby project metadata before editing.", + "- Keep gitzone CLI config under `@git.zone/cli` in `.smartconfig.json`.", + `- Use schemaVersion ${CURRENT_GITZONE_CLI_SCHEMA_VERSION} for ` + + "`@git.zone/cli`.", + "- Use target-based release config: `release.targets.git`, `release.targets.npm`, and `release.targets.docker`.", + "- Keep npm registries only at `@git.zone/cli.release.targets.npm.registries`.", + "- Do not add runtime legacy compatibility code. If legacy config exists, migrate it explicitly.", + "- Do not commit, release, install dependencies, or modify unrelated files.", + "- Use pnpm commands only if commands are needed.", + "- Run `gitzone config doctor --json` after changes and keep fixing until no errors remain.", + "- Run `git diff --check` after changes to catch whitespace problems.", + "", + "Current doctor findings:", + JSON.stringify(findings, null, 2), + ]; + + if (extraInstructions) { + promptParts.push("", "Additional user instructions:", extraInstructions); + } + + return promptParts.join("\n"); +} + +function printDoctorResult(findings: IDoctorFinding[], mode: ICliMode): void { + const counts = countDoctorFindings(findings); if (mode.json) { printJson({ @@ -1666,6 +1795,7 @@ export function showHelp(mode?: ICliMode): void { { name: "cli", description: "Configure CLI behavior interactively" }, { name: "release", description: "Configure release workflow interactively" }, { name: "doctor", description: "Validate .smartconfig.json" }, + { name: "fix [instructions]", description: "Use opencode to repair .smartconfig.json" }, { name: "get ", description: "Read a single config value" }, { name: "set ", description: "Write a config value" }, { name: "unset ", description: "Delete a config value" }, @@ -1689,6 +1819,8 @@ export function showHelp(mode?: ICliMode): void { "gitzone config show --json", "gitzone config project", "gitzone config doctor --json", + "gitzone config fix", + "gitzone config fix -y", "gitzone config get release.targets.npm.accessLevel", "gitzone config set cli.interactive false", "gitzone config set cli.output json", @@ -1708,6 +1840,7 @@ export function showHelp(mode?: ICliMode): void { console.log(" cli Configure CLI behavior interactively"); console.log(" release Configure release workflow interactively"); console.log(" doctor Validate .smartconfig.json"); + console.log(" fix [instructions] Use opencode to repair .smartconfig.json"); console.log(" get Read a single config value"); console.log(" set Write a config value"); console.log(" unset Delete a config value"); @@ -1730,6 +1863,8 @@ export function showHelp(mode?: ICliMode): void { console.log(" gitzone config cli"); console.log(" gitzone config release"); console.log(" gitzone config doctor --json"); + console.log(" gitzone config fix"); + console.log(" gitzone config fix -y"); console.log(" gitzone config get release.targets.npm.accessLevel"); console.log(" gitzone config set cli.interactive false"); console.log(" gitzone config set cli.output json");