Compare commits

..

2 Commits

Author SHA1 Message Date
jkunz a3ad48368d v2.18.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-10 13:43:05 +00:00
jkunz c10b764c0a feat(config): add opencode config fix 2026-05-10 13:42:57 +00:00
5 changed files with 151 additions and 7 deletions
+6
View File
@@ -3,6 +3,12 @@
## Pending ## Pending
## 2026-05-10 - 2.18.0
### Features
- Add `gitzone config fix` to invoke opencode for configuration repair.
## 2026-05-10 - 2.17.0 ## 2026-05-10 - 2.17.0
### Features ### Features
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@git.zone/cli", "name": "@git.zone/cli",
"private": false, "private": false,
"version": "2.17.0", "version": "2.18.0",
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.", "description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
+3
View File
@@ -266,6 +266,9 @@ gitzone config release
# Validate schema, legacy keys, release targets, registries, and npm auth # Validate schema, legacy keys, release targets, registries, and npm auth
gitzone config doctor gitzone config doctor
# Use opencode to repair configuration issues found by doctor
gitzone config fix
# Read the npm release target registries # Read the npm release target registries
gitzone config get release.targets.npm.registries gitzone config get release.targets.npm.registries
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/cli', name: '@git.zone/cli',
version: '2.17.0', version: '2.18.0',
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
} }
+140 -5
View File
@@ -117,6 +117,9 @@ export const run = async (argvArg: any) => {
case "doctor": case "doctor":
await handleDoctor(mode); await handleDoctor(mode);
break; break;
case "fix":
await handleFix(argvArg, mode);
break;
case "migrate": case "migrate":
await handleMigrate(value, mode); await handleMigrate(value, mode);
break; break;
@@ -165,6 +168,7 @@ async function handleInteractiveMenu(): Promise<void> {
{ name: "Configure release workflow", value: "release" }, { name: "Configure release workflow", value: "release" },
{ name: "Configure services", value: "services" }, { name: "Configure services", value: "services" },
{ name: "Validate configuration (doctor)", value: "doctor" }, { name: "Validate configuration (doctor)", value: "doctor" },
{ name: "Fix configuration with opencode", value: "fix" },
{ name: "Add an npm target registry", value: "add" }, { name: "Add an npm target registry", value: "add" },
{ name: "Remove an npm target registry", value: "remove" }, { name: "Remove an npm target registry", value: "remove" },
{ name: "Clear npm target registries", value: "clear" }, { name: "Clear npm target registries", value: "clear" },
@@ -213,6 +217,9 @@ async function handleInteractiveMenu(): Promise<void> {
case "doctor": case "doctor":
await handleDoctor(defaultCliMode); await handleDoctor(defaultCliMode);
break; break;
case "fix":
await handleFix({ _: ["config", "fix"] }, defaultCliMode);
break;
case "help": case "help":
showHelp(); showHelp();
break; break;
@@ -890,6 +897,86 @@ async function handleRelease(mode: ICliMode): Promise<void> {
} }
async function handleDoctor(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 findings: IDoctorFinding[] = [];
const smartconfigPath = getSmartconfigPath(); const smartconfigPath = getSmartconfigPath();
const smartconfigExists = await plugins.smartfs.file(smartconfigPath).exists(); 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", message: ".smartconfig.json does not exist",
fix: "Run `gitzone config project` to create project basics.", fix: "Run `gitzone config project` to create project basics.",
}); });
return printDoctorResult(findings, mode); return findings;
} }
let smartconfigData: Record<string, any>; let smartconfigData: Record<string, any>;
@@ -912,7 +999,7 @@ async function handleDoctor(mode: ICliMode): Promise<void> {
message: ".smartconfig.json is not valid JSON", message: ".smartconfig.json is not valid JSON",
fix: error instanceof Error ? error.message : String(error), fix: error instanceof Error ? error.message : String(error),
}); });
return printDoctorResult(findings, mode); return findings;
} }
const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {}; const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
@@ -958,7 +1045,7 @@ async function handleDoctor(mode: ICliMode): Promise<void> {
validateCommitConfig(cliConfig.commit || {}, findings); validateCommitConfig(cliConfig.commit || {}, findings);
await validateReleaseConfig(cliConfig.release || {}, findings); await validateReleaseConfig(cliConfig.release || {}, findings);
printDoctorResult(findings, mode); return findings;
} }
/** /**
@@ -1274,14 +1361,56 @@ function getDefaultEnabledTargets(currentTargets: Record<string, any>): string[]
return enabledTargets; return enabledTargets;
} }
function printDoctorResult(findings: IDoctorFinding[], mode: ICliMode): void { function countDoctorFindings(
const counts = findings.reduce( findings: IDoctorFinding[],
): Record<TDoctorFindingLevel, number> {
return findings.reduce(
(accumulator, finding) => { (accumulator, finding) => {
accumulator[finding.level] += 1; accumulator[finding.level] += 1;
return accumulator; return accumulator;
}, },
{ ok: 0, warn: 0, error: 0 } as Record<TDoctorFindingLevel, number>, { 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) { if (mode.json) {
printJson({ printJson({
@@ -1666,6 +1795,7 @@ export function showHelp(mode?: ICliMode): void {
{ name: "cli", description: "Configure CLI behavior interactively" }, { name: "cli", description: "Configure CLI behavior interactively" },
{ name: "release", description: "Configure release workflow interactively" }, { name: "release", description: "Configure release workflow interactively" },
{ name: "doctor", description: "Validate .smartconfig.json" }, { 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: "get <path>", description: "Read a single config value" },
{ name: "set <path> <value>", description: "Write a config value" }, { name: "set <path> <value>", description: "Write a config value" },
{ name: "unset <path>", description: "Delete 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 show --json",
"gitzone config project", "gitzone config project",
"gitzone config doctor --json", "gitzone config doctor --json",
"gitzone config fix",
"gitzone config fix -y",
"gitzone config get release.targets.npm.accessLevel", "gitzone config get release.targets.npm.accessLevel",
"gitzone config set cli.interactive false", "gitzone config set cli.interactive false",
"gitzone config set cli.output json", "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(" cli Configure CLI behavior interactively");
console.log(" release Configure release workflow interactively"); console.log(" release Configure release workflow interactively");
console.log(" doctor Validate .smartconfig.json"); 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(" get <path> Read a single config value");
console.log(" set <path> <value> Write a config value"); console.log(" set <path> <value> Write a config value");
console.log(" unset <path> Delete 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 cli");
console.log(" gitzone config release"); console.log(" gitzone config release");
console.log(" gitzone config doctor --json"); 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 get release.targets.npm.accessLevel");
console.log(" gitzone config set cli.interactive false"); console.log(" gitzone config set cli.interactive false");
console.log(" gitzone config set cli.output json"); console.log(" gitzone config set cli.output json");