feat(config): add opencode config fix
This commit is contained in:
+140
-5
@@ -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<void> {
|
||||
{ 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<void> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
async function handleDoctor(mode: ICliMode): Promise<void> {
|
||||
const findings = await collectDoctorFindings();
|
||||
printDoctorResult(findings, mode);
|
||||
}
|
||||
|
||||
async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
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<IDoctorFinding[]> {
|
||||
const findings: IDoctorFinding[] = [];
|
||||
const smartconfigPath = getSmartconfigPath();
|
||||
const smartconfigExists = await plugins.smartfs.file(smartconfigPath).exists();
|
||||
@@ -900,7 +987,7 @@ async function handleDoctor(mode: ICliMode): Promise<void> {
|
||||
message: ".smartconfig.json does not exist",
|
||||
fix: "Run `gitzone config project` to create project basics.",
|
||||
});
|
||||
return printDoctorResult(findings, mode);
|
||||
return findings;
|
||||
}
|
||||
|
||||
let smartconfigData: Record<string, any>;
|
||||
@@ -912,7 +999,7 @@ async function handleDoctor(mode: ICliMode): Promise<void> {
|
||||
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<void> {
|
||||
validateCommitConfig(cliConfig.commit || {}, findings);
|
||||
await validateReleaseConfig(cliConfig.release || {}, findings);
|
||||
|
||||
printDoctorResult(findings, mode);
|
||||
return findings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1274,14 +1361,56 @@ function getDefaultEnabledTargets(currentTargets: Record<string, any>): string[]
|
||||
return enabledTargets;
|
||||
}
|
||||
|
||||
function printDoctorResult(findings: IDoctorFinding[], mode: ICliMode): void {
|
||||
const counts = findings.reduce(
|
||||
function countDoctorFindings(
|
||||
findings: IDoctorFinding[],
|
||||
): Record<TDoctorFindingLevel, number> {
|
||||
return findings.reduce(
|
||||
(accumulator, finding) => {
|
||||
accumulator[finding.level] += 1;
|
||||
return accumulator;
|
||||
},
|
||||
{ ok: 0, warn: 0, error: 0 } as Record<TDoctorFindingLevel, number>,
|
||||
);
|
||||
}
|
||||
|
||||
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 <path>", description: "Read a single config value" },
|
||||
{ name: "set <path> <value>", description: "Write a config value" },
|
||||
{ name: "unset <path>", 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 <path> Read a single config value");
|
||||
console.log(" set <path> <value> Write a config value");
|
||||
console.log(" unset <path> 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");
|
||||
|
||||
Reference in New Issue
Block a user